diff --git a/.github/workflows/extended_checks.yml b/.github/workflows/extended_checks.yml index 1dc9eacc9e..68aa30d01d 100644 --- a/.github/workflows/extended_checks.yml +++ b/.github/workflows/extended_checks.yml @@ -281,6 +281,14 @@ jobs: - name: "Run tutorial dry-runs" run: cmake --build ./build --config Release --target dry_run_tutorials + - name: "Verify authored-doc code blocks (nightly only)" + # doc-verify compiles every das code block of the authored RST corpus + # (skills/doc_sweep.md). Nightly-only per policy: doc rot is not a + # per-PR gate. Skipped on windows: the checker spawns one daslang per + # page via popen and is exercised on the posix cells. + if: (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && matrix.target != 'windows' + run: $BIN/daslang ./utils/doc-verify/main.das -- --daslang $BIN/daslang + - name: "Build standalone executables" run: | set -eux diff --git a/CLAUDE.md b/CLAUDE.md index 1bc868a3cc..7761e47ccd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -90,6 +90,7 @@ Task-specific instructions are split into skill files under `skills/`. You MUST | `skills/gc_migration.md` | Migrating external/archived code from `smart_ptr` AST patterns to gc_node (in-tree migration is complete) | | `skills/version_update.md` | Bumping the daslang version number | | `skills/doc_archiving.md` | Archiving a completed arc's design/plan/audit docs into `/history` — the archive-vs-stay test, reference-update discipline, area-index notes, the `history/README.md` ledger | +| `skills/doc_sweep.md` | Running the each-release authored-RST doc sweep, adding/editing `.. das-doc:` markers on a doc page, or extending `utils/doc-verify/` — rule 0, the page-literate model, the full marker vocabulary, authoring rules, regen traps | | `skills/jobque_debugging.md` | Channel/LockBox/JobStatus/Feature leaks (`--track-job-status`, `DumpJobQueLeaks`) | | `skills/memory_leak_detection.md` | Any leak report at exit — master index of all six leak-detection mechanisms (gc_node, `--das-profiler-leaks`, `-track-allocations`, smart_ptr tracking, jobque, HandleRegistry) and which to reach for | | `skills/make_pr.md` | Creating a pull request (lint, test, AOT, format checklist) | diff --git a/daslib/enum_trait.das b/daslib/enum_trait.das index 8d516add79..d68a68bad4 100644 --- a/daslib/enum_trait.das +++ b/daslib/enum_trait.das @@ -7,8 +7,9 @@ module enum_trait shared private //! Enumeration reflection traits. //! //! Generates helper functions for enumerations: ``each`` (iterate over all -//! values), ``each_name`` (iterate over all name strings), and -//! ``find_value`` (look up a value by name). +//! values), ``enum_names`` (all name strings), ``to_enum`` (look up a value +//! by name, with or without a default), ``enum_to_table``, ``enum_length``, +//! plus ``string``/``bool`` conversions and ``[string_to_enum]``. require daslib/ast require daslib/ast_boost diff --git a/daslib/jobque_boost.das b/daslib/jobque_boost.das index 145517aa91..9e24223417 100644 --- a/daslib/jobque_boost.das +++ b/daslib/jobque_boost.das @@ -8,10 +8,11 @@ module jobque_boost shared public //! Job queue macro helpers. //! //! Provides ``new_job``, ``new_thread``, ``with_job_que``, -//! ``with_job_status``, ``channel_next``, ``channel_push``, and +//! ``with_job_status``, channel capture helpers +//! (``capture_jobque_channel``/``release_capture_jobque_channel``), and //! other convenience wrappers that capture lambda closures and //! correctly manage context cloning for multi-threaded execution. -//! Extends the built-in ``jobque`` module. +//! Extends the built-in ``jobque`` module, which it re-exports. require jobque public require math diff --git a/daslib/strings_boost.das b/daslib/strings_boost.das index 322696a2de..faf5d3da1d 100644 --- a/daslib/strings_boost.das +++ b/daslib/strings_boost.das @@ -6,9 +6,12 @@ module strings_boost shared public //! Extended string manipulation functions. //! -//! Adds ``wide``, ``split``, ``join``, ``escape``, ``unescape``, -//! ``starts_with``, ``ends_with``, ``replace``, string-builder helpers, -//! and many other utilities on top of the built-in ``strings`` module. +//! Adds ``wide``, ``split``, ``join``, ``contains``, ``count``, +//! ``trim_prefix``/``trim_suffix``, ``pad_left``/``pad_right``, +//! ``levenshtein_distance``, ``replace_multiple``, ``glob_match``, and other +//! utilities on top of the built-in ``strings`` module, which it re-exports +//! (``escape``, ``unescape``, ``starts_with``, ``ends_with``, ``replace`` +//! come from ``strings`` via that re-export). require math require strings public diff --git a/doc/source/reference/language/aliases.rst b/doc/source/reference/language/aliases.rst index 5be4f56b6c..d7c0ec899a 100644 --- a/doc/source/reference/language/aliases.rst +++ b/doc/source/reference/language/aliases.rst @@ -47,6 +47,7 @@ Publicity Type aliases can be ``public`` or ``private``: +.. das-doc: alt .. code-block:: das typedef public Vec3 = float3 // visible to other modules @@ -77,6 +78,7 @@ Instead of writing a ``typedef`` for a tuple, you can use the ``tuple`` keyword This is equivalent to: +.. das-doc: alt .. code-block:: das typedef Vertex = tuple @@ -99,6 +101,7 @@ Variants support a similar shorthand: This is equivalent to: +.. das-doc: alt .. code-block:: das typedef Value = variant @@ -121,6 +124,7 @@ Bitfields also support the shorthand syntax: This is equivalent to: +.. das-doc: alt .. code-block:: das typedef Permissions = bitfield diff --git a/doc/source/reference/language/annotations.rst b/doc/source/reference/language/annotations.rst index 8f0efcd512..d8210c0af1 100644 --- a/doc/source/reference/language/annotations.rst +++ b/doc/source/reference/language/annotations.rst @@ -22,6 +22,7 @@ An annotation is written in square brackets before the declaration it applies to Multiple annotations can be combined with commas: +.. das-doc: alt .. code-block:: das [export, no_aot] @@ -31,6 +32,7 @@ Multiple annotations can be combined with commas: Some annotations accept arguments: +.. das-doc: alt .. code-block:: das [init(tag="db")] @@ -55,6 +57,7 @@ Lifecycle Marks a function as callable from the host application. The host invokes exported functions by name through the context API: + .. das-doc: alt .. code-block:: das [export] @@ -107,15 +110,19 @@ Lifecycle Supports a ``late`` attribute for ordering. ``[run]`` - Marks a function to run at compile time: + Evaluates calls to the function at compile time and folds the result into the program. + The function must be free of side effects and must return a foldable value — a call the + compiler cannot fold is ``error[50500] function did not run at compilation time``: .. code-block:: das [run] - def compile_time_check { - print("compiling...\n") + def table_size(n : int) : int { + return n * n + 1 } + let size = table_size(4) // folded to 17 during compilation + Disabled by the ``disable_run`` option (see :ref:`Options `). (see :ref:`Program Structure ` for the full initialization lifecycle). @@ -185,6 +192,7 @@ Lint Control ``[nodiscard]`` Errors if the return value of the function is discarded: + .. das-doc: expect error[30166] .. code-block:: das [nodiscard] @@ -192,7 +200,7 @@ Lint Control return 42 } - compute() // error: return value discarded + compute() // error[30166]: call to compute result is discarded ``[deprecated]`` Marks a function as deprecated. Produces a compile-time warning when called: @@ -282,9 +290,11 @@ Optimization and AOT .. code-block:: das + require math + [inline] def clamp01(x : float) : float { - return saturate(x); + return saturate(x) } Leaf arguments (constants and variables) substitute textually; pure single-use @@ -356,10 +366,13 @@ Macros ``[macro]`` Defined in ``daslib/ast_boost``. Like ``[_macro]`` but wraps the function body in a - module-ready check. Requires ``require daslib/ast_boost``: + module-ready check. Requires ``require daslib/ast_boost``, and — like every macro + registration — a file that declares a ``module``: + .. das-doc: alt .. code-block:: das + module my_macros require daslib/ast_boost [macro] @@ -422,7 +435,7 @@ Structure and Class Annotations ``[persistent]`` Makes a structure persistent (survives context reset). All fields must be POD unless - ``non_pod=true`` is specified: + ``mixed_heap=true`` is specified: .. code-block:: das @@ -514,9 +527,9 @@ All accept an optional ``name`` argument. If omitted, the class name is used. * - ``[pre_infer_macro]`` - ``AstPassMacro`` - Runs before every (re-)inference pass - * - ``[pre_simulate_macro]`` + * - ``[post_infer_macro]`` - ``AstPassMacro`` - - Runs after inference, before codegen + - Runs once inference is done, before lint, folding, and codegen * - ``[lint_macro]`` - ``AstPassMacro`` - Runs during linting @@ -528,17 +541,19 @@ All accept an optional ``name`` argument. If omitted, the class name is used. - Attaches the macro to every function tagged with ``[tag_function]`` Every pass macro — ``[infer_macro]``, ``[dirty_infer_macro]``, ``[optimization_macro]``, -``[pre_infer_macro]``, ``[pre_simulate_macro]``, ``[lint_macro]``, ``[global_lint_macro]`` — +``[pre_infer_macro]``, ``[post_infer_macro]``, ``[lint_macro]``, ``[global_lint_macro]`` — derives from the single ``AstPassMacro`` base class and overrides ``apply(prog : ProgramPtr; mod : Module?) : bool``. There is no per-pass base class. ``[tag_function_macro]`` additionally requires a ``tag`` argument; unlike the other annotations here, that one is not optional. -Example: +Example — macro registration runs during module compilation, so the file declares a ``module``: +.. das-doc: alt .. code-block:: das + module my_macros require daslib/ast_boost [function_macro(name="my_decorator")] @@ -592,8 +607,10 @@ Requires ``require daslib/contracts``. - Argument must be a function type * - ``[expect_any_lambda(arg)]`` - Argument must be a lambda - * - ``[expect_ref(arg)]`` - - Argument must be a reference + * - ``[contracts::expect_ref(arg)]`` + - Argument must be a reference. The name collides with the built-in ``[expect_ref]`` + above, so in a file that requires ``daslib/contracts`` this one is written + module-qualified — the bare name is ``error[30600] too many options for annotation`` * - ``[expect_pointer(arg)]`` - Argument must be a pointer * - ``[expect_class(arg)]`` @@ -603,6 +620,7 @@ Requires ``require daslib/contracts``. Example: +.. das-doc: alt .. code-block:: das require daslib/contracts @@ -646,6 +664,9 @@ declaration: value : int } -These ``@`` decorators attach metadata to the field. They are accessible via ``typeinfo`` and -at compile time in macros. +These ``@`` decorators attach metadata to the field. Macros read them from the structure's +field declarations at compile time. At runtime they are reachable through ``rtti`` — the +field's ``VarInfo`` carries ``annotation_argument_count`` and ``get_annotation_argument``, +under ``options rtti``. A bare ``@big`` is a ``bool`` argument set to ``true``; ``@min = 13`` +carries the value. diff --git a/doc/source/reference/language/arrays.rst b/doc/source/reference/language/arrays.rst index b6a03b84db..b1da9d48ae 100644 --- a/doc/source/reference/language/arrays.rst +++ b/doc/source/reference/language/arrays.rst @@ -68,13 +68,13 @@ Arrays can be constructed inline: .. code-block:: das - let arr = fixed_array(1.,2.,3.,4.5) + let arr = fixed_array(1.,2.,3.,4.5) This expands to: .. code-block:: das - let arr : float[4] = fixed_array(1.,2.,3.,4.5) + let arr : float[4] = fixed_array(1.,2.,3.,4.5) Fixed-size arrays can be multi-dimensional. Dimensions read outermost first — ``float[4][4]`` is 4 rows of ``float[4]`` — and indexing peels one level at a time: @@ -115,13 +115,13 @@ Dynamic arrays can also be constructed inline: .. code-block:: das - let arr <- ["one", "two", "three"] + let arr <- ["one", "two", "three"] This is syntactic equivalent to: .. code-block:: das - let arr : array <- array("one","two","three") + let arr : array <- array("one","two","three") Alternative syntax is: @@ -161,7 +161,9 @@ Arrays of tuples can be constructed inline: When array elements can't be copied, use ``push_clone`` to insert a clone of a value, or ``emplace`` to move it in. -``resize`` can potentially create new array elements. Those elements are initialized with 0. +``resize`` can potentially create new array elements. Those elements are initialized with +``default`` — zeros for plain types, and the declared field initializers for structures +that have them. ``reserve`` is there for performance reasons. Generally, array capacity doubles, if exceeded. ``reserve`` allows you to specify the exact known capacity and significantly reduce the overhead of multiple ``push`` operations. @@ -184,21 +186,23 @@ It's possible to iterate over an array via a regular ``for`` loop. Additionally, a collection of unsafe iterators is provided: +.. das-doc: signatures .. code-block:: das - def each ( a : auto(TT)[] ) : iterator - def each ( a : array ) : iterator + [unsafe_outside_of_for] def each ( a : auto(TT)[] ) : iterator + [unsafe_outside_of_for] def each ( a : array ) : iterator The reason both are unsafe operations is that they do not capture the array. Search functions are available for both static and dynamic arrays: +.. das-doc: signatures .. code-block:: das - def find_index ( arr : array implicit; key : TT ) - def find_index ( arr : auto(TT)[] implicit; key : TT ) - def find_index_if ( arr : array implicit; blk : block<(key:TT):bool> ) - def find_index_if ( arr : auto(TT)[] implicit; blk : block<(key:TT):bool> ) + def find_index ( arr : array | #; key : TT ) + def find_index ( arr : auto(TT)[] | #; key : TT ) + def find_index_if ( arr : array | #; blk : block<(key:TT):bool> ) + def find_index_if ( arr : auto(TT)[] | #; blk : block<(key:TT):bool> ) .. seealso:: diff --git a/doc/source/reference/language/ast_matching.rst b/doc/source/reference/language/ast_matching.rst index 693305510d..ed7578122c 100644 --- a/doc/source/reference/language/ast_matching.rst +++ b/doc/source/reference/language/ast_matching.rst @@ -101,10 +101,15 @@ qmatch_function ^^^^^^^^^^^^^^^ ``qmatch_function(func) $(args) : RetType { stmts }`` matches a compiled function's -arguments, return type, and body: +arguments, return type, and body. Reaching the compiled function needs ``options rtti``, +and the matcher clones AST nodes, so the body runs inside ``ast_gc_guard``: +.. das-doc: alt .. code-block:: das + options rtti + require dastest/testing_boost public + [export] def target_add(a, b : int) : int { return a + b @@ -112,11 +117,13 @@ arguments, return type, and body: [test] def test_add(t : T?) { - var func = find_module_function_via_rtti(compiling_module(), @@target_add) - let r = qmatch_function(func) $(a : int; b : int) : int { - return a + b + ast_gc_guard() { + var func = find_module_function_via_rtti(compiling_module(), @@target_add) + let r = qmatch_function(func) $(a : int; b : int) : int { + return a + b + } + t |> success(r.matched, "target_add matches the pattern") } - assert(r.matched) } The function has been through compilation and optimization, so the AST may differ from the source. @@ -166,6 +173,7 @@ Also works on ``let`` declarations and ``for`` loop iterators: .. code-block:: das + var var_name : string let r = qmatch_block(blk) $ { var $i(var_name) = 5 } @@ -211,8 +219,10 @@ Works in both ``qmatch_function`` and ``qmatch_block``. Fixed arguments before ` are matched by name and type as usual; everything after goes into the array. ``$a`` must be the last argument in the pattern. -Capture remaining function arguments: +Capture remaining function arguments (``func`` is a ``FunctionPtr``, as returned by +``find_module_function_via_rtti`` above): +.. das-doc: given var func : FunctionPtr .. code-block:: das var rest : array @@ -375,6 +385,7 @@ and a qualified pattern name still requires an exact match. Result type ----------- +.. das-doc: signatures .. code-block:: das enum QMatchError : int { diff --git a/doc/source/reference/language/bitfields.rst b/doc/source/reference/language/bitfields.rst index 73c1195464..76869b54a0 100644 --- a/doc/source/reference/language/bitfields.rst +++ b/doc/source/reference/language/bitfields.rst @@ -20,7 +20,12 @@ There is a shorthand type alias syntax to define a bitfield: three } - typedef bits123 = bitfield // exactly the same as the declaration above +The shorthand declares exactly the same type alias as the ``typedef`` form: + +.. das-doc: alt +.. code-block:: das + + typedef bits123 = bitfield Bitfield flags can be 8, 16, 32 or 64 bits in size. By default, bitfields are 32 bits. To specify a different size, use ``: bitfield : uintXX`` syntax: diff --git a/doc/source/reference/language/blocks.rst b/doc/source/reference/language/blocks.rst index 1117006588..967bd2f4a4 100644 --- a/doc/source/reference/language/blocks.rst +++ b/doc/source/reference/language/blocks.rst @@ -10,12 +10,14 @@ Blocks offer significant performance advantages over lambdas (see :ref:`Lambda < The block type can be declared with a function-like syntax. The type is written as ``block`` followed by an optional type signature in angle brackets: +.. das-doc: fragment .. code-block:: das block < (arg1:int; arg2:float&) : bool > The ``->`` operator can be used instead of ``:`` for the return type: +.. das-doc: fragment .. code-block:: das block < (arg1:int; arg2:float&) -> bool > // equivalent @@ -68,6 +70,9 @@ lambda placed immediately after a function call is automatically passed as the l argument. This works with named function calls, dot-method calls, and arrow-method calls (see :ref:`Pipe Operators ` for full details): +.. das-doc: given class Receiver { def call_method(b : block) { invoke(b) } def fn(b : block) { invoke(b) } } +.. das-doc: given var res : int +.. das-doc: given var c : Receiver? .. code-block:: das var v1 = 1 @@ -125,16 +130,19 @@ Nested blocks are allowed: Loop control expressions are not allowed to cross block boundaries: +.. das-doc: given def take_any(b : block) { invoke(b) } +.. das-doc: expect error[30125] .. code-block:: das while ( true ) { take_any() { - break // 30125, captured block can't 'break' outside of the block + break // error[30125] captured block can't 'break' outside of the block } } Blocks can have annotations: +.. das-doc: fragment .. code-block:: das def queryOne(dt:float=1.0f) { diff --git a/doc/source/reference/language/builtin_functions.rst b/doc/source/reference/language/builtin_functions.rst index fdee9f930e..20e78a498c 100644 --- a/doc/source/reference/language/builtin_functions.rst +++ b/doc/source/reference/language/builtin_functions.rst @@ -57,6 +57,7 @@ Assertions ``assert`` may be removed in release builds, so the expression ``x`` must have **no side effects** — the compiler will reject it otherwise: + .. das-doc: given var index : int .. code-block:: das assert(index >= 0, "index must be non-negative") @@ -68,6 +69,7 @@ Assertions (it generates ``DAS_VERIFY`` in C++ rather than ``DAS_ASSERT``). Additionally, the expression ``x`` is allowed to have side effects: + .. das-doc: given def initialize_system : bool { return true } .. code-block:: das verify(initialize_system(), "initialization failed") @@ -78,6 +80,7 @@ Assertions ``x`` must be a compile-time constant. ``static_assert`` expressions are removed from the compiled program: + .. das-doc: given struct Foo { a : int } .. code-block:: das static_assert(typeinfo is_pod(type), "Foo must be POD") @@ -104,6 +107,7 @@ Debug Prints the string ``str`` and the value of ``x`` (similar to ``print``), then **returns** ``x``. This makes it suitable for debugging inside expressions: + .. das-doc: given var x, y, z : int .. code-block:: das let mad = debug(x, "x") * debug(y, "y") + debug(z, "z") @@ -117,7 +121,7 @@ Debug print("hello\n") // ok print("{13}\n") // ok, integer is interpolated into the string - // print(13) // error: print expects a string + // print(13) // error[30341] no matching functions or generics: print(int const) --------------------- Panic @@ -158,6 +162,7 @@ Memory & Type Utilities Converts a pointer (raw or smart) to a ``uint64`` integer value representing its address: + .. das-doc: given var some_ptr : int? .. code-block:: das let address = intptr(some_ptr) @@ -167,6 +172,8 @@ Memory & Type Utilities Provides compile-time type information about an expression or a ``type`` argument. Used extensively in generic programming: + .. das-doc: given struct MyStruct { x : int } + .. das-doc: given var myStruct : MyStruct .. code-block:: das typeinfo sizeof(type) // 12 @@ -188,20 +195,25 @@ Resize & Reserve .. das:function:: resize(var arr : array; new_size : int) - Resizes the array to ``new_size`` elements. New elements are zero-initialized. + Resizes the array to ``new_size`` elements. New elements are zero-initialized, and + shrinking finalizes the elements that are dropped. When the element type carries + field initializers, ``resize`` forwards to ``resize_and_init`` so that new elements + get ``default`` rather than zeroes. ``new_size`` may also be ``int64``. -.. das:function:: resize_and_init(var arr : array; new_size : int) +.. das:function:: resize_and_init(var arr : array; new_size : int [; init_value : T]) - Resizes the array and default-initializes all new elements using the element type's - default constructor. + Resizes the array and initializes all new elements — with ``default``, or with + ``init_value`` when one is given. .. das:function:: resize_no_init(var arr : array; new_size : int) - Resizes without initializing new elements. Only valid for POD/raw element types. + Resizes without initializing new elements. Only valid for raw element types + (anything else is a ``can't resize_no_init non-raw array`` contract error). .. das:function:: reserve(var arr : array; capacity : int) Pre-allocates memory for at least ``capacity`` elements without changing the array length. + Also defined for ``table``. ^^^^^^^^^^^^^^^^ Push & Emplace @@ -252,6 +264,7 @@ Remove & Erase Removes all elements for which ``blk`` returns ``true``: + .. das-doc: given var arr : array .. code-block:: das erase_if(arr) $(x) { return x < 0 } @@ -262,7 +275,8 @@ Remove & Erase .. das:function:: pop(var arr : array) - Removes the last element of the array. + Removes the last element of the array. Panics on an empty array + (``resizing array to negative size``). ^^^^^^^^^^^^^^^^ Access & Search @@ -317,7 +331,7 @@ Sorting .. code-block:: das - sort(arr) $(a, b) { return a > b } // descending order + sort(arr) $(x, y) { return x > y } // descending order ^^^^^^^^^^^^^^^^ Swap @@ -344,6 +358,7 @@ Lookup Looks up ``key`` in the table. If found, the table is locked and ``blk`` is invoked with a reference to the value. Returns ``true`` if the key was found: + .. das-doc: given var tab : table .. code-block:: das get(tab, "key") $(value) { @@ -424,6 +439,7 @@ Iterator Operations Creates an iterator from a range, array, fixed-size array, string, or lambda: + .. das-doc: given var my_range : range .. code-block:: das for (x in each(my_range)) { @@ -437,7 +453,8 @@ Iterator Operations Creates an iterator over all values of an enumeration type. - .. note:: Deprecated — use the built-in enumeration iteration instead. + .. note:: Deprecated — use regular iteration, or ``each`` from ``daslib/enum_trait`` + (see :ref:`Enumerations `). .. das:function:: next(var it : iterator; var value : T&) : bool @@ -512,6 +529,7 @@ Lock Operations then unlocks. While locked, the container cannot be resized or modified structurally: + .. das-doc: given var my_table : table .. code-block:: das lock(my_table) $(t) { @@ -565,11 +583,13 @@ Memory Mapping .. das:function:: map_to_array(data : void?; len : int; blk) - Maps raw memory to a temporary mutable array view. This is an **unsafe** operation. + Maps raw memory to a temporary mutable array view. ``len`` is the size in **bytes**; + the element count is ``len / sizeof(T)``. This is an **unsafe** operation. .. das:function:: map_to_ro_array(data : void?; len : int; blk) - Maps raw memory to a temporary read-only array view. This is an **unsafe** operation. + Maps raw memory to a temporary read-only array view. ``len`` is the size in **bytes**. + This is an **unsafe** operation. --------------------- Vector Construction diff --git a/doc/source/reference/language/classes.rst b/doc/source/reference/language/classes.rst index 153647d934..5197601003 100644 --- a/doc/source/reference/language/classes.rst +++ b/doc/source/reference/language/classes.rst @@ -84,18 +84,17 @@ Finalizers can be defined explicitly as void functions named ``finalize``: class Foo { ... def finalize { // custom finalizer - delFoo ++ + print("Foo finalized\n") } } -An alternative syntax for finalizers is: +An alternative syntax for finalizers is ``def operator delete``: .. code-block:: das - class Foo { - ... + class Bar { def operator delete { // custom finalizer - delFoo ++ + print("Bar finalized\n") } } @@ -123,6 +122,7 @@ Calling Parent Methods Inside a derived class, ``super()`` calls the parent class constructor: +.. das-doc: given class Base { z : int = 0; def Base { } def process(x : int) { } def operator delete { } } .. code-block:: das class Derived : Base { @@ -151,19 +151,20 @@ up the inheritance chain to the nearest ancestor that does: .. code-block:: das - class Base { + class Root { + def Root { /* ... */ } def process(x : int) { /* ... */ } } - class Mid : Base { // empty intermediate + class Mid : Root { // empty intermediate } class Leaf : Mid { def Leaf { - super() // resolves to Base`Base(self) — Mid is skipped + super() // resolves to Root`Root(self) — Mid is skipped } def override process(x : int) { - super.process(x) // resolves to Base`process(self, x) — Mid is skipped + super.process(x) // resolves to Root`process(self, x) — Mid is skipped } } @@ -243,6 +244,7 @@ way classes do. Alternatively, the parent's method can be called directly using the backtick syntax: +.. das-doc: skip .. code-block:: das Foo`set(self, X, Y) // equivalent to super.set(X, Y) from within Foo3D @@ -264,7 +266,7 @@ Sealed functions cannot be overridden. The ``sealed`` keyword is used to prevent class Foo3D : Foo { def sealed set(X,Y:int ) { // subclasses of Foo3D can no longer override this method - xyz = X + Y + x = X + Y } } @@ -272,18 +274,19 @@ Sealed classes can not be inherited from. The ``sealed`` keyword is used to prev .. code-block:: das - class sealed Foo3D : Foo // Foo3D can no longer be inherited from + class sealed Foo3D : Foo { // Foo3D can no longer be inherited from ... + } -A pointer named ``self`` is available inside any class method. +A reference named ``self`` is available inside any class method. Because the method body is wrapped in ``with(self)``, all fields and methods can be accessed directly — ``self.`` is not required: .. code-block:: das class Foo { - x : int - def set(val : int) { + ... + def setX(val : int) { x = val // same as self.x = val } } @@ -299,7 +302,7 @@ Local class variables are unsafe: .. code-block:: das unsafe { - var f = Foo() // unsafe + var localFoo = Foo() // unsafe } Class methods can be invoked using ``.`` syntax: @@ -324,6 +327,8 @@ Class methods can be constant: .. code-block:: das + require math + class Foo { dir : float3 def const length { @@ -335,6 +340,8 @@ Class methods can be operators: .. code-block:: das + require math + class Foo { dir : float3 def Foo ( x,y,z:float ) { @@ -464,6 +471,9 @@ not instantiated directly — they serve as blueprints for code generation via m .. code-block:: das + module tcache + require daslib/typemacro_boost + [template_structure(KeyType, ValueType)] class template TCache { keys : array @@ -480,7 +490,7 @@ not instantiated directly — they serve as blueprints for code generation via m Template parameters (``KeyType``, ``ValueType``) are replaced with concrete types during instantiation. The ``template_structure`` annotation (from ``daslib/typemacro_boost``) handles -the substitution. +the substitution; it runs as a compile-time macro, so the file that uses it must be a module. Template methods automatically inherit the template flag. @@ -702,6 +712,7 @@ Class initializers are generated by adding a local ``self`` variable with ``cons The body of the method is prefixed via a ``with self`` expression. The final expression is a ``return <- self``: +.. das-doc: skip .. code-block:: das def Foo ( X:int const; Y:int const ) : Foo { @@ -715,6 +726,7 @@ The final expression is a ``return <- self``: Class methods and finalizers are generated by providing the extra argument ``self``. The body of the method is prefixed with a ``with self`` expression: +.. das-doc: skip .. code-block:: das def Foo3D`set ( var self:Foo3D; X:int const; Y:int const ) { @@ -726,6 +738,7 @@ The body of the method is prefixed with a ``with self`` expression: Calling virtual methods is implemented via invoke: +.. das-doc: skip .. code-block:: das invoke(f3d.set,cast(f3d),1,2) @@ -733,6 +746,7 @@ Calling virtual methods is implemented via invoke: Every base class gets an ``__rtti`` pointer, and a ``__finalize`` function pointer. Additionally, a function pointer is added for each member function: +.. das-doc: skip .. code-block:: das class Foo { @@ -746,6 +760,7 @@ Additionally, a function pointer is added for each member function: ``__rtti`` contains rtti::TypeInfo for the specific class instance. There is helper function in the rtti module to access class_info safely: +.. das-doc: signatures .. code-block:: das def class_info ( cl ) : StructInfo const? diff --git a/doc/source/reference/language/clone.rst b/doc/source/reference/language/clone.rst index 82532cb29d..1c41552a66 100644 --- a/doc/source/reference/language/clone.rst +++ b/doc/source/reference/language/clone.rst @@ -11,6 +11,7 @@ see :ref:`Move, Copy, and Clone `. Cloning is invoked via the clone operator ``:=``: +.. das-doc: given var a, b, y : array .. code-block:: das a := b @@ -23,6 +24,7 @@ Cloning can be also invoked via the clone initializer in a variable declaration: This in turn expands into ``clone_to_move``: +.. das-doc: alt .. code-block:: das var x <- clone_to_move(y) @@ -92,23 +94,29 @@ Those in turn clone each of the array elements: var c, d : Foo[10] c := d -This expands to: +This expands to (as printed by ``daslang -log``, which shows the instantiated generics): +.. das-doc: skip .. code-block:: das - def builtin`clone ( var a:array explicit; b:array const ) { - resize(a,length(b)) + def builtin`clone ( var a:array explicit; var b:array ==const ) { + let ln = length(b) + resize(a,ln) + return if ( ln == 0 ) for ( aV,bV in a,b ) { aV := bV } } - def builtin`clone_dim ( var a:Foo[10] explicit; b:Foo const[10] implicit explicit ) { + def builtin`clone_dim ( var a:Foo[10] explicit; b:Foo const[10] explicit ) { for ( aV,bV in a,b ) { aV := bV } } +When the element type is POD, both generics substitute a single ``memcpy`` for the +element-by-element loop. + For tables, the ``clone`` generic is called, which in turn clones its values: .. code-block:: das @@ -118,12 +126,14 @@ For tables, the ``clone`` generic is called, which in turn clones its values: This expands to: +.. das-doc: skip .. code-block:: das - def builtin`clone ( var a:table explicit; b:table const ) { + def builtin`clone ( var a:table explicit; var b:table ==const ) { clear(a) for ( k,v in keys(b),values(b) ) { - a[k] := v + let kk := k // the string-key overload clones the key as well + a[kk] := v } } @@ -138,9 +148,10 @@ For structures, the default ``clone`` function is generated, in which each eleme This expands to: +.. das-doc: skip .. code-block:: das - def clone ( var a:Foo explicit; b:Foo const ) { + def clone ( var a:Foo explicit; b:Foo const implicit ) { a.a := b.a a.b = b.b // note copy instead of clone } @@ -154,9 +165,10 @@ For tuples, each individual element is cloned: This expands to: +.. das-doc: skip .. code-block:: das - def clone ( var dest:tuple;string> -const; src:tuple;string> const -const ) { + def clone ( var dest:tuple;string> -const; var src:tuple;string> implicit ==const -const ) { dest._0 = src._0 dest._1 := src._1 dest._2 = src._2 @@ -171,9 +183,10 @@ For variants, only the currently active element is cloned: This expands to: +.. das-doc: skip .. code-block:: das - def clone ( var dest:variant;s:string> -const; src:variant;s:string> const -const ) { + def clone ( var dest:variant;s:string> -const; var src:variant;s:string> implicit ==const -const ) { if ( src is i ) { set_variant_index(dest,0) dest.i = src.i @@ -192,16 +205,23 @@ This expands to: clone_to_move implementation ---------------------------- -``clone_to_move`` is implemented via regular generics as part of the builtin module: +``clone_to_move`` is implemented via regular generics as part of the builtin module. +The ``| #`` in the argument type accepts a temporary source, and ``-#`` on the return +type makes the result a regular (non-temporary) value: +.. das-doc: signatures .. code-block:: das - def clone_to_move(clone_src:auto(TT)) : TT -const { - var clone_dest : TT - clone_dest := clone_src - return <- clone_dest + def clone_to_move(clone_src : auto(TT) ==const | #) : TT -const -# { + unsafe { + var clone_dest : TT -# + clone_dest := clone_src + return <- clone_dest + } } +A second overload with a ``var`` source exists, so that a mutable value can be cloned too. + Note that for non-cloneable types, Daslang will not promote ``:=`` initialize into ``clone_to_move``. .. seealso:: diff --git a/doc/source/reference/language/contexts.rst b/doc/source/reference/language/contexts.rst index fe95eaf816..7142f2a320 100644 --- a/doc/source/reference/language/contexts.rst +++ b/doc/source/reference/language/contexts.rst @@ -44,12 +44,13 @@ It is initialized in the following order: 3. All specifically ordered functions tagged with ``[init]`` are called in the order they appear after topological sort. The topological sort order for the init functions is specified in the init annotation. - * ``tag`` attribute specifies that the function will appear during the specified pass - * ``before`` attribute specifies that the function will appear before the specified pass - * ``after`` attribute specifies that the function will appear after the specified pass + * ``tag`` attribute puts the function into the named pass + * ``before`` attribute names a pass that runs **before** this function, so the function is scheduled after every function of that pass + * ``after`` attribute names a pass that runs **after** this function, so the function is scheduled before every function of that pass Consider the following example: +.. das-doc: given var order : array .. code-block:: das [init(before="middle")] @@ -75,6 +76,7 @@ The functions will execute in the following order: 3. a During shutdown, the context runs all functions marked with ``[finalize]`` in the order they are declared, per module. +Those marked ``[finalize(late=true)]`` run after all the regular ones. Macro contexts -------------- @@ -90,9 +92,13 @@ Shared macro modules are initialized during their first compilation, and are shu Locking ------- -A context contains a ``recursive_mutex`` and can be locked and unlocked with the ``lock_context`` or ``lock_this_context`` RAII blocks. +A context can carry a ``recursive_mutex``, and is locked and unlocked with the ``lock_context`` or ``lock_this_context`` RAII blocks. Cross-context calls via ``invoke_in_context`` automatically lock the target context. +The mutex is only created when the program needs it: when it contains a cross-context call, +when the ``threadlock_context`` option or policy is set, or when the debugger is enabled. +Locking a context that has none reports ``threadlock_context is not set``. + Lookups ------- diff --git a/doc/source/reference/language/datatypes.rst b/doc/source/reference/language/datatypes.rst index c364544b7d..73e1193c43 100644 --- a/doc/source/reference/language/datatypes.rst +++ b/doc/source/reference/language/datatypes.rst @@ -7,6 +7,7 @@ Values and Data Types Daslang is a strong, statically typed language. All variables have a type. Daslang's basic POD (plain old data) data types are: +.. das-doc: skip .. code-block:: das int, uint, float, bool, double, int64, uint64 @@ -17,6 +18,7 @@ All PODs are represented with machine register/word. All PODs are passed to func Daslang's storage types are: +.. das-doc: skip .. code-block:: das int8, uint8, int16, uint16 - 8/16-bits signed and unsigned integers @@ -25,6 +27,7 @@ They have no arithmetic of their own, but can be used as a storage type within s Daslang's 16/8-bit vector lattice extends the storage tier with packed small-element vectors: +.. das-doc: skip .. code-block:: das float16 - IEEE-754 binary16 scalar (`half` is an alias) @@ -73,6 +76,7 @@ two namespaces never mix in one mask. Daslang's other types are: +.. das-doc: skip .. code-block:: das string, das_string, struct, pointers, references, block, lambda, function pointer, @@ -210,6 +214,7 @@ Where promotion applies Promotion is **not** a general "the compiler already knows the target type" rule. It fires in a fixed set of contexts, and nowhere else: +.. das-doc: given variant V { arm : float; other : int } .. code-block:: das var a : float = 1 // local var init @@ -230,28 +235,33 @@ Function-call arguments, function-parameter default values, and ``ExprMove`` (``<-``) are intentionally **not** promoted, even though the target type is plainly visible in each case: +.. das-doc: expect error[30341] .. code-block:: das def take_f(a : float) {} - take_f(1) // error[30341]: no matching functions or generics: - // take_f(int const) + def f(a : float = 1) {} // error[30161]: function argument default value + // type mismatch; 'float const' vs 'int const' - def f(a : float = 1) {} - // error[30161]: function argument default value type mismatch - // 'float const' vs 'int const' - - var m : float ; m <- 1 // error[30941]: can only move compatible type; - // float& -const = int const + take_f(1) // error[30341]: no matching functions or generics: + // take_f(int const) + var m : float ; m <- 1 // error[30941]: can only move compatible type; + // float& -const = int const Spell the literal in the target type instead: ``take_f(1.0f)`` (or ``take_f(float(1))``), and ``def f(a : float = 1.0f)``. Only *integer* literals promote. A float literal never promotes to ``double``: +.. das-doc: expect error[30344] .. code-block:: das var d : double = 1.0 // error[30344]: local variable d initialization // type mismatch; double -const = float const + +Spell the double literal instead: + +.. code-block:: das + var d : double = 1.0lf // ok — double literal Accepted target types: ``int8`` / ``int16`` / ``int`` / ``int64``, @@ -267,14 +277,15 @@ target's exact range. A value that doesn't fit raises a single ``error[30515] exceeds_constant_range`` and **no** downstream type-mismatch error: +.. das-doc: expect error[30515] .. code-block:: das - var d : uint8 = 256 + var too_big : uint8 = 256 // error[30515]: constant value 256 does not fit in uint8 // expected range [0..255] - var d : int8 = -129 // out of range - var d : uint8 = -1 // negative literal, unsigned target + var too_small : int8 = -129 // out of range + var negative : uint8 = -1 // negative literal, unsigned target Float and double — precision is a lint warning ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -485,7 +496,8 @@ Pointers are types that 'reference' (point to) some other data, but can be null In order to work with actual value, one need to dereference it using the dereference or safe navigation operators. Dereferencing will panic if a null pointer is passed to it. Pointers can be created using the new operator, or with the C++ environment. -:: + +.. code-block:: das def twice(var a: int&) { a = a + a @@ -522,6 +534,9 @@ as handled types from the C++ side. Smart pointers in AST code: +.. das-doc: given class DocVisitor : AstVisitor {} +.. das-doc: given var visitor : DocVisitor? +.. das-doc: given var program : ProgramPtr .. code-block:: das require daslib/ast @@ -538,19 +553,23 @@ Smart pointers in AST code: The key properties of smart pointers: * They maintain a reference count and automatically release the object when the count reaches zero -* They can be moved but not copied via ``<-`` +* They can be moved with ``<-``, but never copied with ``=`` * Dereferencing works the same as regular pointers (``*ptr`` and ``ptr.field``) -* Moving from a smart pointer value requires ``unsafe`` unless the value is a ``new`` expression +* Move-**assignment** into a variable that is already live requires ``unsafe`` + (``error[31029]``); move-**initialization** of a fresh ``var inscope`` does not Because ``strict_smart_pointers`` is enabled by default, smart pointer variables must be -declared with ``inscope`` to ensure automatic cleanup: +declared with ``inscope`` to ensure automatic cleanup — a plain ``var`` raises +``error[31018] requires var inscope to be safe``: .. code-block:: das - var inscope a <- some_function() // create — safe, no unsafe needed - var inscope b <- a // move — safe, a becomes null + require daslib/ast + + var inscope a <- make_file_access("") // create — safe, no unsafe needed + var inscope b <- a // move-initialize — safe, a becomes null unsafe { - var inscope c <- some_function() // move from function result — unsafe + b <- a // move-assign into a live variable — unsafe } Ownership transfer functions diff --git a/doc/source/reference/language/expressions.rst b/doc/source/reference/language/expressions.rst index 72a2d391f2..18f827328e 100644 --- a/doc/source/reference/language/expressions.rst +++ b/doc/source/reference/language/expressions.rst @@ -21,6 +21,7 @@ Daslang provides three kinds of assignment: **Copy assignment** (``=``) performs a bitwise copy of the value: +.. das-doc: given var a : int .. code-block:: das a = 10 @@ -32,9 +33,9 @@ Arrays, tables, and other container types cannot be copied — use move or clone .. code-block:: das - var b = new Foo() - var a : Foo? - a <- b // a now points to the Foo instance, b is null + var src = new Foo() + var dst : Foo? + dst <- src // dst now points to the Foo instance, src is null Move is the primary mechanism for transferring ownership of heavy types such as arrays and tables. Some handled types may be movable but not copyable. @@ -43,8 +44,9 @@ Some handled types may be movable but not copyable. .. code-block:: das - var a : array - a := b // a is now a deep copy of b + var src : array + var dst : array + dst := src // dst is now a deep copy of src Clone is syntactic sugar for calling the ``clone`` function. For POD types, clone falls back to a regular copy. @@ -70,6 +72,9 @@ Daslang supports the standard arithmetic operators ``+``, ``-``, ``*``, ``/``, a (modulo). Compound assignment operators ``+=``, ``-=``, ``*=``, ``/=``, ``%=`` and increment/decrement operators ``++`` and ``--`` are also available: +.. das-doc: alt +.. das-doc: given var x : int +.. das-doc: given var y : int .. code-block:: das a += 2 // equivalent to a = a + 2 @@ -89,6 +94,7 @@ Relational Relational operators compare two values and return a ``bool`` result: ``==``, ``!=``, ``<``, ``<=``, ``>``, ``>=``: +.. das-doc: given var b : int .. code-block:: das if ( a == b ) { print("equal\n") } @@ -135,6 +141,7 @@ Daslang supports C-like bitwise operators for integer types: Compound assignment forms: ``&=``, ``|=``, ``^=``, ``<<=``, ``>>=``, ``<<<=``, ``>>>=``: +.. das-doc: given var value : int .. code-block:: das let flags = 0xFF & 0x0F // 0x0F @@ -152,7 +159,7 @@ Pipe operators pass a value as the first (right pipe) or last (left pipe) argume * ``|>`` — right pipe. ``x |> f(y)`` is equivalent to ``f(x, y)`` * ``<|`` — left pipe. ``f(y) <| x`` is equivalent to ``f(y, x)`` -:: +.. code-block:: das def addX(a, b) { return a + b @@ -168,7 +175,7 @@ Left pipe is commonly used to pass blocks and lambdas to functions: invoke(blk) } - doSomething{ + doSomething() <| $() { print("hello\n") } @@ -176,9 +183,12 @@ In gen2 syntax a block or lambda that immediately follows a function call is automatically piped as the last argument, so the explicit ``<|`` can be omitted. Parameterless blocks also do not need the ``$`` prefix: +.. das-doc: given require strings +.. das-doc: given var arr : array +.. das-doc: given def apply_twice(n : int; l : lambda<(a : int) : int>) : int { return invoke(l, invoke(l, n)) } .. code-block:: das - doSomething() { // same as doSomething{ ... } + doSomething() { // same as doSomething() <| $() { ... } print("hello\n") } @@ -196,6 +206,7 @@ This shorthand — called **assumed pipe** — works with all three call forms: * **Dot-method calls** — ``obj.method()`` * **Arrow-method calls** — ``obj->fn()`` +.. das-doc: given struct Callable { fn : lambda<(blk : block<() : int>) : int> } .. code-block:: das // dot-method call with assumed pipe @@ -323,6 +334,9 @@ The ``?[`` operator provides null-safe indexing into tables: Both operators can be used on the left side of an assignment with ``??``: +.. das-doc: given struct Foo { x : int } +.. das-doc: given struct Bar { fooPtr : Foo? } +.. das-doc: given var bar : Bar? .. code-block:: das var dummy = 0 @@ -404,6 +418,8 @@ Cast, Upcast, and Reinterpret **cast** performs a safe upcast from a derived structure type to a parent (base) type: +.. das-doc: given struct Base { z : int } +.. das-doc: given struct Derived : Base { w : int } .. code-block:: das var derived : Derived = Derived() @@ -412,6 +428,7 @@ Cast, Upcast, and Reinterpret **upcast** performs an unsafe upcast from a base type to a derived type. This requires ``unsafe`` because the actual runtime type may not match: +.. das-doc: given var base_ref : Base .. code-block:: das unsafe { @@ -484,18 +501,25 @@ Dot Operator Bypass ^^^^^^^^^^^^^^^^^^^ .. index:: - pair: .. Operator; Operators + pair: . . Operator; Operators Smart pointers (``smart_ptr``) are accessed the same way as regular pointers — using ``.`` for field access and ``?.`` for null-safe field access. -The ``..`` operator bypasses any ``.`` operator overloading and accesses the -underlying field directly. This is useful when a handled type defines a custom -``.`` operator but you need to reach the actual field: +A doubled dot bypasses any ``.`` operator overloading (a property, see +:ref:`Classes `) and accesses the underlying field directly. This is +useful when a type defines a custom ``.`` operator but you need to reach the +actual field: +.. das-doc: given struct Boxed { payload : int } +.. das-doc: given var sp : Boxed? .. code-block:: das - sp..x = 42 // accesses field x directly, skipping any . overload + sp. .payload = 42 // accesses field payload directly, skipping any . overload + +The two dots must be **separated by a space**: ``..`` with no space is the +:ref:`interval ` operator token, so ``sp..payload`` parses as +``interval(sp, payload)``. ^^^^^^^^^^^^^^^^^^^ Safe Index (?[) @@ -521,6 +545,7 @@ Unsafe Expression Individual expressions can be marked as unsafe without wrapping an entire block: +.. das-doc: alt .. code-block:: das let p = unsafe(addr(x)) @@ -620,20 +645,20 @@ Structures can be initialized by specifying field values: .. code-block:: das - struct Foo { + struct Point { x : int = 1 y : int = 2 } - let a = Foo(x = 13, y = 11) // x = 13, y = 11 - let b = Foo(x = 13) // x = 13, y = 2 (default) - let c = unsafe(Foo(uninitialized x = 13)) // x = 13, y = 0 (uninitialized construction requires unsafe) + let a = Point(x = 13, y = 11) // x = 13, y = 11 + let b = Point(x = 13) // x = 13, y = 2 (default) + let c = unsafe(Point(uninitialized x = 13)) // x = 13, y = 0 (uninitialized construction requires unsafe) Arrays of structures can be constructed inline: .. code-block:: das - var arr <- array struct((x=11, y=22), (x=33), (y=44)) + var arr <- array struct((x=11, y=22), (x=33), (y=44)) Classes and handled (external) types can also be initialized using this syntax. Classes and handled types cannot use ``uninitialized``. @@ -670,19 +695,19 @@ Variants are created by specifying exactly one field: .. code-block:: das - variant Foo { + variant Number { i : int f : float } - let x = Foo(i = 3) - let y = Foo(f = 4.0) + let ni = Number(i = 3) + let nf = Number(f = 4.0) Variants can also be declared as type aliases: .. code-block:: das - typedef Foo = variant + typedef Number = variant (see :ref:`Variants `). @@ -717,15 +742,15 @@ The ``default`` expression creates a default-initialized value of a given type: .. code-block:: das - var a = default // all fields zeroed, then default initializer called - var b = unsafe(default uninitialized) // all fields zeroed, no initializer (uninitialized requires unsafe) + var a = default // all fields zeroed, then default initializer called + var b = unsafe(default uninitialized) // all fields zeroed, no initializer (uninitialized requires unsafe) The ``new`` operator allocates a value on the heap and returns a pointer: .. code-block:: das - var p = new Foo() // Foo? pointer, default initialized - var q = new Foo(x = 13) // with field initialization + var p = new Point() // Point? pointer, default initialized + var q = new Point(x = 13) // with field initialization ``new`` can also be combined with array and table literals to allocate them on the heap: @@ -743,6 +768,7 @@ typeinfo The ``typeinfo`` expression provides compile-time type information. It is primarily used in generic functions to inspect argument types: +.. das-doc: given var myStruct : Foo .. code-block:: das typeinfo typename(type) // returns "int" at compile time diff --git a/doc/source/reference/language/finalizers.rst b/doc/source/reference/language/finalizers.rst index 6acd7b2f0e..e8437104f3 100644 --- a/doc/source/reference/language/finalizers.rst +++ b/doc/source/reference/language/finalizers.rst @@ -79,6 +79,7 @@ and then calling the native memory finalizer on the result: This expands to: +.. das-doc: skip .. code-block:: das def finalize ( var __this:Foo?& explicit -const ) { @@ -98,6 +99,7 @@ Static arrays call ``finalize_dim`` generically, which finalizes all its values: This expands to: +.. das-doc: skip .. code-block:: das def builtin`finalize_dim ( var a:Foo aka TT[5] explicit ) { @@ -115,13 +117,14 @@ Dynamic arrays call ``finalize`` generically, which finalizes all its values: This expands to: +.. das-doc: skip .. code-block:: das def builtin`finalize ( var a:array explicit ) { for ( aV in a ) { _::finalize(aV) } - __builtin_array_free(a,4,__context__) + __builtin_array_free(a,4,__context__,__lineinfo__) } Tables call ``finalize`` generically, which finalizes all its values, but not its keys: @@ -133,13 +136,14 @@ Tables call ``finalize`` generically, which finalizes all its values, but not it This expands to: +.. das-doc: skip .. code-block:: das def builtin`finalize ( var a:table explicit ) { for ( aV in values(a) ) { _::finalize(aV) } - __builtin_table_free(a,8,4,__context__) + __builtin_table_free(a,8,4,__context__,__lineinfo__) } Custom finalizers are generated for structures. A field annotated ``@do_not_delete`` is skipped @@ -158,6 +162,7 @@ entirely — no ``finalize`` call is generated for it, and nothing it owns is fr This expands to: +.. das-doc: skip .. code-block:: das def finalize ( var __this:Goo explicit ) { @@ -177,6 +182,7 @@ Tuples behave similarly to structures. There is no way to ignore individual fiel This expands to: +.. das-doc: skip .. code-block:: das def finalize ( var __this:tuple explicit -const ) { @@ -193,12 +199,13 @@ Variants behave similarly to tuples. Only the currently active variant is finali This expands to: +.. das-doc: skip .. code-block:: das def finalize ( var __this:variant> explicit -const ) { if ( __this is f ) { _::finalize(__this.f) - } else if (__this is ai) { + } elif ( __this is ai ) { __::builtin`finalize(__this.ai) } memzero(__this) diff --git a/doc/source/reference/language/functions.rst b/doc/source/reference/language/functions.rst index 09d2cfe31e..d01fc10402 100644 --- a/doc/source/reference/language/functions.rst +++ b/doc/source/reference/language/functions.rst @@ -34,7 +34,11 @@ Completely empty functions (without arguments) can be also declared: print("foo") } - //same as above +The parenthesized form declares exactly the same function: + +.. das-doc: alt +.. code-block:: das + def foo() { print("foo") } @@ -42,13 +46,14 @@ Completely empty functions (without arguments) can be also declared: Daslang can always infer a function's return type. Returning different types is a compilation error: +.. das-doc: expect error[30343] .. code-block:: das - def foo(a:bool) { + def bad_return(a:bool) { if ( a ) { return 1 } else { - return 2.0 // error, expecting int + return 2.0 // error[30343], expecting int } } @@ -60,7 +65,12 @@ The return type can be specified explicitly with ``:`` or ``->`` — both are eq return a + b } - def add(a, b : int) -> int { // same as above +The ``->`` spelling declares exactly the same function: + +.. das-doc: alt +.. code-block:: das + + def add(a, b : int) -> int { return a + b } @@ -111,6 +121,7 @@ Publicity Functions can be ``private`` or ``public`` +.. das-doc: alt .. code-block:: das def private foo(a:bool) { @@ -158,15 +169,17 @@ You can also call a function by using its name and passing all its arguments wit Named arguments should be still in the same order: +.. das-doc: expect error[30341] .. code-block:: das - def bar { - foo([b = 1, a = 2]) // error, out of order + def bar_out_of_order { + foo([b = 1, a = 2]) // error[30341], out of order } Named argument calls increase the readability of callee code and ensure correctness in refactorings of the existing functions. They also allow default values for arguments other than the last ones: +.. das-doc: alt .. code-block:: das def foo(a:int=13, b: int) { @@ -185,12 +198,14 @@ Function pointer Pointers to a function use a similar declaration to that of a block or lambda. The type is written as ``function`` followed by an optional type signature in angle brackets: +.. das-doc: fragment .. code-block:: das function < (arg1:int; arg2:float&) : bool > The ``->`` operator can be used instead of ``:`` for the return type: +.. das-doc: fragment .. code-block:: das function < (arg1:int; arg2:float&) -> bool > // equivalent @@ -200,6 +215,7 @@ an unspecified signature. Function pointers can be obtained by using the ``@@`` operator: +.. das-doc: alt .. code-block:: das def twice(a:int) { @@ -210,6 +226,7 @@ Function pointers can be obtained by using the ``@@`` operator: When multiple functions have the same name, a pointer can be obtained by explicitly specifying signature: +.. das-doc: alt .. code-block:: das def twice(a:int) { @@ -224,6 +241,7 @@ When multiple functions have the same name, a pointer can be obtained by explici Function pointers can be called via ``invoke`` or via call notation: +.. das-doc: given var fn : function<(a:int):int> .. code-block:: das let t = invoke(fn, 1) // t = 2 @@ -244,16 +262,18 @@ similar to that of lambdas or blocks (see :ref:`Blocks `): Nameless local functions do not capture variables at all: +.. das-doc: expect error[30838] .. code-block:: das var count = 1 let fn <- @@ ( a : int ) { - return a + count // compilation error, can't locate variable count + return a + count // error[30838], can't locate variable count } Internally, a regular function will be generated (illustrative — the backtick-mangled name is compiler-internal and is not source you can type): +.. das-doc: skip .. code-block:: das def _localfunction_thismodule_8_8_1`function ( a:int const ) : int { @@ -285,6 +305,7 @@ You cannot take the address of a generic function. Unspecified types can also be written via ``auto`` notation: +.. das-doc: alt .. code-block:: das def twice(a:auto) { // same as 'twice' above @@ -293,6 +314,7 @@ Unspecified types can also be written via ``auto`` notation: Generic functions can specialize generic type aliases, and use them as part of the declaration: +.. das-doc: alt .. code-block:: das def twice(a:auto(TT)) : TT { @@ -322,6 +344,7 @@ Function overloading Functions can be specialized if their argument types are different: +.. das-doc: alt .. code-block:: das def twice(a: int) { @@ -340,6 +363,7 @@ Declaring functions with the same exact argument list is a compilation-time erro Functions can be partially specialized: +.. das-doc: alt .. code-block:: das def twice(a:int) { // int @@ -401,6 +425,8 @@ Available logic operations are ``!``, ``&&``, ``||`` and ``^^``. LSP can be explicitly prohibited for a particular function argument via the ``explicit`` keyword: +.. das-doc: alt +.. das-doc: given struct Foo { a : int } .. code-block:: das def foo ( a : Foo explicit ) { // will accept Foo, but not any subtype of Foo @@ -432,7 +458,7 @@ It is valid to declare default values for arguments other than the last one: .. code-block:: das - def test(c: int = 1, d: int = 1, a, b: int) { // valid! + def test2(c: int = 1, d: int = 1, a, b: int) { // valid! return a + b + c + d } @@ -440,18 +466,25 @@ Calling such functions with default arguments requires a named arguments call: .. code-block:: das - test(2, 3) // invalid call, a,b parameters are missing - test([a = 2, b = 3]) // valid call + test2([a = 2, b = 3]) // valid call + +A positional call cannot skip the defaulted arguments, so ``a`` and ``b`` end up missing: + +.. das-doc: expect error[30341] +.. code-block:: das + + test2(2, 3) // error[30341], a,b parameters are missing Default arguments can be combined with overloading: +.. das-doc: alt .. code-block:: das def test(c: int = 1, d: int = 1, a, b: int) { return a + b + c + d } def test(a, b: int) { // now test(2, 3) is valid call - return test([a = a, b = b]) + return test([c = 1, d = 1, a = a, b = b]) } --------------- @@ -472,7 +505,7 @@ However, code can be easily written "OOP style" by using the right pipe operator thisFoo.y = y } ... - var foo:Foo + var foo = Foo() foo |> setXY(10, 11) // this is syntactic sugar for setXY(foo, 10, 11) setXY(foo, 10, 11) // exactly same as above line @@ -500,6 +533,7 @@ Operator Overloading Daslang allows you to overload operators, which means that you can define custom behavior for operators when used with your own data types. To overload an operator, you need to define a special function with the name of the operator you want to overload. Here's the syntax: +.. das-doc: skip .. code-block:: das def operator () : @@ -584,6 +618,7 @@ Unary operators Unary operators take a single argument. To overload unary minus (negate): +.. das-doc: given struct Vec2 { x, y : float } .. code-block:: das def operator -(a : Vec2) : Vec2 { @@ -660,6 +695,7 @@ Additional index operators include ``[]<-`` (move into index), ``[]:=`` (clone i ``[]<-`` takes the right-hand side as a ``var`` parameter and moves from it (zeroing the source) — this is the store operator for non-copyable element types: +.. das-doc: given struct Rows { rows : array> } .. code-block:: das def operator []<-(var m : Rows; i : int; var v : array) { @@ -742,10 +778,11 @@ Null-coalesce operator ``operator ??`` can be overloaded to provide a default value when a nullable or optional type is null: +.. das-doc: given struct MyOptional { has_value : bool; value : int } .. code-block:: das def operator ??(a : MyOptional; default_value : int) : int { - // return contained value or default_value + return a.has_value ? a.value : default_value } --------------------------------------------- @@ -778,6 +815,7 @@ Overloading the '.' and '?.' operators Daslang allows you to overload the dot . operator, which is used to access fields of structure or a class. To overload the dot . operator, you need to define a special function with the name operator `.` Here's the syntax: +.. das-doc: skip .. code-block:: das def operator.(: , : string) : @@ -785,6 +823,7 @@ To overload the dot . operator, you need to define a special function with the n Alternatively you can specify field explicitly: +.. das-doc: skip .. code-block:: das def operator. (: ) : diff --git a/doc/source/reference/language/generators.rst b/doc/source/reference/language/generators.rst index 337cbadd89..332bd48574 100644 --- a/doc/source/reference/language/generators.rst +++ b/doc/source/reference/language/generators.rst @@ -127,50 +127,55 @@ In the following example: return false } -A lambda is generated with all captured variables: +A lambda is generated with all captured variables. Generated names embed the +source position they come from, so they differ from file to file — here the +generator expression sits on line 8, and its ``for`` loop on line 9: +.. das-doc: skip .. code-block:: das - struct _lambda_thismodule_8_8_1 { - __lambda : function<(__this:_lambda_thismodule_8_8_1;_yield_8:int&):bool const> = @@_::_lambda_thismodule_8_8_1`function - __finalize : function<(__this:_lambda_thismodule_8_8_1? -const):void> = @@_::_lambda_thismodule_8_8_1`finalizer + struct _lambda_thismodule_8_1 { + __lambda : function<(var __this:_lambda_thismodule_8_1;var _yield_8:int&):bool const> + __finalize : function<(var __this:_lambda_thismodule_8_1? -const):void> __yield : int - _loop_at_8 : bool - x : int // captured constant - _pvar_0_at_8 : void? - _source_0_at_8 : iterator + _loop_at_9_8 : bool + __x_rename_at_9_14 : int // captured constant + _pvar_0_at_9_8 : void? + _source_0_at_9_8 : iterator } A lambda function is generated: +.. das-doc: skip .. code-block:: das [GENERATOR] - def _lambda_thismodule_8_8_1`function ( var __this:_lambda_thismodule_8_8_1; var _yield_8:int& ) : bool const { + [LAMBDA] + def private _lambda_thismodule_8_1`function(var __this:_lambda_thismodule_8_1 explicit; var _yield_8:int&) : bool const { goto __this.__yield label 0: - __this._loop_at_8 = true - __this._source_0_at_8 <- __::builtin`each(range(0,10)) - memzero(__this.x) - __this._pvar_0_at_8 = reinterpret(addr(__this.x)) - __this._loop_at_8 &&= _builtin_iterator_first(__this._source_0_at_8,__this._pvar_0_at_8,__context__) - label 3: /*begin for at line 8*/ - if ( !__this._loop_at_8 ) { + __this._loop_at_9_8 = true + __this._source_0_at_9_8 <- __::builtin`each(range(0,10)) + memzero(__this.__x_rename_at_9_14) + __this._pvar_0_at_9_8 = reinterpret addr(__this.__x_rename_at_9_14) + __this._loop_at_9_8 = _builtin_iterator_first(__this._source_0_at_9_8,__this._pvar_0_at_9_8,__context__,__lineinfo__) && __this._loop_at_9_8 + label 3: /*begin for at line 9*/ + if ( !__this._loop_at_9_8 ) { goto label 5 } - if ( !((__this.x & 1) == 0) ) { + if ( (__this.__x_rename_at_9_14 & 1) != 0 ) { goto label 2 } - _yield_8 = __this.x + _yield_8 = __this.__x_rename_at_9_14 __this.__yield = 1 return /*yield*/ true - label 1: /*yield at line 10*/ - label 2: /*end if at line 9*/ - label 4: /*continue for at line 8*/ - __this._loop_at_8 &&= _builtin_iterator_next(__this._source_0_at_8,__this._pvar_0_at_8,__context__) + label 1: /*yield at line 11*/ + label 2: /*end if at line 10*/ + label 4: /*continue for at line 9*/ + __this._loop_at_9_8 &&= _builtin_iterator_next(__this._source_0_at_9_8,__this._pvar_0_at_9_8,__context__,__lineinfo__) goto label 3 - label 5: /*end for at line 8*/ - _builtin_iterator_close(__this._source_0_at_8,__this._pvar_0_at_8,__context__) + label 5: /*end for at line 9*/ + _builtin_iterator_close(__this._source_0_at_9_8,__this._pvar_0_at_9_8,__context__) return false } @@ -181,18 +186,23 @@ This effectively produces a finite state machine, with the ``yield`` variable ho The ``yield`` expression is converted into a copy result and return value pair. A label is created to specify where to go to next time, after the ``yield``: +.. das-doc: skip .. code-block:: das - _yield_8 = __this.x // produce next iterator value - __this.__yield = 1 // label to go to next (1) - return /*yield*/ true // return true to indicate, that iterator produced a value - label 1: /*yield at line 10*/ // next label marker (1) + _yield_8 = __this.__x_rename_at_9_14 // produce next iterator value + __this.__yield = 1 // label to go to next (1) + return /*yield*/ true // return true — the iterator produced a value + label 1: /*yield at line 11*/ // next label marker (1) Iterator initialization is replaced with the creation of the lambda: +.. das-doc: skip .. code-block:: das - var gen:iterator <- each(new> default<_lambda_thismodule_8_8_1>) + var gen:iterator <- __::builtin`each( + new> struct<_lambda_thismodule_8_1>( + uninitialized __lambda = @@_::_lambda_thismodule_8_1`function, + __finalize = @@_::_lambda_thismodule_8_1`finalizer)) .. seealso:: diff --git a/doc/source/reference/language/generic_programming.rst b/doc/source/reference/language/generic_programming.rst index de62f0afde..85bda003eb 100644 --- a/doc/source/reference/language/generic_programming.rst +++ b/doc/source/reference/language/generic_programming.rst @@ -48,6 +48,7 @@ It is the primary mechanism for inspecting types in generic functions. All ``typeinfo`` traits can operate on either an expression or a ``type`` argument: +.. das-doc: given var my_variable : int .. code-block:: das typeinfo sizeof(type) // 12 @@ -173,6 +174,7 @@ Instead of omitting the type name in a generic, it is possible to use an explici or +.. das-doc: alt .. code-block:: das def fn(a: auto(some_name)): some_name { @@ -181,6 +183,7 @@ or This is the same as: +.. das-doc: alt .. code-block:: das def fn(a) { @@ -195,11 +198,14 @@ This is very helpful if the function accepts numerous arguments, and some of the return a + b } -This is not the same as: +Naming the ``auto`` is what ties arguments together; a bare ``auto`` does not. +``def fn(a, b: auto)`` is the very same generic as ``def fn(a, b)`` — declaring both is +``error[30702]: generic function is already defined`` — and both accept mismatched types, +failing only later, inside the body. Reuse a named alias instead: .. code-block:: das - def fn(a, b: auto) { // a and b are one type + def fn(a: auto(T); b: T) { // a and b have to be of the same type return a + b } @@ -223,6 +229,7 @@ To get a clearer error, constrain the types directly in the signature: Usage of named ``auto`` with ``typeinfo`` +.. das-doc: alt .. code-block:: das def fn(a: auto(some)) { @@ -233,6 +240,7 @@ Usage of named ``auto`` with ``typeinfo`` You can also modify the type with delete syntax: +.. das-doc: alt .. code-block:: das def fn(a: auto(some)) { @@ -248,12 +256,14 @@ Generic function arguments, result, and inferred type aliases can be operated on ``const`` specifies, that constant and regular expressions will be matched: +.. das-doc: signatures .. code-block:: das def foo ( a : Foo const ) // accepts Foo and Foo const ``==const`` specifies, that const of the expression has to match const of the argument: +.. das-doc: signatures .. code-block:: das def foo ( a : Foo const ==const ) // accepts Foo const only @@ -261,12 +271,14 @@ Generic function arguments, result, and inferred type aliases can be operated on ``-const`` will remove const from the matching type: +.. das-doc: signatures .. code-block:: das def foo ( a : array ) // matches any array, with non-const elements ``#`` specifies that only temporary types are accepted: +.. das-doc: signatures .. code-block:: das def foo ( a : Foo# ) // accepts Foo# only @@ -281,12 +293,14 @@ Generic function arguments, result, and inferred type aliases can be operated on ``&`` specifies that argument is passed by reference: +.. das-doc: signatures .. code-block:: das def foo ( a : auto& ) // accepts any type, passed by reference ``==&`` specifies that reference of the expression has to match reference of the argument: +.. das-doc: signatures .. code-block:: das def foo ( a : auto& ==& ) // accepts any type, passed by reference (for example variable i, even if its integer) @@ -302,6 +316,7 @@ Generic function arguments, result, and inferred type aliases can be operated on ``[]`` specifies that the argument is a fixed-size array: +.. das-doc: signatures .. code-block:: das def foo ( a : auto[] ) // accepts a fixed-size array of any type and size @@ -340,6 +355,7 @@ dimensions included: ``implicit`` specifies that both temporary and regular types can be matched, but the type will be treated as specified. ``implicit`` is _UNSAFE_: +.. das-doc: signatures .. code-block:: das def foo ( a : Foo implicit ) // accepts Foo and Foo#, a will be treated as Foo @@ -347,6 +363,7 @@ dimensions included: ``explicit`` specifies that LSP will not be applied, and only exact type match will be accepted: +.. das-doc: signatures .. code-block:: das def foo ( a : Foo ) // accepts Foo and any type that is inherited from Foo directly or indirectly @@ -357,6 +374,7 @@ options Multiple options can be specified as a function argument: +.. das-doc: signatures .. code-block:: das def foo ( a : int | float ) // accepts int or float @@ -365,12 +383,14 @@ OR types always make the function generic. Generic options will be matched in the order listed: +.. das-doc: signatures .. code-block:: das def foo ( a : Bar explicit | Foo ) // first will try to match exactly Bar, then anything else inherited from Foo ``|#`` shortcut matches previous type, with temporary flipped: +.. das-doc: signatures .. code-block:: das def foo ( a : Foo |# ) // accepts Foo and Foo# in that order @@ -482,11 +502,11 @@ Prefix Resolution =========== =================================================================== This distinction matters whenever a library generic should dispatch to -user-provided overloads. For example: +user-provided overloads. Given the library module ``serializer.das``: +.. das-doc: file serializer.das .. code-block:: das - // --- module "serializer" --- module serializer [generic] @@ -494,7 +514,11 @@ user-provided overloads. For example: _::write(val) // resolves in the caller's module } - // --- user code --- +user code supplies the overload the generic dispatches to: + +.. das-doc: fresh +.. code-block:: das + require serializer struct Color { r : float; g : float; b : float } diff --git a/doc/source/reference/language/iterators.rst b/doc/source/reference/language/iterators.rst index 3ecba3fe10..5738addbe4 100644 --- a/doc/source/reference/language/iterators.rst +++ b/doc/source/reference/language/iterators.rst @@ -8,6 +8,7 @@ Iterators are objects that traverse a sequence without exposing the details of t The iterator type is written as ``iterator`` followed by the element type in angle brackets: +.. das-doc: fragment .. code-block:: das iterator // iterates over integers @@ -26,6 +27,8 @@ Iterators can be created via the ``each`` function from a range, static array, o The most straightforward way to traverse an iterator is with a ``for`` loop: +.. das-doc: given var it : iterator +.. das-doc: alt .. code-block:: das for ( x in it ) { // iterates over contents of 'it' @@ -152,6 +155,7 @@ The ``empty`` function checks if an iterator is null or already sequenced out: More complicated iteration patterns may require the ``next`` function: +.. das-doc: alt .. code-block:: das var x : int @@ -207,16 +211,16 @@ next implementation details The function ``next`` is implemented as follows: +.. das-doc: alt .. code-block:: das - def next ( var it:iterator; var value : TT& ) : bool { + def next(var it : iterator; var value : TT&) : bool { static_if (!typeinfo can_copy(type)) { - concept_assert(false, "requires type - which can be copied") + concept_assert(false, "requires type which can be copied") } static_elif (typeinfo is_ref_value(type)) { - var pValue : TT - & ? + var pValue : TT -&? unsafe { - if ( _builtin_iterator_iterate(it, addr(pValue)) ) { + if (_builtin_iterator_iterate(it, addr(pValue))) { value = *pValue return true } else { diff --git a/doc/source/reference/language/lambdas.rst b/doc/source/reference/language/lambdas.rst index b44187a53d..29433018d3 100644 --- a/doc/source/reference/language/lambdas.rst +++ b/doc/source/reference/language/lambdas.rst @@ -10,12 +10,14 @@ Lambdas are slower than blocks, but allow for more flexibility in lifetime and c The lambda type can be declared with a function-like syntax. The type is written as ``lambda`` followed by an optional type signature in angle brackets: +.. das-doc: fragment .. code-block:: das lambda < (arg1:int; arg2:float&) : bool > The ``->`` operator can be used instead of ``:`` for the return type: +.. das-doc: fragment .. code-block:: das lambda < (arg1:int; arg2:float&) -> bool > // equivalent @@ -105,6 +107,8 @@ Lambdas can be deleted, which causes finalizers to be called on all captured dat Because copies alias the same capture frame, ``delete`` requires ``unsafe`` — the caller is asserting no other live copy exists: +.. das-doc: given var lam : lambda +.. das-doc: alt .. code-block:: das unsafe { delete lam; } @@ -173,6 +177,7 @@ Daslang will generated the following code: Capture structure: +.. das-doc: fragment .. code-block:: das struct _lambda_thismodule_7_8_1 { @@ -183,6 +188,7 @@ Capture structure: Body function: +.. das-doc: fragment .. code-block:: das def _lambda_thismodule_7_8_1`function ( var __this:_lambda_thismodule_7_8_1; extra:int const ) : int { @@ -193,22 +199,24 @@ Body function: Finalizer function: +.. das-doc: fragment .. code-block:: das def _lambda_thismodule_7_8_1`finalizer ( var __this:_lambda_thismodule_7_8_1? explicit ) { - delete *this + delete *__this delete __this } Lambda creation is replaced with the ascend of the capture structure: +.. das-doc: fragment .. code-block:: das let counter:lambda<(extra:int const):int> const <- new> (CNT = CNT) The C++ Lambda class contains single void pointer for the capture data: -.. code-block:: das +.. code-block:: cpp struct Lambda { ... diff --git a/doc/source/reference/language/lexical_structure.rst b/doc/source/reference/language/lexical_structure.rst index ce8a174296..cc7be71eed 100644 --- a/doc/source/reference/language/lexical_structure.rst +++ b/doc/source/reference/language/lexical_structure.rst @@ -21,6 +21,7 @@ three distinct identifiers. Backticks are used in system-generated identifiers (such as mangled names) and are generally not used in user code: +.. das-doc: fragment .. code-block:: das my_variable // valid @@ -74,7 +75,15 @@ The following words are reserved as built-in type names and cannot be used as id +----------------------------------------------+----------------------------------------------+----------------------------------------------+----------------------------------------------+----------------------------------------------+----------------------------------------------+ | :ref:`lambda ` | ``int8`` | ``uint8`` | ``int16`` | ``uint16`` | :ref:`tuple ` | +----------------------------------------------+----------------------------------------------+----------------------------------------------+----------------------------------------------+----------------------------------------------+----------------------------------------------+ -| :ref:`variant ` | ``range64`` | ``urange64`` | | | | +| :ref:`variant ` | ``range64`` | ``urange64`` | ``float16`` | ``half2`` | ``half3`` | ++----------------------------------------------+----------------------------------------------+----------------------------------------------+----------------------------------------------+----------------------------------------------+----------------------------------------------+ +| ``half4`` | ``half8`` | ``short2`` | ``short3`` | ``short4`` | ``short8`` | ++----------------------------------------------+----------------------------------------------+----------------------------------------------+----------------------------------------------+----------------------------------------------+----------------------------------------------+ +| ``ushort2`` | ``ushort3`` | ``ushort4`` | ``ushort8`` | ``byte2`` | ``byte3`` | ++----------------------------------------------+----------------------------------------------+----------------------------------------------+----------------------------------------------+----------------------------------------------+----------------------------------------------+ +| ``byte4`` | ``byte8`` | ``byte16`` | ``ubyte2`` | ``ubyte3`` | ``ubyte4`` | ++----------------------------------------------+----------------------------------------------+----------------------------------------------+----------------------------------------------+----------------------------------------------+----------------------------------------------+ +| ``ubyte8`` | ``ubyte16`` | | | | | +----------------------------------------------+----------------------------------------------+----------------------------------------------+----------------------------------------------+----------------------------------------------+----------------------------------------------+ Keywords and types are covered in detail in subsequent sections of this documentation. @@ -101,7 +110,7 @@ Daslang recognizes the following operators: +----------+----------+----------+----------+----------+----------+----------+----------+ | ``<`` | ``!`` | ``~`` | ``&&`` | ``||`` | ``^^`` | ``&&=`` | ``||=`` | +----------+----------+----------+----------+----------+----------+----------+----------+ -| ``^^=`` | ``..`` | | | | | | | +| ``^^=`` | ``..`` | ``=`` | ``&=`` | | | | | +----------+----------+----------+----------+----------+----------+----------+----------+ Notable operators unique to Daslang: @@ -193,6 +202,7 @@ Float suffixes: * ``f`` (or no suffix after a decimal point) — 32-bit float * ``d``, ``lf`` — 64-bit double +* ``h`` or ``H`` — 16-bit float (``float16``) ^^^^^^^^^^^^^^^ String Literals @@ -324,6 +334,10 @@ in curly braces, unless the line is also inside parentheses or brackets: This means that expressions can be split across multiple lines when parentheses or brackets keep them together: +.. das-doc: given def some_long_function_name(a, b : int) : int { return a + b } +.. das-doc: given let arg1 = 1 +.. das-doc: given let arg2 = 2 +.. das-doc: given let another_value = 3 .. code-block:: das let result = ( @@ -335,6 +349,12 @@ A closing ``}`` also terminates the last statement of the block it closes, so th statement of a one-liner block needs no trailing semicolon — just as if a newline preceded the ``}``: +.. das-doc: given var ready = true +.. das-doc: given def start() { } +.. das-doc: given var n = 4 +.. das-doc: given var total = 0 +.. das-doc: given var a = 0 +.. das-doc: given var b = 0 .. code-block:: das if ( ready ) { start() } // no ';' needed before '}' diff --git a/doc/source/reference/language/lint.rst b/doc/source/reference/language/lint.rst index 280fcefe18..d0ffe91d80 100644 --- a/doc/source/reference/language/lint.rst +++ b/doc/source/reference/language/lint.rst @@ -11,6 +11,63 @@ Lint Tools single: Performance Lint single: Style Lint +.. das-doc: given require strings +.. das-doc: given require math +.. das-doc: given require daslib/json_boost +.. das-doc: given struct Box { value : int } +.. das-doc: given struct Session { pos : int64 } +.. das-doc: given struct SomeStruct { value : int64 } +.. das-doc: given struct Foo { x : int } +.. das-doc: given def compute() : int { return 0 } +.. das-doc: given def report(ok : bool) { } +.. das-doc: given def process(v : int) { } +.. das-doc: given def make_thing() : array { var r : array; return <- r } +.. das-doc: given def make_more() : array { var r : array; return <- r } +.. das-doc: given def takes_block(blk : block<>) { invoke(blk) } +.. das-doc: given var x : int +.. das-doc: given var y : int +.. das-doc: given var a : int +.. das-doc: given var b : int +.. das-doc: given var c : int +.. das-doc: given var s : string +.. das-doc: given var n : int64 +.. das-doc: given var arr : array +.. das-doc: given var src : array +.. das-doc: given var dst : array +.. das-doc: given var cond : bool +.. das-doc: given var flag : bool +.. das-doc: given var size : int +.. das-doc: given var capacity : int +.. das-doc: given var value : int +.. das-doc: given var then_value : int +.. das-doc: given var else_value : int +.. das-doc: given var divisor : int +.. das-doc: given var const_ptr : int? +.. das-doc: given var name : string +.. das-doc: given var lo : int +.. das-doc: given var hi : int +.. das-doc: given var xs : array +.. das-doc: given var ys : array +.. das-doc: given var uv : uint +.. das-doc: given var p : int? +.. das-doc: given var f : int +.. das-doc: given var raw : void? +.. das-doc: given var key : string +.. das-doc: given var tab : table +.. das-doc: given var target : int +.. das-doc: given var dx : int +.. das-doc: given var dy : int +.. das-doc: given var pending : array +.. das-doc: given var rest : array +.. das-doc: given var consumed : int64 +.. das-doc: given var HOP : int64 +.. das-doc: given var nbytes : int64 +.. das-doc: given var newSize : int64 +.. das-doc: given var jv : JsonValue? +.. das-doc: given def emit_header(var w : StringBuilderWriter) { } +.. das-doc: given def emit_body(var w : StringBuilderWriter) { } +.. das-doc: given def emit_footer(var w : StringBuilderWriter) { } + daslang provides three complementary lint passes that detect issues at compile time: - **Paranoid lint** (``daslib/lint``) — unreachable code, unused variables and arguments, variables that can be ``let``, underscore naming, redundant reinterpret casts, 64-bit narrowing traps (error code ``50503``) @@ -153,6 +210,7 @@ LINT001 — unreachable code Code after a ``return`` or ``panic()`` in the same block is unreachable and will never execute. +.. das-doc: alt .. code-block:: das def foo() { @@ -166,6 +224,7 @@ LINT002 — unused variable A declared variable is never read. Prefix the name with an underscore (``_x``) to suppress the warning, or remove the variable entirely. +.. das-doc: alt .. code-block:: das def foo() { @@ -178,6 +237,7 @@ LINT003 — variable can be ``let`` A ``var`` variable is never mutated. Declare it with ``let`` instead. +.. das-doc: alt .. code-block:: das // Bad @@ -194,6 +254,7 @@ flagged, even when the callee never writes through it — a ``let`` argument would no longer match the ``var`` parameter and the build would break. The callee side of that situation is LINT014's report. +.. das-doc: alt .. code-block:: das def probe(var a : float[4][4]) : string { // never writes a — see LINT014 @@ -214,6 +275,7 @@ a ``_``-prefixed *argument* is never flagged (a parameter name is often constrained: intentionally unused, or dodging a reserved keyword / shadow such as ``_in``). +.. das-doc: alt .. code-block:: das def foo() : int { @@ -231,6 +293,7 @@ The rule skips casts that strip ``const`` or ``temporary`` modifiers (those serve a purpose) and casts between ``void?`` and typed pointers. It also skips generic instantiations and compiler-generated functions. +.. das-doc: alt .. code-block:: das // Bad — x is already int? @@ -250,6 +313,7 @@ LINT006 — division by zero (constant zero divisor) typo. Also covers the compound forms ``/=`` and ``%=``. Recognizes literal zero across ``int``, ``uint``, ``int64``, ``uint64``, ``float``, and ``double``. +.. das-doc: alt .. code-block:: das // Bad @@ -268,6 +332,7 @@ and the code is almost always a copy-paste typo. Triggers on: ``==``, ``!=``, ``<``, ``>``, ``<=``, ``>=``, ``-``, ``/``, ``%``, ``&&``, ``||``, ``&``, ``|``, ``^``, ``-=``, ``/=``, ``%=``. +.. das-doc: fragment .. code-block:: das // Bad — author meant `size == capacity` or similar @@ -284,6 +349,7 @@ intentional. NaN check — daslang has no dedicated ``is_nan`` helper for scalar floats. Suppress LINT007 on the one line that needs it: +.. das-doc: alt .. code-block:: das def is_nan(x : float) : bool { @@ -306,6 +372,7 @@ LINT008 — both ternary branches equivalent ``cond ? x : x`` ignores ``cond`` and always produces ``x``. Copy-paste bug. +.. das-doc: alt .. code-block:: das // Bad @@ -321,6 +388,7 @@ LINT009 — ``then`` branch equivalent to ``else`` branch copy-pasted one branch and forgot to edit the other. Caught even when ``A`` has side effects — the structural pattern is suspicious regardless of purity. +.. das-doc: alt .. code-block:: das // Bad @@ -352,6 +420,7 @@ overwritten by a later write with no intervening read, or it goes out of scope The variable being read elsewhere keeps LINT002 (unused variable) silent — this rule is for *partial* deadness within an otherwise-used local. +.. das-doc: alt .. code-block:: das // Bad — re-init before any read @@ -413,6 +482,7 @@ sources cap at ``uint32`` (``2^32 - 1``). LINT011 therefore never fires on ``double`` targets today — the rule is wired symmetrically so future broader sources stay covered. +.. das-doc: alt .. code-block:: das // Bad — float can't represent 2^24 + 1 exactly @@ -443,6 +513,7 @@ Arguments of **class methods** are exempt: their signature is dictated by the base class or interface, so an unused parameter there is structural rather than a mistake. LINT012 fires on free functions only. +.. das-doc: fragment .. code-block:: das // Bad — `b` is never used @@ -468,16 +539,17 @@ The same check for the parameters of a block, lambda, or generator passed as a callback. Callbacks that ignore a parameter are common; suppress exactly as for LINT012. +.. das-doc: alt .. code-block:: das - // Bad — the callback ignores its second parameter - tab |> get(key) $(found : bool; value : int) { // LINT013 on value - report(found) + // Bad — the callback ignores its parameter + tab |> get(key) $(var value : int&) { // LINT013 on value + report(true) } // Good - tab |> get(key) $(found : bool; _value : int) { - report(found) + tab |> get(key) $(var _value : int&) { + report(true) } LINT014 — mutable (``var``) argument is never written @@ -502,6 +574,7 @@ which only ``var b`` provides. Slots that accept a const pointer is skipped — the leaf callee is flagged first; once its signature is fixed, the next lint run exposes the caller. +.. das-doc: alt .. code-block:: das // Bad — b is only read @@ -545,6 +618,7 @@ and unary, so a split binary ``a + b`` orphans as a valid unary statement. ``--`` mutate (real statements), and an operator that cannot begin a statement (``*``, ``|>``, …) raises a loud parse error instead. +.. das-doc: alt .. code-block:: das // Bad — `+ b` and `+ c` become separate `+b` / `+c` statements, dropped @@ -571,6 +645,7 @@ spelling is misleading. Use ordinary copy syntax for same-context storage, and make a cross-context copy explicit with ``clone_string`` in the receiving context: +.. das-doc: fragment .. code-block:: das dst = src @@ -604,6 +679,7 @@ string length, which carry an always-on guard. Either way the cast looks like it buys 64-bit range and buys nothing. Call the ``long_`` form, which is 64-bit the whole way through. +.. das-doc: alt .. code-block:: das // Bad — wraps before the cast ever runs @@ -633,6 +709,7 @@ overloads. An ``int(...)`` cast on the size or position argument of any of them is pure loss: above 2\ :sup:`31` it silently covers the wrong count while the overload would have taken the 64-bit value straight through. +.. das-doc: fragment .. code-block:: das // Bad — truncates for nbytes > 2GB @@ -674,6 +751,7 @@ the per-file scan cannot see which instantiations elsewhere consume it. Tag it w ``LINT019`` instead; removal is only safe where the author knows no other compile reaches the line. +.. das-doc: fragment .. code-block:: das // Bad — PERF006 no longer fires here; the directive outlived its rule hit @@ -692,6 +770,7 @@ starts. ``range64`` and ``urange64`` take the 64-bit value directly, and the loop variable then indexes arrays, fixed arrays and (unsafe) pointers as-is — ``TypeDecl::isIndexExt`` admits ``int64``/``uint64`` subscripts natively. +.. das-doc: fragment .. code-block:: das // Bad — n > 2^31 wraps before the loop starts @@ -724,6 +803,7 @@ is computed and thrown away at every sink, truncating silently above ``length()`` instead of ``long_length()``), or lift the sinks to 64-bit (``range64``, the ``int64`` ``resize``/``reserve``/``erase`` overloads). +.. das-doc: fragment .. code-block:: das // Bad — keep is 64-bit, yet every single use narrows @@ -766,6 +846,7 @@ PERF001 — string ``+=`` in loop String concatenation with ``+=`` inside a loop creates O(n\ :sup:`2`) allocations. Each iteration allocates a new string of increasing length, copying all previous content. +.. das-doc: alt .. code-block:: das // Bad — O(n^2) @@ -788,6 +869,7 @@ PERF002 — ``character_at`` in loop with loop variable to validate the index. In a loop iterating over string indices with the loop variable as the index, this becomes O(n\ :sup:`2`) total. +.. das-doc: alt .. code-block:: das // Bad — O(n^2) @@ -810,6 +892,7 @@ check by scanning to the index. For accessing the first character, use ``first_character`` which is O(1). For bulk access in hot paths, consider ``peek_data`` for reads or ``modify_data`` for mutations. +.. das-doc: alt .. code-block:: das let ch = character_at(s, 0) // PERF003 — use first_character(s) instead @@ -822,6 +905,7 @@ PERF004 — string interpolation reassignment in loop ``str += "..."``. Each iteration allocates a new string containing all previous content. +.. das-doc: alt .. code-block:: das // Bad — O(n^2) @@ -844,6 +928,7 @@ PERF005 — ``length(string)`` in while condition is not modified in the loop body, this is wasted work. Note that ``for`` loops do **not** have this problem because ``for`` computes its source expression once. +.. das-doc: alt .. code-block:: das // Bad — strlen every iteration @@ -872,6 +957,7 @@ Conditional pushes (inside ``if``/``else``) and loops with ``break``/``continue` are not flagged — the number of items is unpredictable, so ``reserve`` would be guesswork. +.. das-doc: alt .. code-block:: das // Bad — may realloc each iteration @@ -894,6 +980,7 @@ PERF007 — unnecessary ``string(das_string)`` in comparison ``das_string`` values via ``==`` and ``!=``. Wrapping in ``string()`` allocates a new string unnecessarily. +.. das-doc: fragment .. code-block:: das // Bad — unnecessary allocation @@ -908,6 +995,7 @@ PERF008 — unnecessary ``get_ptr()`` for ``is``/``as`` ``ExpressionPtr`` and ``TypeDeclPtr`` support ``is`` and ``as`` type checks directly. Calling ``get_ptr()`` first is unnecessary. +.. das-doc: fragment .. code-block:: das // Bad — get_ptr is redundant @@ -927,6 +1015,7 @@ The clone-init flavor — ``var x := src; return <- x`` (lowered to ``<- clone_to_move(...)``) — collapses to ``return clone_to_move(src)``, **not** ``return <- src`` (which would move/destroy the clone source). +.. das-doc: alt .. code-block:: das // Bad — redundant variable @@ -949,6 +1038,7 @@ PERF010 — unnecessary ``get_ptr()`` for null comparison ``smart_ptr`` supports ``==`` and ``!=`` against ``null`` directly. Calling ``get_ptr()`` first is unnecessary overhead. +.. das-doc: fragment .. code-block:: das // Bad — get_ptr is redundant @@ -963,6 +1053,7 @@ PERF011 — unnecessary ``get_ptr()`` for field access ``smart_ptr`` auto-dereferences for field access. Calling ``get_ptr()`` first to access a field is unnecessary. +.. das-doc: fragment .. code-block:: das // Bad — get_ptr is redundant @@ -979,6 +1070,7 @@ the ``strings`` module allocates a temporary string unnecessarily. Use ``peek(das_string)`` instead, which provides a zero-allocation read-only string reference. +.. das-doc: fragment .. code-block:: das // Bad — allocates a temporary string @@ -1000,6 +1092,7 @@ the canonical inc/dec idiom. Applies to the six numeric workhorse scalars (``int2``, ``float3``, …) do **not** support ``++``/``--`` so they are skipped. ``+= -1`` is also flagged (same effect as ``-= 1``). +.. das-doc: alt .. code-block:: das // Bad @@ -1036,6 +1129,7 @@ Deliberately **not** flagged: *intersection* with different endpoints, distinct from the ``||`` strict-inequality *complement* (``c < '0' || c > '9'``) which is flagged. +.. das-doc: fragment .. code-block:: das // Bad @@ -1056,6 +1150,7 @@ PERF015 — ternary min / max vec-friendly and the intent is clearer. All eight orientations of ``< / <= / > / >=`` × ``T==L,F==R`` / ``T==R,F==L`` are flagged. +.. das-doc: alt .. code-block:: das // Bad @@ -1074,6 +1169,7 @@ signed numeric type. Only the four orientations that match ``abs`` are flagged; the negabs shape (``x < 0 ? x : -x``) is **not** — it is a different function. +.. das-doc: alt .. code-block:: das // Bad @@ -1097,6 +1193,7 @@ idiomatic form. Six comparison ops are mapped to either ``empty(x)`` or Vector magnitude (``length(float3_var)`` from the math module) is **not** flagged — different semantics, no ``empty`` for vectors. +.. das-doc: fragment .. code-block:: das // Bad @@ -1124,6 +1221,7 @@ the existing ``find_expr_path`` chain walker. Every use of ``i`` in the body mus expression disqualifies the loop. Bare-variable sibling arrays indexed by the same ``i`` route the loop to PERF029 instead. +.. das-doc: alt .. code-block:: das // Bad — i used only as arr[i] @@ -1154,6 +1252,7 @@ cannot express those. Loops whose ``i`` never subscripts the ``range`` source itself also stay silent — there the source is only a bound, and zipping would change which array limits the walk. +.. das-doc: alt .. code-block:: das // Bad — xs and ys coupled through i @@ -1183,6 +1282,7 @@ lowering, and generated moves are excluded — in particular the early-out relocation that splits ``var inscope x <- init`` into a hoisted declaration plus a generated move (the target is fresh and the ``finally`` releases it). +.. das-doc: fragment .. code-block:: das // Bad — a's old array is dropped unreleased @@ -1218,9 +1318,14 @@ observe the rule firing on it. The lint runner sets Dastest coverage in ``utils/lint/tests/perf019_int_cast_collapse.das`` uses runtime operands; the constant case is covered by the CI lint gate. +.. das-doc: alt .. code-block:: das - bitfield Mode { read; write; exec } + bitfield Mode { + read + write + exec + } // Bad var mask = int(Mode.read) | int(Mode.write) // PERF019 @@ -1252,6 +1357,7 @@ trigger) combined with a strict ``arg._type.baseType`` equality check against the cast's target type. Const / reference / temporary qualifiers on the argument are ignored — only ``baseType`` matters. +.. das-doc: alt .. code-block:: das // Bad — a is already int64 @@ -1304,6 +1410,7 @@ If the argument base types differ (e.g. ``cond ? string(intV) : string(int64V)``), the rule does NOT fire; the rewrite would need a manual widen on one branch and that is left to the author. +.. das-doc: alt .. code-block:: das // Bad @@ -1349,6 +1456,7 @@ because those have no direct bulk equivalent. Compiler folds ``B |> push(s)``, ``B.push(s)``, and ``push(B, s)`` to the same call shape, so all three forms are detected by the same rule. +.. das-doc: alt .. code-block:: das // Bad @@ -1365,6 +1473,7 @@ and generator sources do not have a bulk overload and are left unflagged. The same recommendation applies to ``push_clone``: +.. das-doc: alt .. code-block:: das // Bad @@ -1390,6 +1499,7 @@ calls ``clone_expression`` on every ``$e(...)`` substitution input. Pre-cloning into a local variable and then splicing the local is wasted work — the same substitution gets cloned a second time at apply-template time. +.. das-doc: fragment .. code-block:: das // Bad @@ -1414,6 +1524,7 @@ each substitution independently, so ``$e(E)`` repeated N times yields N independent clones — equivalent to one user-side clone repeated N times via ``$e(X)``: +.. das-doc: fragment .. code-block:: das // Bad — three pre-clones for three splice slots @@ -1453,6 +1564,7 @@ whose only uses are direct arguments at ``[clone(...)]`` positions — any other use (assignment, passing elsewhere, storing into a field) makes the pre-clone load-bearing and the lint stays silent. +.. das-doc: fragment .. code-block:: das [clone(node)] @@ -1476,6 +1588,7 @@ string builder's ``DebugDataWalker``. Wrapping an element in ``string(...)`` allocates an intermediate string that the builder then copies — a wasted heap allocation per interpolation. +.. das-doc: alt .. code-block:: das // Bad @@ -1499,6 +1612,7 @@ but with an extra note: they interpolate as **hex** by default (``"{42u}"`` → ``0x2a``), so dropping the cast changes the output. Use the ``:d`` format tag to keep decimal: +.. das-doc: alt .. code-block:: das // Bad — string() gives decimal "42" @@ -1528,6 +1642,7 @@ declares a contract. They exist for code where an allocation, an environment lookup or a log line is a bug rather than a smell — a decode step, an audio callback, a frame loop. +.. das-doc: fragment .. code-block:: das [hot_path] // all three contracts @@ -1561,6 +1676,7 @@ read), and any builtin returning a freshly allocated string. reused is not an accident, and saying so at the buffer beats a suppression at every call site: +.. das-doc: fragment .. code-block:: das struct Session { @@ -1606,6 +1722,7 @@ STYLE001 — unnecessary ``<|`` pipe before block argument The ``<|`` pipe syntax is gen1 style and unnecessary in gen2. Use direct trailing block syntax instead. +.. das-doc: alt .. code-block:: das // Bad — gen1 pipe style @@ -1624,6 +1741,7 @@ STYLE002 — ``<|`` pipe before parameterless block When the block takes no parameters, both the ``<|`` pipe and ``$()`` are unnecessary. Use a direct trailing block. +.. das-doc: alt .. code-block:: das // Bad — pipe and $() both unnecessary @@ -1642,6 +1760,7 @@ STYLE003 — redundant ``$()`` on parameterless block When a block takes no parameters, the ``$()`` prefix is unnecessary even without a pipe. Use a bare trailing block. +.. das-doc: alt .. code-block:: das // Bad — redundant $() @@ -1667,6 +1786,7 @@ synthesized block and its inner terminator for both braceless ``if (c) return`` and postfix-desugared ``return X if (c)``, so a real user-written ``{...}`` is detectable as ``blk.at != inner.at``. +.. das-doc: alt .. code-block:: das // Bad — braces around a single terminator @@ -1692,6 +1812,7 @@ STYLE006 — ``string(__rtti)`` comparison should use ``is`` Comparing ``string(expr.__rtti) == "ExprFoo"`` is verbose and fragile. Use the ``is`` operator instead, which is type-safe and cleaner. +.. das-doc: fragment .. code-block:: das // Bad — manual RTTI string comparison @@ -1706,6 +1827,7 @@ STYLE010 — ``if (true)`` should be a bare block ``if (true)`` is always taken and adds unnecessary noise. Use a bare block (lexical scope) instead. +.. das-doc: alt .. code-block:: das // Bad — always true @@ -1728,6 +1850,7 @@ with initialization. The rule excludes ``var inscope`` (needs separate declaration for cleanup semantics), compiler-generated variables, and generic instantiations. +.. das-doc: fragment .. code-block:: das // Bad — split declaration and init @@ -1762,6 +1885,7 @@ array-literal equivalent. The rule excludes ``var inscope``, compiler-generated variables, and generic instantiations (same exclusions as STYLE011). +.. das-doc: fragment .. code-block:: das // Bad — two pushes after empty array declaration @@ -1799,6 +1923,7 @@ Foo()``, ``var a = new Foo()``). A non-empty constructor, a factory call, or a single field assignment is not flagged. ``var inscope``, compiler-generated variables, and generic instantiations are excluded. +.. das-doc: alt .. code-block:: das struct Foo { x : int; y : int } @@ -1829,6 +1954,7 @@ The block before the file's first AST decl (the module-leading docstring, e.g. ``daslib/regex_boost.das`` lines 9–18) is always allowed. Suppress an individual block on its first line: +.. das-doc: fragment .. code-block:: das // Bad — 5 contiguous //! lines on a public function @@ -1867,6 +1993,7 @@ comment prose inside a ``def private`` body is dead weight. Trim to one line, or suppress with ``// nolint:STYLE015`` on the first line of the block. +.. das-doc: alt .. code-block:: das def private bad() { @@ -1890,6 +2017,7 @@ with ``||``. Two AST shapes are detected: * two adjacent ``if (a) { return X }`` statements in the same block * the ``if (a) { return X } else if (b) { return X }`` chain +.. das-doc: alt .. code-block:: das // Bad @@ -1915,6 +2043,7 @@ both forms: * ``if (cond) return b1 else return b2`` (b1 ≠ b2) * ``if (cond) return b1`` immediately followed by ``return b2`` (b1 ≠ b2) +.. das-doc: alt .. code-block:: das // Bad @@ -1937,6 +2066,7 @@ Comparing a bool to a boolean literal is redundant — the bool already IS the value. Drop the comparison. Both Yoda forms (``true == flag``) are detected. +.. das-doc: fragment .. code-block:: das // Bad @@ -1957,6 +2087,7 @@ directly. Both orientations (and the mirror form) are detected — the inner call must resolve to the math module's ``min`` / ``max``, not a user overload. +.. das-doc: alt .. code-block:: das // Bad @@ -1983,6 +2114,7 @@ instantiation chain (two levels deep for json_boost's ``from_JV`` / ``json_boost``. The result-type check uses ``expr._type``, which is robust under both pre- and post-instantiation argument shapes. +.. das-doc: alt .. code-block:: das // Bad @@ -2008,6 +2140,7 @@ receiver resolves to the same variable. Computed keys disqualify the chain — such runs fall through to :ref:`STYLE031 ` instead (a table literal accepts computed keys, ``JV((...))`` does not). +.. das-doc: alt .. code-block:: das // Bad @@ -2041,9 +2174,14 @@ an explicit ``ExprOp1("~", ExprField)`` or a single-bit-complement ``ExprConstBitfield``. A bare ``foo &= BfT.m`` (no ``~``) is *not* the bit-clear idiom; it would mask off every other bit, so it stays silent. +.. das-doc: alt .. code-block:: das - bitfield Mode { read; write; exec } + bitfield Mode { + read + write + exec + } // Bad var f : Mode @@ -2073,9 +2211,14 @@ under normal compile as ``ExprConstBitfield`` with a single-bit mask. Multi-bit masks (``Mode.read | Mode.write``) are left alone since the ``!= 0`` semantics differ from any single field read. +.. das-doc: fragment .. code-block:: das - bitfield Mode { read; write; exec } + bitfield Mode { + read + write + exec + } struct Io { flags : Mode } // Bad @@ -2097,6 +2240,7 @@ The check walks the wrapped subtree for inherently-unsafe leaves writes, calls flagged ``unsafeOperation``). When none are present, the wrap is flagged. Macro-generated subtrees are skipped by design. +.. das-doc: alt .. code-block:: das // Bad — nothing inside needs unsafe @@ -2113,6 +2257,7 @@ unsafe, the block scope is too broad. Narrow it to the expression form ``unsafe()`` wrapping just the operation that requires it. When two or more statements need unsafe the block is justified and stays silent. +.. das-doc: alt .. code-block:: das // Bad — only the reinterpret needs unsafe @@ -2133,6 +2278,7 @@ redundant — the outer wrap already covers it. Drop the inner block. Closure, lambda, and generator bodies are not "nested" for this rule: they execute in a separate context the outer wrap does not reach. +.. das-doc: fragment .. code-block:: das // Bad — inner unsafe is already covered @@ -2162,6 +2308,7 @@ the variable (nested ``for`` / ``if`` filters allowed up to the rule's budget). ``var inscope``, compiler-generated variables, and generic instantiations are excluded. +.. das-doc: alt .. code-block:: das // Bad — empty var then a push-only loop @@ -2185,6 +2332,7 @@ Source inspection confirms the literal ``self->`` spelling before flagging — the post-inference AST cannot distinguish ``self->m()``, ``self.m()``, and bare ``m()``. +.. das-doc: fragment .. code-block:: das class Widget { @@ -2211,6 +2359,7 @@ an indirect dependency. Require those modules directly and drop ``X``. Skipped when ``X`` provides macros or an ``[init]`` (requiring it has a side effect beyond symbol visibility). +.. das-doc: fragment .. code-block:: das // Bad — only Y's symbols are used; X just re-exports Y @@ -2228,6 +2377,7 @@ re-exports) is referenced anywhere in the file. Remove it. Skipped when ``X`` provides any macro or an ``[init]``, or only re-exports builtins used through it. Suppress a deliberate keep with ``// nolint:STYLE030``. +.. das-doc: alt .. code-block:: das // Bad — nothing from strings is used @@ -2255,6 +2405,7 @@ keys at compile time (error 30706), so the rewrite would not compile. ``table`` runs with constant keys are owned by STYLE021 (the ``JV((k1=..., k2=...))`` form is the stronger suggestion). +.. das-doc: alt .. code-block:: das // Bad @@ -2279,6 +2430,7 @@ An empty ``var w : array`` immediately followed by a single an ``array`` is just a verbose clone of ``src``. ``:=`` clones the whole array (each element, for ``push_clone_from``) in one step: +.. das-doc: fragment .. code-block:: das // Bad @@ -2314,6 +2466,7 @@ expression (``concat`` lives in ``daslib/linq``, so the rewrite needs ``require daslib/linq``). Into an **already-live** array the run collapses to a single variadic call: +.. das-doc: fragment .. code-block:: das require daslib/linq @@ -2355,6 +2508,7 @@ STYLE034 — ``reinterpret(addr(x))`` collapses to ``addr(x)`` ``addr(x)`` is pure sugar for ``reinterpret(addr(x))``, with one ``unsafe()`` covering both halves — the spelled-out form needs two gates: +.. das-doc: alt .. code-block:: das // Bad — two unsafe gates for one operation @@ -2378,6 +2532,7 @@ them once it consumes them. A cast target that is already concrete has nothing to consume the contract, so it does nothing at all — ``void?`` is ``void?`` regardless of ``-const``. +.. das-doc: alt .. code-block:: das // Bad — the -const strips nothing @@ -2415,6 +2570,7 @@ Not counted: ``&&`` / ``||`` short-circuit operators, the null-safe chain operators ``??`` / ``?.`` / ``?[`` / ``?as``, ``static_if`` branches, and macro-generated control flow. +.. das-doc: fragment .. code-block:: das // Bad — one function absorbing every case @@ -2453,6 +2609,7 @@ default to 50 (ESLint ``max-lines-per-function``, SwiftLint warning), 60 (golangci-lint ``funlen``, detekt ``LongMethod``) and 150 (Checkstyle ``MethodLength``). +.. das-doc: alt .. code-block:: das // Bad — one function carrying a hundred lines of straight-line work diff --git a/doc/source/reference/language/macros.rst b/doc/source/reference/language/macros.rst index 0b9687021f..2537b8b413 100644 --- a/doc/source/reference/language/macros.rst +++ b/doc/source/reference/language/macros.rst @@ -7,6 +7,14 @@ Macros In Daslang, macros are the machinery that allow direct manipulation of the syntax tree. Macros are exposed via the :ref:`daslib/ast ` module and :ref:`daslib/ast_boost ` helper module. +Every example on this page assumes both are required, and lives in a file which declares a +:ref:`module ` — a file that carries macros has to be one: + +.. das-doc: given module doc_macros +.. code-block:: das + + require daslib/ast + require daslib/ast_boost Macros are evaluated at compilation time during different compilation passes. Macros assigned to a specific module are evaluated as part of the module every time that module is included. @@ -89,12 +97,9 @@ For example, this is how this lifetime cycle is implemented for the reader macro .. code-block:: das - def add_new_reader_macro ( name:string; someClassPtr ) { - var ann <- make_reader_macro(name, someClassPtr) + def add_new_reader_macro ( name:string; var someClassPtr ) { + var ann = make_reader_macro(name, someClassPtr) this_module() |> add_reader_macro(ann) - unsafe { - delete ann - } } --------------------- @@ -109,6 +114,7 @@ There is additionally the ``[function_macro]`` annotation which accomplishes the ``AstFunctionAnnotation`` allows several different manipulations: +.. das-doc: signatures .. code-block:: das class AstFunctionAnnotation { @@ -170,25 +176,28 @@ Annotations that perform purely structural rewrites (no type information needed) example, ``[class_method]`` injecting a ``self`` argument — return true so that the rewrite happens once on the template and every instantiation inherits it. -Lets review the following example from ``ast_boost`` of how the ``macro`` annotation is implemented: +Lets review the following example from ``ast_boost`` of how the ``macro`` annotation is implemented +(the excerpt is quoted from that module — a second declaration of ``MacroMacro`` cannot coexist with +it in one program): +.. das-doc: fragment .. code-block:: das class MacroMacro : AstFunctionAnnotation { def override apply ( var func:FunctionPtr; var group:ModuleGroup; args:AnnotationArgumentList; var errors : das_string ) : bool { - compiling_program().flags |= ProgramFlags.needMacroModule - func.flags |= FunctionFlags.init + compiling_program().flags.needMacroModule = true + func.flags.macroInit = true var blk = new ExprBlock(at=func.at) var ifm = new ExprCall(at=func.at, name:="is_compiling_macros") var ife = new ExprIfThenElse(at=func.at, cond=ifm, if_true=func.body) - push(blk.list,ife) + emplace(blk.list,ife) func.body = blk return true } } During the ``apply`` pass the function body is appended with the ``if is_compiling_macros()`` closure. -Additionally, the ``init`` flag is set, which is equivalent to a ``_macro`` annotation. +Additionally, the ``macroInit`` flag is set, which is what the ``_macro`` annotation itself sets. Functions annotated with ``[macro]`` are evaluated during module compilation. ------------------ @@ -197,6 +206,7 @@ AstBlockAnnotation ``AstBlockAnnotation`` is used to manipulate block expressions (blocks, lambdas, local functions): +.. das-doc: signatures .. code-block:: das class AstBlockAnnotation { @@ -217,6 +227,7 @@ AstStructureAnnotation The ``AstStructureAnnotation`` macro lets you manipulate structure or class definitions via annotation: +.. das-doc: signatures .. code-block:: das class AstStructureAnnotation { @@ -224,6 +235,9 @@ The ``AstStructureAnnotation`` macro lets you manipulate structure or class defi def abstract finish ( var st:StructurePtr; var group:ModuleGroup; args:AnnotationArgumentList; var errors : das_string ) : bool def abstract patch ( var st:StructurePtr; var group:ModuleGroup; args:AnnotationArgumentList; var errors : das_string; var astChanged:bool& ) : bool def abstract complete ( var st:StructurePtr; var ctx:smart_ptr ) : void + def abstract aotPrefix ( var st:StructurePtr; args:AnnotationArgumentList; var writer:StringBuilderWriter ) : void + def abstract aotBody ( var st:StructurePtr; args:AnnotationArgumentList; var writer:StringBuilderWriter ) : void + def abstract aotSuffix ( var st:StructurePtr; args:AnnotationArgumentList; var writer:StringBuilderWriter ) : void } ``add_new_structure_annotation`` adds a structure annotation to a module. @@ -238,6 +252,9 @@ After this, the structure is fully inferred and defined and can no longer be mod ``complete`` is invoked during the ``simulate`` portion of context creation. At this point Context is available. +``aotPrefix``, ``aotBody`` and ``aotSuffix`` write C++ text into the generated AOT code: before the +generated ``struct``, inside its body, and after its closing brace. + An example of such annotation is ``SetupAnyAnnotation`` from :ref:`daslib/ast_boost `. ------------------------ @@ -246,6 +263,7 @@ AstEnumerationAnnotation The ``AstEnumerationAnnotation`` macro lets you manipulate enumerations via annotation: +.. das-doc: signatures .. code-block:: das class AstEnumerationAnnotation { @@ -257,10 +275,17 @@ 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: +An enumeration macro is a class registered with ``[enumeration_macro]``. It lives in its own +module, because the annotation it registers has to exist before the file that uses it is parsed: +.. das-doc: file enum_macro_mod.das .. code-block:: das + module enum_macro_mod public + + require daslib/ast + require daslib/ast_boost + [enumeration_macro(name="enum_total")] class EnumTotalAnnotation : AstEnumerationAnnotation { def override apply(var enu : EnumerationPtr; var group : ModuleGroup; @@ -271,8 +296,14 @@ In gen2 syntax, register an enumeration macro and annotate enums with: } } +A file which requires that module can then annotate its enumerations: + +.. code-block:: das + + require enum_macro_mod + [enum_total] - enum Direction { North; South; East; West } + enum Direction { North, South, East, West } .. seealso:: @@ -293,6 +324,7 @@ There is additionally the ``[variant_macro]`` annotation which accomplishes the Each of the 3 transformations are covered in the appropriate abstract function: +.. das-doc: signatures .. code-block:: das class AstVariantMacro { @@ -303,6 +335,7 @@ Each of the 3 transformations are covered in the appropriate abstract function: Let's review the following example from :ref:`daslib/ast_boost `: +.. das-doc: fragment .. code-block:: das // replacing ExprIsVariant(value,name) => ExprOp2("==", value.__rtti, "name") @@ -344,6 +377,7 @@ There is additionally the ``[reader_macro]`` annotation, which essentially autom Reader macros accept characters, collect them if necessary, and produce output via one of two patterns: +.. das-doc: signatures .. code-block:: das class AstReaderMacro { @@ -357,12 +391,22 @@ The ``accept`` function notifies the correct terminator of the character sequenc .. code-block:: das + require arr_macro_mod // the module below, which registers `arr` + var x = %arr~\{\}\w\x\y\n%% // invoking reader macro arr, %% is a terminator -Consider the implementation for the example above: +Consider the implementation for the example above. Like every macro, it lives in its own +module, so that the reader macro is registered before the file using it is parsed: +.. das-doc: file arr_macro_mod.das .. code-block:: das + module arr_macro_mod public + + require daslib/ast + require daslib/ast_boost + require strings + [reader_macro(name="arr")] class ArrayReader : AstReaderMacro { def override accept ( prog:ProgramPtr; mod:Module?; var expr:ExprReader?; ch:int; info:LineInfo ) : bool { @@ -377,9 +421,8 @@ Consider the implementation for the example above: } def override visit ( prog:ProgramPtr; mod:Module?; expr:ExprReader? ) : ExpressionPtr { let seqStr = string(expr.sequence) - var arrT = new TypeDecl(baseType=Type.tInt) - push(arrT.dim,length(seqStr)) - var mkArr = new ExprMakeArray(at = expr.at, makeType <- arrT) + var arrT = make_fixed_array_type(length(seqStr), new TypeDecl(baseType=Type.tInt)) + var mkArr = new ExprMakeArray(at = expr.at, makeType = arrT) for ( x in seqStr ) { var mkC = new ExprConstInt(at=expr.at, value=x) push(mkArr.values,mkC) @@ -408,6 +451,7 @@ Reader macros are normally invoked with the ``~`` separator (``%name~ ... %%``). **inline** form that uses a ``!`` separator (``%name! ... %%``) and runs ``suffix`` **in expression position**: +.. das-doc: fragment .. code-block:: das var total = %sum! 1, 2, 3 %% // rewrites to ( 1 + 2 + 3 ), re-parsed in place @@ -433,17 +477,19 @@ It occurs during the infer pass. ``add_new_call_macro`` adds a call macro to a module. The ``[call_macro]`` annotation automates the same thing: - .. code-block:: das +.. das-doc: signatures +.. code-block:: das - class AstCallMacro { - def abstract preVisit ( prog:ProgramPtr; mod:Module?; expr:ExprCallMacro? ) : void - def abstract visit ( prog:ProgramPtr; mod:Module?; expr:ExprCallMacro? ) : ExpressionPtr - def abstract canVisitArgument ( expr:ExprCallMacro?; argIndex:int ) : bool - def abstract canFoldReturnResult ( expr:ExprCallMacro? ) : bool - } + class AstCallMacro { + def abstract preVisit ( prog:ProgramPtr; mod:Module?; expr:ExprCallMacro? ) : void + def abstract visit ( prog:ProgramPtr; mod:Module?; expr:ExprCallMacro? ) : ExpressionPtr + def abstract canVisitArgument ( expr:ExprCallMacro?; argIndex:int ) : bool + def abstract canFoldReturnResult ( expr:ExprCallMacro? ) : bool + } ``apply`` from :ref:`daslib/apply ` is an example of such a macro: +.. das-doc: fragment .. code-block:: das [call_macro(name="apply")] // apply(value, block) @@ -470,16 +516,20 @@ AstPassMacro ``AstPassMacro`` is one macro to rule them all. It gets the entire program as input and can be invoked at numerous passes: +.. das-doc: signatures .. code-block:: das class AstPassMacro { def abstract apply(prog : ProgramPtr; mod : Module?) : bool } -Five annotations control when a pass macro runs: +Seven 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. +- ``[pre_infer_macro]`` — before each inference leg, on the not-yet-inferred tree. +- ``[post_infer_macro]`` — once inference is finished, before the tree is consumed + (access flags, lint, each optimisation round). - ``[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. @@ -501,6 +551,7 @@ AstTypeMacro ``AstTypeMacro`` lets you define custom type expressions resolved during type inference. It has a single method: +.. das-doc: signatures .. code-block:: das class AstTypeMacro { @@ -512,19 +563,19 @@ The ``[type_macro(name="…")]`` annotation automates registration. The compiler parses invocations like ``name(type, N)`` in type position into a ``TypeDecl`` with ``baseType = Type.typeMacro``. The arguments are -stored in ``td.dimExpr``: +stored in ``td.typeMacroExpr``: -- ``dimExpr[0]`` — ``ExprConstString`` with the macro name -- ``dimExpr[1..]`` — user arguments (``ExprTypeDecl`` for types, +- ``typeMacroExpr[0]`` — ``ExprConstString`` with the macro name +- ``typeMacroExpr[1..]`` — user arguments (``ExprTypeDecl`` for types, ``ExprConstInt`` for integers, etc.) ``visit()`` is called in two contexts: - **Concrete** — all types are inferred; ``passT`` is null; - ``dimExpr[i]._type`` is the resolved type. + ``typeMacroExpr[i]._type`` is the resolved type. - **Generic** — type parameters like ``auto(TT)`` are unresolved; ``passT`` carries the actual argument type for matching; - ``dimExpr[i]._type`` is null. + ``typeMacroExpr[i]._type`` is null. .. seealso:: @@ -537,6 +588,7 @@ AstTypeInfoMacro ``AstTypeInfoMacro`` is designed to implement custom type information inside a typeinfo expression: +.. das-doc: signatures .. code-block:: das class AstTypeInfoMacro { @@ -571,6 +623,7 @@ AstForLoopMacro ``AstForLoopMacro`` is designed to implement custom processing of for loop expressions: +.. das-doc: signatures .. code-block:: das class AstForLoopMacro { @@ -588,6 +641,7 @@ AstCaptureMacro ``AstCaptureMacro`` is designed to implement custom capturing and finalization of lambda expressions: +.. das-doc: signatures .. code-block:: das class AstCaptureMacro { @@ -623,6 +677,7 @@ AstCommentReader ``AstCommentReader`` is designed to implement custom processing of comment expressions: +.. das-doc: signatures .. code-block:: das class AstCommentReader { @@ -640,8 +695,24 @@ AstCommentReader def abstract afterGlobalVariable ( name:string; prog:ProgramPtr; mod:Module?; info:LineInfo ) : void def abstract afterGlobalVariables ( prog:ProgramPtr; mod:Module?; info:LineInfo ) : void def abstract beforeVariant ( prog:ProgramPtr; mod:Module?; info:LineInfo ) : void + def abstract beforeVariantEntries ( prog:ProgramPtr; mod:Module?; info:LineInfo ) : void + def abstract afterVariantEntry ( name:string; prog:ProgramPtr; mod:Module?; info:LineInfo ) : void + def abstract afterVariantEntries ( prog:ProgramPtr; mod:Module?; info:LineInfo ) : void def abstract afterVariant ( name:string; prog:ProgramPtr; mod:Module?; info:LineInfo ) : void + def abstract beforeTuple ( prog:ProgramPtr; mod:Module?; info:LineInfo ) : void + def abstract beforeTupleEntries ( prog:ProgramPtr; mod:Module?; info:LineInfo ) : void + def abstract afterTupleEntry ( name:string; prog:ProgramPtr; mod:Module?; info:LineInfo ) : void + def abstract afterTupleEntries ( prog:ProgramPtr; mod:Module?; info:LineInfo ) : void + def abstract afterTuple ( name:string; prog:ProgramPtr; mod:Module?; info:LineInfo ) : void + def abstract beforeBitfield ( prog:ProgramPtr; mod:Module?; info:LineInfo ) : void + def abstract beforeBitfieldEntries ( prog:ProgramPtr; mod:Module?; info:LineInfo ) : void + def abstract afterBitfieldEntry ( name:string; prog:ProgramPtr; mod:Module?; info:LineInfo ) : void + def abstract afterBitfieldEntries ( prog:ProgramPtr; mod:Module?; info:LineInfo ) : void + def abstract afterBitfield ( name:string; prog:ProgramPtr; mod:Module?; info:LineInfo ) : void def abstract beforeEnumeration ( prog:ProgramPtr; mod:Module?; info:LineInfo ) : void + def abstract beforeEnumerationEntries ( prog:ProgramPtr; mod:Module?; info:LineInfo ) : void + def abstract afterEnumerationEntry ( name:string; prog:ProgramPtr; mod:Module?; info:LineInfo ) : void + def abstract afterEnumerationEntries ( prog:ProgramPtr; mod:Module?; info:LineInfo ) : void def abstract afterEnumeration ( name:string; prog:ProgramPtr; mod:Module?; info:LineInfo ) : void def abstract beforeAlias ( prog:ProgramPtr; mod:Module?; info:LineInfo ) : void def abstract afterAlias ( name:string; prog:ProgramPtr; mod:Module?; info:LineInfo ) : void @@ -669,9 +740,14 @@ There is additionally the ``[comment_reader]`` annotation, which essentially aut ``afterGlobalVariable`` occurs after each individual global variable declaration. ``beforeVariant`` and ``afterVariant`` occur before and after each variant declaration, regardless of if it has comments. +The same pair exists for tuples (``beforeTuple`` / ``afterTuple``) and bitfields (``beforeBitfield`` / ``afterBitfield``). ``beforeEnumeration`` and ``afterEnumeration`` occur before and after each enumeration declaration, regardless of if it has comments. +Variants, tuples, bitfields and enumerations additionally report their bodies: ``beforeEntries`` +and ``afterEntries`` bracket the entry list, and ``afterEntry`` occurs after each single +entry, carrying its name. + ``beforeAlias`` and ``afterAlias`` occur before and after each alias type declaration, regardless or if it has comments. ---------------- @@ -680,6 +756,7 @@ AstSimulateMacro ``AstSimulateMacro`` is designed to customize the simulation of the program: +.. das-doc: signatures .. code-block:: das class AstSimulateMacro { @@ -698,6 +775,7 @@ AstVisitor ``AstVisitor`` implements the visitor pattern for the Daslang expression tree. It contains a callback for every single expression in prefix and postfix form, as well as some additional callbacks: +.. das-doc: signatures .. code-block:: das class AstVisitor { @@ -710,24 +788,31 @@ It contains a callback for every single expression in prefix and postfix form, a Postfix callbacks can return expressions to replace the ones passed to the callback. -PrintVisitor from the ``ast_print`` example implements the printing of every single expression in Daslang syntax. +``PrintVisitor`` from ``daslib/ast_print`` implements the printing of every single expression in Daslang syntax. ``make_visitor`` creates a visitor adapter from the class, derived from ``AstVisitor``. The adapter then can be applied to a program via the ``visit`` function: .. code-block:: das + require daslib/ast_print + var astVisitor = new PrintVisitor() make_visitor(*astVisitor) $ (astVisitorAdapter) { visit(this_program(), astVisitorAdapter) } -If an expression needs to be visited, and can potentially be fully substituted, the ``visit_expression`` function should be used: +If an expression needs to be visited, and can potentially be fully substituted, the +``visit_expression`` function from ``daslib/templates_boost`` should be used. It takes the +expression by reference and replaces it in place, so there is nothing to assign back: +.. das-doc: given var expr : ExpressionPtr .. code-block:: das + require daslib/templates_boost + make_visitor(*astVisitor) $ (astVisitorAdapter) { - expr <- visit_expression(expr,astVisitorAdapter) + visit_expression(expr,astVisitorAdapter) } --------------------- diff --git a/doc/source/reference/language/modules.rst b/doc/source/reference/language/modules.rst index 9449f0dd91..36f43fcad2 100644 --- a/doc/source/reference/language/modules.rst +++ b/doc/source/reference/language/modules.rst @@ -34,6 +34,7 @@ starts with one of the three recognized prefixes (``./``, ``../``, ``%/``) and ends in ``.das`` or ``.das_project``. Anything else continues to resolve as a module name through the normal path. ``%`` expands to ``get_das_root()``. +.. das-doc: fragment .. code-block:: das require ./helpers.das // relative to the current file @@ -120,17 +121,20 @@ Native modules A native module is a separate Daslang file, with an optional ``module`` name: +.. das-doc: file custom.das .. code-block:: das - module custom // specifies module name - ... - def foo // defines function in module - ... + module custom // specifies module name + + def public foo { // defines function in module + ... + } If not specified, the module name defaults to that of the file name. Modules can be ``private`` or ``public``: +.. das-doc: fragment .. code-block:: das module Foo private @@ -143,6 +147,7 @@ The default publicity of functions, structures, and enumerations is that of the Module can be made visible to all modules in the project via the ``!inscope`` modifier: +.. das-doc: fragment .. code-block:: das module Foo !inscope @@ -160,6 +165,7 @@ Shared modules Shared modules are modules that are shared between compilation of multiple contexts. Typically, modules are compiled anew for each context, but when the 'shared' keyword is specified, the module gets promoted to a builtin module: +.. das-doc: fragment .. code-block:: das module Foo shared @@ -191,6 +197,7 @@ functions in the module that calls them. Inside an instanced generic, the module being compiled` is therefore the **caller's** module — neither prefix pins a lookup to the module where the generic was written: +.. das-doc: file b.das .. code-block:: das module b diff --git a/doc/source/reference/language/move_copy_clone.rst b/doc/source/reference/language/move_copy_clone.rst index 40c8a8a0a1..44d589b7ea 100644 --- a/doc/source/reference/language/move_copy_clone.rst +++ b/doc/source/reference/language/move_copy_clone.rst @@ -50,6 +50,7 @@ be copied. ``lambda`` *is* copyable (a copy aliases the capture frame, the same raw pointer copy aliases its target), but ``delete lam`` then requires ``unsafe``. Attempting to copy a non-copyable type produces: +.. das-doc: skip .. code-block:: das // error: this type can't be copied, use move (<-) or clone (:=) instead @@ -65,6 +66,7 @@ By default, the compiler automatically promotes ``=`` to ``<-`` when: This means you can often write ``=`` and the compiler will do the right thing: +.. das-doc: given def get_data : array { return <- [1, 2] } .. code-block:: das var a : array @@ -100,7 +102,7 @@ Use ``<-`` when: - You are initializing a variable from a function return value - You are passing ownership into a struct field or container -:: +.. code-block:: das def make_data() : array { var result : array @@ -255,6 +257,7 @@ Variable Initialization The three initialization forms correspond to the three operators: +.. das-doc: given var expr : string .. code-block:: das var x = expr // copy initialization @@ -264,6 +267,7 @@ The three initialization forms correspond to the three operators: For local variable declarations, the compiler checks the type and reports an error if the chosen initialization mode is not supported: +.. das-doc: fragment .. code-block:: das var a = get_array() // error[30197] if relaxed_assign is false: @@ -348,16 +352,21 @@ Multiple captures are separated by commas: .. code-block:: das - return @ capture(= a, <- arr, := table) () { - // a is copied, arr is moved, table is cloned + def make_multi(a : int) { + var arr : array + var tab : table + return @ capture(= a, <- arr, := tab) () { + // a is copied, arr is moved, tab is cloned + } } Generators also support captures: .. code-block:: das - var g <- generator capture(= a) { - for (x in range(1, a)) { + var limit = 5 + var g <- generator capture(= limit) { + for (x in range(1, limit)) { yield x } return false @@ -416,6 +425,7 @@ Custom Clone You can define a custom clone function for any type. If a custom clone exists, it is called by the ``:=`` operator regardless of whether the type is natively cloneable: +.. das-doc: given def open_new_socket : int { return 42 } .. code-block:: das struct Connection { diff --git a/doc/source/reference/language/pattern_matching.rst b/doc/source/reference/language/pattern_matching.rst index 3bd870098e..47548c31f9 100644 --- a/doc/source/reference/language/pattern_matching.rst +++ b/doc/source/reference/language/pattern_matching.rst @@ -8,6 +8,11 @@ Pattern matching allows you to compare a value against a set of structural patte fields when a pattern matches. In Daslang, pattern matching is implemented via macros in the ``daslib/match`` module. +``match`` is a statement, not an expression. Each arm is a block, and a value leaves the +match through a ``return`` (or an assignment) inside that arm. ``return match ( x ) { ... }`` +is an error — the compiler reports ``error[30220]`` and asks for the match to be written as a +statement whose arms return. + Enumeration Matching -------------------- @@ -16,6 +21,8 @@ The ``_`` pattern is a catch-all that matches anything not covered by previous c .. code-block:: das + require daslib/match + enum Color { Black Red @@ -291,6 +298,7 @@ The ``||`` operator matches either of the provided patterns. Both sides must dec The ``[match_as_is]`` annotation enables pattern matching for structures of different types, provided the necessary ``is`` and ``as`` operators have been implemented: +.. das-doc: given struct Cmd { rtti : string } .. code-block:: das [match_as_is] @@ -331,7 +339,7 @@ The required ``is`` and ``as`` operators: return default } -With these operators in place, you can match against ``CmdMove`` in a ``match`` expression: +With these operators in place, you can match against ``CmdMove`` in a ``match`` statement: .. code-block:: das @@ -401,6 +409,8 @@ capture, and nested node patterns recurse: .. code-block:: das + require daslib/ast_boost + def classify ( e : ExpressionPtr ) { match ( e ) { if ( ExprOp2(op="+", left=ExprOp2(op="*", left=$v(a), right=$v(b)), right=$v(c)) ) { @@ -429,6 +439,7 @@ Static Matching ``static_match`` works like ``match``, but ignores patterns with type mismatches at compile time instead of reporting errors. This makes it suitable for generic functions: +.. das-doc: skip .. code-block:: das static_match ( match_expression ) { @@ -476,6 +487,7 @@ match_type The ``match_type`` subexpression matches based on the type of an expression: +.. das-doc: skip .. code-block:: das if ( match_type(type, expr) ) { diff --git a/doc/source/reference/language/pointers.rst b/doc/source/reference/language/pointers.rst index e2f26aa4c8..4a9c968353 100644 --- a/doc/source/reference/language/pointers.rst +++ b/doc/source/reference/language/pointers.rst @@ -29,6 +29,7 @@ Type Description Pointer types are declared by appending ``?`` to any type: +.. das-doc: given struct Point { x, y : float } .. code-block:: das var p : int? // pointer to int — null by default @@ -71,12 +72,12 @@ addr ``addr(x)`` returns a pointer to an existing variable. **Requires unsafe.** -:: +.. code-block:: das - var x = 42 + var n = 42 unsafe { - var p = addr(x) // p is int? - *p = 100 // modifies x + var pn = addr(n) // pn is int? + *pn = 100 // modifies n } The pointer is valid only while the variable is alive — using it after @@ -93,8 +94,8 @@ local or global variable (not a field of a temporary): require daslib/safe_addr var a = 13 - var p = safe_addr(a) // p is int?# (temporary pointer) - print("{*p}\n") + var sp = safe_addr(a) // sp is int?# (temporary pointer) + print("{*sp}\n") Temporary pointers cannot be stored in containers or returned from functions. @@ -157,8 +158,14 @@ Safe navigation ``?.`` .. code-block:: das - p?.x // returns x if p is non-null, null otherwise - a?.b?.c // chains — short-circuits on first null + struct Segment { + head : Point? + } + + var seg : Segment? + + seg?.head // returns head if seg is non-null, null otherwise + seg?.head?.x // chains — short-circuits on first null Safe navigation results are themselves nullable, so combine with ``??`` for a concrete fallback: @@ -170,17 +177,21 @@ for a concrete fallback: Null coalescing ``??`` ^^^^^^^^^^^^^^^^^^^^^^ -``??`` provides a default value when the left side is null: +``??`` dereferences the pointer on the left, and falls back to the value on +the right when that pointer is null. The default is therefore a value of the +**pointee** type — not another pointer — and so is the result: .. code-block:: das - let x = p ?? default_value + var origin = Point(x = 0.0, y = 0.0) + let pos = p ?? origin // Point; origin if p is null -For a pointer with an integer/scalar pointee, coalesce the pointer itself: +The same holds for a pointer with an integer or other scalar pointee: .. code-block:: das - let x = p ?? 0 // 0 if p is null + var pi : int? + let count = pi ?? 0 // int; 0 if pi is null .. _pointer_delete: @@ -190,11 +201,11 @@ Deletion ``delete`` frees heap memory and sets the pointer to null. **Requires unsafe.** -:: +.. code-block:: das - var p = new Point() + var dp = new Point() unsafe { - delete p // frees memory, p becomes null + delete dp // frees memory, dp becomes null } Prefer ``var inscope`` for automatic cleanup — it adds a ``finally`` block @@ -233,11 +244,15 @@ Indexing Increment and addition ^^^^^^^^^^^^^^^^^^^^^^ -:: +.. das-doc: alt +.. code-block:: das + var data <- [10, 20, 30, 40, 50] unsafe { - ++ p // advance pointer by one element - p += 3 // advance by three elements + var p = addr(data[0]) + ++ p // advance pointer by one element + p += 3 // advance by three elements + print("{*p}\n") // 50 } .. warning:: @@ -300,11 +315,11 @@ to a typed pointer before dereferencing: .. code-block:: das unsafe { - var x = 123 - var px = addr(x) - var vp : void? = reinterpret(px) // erase type - var px2 = reinterpret(vp) // restore type - print("{*px2}\n") // 123 + var raw = 123 + var praw = addr(raw) + var vraw : void? = reinterpret(praw) // erase type + var praw2 = reinterpret(vraw) // restore type + print("{*praw2}\n") // 123 } .. _pointer_intptr: @@ -344,9 +359,10 @@ Can also cast between pointer types: .. code-block:: das unsafe { - var p : int? = addr(x) - var vp = reinterpret(p) // to void? - var p2 = reinterpret(vp) // back to int? + var i = 7 + var ip : int? = addr(i) + var ivp = reinterpret(ip) // to void? + var ip2 = reinterpret(ivp) // back to int? } .. _pointer_typeinfo: @@ -362,7 +378,11 @@ Several ``typeinfo`` queries test pointer properties at compile time: typeinfo is_pointer(p) // true if p is a pointer type typeinfo is_smart_ptr(p) // true if p is a smart_ptr typeinfo is_void_pointer(p) // true if p is void? - typeinfo can_delete_ptr(p) // true if delete is valid for p + typeinfo can_delete(p) // true if delete is valid for p + +``can_delete_ptr`` asks the same question of a **pointee** type rather than of +a pointer, so it is written against the dereferenced value — +``typeinfo can_delete_ptr(*p)`` is true exactly when ``delete p`` is legal. .. _pointer_summary: diff --git a/doc/source/reference/language/program_structure.rst b/doc/source/reference/language/program_structure.rst index 731755043b..211356f491 100644 --- a/doc/source/reference/language/program_structure.rst +++ b/doc/source/reference/language/program_structure.rst @@ -69,6 +69,8 @@ Module Declaration The ``module`` declaration names the current file's module: +.. das-doc: fragment + .. code-block:: das module my_module @@ -85,6 +87,8 @@ The ``module`` declaration supports several modifiers: Promotes the module to a built-in module. Only one instance is created per compilation environment, and it is shared across contexts: + .. das-doc: fragment + .. code-block:: das module my_lib shared @@ -93,6 +97,8 @@ The ``module`` declaration supports several modifiers: Sets the default visibility of all declarations in the module. Functions, structs, enums, and globals inherit this default unless they specify their own visibility: + .. das-doc: fragment + .. code-block:: das module my_lib public // all declarations are public by default @@ -104,11 +110,16 @@ The ``module`` declaration supports several modifiers: Makes the module visible to all modules in the project without an explicit ``require``. This uses the ``!inscope`` syntax: + .. das-doc: fragment + .. code-block:: das module my_lib !inscope -Modifiers can be combined: +Modifiers can be combined, in the order ``shared``, then ``public``/``private``, then +``!inscope``: + +.. das-doc: fragment .. code-block:: das @@ -144,21 +155,36 @@ that requires the current one: Aliasing ^^^^^^^^^^^^^^^^^^^ -The ``as`` keyword gives a required module a local qualifier of your choosing: +The ``as`` keyword gives a required module a local qualifier of your choosing. Given a +sibling module file ``event.das``: + +.. das-doc: file event.das + +.. code-block:: das + + module event + + def public process { + print("event\n") + } + +a require can bind it to any local name: .. code-block:: das - require ./sub/event.das as sub_event + require ./event.das as sub_event def handle { sub_event::process() // qualified call using the alias } -Aliasing only applies to **path** requires — those beginning with ``./``, ``../`` or ``%/`` -and naming the ``.das`` file explicitly. Under the default resolver a module-name require -registers no qualifier at all, so ``require daslib/random as rng`` compiles but silently -leaves ``rng::`` undefined; use the path form ``require %/daslib/random.das as rng`` when an -alias is needed. +An explicit ``as`` alias registers for every require form, module-name requires +(``require daslib/random as rng``) included. Without ``as``, a **path** require — one +beginning with ``./``, ``../`` or ``%/`` and naming the ``.das`` file explicitly — still +registers the file stem as a qualifier, so ``event::process()`` resolves in the example +above as well; a module-name require registers no implicit qualifier. In either case the +module's own declared name keeps working as a qualifier, so ``random::random_seed(seed)`` +resolves whether or not an alias was given. (see :ref:`Modules ` for details on module function visibility and the ``_`` / ``__`` module prefixes). @@ -212,6 +238,8 @@ Visibility Each top-level declaration can be marked ``public`` or ``private``: +.. das-doc: alt + .. code-block:: das def public helper(x : int) : int { // visible to other modules @@ -288,7 +316,9 @@ attributes: pass } -The option ``no_init`` disables all ``[init]`` functions. +The option ``no_init`` forbids ``[init]`` outright — with it set, declaring an ``[init]`` +(or ``[finalize]``) function, or giving a global a non-constant initializer, is +``error[30164]``. ^^^^^^^^^^^^^^^^^^^ [finalize] @@ -308,55 +338,58 @@ no arguments, no return value: Expect Declaration -------------------- -The ``expect`` declaration is used in test files to declare expected compilation errors. -When present, the compiler treats the listed errors as intentional — the file compiles -"successfully" only if exactly those errors (and no others) are produced. +The ``expect`` declaration records the compilation errors a file is *supposed* to produce. +It does not silence them: the compiler still reports each error and still fails the file. +What ``expect`` does is attach the list to the resulting ``Program``, where the test runner +reads it back (through ``for_each_expected_error``) and turns a failed compile into a passing +test — provided every reported error is covered by an ``expect`` entry and no expected count +is left unused. Running such a file directly with ``daslang`` still exits non-zero. + +This is used by negative test suites, run through ``dastest``, to verify that the compiler +correctly rejects invalid code: -This is primarily used in negative test suites to verify that the compiler correctly -rejects invalid code: +.. das-doc: fragment .. code-block:: das - expect 40214:3 // expect error 40214 exactly 3 times - expect 30304, 30101 // expect each error once (count defaults to 1) + expect 50501:3 // expect error 50501 exactly 3 times + expect 30308, 30341 // expect each error once (count defaults to 1) The syntax is: -.. code-block:: das +.. code-block:: text expect [: ] [, [: ] ...] Multiple ``expect`` declarations can appear in the same file. Error codes are numeric -identifiers organized by compilation phase: - -+------------------+--------------------------------------------+ -| Range | Category | -+==================+============================================+ -| ``10001–10011`` | Lexer errors (mismatched brackets, etc.) | -+------------------+--------------------------------------------+ -| ``20000–20001`` | Parser errors (syntax errors) | -+------------------+--------------------------------------------+ -| ``30101–30128`` | Semantic: invalid type/annotation/name | -+------------------+--------------------------------------------+ -| ``30201–30213`` | Semantic: already declared / too many args | -+------------------+--------------------------------------------+ -| ``30301–30311`` | Semantic: not found (type, func, var, etc.)| -+------------------+--------------------------------------------+ -| ``30401–30403`` | Semantic: mismatching type / argument | -+------------------+--------------------------------------------+ -| ``30501–30509`` | Semantic: exceeds limit (recursion, etc.) | -+------------------+--------------------------------------------+ -| ``30601–30602`` | Semantic: ambiguous symbol (func/type/etc.)| -+------------------+--------------------------------------------+ -| ``31300`` | Unsafe operation outside ``unsafe`` block | -+------------------+--------------------------------------------+ -| ``39901–39903`` | Semantic: missing value/typeinfo | -+------------------+--------------------------------------------+ -| ``40101–40214`` | Lint-time errors and warnings | -+------------------+--------------------------------------------+ +identifiers; the leading digit is the compilation stage that raised the error: + ++------------------+--------------------------------------------------------+ +| Range | Stage | ++==================+========================================================+ +| ``1xxxx`` | Lexer — bad literals, mismatched brackets, size limits | ++------------------+--------------------------------------------------------+ +| ``2xxxx`` | Parser — syntax, module/require, duplicate declaration | ++------------------+--------------------------------------------------------+ +| ``3xxxx`` | Semantic analysis — types, lookup, unsafe operations | ++------------------+--------------------------------------------------------+ +| ``40500`` | Lint — AOT side effects (the only ``4xxxx`` code) | ++------------------+--------------------------------------------------------+ +| ``5xxxx`` | Integration — options, AOT link, internal errors | ++------------------+--------------------------------------------------------+ + +Within a stage the codes are grouped by category. In the semantic stage, for example, +``30100–30298`` are ``invalid_*``, ``30300–30352`` ``missing_*``, ``30400–30413`` +``mismatching_*``, ``30500–30515`` ``exceeds_*``, ``30600–30615`` ``ambiguous_*``, +``30700–30709`` ``already_declared_*``, ``30800–30840`` ``lookup_*``, ``30900–30952`` +``cant_*``, ``31000–31037`` ``unsafe_*``, ``31100–31106`` ``recursion_*``, +``31200–31211`` ``runtime_*``, and ``31300–31336`` ``not_*``. The authoritative list of +names and codes is ``include/daScript/ast/compilation_errors.h``. For example, a test that verifies the compiler rejects copying an array: +.. das-doc: fragment + .. code-block:: das expect 30197 // invalid_local_init (array can only be move-initialized) @@ -424,9 +457,9 @@ The following example shows a complete program with all structural elements: dead } - let GRAVITY = float3(0.0, -9.8, 0.0) + let private GRAVITY = float3(0.0, -9.8, 0.0) - var particles : array + var private particles : array def update_particle(var p : Particle; dt : float) : State { p.vel += GRAVITY * dt @@ -456,9 +489,7 @@ The following example shows a complete program with all structural elements: [finalize] def cleanup { - unsafe { - delete particles - } + delete particles } Expected output: diff --git a/doc/source/reference/language/reification.rst b/doc/source/reference/language/reification.rst index 143d876a2a..1cf421677e 100644 --- a/doc/source/reference/language/reification.rst +++ b/doc/source/reference/language/reification.rst @@ -11,7 +11,11 @@ Expression reification is used to generate AST expression trees in a convenient It provides a collection of escaping sequences to allow for different types of expression substitutions. At the top level, reification is supported by multiple call macros, which are used to generate different AST objects. -Reification is implemented in daslib/templates_boost. +Reification is implemented in ``daslib/templates_boost`` — every example on this page +assumes ``require daslib/templates_boost``. The examples that call +``typeinfo ast_typedecl`` additionally need ``options rtti``. Code that builds AST at +runtime, rather than inside the compilation pipeline, should also wrap the work in +``ast_gc_guard()`` from ``daslib/ast``, or the nodes are reported as leaked at exit. -------------- Simple example @@ -21,6 +25,8 @@ Let's review the following example: .. code-block:: das + require daslib/templates_boost + var foo = "foo" var fun <- qmacro_function("madd") <| $ ( a, b ) { return $i(foo) * a + b @@ -31,8 +37,8 @@ The output would be: .. code-block:: text - def public madd ( a:auto const; b:auto const ) : auto { - return (foo * a) + b + def public madd(a:auto const; b:auto const) : auto { + return (foo * a) + b; } What happens here is that call to macro ``qmacro_function`` generates a new function named ``madd``. @@ -60,7 +66,7 @@ prints: .. code-block:: text - (2+2) + (2 + 2) qmacro_block ^^^^^^^^^^^^ @@ -111,6 +117,8 @@ Consider the following example: .. code-block:: das + options rtti + var foo = typeinfo ast_typedecl(type) var typ = qmacro_type <| type<$t(foo)?> print(describe(typ)) @@ -170,8 +178,10 @@ prints: .. code-block:: text - let bus:auto const = "busbus" - let t:auto const = bus + { + let bus:auto const = "busbus"; + let t:auto const = bus; + } ``$i`` works in every slot of a multi-name list — all iterator slots of a multi-source ``for`` loop or comprehension, every name of a multi-name variable declaration, and shared-type block @@ -216,7 +226,9 @@ prints: .. code-block:: text - foo.fieldname = 13 + { + foo.fieldname = 13; + } $v(value) ^^^^^^^^^ @@ -235,7 +247,7 @@ prints: .. code-block:: text - (1,2f,"3") + tuple(1,2f,"3") In the example above, a tuple is substituted with the expression that generates this tuple. @@ -256,7 +268,9 @@ prints: .. code-block:: text - let foo:auto const = (2 + 2) + { + let foo:auto const = (2 + 2); + } $b(array-of-expr) ^^^^^^^^^^^^^^^^^ @@ -279,9 +293,13 @@ prints: .. code-block:: text - print(string_builder(0, "\n")) - print(string_builder(1, "\n")) - print(string_builder(2, "\n")) + { + { + print(string_builder(0, "\n")); + print(string_builder(1, "\n")); + print(string_builder(2, "\n")); + } + } $a(arguments) ^^^^^^^^^^^^^ @@ -320,8 +338,8 @@ prints: .. code-block:: text - def public show ( a:int const; var v1:int; var v2:float = 1.2f; b:int const ) : auto { - return a + b + def public show(a:int const; var v1:int; var v2:float = 1.2f; b:int const) : auto { + return a + b; } $t(type) @@ -342,7 +360,9 @@ we create pointer to a subtype: .. code-block:: text - var a:int? -const + { + var a:int? -const; + } $c(call-name) ^^^^^^^^^^^^^ diff --git a/doc/source/reference/language/statements.rst b/doc/source/reference/language/statements.rst index 5b7f477a16..4117947db6 100644 --- a/doc/source/reference/language/statements.rst +++ b/doc/source/reference/language/statements.rst @@ -72,6 +72,9 @@ Sequential bare blocks allow reusing the same variable name with a different typ Bare blocks can have a ``finally`` clause that runs when the block exits: +.. das-doc: given def acquire_resource : int { return 1 } +.. das-doc: given def compute(t : int) : int { return t * 2 } + .. code-block:: das def process { @@ -106,6 +109,8 @@ if/elif/else Conditionally execute a block depending on the result of a boolean expression: +.. das-doc: given var a, b : int + .. code-block:: das if ( a > b ) { @@ -133,7 +138,10 @@ call) can carry the condition after it: .. code-block:: das - return b if ( a < b ) + def smaller(a, b : int) : int { + return b if ( a < b ) + return a + } **Ternary-style if:** @@ -141,7 +149,9 @@ A full ternary expression can use if/else inline: .. code-block:: das - return 13 if ( a == 42 ) else return 7 + def pick(a : int) : int { + return 13 if ( a == 42 ) else return 7 + } .. _static_if: @@ -184,6 +194,8 @@ while Execute a block repeatedly while a boolean condition is true: +.. das-doc: given def done : bool { return true } + .. code-block:: das var i = 0 @@ -285,6 +297,9 @@ break Terminate the enclosing ``for`` or ``while`` loop immediately: +.. das-doc: given var arr : array +.. das-doc: given var target : int + .. code-block:: das for ( x in arr ) { @@ -375,7 +390,11 @@ Move semantics are also supported: .. code-block:: das - yield <- some_array + var gen_arrays <- generator> { + var some_array <- [1, 2, 3] + yield <- some_array + return false + } (see :ref:`Generators `). @@ -423,21 +442,28 @@ every iteration** — on normal fall-through, ``continue``, ``break``, and ``ret This makes ``var inscope`` inside a loop body safe: each iteration finalizes its own scoped variables before the next iteration begins. +.. das-doc: given var sizes : array +.. das-doc: given def allocate(n : int) : array { var r : array; r |> resize(n); return <- r } +.. das-doc: given def consume(b : array) { pass } + .. code-block:: das - for ( x in data ) { + for ( x in sizes ) { var inscope buf <- allocate(x) - process(buf) + consume(buf) } finally { print("iteration done\n") } - // "iteration done" prints once per element in `data` + // "iteration done" prints once per element in `sizes` If the iterator is empty (or the initial ``while`` condition is false), the body never enters and the loop's ``finally`` never runs. ``finally`` can also be attached to a bare visibility block, where it runs once -when the block exits: +when the block exits. The block's own locals are still in scope there: + +.. das-doc: given def open_file(name : string) : array { var r : array; r |> push(1); return <- r } +.. das-doc: given def close_file(var h : array) { pass } .. code-block:: das @@ -453,6 +479,9 @@ A ``finally`` block cannot contain ``break``, ``continue``, or ``return`` statem The ``defer`` macro from ``daslib/defer`` provides a convenient way to add cleanup code to the current scope's ``finally`` block: +.. das-doc: given def acquire : array { var r : array; r |> push(1); return <- r } +.. das-doc: given def release(var r : array) { pass } + .. code-block:: das require daslib/defer @@ -559,6 +588,11 @@ Every use of the alias substitutes the original expression: ``assume`` is particularly useful for simplifying repeated access to nested data: +.. das-doc: given struct Resolution { width, height : int } +.. das-doc: given struct Graphics { resolution : Resolution } +.. das-doc: given struct Settings { graphics : Graphics } +.. das-doc: given var settings : Settings + .. code-block:: das assume cfg = settings.graphics.resolution @@ -606,7 +640,22 @@ with (module ...) The module flavor of ``with`` is a compile-time resolution scope: names inside the block resolve as if the code were written in the named module — including that module's private -functions, globals, structures, enumerations, and type aliases: +functions, globals, structures, enumerations, and type aliases. Given a module that keeps +both of those to itself: + +.. das-doc: file game_internals.das + +.. code-block:: das + + module game_internals + + var private secret_state = 42 + + def private dump_internal_tables { + print("internal tables\n") + } + +a ``with (module ...)`` block reaches them: .. code-block:: das @@ -749,7 +798,10 @@ Daslang supports numeric labels and goto for low-level control flow: label 1: print("end\n") -Labels use integer identifiers. Computed goto is also supported: +Labels use integer identifiers. Computed goto is also supported — the target is any +integer expression naming a label: + +.. das-doc: given var label_expression : int .. code-block:: das @@ -766,13 +818,19 @@ Expression Statement .. index:: pair: Expression statement; statement -Any expression is also valid as a statement. The result of the expression is discarded: +An expression is valid as a statement when it *does* something — a call, a pipe, an +assignment, an increment. Its result is discarded: .. code-block:: das foo() // function call as statement - a + b // valid but result is unused arr |> push(42) // pipe expression as statement + counter++ // increment as statement + +A bare built-in binary operator is **not** accepted at statement level: since it has no +side effect, its result could only be discarded, so ``a + b`` on its own line is +``error[30151]: top level no side effect operation +``. (A user-defined ``operator +`` +is not built-in and is therefore not rejected.) ---------------- Global Variables @@ -794,6 +852,8 @@ Global variables are initialized once during script initialization (or each time ``shared`` indicates that the variable's memory is shared between multiple Context instances and initialized only once: +.. das-doc: given def generate_table : table { var t : table; return <- t } + .. code-block:: das let shared lookup_table <- generate_table() diff --git a/doc/source/reference/language/string_builder.rst b/doc/source/reference/language/string_builder.rst index 57a7965fa3..390fffdef7 100644 --- a/doc/source/reference/language/string_builder.rst +++ b/doc/source/reference/language/string_builder.rst @@ -68,30 +68,40 @@ representation of the interpolated value: let pi = 3.14159 print("pi = {pi:5.2f}\n") // fixed-point, 5 wide, 2 decimals -Format specifiers follow a syntax similar to C ``printf`` format strings. -The general form is: +Specifiers use the libfmt (Python ``format``) replacement-field syntax, **not** C +``printf`` syntax: the compiler rewrites ``{expression:spec}`` into +``fmt(":spec", expression)``. The general form is: -.. code-block:: das +.. code-block:: text - {expression:flags width.precision type} + {expression:[[fill]align][sign][#][0][width][.precision][type]} Where: -* **flags** — optional characters such as ``-`` (left-align), ``+`` (force sign), - ``0`` (zero-pad), ``#`` (alternate form, e.g. ``0x`` prefix for hex) +* **fill / align** — ``<`` left-align, ``>`` right-align, ``^`` center, each optionally + preceded by the character to pad with (``{v:*>8}``) +* **sign** — ``+`` prints a sign for positive numbers too; ``-`` (the default) only for + negative ones; a space puts a space where ``+`` would go +* ``#`` — alternate form: ``0x`` / ``0b`` / ``0`` prefix for hex, binary, octal +* ``0`` — zero-pad to the field width * **width** — minimum field width -* **precision** — number of decimal places (for floating-point) or maximum string length +* **precision** — number of decimal places, for floating-point * **type** — conversion character: - * ``d`` or ``i`` — signed decimal integer - * ``u`` — unsigned decimal integer - * ``x`` — hexadecimal (lowercase) - * ``X`` — hexadecimal (uppercase) + * ``d`` — decimal integer + * ``b`` or ``B`` — binary * ``o`` — octal - * ``f`` — fixed-point decimal - * ``e`` — scientific notation - * ``E`` — scientific notation (uppercase) - * ``g`` — general (shortest of ``f`` or ``e``) - * ``G`` — general (shortest of ``f`` or ``E``) + * ``x`` / ``X`` — hexadecimal (lowercase / uppercase) + * ``c`` — the integer's character + * ``f`` or ``F`` — fixed-point decimal + * ``e`` / ``E`` — scientific notation (lowercase / uppercase) + * ``g`` / ``G`` — general (shortest of ``f`` or ``e``) + * ``a`` / ``A`` — hexadecimal floating-point + +A specifier only applies to numeric values — ``fmt`` has no ``string`` or ``bool`` +overload, so ``"{name:>8}"`` fails to compile with ``error[30341]``; pad text with +``pad_left`` / ``pad_right`` from ``daslib/strings_boost``. A type character libfmt does +not know — ``i`` and ``u``, which C ``printf`` accepts, among them — panics at runtime +with ``fmt error: invalid format specifier``. Examples: @@ -100,8 +110,9 @@ Examples: print("{42:08x}\n") // "0000002a" — 8-digit zero-padded hex print("{42:08X}\n") // "0000002A" — uppercase hex print("{3.14159:.2f}\n") // "3.14" — 2 decimal places - print("{-7:+d}\n") // "-7" — with sign + print("{7:+d}\n") // "+7" — sign forced on a positive number print("{255:#x}\n") // "0xff" — with 0x prefix + print("{42:*>8}\n") // "******42" — right-aligned, '*' fill ----------------------- Escaping Curly Brackets @@ -120,6 +131,8 @@ Multi-line Strings String interpolation works in multi-line (heredoc) strings as well: +.. das-doc: given var value1, value2, value3 : int + .. code-block:: das let msg = "Line 1: {value1} @@ -144,6 +157,8 @@ Relationship to print The ``print`` function accepts string builder strings directly: +.. das-doc: given var x, y : int + .. code-block:: das print("x = {x}, y = {y}\n") diff --git a/doc/source/reference/language/structs.rst b/doc/source/reference/language/structs.rst index 55d40ccde5..7b848a7e4f 100644 --- a/doc/source/reference/language/structs.rst +++ b/doc/source/reference/language/structs.rst @@ -158,6 +158,8 @@ with that declaration the bare form reports Since function pointers are first-class values, you can emulate virtual functions by storing function pointers as members: +.. das-doc: alt + .. code-block:: das struct Foo { @@ -241,6 +243,10 @@ whose first argument is ``self``. Inside such a finalizer for a derived struct, .. code-block:: das + def operator delete(var self: Foo) { + print("releasing Foo\n") + } + struct Bar: Foo {} def operator delete(var self: Bar) { @@ -330,11 +336,12 @@ It is safe to use the ``cast`` keyword to cast a derived structure instance into It is unsafe to cast a base struct to its derived child type: +.. das-doc: expect error[30131] + .. code-block:: das - var f3d: Foo3D = Foo3D() - def foo(var foo: Foo) { - (cast(foo)).z = 5 // error, won't compile + def widen(var foo: Foo) { + (cast(foo)).z = 5 // error[30131]: incompatible cast, won't compile } If needed, the upcast can be used with the ``unsafe`` keyword: diff --git a/doc/source/reference/language/tables.rst b/doc/source/reference/language/tables.rst index 093f2e13a2..b53c1cc3e9 100644 --- a/doc/source/reference/language/tables.rst +++ b/doc/source/reference/language/tables.rst @@ -10,20 +10,27 @@ Table Tables are associative containers implemented as a set of key/value pairs: +.. das-doc: given var tab : table +.. das-doc: given struct Bag { tab : table } +.. das-doc: given var bag : Bag +.. das-doc: given var tab1 : table +.. das-doc: given var tab2 : table .. code-block:: das - var tab: table - unsafe { - tab["10"] = 10 - tab["20"] = 20 - tab["some"] = 10 - tab["some"] = 20 // replaces the value for 'some' key - } + var tab : table + tab["10"] = 10 + tab["20"] = 20 + tab["some"] = 10 + tab["some"] = 20 // replaces the value for 'some' key + +Indexing a table is safe by default — it does not require an ``unsafe`` section (see +``unsafe_table_lookup`` below for the policy that changes this). Daslang containers store unboxed values, so table lookups via the index operator (``tab[key]``) can cause undefined behavior when the **same table** is referenced more than once in the **same expression**. Consider the following example: +.. das-doc: expect error[30250] .. code-block:: das tab["1"] = tab["2"] // ERROR: potential table lookup collision @@ -31,33 +38,41 @@ Consider the following example: What happens is the table may get resized after either ``tab["1"]`` or ``tab["2"]`` if the key is missing (similar to C++ STL hash_map), invalidating the reference returned by the other lookup. -The compiler detects this and reports a ``table_lookup_collision`` lint error. The check catches any -expression where the same table appears in two or more index operations that keep the result as a -reference (assignments, moves, clones). Value lookups (where the result is immediately copied out) -are safe and not flagged. +The compiler detects this and reports ``error[30250]: potential table lookup collision``. The check +catches any expression where the same table appears in two or more index operations that keep the +result as a reference (assignments, moves, clones). Value lookups (where the result is immediately +copied out) are safe and not flagged. Setting ``CodeOfPolicies::temp_table_lint_warning`` downgrades +the error to a compiler-log warning. Other dangerous patterns include: +.. das-doc: expect error[30250] .. code-block:: das - tab[1] := tab[2] // ERROR: clone between two lookups - tab[1] <- tab[2] // ERROR: move between two lookups - foo.tab[1] = foo.tab[2] // ERROR: same table via field access + tab["1"] := tab["2"] // ERROR: clone between two lookups + tab["1"] <- tab["2"] // ERROR: move between two lookups + bag.tab["1"] = bag.tab["2"] // ERROR: same table via field access A single ``tab[key]`` in an expression is always safe. Multiple lookups of **different** tables in the same expression are also safe: .. code-block:: das - tab1[1] = tab2[2] // OK: different tables + tab1["1"] = tab2["2"] // OK: different tables It is possible to make **all** table lookups unsafe (requiring an ``unsafe`` block) via ``CodeOfPolicies`` or the following option: +.. das-doc: alt .. code-block:: das options unsafe_table_lookup // makes every tab[key] require unsafe + var scores : table + unsafe { + scores["one"] = 1 // now requires unsafe + } + Safe navigation of the table is safe, since it does not create missing keys: .. code-block:: das @@ -114,16 +129,24 @@ Tables cannot be assigned, only cloned or moved. .. code-block:: das - def clone_table(var a, b: table) { + def clone_table(var a, b : table) { a := b // a is now a deep copy of b clone(a, b) // same as above - a = b // error } - def move_table(var a, b: table) { + def move_table(var a, b : table) { a <- b // a now points to the same data as b, and b is empty } +Plain assignment is rejected: + +.. das-doc: expect error[30950] +.. code-block:: das + + def assign_table(var a, b : table) { + a = b // error[30950]: this type can't be copied + } + A table literal can also be move-assigned to an existing variable: .. code-block:: das @@ -151,14 +174,14 @@ This is syntax sugar for: .. code-block:: das - let tab : table <- to_table_move(fixed_array>(("one",1),("two",2))) + let tab : table <- to_table_move(fixed_array>(("one",1),("two",2))) Alternative syntax is: .. code-block:: das let tab <- table("one"=>1, "two"=>2) - let tab <- table("one"=>1, "two"=>2) + let tab <- table("one"=>1, "two"=>2) A table that holds no associative data can also be declared: diff --git a/doc/source/reference/language/temporary.rst b/doc/source/reference/language/temporary.rst index ff43f759f5..0494125a05 100644 --- a/doc/source/reference/language/temporary.rst +++ b/doc/source/reference/language/temporary.rst @@ -10,18 +10,19 @@ Let's review the following C++ example: .. code-block:: cpp - void peek_das_string(const string & str, const TBlock> & block, Context * context) { + void peek_das_string(const string & str, const TBlock> & block, Context * context, LineInfoArg * at) { vec4f args[1]; args[0] = cast::from(str.c_str()); - context->invoke(block, args, nullptr); + context->invoke(block, args, nullptr, at); } The C++ function here exposes a pointer to a C-string, internal to std::string. From Daslang's perspective, the declaration of the function looks like this: +.. das-doc: signatures .. code-block:: das - def peek ( str : das_string; blk : block<(arg:string#):void> ) + def peek ( src : das_string implicit; block : block<(arg0:string#):void> implicit ) : void Where string# is a temporary version of a Daslang string type. @@ -31,12 +32,13 @@ Temporary values enforce this through the following rules. Temporary values can't be copied or moved: +.. das-doc: expect error[30915] .. code-block:: das - def sample ( var t : das_string ) { + def copy_temporary ( var t : das_string ) { var s : string peek(t) $ ( boo : string# ) { - s = boo // error, can't copy temporary value + s = boo // error[30915]: can't copy temporary value } } @@ -48,7 +50,10 @@ Temporary values can't be returned or passed to functions, which require regular print("s={s}\n") } - def sample ( var t : das_string ) { +.. das-doc: expect error[30341] +.. code-block:: das + + def pass_temporary ( var t : das_string ) { peek(t) $ ( boo : string# ) { accept_string(boo) // error } @@ -72,7 +77,7 @@ These functions implicitly promise that the data will not be cached (copied, mov print("s={s}\n") } - def sample ( var t : das_string ) { + def pass_implicit ( var t : das_string ) { peek(t) $ ( boo : string# ) { accept_any_string(boo) } @@ -82,7 +87,7 @@ Temporary values can and are intended to be cloned: .. code-block:: das - def sample ( var t : das_string ) { + def clone_temporary ( var t : das_string ) { peek(t) $ ( boo : string# ) { var boo_clone : string := boo accept_string(boo_clone) diff --git a/doc/source/reference/language/tuples.rst b/doc/source/reference/language/tuples.rst index 7abf247fda..204db4acda 100644 --- a/doc/source/reference/language/tuples.rst +++ b/doc/source/reference/language/tuples.rst @@ -10,8 +10,8 @@ A tuple type is declared with the ``tuple`` keyword followed by a list of elemen .. code-block:: das - tuple // unnamed elements - tuple // named elements + var unnamed : tuple // unnamed elements + var named : tuple // named elements Tuple field names are part of the type. Two tuple declarations are the same only if they have the same number of elements, the same element types, **and the same @@ -21,13 +21,27 @@ names — even when the element types match: .. code-block:: das - var a : tuple - var b : tuple - var c : tuple - // a = b // error: tuple is not the same type as tuple - // b = c // error: tuple is not the same type as tuple - var d : tuple - b = d // ok — same names, same types + var ta : tuple + var tb : tuple + var tc : tuple + var td : tuple + tb = td // ok — same names, same types + +Both mismatched assignments are rejected: + +.. das-doc: expect error[30915] +.. code-block:: das + + var lhs : tuple + var rhs : tuple + lhs = rhs // error[30915]: tuple is not the same type as tuple + +.. das-doc: expect error[30915] +.. code-block:: das + + var lhs : tuple + var rhs : tuple + lhs = rhs // error[30915]: tuple is not the same type as tuple The same rule applies to construction: a bare positional literal ``(1, 2.0)`` produces an unnamed ``tuple`` and is not accepted where a named @@ -36,8 +50,12 @@ tuple directly: .. code-block:: das - var b : tuple = (i = 1, f = 2.0) // ok - // var b : tuple = (1, 2.0) // error: not the same type + var named_ok : tuple = (i = 1, f = 2.0) // ok + +.. das-doc: expect error[30344] +.. code-block:: das + + var named_bad : tuple = (1, 2.0) // error[30344]: not the same type Mixing named and positional fields in the same literal is **not** supported — either every field is named or none are. @@ -68,8 +86,8 @@ overload, use the explicit named-field literal: .. code-block:: das - def overload_pick(x : tuple) { return 1 } - def overload_pick(x : tuple) { return 2 } + def overload_pick(hit : tuple) { return 1 } + def overload_pick(hit : tuple) { return 2 } let x = 1 let y = 2.0 overload_pick((x, y)) // returns 1: unnamed overload wins @@ -78,12 +96,13 @@ overload, use the explicit named-field literal: A name mismatch fails compilation rather than silently constructing the unnamed tuple: +.. das-doc: expect error[30341] .. code-block:: das let foo = 1 let bar = 1.1 - var arr : array> - // arr |> push((foo, bar)) // error: function_not_found + var hits : array> + hits |> push((foo, bar)) // error[30341]: no matching functions or generics Promotion does not fire when any element is not a bare variable reference, e.g. ``(a, a+1)`` stays unnamed. @@ -92,16 +111,16 @@ Tuple elements can be accessed via nameless fields, i.e. _ followed by the 0 bas .. code-block:: das - a._0 = 1 - a._1 = 2.0 + ta._0 = 1 + ta._1 = 2.0 Named tuple elements can be accessed by name as well as via nameless field: .. code-block:: das - b.i = 1 // same as _0 - b.f = 2.0 // same as _1 - b._1 = 2.0 // _1 is also available + tb.i = 1 // same as _0 + tb.f = 2.0 // same as _1 + tb._1 = 2.0 // _1 is also available Tuples follow the same alignment rules as structures (see :ref:`Structures `). @@ -118,20 +137,20 @@ It's the same as: .. code-block:: das - typedef Foo = tuple + typedef Foo = tuple Tuples can be constructed using the tuple constructor, for example: .. code-block:: das - var a = (1,2.0,"3") - var b = tuple(1, 2.0, "3") + var tup_a = (1,2.0,"3") + var tup_b = tuple(1, 2.0, "3") The ``=>`` operator creates a 2-element tuple from its left and right operands: .. code-block:: das - var c = "one" => 1 // same as tuple("one", 1) + var pair = "one" => 1 // tuple, same as tuple("one", 1) This works in any expression context, not just table literals. Table literals like ``{ "one"=>1, "two"=>2 }`` use ``=>`` to form key-value tuples @@ -141,29 +160,34 @@ Tuple elements can be assigned names via tuple constructor: .. code-block:: das - var a = tuple(a=1, b=2.0, c="3") + var named3 = tuple(a=1, b=2.0, c="3") -Both ``auto`` and a full type specification can be used to construct a tuple. +A tuple constructor that spells out the element types accepts **named** arguments only — +``tuple(a=1, b=2.0)`` is fine, ``tuple(1, 2.0)`` is a syntax error. +Use the ``auto`` form ``tuple(1, 2.0)`` for positional construction. Array of tuples can be constructed using similar syntax, with a comma as a separator: .. code-block:: das + typedef Tup = tuple + let H : array <- array((a = 1, b = 2., c = "3"), (a = 4, b = 5., c = "6")) Tuples can be expanded upon the variable declaration, for example: .. code-block:: das - var (a, b, c) = (1, 2.0, "3") + var (first, second, third) = (1, 2.0, "3") In this case only one variable is created, as well as for 'assume' expressions. I.e: +.. das-doc: alt .. code-block:: das - var a`b`c = (1, 2.0, "3") - assume a = a`b`c._0 - assume b = a`b`c._1 - assume c = a`b`c._2 + var first`second`third = (1, 2.0, "3") + assume first = first`second`third._0 + assume second = first`second`third._1 + assume third = first`second`third._2 Iterators and containers can be expanded in the for-loop in a similar way: @@ -187,21 +211,20 @@ parameter tuple has ``T const?``: .. code-block:: das + struct Loc { line : int } struct Node { at : Loc } - var ats : array> // Exact-match overload. - def takeng(a : array>; - b : tuple) { ... } + def takeng(hits : array>; hit : tuple) { pass } // Generic overload — TT is inferred from the array element type, - // so b must match tuple as well. - def take(a : array; b : TT) { ... } + // so hit must match tuple as well. + def take(hits : array; hit : TT) { pass } - def feed(var a : Node?&) { - take(ats, ("test", unsafe(addr(a.at)))) // tuple - takeng(ats, ("test", unsafe(addr(a.at)))) // accepted for - // tuple + def feed(var node : Node?&; ats : array>) { + take(ats, ("test", unsafe(addr(node.at)))) // tuple + takeng(ats, ("test", unsafe(addr(node.at)))) // accepted for + // tuple } The widening is one-directional (``T?`` widens to ``T const?``, not the diff --git a/doc/source/reference/language/unsafe.rst b/doc/source/reference/language/unsafe.rst index dd677d0dbd..aa9d27d352 100644 --- a/doc/source/reference/language/unsafe.rst +++ b/doc/source/reference/language/unsafe.rst @@ -10,10 +10,17 @@ Unsafe The ``unsafe`` keyword denotes unsafe contents, which is required for operations, but could potentially crash the application: +.. das-doc: given struct Foo { a : int } +.. das-doc: given variant VB { Bar : float; Baz : int } +.. das-doc: given class TestClass { cx : int } +.. das-doc: given class Goo { gy : int } +.. das-doc: given var x = 42 +.. das-doc: given var v = 0x3f800000 +.. das-doc: given var vb : VB .. code-block:: das unsafe { - let px = addr(x) + let px = addr(x) // error[31000] without unsafe } Expressions (and subexpressions) can also be unsafe: @@ -24,26 +31,30 @@ Expressions (and subexpressions) can also be unsafe: The ``unsafe`` keyword is followed by a block that can include such operations. Nested unsafe sections are allowed. ``unsafe`` is not inherited in lambdas, generators, or local functions, but it is inherited in local blocks. -Individual expressions can cause a ``CompilationError::unsafe`` error, unless they are part of the unsafe section. Additionally, macros can explicitly set the ``ExprGenFlags::alwaysSafe`` flag. +Individual expressions raise an error from the ``unsafe_*`` family (``error[31000]`` through +``error[31037]``) unless they are part of the unsafe section. Additionally, macros can explicitly +set the ``ExprGenFlags::alwaysSafe`` flag. The address of expression is unsafe: .. code-block:: das - unsafe { - let a : int - let pa = addr(a) - return pa // accessing *pa can potentially corrupt stack + def return_stack_address() : int? { + unsafe { + var n = 13 + var pn = addr(n) // error[31000] without unsafe + return pn // accessing *pn can potentially corrupt stack + } } Lambdas or generators require unsafe sections for the implicit capture by move or by reference: .. code-block:: das - var a : array + var values : array unsafe { var counter <- @ (extra:int) : int { - return a[0] + extra // a is implicitly moved + return values[0] + extra // error[31003]: values is implicitly moved } } @@ -54,15 +65,19 @@ Deleting any pointer requires an unsafe section: var p = new Foo() var q = p unsafe { - delete p // accessing q can potentially corrupt memory + delete p // error[31009] without unsafe; + // accessing q can potentially corrupt memory } Upcast and reinterpret cast require an unsafe section: .. code-block:: das - unsafe { - return reinterpret(13) // reinterpret can create unsafe pointers + def erase_type() : void? { + unsafe { + return reinterpret(13) // error[31004] without unsafe; + // reinterpret can create unsafe pointers + } } ``addr(x)`` — sugar for ``reinterpret(addr(x))`` — needs only one @@ -70,42 +85,75 @@ Upcast and reinterpret cast require an unsafe section: .. code-block:: das - let p = unsafe(addr(v)) // instead of unsafe(reinterpret(unsafe(addr(v)))) + let pv = unsafe(addr(v)) // instead of unsafe(reinterpret(unsafe(addr(v)))) Indexing into a pointer is unsafe: .. code-block:: das - unsafe { - var p = new Foo() - return p[13] // accessing out of bounds pointer can potentially corrupt memory + def read_out_of_bounds() : Foo { + unsafe { + var pf = new Foo() + return pf[13] // error[31023] without unsafe; accessing an out of + // bounds pointer can potentially corrupt memory + } } A safe index is unsafe when not followed by the null coalescing operator: .. code-block:: das - var a = { 13 => 12 } - unsafe { - var t = a?[13] ?? 1234 // safe - return a?[13] // unsafe; safe index is a form of 'addr' operation - // it can create pointers to temporary objects + def unsafe_safe_index() : int? { + var tab = { 13 => 12 } + let t = tab?[13] ?? 1234 // safe + unsafe { + // safe index is a form of 'addr' operation, so it can create + // pointers to temporary objects + return tab?[13] + } + } + +Without the ``unsafe`` section it does not compile: + +.. das-doc: expect error[31034] +.. code-block:: das + + def bare_safe_index() : int? { + var tab = { 13 => 12 } + // error[31034]: safe-index of table<> must be inside the 'unsafe' block + return tab?[13] } Variant ``?as`` on local variables is unsafe when not followed by the null coalescing operator: .. code-block:: das - unsafe { - return a ?as Bar // safe as is a form of 'addr' operation + def unsafe_safe_as() : float? { + unsafe { + return vb ?as Bar // safe as is a form of 'addr' operation + } + } + +Without the ``unsafe`` section it does not compile: + +.. das-doc: expect error[31036] +.. code-block:: das + + def bare_safe_as() : float? { + // error[31036]: variant ?as on non-pointer requires unsafe + return vb ?as Bar } Variant ``?.field`` is unsafe when not followed by the null coalescing operator: .. code-block:: das - unsafe { - return a?.Bar // safe navigation of a variant is a form of 'addr' operation + def unsafe_safe_field() : float? { + unsafe { + // safe navigation of a variant is a form of 'addr' operation; + // error[31036] without unsafe + return vb?.Bar + } } @@ -113,8 +161,19 @@ Variant ``.field`` is unsafe: .. code-block:: das - unsafe { - return a.Bar // this is potentially a reinterpret cast + def unsafe_field() : float { + unsafe { + return vb.Bar // this is potentially a reinterpret cast + } + } + +Without the ``unsafe`` section it does not compile: + +.. das-doc: expect error[31035] +.. code-block:: das + + def bare_field() : float { + return vb.Bar // error[31035]: variant.field requires unsafe } Certain functions and operators are inherently unsafe or marked unsafe via the [unsafe_operation] annotation: @@ -122,30 +181,38 @@ Certain functions and operators are inherently unsafe or marked unsafe via the [ .. code-block:: das unsafe { - var a : int? - a += 13 // pointer arithmetic can create invalid pointers + var ptr : int? + ptr += 13 // error[31013]: pointer arithmetic can create + // invalid pointers var boo : int[13] - var it = each(boo) // each() of array is unsafe, for it does not capture + var it = each(boo) // error[31013]: each() of array is unsafe, + // for it does not capture } -Moving from a smart pointer value requires unsafe, unless that value is the 'new' operator: +A move statement whose source is a smart pointer **value** requires unsafe, unless that value +comes from the ``new`` operator. Moving from a smart pointer **reference** stays safe. +``TestObjectSmart`` below is a C++ smart pointer type, so this block has no standalone context: +.. das-doc: fragment .. code-block:: das + var a <- new TestObjectSmart() // safe, its explicitly new + var b : TestObjectSmart? unsafe { - var a <- new TestObjectSmart() // safe, its explicitly new - var b <- someSmartFunction() // unsafe since lifetime is not obvious - b <- a // safe, values are not lost + b <- someSmartFunction() // error[31021] without unsafe, + // since lifetime is not obvious } + b <- a // safe, moving from a reference Moving or copying classes is unsafe: .. code-block:: das - def foo ( var b : TestClass ) { + def move_class ( var src : TestClass ) { unsafe { - var a : TestClass - a <- b // potentially moving from derived class + var dst : TestClass + dst <- src // error[31005] without unsafe; + // potentially moving from derived class } } @@ -154,7 +221,8 @@ Local class variables are unsafe: .. code-block:: das unsafe { - var g = Goo() // potential lifetime issues + var g = Goo() // error[31017] without unsafe; + // potential lifetime issues } implicit @@ -163,12 +231,16 @@ implicit ``implicit`` keyword is used to specify that type can be either temporary or regular type. The parameter is treated as the type written in the declaration, while also accepting the other form (temporary or regular) as an argument. For example: +.. das-doc: signatures .. code-block:: das def foo ( a : Foo implicit ) // a will be treated as Foo, but will also accept Foo# as argument def foo ( a : Foo# implicit ) // a will be treated as Foo#, but will also accept Foo as argument -Unfortunately implicit conversions like this are unsafe, so ``implicit`` is unsafe by definition. +``implicit`` switches off the temporary-type lifetime check for that parameter, and nothing in +the compiler can verify what the body then does with the value. Declaring it does not require an +``unsafe`` section — it is a promise by the author that the value is never cached (copied, moved, +or stored). other cases ----------- @@ -176,13 +248,14 @@ other cases There are several additional cases where ``unsafe`` is required. They are typically controlled via CodeOfPolicies or an appropriate option: +.. das-doc: alt .. code-block:: das options unsafe_table_lookup // makes ALL table indexing unsafe. refers to CodeOfPolicies::unsafe_table_lookup - var tab <- { 1=>"one", 2=>"two" } + var names <- { 1=>"one", 2=>"two" } unsafe { - tab[3] = "three" // requires unsafe when unsafe_table_lookup is enabled + names[3] = "three" // error[31033] without unsafe, when unsafe_table_lookup is enabled } By default ``unsafe_table_lookup`` is ``false`` — individual table lookups are safe. However, the compiler diff --git a/doc/source/reference/language/variants.rst b/doc/source/reference/language/variants.rst index cde07efcfc..b7f06ca419 100644 --- a/doc/source/reference/language/variants.rst +++ b/doc/source/reference/language/variants.rst @@ -9,7 +9,7 @@ possibly each with different values and types: .. code-block:: das - var t : variant + var t : variant There is a shorthand type alias syntax to define a variant: @@ -20,7 +20,12 @@ There is a shorthand type alias syntax to define a variant: f_value : float } - typedef U_F = variant // exactly the same as the declaration above +The ``typedef`` form declares exactly the same type: + +.. das-doc: alt +.. code-block:: das + + typedef U_F = variant Any two variants are the same type if they have the same named cases of the same types in the same order. @@ -30,6 +35,7 @@ The current case selection can be checked via the ``is`` operator, and accessed .. code-block:: das + t = U_F(i_value = 0x3f800000) assert(t is i_value) assert(t as i_value == 0x3f800000) @@ -44,22 +50,27 @@ Accessing a variant case of the incorrect type will cause a panic: .. code-block:: das - t = U_F(i_value = 0x40000000) - return t as f_value // panic, invalid variant index + def read_float(u : U_F) : float { + return u as f_value // panic when the current case is not f_value + } Safe navigation is available via the ``?as`` operation: .. code-block:: das - return t ?as f_value ?? 1.0 // will return 1.0 if t is not f_value + def read_float_or(u : U_F) : float { + return u ?as f_value ?? 1.0 // will return 1.0 if u is not f_value + } Cases can also be accessed in an unsafe manner without checking the type: .. code-block:: das - unsafe { - t.i_value = 0x3f800000 - return t.f_value // will return memory, occupied by f_value - i.e. 1.0f + def reinterpret_case(var u : U_F) : float { + unsafe { + u.i_value = 0x3f800000 + return u.f_value // returns the memory occupied by f_value - i.e. 1.0f + } } The current index can be determined via the ``variant_index`` function: @@ -76,11 +87,18 @@ The index value for a specific case can be determined via the ``variant_index`` assert(typeinfo variant_index(t)==0) assert(typeinfo variant_index(t)==1) - assert(typeinfo variant_index(t)==-1) // compilation error assert(typeinfo safe_variant_index(t)==0) assert(typeinfo safe_variant_index(t)==1) - assert(typeinfo safe_variant_index(t)==-1) + assert(typeinfo safe_variant_index(t)==-1) // -1, no error + +``variant_index`` on an unknown case name is a compilation error: + +.. das-doc: expect error[30839] +.. code-block:: das + + var u : U_F + assert(typeinfo variant_index(u)==-1) // error[30839]: variant unknown_value not found Current case selection can be modified with the unsafe operation ``set_variant_index``: @@ -96,7 +114,7 @@ Alignment and data layout Variants contain the 'index' of the current case, followed by a union of individual cases, similar to the following C++ layout: -.. code-block:: das +.. code-block:: cpp struct MyVariantName { int32_t __variant_index; diff --git a/doc/source/reference/language/very_safe_context.rst b/doc/source/reference/language/very_safe_context.rst index 7933f4dc96..97bb17bcd1 100644 --- a/doc/source/reference/language/very_safe_context.rst +++ b/doc/source/reference/language/very_safe_context.rst @@ -54,11 +54,16 @@ Both ``data[5]`` and ``data[100]`` must share the same lifetime, but ``5`` is ev and ``100`` after it. No order of operations can make this code correct with unboxed containers and pass-by-reference semantics. Equivalent C++ code exhibits the same behavior. -The issue is even more apparent with tables: +The issue is even more apparent with tables, where the compiler rejects the expression outright +(see :ref:`Tables `): +.. das-doc: expect error[30250] .. code-block:: das - tab[key1] <- tab[key2] // may rehash the table, invalidating the key1 reference + var tab : table + let key1 = "one" + let key2 = "two" + tab[key1] <- tab[key2] // error[30250]: may rehash the table, invalidating the key1 reference --------------------------------- What ``very_safe_context`` does diff --git a/doc/source/reference/tutorials/32_operator_overloading.rst b/doc/source/reference/tutorials/32_operator_overloading.rst index ae533c24bf..9bd61b72c0 100644 --- a/doc/source/reference/tutorials/32_operator_overloading.rst +++ b/doc/source/reference/tutorials/32_operator_overloading.rst @@ -45,6 +45,8 @@ Comparison operators Overload ``==``, ``!=``, ``<``, ``>``, ``<=``, ``>=`` for custom comparisons. Returning ``bool`` is required: +.. das-doc: given struct Vec2 { x, y : float } + .. code-block:: das def operator ==(a, b : Vec2) : bool { @@ -153,8 +155,9 @@ Usage:: m[0] += 5.0 // calls operator []+= print("{m[0]}\n") // 15 -The safe index operator ``?[]`` returns a default value when the index is out of -range, following the same pattern. +``operator ?[]`` follows the same pattern for safe indexing. For built-in +containers ``?[]`` yields a pointer that is ``null`` when the index is out of +range, so call sites pair it with ``??`` to supply a fallback. Dot operators / property accessors ================================== diff --git a/doc/source/reference/tutorials/33_algorithm.rst b/doc/source/reference/tutorials/33_algorithm.rst index 881034ddbe..1c1eb390ab 100644 --- a/doc/source/reference/tutorials/33_algorithm.rst +++ b/doc/source/reference/tutorials/33_algorithm.rst @@ -101,9 +101,12 @@ is fully sorted, but the ``k``-th position is correctly the ``k``-th-smallest: Both accept a custom comparator block — same shape as ``sort``: +.. das-doc: given struct PricePoint { item_id, price : int } + .. code-block:: das - var c <- [PricePoint(item_id=1, price=50), PricePoint(item_id=2, price=20), ...] + var c <- [PricePoint(item_id=1, price=50), PricePoint(item_id=2, price=20), + PricePoint(item_id=3, price=80), PricePoint(item_id=4, price=10)] sort_boost::partial_sort(c, 2) $(x, y : PricePoint) : bool { return x.price < y.price } @@ -178,7 +181,8 @@ without writing a full comparator block: name : string age : int } - let people <- [Person(name = "Alice", age = 30), Person(name = "Bob", age = 25), ...] + let people <- [Person(name = "Alice", age = 30), Person(name = "Bob", age = 25), + Person(name = "Carol", age = 35), Person(name = "Dave", age = 22)] let youngest3 <- top_n_by(people, 3, @@(p : Person -&) => p.age) // sorted ascending by age @@ -196,9 +200,13 @@ should be sorted first for full deduplication: print("{a}\n") // [1, 2, 3, 5] var b <- [3, 1, 3, 1] - var c <- unique(b) + var c <- algorithm::unique(b) print("{c}\n") // [3, 1, 3, 1] — no adjacent dups removed +``daslib/linq`` declares ``unique`` and ``reverse`` too, so a program that +requires both modules must qualify the call — ``algorithm::unique``, +``algorithm::reverse`` — or the compiler reports two matching candidates. + Array manipulation ================== @@ -206,7 +214,7 @@ Array manipulation // reverse — in place var a <- [1, 2, 3, 4, 5] - reverse(a) // [5, 4, 3, 2, 1] + algorithm::reverse(a) // [5, 4, 3, 2, 1] // combine — concatenate into a new array var both <- combine([1, 2], [3, 4]) // [1, 2, 3, 4] @@ -251,8 +259,9 @@ Tables with no value type serve as sets. The module provides: var diff <- difference(a, b) // {1, 2} var sdiff <- symmetric_difference(a, b) // {1, 2, 5, 6} + var sub <- { 2, 3 } print("identical: {identical(a, a)}\n") // true - print("is_subset({2, 3}, a): {is_subset({2, 3}, a)}\n") // true + print("is_subset(sub, a): {is_subset(sub, a)}\n") // true Topological sort ================ @@ -273,7 +282,7 @@ have an ``id`` field and a ``before`` table listing which ids must come first: id=0 )] var sorted <- topological_sort(nodes) - // sorted: [0, 1, 2] + // sorted holds the nodes themselves, in dependency order — ids 0, 1, 2 If the graph contains a cycle, ``topological_sort`` calls ``panic``. @@ -288,7 +297,7 @@ Most functions (``reverse``, ``fill``, ``lower_bound``, ``binary_search``, var a = fixed_array(5, 3, 1, 4, 2) print("min: {min_element(a)}\n") // 2 (value 1) - reverse(a) // [2, 4, 1, 3, 5] + algorithm::reverse(a) // [2, 4, 1, 3, 5] .. seealso:: diff --git a/doc/source/reference/tutorials/34_decs.rst b/doc/source/reference/tutorials/34_decs.rst index cb1a5b1b3b..7e71ae5bce 100644 --- a/doc/source/reference/tutorials/34_decs.rst +++ b/doc/source/reference/tutorials/34_decs.rst @@ -64,6 +64,8 @@ Component names in the block signature match component names on entities: Query a specific entity by passing its ``EntityId``: +.. das-doc: given var eid : EntityId + .. code-block:: das query(eid) $(tag : string; val : int) { @@ -319,6 +321,9 @@ Utility functions It returns ``false`` for ``INVALID_ENTITY_ID``, deleted entities, and stale generation IDs after slot recycling: +.. das-doc: given var hero : EntityId +.. das-doc: given var deleted_eid : EntityId + .. code-block:: das print("alive? {is_alive(hero)}\n") // true diff --git a/doc/source/reference/tutorials/35_jobque.rst b/doc/source/reference/tutorials/35_jobque.rst index 3927da0a27..d0f1eaac6b 100644 --- a/doc/source/reference/tutorials/35_jobque.rst +++ b/doc/source/reference/tutorials/35_jobque.rst @@ -70,6 +70,9 @@ Spawning jobs ``new_job`` dispatches work to the thread pool. Each job runs in a cloned context. Use channels to communicate results back: +.. das-doc: given struct IntVal { v : int } +.. das-doc: given struct StringVal { s : string } + .. code-block:: das with_job_que() { diff --git a/doc/source/reference/tutorials/37_utility_patterns.rst b/doc/source/reference/tutorials/37_utility_patterns.rst index c78c65e946..60f74b23c8 100644 --- a/doc/source/reference/tutorials/37_utility_patterns.rst +++ b/doc/source/reference/tutorials/37_utility_patterns.rst @@ -107,6 +107,9 @@ Practical: paired acquire/release The classic use case: acquire a resource, immediately defer its release: +.. das-doc: given def acquire_resource(name : string) { print(" acquired {name}\n") } +.. das-doc: given def release_resource(name : string) { print(" released {name}\n") } + .. code-block:: das acquire_resource("database") diff --git a/doc/source/reference/tutorials/42_testing_tools.rst b/doc/source/reference/tutorials/42_testing_tools.rst index b0ab614909..ee1642968a 100644 --- a/doc/source/reference/tutorials/42_testing_tools.rst +++ b/doc/source/reference/tutorials/42_testing_tools.rst @@ -100,7 +100,7 @@ Faker has configurable fields to control the output: var fake <- Faker() fake.min_year = 2020u // restrict year range - fake.total_years = 5u // 2020-2025 + fake.total_years = 5u // dates land in 2020-2024 fake.max_long_string = 32u // limit long_string length delete fake @@ -177,6 +177,16 @@ Faker's ``any_string`` is useful for testing string-processing functions: .. code-block:: das + def reverse_string(s : string) : string { + return build_string() $(var w) { + var i = length(s) - 1 + while (i >= 0) { + w |> write(slice(s, i, i + 1)) + i -- + } + } + } + var fake <- Faker() var failures = 0 fuzz(100) { diff --git a/doc/source/reference/tutorials/43_interfaces.rst b/doc/source/reference/tutorials/43_interfaces.rst index 662553170d..6b59c6ca81 100644 --- a/doc/source/reference/tutorials/43_interfaces.rst +++ b/doc/source/reference/tutorials/43_interfaces.rst @@ -123,12 +123,23 @@ to functions that accept the interface type: .. code-block:: das + [implements(IDrawable)] + class Circle { + radius : float + def Circle(r : float) { radius = r } + def IDrawable`draw(x, y : int) { + print("Circle(r={radius}) at ({x},{y})\n") + } + } + def draw_all(var objects : array) { for (obj in objects) { obj->draw(0, 0) } } + var circle = new Circle(3.0) + var sprite = new Sprite("tree") var drawables : array drawables |> push(circle as IDrawable) drawables |> push(sprite as IDrawable) @@ -225,7 +236,11 @@ reports an error at compile time: .. code-block:: text - error[30111]: Foo does not implement IBar.method + error[30926]: can't finalize structure annotation [implements] + example.das:8:5 + class Foo { + ^^^^ + Foo does not implement IBar.method Methods with default implementations are optional — the proxy inherits the default from the interface class. Only abstract diff --git a/doc/source/reference/tutorials/44_compile_and_run.rst b/doc/source/reference/tutorials/44_compile_and_run.rst index 585c3e9010..7b1ca5798d 100644 --- a/doc/source/reference/tutorials/44_compile_and_run.rst +++ b/doc/source/reference/tutorials/44_compile_and_run.rst @@ -36,9 +36,26 @@ The simplest way to compile daslang at runtime is ``compile``, which takes a module name, source text, and ``CodeOfPolicies``. The callback receives ``(ok : bool, program : smart_ptr, issues : string)``. +``src`` is the child program, held as an ordinary string. This one exports +a single function: + +.. das-doc: alt + +.. code-block:: das + + options gen2 + + [export] + def hello() { + print(" hello from compiled code!\n") + } + Always set ``cop.threadlock_context = true`` — this is required for ``invoke_in_context`` to work: +.. das-doc: given var src = "" +.. das-doc: given var context : smart_ptr + .. code-block:: das using() $(var cop : CodeOfPolicies) { @@ -105,6 +122,8 @@ You can compile code that does not exist on disk by injecting virtual files into the ``FileAccess`` object with ``set_file_source``. This is useful for code generation, REPLs, and eval-like tools: +.. das-doc: given var generated_code = "" + .. code-block:: das var inscope access <- make_file_access("") @@ -229,13 +248,17 @@ Compilation and simulation can fail. Always check the ``ok`` / ``sok`` flags. Runtime errors in the child context can be caught with ``try``/``recover``: +.. das-doc: given var bad_src = "" + .. code-block:: das // 1) Compilation error - compile("bad", bad_src, cop) $(ok, program, issues) { - if (!ok) { - print("compile error: {issues}\n") - return + using() $(var cop : CodeOfPolicies) { + compile("bad", bad_src, cop) $(ok, program, issues) { + if (!ok) { + print("compile error: {issues}\n") + return + } } } diff --git a/doc/source/reference/tutorials/45_debug_agents.rst b/doc/source/reference/tutorials/45_debug_agents.rst index 85a4213bb3..0ad0899444 100644 --- a/doc/source/reference/tutorials/45_debug_agents.rst +++ b/doc/source/reference/tutorials/45_debug_agents.rst @@ -61,6 +61,7 @@ of the program that stays resident: print(" has 'counter' = {has_debug_agent_context("counter")}\n") fork_debug_agent_context(@@install_counter) print(" has 'counter' = {has_debug_agent_context("counter")}\n") + } // output: // has 'counter' = false // has 'counter' = true @@ -75,11 +76,12 @@ output to stdout is suppressed. If it returns ``false``, output proceeds normally. This is how profiling tools, IDE log panels, and custom loggers -intercept program output: +intercept program output. The counter the agent bumps is a module-level +``var log_intercept_count : int = 0``: -.. code-block:: das +.. das-doc: given var log_intercept_count : int = 0 - var log_intercept_count : int = 0 +.. code-block:: das class LogAgent : DapiDebugAgent { def override onLog(context : Context?; at : LineInfo const?; @@ -129,11 +131,12 @@ agent's copy of module-level variables — not the caller's. The ``[pinvoke]`` annotation is required — it enables the context mutex needed for cross-context invocation. -To return values, pass a pointer to a result variable: +To return values, pass a pointer to a result variable. The counter here +is a module-level ``var agent_counter : int = 0``: -.. code-block:: das +.. das-doc: given var agent_counter : int = 0 - var agent_counter : int = 0 +.. code-block:: das [export, pinvoke] def agent_increment() { @@ -159,6 +162,7 @@ To return values, pass a pointer to a result variable: } print(" agent_counter (in agent) = {result}\n") print(" agent_counter (local) = {agent_counter}\n") + } // output: // agent_counter (in agent) = 3 // agent_counter (local) = 0 @@ -228,12 +232,14 @@ debuggers show custom watch variables and application diagnostics: report_context_state(ctx, "Diagnostics", "collection_count", unsafe(addr(tinfo)), unsafe(addr(collection_count))) } + } def override onVariable(var ctx : Context; category, name : string; info : TypeInfo; data : void?) : void { unsafe { let value = sprint_data(data, addr(info), print_flags.singleLine) print(" {category}: {name} = {value}\n") } + } } // Trigger collection @@ -263,6 +269,8 @@ Auto-start module pattern In modules, agents are installed automatically via a ``[_macro]`` function. Four guards ensure safe, single installation: +.. das-doc: fragment + .. code-block:: das [_macro] @@ -291,11 +299,12 @@ A common pattern is to create a plain ``DapiDebugAgent`` (no overrides) just to own a named context. Module-level variables in that context become shared state accessible via ``invoke_in_context``. This is the foundation of the -``[apply_in_context]`` pattern (Tutorial 46): +``[apply_in_context]`` pattern (Tutorial 46). The shared state is a +module-level ``var shared_data : int = 0``: -.. code-block:: das +.. das-doc: given var shared_data : int = 0 - var shared_data : int = 0 +.. code-block:: das [export, pinvoke] def add_data(amount : int) { @@ -309,10 +318,13 @@ in that context become shared state accessible via } } + [unused_argument(ctx)] def install_data_host(ctx : Context) { install_new_debug_agent(new DapiDebugAgent(), "data_host") } + fork_debug_agent_context(@@install_data_host) + // Multiple calls accumulate in the agent's copy unsafe { invoke_in_context(get_debug_agent_context("data_host"), "add_data", 10) diff --git a/doc/source/reference/tutorials/46_apply_in_context.rst b/doc/source/reference/tutorials/46_apply_in_context.rst index d0d39603c3..f4d4238335 100644 --- a/doc/source/reference/tutorials/46_apply_in_context.rst +++ b/doc/source/reference/tutorials/46_apply_in_context.rst @@ -31,11 +31,12 @@ Setting up a named context First, create a debug agent context to host shared state. A plain ``DapiDebugAgent`` with no overrides is sufficient — -the agent exists solely to own a named context: +the agent exists solely to own a named context. The state it hosts is a +module-level ``var counter : int = 0``: -.. code-block:: das +.. das-doc: given var counter : int = 0 - var counter : int = 0 +.. code-block:: das [unused_argument(ctx)] def install_service(ctx : Context) { @@ -97,9 +98,10 @@ copy stays at zero. Argument constraints ===================== -Arguments that cross context boundaries must use types that -can be safely marshalled. Reference-type arguments must be -marked ``implicit``: +Arguments that cross context boundaries must use types that can be safely +marshalled. A reference-type argument has to be spelled either ``implicit`` +or temporary (``#``); anything else is rejected with "argument needs +to be temporary or implicit": - ``string implicit`` — strings are reference types in daslang - ``var x : int& implicit`` — explicit reference parameters @@ -120,13 +122,13 @@ Value types (``int``, ``float``, ``bool``) work without annotation: set_counter_name("my_counter") // output: - // counter named 'my_counter', value = 12 + // counter named 'my_counter', value = 13 var val = 0 read_counter(val) print(" read_counter() -> val = {val}\n") // output: - // read_counter() -> val = 12 + // read_counter() -> val = 13 A cache service @@ -134,11 +136,12 @@ A cache service A practical use case: a shared cache backed by a table that lives in the agent context. Any module or context can call put / get / has -without worrying about which context they're in: +without worrying about which context they're in. The table is a +module-level ``var cache : table``: -.. code-block:: das +.. das-doc: given var cache : table - var cache : table +.. code-block:: das [unused_argument(ctx)] def install_cache(ctx : Context) { @@ -227,19 +230,21 @@ three parts: For a function with a return value, the expansion is similar to: +.. das-doc: fragment + .. code-block:: das def get_counter() : int { - verify(has_debug_agent_context("counter_service")) - var __res__ : int unsafe { - invoke_in_context( - get_debug_agent_context("counter_service"), - "counter_service`get_counter", - addr(__res__) - ) + verify(has_debug_agent_context("counter_service"), + "debug agent is not installed") + verify(addr(get_debug_agent_context("counter_service")) != addr(this_context()), + "agent context mismatch") + let __res__ : int + invoke_in_context(get_debug_agent_context("counter_service"), + @@CONTEXT`get_counter, unsafe(addr(__res__))) + return __res__ } - return __res__ } The annotation adds ``[pinvoke]`` to the generated context diff --git a/doc/source/reference/tutorials/47_data_walker.rst b/doc/source/reference/tutorials/47_data_walker.rst index 8daf4693b3..b5f3a0bf7d 100644 --- a/doc/source/reference/tutorials/47_data_walker.rst +++ b/doc/source/reference/tutorials/47_data_walker.rst @@ -32,8 +32,9 @@ Minimal walker — scalar types ============================== A ``DapiDataWalker`` subclass overrides only the callbacks you need. -All 87 methods default to no-ops, so a minimal walker that prints -integers, floats, strings, and booleans is very small: +All 110 methods carry a default — the twelve ``canVisit*`` filters return +``true``, every other callback does nothing — so a minimal walker that +prints integers, floats, strings, and booleans is very small: .. literalinclude:: ../../../../tutorials/language/47_data_walker.das :language: das @@ -42,6 +43,8 @@ integers, floats, strings, and booleans is very small: To walk a value, create the walker, wrap it with ``make_data_walker``, then call ``walk_data`` with a pointer and ``TypeInfo``: +.. das-doc: given class ScalarPrinter : DapiDataWalker {} + .. code-block:: das var walker = new ScalarPrinter() @@ -135,6 +138,12 @@ with key/value pairs: class ContainerPrinter : DapiDataWalker { indent : int = 0 + def pad() { + for (_ in range(indent)) { + print(" ") + } + } + def override beforeArrayData(ps : void?; stride : uint; count : uint64; ti : TypeInfo) : void { self->pad() @@ -157,9 +166,9 @@ with key/value pairs: // ... afterTable, beforeTableKey, afterTableKey, etc. } -Note that ``count``, ``index``, and ``pa.size`` are ``uint`` values -which print as hexadecimal by default — cast to ``int`` for decimal -output. +``count``, ``index``, and ``pa.size`` are unsigned (``uint64``), and +unsigned values print as hexadecimal — cast to ``int64`` (or ``int``, +when the value is known to be small) for decimal output. Tuples and variants @@ -174,6 +183,14 @@ alternative. ``beforeTupleEntry`` receives the element index, typedef Result = variant class TupleVariantPrinter : DapiDataWalker { + indent : int = 0 + + def pad() { + for (_ in range(indent)) { + print(" ") + } + } + def override beforeTupleEntry(ps : void?; ti : TypeInfo; pv : void?; idx : int; last : bool) : void { self->pad() @@ -248,6 +265,23 @@ for efficiency — no intermediate string concatenation: indent : int = 0 needComma : array + def comma() { + if (!empty(needComma) && needComma[length(needComma) - 1]) { + *writer |> write(",") + } + } + + def nl() { + *writer |> write("\n") + for (_ in range(indent)) { + *writer |> write(" ") + } + } + + def pushComma() { + needComma |> push(false) + } + // --- structures --- def override beforeStructure(ps : void?; si : StructInfo) : void { *writer |> write("\{") diff --git a/doc/source/reference/tutorials/48_apply.rst b/doc/source/reference/tutorials/48_apply.rst index c9997814aa..0b13f4e5e7 100644 --- a/doc/source/reference/tutorials/48_apply.rst +++ b/doc/source/reference/tutorials/48_apply.rst @@ -121,7 +121,7 @@ Tuples // _0 = 42 // _1 = hello - let point : tuple = (1.0, 2.0) + let point = (x = 1.0, y = 2.0) apply(point) $(name, field) { print(" {name} = {field}\n") } @@ -206,6 +206,9 @@ for each field. ``RttiValue`` is a variant with alternatives .. code-block:: das + let record = DbRecord(name = "Alice", email = "alice@example.com", + id = 42, age = 30) + apply(record) $(name : string; field; annotations) { var column_name = name var skip = false diff --git a/doc/source/reference/tutorials/50_soa.rst b/doc/source/reference/tutorials/50_soa.rst index a596b4ed2f..4f67b389f5 100644 --- a/doc/source/reference/tutorials/50_soa.rst +++ b/doc/source/reference/tutorials/50_soa.rst @@ -14,8 +14,8 @@ Structure-of-Arrays (SOA) This tutorial covers ``daslib/soa`` — a compile-time macro that transforms regular structs into a Structure-of-Arrays layout. The ``[soa]`` annotation generates parallel arrays for every field, plus all the container operations -you need (push, erase, pop, clear, resize, reserve, swap, from_array, -to_array). +you need (indexing, length, push, push_clone, emplace, erase, pop, clear, +resize, reserve, capacity, swap, from_array, to_array). Prerequisites: familiarity with structs and arrays. @@ -130,12 +130,12 @@ all work the same as on regular arrays: print("\n=== container operations ===\n") var soa : Particle`SOA - // push — move semantics + // push — clones the value into the column arrays soa |> push <| Particle(pos = float3(1.0), life = 10.0) soa |> push <| Particle(pos = float3(2.0), life = 20.0) soa |> push <| Particle(pos = float3(3.0), life = 30.0) - // push_clone — copy from a const value + // push_clone — same, spelled explicitly let p = Particle(pos = float3(4.0), life = 40.0) soa |> push_clone(p) @@ -213,7 +213,10 @@ any sorting algorithm: Bulk conversion — from_array, to_array ======================================== -Convert between AOS (``array``) and SOA (``T`SOA``) layouts: +Convert between AOS (``array``) and SOA (``T`SOA``) layouts. +``from_array`` **appends** — it reserves and pushes into the existing +columns, so call it on a fresh (or cleared) container unless you mean to +concatenate. ``to_array`` builds a new ``array``: .. code-block:: das @@ -300,8 +303,10 @@ SOA works well for game entity tables with mixed field types: id = 2, name = "mage", health = 60.0, alive = true) entities |> push <| GameEntity( id = 3, name = "archer", health = 80.0, alive = true) + entities |> push <| GameEntity( + id = 4, name = "thief", health = 50.0, alive = true) - // Apply damage + // Apply damage — the thief drops to -5 and dies for (it in entities) { it.health -= 55.0 if (it.health <= 0.0) { diff --git a/doc/source/reference/tutorials/51_delegate.rst b/doc/source/reference/tutorials/51_delegate.rst index 32365752e9..fc2fb68e11 100644 --- a/doc/source/reference/tutorials/51_delegate.rst +++ b/doc/source/reference/tutorials/51_delegate.rst @@ -42,7 +42,8 @@ Use ``typedef`` with the ``delegate()`` type macro. Pass either Construction from lambdas ========================== -Pass a lambda directly to the constructor: +Pass a lambda directly to the constructor. A delegate holds an array of +lambdas, so bind it with ``<-``: .. code-block:: das @@ -69,9 +70,8 @@ Pass a function pointer with ``@@``: var del <- OnDamage(@@apply_damage) let result = del.invoke("dragon", 25) - // output: - // apply_damage(dragon, 25) - // result = 50 + // prints: apply_damage(dragon, 25) + // result == 50 The function pointer is automatically wrapped in a lambda internally. @@ -119,14 +119,15 @@ The ``+=`` operator appends a handler to the invocation list: // All handlers are called; the last handler's return value is returned let result = del.invoke("orc", 10) - // output: + // prints: // handler 1: 10 // handler 2: 20 // apply_damage(orc, 10) - // result (from last handler) = 20 + // result == 20 — the value from the LAST handler For non-void delegates, all handlers execute in order but only the **last** -handler's return value is returned. +handler's return value is returned. Invoking a delegate with no handlers +registered is not an error: it returns a default-constructed value. Void delegates @@ -160,8 +161,8 @@ Utilities: ``empty``, ``length``, ``clear`` del.empty() // true del.length() // 0 - del += @() { ... } - del += @() { ... } + del += @() { print("tick!\n") } + del += @() { print("tock!\n") } del.empty() // false del.length() // 2 @@ -186,7 +187,10 @@ Delegates support ``for``-loop iteration via the ``each`` iterator: } // count = 2 -This gives read-only access to the underlying lambda list. +The loop walks the underlying lambda list. ``each`` is declared +``var self : T ==const``, so the delegate must be a ``var`` --- a ``let`` +delegate cannot be iterated at all --- and the loop variable is the +stored handler itself, not a copy. Practical example — event system @@ -213,10 +217,10 @@ Practical example — event system on_hit += @@log_hit let final_damage = on_hit.invoke("Hero", 12) - // output: + // prints: // armor: 12 -> 7 // log: Hero took 12 damage - // final_damage = 12 + // final_damage == 12 — log_hit ran last and returned the damage // Replace all handlers — god mode! on_hit := @(player : string; damage : int) : int { @@ -243,7 +247,7 @@ API summary +-----------------------------------+--------------------------------------------------+ | ``del += handler`` | Append handler | +-----------------------------------+--------------------------------------------------+ -| ``del.invoke(args...)`` | Call all handlers, return last result | +| ``del.invoke(args...)`` | Call all handlers, return last result | +-----------------------------------+--------------------------------------------------+ | ``del.empty()`` | True if no handlers registered | +-----------------------------------+--------------------------------------------------+ diff --git a/doc/source/reference/tutorials/52_option_and_result.rst b/doc/source/reference/tutorials/52_option_and_result.rst index aa96c03daa..c121e76390 100644 --- a/doc/source/reference/tutorials/52_option_and_result.rst +++ b/doc/source/reference/tutorials/52_option_and_result.rst @@ -10,10 +10,18 @@ Option and Result single: Tutorial; Monadic types single: Tutorial; Error handling -``daslib/option`` and ``daslib/result`` are two small template-structure modules +``daslib/option`` and ``daslib/result`` are two small template-tuple modules that give daslang a principled way to express "value or nothing" and "value or error" for ordinary value types. They compose through the existing ``|>`` pipe. +Both are declared with ``[template_tuple]``, so ``Option`` resolves to +``tuple<_has_value : bool; _value : T>`` and ``Result`` to +``tuple<_is_ok : bool; _value : T; _error : E>`` — a structural type with no +module home, which is what lets the same type reached through any chain of +``require ... public`` stay one canonical type. Because it is a tuple and not +a variant, there is no ``r is ok``: read the tag and the payload through the +accessor functions below. + ``Option`` models **absence**. ``Result`` models **failure with a reason**. Prefer them over sentinel return values (``-1``, ``""``) or nullable pointers (``T?``). @@ -21,8 +29,7 @@ pointers (``T?``). The payload may be any type, including non-copyable ones (``array``, ``table``, lambdas) — see `Non-copyable payloads`_. -Prerequisites: familiarity with template structures, blocks, and the pipe -operator ``|>``. +Prerequisites: familiarity with tuples, blocks, and the pipe operator ``|>``. .. code-block:: das @@ -87,22 +94,24 @@ it to chain fallible steps; the chain short-circuits on the first ``none``. Filtering and fallbacks ----------------------- +.. das-doc: given def expensive() : int { return 99 } .. code-block:: das let kept = some(10) |> filter() $(x : int) { return x > 5; } // some(10) let gone = some(3) |> filter() $(x : int) { return x > 5; } // none let eager = none(type) |> or_value(99) // some(99) - let lazy = none(type) |> or_else() $ { return some(expensive()); } + let lazy = none(type) |> or_else() { return some(expensive()); } Extraction ---------- +.. das-doc: given def compute() : int { return 7 } .. code-block:: das some(5) |> unwrap // 5 — panics on none none(type) |> unwrap_or(7) // 7 - some(5) |> unwrap_or_else() $ { return compute(); } // 5 (block not called) + some(5) |> unwrap_or_else() { return compute(); } // 5 (block not called) none(type) |> unwrap_or_default // "" Operators @@ -126,10 +135,12 @@ Operators Side-effect combinators ----------------------- +.. das-doc: given def do_work(x : int) { print("work {x}\n") } +.. das-doc: given def report_missing() { print("missing\n") } .. code-block:: das some(3) |> if_some() $(x : int) { do_work(x); } - none(type) |> if_none() $ { report_missing(); } + none(type) |> if_none() { report_missing(); } Pairing two options with ``zip`` -------------------------------- @@ -206,7 +217,7 @@ Same shape as ``Option``, plus ``unwrap_err`` for the error side: ok(42, type) |> unwrap // 42 err("e", type) |> unwrap_or(17) // 17 - err("xx", type) |> unwrap_or_else() $(e : string) { return length(e); } // 2 + err("xx", type) |> unwrap_or_else() $(msg : string) { return length(msg); } // 2 err("boom", type) |> unwrap_err // "boom" Bridging to Option @@ -226,9 +237,10 @@ Non-copyable payloads ``Option`` and ``Result`` work for any payload type, including non-copyable ones such as ``array``, ``table``, and lambdas. The -constructors and combinators dispatch internally on -``static_if (typeinfo can_copy(...))`` and clone (or move) on the non-copyable -branch — call sites look identical to the workhorse-type case. +cloning constructors (``some`` / ``ok`` / ``err``) and most combinators +dispatch internally on ``static_if (typeinfo can_copy(...))`` and clone on +the non-copyable branch; the ``move_`` constructors always move. Either +way, call sites look identical to the workhorse-type case. .. code-block:: das @@ -244,8 +256,8 @@ empty: .. code-block:: das var src <- [1, 2, 3] - let o = move_some(src) // src is now [] - // o |> unwrap == [1, 2, 3] + var o = move_some(src) // src is now [] + var got <- o |> move_unwrap // got is [1, 2, 3] ``Result`` provides the same pair on each side: ``ok`` / ``err`` clone, while ``move_ok`` / ``move_err`` move: @@ -253,13 +265,19 @@ empty: .. code-block:: das var payload <- [4, 5, 6] - let r = move_ok(payload, type) // payload is now [] - // r |> unwrap == [4, 5, 6] + var r = move_ok(payload, type) // payload is now [] + var back <- r |> move_unwrap // back is [4, 5, 6] + +Extraction has the same pair. ``unwrap`` **clones** the payload out, which +for an ``array`` or ``table`` is a deep copy; ``move_unwrap`` (and +``move_unwrap_err`` on the error side) moves it out instead, leaving the +option / result empty. Reach for the ``move_`` form whenever the payload is +non-copyable and you do not need the container afterwards. For workhorse types (``int``, ``float``, ``bool``, ``string``, …) ``move_some`` / -``move_ok`` / ``move_err`` are equivalent to their non-``move`` siblings — -prefer the plain form for readability and use the move-variants only when -you genuinely want to drain a non-copyable source. +``move_ok`` / ``move_err`` / ``move_unwrap`` are equivalent to their +non-``move`` siblings — prefer the plain form for readability and use the +move-variants only when you genuinely want to drain a non-copyable source. API reference at a glance @@ -289,7 +307,9 @@ Option +-------------------------------+-------------------------------------------------+ | ``or_else(o, f)`` | Lazy fallback on ``none`` | +-------------------------------+-------------------------------------------------+ -| ``unwrap(o)`` | Value or panic | +| ``unwrap(o)`` | Value or panic (clones the payload) | ++-------------------------------+-------------------------------------------------+ +| ``move_unwrap(o)`` | Value or panic, moved out of the option | +-------------------------------+-------------------------------------------------+ | ``unwrap_or(o, d)`` | Value or eager default | +-------------------------------+-------------------------------------------------+ @@ -305,7 +325,7 @@ Option +-------------------------------+-------------------------------------------------+ | ``zip(a, b)`` | Pair two options; some iff both some | +-------------------------------+-------------------------------------------------+ -| ``o ?? d`` | Unwrap-or-default operator | +| ``o ?? d`` | Value, or the given fallback ``d`` | +-------------------------------+-------------------------------------------------+ | ``a == b`` | Structural equality | +-------------------------------+-------------------------------------------------+ @@ -338,7 +358,9 @@ Result +---------------------------------+-------------------------------------------------+ | ``or_else(r, f)`` | Monadic recovery on err | +---------------------------------+-------------------------------------------------+ -| ``unwrap(r)`` | Ok value or panic | +| ``unwrap(r)`` | Ok value or panic (clones the payload) | ++---------------------------------+-------------------------------------------------+ +| ``move_unwrap(r)`` | Ok value or panic, moved out of the result | +---------------------------------+-------------------------------------------------+ | ``unwrap_or(r, d)`` | Ok value or eager default | +---------------------------------+-------------------------------------------------+ @@ -346,7 +368,9 @@ Result +---------------------------------+-------------------------------------------------+ | ``unwrap_or_default(r)`` | Ok value or ``default`` | +---------------------------------+-------------------------------------------------+ -| ``unwrap_err(r)`` | Err value or panic | +| ``unwrap_err(r)`` | Err value or panic (clones the payload) | ++---------------------------------+-------------------------------------------------+ +| ``move_unwrap_err(r)`` | Err value or panic, moved out of the result | +---------------------------------+-------------------------------------------------+ | ``expect_value(r, msg)`` | Ok value or panic with custom message | +---------------------------------+-------------------------------------------------+ @@ -358,7 +382,7 @@ Result +---------------------------------+-------------------------------------------------+ | ``err_to_option(r)`` | Discard value; ``Option`` | +---------------------------------+-------------------------------------------------+ -| ``r ?? d`` | Unwrap-or-default operator | +| ``r ?? d`` | Ok value, or the given fallback ``d`` | +---------------------------------+-------------------------------------------------+ | ``a == b`` | Structural equality | +---------------------------------+-------------------------------------------------+ diff --git a/doc/source/reference/tutorials/53_clargs.rst b/doc/source/reference/tutorials/53_clargs.rst index e32bad1574..c2ff999470 100644 --- a/doc/source/reference/tutorials/53_clargs.rst +++ b/doc/source/reference/tutorials/53_clargs.rst @@ -10,12 +10,13 @@ Command-Line Argument Parsing (clargs) single: Tutorial; CLI Flags single: Tutorial; CommandLineArgs -This tutorial covers ``daslib/clargs`` — a structure macro that generates a -type-safe CLI argument parser from an annotated struct. Declare your flags as -struct fields; the macro generates a ``parse_args`` function and runtime -reflection helpers automatically. +This tutorial covers ``daslib/clargs`` — a structure macro that turns an +annotated struct into a type-safe CLI parser. Declare your flags as struct +fields; the macro generates the parse functions and the runtime flag metadata +for that struct. -Prerequisites: familiarity with structs, enums, and arrays. +Prerequisites: structs, enums, arrays, and ``Result`` / ``Option`` +(:ref:`tutorial_option_and_result`). .. code-block:: das @@ -27,14 +28,22 @@ Prerequisites: familiarity with structs, enums, and arrays. Defining a CLI args struct =========================== -Annotate any struct with ``[CommandLineArgs]``. The macro generates three -functions for it: +Annotate any struct with ``[CommandLineArgs]``. The macro adds these functions +to your module: -* ``parse_args(var dst; args : array) : string`` — parse a provided list -* ``parse_args(var dst) : string`` — parse from the process command line - (post-``--`` slice; see *Reading process arguments* below) +* ``parse_args(type; args : array) : Result`` — parse a + provided list +* ``parse_args(type) : Result`` — parse the process command line, + read through ``get_user_args()`` +* ``parse_args_with_help(var dst : T; prog_name : string) : int`` — parse, and + answer ``--help`` / ``-h`` on the way (generated only for structs that + declare neither flag themselves) * ``get_command_info(type) : CommandInfo`` — runtime flag metadata +``parse_args`` builds a fresh ``T`` and hands it back inside a ``Result``. +``move_unwrap`` takes the struct out on success; ``unwrap_err`` gives a one-line +message naming the flag on failure. + Field names map to flag names with underscores converted to dashes (``output_file`` → ``--output-file``). @@ -48,36 +57,40 @@ Field names map to flag names with underscores converted to dashes timeout : float // --timeout } - var cfg = Config() - let err = parse_args(cfg, ["--name", "Alice", "--count=42", "--verbose", "--timeout=1.5"]) - // err == "" (success) + var res <- parse_args(type, ["--name", "Alice", "--count=42", "--verbose", "--timeout=1.5"]) + let cfg <- res |> move_unwrap // cfg.name == "Alice" // cfg.count == 42 // cfg.verbose == true // cfg.timeout == 1.5 Each flag accepts two forms: ``--flag value`` (space-separated) and -``--flag=value`` (equals sign, no space). +``--flag=value`` (equals sign, no space). A flag given twice keeps the last +value, so a wrapper script can set a default that the user overrides. Tokens +that look like flags but match no field are ignored, which lets another parser +share the same argv. Supported field types ====================== -+---------------------+-----------------------------------------------+ -| Field type | Accepted flag values | -+=====================+===============================================+ -| ``string`` | Any string | -+---------------------+-----------------------------------------------+ -| ``int`` | Decimal integer (optional leading ``+``/``-``)| -+---------------------+-----------------------------------------------+ -| ``float`` | Decimal float with optional exponent | -+---------------------+-----------------------------------------------+ -| ``bool`` | Bare flag (true), ``=true``, or ``=false`` | -+---------------------+-----------------------------------------------+ -| ``enum E`` | Enum entry name as a string (e.g. ``"Red"``) | -+---------------------+-----------------------------------------------+ -| ``array`` | Flag may appear multiple times | -+---------------------+-----------------------------------------------+ ++---------------------+----------------------------------------------------+ +| Field type | Accepted flag values | ++=====================+====================================================+ +| ``string`` | Any string | ++---------------------+----------------------------------------------------+ +| ``int`` | Decimal integer (optional leading ``+``/``-``) | ++---------------------+----------------------------------------------------+ +| ``float`` | Decimal float with optional exponent | ++---------------------+----------------------------------------------------+ +| ``bool`` | Bare flag (true), ``=true``, or ``=false`` | ++---------------------+----------------------------------------------------+ +| ``enum E`` | Enum entry name as a string (e.g. ``"Red"``) | ++---------------------+----------------------------------------------------+ +| ``array`` | Flag may appear multiple times | ++---------------------+----------------------------------------------------+ +| ``Option`` | Same as ``T``; ``none`` when the flag is absent | ++---------------------+----------------------------------------------------+ Bool flags @@ -88,30 +101,37 @@ to be explicit: .. code-block:: das - parse_args(cfg, ["--verbose"]) // verbose = true - parse_args(cfg, ["--verbose=false"]) // verbose = false + let on <- parse_args(type, ["--verbose"]) |> move_unwrap + let off <- parse_args(type, ["--verbose=false"]) |> move_unwrap + // on.verbose == true + // off.verbose == false Enum flags =========== -Pass the enum entry name as a string. An unknown name returns an error: +Pass the enum entry name as a string. An unknown name is an error: .. code-block:: das - enum LogLevel { Debug; Info; Warning; Error } + enum LogLevel { + Debug + Info + Warning + Error + } [CommandLineArgs] struct LogConfig { - level : LogLevel // --level (accepts "Debug", "Info", "Warning", "Error") + level : LogLevel // --level accepts "Debug", "Info", "Warning", "Error" } - var cfg = LogConfig() - parse_args(cfg, ["--level", "Warning"]) + var res <- parse_args(type, ["--level", "Warning"]) + let cfg <- res |> move_unwrap // cfg.level == LogLevel.Warning - let err = parse_args(cfg, ["--level", "Verbose"]) - // err == "--level: invalid enum value 'Verbose'" + let bad <- parse_args(type, ["--level", "Verbose"]) + // bad |> unwrap_err == "--level: invalid enum value 'Verbose'" Array flags @@ -127,16 +147,16 @@ array. Both forms (``--tag value`` and ``--tag=value``) are supported: tags : array } - var cfg = BuildConfig() - parse_args(cfg, ["--tags=debug", "--tags", "release", "--tags=profile"]) + var res <- parse_args(type, ["--tags=debug", "--tags", "release", "--tags=profile"]) + let cfg <- res |> move_unwrap // cfg.tags == ["debug", "release", "profile"] Required flags =============== -``@clarg_required`` makes a flag mandatory. ``parse_args`` returns an error if -the flag is absent: +``@clarg_required`` makes a flag mandatory. ``parse_args`` fails when the flag +is absent: .. code-block:: das @@ -148,31 +168,71 @@ the flag is absent: token : string } - var cfg = DeployConfig() + let missing <- parse_args(type, ["--host=prod.example.com"]) + // missing |> unwrap_err == "--token: missing required flag" + + var res <- parse_args(type, ["--host=prod.example.com", "--token=secret"]) + let cfg <- res |> move_unwrap + // cfg.token == "secret" + + +Optional fields +================ + +An absent ``int`` field parses as ``0``, which is also a value a user can type. +Declare the field ``Option`` when the two cases must stay apart: - let err1 = parse_args(cfg, ["--host=prod.example.com"]) - // err1 == "--token: missing required flag" +.. code-block:: das + + [CommandLineArgs] + struct RetryConfig { + @clarg_doc = "retry count; none when the flag is absent" + retries : Option + } + + var res <- parse_args(type, ["--retries=3"]) + let cfg <- res |> move_unwrap + if (cfg.retries |> is_some) { + print("retries = {cfg.retries |> unwrap}\n") + } else { + print("no --retries; the built-in policy decides\n") + } - let err2 = parse_args(cfg, ["--host=prod.example.com", "--token=secret"]) - // err2 == "" (success) +``string``, ``int``, ``float``, ``bool``, and ``array`` all wrap. An +enum field does not — wrap one and the macro fails at compile time. Field-level attributes ======================= -Three field annotations fine-tune parsing behaviour: +Field annotations fine-tune the flag a field becomes: ``@clarg_name = "flag"`` - Overrides the auto-generated flag name. + Overrides the auto-generated flag name. clargs prepends the ``--``. ``@clarg_short = "X"`` - Attaches a single-character short flag (see *Short flags* below). + Attaches a single-character short flag. ``@clarg_doc = "text"`` - Attaches a description used by help generators (see *Help rendering* below). + Description for the help renderer and for ``get_command_info``. ``@clarg_skip`` - Excludes the field from CLI parsing entirely (set it in code directly). + Excludes the field from the CLI schema. Set it in code instead. + +``@clarg_required`` + The flag must be supplied. + +``@clarg_positional`` + Fills the field from a bare token instead of a flag. + +``@clarg_count`` + Sums every occurrence of the flag into a plain ``int`` field. + +``@clarg_mutex_group = "name"`` + Puts the flag in a group whose members exclude each other. + +``@clarg_env = "NAME"`` + Reads an environment variable when the flag is absent. .. code-block:: das @@ -189,45 +249,187 @@ Three field annotations fine-tune parsing behaviour: internal_id : int // not a CLI flag } - var cfg = AppConfig() - cfg.internal_id = 99 - parse_args(cfg, ["--output-dir=/tmp/out", "--workers=4"]) - // cfg.out_path == "/tmp/out" - // cfg.workers == 4 - // cfg.internal_id == 99 + var res <- parse_args(type, ["--output-dir=/tmp/out", "--workers=4"]) + var cfg <- res |> move_unwrap + cfg.internal_id = 99 // set in code; there is no --internal-id + // cfg.out_path == "/tmp/out" + // cfg.workers == 4 + + +Positional arguments +===================== + +``@clarg_positional`` fields take the bare tokens of the command line, in +declaration order. A plain ``string`` positional is required, an +``Option`` one is optional, and an ``array`` one swallows every +remaining token: + +.. code-block:: das + + [CommandLineArgs] + struct PkgConfig { + @clarg_positional + @clarg_doc = "subcommand" + command : string + + @clarg_positional + @clarg_doc = "package name" + pkg : Option + + @clarg_doc = "project root" + root : string = "." + } + + var res <- parse_args(type, ["--root", "/tmp", "install", "dasImgui"]) + let cfg <- res |> move_unwrap + // cfg.command == "install" + // cfg.pkg |> unwrap == "dasImgui" + // cfg.root == "/tmp" + +Flags and positionals interleave freely: clargs first removes every flag it +knows (with its value, when the ``--flag value`` form is used), then reads what +is left in order. Unknown flag-shaped tokens are dropped rather than counted +as positionals. + +The macro rejects orders it cannot fill: an ``array`` positional must +be last, a required positional cannot follow an optional one, and +``@clarg_positional`` combines with none of ``@clarg_short``, ``@clarg_count``, +or ``@clarg_env``. A missing required positional reports +``": missing required positional argument"``. + + +Counting occurrences +===================== + +``@clarg_count`` on a plain ``int`` field counts how often the flag appears — +the ``-v -v -v`` idiom for verbosity levels. Long and short forms sum together: + +.. code-block:: das + + [CommandLineArgs] + struct VerbosityConfig { + @clarg_count + @clarg_short = "v" + @clarg_doc = "verbosity; repeat to raise the level" + verbose : int + } + + var res <- parse_args(type, ["-v", "-v", "--verbose"]) + let cfg <- res |> move_unwrap + // cfg.verbose == 3 + +Count flags carry no value, so ``--verbose=2`` reports +``"--verbose: count flag does not accept a value"``. Bundling is not +implemented: ``-vvv`` matches nothing and leaves the field at ``0``. + + +Mutually exclusive flags +========================= + +Flags that share a ``@clarg_mutex_group`` name may not appear together: + +.. code-block:: das + + [CommandLineArgs] + struct OutputConfig { + @clarg_mutex_group = "color" + color : bool + + @clarg_name = "no-color" + @clarg_mutex_group = "color" + no_color : bool + } + + let clash <- parse_args(type, ["--color", "--no-color"]) + // clash |> unwrap_err == "--color, --no-color: mutually exclusive (group 'color')" + +Groups are independent — one flag from each of two groups is fine. The check +reads the command line only, so a value that arrived from an environment twin +never collides with an explicit flag. + + +Environment twins +================== + +Any flag can also read an environment variable. ``@clarg_env = "NAME"`` names +one per field; ``[CommandLineArgs(env_prefix = "TOOL")]`` derives +``TOOL_LONG_NAME`` from every long flag name, with hyphens becoming +underscores. ``@clarg_env = ""`` opts a single field out of that derivation: + +.. code-block:: das + + [CommandLineArgs(env_prefix = "MYTOOL")] + struct ServeConfig { + @clarg_doc = "listen port" + port : int // --port, or MYTOOL_PORT + + @clarg_env = "MYTOOL_ROOT_DIR" + @clarg_doc = "document root" + root : string // --root, or MYTOOL_ROOT_DIR + + @clarg_env = "" + @clarg_doc = "debug logging" + debug : bool // --debug only + } + +The command line wins over the variable, and the variable wins over the field +initializer. Booleans read ``""``, ``0``, ``false``, ``off``, and ``no`` (any +case) as false and anything else as true. A variable that is set but empty +counts as unset. Garbage in an int, float, or enum variable is an error, the +same way it is on the command line. ``@clarg_required`` is satisfied by either +carrier. Positional, count, and repeatable fields have no environment form. + +Libraries have no argv at all, so their ambient knobs use the sibling +annotation ``[EnvConfig]``, which reads the same ``@clarg_doc`` / +``@clarg_env`` vocabulary and generates ``env_config(type) : T``. Error handling =============== -``parse_args`` returns an empty string on success or a descriptive error message -on the first failure: +``parse_args`` returns ``Result``. ``is_err`` reports the outcome, +``unwrap_err`` gives the message, and ``move_unwrap`` takes the parsed struct: .. code-block:: das - let err = parse_args(cfg, ["--count", "not_a_number"]) - // err == "--count: invalid int value 'not_a_number'" + [CommandLineArgs] + struct TypedConfig { + count : int + } - if (err != "") { - print("usage error: {err}\n") - return + def load_config(argv : array) : bool { + var res <- parse_args(type, argv) + if (res |> is_err) { + print("usage error: {res |> unwrap_err}\n") + return false + } + let cfg <- res |> move_unwrap + print("count = {cfg.count}\n") + return true } -Common error forms: + // load_config(["--count", "not_a_number"]) prints + // usage error: --count: invalid int value 'not_a_number' + +Parsing stops at the first failure. Common messages: * ``"--flag: invalid int value 'abc'"`` * ``"--flag: invalid float value 'abc'"`` * ``"--flag: invalid enum value 'Unknown'"`` -* ``"--flag: missing required flag"`` * ``"--flag: invalid bool value: 'yes'"`` +* ``"--flag: missing value"`` — the flag is there, its value is not +* ``"--flag: missing required flag"`` +* ``": missing required positional argument"`` +* ``"--flag: count flag does not accept a value"`` +* ``"--fast, --slow: mutually exclusive (group 'mode')"`` Short flags ============ ``@clarg_short = "X"`` attaches a single-character short flag. Both the long -and short forms are recognised by ``parse_args``, with identical value syntax -(``-X value``, ``-X=value``, or bare ``-X`` for booleans): +and short forms are recognised, with identical value syntax (``-X value``, +``-X=value``, or bare ``-X`` for booleans): .. code-block:: das @@ -246,8 +448,8 @@ and short forms are recognised by ``parse_args``, with identical value syntax tags : array } - var cfg = ServerConfig() - parse_args(cfg, ["-p", "8080", "-v", "-t=alpha", "-t=beta"]) + var res <- parse_args(type, ["-p", "8080", "-v", "-t=alpha", "-t=beta"]) + let cfg <- res |> move_unwrap // cfg.port == 8080 // cfg.verbose == true // cfg.tags == ["alpha", "beta"] @@ -255,8 +457,9 @@ and short forms are recognised by ``parse_args``, with identical value syntax Mixing long and short occurrences of an array flag preserves command-line order: ``--tags=a -t b --tags=c`` collects ``["a", "b", "c"]``. -Two fields cannot share a short flag. ``@clarg_short`` must be exactly one -character; both are compile-time errors from the macro. +The macro rejects a short flag it cannot parse back: two fields sharing one +character, a value longer than one character, and ``-``, ``=``, or whitespace +as the character are all compile-time errors. Introspection with ``get_command_info`` @@ -264,8 +467,8 @@ Introspection with ``get_command_info`` ``get_command_info(type)`` returns a ``CommandInfo`` value containing a ``CommandArgumentInfo`` entry for each parsed flag — the same data the -help renderer below uses, but exposed for programmatic inspection (custom -help formats, validation rules, configuration dumps, etc.): +help renderer uses, exposed for programmatic inspection (custom help formats, +validation rules, configuration dumps, shell completion): .. code-block:: das @@ -278,27 +481,44 @@ help formats, validation rules, configuration dumps, etc.): // -v, --verbose (tBool) verbose logging // -t, --tags (tString) tag (repeated) +``CommandInfo`` carries ``args`` plus ``has_user_help``, which is ``true`` when +the struct declares its own ``--help`` or ``-h`` flag. + ``CommandArgumentInfo`` fields: -+---------------------+------------------+--------------------------------------------------+ -| Field | Type | Description | -+=====================+==================+==================================================+ -| ``flag_name`` | ``string`` | Full flag string (e.g. ``"--output-dir"``) | -+---------------------+------------------+--------------------------------------------------+ -| ``short_flag_name`` | ``string`` | ``"-X"`` from ``@clarg_short``, or ``""`` | -+---------------------+------------------+--------------------------------------------------+ -| ``field_name`` | ``string`` | Struct field name | -+---------------------+------------------+--------------------------------------------------+ -| ``doc_string`` | ``string`` | ``@clarg_doc`` text, or ``""`` | -+---------------------+------------------+--------------------------------------------------+ -| ``is_required`` | ``bool`` | ``true`` if ``@clarg_required`` | -+---------------------+------------------+--------------------------------------------------+ -| ``is_array`` | ``bool`` | ``true`` for ``array`` fields | -+---------------------+------------------+--------------------------------------------------+ -| ``value_type`` | ``Type`` | Base type (``tString``, ``tInt``, etc.) | -+---------------------+------------------+--------------------------------------------------+ -| ``enum_values`` | ``array``| Entry names for enum fields, empty otherwise | -+---------------------+------------------+--------------------------------------------------+ ++------------------------+--------------------+----------------------------------------------------+ +| Field | Type | Description | ++========================+====================+====================================================+ +| ``field_name`` | ``string`` | Struct field name | ++------------------------+--------------------+----------------------------------------------------+ +| ``flag_name`` | ``string`` | ``--output-dir``, or ```` for a positional | ++------------------------+--------------------+----------------------------------------------------+ +| ``short_flag_name`` | ``string`` | ``-X`` from ``@clarg_short``, or ``""`` | ++------------------------+--------------------+----------------------------------------------------+ +| ``env_name`` | ``string`` | Environment twin, or ``""`` | ++------------------------+--------------------+----------------------------------------------------+ +| ``doc_string`` | ``string`` | ``@clarg_doc`` text, or ``""`` | ++------------------------+--------------------+----------------------------------------------------+ +| ``default_doc`` | ``string`` | Default as text; ``[EnvConfig]`` structs only | ++------------------------+--------------------+----------------------------------------------------+ +| ``value_type`` | ``Type`` | Base type (``tString``, ``tInt``, ...) | ++------------------------+--------------------+----------------------------------------------------+ +| ``is_path`` | ``bool`` | ``@clarg_path``; ``[EnvConfig]`` structs only | ++------------------------+--------------------+----------------------------------------------------+ +| ``is_array`` | ``bool`` | ``true`` for ``array`` fields | ++------------------------+--------------------+----------------------------------------------------+ +| ``is_required`` | ``bool`` | ``true`` if ``@clarg_required`` | ++------------------------+--------------------+----------------------------------------------------+ +| ``is_positional`` | ``bool`` | ``true`` if ``@clarg_positional`` | ++------------------------+--------------------+----------------------------------------------------+ +| ``is_optional_wrap`` | ``bool`` | ``true`` for ``Option`` fields | ++------------------------+--------------------+----------------------------------------------------+ +| ``is_count`` | ``bool`` | ``true`` if ``@clarg_count`` | ++------------------------+--------------------+----------------------------------------------------+ +| ``mutex_group`` | ``string`` | ``@clarg_mutex_group`` name, or ``""`` | ++------------------------+--------------------+----------------------------------------------------+ +| ``enum_values`` | ``array`` | Entry names for enum fields, empty otherwise | ++------------------------+--------------------+----------------------------------------------------+ Help rendering @@ -309,10 +529,13 @@ The library ships a ``--help`` renderer over ``CommandInfo``: * ``print_help(info, prog_name)`` — writes the formatted help to stdout. * ``format_help(info, prog_name) : string`` — returns the same text, useful in tests or when redirecting into a logger. +* ``format_help_with_auto_help(info, prog_name) : string`` — the same text + with a ``-h, --help`` row appended, unless the struct declares its own. -``parse_args`` does **not** auto-recognise ``--help`` — declare an explicit -``help`` field and check it after parsing. This keeps ``parse_args`` a pure -parser and leaves the exit policy to the caller: +``parse_args_with_help`` wires all of that up for a struct with no help flag of +its own. It prints the help and returns ``0`` when it sees ``--help`` or +``-h``, returns ``-1`` after a clean parse (your struct is populated), and +returns ``1`` on a parse error it has already logged at ``LOG_ERROR``: .. code-block:: das @@ -328,28 +551,18 @@ parser and leaves the exit policy to the caller: @clarg_short = "v" @clarg_doc = "verbose logging" verbose : bool - - @clarg_short = "h" - @clarg_doc = "show this help and exit" - help : bool } [export] def main() : int { var cfg = DemoConfig() - let err = parse_args(cfg) - if (err != "") { - print("error: {err}\n") - return 1 - } - if (cfg.help) { - print_help(get_command_info(type), "demo") - return 0 - } + let rc = parse_args_with_help(cfg, "demo") + return rc if (rc >= 0) + print("hello, {cfg.name}\n") return 0 } -The rendered output: +The rendered output, from a standalone ``daslang -exe`` binary: .. code-block:: text @@ -364,23 +577,64 @@ The rendered output: Format rules: * Per-flag line: ``-X, --long=PLACEHOLDER doc_string``. Fields with no - ``@clarg_short`` indent the short slot blank to keep the long flags vertically + ``@clarg_short`` leave the short slot blank to keep the long flags vertically aligned. * ``=PLACEHOLDER`` is the uppercased type name (``STRING`` / ``INT`` / ``FLOAT`` - / ``ENUM``). Bool flags omit it. + / ``ENUM``). Bool and count flags omit it. * Enum values render inline as ``(V1|V2|V3)``. -* ``(required)`` / ``(repeated)`` markers are appended to the doc column for - required flags and array flags respectively. -* Defaults are not shown — ``CommandInfo`` does not currently carry them. +* The doc column then picks up the markers that apply: ``(env: NAME)``, + ``(required)``, ``(repeated)`` for array flags, ``(repeats)`` for count + flags, and ``(mutex: group)``. +* Positionals get their own ``Positional arguments:`` block and appear in the + usage line as ````, ``[]``, or ``[...]`` for the array tail. +* The usage line follows the host. A standalone binary owns argv, so it reads + ``Usage: demo [flags]``; under the script host the same call renders + ``Usage: daslang demo -- [flags]``, which is the line a user can copy. +* Defaults are not rendered. Write them into ``@clarg_doc`` when they matter. + +Under the script host, ``daslang`` itself takes ``-h`` and ``--help`` before it +forwards anything to your script, so the auto help flag is reachable only from +a standalone binary. A script that needs a help flag declares its own and +wires it to ``-?``, which the host leaves alone. A field that spells +``--help`` or ``-h`` also turns ``parse_args_with_help`` off for that struct — +once you name the flag, the exit policy is yours: + +.. code-block:: das + + [CommandLineArgs] + struct ScriptConfig { + @clarg_short = "n" + @clarg_doc = "user's display name" + name : string + + @clarg_short = "?" + @clarg_name = "show-help" + @clarg_doc = "show this help and exit" + show_help : bool + } + + var res <- parse_args(type) + if (res |> is_err) { + print("error: {res |> unwrap_err}\n\n") + print_help(get_command_info(type), "demo") + } else { + let cfg <- res |> move_unwrap + if (cfg.show_help) { + print_help(get_command_info(type), "demo") + } + } Reading process arguments ========================== -Two helpers feed ``argv`` into ``parse_args``, depending on how the program is -invoked. Each has a zero-argument form (operating on the live process command -line) and a one-argument form (taking an explicit ``argv`` array, useful in -tests): +``parse_args(type)`` reads argv through ``get_user_args()``, which picks the +slice that belongs to your program: + +``get_user_args()`` + ``argv[1..]`` for a standalone ``daslang -exe`` binary, and the slice after + the ``--`` separator under the interpreter or the JIT. One spelling works + in all three, which is why the generated ``parse_args`` uses it. ``get_cli_arguments() / get_cli_arguments(argv)`` — script-style Returns the slice **after** the ``--`` separator in argv (or empty if no @@ -391,9 +645,6 @@ tests): daslang.exe my_script.das -- --name Alice --count 5 - The no-argument ``parse_args(cfg)`` overload generated by the macro calls - this internally. - ``get_program_args() / get_program_args(argv)`` — standalone-tool style Returns ``argv[1..]`` — the full argv with the program name stripped. Use this for AOT'd binaries that own the full argv themselves and have no @@ -401,15 +652,22 @@ tests): .. code-block:: das - [export] - def main() : int { - var cfg = FmtConfig() - let err = parse_args(cfg, get_program_args()) - if (err != "") { - print("error: {err}\n") - return 1 - } - return 0 + [CommandLineArgs] + struct FmtConfig { + @clarg_doc = "rewrite files in place" + write : bool + + @clarg_positional + @clarg_doc = "files to format" + files : array + } + + var res <- parse_args(type, get_program_args()) + if (res |> is_err) { + print("error: {res |> unwrap_err}\n") + } else { + let cfg <- res |> move_unwrap + print("formatting {length(cfg.files)} files\n") } The explicit-argv overloads make the splitting logic unit-testable without diff --git a/doc/source/reference/tutorials/56_linq_query.rst b/doc/source/reference/tutorials/56_linq_query.rst index 436c2f904b..e11a667e46 100644 --- a/doc/source/reference/tutorials/56_linq_query.rst +++ b/doc/source/reference/tutorials/56_linq_query.rst @@ -20,6 +20,40 @@ It assumes you have read :ref:`tutorial_linq` (the pipe-form linq surface and th ``_fold`` macro). The full clause grammar and per-source details live in the :ref:`linq_das` reference page. +Sample data +=========== + +Every query on this page runs over the same two record types and three arrays: + +.. code-block:: das + + struct Car { + name : string + brand : string + price : int + } + + struct BrandHQ { + brand : string + country : string + } + + let cars <- [Car(name = "i3", brand = "bmw", price = 100), + Car(name = "m3", brand = "bmw", price = 250), + Car(name = "a4", brand = "audi", price = 200), + Car(name = "a8", brand = "audi", price = 400), + Car(name = "rio", brand = "kia", price = 50)] + + let hqs <- [BrandHQ(brand = "bmw", country = "DE"), + BrandHQ(brand = "audi", country = "DE"), + BrandHQ(brand = "kia", country = "KR")] + + // hqs2 adds a brand with no cars, for the outer group join below + let hqs2 <- [BrandHQ(brand = "bmw", country = "DE"), + BrandHQ(brand = "audi", country = "DE"), + BrandHQ(brand = "kia", country = "KR"), + BrandHQ(brand = "tesla", country = "US")] + from / where / select ===================== @@ -74,7 +108,7 @@ down to ``GROUP BY`` + ``COUNT/SUM/AVG/...``. select (brand = g.key, count = g |> length, total = g |> select($(u : Car) => u.price) |> sum) %% - // audi: count=2 total=600 ; kia: count=1 total=50 ; bmw: count=2 total=350 + // bmw: count=2 total=350 ; audi: count=2 total=600 ; kia: count=1 total=50 The continuation may itself filter / order the groups: @@ -112,6 +146,10 @@ range variable. .. code-block:: das + let discounted <- %linq! from c in cars let net = c.price - 20 + where net > 100 select (n = c.name, p = net) %% + // i3 (80) and rio (30) fall out; m3 230, a4 180, a8 380 + let located <- %linq! from c in cars join h in hqs on c.brand equals h.brand select (car = c.name, country = h.country) %% @@ -134,9 +172,10 @@ cars, so it still appears with count 0. total = g |> select($(u : Car) => u.price) |> sum) %% // output (one row per HQ; tesla has n=0 total=0) -It is array-source, select-terminal only (a pre-join ``where`` is allowed); over -a SQL source the group join is in-memory only. See :ref:`linq_das_join` for the -exact scope. +It is array-source, select-terminal only (a pre-join ``where`` is allowed). +``_group_join`` has no SQL push-down, so over a SQL source the query is +rejected at compile time — write that aggregate in raw SQL instead. See +:ref:`linq_das_join` for the exact scope. .. seealso:: diff --git a/doc/source/reference/tutorials/daStrudel_01_hello_pattern.rst b/doc/source/reference/tutorials/daStrudel_01_hello_pattern.rst index ce11226d4b..4507717ef6 100644 --- a/doc/source/reference/tutorials/daStrudel_01_hello_pattern.rst +++ b/doc/source/reference/tutorials/daStrudel_01_hello_pattern.rst @@ -23,6 +23,8 @@ What is a Pattern? A ``Pattern`` is **a pure function from a query window to a list of events**. Concretely: +.. das-doc: signatures + .. code-block:: das typedef Pattern = lambda<(span : TimeSpan) : array> @@ -41,6 +43,8 @@ What is a Hap? A ``Hap`` is one event with two timestamps and a value: +.. das-doc: signatures + .. code-block:: das struct Hap { @@ -132,8 +136,10 @@ optional second argument picks an oscillator: let pat <- note("c4", "sine") |> sustain(0.5) play(pat, 4.0) -``sustain(0.5)`` says each note holds for half its slot. Without it the -default ADSR envelope would cut the note very short. +``sustain(0.5)`` sets the envelope's sustain **level**: the note holds +at half amplitude for as long as it is scheduled. It is a volume, not a +duration — tutorial 10 covers the whole ADSR envelope, including what +the defaults do when you set nothing. The pipe operator ``|>`` is just function call with the left side as the first argument — ``pat |> sustain(0.5)`` is identical to diff --git a/doc/source/reference/tutorials/daStrudel_02_mini_notation_fundamentals.rst b/doc/source/reference/tutorials/daStrudel_02_mini_notation_fundamentals.rst index 04db450136..415f19e2eb 100644 --- a/doc/source/reference/tutorials/daStrudel_02_mini_notation_fundamentals.rst +++ b/doc/source/reference/tutorials/daStrudel_02_mini_notation_fundamentals.rst @@ -22,6 +22,9 @@ Part A: Sequences and rests The simplest mini-notation is a space-separated list of tokens. Each token gets an equal slice of the cycle: +.. das-doc: given require strudel/strudel public +.. das-doc: given def play(var pat : Pattern; seconds : float = 4.0; cps : double = 0.5lf) { } + .. code-block:: das let pat <- s("bd sd ~ cp") diff --git a/doc/source/reference/tutorials/daStrudel_03_mini_notation_advanced.rst b/doc/source/reference/tutorials/daStrudel_03_mini_notation_advanced.rst index 30e7fb8965..9bd7660a05 100644 --- a/doc/source/reference/tutorials/daStrudel_03_mini_notation_advanced.rst +++ b/doc/source/reference/tutorials/daStrudel_03_mini_notation_advanced.rst @@ -26,6 +26,9 @@ Part A: Alternation with ``< >`` Angle brackets pick **one element per cycle**, advancing on each cycle and looping when it runs out: +.. das-doc: given require strudel/strudel public +.. das-doc: given def play(var pat : Pattern; seconds : float = 4.0; cps : double = 0.5lf) { } + .. code-block:: das let pat <- s("") @@ -79,9 +82,9 @@ probability. Hi-hats are the canonical use: play(pat, 6.0) The kick and snare are reliable; the hats appear roughly half the time, -randomly per cycle. Run it twice and you get different results — the -RNG seed is tied to the cycle position so the rhythm is deterministic -within one play, but the pattern feels human. +varying from cycle to cycle. The drop is decided by hashing each event's +start time, so a given pattern loses the same hats on every run. It +feels human, but it is reproducible — not random. You can also write ``hh?0.25`` to use a different drop probability — the default ``?`` is shorthand for ``?0.5``. @@ -89,21 +92,26 @@ the default ``?`` is shorthand for ``?0.5``. Part D: Replicate with ``!N`` ============================= -Postfix ``!N`` is like ``*N`` from tutorial 02 but expands the element -into N **parent slots** instead of squeezing them into one: +Postfix ``!N`` repeats an element N times. The parser reads it as a +modifier on the element it follows, exactly like ``*N``, so the repeats +land **inside that element's own slot**: .. code-block:: das let pat <- s("bd!3 sd") play(pat, 4.0) -This expands to four equal slots ``bd bd bd sd`` — three kicks then a -snare. Compare to ``bd*3 sd`` which packs three kicks into one slot -followed by a snare in the second. +``"bd!3 sd"`` is two slots: three kicks packed into the first half, one +snare in the second. It produces the same haps as ``"bd*3 sd"`` — query +both by hand and the timestamps match. + +To give each repeat a slot of its own — four equal slots, three kicks +then a snare — write the element out: -The mental model: ``*N`` divides time, ``!N`` adds slots. Use ``*`` to -make things faster within one slot, ``!`` to repeat the same element -across the parent sequence. +.. code-block:: das + + let pat <- s("bd bd bd sd") + play(pat, 4.0) Part E: Euclidean rhythms with ``(k,n)`` and ``(k,n,rot)`` ========================================================== diff --git a/doc/source/reference/tutorials/daStrudel_04_time_manipulation.rst b/doc/source/reference/tutorials/daStrudel_04_time_manipulation.rst index 0d6cce074d..974e10fd41 100644 --- a/doc/source/reference/tutorials/daStrudel_04_time_manipulation.rst +++ b/doc/source/reference/tutorials/daStrudel_04_time_manipulation.rst @@ -37,6 +37,9 @@ Part A: ``fast(N)`` ``fast(N)`` repeats the pattern N times per cycle: +.. das-doc: given require strudel/strudel public +.. das-doc: given def play(var pat : Pattern; seconds : float = 4.0; cps : double = 0.5lf) { } + .. code-block:: das let pat <- note("c4 e4 g4 c5", "sine") |> sustain(0.4) |> fast(2.0lf) @@ -82,10 +85,11 @@ stereo widening (tutorial 07 covers combinators). Part D: ``hurry(N)`` ==================== -``hurry(N)`` speeds time up by N **and** multiplies the per-event -playback ``speed`` by N. For sample-based sounds (drum hits, -field-recordings), speed scales playback rate, which means pitch goes -up by ``log2(N)`` octaves: +``hurry(N)`` speeds time up by N **and** sets the per-event playback +``speed`` to N — it is ``fast(N)`` followed by ``speed(N)``, so a +``speed`` you set earlier in the chain is overwritten. For sample-based +sounds (drum hits, field-recordings), speed scales playback rate, which +means pitch goes up by ``log2(N)`` octaves: .. code-block:: das diff --git a/doc/source/reference/tutorials/daStrudel_05_euclidean_rhythms.rst b/doc/source/reference/tutorials/daStrudel_05_euclidean_rhythms.rst index 19708ce471..2493b25c57 100644 --- a/doc/source/reference/tutorials/daStrudel_05_euclidean_rhythms.rst +++ b/doc/source/reference/tutorials/daStrudel_05_euclidean_rhythms.rst @@ -28,6 +28,9 @@ Before the combinator, here is the raw algorithm. ``bjorklund(3, 8)`` returns a length-8 ``array`` with three ``true`` entries distributed evenly: +.. das-doc: given require strudel/strudel public +.. das-doc: given def play(var pat : Pattern; seconds : float = 4.0; cps : double = 0.5lf) { } + .. code-block:: das let r <- bjorklund(3, 8) @@ -100,9 +103,11 @@ with different ``k`` and you have a polyrhythm: Both layers complete every cycle (``n = 8`` in both), but the **3 onsets** of the kick and the **5 onsets** of the hat sit at different -grid positions, so the perceived feel is polyrhythmic. Use mismatched -``n`` values (e.g. ``(3, 8)`` against ``(2, 5)``) and the cycle length -becomes ``lcm(8, 5) = 40`` — the patterns realign every 40 steps. +grid positions, so the perceived feel is polyrhythmic. Mismatched ``n`` +values (e.g. ``(3, 8)`` against ``(2, 5)``) cross two grids — eighths +against fifths — inside the same cycle. ``euclid`` always fits its ``n`` +steps into one cycle, so both layers restart together on every cycle +boundary: the polyrhythm lives inside the cycle, not across cycles. Part E: ``euclidRot(pat, k, n, rot)`` — rotate the onsets ========================================================= diff --git a/doc/source/reference/tutorials/daStrudel_06_stacking_combining.rst b/doc/source/reference/tutorials/daStrudel_06_stacking_combining.rst index 2bdc50f3f0..45d1acdfc5 100644 --- a/doc/source/reference/tutorials/daStrudel_06_stacking_combining.rst +++ b/doc/source/reference/tutorials/daStrudel_06_stacking_combining.rst @@ -32,6 +32,8 @@ Part A: ``stack`` — play patterns at the same time ``stack`` takes an array of patterns and merges their events into the same cycle. Use it to combine independent voices (bass + lead + drums): +.. das-doc: given require strudel/strudel public + .. code-block:: das let pat <- stack([ diff --git a/doc/source/reference/tutorials/daStrudel_07_per_voice_fx.rst b/doc/source/reference/tutorials/daStrudel_07_per_voice_fx.rst index 87f70822e5..72fa111ee2 100644 --- a/doc/source/reference/tutorials/daStrudel_07_per_voice_fx.rst +++ b/doc/source/reference/tutorials/daStrudel_07_per_voice_fx.rst @@ -22,6 +22,8 @@ Part A: ``jux`` — stereo splitting via a transform ``fn(pat)`` in the right. The classic move is to reverse the right side, producing the trademark live-coding stereo wobble: +.. das-doc: given require strudel/strudel public + .. code-block:: das let pat <- jux(note("c4 e4 g4 c5", "sine") |> sustain(0.4), @(p) => rev(p)) diff --git a/doc/source/reference/tutorials/daStrudel_08_effects_filters.rst b/doc/source/reference/tutorials/daStrudel_08_effects_filters.rst index 6abee4bbe0..770b5442c8 100644 --- a/doc/source/reference/tutorials/daStrudel_08_effects_filters.rst +++ b/doc/source/reference/tutorials/daStrudel_08_effects_filters.rst @@ -42,6 +42,8 @@ Part A: per-voice filters — ``lpf`` and ``hpf`` Compare a muffled sawtooth (``lpf 200``) against a thin one (``hpf 2000``): +.. das-doc: given require strudel/strudel public + .. code-block:: das // Dark, all-bass: @@ -57,8 +59,9 @@ Part B: per-orbit reverb — ``room`` and ``roomsize`` ==================================================== ``room(amount)`` sets the wet send to the orbit's reverb bus; -``roomsize(N)`` controls the room dimensions. Both live on the bus, so -all voices on the same orbit share one reverb instance: +``roomsize(N)`` sets that reverb's decay time in seconds — how big the +room feels. Both live on the bus, so all voices on the same orbit share +one reverb instance: .. code-block:: das diff --git a/doc/source/reference/tutorials/daStrudel_09_signals_modulation.rst b/doc/source/reference/tutorials/daStrudel_09_signals_modulation.rst index ea6ad884de..8f8a3fbca2 100644 --- a/doc/source/reference/tutorials/daStrudel_09_signals_modulation.rst +++ b/doc/source/reference/tutorials/daStrudel_09_signals_modulation.rst @@ -25,6 +25,9 @@ Query one by hand and you get a number, not a drum hit. ``sine()`` is a *continuous* signal — one query window yields one Hap holding the wave's value at the start of that window: +.. das-doc: given require strudel/strudel public +.. das-doc: given def play(var pat : Pattern; cps : double = 0.5lf; seconds : double = 4.0lf) { } + .. code-block:: das let sig <- sine() diff --git a/doc/source/reference/tutorials/daStrudel_10_adsr_envelopes.rst b/doc/source/reference/tutorials/daStrudel_10_adsr_envelopes.rst index b45c7f76f8..0595969bc7 100644 --- a/doc/source/reference/tutorials/daStrudel_10_adsr_envelopes.rst +++ b/doc/source/reference/tutorials/daStrudel_10_adsr_envelopes.rst @@ -43,6 +43,9 @@ With no ADSR setters at all, the resolver picks a held-tone envelope: the note rings for its full scheduled duration, instead of decaying to silence in 50ms as in older defaults. +.. das-doc: given require strudel/strudel public +.. das-doc: given def play(var pat : Pattern; cps : double = 0.5lf; seconds : double = 4.0lf) { } + .. code-block:: das let pat <- note("c4 e4 g4 c5", "sine") diff --git a/doc/source/reference/tutorials/daStrudel_11_scales_music_theory.rst b/doc/source/reference/tutorials/daStrudel_11_scales_music_theory.rst index dcf14632c7..924e579591 100644 --- a/doc/source/reference/tutorials/daStrudel_11_scales_music_theory.rst +++ b/doc/source/reference/tutorials/daStrudel_11_scales_music_theory.rst @@ -21,6 +21,8 @@ Part A: ``scale_pattern`` — degrees against a scale ``n(notation) |> scale(scale_def) |> sound(sound)``. The same ``"0 2 4 6"`` against C major and C minor is a one-line A/B test: +.. das-doc: given require strudel/strudel public + .. code-block:: das let pat <- stack([ diff --git a/doc/source/reference/tutorials/daStrudel_13_samples.rst b/doc/source/reference/tutorials/daStrudel_13_samples.rst index d80e396c66..d22388da51 100644 --- a/doc/source/reference/tutorials/daStrudel_13_samples.rst +++ b/doc/source/reference/tutorials/daStrudel_13_samples.rst @@ -33,6 +33,10 @@ Every audio file inside the folder becomes an indexed variation: ``strudel_load_sound`` loads one folder under a given name. Files are sorted alphabetically, so the ``:0``, ``:1``, ... indices are stable: +.. das-doc: given require strudel/strudel public +.. das-doc: given require daslib/fio +.. das-doc: given let MEDIA = "{get_das_root()}/examples/media" + .. code-block:: das require strudel/strudel_player @@ -104,8 +108,8 @@ Part E: Playing a Slice with ``begin`` / ``end_pos`` ``begin(x)`` and ``end_pos(x)`` take normalized positions in 0..1 and play only the ``[begin, end)`` window of a sample. ``begin(0.3)`` skips -the first 30 % of the file; ``end_pos(0.8)`` trims the last 20 %. (The -setter is ``end_pos`` because ``end`` is a reserved word.) +the first 30 % of the file; ``end_pos(0.8)`` trims the last 20 %. The +setter and the ``Event`` field are both spelled ``end_pos``. .. code-block:: das @@ -135,17 +139,24 @@ Three ways to cut a sample into ``n`` grains: Part G: Loading a Whole Directory with ``strudel_load_sample_dir`` ================================================================== -``strudel_load_sample_dir(root)`` loads every audio file directly under -``root``, using each filename as the sound name. It is the quick way to -bring in an external pack such as `tidalcycles/dirt-samples -`_. The built-in drums -need no loading at all, so reach for this only when you want sounds -beyond ``bd`` / ``sd`` / ``hh`` / ``cp``: +``strudel_load_sample_dir(root)`` walks the **folders** directly under +``root`` and loads each one as a sound named after the folder — one +``strudel_load_sound`` call per folder, done for you. That is the +layout an external pack such as `tidalcycles/dirt-samples +`_ ships in, and the layout +``examples/media/drums`` uses: .. code-block:: das - strudel_load_sample_dir("{MEDIA}/audio") - let pat <- s("gong") |> gain(0.6) + strudel_load_sample_dir("{MEDIA}/drums") + let pat <- s("bd sd hh cp") |> gain(0.6) + +One call replaces the four in Part A, and it also picks up the folders +they skipped (``ride``, ``tom_low``, ``sidestick``, …). Point it at a +folder of loose audio files and it loads nothing: a file is not a folder +of variations, so there is nothing to index. For loose files, call +``strudel_load_sound(folder, name)`` and pick the name yourself — that +is how ``examples/media/audio`` becomes the sound ``gong``. .. seealso:: diff --git a/doc/source/reference/tutorials/daStrudel_14_sf2_soundfont.rst b/doc/source/reference/tutorials/daStrudel_14_sf2_soundfont.rst index 3710ea3f0f..637f947a70 100644 --- a/doc/source/reference/tutorials/daStrudel_14_sf2_soundfont.rst +++ b/doc/source/reference/tutorials/daStrudel_14_sf2_soundfont.rst @@ -24,6 +24,8 @@ Part A: Loading a SoundFont ``strudel_load_sf2`` reads a ``.sf2`` file and activates it for the next patterns. It returns ``true`` on success: +.. das-doc: given require strudel/strudel public + .. code-block:: das require strudel/strudel_player @@ -59,7 +61,7 @@ Program Instrument 40 violin 56 trumpet 73 flute -81 lead square +80 square lead ================= ======================= Use the program number when the name lookup does not match the preset diff --git a/doc/source/reference/tutorials/daStrudel_15_midi_files.rst b/doc/source/reference/tutorials/daStrudel_15_midi_files.rst index 45dd3bf748..e043dc7ddc 100644 --- a/doc/source/reference/tutorials/daStrudel_15_midi_files.rst +++ b/doc/source/reference/tutorials/daStrudel_15_midi_files.rst @@ -19,6 +19,8 @@ Part A: Parsing ``load_midi`` reads a ``.mid`` file and returns a ``MidiFile`` struct: +.. das-doc: given let media_path = "examples/media" + .. code-block:: das require strudel/strudel_midi @@ -65,6 +67,7 @@ and a built-in piano + drum sample bank. The lifecycle: .. code-block:: das require strudel/strudel_midi_player + require daslib/fio // sleep midi_load_samples(media_path) // load built-in banks from media folder midi_init() // start the playback thread diff --git a/doc/source/reference/tutorials/daStrudel_16_live_reloading.rst b/doc/source/reference/tutorials/daStrudel_16_live_reloading.rst index b73ad745d1..2665282eef 100644 --- a/doc/source/reference/tutorials/daStrudel_16_live_reloading.rst +++ b/doc/source/reference/tutorials/daStrudel_16_live_reloading.rst @@ -31,6 +31,8 @@ Part A: Lifecycle Functions Scripts that export ``init`` / ``update`` / ``shutdown`` run in **lifecycle mode** under daslang-live: +.. das-doc: signatures + .. code-block:: das [export] def init() { ... } // called on start and after every reload @@ -50,6 +52,8 @@ player state (wall time, CPS/BPM, SID, and the sample bank) and the loaded SF2 data into the persistent byte store. Tracks are not serialised — ``strudel_init`` re-adds them on every reload: +.. das-doc: given def load_samples() { } + .. code-block:: das require strudel/strudel public @@ -112,17 +116,19 @@ Part D: Custom Persistent State strudel_live handles the player. Your own state needs its own hooks — use ``[before_reload]`` / ``[after_reload]`` and the persistent byte -store to keep globals alive: +store to keep globals alive. Here the global is a module-scope +``var g_reload_count : int = 0``, and the two hooks write it out and +read it back: -.. code-block:: das +.. das-doc: given var g_reload_count : int = 0 - var g_reload_count : int = 0 +.. code-block:: das [before_reload] def save_my_state() { var data : array data |> resize(4) - unsafe { *reinterpret(addr(data[0])) = g_reload_count; } + unsafe { *addr(data[0]) = g_reload_count; } live_store_bytes("tutorial15_reload_count", data) } @@ -130,7 +136,7 @@ store to keep globals alive: def restore_my_state() { var data : array if (live_load_bytes("tutorial15_reload_count", data) && length(data) >= 4) { - unsafe { g_reload_count = *reinterpret(addr(data[0])); } + unsafe { g_reload_count = *addr(data[0]); } g_reload_count ++ } } diff --git a/doc/source/reference/tutorials/daStrudel_17_hrtf_position.rst b/doc/source/reference/tutorials/daStrudel_17_hrtf_position.rst index 536e62b185..78e6b777ed 100644 --- a/doc/source/reference/tutorials/daStrudel_17_hrtf_position.rst +++ b/doc/source/reference/tutorials/daStrudel_17_hrtf_position.rst @@ -37,6 +37,9 @@ when you want spatial cues plain pan can't provide. API === +.. das-doc: given require strudel/strudel public +.. das-doc: signatures + .. code-block:: das pat |> hrtf_azimuth(deg) // numeric or pattern-valued, -180..180 diff --git a/doc/source/reference/tutorials/daStrudel_18_sfx_lab.rst b/doc/source/reference/tutorials/daStrudel_18_sfx_lab.rst index cf61553b7b..70dde5110c 100644 --- a/doc/source/reference/tutorials/daStrudel_18_sfx_lab.rst +++ b/doc/source/reference/tutorials/daStrudel_18_sfx_lab.rst @@ -115,6 +115,8 @@ readable daslang — a self-contained function with the standard one-shot drum signature that you can paste straight into your own code (or into ``strudel_synth``) and call with no editor and no ``.sfx`` file: +.. das-doc: given require strudel/strudel_sfx + .. code-block:: das // Generated by the SFX Lab. Drop-in render function; requires: diff --git a/doc/source/reference/tutorials/daStrudel_19_one_shots.rst b/doc/source/reference/tutorials/daStrudel_19_one_shots.rst index 81661aee0e..231fe7540b 100644 --- a/doc/source/reference/tutorials/daStrudel_19_one_shots.rst +++ b/doc/source/reference/tutorials/daStrudel_19_one_shots.rst @@ -33,6 +33,9 @@ You can see the *stop* without listening: query the gated pattern and watch the onsets vanish past the window. ``once`` keeps cycle 0; ``playFor(pat, n)`` keeps the first ``n`` cycles: +.. das-doc: given require strudel/strudel public +.. das-doc: given require strudel/strudel_player public + .. code-block:: das let phrase <- once(note("c5 e5 g5 c6", "triangle")) @@ -81,6 +84,8 @@ Each one-shot is a fresh, independent track, so the pattern can vary per event. Fire a higher sting as "events" intensify — pitch by impact, instrument by surface — all without a single audio file: +.. das-doc: given let i = 0 + .. code-block:: das let phrases <- ["c4 e4 g4", "e4 g4 c5", "g4 c5 e5", "c5 e5 g5"] diff --git a/doc/source/reference/tutorials/dasAudio_04_spatial_audio.rst b/doc/source/reference/tutorials/dasAudio_04_spatial_audio.rst index 8980cc93d5..42f9e929df 100644 --- a/doc/source/reference/tutorials/dasAudio_04_spatial_audio.rst +++ b/doc/source/reference/tutorials/dasAudio_04_spatial_audio.rst @@ -24,10 +24,12 @@ Listener Setup .. code-block:: das - // Listener at origin, facing forward (+Y), stationary - set_head_position(float3(0, 0, 0), float3(0, 1, 0), float3(0, 0, 0)) + // Listener at origin, facing forward (-Y), stationary + set_head_position(float3(0, 0, 0), float3(0, -1, 0), float3(0, 0, 0)) -The coordinate system is left-handed: +X is right, +Y is forward, +Z is up. +The coordinate system is left-handed: +X is right, -Y is forward, +Z is up. +Set the listener explicitly before placing sources --- the engine starts with a +head direction of +Y, which puts +X on the *left*. Playing 3D Sound ================ @@ -35,6 +37,8 @@ Playing 3D Sound ``play_3d_sound_loop_from_pcm`` places a looping sound at a 3D position with a specified attenuation model: +.. das-doc: given def generate_click_burst() : array { return <- [for (x in range(4)); 0.0] } + .. code-block:: das var samples <- generate_click_burst() @@ -84,8 +88,8 @@ Four built-in attenuation models control how volume decreases with distance. ``linear_attenuation`` and ``quadratic_attenuation`` take a ``max_distance`` parameter (distance at which volume reaches zero). ``inverse_distance_attenuation`` and ``inverse_square_attenuation`` take a -``dmin`` reference distance (volume is 1.0 at that distance, rolling off -beyond it): +``dmin`` reference distance --- volume is 1.0 at the listener and 0.5 at +``dmin``, rolling off from there: .. list-table:: :header-rows: 1 @@ -96,22 +100,31 @@ beyond it): - Character * - ``inverse_distance_attenuation`` - *dmin* / (*d* + *dmin*) - - Natural rolloff; full volume at *dmin* + - Natural rolloff; half volume at *dmin* * - ``linear_attenuation`` - 1 - *d* / *max* - Straight line to silence at *max* * - ``quadratic_attenuation`` - 1 - (*d* / *max*)\ :sup:`2` - - Faster than linear near *max* + - Louder than linear, then drops steeply near *max* * - ``inverse_square_attenuation`` - *dmin*\ :sup:`2` / (*d*\ :sup:`2` + *dmin*\ :sup:`2`) - - Smooth near *dmin*, rapid falloff + - Half volume at *dmin*, then rapid falloff Example --- comparing all four at different distances: .. code-block:: das let max_dist = 30.0 + let pos = float3(2, 0, 0) + + // every play_*_from_pcm call MOVES its samples array, so each source + // needs its own copy + var samples_a <- generate_click_burst() + var samples_b <- clone(samples_a) + var samples_c <- clone(samples_a) + var samples_d <- clone(samples_a) + let sid_inv = play_3d_sound_loop_from_pcm(pos, inverse_distance_attenuation(max_dist), MA_SAMPLE_RATE, 1, samples_a) let sid_lin = play_3d_sound_loop_from_pcm(pos, @@ -127,8 +140,14 @@ HRTF HRTF (Head-Related Transfer Function) is enabled by default in the audio engine. It applies binaural filtering so that sounds placed in 3D space are perceived at their correct spatial position when listening through -headphones. No additional setup is required --- all ``play_3d_sound_*`` -functions automatically benefit from HRTF processing. +headphones. No additional setup is required --- every ``play_3d_sound_*`` +call is a candidate for HRTF processing. + +The convolution is budgeted, though: the 32 3D channels closest to the +listener get HRTF, and any beyond that fall back to simulated 3D --- +constant-power panning plus distance attenuation, no convolution. +``set_hrtf_budget(n)`` moves the line: 0 for all-simulated, a large number +(say 999) for all-HRTF. Running the Tutorial ==================== @@ -137,8 +156,8 @@ Running the Tutorial daslang.exe tutorials/dasAudio/04_spatial_audio.das -The tutorial places a clicking sound to the right, orbits it around the -listener over 5 seconds, then demonstrates the four attenuation models at +The tutorial places a clicking sound beside the listener, orbits it around +the listener over 5 seconds, then demonstrates the four attenuation models at varying distances. Use headphones for the best spatial experience. .. seealso:: diff --git a/doc/source/reference/tutorials/dasAudio_06_streaming.rst b/doc/source/reference/tutorials/dasAudio_06_streaming.rst index 5b0b89d92c..dc341e0ba1 100644 --- a/doc/source/reference/tutorials/dasAudio_06_streaming.rst +++ b/doc/source/reference/tutorials/dasAudio_06_streaming.rst @@ -39,6 +39,10 @@ and an ``array`` of samples. Each chunk can be any size; the audio engine buffers them internally. Maintain phase continuity between chunks to avoid clicks at chunk boundaries: +.. das-doc: given let chunk_duration = 0.1 +.. das-doc: given let chunk_samples = MA_SAMPLE_RATE / 10 +.. das-doc: given let num_chunks = 30 + .. code-block:: das var phase = 0.0 @@ -60,6 +64,8 @@ The tutorial demonstrates streaming with a sine wave that sweeps from 30 chunks of 100 ms each, and phase is accumulated so the waveform remains continuous: +.. das-doc: fragment + .. code-block:: das let start_freq = 220.0 diff --git a/doc/source/reference/tutorials/dasAudio_08_midi.rst b/doc/source/reference/tutorials/dasAudio_08_midi.rst index becdf91029..6adfac5be7 100644 --- a/doc/source/reference/tutorials/dasAudio_08_midi.rst +++ b/doc/source/reference/tutorials/dasAudio_08_midi.rst @@ -72,23 +72,29 @@ lifecycle: - ``midi_load_samples(media_path)`` — loads the sample banks from the media directory -- ``midi_init()`` — starts the MIDI playback thread +- ``midi_init()`` — starts the MIDI playback thread. The audio system has + to be up first, so all of this runs inside ``with_audio_system()``; + calling it without one panics - ``midi_shutdown()`` — stops the thread and releases resources ``midi_play`` starts a named MIDI track. The name is an arbitrary string that identifies the track for later control. Multiple tracks can play simultaneously: +.. das-doc: given let media_path = "{get_das_root()}/examples/media" + .. code-block:: das require strudel/strudel_midi_player - midi_load_samples(media_path) - midi_init() - midi_play("music", "fur_elise.mid", [gain = 0.8, looping = true]) - sleep(5000u) - midi_stop("music") - midi_shutdown() + with_audio_system() { + midi_load_samples(media_path) + midi_init() + midi_play("music", "fur_elise.mid", [gain = 0.8, looping = true]) + sleep(5000u) + midi_stop("music") + midi_shutdown() + } Part C: Cross-fading ==================== @@ -97,6 +103,9 @@ Because tracks are named and independent, you can run two MIDI files at once and cross-fade between them. ``midi_set_volume`` smoothly transitions a track's volume over a specified fade time in seconds: +.. das-doc: given let fur_elise = "{media_path}/midi/fur_elise.mid" +.. das-doc: given let bach_air = "{media_path}/midi/Bach_Air_on_G_string_BWV1068.mid" + .. code-block:: das // Start both — track_a at full volume, track_b silent @@ -113,8 +122,7 @@ transitions a track's volume over a specified fade time in seconds: midi_set_volume("track_b", 0.0, 3.0) sleep(5000u) - midi_stop() // stop all tracks - midi_shutdown() + midi_stop() // no name: stops every track and shuts the player thread down This pattern is common in games for transitioning between exploration and combat music without an audible cut. diff --git a/doc/source/reference/tutorials/dasAudio_09_playback_status.rst b/doc/source/reference/tutorials/dasAudio_09_playback_status.rst index 03f2e51df9..eef0cd9357 100644 --- a/doc/source/reference/tutorials/dasAudio_09_playback_status.rst +++ b/doc/source/reference/tutorials/dasAudio_09_playback_status.rst @@ -22,6 +22,8 @@ Create a ``LockBox`` with ``lock_box_create`` and attach it to a sound with ``set_status_update``. The call seeds the box with state ``starting``; the audio thread updates it from then on: +.. das-doc: given var tone : array + .. code-block:: das require audio/audio_boost diff --git a/doc/source/reference/tutorials/dasHV_01_http_requests.rst b/doc/source/reference/tutorials/dasHV_01_http_requests.rst index fe9cb0afbf..a814d928dc 100644 --- a/doc/source/reference/tutorials/dasHV_01_http_requests.rst +++ b/doc/source/reference/tutorials/dasHV_01_http_requests.rst @@ -26,6 +26,8 @@ All request functions create a fresh connection per call — ideal for scripts, tools, and one-off requests. For persistent connections, custom timeouts, and authentication, see :ref:`tutorial_dasHV_http_requests_advanced`. +.. das-doc: given let url = "http://example.com/api" + GET Requests ============ @@ -68,6 +70,8 @@ PUT and PATCH PUT and PATCH follow the same signature as POST — a URL, a body, and optional headers and form-data tables: +.. das-doc: signatures + .. code-block:: das PUT(url, "payload") <| $(resp) { ... } @@ -84,6 +88,8 @@ DELETE and HEAD DELETE takes a URL and optional headers. HEAD is identical in signature to GET but returns only the response headers (no body): +.. das-doc: signatures + .. code-block:: das DELETE(url) <| $(resp) { ... } diff --git a/doc/source/reference/tutorials/dasHV_02_http_requests_advanced.rst b/doc/source/reference/tutorials/dasHV_02_http_requests_advanced.rst index 973d7b5f84..3cc63167aa 100644 --- a/doc/source/reference/tutorials/dasHV_02_http_requests_advanced.rst +++ b/doc/source/reference/tutorials/dasHV_02_http_requests_advanced.rst @@ -17,6 +17,9 @@ authentication, query parameters, redirects, and content type. Prerequisites: :ref:`tutorial_dasHV_http_requests` (the fire-and-forget API). +.. das-doc: given let base_url = "http://127.0.0.1:18080" +.. das-doc: given var req : HttpRequest? + The with_http_request Pattern ============================= diff --git a/doc/source/reference/tutorials/dasHV_03_http_server.rst b/doc/source/reference/tutorials/dasHV_03_http_server.rst index 827f5309ac..c26db80bc9 100644 --- a/doc/source/reference/tutorials/dasHV_03_http_server.rst +++ b/doc/source/reference/tutorials/dasHV_03_http_server.rst @@ -21,7 +21,7 @@ Server Class ============ Every server extends ``HvWebServer`` and overrides ``onInit`` to register -routes. The four required WebSocket callbacks can be left empty: +routes. An HTTP-only server overrides nothing else: .. code-block:: das @@ -36,13 +36,20 @@ routes. The four required WebSocket callbacks can be left empty: Every handler receives the request and response by reference and must return an ``http_status`` value. -WebSocket callbacks (``onWsOpen``, ``onWsClose``, ``onWsMessage``) and -``onTick`` have empty defaults in the base class — override them only -when you need WebSocket support. +The WebSocket callbacks (``onWsOpen``, ``onWsClose``, ``onWsMessage``) and +``onTick`` are declared ``abstract`` in the base class — they have no body. +The native side checks for an override before calling one, so leaving them +unimplemented is safe; add them when you need WebSocket support or periodic +work. ``onInit`` is the exception: it has an empty body you replace. + +The route snippets that follow are bodies of ``onInit`` — each one goes +inside the class above. GET Route ========= +.. das-doc: member MyServer + .. code-block:: das GET("/hello") <| @(var req : HttpRequest?; var resp : HttpResponse?) : http_status { @@ -55,6 +62,8 @@ Pass an optional status to override: ``TEXT_PLAIN(resp, text, http_status.BAD_RE POST Route ========== +.. das-doc: member MyServer + .. code-block:: das POST("/echo") <| @(var req : HttpRequest?; var resp : HttpResponse?) : http_status { @@ -70,6 +79,8 @@ All HTTP Methods Use ``PUT``, ``PATCH``, ``DELETE``, ``HEAD``, and ``ANY`` to register handlers for additional methods: +.. das-doc: member MyServer + .. code-block:: das PUT("/data") <| @(var req : HttpRequest?; var resp : HttpResponse?) : http_status { @@ -95,6 +106,8 @@ Path Parameters Use ``:name`` in the route to capture path segments. Read them with ``get_param``: +.. das-doc: member MyServer + .. code-block:: das GET("/users/:id") <| @(var req : HttpRequest?; var resp : HttpResponse?) : http_status { @@ -104,6 +117,8 @@ Use ``:name`` in the route to capture path segments. Read them with Multiple path parameters work naturally: +.. das-doc: member MyServer + .. code-block:: das GET("/users/:id/posts/:post_id") <| @(var req : HttpRequest?; var resp : HttpResponse?) : http_status { @@ -115,7 +130,14 @@ Multiple path parameters work naturally: Query Parameters ================ -Iterate all query parameters with ``each_param``: +Iterate all query parameters with ``each_param``. This example joins them +with ``join``, so it needs ``daslib/strings_boost``: + +.. code-block:: das + + require daslib/strings_boost + +.. das-doc: member MyServer .. code-block:: das @@ -134,6 +156,8 @@ Response Headers ``set_header`` on the response object adds custom headers: +.. das-doc: member MyServer + .. code-block:: das GET("/api/info") <| @(var req : HttpRequest?; var resp : HttpResponse?) : http_status { @@ -154,8 +178,12 @@ string to ``JSON(resp, ...)``: require daslib/json_boost +.. das-doc: member MyServer + +.. code-block:: das + GET("/api/data") <| @(var req : HttpRequest?; var resp : HttpResponse?) : http_status { - let payload : tuple = ("hello", 42) + let payload = (message = "hello", count = 42) return resp |> JSON(write_json(JV(payload))) } diff --git a/doc/source/reference/tutorials/dasHV_04_http_server_advanced.rst b/doc/source/reference/tutorials/dasHV_04_http_server_advanced.rst index da9880408e..f08ae8be5e 100644 --- a/doc/source/reference/tutorials/dasHV_04_http_server_advanced.rst +++ b/doc/source/reference/tutorials/dasHV_04_http_server_advanced.rst @@ -18,22 +18,32 @@ content-type control, and status codes. Prerequisites: :ref:`tutorial_dasHV_http_server` (basic routes and JSON). +.. das-doc: given var server : HvWebServer? + Static File Serving =================== -``STATIC(server, path, dir)`` maps a URL prefix to a filesystem -directory. All files under that directory are served with the correct -``Content-Type`` based on their extension. +``STATIC`` maps a URL prefix to a filesystem directory. All files under +that directory are served with the correct ``Content-Type`` based on +their extension. + +Two spellings reach the same call. From outside the class, the free +function takes the native server handle — the ``server`` field of your +``HvWebServer`` instance: .. code-block:: das // After server->init(port) but before server->start(): STATIC(server.server, "/static", "/path/to/public") +Inside ``onInit`` the class method takes the path and directory alone: +``STATIC("/static", "/path/to/public")``. + .. important:: ``STATIC`` must be called after ``init()`` (which registers the - HTTP service with the router) and before ``start()``. + HTTP service with the router) and before ``start()``. ``onInit`` + itself runs inside ``init()``, so registering there is in time. Clients can then fetch ``/static/index.html``, ``/static/style.css``, etc. @@ -42,23 +52,30 @@ CORS (Cross-Origin Resource Sharing) ===================================== ``allow_cors()`` enables CORS headers on all responses. Call it in -``onInit``: +``onInit``, alongside your route registrations: .. code-block:: das - def override onInit { - allow_cors() - // ... routes ... + class AdvancedServer : HvWebServer { + def override onInit { + allow_cors() + // ... routes ... + } } The server will respond to ``OPTIONS`` preflight requests automatically with ``Access-Control-Allow-Origin`` and related headers. +Every route snippet below is a body of ``AdvancedServer``'s ``onInit`` — +each one goes where the ``// ... routes ...`` comment sits. + HTTP Redirects ============== ``REDIRECT(resp, location, status)`` sends a redirect response: +.. das-doc: member AdvancedServer + .. code-block:: das GET("/old-path") <| @(var req : HttpRequest?; var resp : HttpResponse?) : http_status { @@ -76,6 +93,8 @@ Custom Response Headers and Caching Use ``set_header`` on the response to add cache-control, ETags, and custom application headers: +.. das-doc: member AdvancedServer + .. code-block:: das GET("/cached") <| @(var req : HttpRequest?; var resp : HttpResponse?) : http_status { @@ -104,6 +123,8 @@ Content-Type and Status Codes ``set_content_type(resp, type)`` gives full control over the response: +.. das-doc: member AdvancedServer + .. code-block:: das // HTML response @@ -119,27 +140,33 @@ Non-200 JSON Responses ``JSON()`` and ``TEXT_PLAIN()`` accept an optional status parameter (defaults to ``http_status.OK``): +.. das-doc: member AdvancedServer + .. code-block:: das GET("/not-found") <| @(var req : HttpRequest?; var resp : HttpResponse?) : http_status { - let payload : tuple = ("resource not found", 404) + let payload = (error = "resource not found", code = 404) return resp |> JSON(write_json(JV(payload)), http_status.NOT_FOUND) } 201 Created with Location Header ''''''''''''''''''''''''''''''''' +.. das-doc: member AdvancedServer + .. code-block:: das POST("/items") <| @(var req : HttpRequest?; var resp : HttpResponse?) : http_status { set_header(resp, "Location", "/items/42") - let payload : tuple = (42, string(req.body)) + let payload = (id = 42, body = string(req.body)) return resp |> JSON(write_json(JV(payload)), http_status.CREATED) } 204 No Content '''''''''''''' +.. das-doc: member AdvancedServer + .. code-block:: das POST("/ack") <| @(var req : HttpRequest?; var resp : HttpResponse?) : http_status { @@ -152,7 +179,8 @@ Quick Reference ============================================= =============================================== Function Description ============================================= =============================================== -``STATIC(server, path, dir)`` Serve static files from directory +``STATIC(path, dir)`` Serve static files from directory (method) +``STATIC(server.server, path, dir)`` Same, from outside the server class ``allow_cors()`` Enable CORS headers on all routes ``REDIRECT(resp, location, status)`` Send 3xx redirect response ``JSON(resp, json_str, status?)`` JSON response (default 200) diff --git a/doc/source/reference/tutorials/dasHV_05_cookies_and_forms.rst b/doc/source/reference/tutorials/dasHV_05_cookies_and_forms.rst index deb7340d32..defe1ad0f5 100644 --- a/doc/source/reference/tutorials/dasHV_05_cookies_and_forms.rst +++ b/doc/source/reference/tutorials/dasHV_05_cookies_and_forms.rst @@ -18,6 +18,9 @@ submitted data). Prerequisites: :ref:`tutorial_dasHV_http_server` and :ref:`tutorial_dasHV_http_server_advanced`. +.. das-doc: given var req : HttpRequest? +.. das-doc: given let upload_dir = "/tmp/uploads" + Cookies ======= @@ -28,6 +31,8 @@ Inside a route handler, ``add_cookie(resp, name, value)`` appends a ``Set-Cookie`` header. An extended overload accepts domain, path, max-age, secure, and httponly flags: +.. das-doc: member HvWebServer + .. code-block:: das GET("/set-cookies") <| @(var req : HttpRequest?; var resp : HttpResponse?) : http_status { @@ -44,13 +49,15 @@ Reading Cookies from a Request On the server side, ``get_cookie`` reads a named cookie from the request pointer: +.. das-doc: member HvWebServer + .. code-block:: das GET("/read-cookies") <| @(var req : HttpRequest?; var resp : HttpResponse?) : http_status { let session = get_cookie(req, "session") let prefs = get_cookie(req, "prefs") - // session and prefs are strings; empty if not found - ... + // both are strings; empty if the cookie is not present + return resp |> TEXT_PLAIN("session={session}, prefs={prefs}") } Iterating All Cookies @@ -117,6 +124,8 @@ Server Side — Reading Form Fields ``get_form_data`` reads a single text field. ``each_form_field`` iterates all fields (text and file): +.. das-doc: member HvWebServer + .. code-block:: das POST("/upload") <| @(var req : HttpRequest?; var resp : HttpResponse?) : http_status { @@ -146,24 +155,33 @@ preserved: URL-Encoded Form Data ====================== -For simple ``application/x-www-form-urlencoded`` submissions: +For simple ``application/x-www-form-urlencoded`` submissions, set fields +with ``set_url_encoded`` and read them back with ``get_url_encoded``. + +Client side: .. code-block:: das - // Client side with_http_request() <| $(var req) { req.method = http_method.POST req.url := "http://localhost:8080/login" set_url_encoded(req, "username", "admin") set_url_encoded(req, "password", "secret123") - request(req) <| $(resp) { ... } + request(req) <| $(resp) { + print("{resp.status_code}\n") + } } - // Server side +Server side: + +.. das-doc: member HvWebServer + +.. code-block:: das + POST("/login") <| @(var req : HttpRequest?; var resp : HttpResponse?) : http_status { let username = get_url_encoded(req, "username") let password = get_url_encoded(req, "password") - ... + return resp |> TEXT_PLAIN(empty(password) ? "missing password" : "welcome {username}") } Quick Reference diff --git a/doc/source/reference/tutorials/dasHV_06_websockets.rst b/doc/source/reference/tutorials/dasHV_06_websockets.rst index c211dfcdc9..f50429af0c 100644 --- a/doc/source/reference/tutorials/dasHV_06_websockets.rst +++ b/doc/source/reference/tutorials/dasHV_06_websockets.rst @@ -18,6 +18,10 @@ and mixing HTTP routes with WebSocket endpoints. Prerequisites: :ref:`tutorial_dasHV_http_server`. +.. das-doc: given let SERVER_PORT = 18085 +.. das-doc: given let base_url = "ws://127.0.0.1:18085" +.. das-doc: given def wait_for_messages(var client : ChatClient?; count : int) { if (length(client.received) < count) { client->process_event_que() } } + WebSocket Server ================ @@ -106,7 +110,10 @@ Connecting and Receiving ======================== Create a client, call ``init(url)`` to connect, then pump the event -queue with ``process_event_que()`` to receive callbacks: +queue with ``process_event_que()`` to receive callbacks. The +``wait_for_messages`` used below is a helper from the companion source: it +pumps the queue until the client has collected the requested number of +messages, or a timeout expires. .. code-block:: das @@ -168,12 +175,15 @@ WebSocket callbacks: .. code-block:: das - def override onInit { - GET("/ping") <| @(var req : HttpRequest?; var resp : HttpResponse?) : http_status { - return resp |> TEXT_PLAIN("pong") - } - GET("/clients") <| @(var req : HttpRequest?; var resp : HttpResponse?) : http_status { - return resp |> TEXT_PLAIN("{length(self.clients)}") + class ChatServer : HvWebServer { + ... + def override onInit { + GET("/ping") <| @(var req : HttpRequest?; var resp : HttpResponse?) : http_status { + return resp |> TEXT_PLAIN("pong") + } + GET("/clients") <| @(var req : HttpRequest?; var resp : HttpResponse?) : http_status { + return resp |> TEXT_PLAIN("{length(self.clients)}") + } } } diff --git a/doc/source/reference/tutorials/dasHV_07_sse_and_streaming.rst b/doc/source/reference/tutorials/dasHV_07_sse_and_streaming.rst index f9a0a4c366..c9be4a7099 100644 --- a/doc/source/reference/tutorials/dasHV_07_sse_and_streaming.rst +++ b/doc/source/reference/tutorials/dasHV_07_sse_and_streaming.rst @@ -18,6 +18,9 @@ delivers the response body incrementally as chunks arrive. Prerequisites: :ref:`tutorial_dasHV_http_requests` and :ref:`tutorial_dasHV_http_server`. +.. das-doc: given let base_url = "http://127.0.0.1:18086" +.. das-doc: given var req : HttpRequest? + What is SSE? ============ diff --git a/doc/source/reference/tutorials/dasLLAMA_00_problem_statement.rst b/doc/source/reference/tutorials/dasLLAMA_00_problem_statement.rst index 38f323a6b3..5bef9fdd30 100644 --- a/doc/source/reference/tutorials/dasLLAMA_00_problem_statement.rst +++ b/doc/source/reference/tutorials/dasLLAMA_00_problem_statement.rst @@ -209,6 +209,7 @@ with a seven-integer header in front. The :ref:`embedding ` table determines. We map the file into memory and read each matrix as a view into it — no copies: +.. das-doc: fragment .. code-block:: das array_view(bytes, 28, (length(bytes) - 28) / 4, type) $(model : array#) { @@ -573,6 +574,7 @@ BOS drops, and ``<0xNN>`` pieces decode back to raw bytes. :ref:`Prefill ` is a plain loop: +.. das-doc: fragment .. code-block:: das for (position, token in range(length(tokens)), tokens) { diff --git a/doc/source/reference/tutorials/dasLLAMA_01_hello_generate.rst b/doc/source/reference/tutorials/dasLLAMA_01_hello_generate.rst index 63d5b6d5a9..51d30f57a3 100644 --- a/doc/source/reference/tutorials/dasLLAMA_01_hello_generate.rst +++ b/doc/source/reference/tutorials/dasLLAMA_01_hello_generate.rst @@ -34,6 +34,7 @@ the precision you ask for. ``QuantMode.q8`` (int8) is the fast everyday choice; ``QuantMode.fp32`` is the token-exact reference the test suite validates against llama.cpp. +.. das-doc: given let path = "SmolLM2-135M-Instruct-Q8_0.gguf" .. code-block:: das require dasllama/dasllama @@ -67,12 +68,14 @@ token); :ref:`tutorial 03 ` covers the knobs. The kernels thread through the job queue — wrap generation in ``with_job_que()`` (from ``daslib/jobque_boost``), or model code will panic -asking for one. +asking for one. ``setup_dasllama_jobque()`` then tunes that queue for the +fork/join matmul dispatch: pooled fork contexts, batched dispatch, and the +worker spin window. .. code-block:: das with_job_que() { - set_jobque_fork_pool(true, true) // pool per-job fork contexts + setup_dasllama_jobque() // pooled forks, batched dispatch, spin window var s = create_session(m) generate(m, s, ids, SamplingParams(), 48l) $(_id, piece) { fprint(fstdout(), piece) @@ -87,6 +90,8 @@ Stats ``stats`` reports the last ``generate``/``respond`` call on the session: token counts, time to first token, and prefill / generation throughput. +.. das-doc: given var s : Session +.. das-doc: alt .. code-block:: das let st = stats(s) diff --git a/doc/source/reference/tutorials/dasLLAMA_02_chat.rst b/doc/source/reference/tutorials/dasLLAMA_02_chat.rst index 341b8bfcc1..df95f8ac95 100644 --- a/doc/source/reference/tutorials/dasLLAMA_02_chat.rst +++ b/doc/source/reference/tutorials/dasLLAMA_02_chat.rst @@ -25,6 +25,7 @@ embedded template, falling back to the arch registry — and creates the session. ``add_user`` queues a message; ``respond`` renders the turn, prefills it, and streams the reply until a stop token or the ``max_new`` budget. +.. das-doc: given var m = Model() .. code-block:: das var chat = create_chat(m, "You are a helpful, friendly assistant.") diff --git a/doc/source/reference/tutorials/dasLLAMA_03_sampling.rst b/doc/source/reference/tutorials/dasLLAMA_03_sampling.rst index 1423b1c439..6580cf6f57 100644 --- a/doc/source/reference/tutorials/dasLLAMA_03_sampling.rst +++ b/doc/source/reference/tutorials/dasLLAMA_03_sampling.rst @@ -9,14 +9,21 @@ dasLLAMA-03 — Sampling single: Tutorial; Sampling single: Tutorial; Temperature -How the next token gets picked. ``SamplingParams`` has four knobs, and their -defaults are greedy:: +How the next token gets picked. ``SamplingParams`` carries the knobs, and its +defaults are greedy: + +.. das-doc: signatures +.. code-block:: das struct SamplingParams { - temp : float = 0.0 // <= 0 => greedy argmax; otherwise softmax temperature - top_k : int64 = 0l // <= 0 or >= vocab => no top-k cutoff - penalty : float = 1.0 // repetition penalty over recent tokens (1.0 = none) - penalty_last_n : int64 = 64l // repetition-penalty window + temp : float = 0.0 // <= 0 => greedy argmax; otherwise softmax temperature + top_k : int64 = 0l // <= 0 or >= vocab => no top-k cutoff + top_p : float = 1.0 // nucleus: keep the smallest probability mass >= top_p (>= 1 = off) + min_p : float = 0.0 // drop tokens under min_p * the top token's probability (<= 0 = off) + penalty : float = 1.0 // multiplicative repetition penalty over recent tokens (1.0 = none) + presence_penalty : float = 0.0 // subtracted from the logit of every distinct token in the window + frequency_penalty : float = 0.0 // subtracted once per occurrence in the window (OpenAI semantics) + penalty_last_n : int64 = 64l // penalty window: the most recent N generated tokens } Run it like tutorial 01:: @@ -39,7 +46,10 @@ The repetition penalty ``penalty > 1`` scales down the logits of the last ``penalty_last_n`` generated tokens before picking, so the argmax can't keep choosing the same -phrase. Still fully deterministic — no randomness involved:: +phrase. ``presence_penalty`` and ``frequency_penalty`` do the same job by +subtraction instead of scaling — once for every distinct token in that window, +and once per occurrence — which is how the OpenAI API spells it. All three are +fully deterministic; no randomness involved:: greedy + penalty 1.3:, I was a young man with dreams of becoming an engineer. My parents were both engineers and they encouraged my passion... @@ -49,9 +59,14 @@ Temperature, top-k, and seeds ``temp > 0`` samples from the softmax distribution (higher = more adventurous); ``top_k > 0`` first cuts it to the k most likely tokens. -Sampling draws from the *session's* RNG, so variety comes from the seed — and -``set_seed`` makes any run exactly reproducible: - +``top_p`` and ``min_p`` cut by probability instead of by count — keep the +smallest set of tokens worth ``top_p`` of the mass, drop everything under +``min_p`` of the top token's probability. Sampling draws from the *session's* +RNG, so variety comes from the seed — and ``set_seed`` makes any run exactly +reproducible: + +.. das-doc: given var m = Model() +.. das-doc: given var prompt : array .. code-block:: das def run_once(m : Model; prompt : array; params : SamplingParams; seed : int) : string { diff --git a/doc/source/reference/tutorials/dasLLAMA_04_sessions_and_memory.rst b/doc/source/reference/tutorials/dasLLAMA_04_sessions_and_memory.rst index 010bede226..9a58894ce4 100644 --- a/doc/source/reference/tutorials/dasLLAMA_04_sessions_and_memory.rst +++ b/doc/source/reference/tutorials/dasLLAMA_04_sessions_and_memory.rst @@ -21,19 +21,23 @@ Run it like tutorial 01:: The KV cache, and why you cap seq_len ===================================== -The KV cache is sized to ``config.seq_len`` *at* ``create_session`` time — -roughly ``2 * n_layers * seq_len * kv_dim`` floats. Models ship with big -native contexts (Llama-3's native ``seq_len`` is 131072, which means tens of -GB of fp32 KV), so cap ``seq_len`` to the context you actually need **before** -creating sessions: - +The KV cache is sized to ``config.seq_len`` *at* ``create_session`` time — one +key row and one value row per position per layer, so +``2 * n_layers * seq_len * kv_dim`` entries. ``create_session`` stores each +entry as ``f16``, two bytes, unless you pass another ``kv_dtype`` +(``KVDtype.f32`` doubles the cache, ``KVDtype.q8_0`` roughly halves it). +Models ship with big native contexts (Llama-3's native ``seq_len`` is 131072, +which means tens of GB of KV), so cap ``seq_len`` to the context you actually +need **before** creating sessions: + +.. das-doc: given var m = Model() .. code-block:: das m.config.seq_len = min(m.config.seq_len, 1024l) var s = create_session(m) -On SmolLM2-135M that's the difference between ~377 MB per session at the -native 8192 and ~47 MB at 1024. +On SmolLM2-135M that's the difference between ~188 MB per session at the +native 8192 and ~24 MB at 1024. One model, many sessions ======================== @@ -49,6 +53,7 @@ eval and sample by hand session's current position and advances it — prefill is just the whole prompt in one call. ``sample`` picks the next token from ``session.logits``: +.. das-doc: given var prompt : array .. code-block:: das var s = create_session(m) diff --git a/doc/source/reference/tutorials/dasLLAMA_07_speech_to_text.rst b/doc/source/reference/tutorials/dasLLAMA_07_speech_to_text.rst index 5d51267368..2cd26503be 100644 --- a/doc/source/reference/tutorials/dasLLAMA_07_speech_to_text.rst +++ b/doc/source/reference/tutorials/dasLLAMA_07_speech_to_text.rst @@ -59,6 +59,7 @@ The one-shot form returns the full text; the block form yields each the raw token ids, and ``avg_logprob``, the mean per-token log-probability (closer to zero = more confident). +.. das-doc: given var samples : array .. code-block:: das var s <- create_session(m, "auto") // "auto": whisper detects the language @@ -82,11 +83,16 @@ the shape a live audio source drives — pair it with dasAudio's microphone capture (:ref:`tutorial_dasAudio_recording`). Models that transcribe whole clips at once say so with a loud panic, matching ``caps().streaming``. +.. das-doc: given var chunk : array .. code-block:: das - feed(m, s, chunk) // as audio arrives - drain(m, s) $(seg) { ... } // complete windows only - flush(m, s) $(seg) { ... } // the sub-30 s tail, at end of stream + feed(m, s, chunk) // as audio arrives + drain(m, s) $(seg) { // complete windows only + print("{seg.text}") + } + flush(m, s) $(seg) { // the sub-30 s tail, at end of stream + print("{seg.text}") + } .. seealso:: diff --git a/doc/source/reference/tutorials/dasLLAMA_08_audio_chat.rst b/doc/source/reference/tutorials/dasLLAMA_08_audio_chat.rst index 49642dac01..517115faf6 100644 --- a/doc/source/reference/tutorials/dasLLAMA_08_audio_chat.rst +++ b/doc/source/reference/tutorials/dasLLAMA_08_audio_chat.rst @@ -30,6 +30,7 @@ next turn; ``add_user`` contributes the turn's text after the audio span. ``respond`` renders the turn — template framing, audio splice, embedding prefill — and streams the reply. +.. das-doc: given var samples : array .. code-block:: das var m <- load_model("Llama-3.2-1B-Instruct-Q8_0.gguf", QuantMode.q8) diff --git a/doc/source/reference/tutorials/dasLLAMA_09_embeddings.rst b/doc/source/reference/tutorials/dasLLAMA_09_embeddings.rst index a047641b3f..71dc41f26d 100644 --- a/doc/source/reference/tutorials/dasLLAMA_09_embeddings.rst +++ b/doc/source/reference/tutorials/dasLLAMA_09_embeddings.rst @@ -25,8 +25,10 @@ One vector per sentence ``embed`` is the whole API: text in, a fixed-width unit vector out. Because every vector is unit length, cosine similarity is just the dot product — a vector's dot product with itself is ``1.0``, the cheapest check that the -result really is normalized. +result really is normalized. ``cosine`` is the tutorial file's own helper: it +multiplies the two vectors coordinate by coordinate and adds up the results. +.. das-doc: given def cosine(a, b : array) : float { var acc = 0.0; for (x, y in a, b) { acc += x * y }; return acc } .. code-block:: das var m <- load_model("SmolLM2-135M-Instruct-Q8_0.gguf", QuantMode.q8) @@ -49,6 +51,10 @@ to the top and the unrelated ones sink; the model scores by meaning, so the concept sentence ("Quicksort and merge sort …") ranks high without sharing the query's words. +.. das-doc: given var m = Model() +.. das-doc: given var qv : array +.. das-doc: given let candidates = ["Use the sorted() built-in to order a Python list.", "The cat curled up on the warm windowsill."] +.. das-doc: alt .. code-block:: das var scored : array> diff --git a/doc/source/reference/tutorials/dasMinfft_01_real_fft.rst b/doc/source/reference/tutorials/dasMinfft_01_real_fft.rst index 3e8d75471f..226f3f3556 100644 --- a/doc/source/reference/tutorials/dasMinfft_01_real_fft.rst +++ b/doc/source/reference/tutorials/dasMinfft_01_real_fft.rst @@ -22,6 +22,8 @@ spectrum of a real signal is symmetric, so the upper half is redundant and not stored). A tone that completes exactly ``k`` cycles over the ``N`` samples lands on bin ``k`` with raw magnitude ``N/2`` times its amplitude: +.. das-doc: given let TWO_PI = 6.2831853f + .. code-block:: das let signal <- [for (i in range(64)); diff --git a/doc/source/reference/tutorials/dasMinfft_02_dct_basics.rst b/doc/source/reference/tutorials/dasMinfft_02_dct_basics.rst index 906d80e2fa..0fab47e8e0 100644 --- a/doc/source/reference/tutorials/dasMinfft_02_dct_basics.rst +++ b/doc/source/reference/tutorials/dasMinfft_02_dct_basics.rst @@ -64,11 +64,13 @@ small. This is the essence of lossy coding: .. code-block:: das - for (k in range(keep, n)) { + let keep = 4 + var trunc := coeff // work on a copy of the coefficients + for (k in range(keep, length(trunc))) { trunc[k] = 0.0f } idct(trunc, back, plan) - // keep 8/32 coefficients -> max error ~0.16 + // on the companion's 32-sample signal: keep 8/32 coefficients -> max error ~0.16 .. seealso:: diff --git a/doc/source/reference/tutorials/dasMinfft_03_dct_image_compression.rst b/doc/source/reference/tutorials/dasMinfft_03_dct_image_compression.rst index 64702e1adf..6bbd7098d1 100644 --- a/doc/source/reference/tutorials/dasMinfft_03_dct_image_compression.rst +++ b/doc/source/reference/tutorials/dasMinfft_03_dct_image_compression.rst @@ -25,6 +25,11 @@ data is a row-major array of ``rows*cols`` floats. ``idct(dct(x))`` scales by .. code-block:: das var plan = make_dct_plan_2d(8, 8) + + var blk : array + blk |> resize(8 * 8) // one 8x8 block, row-major + var coeff, back : array + dct(blk, coeff, plan) // forward idct(coeff, back, plan) // inverse; back[i] / 256.0f recovers blk[i] @@ -40,7 +45,11 @@ quant table weights each frequency. The pipeline ============ -Forward DCT, quantize against the JPEG luminance table, dequantize, inverse DCT: +Forward DCT, quantize against the JPEG luminance table, dequantize, inverse DCT +— the two lines at the heart of the per-coefficient loop (``u``/``v`` are the +coefficient's row/column frequency, ``i / 8`` and ``i % 8``): + +.. das-doc: fragment .. code-block:: das diff --git a/doc/source/reference/tutorials/dasOPENAI_01_first_chat.rst b/doc/source/reference/tutorials/dasOPENAI_01_first_chat.rst index 736ffc3ff7..64b819175c 100644 --- a/doc/source/reference/tutorials/dasOPENAI_01_first_chat.rst +++ b/doc/source/reference/tutorials/dasOPENAI_01_first_chat.rst @@ -45,7 +45,14 @@ Constructing a client // local server, no key let client = openai_client("http://localhost:11434/v1") - // real OpenAI, key from the environment (requires daslib/fio) +Against a real endpoint you pass a key. Reading it from the environment needs +``daslib/fio``: + +.. das-doc: alt +.. code-block:: das + + require daslib/fio + let client = openai_client("https://api.openai.com/v1", get_env_variable("OPENAI_API_KEY")) The chat_text one-liner diff --git a/doc/source/reference/tutorials/dasOPENAI_02_conversations_and_params.rst b/doc/source/reference/tutorials/dasOPENAI_02_conversations_and_params.rst index e97463ebdd..14f0fe0ee4 100644 --- a/doc/source/reference/tutorials/dasOPENAI_02_conversations_and_params.rst +++ b/doc/source/reference/tutorials/dasOPENAI_02_conversations_and_params.rst @@ -21,6 +21,7 @@ The model sees every prior turn. Roles are ``system`` (instructions), ``user``, ``assistant`` (previous model replies), and ``tool`` (function results — see :ref:`tutorial_dasOPENAI_tools`): +.. das-doc: given let client = openai_client("http://localhost:11434/v1") .. code-block:: das let req = ChatCompletionRequest(model = "gpt-4o-mini", messages <- [ diff --git a/doc/source/reference/tutorials/dasOPENAI_03_structured_outputs.rst b/doc/source/reference/tutorials/dasOPENAI_03_structured_outputs.rst index 132c082a94..d68b09f2e4 100644 --- a/doc/source/reference/tutorials/dasOPENAI_03_structured_outputs.rst +++ b/doc/source/reference/tutorials/dasOPENAI_03_structured_outputs.rst @@ -20,6 +20,7 @@ Requesting JSON ``json_object_format()`` returns the ``response_format`` value for JSON mode. Assign it to the request: +.. das-doc: given let client = openai_client("http://localhost:11434/v1") .. code-block:: das var req = ChatCompletionRequest(model = "gpt-4o-mini", diff --git a/doc/source/reference/tutorials/dasOPENAI_04_tools_and_function_calling.rst b/doc/source/reference/tutorials/dasOPENAI_04_tools_and_function_calling.rst index b6fbb29f67..46e870ac7e 100644 --- a/doc/source/reference/tutorials/dasOPENAI_04_tools_and_function_calling.rst +++ b/doc/source/reference/tutorials/dasOPENAI_04_tools_and_function_calling.rst @@ -20,6 +20,7 @@ Declaring a tool A ``Tool`` wraps a ``FunctionDef``. ``parameters`` is a raw JSON-schema string describing the arguments the model should fill in: +.. das-doc: given let client = openai_client("http://localhost:11434/v1") .. code-block:: das def weather_tool() : Tool { diff --git a/doc/source/reference/tutorials/dasOPENAI_05_embeddings_and_models.rst b/doc/source/reference/tutorials/dasOPENAI_05_embeddings_and_models.rst index fdc88ebacc..1c9ea341dc 100644 --- a/doc/source/reference/tutorials/dasOPENAI_05_embeddings_and_models.rst +++ b/doc/source/reference/tutorials/dasOPENAI_05_embeddings_and_models.rst @@ -20,6 +20,7 @@ Embeddings and cosine similarity produce vectors that point in similar directions — measured by cosine similarity (1.0 = identical direction, 0.0 = orthogonal): +.. das-doc: given let client = openai_client("http://localhost:11434/v1") .. code-block:: das require openai/openai_embeddings @@ -37,8 +38,9 @@ similarity (1.0 = identical direction, 0.0 = orthogonal): } print("similarity: {cosine(v1, v2)}\n") -For the full ``EmbeddingResponse`` (per-input vectors + usage), use -``embeddings(client, req)`` with an ``EmbeddingRequest``. +``embed`` embeds one string. For a batch, call ``embeddings(client, req)`` with +an ``EmbeddingRequest``; it returns an ``EmbeddingResult`` whose ``response`` +holds one vector per input plus the token usage. Listing and retrieving models ============================= diff --git a/doc/source/reference/tutorials/dasOPENAI_06_audio.rst b/doc/source/reference/tutorials/dasOPENAI_06_audio.rst index 9765d4bf10..1c9cdd934f 100644 --- a/doc/source/reference/tutorials/dasOPENAI_06_audio.rst +++ b/doc/source/reference/tutorials/dasOPENAI_06_audio.rst @@ -19,6 +19,7 @@ Text to speech (``array``, empty on error). Write them to a file to play later, or stream them onward — the byte format is whatever the server produced: +.. das-doc: given let client = openai_client("http://localhost:11434/v1") .. code-block:: das require openai/openai_audio diff --git a/doc/source/reference/tutorials/dasOPENAI_07_streaming_chat.rst b/doc/source/reference/tutorials/dasOPENAI_07_streaming_chat.rst index ec5604e809..577097a676 100644 --- a/doc/source/reference/tutorials/dasOPENAI_07_streaming_chat.rst +++ b/doc/source/reference/tutorials/dasOPENAI_07_streaming_chat.rst @@ -22,6 +22,7 @@ Streaming with on_delta the trailing block; the return value carries the full accumulated content and the ``finish_reason``: +.. das-doc: given let client = openai_client("http://localhost:11434/v1") .. code-block:: das var req = ChatCompletionRequest(model = "gpt-4o-mini", diff --git a/doc/source/reference/tutorials/dasOPENAI_08_vision.rst b/doc/source/reference/tutorials/dasOPENAI_08_vision.rst index d6205a8fb7..2bf34458d1 100644 --- a/doc/source/reference/tutorials/dasOPENAI_08_vision.rst +++ b/doc/source/reference/tutorials/dasOPENAI_08_vision.rst @@ -21,6 +21,7 @@ normal ``ChatResult`` — the assistant's text is in ``choices[0].message.content``. The image can be an ``http(s)`` URL or a ``data:`` URL (base64-inlined bytes): +.. das-doc: given let base_url = "http://localhost:11434/v1" .. code-block:: das require openai/openai_vision diff --git a/doc/source/reference/tutorials/dasOPENAI_09_image_generation.rst b/doc/source/reference/tutorials/dasOPENAI_09_image_generation.rst index 0011d5bff4..61630a040b 100644 --- a/doc/source/reference/tutorials/dasOPENAI_09_image_generation.rst +++ b/doc/source/reference/tutorials/dasOPENAI_09_image_generation.rst @@ -18,6 +18,7 @@ Building the Request Only ``prompt`` is required on ``ImageRequest``; the rest are optional and are omitted from the wire payload when unset (``options rtti`` honors ``@optional``): +.. das-doc: given let client = openai_client("http://localhost:11434/v1") .. code-block:: das require openai/openai_images diff --git a/doc/source/reference/tutorials/dasOPENAI_10_moderations.rst b/doc/source/reference/tutorials/dasOPENAI_10_moderations.rst index 948a31af89..329cdc55fc 100644 --- a/doc/source/reference/tutorials/dasOPENAI_10_moderations.rst +++ b/doc/source/reference/tutorials/dasOPENAI_10_moderations.rst @@ -19,6 +19,7 @@ Classifying Text ``ModerationRequest`` takes a model and an array of input strings; ``moderations`` returns one verdict per input in ``response.results``: +.. das-doc: given let client = openai_client("http://localhost:11434/v1") .. code-block:: das require openai/openai_moderations diff --git a/doc/source/reference/tutorials/dasOPENAI_11_completions.rst b/doc/source/reference/tutorials/dasOPENAI_11_completions.rst index ada528618f..880bee4246 100644 --- a/doc/source/reference/tutorials/dasOPENAI_11_completions.rst +++ b/doc/source/reference/tutorials/dasOPENAI_11_completions.rst @@ -21,6 +21,7 @@ The completions() Call On success, ``response.choices`` holds the generated text and ``finish_reason``, and ``response.usage`` the token accounting: +.. das-doc: given let client = openai_client("http://localhost:11434/v1") .. code-block:: das require openai/openai_completions diff --git a/doc/source/reference/tutorials/dasPEG_01_hello_parser.rst b/doc/source/reference/tutorials/dasPEG_01_hello_parser.rst index 32f10073cc..215e299e95 100644 --- a/doc/source/reference/tutorials/dasPEG_01_hello_parser.rst +++ b/doc/source/reference/tutorials/dasPEG_01_hello_parser.rst @@ -115,6 +115,7 @@ Character Sets ``set()`` matches a single character from one or more ranges or individual characters: +.. das-doc: fragment .. code-block:: das set('a'..'z', 'A'..'Z') // letters @@ -127,6 +128,7 @@ Multiple Rules and WS Grammars can have any number of rules. ``WS`` is a built-in terminal that matches zero or more whitespace characters: +.. das-doc: fragment .. code-block:: das parse(input) { diff --git a/doc/source/reference/tutorials/dasPEG_02_calculator.rst b/doc/source/reference/tutorials/dasPEG_02_calculator.rst index 3bb97bc9de..1490100df4 100644 --- a/doc/source/reference/tutorials/dasPEG_02_calculator.rst +++ b/doc/source/reference/tutorials/dasPEG_02_calculator.rst @@ -120,6 +120,7 @@ PEEK (Lookahead) input**. In the calculator, ``PEEK(set('0'..'9'))`` verifies the next character is a digit before committing to the ``number`` terminal: +.. das-doc: fragment .. code-block:: das rule(PEEK(set('0'..'9')), commit, number as n) { diff --git a/doc/source/reference/tutorials/dasPEG_03_csv_parser.rst b/doc/source/reference/tutorials/dasPEG_03_csv_parser.rst index 310616b7cc..9873177fb6 100644 --- a/doc/source/reference/tutorials/dasPEG_03_csv_parser.rst +++ b/doc/source/reference/tutorials/dasPEG_03_csv_parser.rst @@ -64,6 +64,7 @@ The canonical PEG idiom for comma-separated lists is: This avoids ambiguity with trailing commas. The last element has no comma: +.. das-doc: fragment .. code-block:: das var row : Row @@ -142,6 +143,7 @@ Inverted Character Sets (not_set) set. It is the complement of ``set()`` and is useful for "match anything except a specific character": +.. das-doc: fragment .. code-block:: das // Match any character that is not a newline or semicolon diff --git a/doc/source/reference/tutorials/dasPEG_04_email_validator.rst b/doc/source/reference/tutorials/dasPEG_04_email_validator.rst index 6d63da8e85..4a02de7d2b 100644 --- a/doc/source/reference/tutorials/dasPEG_04_email_validator.rst +++ b/doc/source/reference/tutorials/dasPEG_04_email_validator.rst @@ -81,6 +81,7 @@ not_set() ``not_set()`` matches any character **not** in the given set. It is the complement of ``set()``: +.. das-doc: fragment .. code-block:: das not_set('\n', '\r') // any character except newlines diff --git a/doc/source/reference/tutorials/dasPEG_05_json_parser.rst b/doc/source/reference/tutorials/dasPEG_05_json_parser.rst index faaf8f7542..9901b1b569 100644 --- a/doc/source/reference/tutorials/dasPEG_05_json_parser.rst +++ b/doc/source/reference/tutorials/dasPEG_05_json_parser.rst @@ -48,6 +48,7 @@ literal braces in grammar strings must be escaped with a backslash: The Grammar (Simplified) ======================== +.. das-doc: fragment .. code-block:: das def parse_json(input : string; @@ -102,6 +103,7 @@ Tuple Return Types Key-value pairs use ``tuple`` as the return type: +.. das-doc: fragment .. code-block:: das var mapping : tuple @@ -117,6 +119,8 @@ Verification The tutorial verifies its output matches ``daslib/json``'s built-in parser: +.. das-doc: given def parse_json(input : string; blk : block<(var val : JsonValue?# implicit; err : array) : void>) {} +.. das-doc: given let test_input = "[1, \"hello\", true, null]" .. code-block:: das var discard_error : string diff --git a/doc/source/reference/tutorials/dasPEG_06_debugging.rst b/doc/source/reference/tutorials/dasPEG_06_debugging.rst index 1b0600d638..b1baa3c3f1 100644 --- a/doc/source/reference/tutorials/dasPEG_06_debugging.rst +++ b/doc/source/reference/tutorials/dasPEG_06_debugging.rst @@ -18,7 +18,7 @@ You will learn: - ``option(color)`` --- colored terminal output - ``option(print_generated)`` --- inspect generated code - ``log("message")`` --- inline debug messages during parsing -- How ``commit`` enables meaningful error messages +- How ``commit`` cuts backtracking and shapes the error list - Reading and interpreting ``ParsingError`` results - Performance tips @@ -28,6 +28,7 @@ Tracing Add ``option(tracing)`` inside the ``parse`` block to see which alternatives are tried and whether they succeed: +.. das-doc: fragment .. code-block:: das parse(input) { @@ -66,6 +67,7 @@ Inline Log Messages ``log("message")`` prints during parsing. Use string interpolation to include bound variables: +.. das-doc: fragment .. code-block:: das parse(input) { @@ -77,31 +79,49 @@ include bound variables: } } -Log messages fire during the **first parse pass** --- they may fire for -alternatives that later fail via backtracking. +A message prints every time the parser reaches that point, including +alternatives that fail a moment later and backtrack. A failed parse reads +the input twice --- once to parse, once to collect the errors --- so each +message prints twice on failure. Commit and Error Reporting ========================== -Without ``commit``, PEG silently backtracks when an alternative fails. -The error array may be empty even on parse failure: +``commit`` is the cut operator. Once the parser passes it, the rest of the +rule's alternatives are skipped: the alternative holding the ``commit`` +either matches, or the whole rule fails. +.. das-doc: fragment .. code-block:: das - // Without commit --- may produce no errors + // Without commit --- both alternatives are tried rule("val", WS, "=", WS, number as n, ";", EOF) { return n } + rule("val", WS, "=", WS, "?", EOF) { + return -1 + } - // With commit --- produces meaningful errors + // With commit --- after "=", the second alternative is dead rule("val", WS, "=", commit, WS, number as n, ";", EOF) { return n } + rule("val", WS, "=", WS, "?", EOF) { + return -1 + } + +On ``"val = ;"`` both grammars fail, and the error lists differ. Without +commit every failing alternative adds its own ``ParsingError``, so you get +two. With commit you get one, for the alternative the parser committed to. +The cut also costs you matches: ``"val = ?"`` parses without commit, and +fails with it, because the second alternative never runs. + +So place ``commit`` after an **unambiguous prefix** --- the point where the +input can only be this alternative. -Place ``commit`` after an **unambiguous prefix** --- the point where -the parser knows which alternative it is in. After commit, if the -rest of the alternative fails, a ``ParsingError`` is generated with -the position and description of the failure. +Errors raised inside a lookahead (``PEEK``, ``!``), a repetition (``*``, +``+``), or an ``MB()`` are suppressed. A parse that fails only there +reports an empty error array. Interpreting ParsingError ========================= @@ -111,6 +131,7 @@ Each ``ParsingError`` has two fields: - ``text : string`` --- human-readable description of what was expected - ``index : int`` --- byte position in the input where the error occurred +.. das-doc: given def parse_with_commit(input : string; blk : block<(val : int; err : array) : void>) {} .. code-block:: das parse_with_commit("val = ;") $(val; err) { diff --git a/doc/source/reference/tutorials/dasPEG_07_basic_interpreter.rst b/doc/source/reference/tutorials/dasPEG_07_basic_interpreter.rst index 6a5ddf92f3..0c6f712d45 100644 --- a/doc/source/reference/tutorials/dasPEG_07_basic_interpreter.rst +++ b/doc/source/reference/tutorials/dasPEG_07_basic_interpreter.rst @@ -74,6 +74,7 @@ Keyword Disambiguation Variable names must not match keywords. Negative lookahead (``!``) prevents ``TO``, ``STEP``, or ``THEN`` from being parsed as identifiers: +.. das-doc: fragment .. code-block:: das var primary : double @@ -87,6 +88,7 @@ Text Capture for Deferred Evaluation The line parser captures expression text **without evaluating it**. Helper rules like ``until_then`` match everything up to a keyword: +.. das-doc: fragment .. code-block:: das var until_then : void? @@ -96,6 +98,7 @@ Helper rules like ``until_then`` match everything up to a keyword: Then in the statement rule: +.. das-doc: fragment .. code-block:: das rule("IF", WS, "{+until_then}" as cond, WS, "THEN", WS, number as target) { diff --git a/doc/source/reference/tutorials/dasPUGIXML_01_parsing.rst b/doc/source/reference/tutorials/dasPUGIXML_01_parsing.rst index 6c16df71f3..f10d3f83c4 100644 --- a/doc/source/reference/tutorials/dasPUGIXML_01_parsing.rst +++ b/doc/source/reference/tutorials/dasPUGIXML_01_parsing.rst @@ -62,26 +62,33 @@ Iterating children ``for_each_child`` iterates over all child elements of a node using a block callback. Pass an optional name to filter by tag: +.. das-doc: given var node : xml_node +.. das-doc: given var child_node : xml_node + .. code-block:: das // all children - root |> for_each_child() <| $(ch) { + node |> for_each_child() <| $(ch) { print("<{ch.name}>\n") } // only children - root |> for_each_child("setting") <| $(ch) { ... } + node |> for_each_child("setting") <| $(ch) { + ... + } ``each_child`` returns a lazy iterator for use in ``for`` loops — same traversal, different syntax: .. code-block:: das - for (ch in each_child(root)) { + for (ch in each_child(node)) { print("<{ch.name}>\n") } - for (ch in each_child(root, "setting")) { ... } + for (ch in each_child(node, "setting")) { + ... + } ``for_each_attribute`` iterates attributes with a block callback: @@ -134,7 +141,7 @@ to typed values: // Attribute access let x_attr = node["x"] // xml_attribute let x_val = node["x"] as int // 10 - let label = node["label"] as string + let lbl = node["label"] as string // `label` is a reserved word // Text access let count = child_node.text as int @@ -149,7 +156,7 @@ Combining these tools to read a book catalog: .. code-block:: das - open_xml("books.xml") <| $(doc, ok) { + open_xml("tutorials/dasPUGIXML/books.xml") <| $(doc, ok) { if (!ok) { return; } doc.document_element |> for_each_child("book") <| $(book) { let title = node_text(book, "title") diff --git a/doc/source/reference/tutorials/dasPUGIXML_02_building.rst b/doc/source/reference/tutorials/dasPUGIXML_02_building.rst index 2acbd77599..23b8e9e5ae 100644 --- a/doc/source/reference/tutorials/dasPUGIXML_02_building.rst +++ b/doc/source/reference/tutorials/dasPUGIXML_02_building.rst @@ -23,8 +23,8 @@ frees it automatically: with_doc() <| $(doc) { var dnode = doc as xml_node - let root = append_child(dnode, "greeting") - set(root.text, "Hello!") + let greeting = append_child(dnode, "greeting") + set(greeting.text, "Hello!") print(to_string(doc)) } @@ -70,6 +70,11 @@ Attribute chaining ``attr()`` appends an attribute and returns the **parent node**, enabling fluent chaining: +.. das-doc: given var config : xml_node +.. das-doc: given var root : xml_node +.. das-doc: given let names = fixed_array("Alice", "Bob", "Charlie") +.. das-doc: given let scores = fixed_array(95, 87, 92) + .. code-block:: das var display = config |> tag("display") diff --git a/doc/source/reference/tutorials/dasPUGIXML_03_xpath.rst b/doc/source/reference/tutorials/dasPUGIXML_03_xpath.rst index 5d90ddd6aa..a5136a356b 100644 --- a/doc/source/reference/tutorials/dasPUGIXML_03_xpath.rst +++ b/doc/source/reference/tutorials/dasPUGIXML_03_xpath.rst @@ -13,7 +13,33 @@ This tutorial demonstrates querying XML documents with XPath, using both convenience wrappers and compiled queries in ``pugixml/PUGIXML_boost``. The tutorial uses an inline catalog XML for most examples, then queries -``books.xml`` at the end. +``books.xml`` at the end. Here is the catalog every query below runs +against: + +.. code-block:: das + + let CATALOG_XML = " + + Wireless Mouse + 29.99 + 4.5 + + + Keyboard + 79.99 + 4.8 + + + daslang Handbook + 49.99 + 4.9 + + + XML in Practice + 34.99 + 4.2 + + " ``select_text`` — first match text ==================================== @@ -25,13 +51,13 @@ first matching node. Returns a default string if nothing matches: parse_xml(CATALOG_XML) <| $(doc, ok) { if (!ok) { return; } - let root = doc.document_element + let catalog = doc.document_element - let first_name = select_text(root, "product[1]/name") + let first_name = select_text(catalog, "product[1]/name") print("first product: {first_name}\n") // first product: Wireless Mouse - let missing = select_text(root, "product/description", "N/A") + let missing = select_text(catalog, "product/description", "N/A") print("description: {missing}\n") // description: N/A } @@ -42,6 +68,8 @@ first matching node. Returns a default string if nothing matches: ``select_value`` returns the string value of the first XPath match — either an attribute's value or an element's text content: +.. das-doc: given var root : xml_node + .. code-block:: das let id = select_value(root, "product[1]/@id") @@ -132,16 +160,16 @@ The tutorial ends by querying the real ``books.xml`` sample file: open_xml("tutorials/dasPUGIXML/books.xml") <| $(doc, ok) { if (!ok) { return; } - let root = doc.document_element + let library = doc.document_element - var en_books = select_nodes(root, "book[@lang='en']") + var en_books = select_nodes(library, "book[@lang='en']") print("English books: {en_books.size}\n") unsafe { delete en_books; } - let cheapest = select_text(root, "book[not(price > ../book/price)]/title") + let cheapest = select_text(library, "book[not(price > ../book/price)]/title") print("cheapest: {cheapest}\n") - root |> for_each_select("book/author") <| $(xn) { + library |> for_each_select("book/author") <| $(xn) { print(" {xn.node.text as string}\n") } } diff --git a/doc/source/reference/tutorials/dasPUGIXML_04_serialization.rst b/doc/source/reference/tutorials/dasPUGIXML_04_serialization.rst index d5400838fe..8ac9737aae 100644 --- a/doc/source/reference/tutorials/dasPUGIXML_04_serialization.rst +++ b/doc/source/reference/tutorials/dasPUGIXML_04_serialization.rst @@ -208,12 +208,24 @@ field without changing the daslang field name: .. code-block:: das + enum Priority { + low + medium + high + } + struct Config { @rename = "type" _type : string @enum_as_int level : Priority name : string } +Here ``_type`` is written as ````, and ``level`` as the integer +``2`` rather than ``high``. A third annotation, ``@unescape``, decodes +backslash escape sequences in a ``string`` field before writing it, so a +``\n`` in the value becomes a real newline in the XML text. XML special +characters (``&``, ``<``, ``>``) are still escaped either way. + The low-level ``XML()`` builder =============================== diff --git a/doc/source/reference/tutorials/dasStbImage_01_loading_images.rst b/doc/source/reference/tutorials/dasStbImage_01_loading_images.rst index 68ce4c1789..c97f04e65a 100644 --- a/doc/source/reference/tutorials/dasStbImage_01_loading_images.rst +++ b/doc/source/reference/tutorials/dasStbImage_01_loading_images.rst @@ -98,6 +98,7 @@ exist: .. code-block:: das + let path = "photo.png" print("HDR: {is_hdr(path)}, 16-bit: {is_16_bit(path)}\n") .. seealso:: diff --git a/doc/source/reference/tutorials/dasStbImage_02_saving_and_encoding.rst b/doc/source/reference/tutorials/dasStbImage_02_saving_and_encoding.rst index 358b3d4a88..1dcfa5e904 100644 --- a/doc/source/reference/tutorials/dasStbImage_02_saving_and_encoding.rst +++ b/doc/source/reference/tutorials/dasStbImage_02_saving_and_encoding.rst @@ -13,11 +13,17 @@ STBIMAGE-02 — Saving and Encoding Images This tutorial covers saving images to files, encoding to in-memory byte arrays, loading from memory, and round-trip verification. +The examples assume ``img`` holds a loaded image — see +:ref:`tutorial_dasStbImage_loading_images`. + +.. das-doc: given var img : Image + Saving to File ============== ``Image.save(path, quality)`` saves to file. The format is determined by -the file extension (``.png``, ``.bmp``, ``.tga``, ``.jpg``, ``.hdr``). +the file extension (``.png``, ``.bmp``, ``.tga``, ``.jpg``/``.jpeg``, +``.hdr``). The quality parameter only matters for JPEG (1–100, default 90): .. code-block:: das @@ -60,7 +66,7 @@ Also available: ``load_hdr_from_memory()``, ``load_16_from_memory()``: .. code-block:: das var inscope img2 : Image - let (ok, error) = img2.load_from_memory(png_buf) + let (ok, error) = img2.load_from_memory(buf) Format detection works from memory too: @@ -78,10 +84,10 @@ an encode/decode cycle: .. code-block:: das var buf : array - original.encode("png", buf) + img.encode("png", buf) var inscope decoded : Image decoded.load_from_memory(buf) - // decoded pixels == original pixels + // decoded.bytes == img.bytes .. seealso:: diff --git a/doc/source/reference/tutorials/dasStbImage_03_transforms.rst b/doc/source/reference/tutorials/dasStbImage_03_transforms.rst index 3b7e601f68..0e429c02f4 100644 --- a/doc/source/reference/tutorials/dasStbImage_03_transforms.rst +++ b/doc/source/reference/tutorials/dasStbImage_03_transforms.rst @@ -13,6 +13,11 @@ STBIMAGE-03 — Image Transforms This tutorial covers resizing, flipping, cropping, and compositing images. +The examples assume ``img`` holds a loaded image — see +:ref:`tutorial_dasStbImage_loading_images`. + +.. das-doc: given var img : Image + Resizing ======== @@ -62,8 +67,8 @@ the same ``bpc``. Pixels outside the destination bounds are clipped: .. code-block:: das - var inscope canvas <- make_solid(64, 64, ...) - var inscope overlay <- make_solid(16, 16, ...) + var inscope canvas <- make_image(64, 64, 4) // RGBA destination + var inscope overlay <- make_image(16, 16, 4) // same channels and bpc canvas.blit(overlay, 8, 8) Creating Blank Canvases diff --git a/doc/source/reference/tutorials/dasStbImage_04_pixel_access_and_conversion.rst b/doc/source/reference/tutorials/dasStbImage_04_pixel_access_and_conversion.rst index ddf82e5952..ef5081cf18 100644 --- a/doc/source/reference/tutorials/dasStbImage_04_pixel_access_and_conversion.rst +++ b/doc/source/reference/tutorials/dasStbImage_04_pixel_access_and_conversion.rst @@ -15,6 +15,11 @@ STBIMAGE-04 — Pixel Access and Format Conversion This tutorial covers reading and modifying pixel data, row-level access, and converting between channel counts and bit depths. +The examples assume ``img`` holds a loaded RGBA image — see +:ref:`tutorial_dasStbImage_loading_images`. + +.. das-doc: given var img : Image + Reading Pixels ============== @@ -57,8 +62,9 @@ image data: .. code-block:: das + let pixel_count = img.width * img.height img |> with_pixels() <| $(var pixels : array#) { - for (i in range(width * height)) { + for (i in range(pixel_count)) { pixels[i * 4 + 0] = uint8(255 - int(pixels[i * 4 + 0])) } } @@ -82,7 +88,7 @@ From To Behavior .. code-block:: das - var inscope rgb <- rgba_img.to_channels(3) // drop alpha + var inscope rgb <- img.to_channels(3) // drop alpha var inscope grey <- rgb.to_channels(1) // to greyscale var inscope back <- grey.to_channels(4) // grey → RGBA diff --git a/doc/source/reference/tutorials/dasStbImage_05_drawing_and_blending.rst b/doc/source/reference/tutorials/dasStbImage_05_drawing_and_blending.rst index f457b59d5f..70824863aa 100644 --- a/doc/source/reference/tutorials/dasStbImage_05_drawing_and_blending.rst +++ b/doc/source/reference/tutorials/dasStbImage_05_drawing_and_blending.rst @@ -49,11 +49,15 @@ Alpha Blending pixel is the alpha value for blending the color ``(r, g, b)`` onto the destination. -The blend formula per channel is:: +The blend formula per color channel is:: - out = (alpha * color + (255 - alpha) * dest + 128) / 255 + out = (alpha * color + (255 - alpha) * dest) / 255 -Alpha channel of the destination is updated as +The code divides by 255 with the integer trick ``(x + 1 + (x >> 8)) >> 8``, +which gives the same answer as ``x / 255`` over the whole byte range — so a +fully opaque source pixel lands on the exact color you passed in. Source +pixels with ``alpha == 0`` are skipped, and the destination keeps its value +there. The alpha channel of the destination becomes ``min(dest_alpha + src_alpha, 255)``. This is the fundamental building block for software text rendering: @@ -100,7 +104,9 @@ is a common pattern in UI rendering: canvas.fill_rect(8, 35, 48, 1, border) // bottom canvas.fill_rect(8, 12, 1, 24, border) // left canvas.fill_rect(55, 12, 1, 24, border) // right - // Alpha-blended icon + // Alpha-blended icon — a 1-channel coverage map, like a glyph from a font atlas + var icon = make_image(12, 12, 1) + icon.fill_rect(2, 2, 8, 8, uint8(255)) canvas.blit_alpha(icon, 0, 0, 14, 18, 12, 12, 255, 255, 255) .. seealso:: diff --git a/doc/source/reference/tutorials/imgui/application_lifecycle.rst b/doc/source/reference/tutorials/imgui/application_lifecycle.rst index c842865725..1e25d68b2e 100644 --- a/doc/source/reference/tutorials/imgui/application_lifecycle.rst +++ b/doc/source/reference/tutorials/imgui/application_lifecycle.rst @@ -75,7 +75,7 @@ collection is needed only occasionally. Standalone programs do not receive the live host's between-update collection pass. Their ``main`` loop must call ``harness_maybe_collect_gc()`` immediately -after ``update()`` returns. The helper delegates to the existing ``glfw_live`` +after ``update()`` returns. The helper delegates to the ``live/live_gc`` fragmentation heuristic: calling the check every loop does **not** mean collecting every loop. It only collects when the heap has enough unused space to make compaction worthwhile. It is a no-op under ``daslang-live``, because @@ -89,6 +89,7 @@ collectable locals that span the collection call, and collect only after For a non-harness service, use the explicit server form: +.. das-doc: fragment .. code-block:: das [export] @@ -105,12 +106,14 @@ For non-harness services the equivalent call is their own ``maybe_collect_gc``. ``utils/dasllama-server/main.das`` is the production reference. Its ``maybe_collect_gc`` skips host-owned live mode, rate-limits collections, uses heap-fragmentation ratios, and can honor a forced diagnostic collection. -Simple GLFW applications can use ``live/glfw_live``'s existing -``maybe_collect_gc`` helper at the same post-update boundary. +Applications that do not use the harness can call ``live/live_gc``'s +``maybe_collect_gc`` helper at the same post-update boundary +(``live/glfw_live`` re-exports it for GLFW callers). Delete-first example ==================== +.. das-doc: fragment .. code-block:: das def update() { diff --git a/doc/source/reference/tutorials/imgui/boost_basics.rst b/doc/source/reference/tutorials/imgui/boost_basics.rst index dd16dd1d8b..3a1b8e5d59 100644 --- a/doc/source/reference/tutorials/imgui/boost_basics.rst +++ b/doc/source/reference/tutorials/imgui/boost_basics.rst @@ -30,6 +30,11 @@ counting would abort the recording. Requires ======== +.. das-doc: given require imgui +.. das-doc: given require imgui/imgui_boost_v2 +.. das-doc: given require imgui/imgui_widgets_builtin +.. das-doc: given require imgui/imgui_containers_builtin + The ``require`` block pulls in: * The C++-bound Dear ImGui surface (``imgui``, ``imgui_app``). @@ -123,7 +128,7 @@ bounds line is plain assignment to the struct field. text("bumps = {BUMP_BTN.click_count}") ``button(BUMP_BTN, ...)`` returns ``true`` on the frame the click registers; the -underlying ``ButtonState`` keeps ``click_count`` updated for later assertions +underlying ``ClickState`` keeps ``click_count`` updated for later assertions (e.g. from ``imgui_playwright``). Standalone vs live diff --git a/doc/source/reference/tutorials/imgui/buttons.rst b/doc/source/reference/tutorials/imgui/buttons.rst index 4c4c7ba36f..a9bcc8be92 100644 --- a/doc/source/reference/tutorials/imgui/buttons.rst +++ b/doc/source/reference/tutorials/imgui/buttons.rst @@ -8,6 +8,7 @@ Six shapes of click trigger. All six use ``ClickState``; ``state.clicked`` is the per-frame bool, ``state.click_count`` accumulates across frames. +.. das-doc: signatures .. code-block:: das button(IDENT, (text = "..")) // the workhorse @@ -35,6 +36,11 @@ Walkthrough Requires ======== +.. das-doc: given require imgui +.. das-doc: given require imgui/imgui_boost_v2 +.. das-doc: given require imgui/imgui_widgets_builtin +.. das-doc: given require imgui/imgui_containers_builtin + Already in the baseline boost layer: * ``imgui/imgui_widgets_builtin`` — every ``*_button`` rail. @@ -113,25 +119,27 @@ interaction stack. image_button — textured trigger =============================== -Renders a texture as the button face. ``user_texture_id : void?`` is -ImGui's opaque texture handle — on GL it's the GL texture id cast to -``void?``. The font atlas (``io.Fonts.TexID``) is always available and -makes for a no-setup demo target: +Renders a texture as the button face. ``user_texture_id : ImTextureRef`` is +ImGui 1.92's texture handle — either a backend-owned texture +(``_TexData``) or your own GL texture name in the ``_TexID`` slot. The +font atlas exposes itself as ``io.Fonts.TexRef``, which is always +available and makes for a no-setup demo target: .. code-block:: das let io & = unsafe(GetIO()) - let font_tex = io.Fonts.TexID - if (font_tex != null) { + if (io.Fonts.TexData != null) { image_button(BTN, (text = "##font", - user_texture_id = font_tex, + user_texture_id = io.Fonts.TexRef, size = float2(48.0f, 48.0f))) } -Real apps load their own textures via ``stbi`` or -``LoadTextureFromFile`` helpers (see ``examples/imgui_demo/widgets.das`` -for the 8-button image-grid pattern). ``uv0`` / ``uv1`` slice into the -texture; ``bg_col`` / ``tint_col`` modulate the rendered face. +The ``TexData != null`` guard skips the frames before the backend has +built the atlas. Real apps decode and upload their own texture and wrap +the GL name in an ``ImTextureRef`` — see :ref:`tutorial_texture_ref` for +the full path, and ``examples/imgui_demo/widgets.das`` for the 8-button +image-grid pattern. ``uv0`` / ``uv1`` slice into the texture; +``bg_col`` / ``tint_col`` modulate the rendered face. tab_item_button — button styled as a tab ======================================== @@ -144,7 +152,7 @@ to the start of the bar, ``Trailing`` to the end — the canonical .. code-block:: das tab_bar(BAR, (text = "MyTabBar", - flags = ImGuiTabBarFlags.FittingPolicyResizeDown)) { + flags = ImGuiTabBarFlags.FittingPolicyShrink)) { if (tab_item_button(HELP, (text = "?", flags = ImGuiTabItemFlags.Leading | ImGuiTabItemFlags.NoTooltip))) { @@ -181,9 +189,13 @@ Every ``*_button`` accepts ``imgui_click`` and snapshot probes: Caller-owned variant ==================== -For sites where the click target lives on an external bool, use -``edit_button`` from ``imgui_boost_runtime`` (see -:ref:`tutorial_edit_external_tour`). +There is no ``edit_button``: a button owns no value, only click +bookkeeping, so there is nothing for the caller to hold. The +caller-owned rail covers the *toggle* end of the click family instead — +``edit_checkbox`` / ``edit_radio_button`` / ``edit_menu_item`` take a +``bool?`` pointer via ``safe_addr`` and write the caller's own flag (see +:ref:`tutorial_edit_external_tour`). For a plain trigger, read +``button(...)``'s return value directly. .. seealso:: diff --git a/doc/source/reference/tutorials/imgui/child.rst b/doc/source/reference/tutorials/imgui/child.rst index c399a4ee51..245f304c87 100644 --- a/doc/source/reference/tutorials/imgui/child.rst +++ b/doc/source/reference/tutorials/imgui/child.rst @@ -8,6 +8,7 @@ Child windows a sub-window that can be sized, bordered, scrolled, and addressed in the registry hierarchy. The wrapper's signature: +.. das-doc: signatures .. code-block:: das child(IDENT, (text = "...", @@ -45,6 +46,11 @@ not over the child, wheel not attributed) would abort the recording. Requires ======== +.. das-doc: given require imgui +.. das-doc: given require imgui/imgui_boost_v2 +.. das-doc: given require imgui/imgui_widgets_builtin +.. das-doc: given require imgui/imgui_containers_builtin + Already in the baseline boost layer: * ``imgui/imgui_containers_builtin`` — defines ``child``, @@ -71,8 +77,9 @@ child_flags vs window_flags ``ImGuiChildFlags`` (the third arg) is child-specific: -* ``Border`` — draw a one-pixel border around the region. -* ``AutoResizeX`` / ``AutoResizeY`` — height/width tracks content extent. +* ``Borders`` — draw a one-pixel border around the region. +* ``ResizeX`` / ``ResizeY`` — user-draggable resize grip on that axis. +* ``AutoResizeX`` / ``AutoResizeY`` — width/height tracks content extent. * ``AlwaysAutoResize`` — combine both axes and re-measure every frame. * ``FrameStyle`` — frame-style chrome (like input groups). Implies a background and rounded corners drawn from the active style. @@ -145,7 +152,7 @@ Horizontal scroll child(SCROLL_C, (text = "scroll_c", size = float2(720.0f, 90.0f), - child_flags = ImGuiChildFlags.Border, + child_flags = ImGuiChildFlags.Borders, window_flags = ImGuiWindowFlags.HorizontalScrollbar)) { text(LONG_LINE, (text = "very wide content: lorem ipsum...")) } diff --git a/doc/source/reference/tutorials/imgui/collapsing_header.rst b/doc/source/reference/tutorials/imgui/collapsing_header.rst index f4ac8678c5..8d6e7a0c3a 100644 --- a/doc/source/reference/tutorials/imgui/collapsing_header.rst +++ b/doc/source/reference/tutorials/imgui/collapsing_header.rst @@ -37,6 +37,11 @@ Walkthrough Requires ======== +.. das-doc: given require imgui +.. das-doc: given require imgui/imgui_boost_v2 +.. das-doc: given require imgui/imgui_widgets_builtin +.. das-doc: given require imgui/imgui_containers_builtin + Already in the baseline boost layer: * ``imgui/imgui_containers_builtin`` — ``collapsing_header``. @@ -47,6 +52,7 @@ Two visibility dimensions ``CollapsingHeaderState`` carries both gates: +.. das-doc: signatures .. code-block:: das var CLOSABLE_CH : CollapsingHeaderState diff --git a/doc/source/reference/tutorials/imgui/color.rst b/doc/source/reference/tutorials/imgui/color.rst index bcf3423f69..fcbeee43d9 100644 --- a/doc/source/reference/tutorials/imgui/color.rst +++ b/doc/source/reference/tutorials/imgui/color.rst @@ -7,6 +7,7 @@ Color Five shapes for picking a color: two inline editors, two pop-out pickers, and one caller-owned swatch: +.. das-doc: signatures .. code-block:: das color_edit3(IDENT, (text = "..")) // inline RGB row @@ -40,6 +41,11 @@ Walkthrough Requires ======== +.. das-doc: given require imgui +.. das-doc: given require imgui/imgui_boost_v2 +.. das-doc: given require imgui/imgui_widgets_builtin +.. das-doc: given require daslib/safe_addr + Already in the baseline boost layer: * ``imgui/imgui_widgets_builtin`` — ``color_edit3/4`` / ``color_picker3/4`` / @@ -47,6 +53,9 @@ Already in the baseline boost layer: * ``imgui/imgui_boost_runtime`` — ``ColorState3`` / ``ColorState4`` / ``ClickState`` structs. +The caller-owned form at the end of this page also needs +``daslib/safe_addr`` for ``safe_addr``. + edit vs picker ============== @@ -97,9 +106,11 @@ Flags ``flags : ImGuiColorEditFlags`` carries the standard edit-mode toggles — ``NoAlpha``, ``NoPicker``, ``NoOptions``, ``NoSmallPreview``, ``NoInputs``, ``NoTooltip``, ``NoLabel``, ``NoSidePreview``, -``NoDragDrop``, ``NoBorder``, ``AlphaBar``, ``AlphaPreview``, -``AlphaPreviewHalf``, ``HDR``, plus display-mode bits -(``DisplayRGB`` / ``DisplayHSV`` / ``DisplayHex``) and input-mode bits +``NoDragDrop``, ``NoBorder``, ``NoColorMarkers``, ``AlphaBar``, +``AlphaOpaque``, ``AlphaNoBg``, ``AlphaPreviewHalf``, ``HDR``, plus +display-mode bits (``DisplayRGB`` / ``DisplayHSV`` / ``DisplayHex``), +data-type bits (``Uint8`` / ``Float``), picker-shape bits +(``PickerHueBar`` / ``PickerHueWheel``) and input-mode bits (``InputRGB`` / ``InputHSV``). Composable via ``|``: .. code-block:: das diff --git a/doc/source/reference/tutorials/imgui/color_button_hover.rst b/doc/source/reference/tutorials/imgui/color_button_hover.rst index b86a73677c..de79c368a5 100644 --- a/doc/source/reference/tutorials/imgui/color_button_hover.rst +++ b/doc/source/reference/tutorials/imgui/color_button_hover.rst @@ -36,6 +36,10 @@ Walkthrough Requires ======== +.. das-doc: given require imgui +.. das-doc: given require imgui/imgui_boost_v2 +.. das-doc: given require imgui/imgui_widgets_builtin + Same baseline as ``color_button`` — already in ``imgui/imgui_widgets_builtin`` (re-exported by ``imgui/imgui_boost_v2``). @@ -58,11 +62,14 @@ Click vs hover The two variants share the same call shape but pick the right state shape for the call site: -* ``color_button(IDENT, (col, size, flags))`` — returns ``bool clicked``, - records ``state.click_count`` and ``state.clicked``. -* ``color_button_hover(IDENT, (col, size, flags))`` — returns +* ``color_button(IDENT, (desc_id, col, size, flags))`` — returns + ``bool clicked``, records ``state.click_count`` and ``state.clicked``. +* ``color_button_hover(IDENT, (desc_id, col, size, flags))`` — returns ``bool hovered``, no click bookkeeping. +``desc_id`` and ``col`` are mandatory in both; ``size`` and ``flags`` +default to ``float2(0, 0)`` and ``ImGuiColorEditFlags.None``. + A site that needs both (click triggers an action, hover updates a preview) should use the regular ``color_button`` and read ``IsItemHovered()`` after the call — the post-item hover query is diff --git a/doc/source/reference/tutorials/imgui/containers.rst b/doc/source/reference/tutorials/imgui/containers.rst index 5de1b75dda..e132ef871f 100644 --- a/doc/source/reference/tutorials/imgui/containers.rst +++ b/doc/source/reference/tutorials/imgui/containers.rst @@ -52,6 +52,11 @@ aborts the recording. Requires ======== +.. das-doc: given require imgui +.. das-doc: given require imgui/imgui_boost_v2 +.. das-doc: given require imgui/imgui_widgets_builtin +.. das-doc: given require imgui/imgui_containers_builtin + One extra module on top of the baseline boost layer: * ``imgui/imgui_containers_builtin`` — defines every container macro @@ -98,23 +103,29 @@ active-tab selection: tab_bar(MAIN_TABS, (text = "MainTabs", flags = ImGuiTabBarFlags.None)) { tab_item(GENERAL_TAB, (text = "General", closable = false, flags = ImGuiTabItemFlags.None)) { - Text("Tabs share a window; only the active tab renders.") + text("Tabs share a window; only the active tab renders.") checkbox(WIRE, (text = "Wireframe")) } - tab_item(AUDIO_TAB, (text = "Audio", ...)) { ... } - tab_item(INFO_TAB, (text = "Info", ...)) { ... } + tab_item(AUDIO_TAB, (text = "Audio", closable = false, + flags = ImGuiTabItemFlags.None)) { + checkbox(MUTE, (text = "Mute")) + } } Only the **active tab's block runs each frame** — widgets inside inactive tabs aren't in the registry that frame, so a snapshot taken while ``GENERAL_TAB`` is active won't list any ``AUDIO_TAB`` children. ``TabItemState.pending_open`` controls the closable-tab visibility -(skip BeginTabItem entirely when ``open=false``), but it does NOT -programmatically select the active tab — that's an ImGui internal -state, set by clicking the tab header. Each ``tab_item`` registers its -header's bbox, so a driver switches tabs the way a user does: an +(skip BeginTabItem entirely when ``open=false``); selecting the active +tab is a separate channel, ``pending_select``, driven by the +``imgui_select`` live command (it hands ImGui +``ImGuiTabItemFlags.SetSelected`` on the next frame and records +``state.selected``). Each ``tab_item`` also registers its header's +bbox, so a driver can instead switch tabs the way a user does: an ``imgui_click`` on the ``tab_item`` target (e.g. ``CONT_WIN/MAIN_TABS/AUDIO_TAB``) lands on the header and selects it. +Prefer ``imgui_select`` when the tab may be scrolled out of the bar — +``imgui_click`` needs a visible header to hit. popup ===== @@ -130,7 +141,7 @@ A popup renders only when explicitly opened. The state struct's } popup(OPTIONS_POPUP, (text = "OptionsPopup", flags = ImGuiWindowFlags.None)) { - Text("Options") + text("Options") checkbox(OPT_VSYNC, (text = "VSync")) if (button(POPUP_CLOSE_BTN, (text = "Close"))) { OPTIONS_POPUP.pending_close = true @@ -154,8 +165,8 @@ No manual gate: button(HOVER_BTN, (text = "Hover me")) item_tooltip(HOVER_TIP) { - Text("This text appears on hover.") - Text("Driven by BeginItemTooltip (auto-gated).") + text("This text appears on hover.") + text("Driven by BeginItemTooltip (auto-gated).") } For tooltips whose own gating logic differs from "previous item diff --git a/doc/source/reference/tutorials/imgui/data_table.rst b/doc/source/reference/tutorials/imgui/data_table.rst index 60e1376075..81d6e78975 100644 --- a/doc/source/reference/tutorials/imgui/data_table.rst +++ b/doc/source/reference/tutorials/imgui/data_table.rst @@ -7,7 +7,7 @@ data_table ImGui's tables API — ``BeginTable`` / ``EndTable`` with body-internal ``TableSetupColumn`` / ``TableHeadersRow`` / ``TableNextRow`` / ``TableSetColumnIndex`` / ``TableNextColumn`` cursor primitives — lives -behind one boost container plus six snake_case pass-throughs in +behind one boost container plus a family of snake_case pass-throughs in ``imgui/imgui_table_builtin``. The container is named ``data_table`` (not ``table``) because ``table`` is a daslang reserved keyword for the ``table`` type constructor. @@ -42,10 +42,15 @@ text, so a header click that failed to re-sort would abort the recording. Requires ======== +.. das-doc: given require imgui +.. das-doc: given require imgui/imgui_boost_v2 +.. das-doc: given require imgui/imgui_table_builtin + One extra module on top of the baseline boost layer: * ``imgui/imgui_table_builtin`` — the ``data_table`` container plus the - six ``table_*`` snake_case primitives the body calls into. + ``table_*`` snake_case primitives the body calls into, the + ``TableSortSpec`` struct and the ``sort_specs`` helper. Container shape =============== @@ -70,20 +75,27 @@ means "no explicit inner width" (use the outer width). Body primitives =============== -The six body cursor calls are plain ``def public`` wrappers — same +The body cursor calls are plain ``def public`` wrappers — same arguments as the underlying ImGui calls, snake_case names: -* ``table_setup_column(label, flags?, init_width?, user_id?)`` — declare a - column before the header row. +* ``table_setup_column(text, flags?, init_width_or_weight?, user_id?)`` — + declare a column before the header row. * ``table_setup_scroll_freeze(cols, rows)`` — pin the first N columns / M rows during scrolling. * ``table_headers_row()`` — submit the header row using the ``table_setup_column`` labels. +* ``table_header(text)`` / ``table_angled_headers_row()`` — a single + custom header cell, and the rotated-label header row. * ``table_next_row(flags?, min_row_height?)`` — start the next row. * ``table_set_column_index(col) -> bool`` — jump to a specific column; returns ``true`` when the column is visible. * ``table_next_column() -> bool`` — advance one column (or wrap to next row); also returns the visibility bool. +* ``table_set_bg_color(target, color, column_n?)`` — paint a row or cell + background. +* ``table_get_column_count()`` / ``table_get_column_index()`` / + ``table_get_row_index()`` / ``table_get_column_name(col?)`` / + ``table_get_column_flags(col?)`` — cursor and layout queries. ``TableState`` (the container's state struct) echoes per-call config — columns, flags, outer_size, inner_width — so snapshot consumers can read @@ -104,7 +116,7 @@ inside the body that ImGui fires when the sort state goes dirty. * ``ImGuiTableFlags.Sortable`` enables single-column sort (click any header). Adding ``ImGuiTableFlags.SortMulti`` enables multi-column sort (Shift+click a second header to append a secondary sort key). -* ``table_setup_column("Name", flags, init_width, user_id=COL_NAME)`` +* ``table_setup_column("Name", flags, init_width_or_weight, user_id = COL_NAME)`` tags the column with a stable identifier (a ``uint``). The sort comparator dispatches on ``column_user_id`` rather than ``column_index``, so the sort stays correct after the user reorders diff --git a/doc/source/reference/tutorials/imgui/display_widgets.rst b/doc/source/reference/tutorials/imgui/display_widgets.rst index 603c1b0866..0f7d3146d9 100644 --- a/doc/source/reference/tutorials/imgui/display_widgets.rst +++ b/doc/source/reference/tutorials/imgui/display_widgets.rst @@ -8,8 +8,7 @@ dasImgui's read-only display family wraps ImGui's two main *output* widgets into the v2 boost surface. ``progress_bar`` wraps ``ImGui::ProgressBar`` with a ``ProgressBarState`` payload carrying ``{fraction, size, overlay}``; ``image`` wraps ``ImGui::Image`` with the full ``uv0`` / ``uv1`` / -``tint_col`` / ``border_col`` quartet plus a ``uint64`` reinterpret of the -texture handle for snapshot readability. +``tint_col`` / ``border_col`` quartet. .. code-block:: das @@ -17,7 +16,8 @@ texture handle for snapshot readability. size = float2(-1.0f, 0.0f), overlay = "33%")) - image(IMG_PLAIN, (user_texture_id = font_tex, + let io & = unsafe(GetIO()) + image(IMG_PLAIN, (user_texture_id = io.Fonts.TexRef, size = float2(96.0f, 96.0f), uv0 = float2(0.0f, 0.0f), uv1 = float2(1.0f, 1.0f), @@ -53,6 +53,10 @@ recording. Requires ======== +.. das-doc: given require imgui +.. das-doc: given require imgui/imgui_boost_v2 +.. das-doc: given require imgui/imgui_widgets_builtin + Baseline boost layer (``imgui/imgui_boost_v2`` re-exports the rail family from ``imgui/imgui_widgets_builtin``). No extra modules. @@ -72,14 +76,17 @@ for the auto-formatted percentage. image ===== -``ImageState`` exposes ``user_texture_id`` as a ``uint64`` so the snapshot -can carry the texture handle as a readable number (raw ``void?`` would -serialize as a pointer string). All four ``uv0`` / ``uv1`` / -``tint_col`` / ``border_col`` defaults match the C++ ``ImGui::Image`` -defaults so an unset call is the identity render. The tutorial uses the -ImGui font atlas (``GetIO().Fonts.TexID``) as a guaranteed-available -texture; production code passes a ``user_texture_id`` from your renderer -that targets a real GPU resource. +``user_texture_id`` is an ``ImTextureRef`` (ImGui 1.92's texture handle) +and is deliberately **not** echoed into ``ImageState`` — the handle is +opaque, so telemetry carries only the actionable per-call args +(``size``, ``uv0``, ``uv1``, ``tint_col``, ``border_col``). All four +``uv0`` / ``uv1`` / ``tint_col`` / ``border_col`` defaults match the C++ +``ImGui::Image`` defaults so an unset call is the identity render. The +tutorial uses the ImGui font atlas (``GetIO().Fonts.TexRef``, guarded on +``Fonts.TexData != null`` for the frames before the backend builds it) +as a guaranteed-available texture; production code passes an +``ImTextureRef`` wrapping its own GPU resource — see +:ref:`tutorial_texture_ref`. Snapshot shape ============== diff --git a/doc/source/reference/tutorials/imgui/docking.rst b/doc/source/reference/tutorials/imgui/docking.rst index 6a84e4205d..141141438c 100644 --- a/doc/source/reference/tutorials/imgui/docking.rst +++ b/doc/source/reference/tutorials/imgui/docking.rst @@ -39,6 +39,12 @@ teardown rather than shipping a clip where nothing happened. Requires ======== +.. das-doc: given require imgui +.. das-doc: given require imgui/imgui_boost_v2 +.. das-doc: given require imgui/imgui_widgets_builtin +.. das-doc: given require imgui/imgui_containers_builtin +.. das-doc: given require imgui/imgui_docking_builtin + Same backend + boost layer as :ref:`tutorial_layout`, but layout helpers are replaced by the docking module: @@ -54,6 +60,7 @@ render nothing and ``dock_window`` panels behave like ordinary windows: .. code-block:: das + var io & = unsafe(GetIO()) io.ConfigFlags |= ImGuiConfigFlags.DockingEnable This goes in ``init()`` once per session. @@ -67,17 +74,24 @@ ship a default arrangement via ``DockBuilder``: .. code-block:: das - DockBuilderRemoveNode(dock_id) // clear any prior state - DockBuilderAddDockSpaceNode(dock_id, flags) // fresh dockspace root - DockBuilderSetNodeSize(dock_id, ImVec2(vp.Size.x, vp.Size.y)) - var left_id, right_id : uint - DockBuilderSplitNode(dock_id, ImGuiDir.Left, 0.25f, left_id, right_id) - var top_id, bottom_id : uint - DockBuilderSplitNode(right_id, ImGuiDir.Up, 0.6f, top_id, bottom_id) - DockBuilderDockWindow("Explorer", left_id) - DockBuilderDockWindow("Source", top_id) - DockBuilderDockWindow("Output", bottom_id) - DockBuilderFinish(dock_id) + def setup_default_layout(dock_id : uint) { + DockBuilderRemoveNode(dock_id) // clear any prior state + DockBuilderAddDockSpaceNode(dock_id, ImGuiDockNodeFlags.None) + let vp = GetMainViewport() + DockBuilderSetNodeSize(dock_id, ImVec2(vp.Size.x, vp.Size.y)) + var left_id, right_id : uint + DockBuilderSplitNode(dock_id, ImGuiDir.Left, 0.25f, left_id, right_id) + var top_id, bottom_id : uint + DockBuilderSplitNode(right_id, ImGuiDir.Up, 0.6f, top_id, bottom_id) + DockBuilderDockWindow("Explorer", left_id) + DockBuilderDockWindow("Source", top_id) + DockBuilderDockWindow("Output", bottom_id) + DockBuilderFinish(dock_id) + } + +Call it from inside the ``dockspace`` block with ``state.dock_id`` — the +wrapper captures that id from ``DockSpaceOverViewport`` before the block +runs. ``DockBuilderDockWindow`` matches by window *title string* — the same string you pass to ``dock_window(NAME, (text = "Explorer"))``. The boost macro @@ -112,9 +126,13 @@ floating / moveable frame around the dockable area. dockspace_in_window(DS, (size = float2(0.0f, 0.0f), flags = ImGuiDockNodeFlags.None)) { dock_window(FILES, (text = "Files", closable = false, - flags = ImGuiWindowFlags.None)) { ... } + flags = ImGuiWindowFlags.None)) { + text("project tree") + } dock_window(OUTPUT, (text = "Output", closable = false, - flags = ImGuiWindowFlags.None)) { ... } + flags = ImGuiWindowFlags.None)) { + text("> ready.") + } } } diff --git a/doc/source/reference/tutorials/imgui/drag.rst b/doc/source/reference/tutorials/imgui/drag.rst index 527e93ef43..2bda6af7e7 100644 --- a/doc/source/reference/tutorials/imgui/drag.rst +++ b/doc/source/reference/tutorials/imgui/drag.rst @@ -9,6 +9,7 @@ widget, drag horizontally, release. The value tracks pixel movement scaled by ``speed``. Same call shape spans scalar / vector / range and float / int — one mental model, ten widgets. +.. das-doc: signatures .. code-block:: das drag_float(IDENT, (text = "..", speed = 0.01f, format = "%.3f", @@ -37,12 +38,20 @@ Walkthrough Requires ======== +.. das-doc: given require imgui +.. das-doc: given require imgui/imgui_boost_v2 +.. das-doc: given require imgui/imgui_widgets_builtin +.. das-doc: given require daslib/safe_addr + Already in the baseline boost layer: * ``imgui/imgui_widgets_builtin`` — every ``drag_*`` rail. * ``imgui/imgui_boost_runtime`` — ``DragStateFloat`` / ``DragStateInt`` / ``DragStateFloat3`` / ``DragStateRangeFloat`` state structs. +The caller-owned form at the end of this page also needs +``daslib/safe_addr`` for ``safe_addr``. + Speed and format ================ diff --git a/doc/source/reference/tutorials/imgui/drag_drop.rst b/doc/source/reference/tutorials/imgui/drag_drop.rst index aa2a9909a0..1f21cc5dd3 100644 --- a/doc/source/reference/tutorials/imgui/drag_drop.rst +++ b/doc/source/reference/tutorials/imgui/drag_drop.rst @@ -38,6 +38,14 @@ actually landed (see *Driving from outside* below). Requires ======== +.. das-doc: given require imgui +.. das-doc: given require imgui/imgui_boost_v2 +.. das-doc: given require imgui/imgui_widgets_builtin +.. das-doc: given require imgui/imgui_containers_builtin +.. das-doc: given var PAYLOAD_VALUE : int = 42 +.. das-doc: given var RECEIVED : int = 0 +.. das-doc: given var app : ImguiApp + Same backend + boost layer as the other container tutorials, plus the ``drag_drop_source`` / ``drag_drop_target`` macros from ``imgui/imgui_containers_builtin`` (which is already required by the @@ -58,7 +66,7 @@ implicitly binds it to the previously submitted item: uint64(typeinfo sizeof(PAYLOAD_VALUE)), ImGuiCond.Once) } - Text("Dragging: {PAYLOAD_VALUE}") + text("Dragging: {PAYLOAD_VALUE}") } The body runs **only while the drag is active** — after the user has @@ -70,7 +78,7 @@ pressed the mouse on ``SOURCE_BTN`` and dragged past ImGui's against; ``data`` is a raw pointer with caller-controlled lifetime (ImGui copies it into its internal buffer on each call, so a stack pointer is fine inside the body). -* Any rendering call (``Text``, ``Image``, etc.) draws the drag preview +* Any rendering call (``text``, ``image``, etc.) draws the drag preview tooltip that follows the cursor. Target side diff --git a/doc/source/reference/tutorials/imgui/drawlist.rst b/doc/source/reference/tutorials/imgui/drawlist.rst index 362a98b4dc..86d4da18b8 100644 --- a/doc/source/reference/tutorials/imgui/drawlist.rst +++ b/doc/source/reference/tutorials/imgui/drawlist.rst @@ -15,6 +15,11 @@ clip-rect rails are control-flow helpers, not registered ``[drawlist_prim]`` entries — they shape what the primitives render but do not themselves create snapshot rows: +.. das-doc: given let p0 = float2(16.0f, 16.0f) +.. das-doc: given let p1 = float2(96.0f, 48.0f) +.. das-doc: given let a = float2(16.0f, 16.0f) +.. das-doc: given let b = float2(96.0f, 48.0f) + .. code-block:: das with_window_drawlist() $(var dl) { @@ -72,7 +77,7 @@ and to keep raw ``GetWindowDrawList`` calls off the boost surface. Primitives ========== -Eight ``[drawlist_prim]``-tagged painters cover the geometric basics: +Nine ``[drawlist_prim]``-tagged painters cover the geometric basics: * ``add_line(dl, a, b, col, thickness = 1.0f)`` * ``add_rect(dl, a, b, col, rounding = 0.0f, flags = ImDrawFlags.None, thickness = 1.0f)`` @@ -82,6 +87,9 @@ Eight ``[drawlist_prim]``-tagged painters cover the geometric basics: * ``add_triangle(dl, a, b, c, col, thickness = 1.0f)`` * ``add_triangle_filled(dl, a, b, c, col)`` * ``add_text(dl, pos, col, text)`` +* ``add_text_clipped(dl, pos, col, text, clip_rect)`` — same glyph run with a + per-call CPU-side clip ``(xmin, ymin, xmax, ymax)``; rasterizes only the + glyphs that intersect it, and touches no clip stack. ``col`` is a packed ABGR ``uint`` — use ``rgba(r, g, b, a)`` to build one, or ``GetColorU32(ImGuiCol.*)`` for style-colour references. Positions are @@ -160,6 +168,11 @@ submitted inside the block to the given screen-space rect. Set clip; ``false`` replaces it. See :download:`modules/dasImgui/examples/features/clip_rect.das <../../../../../modules/dasImgui/examples/features/clip_rect.das>`. +To clip one drawlist only — leaving ImGui's widget hit-testing and the other +drawlists alone — use ``with_drawlist_clip_rect(dl, min, max, +intersect_with_current) { ... }`` from ``imgui_drawlist_builtin``: it pushes +and pops on that drawlist's own clip stack. + Path-key telemetry ================== @@ -170,10 +183,17 @@ global is registered). Each primitive's body publishes a lightweight ``bbox`` — into the per-frame registry under that key. Playwright / mouse-cards can then target a specific call site by path: +.. das-doc: given require dastest/testing_boost +.. das-doc: given require imgui/imgui_playwright +.. das-doc: given require daslib/json_boost +.. das-doc: given var d : ImguiApp +.. das-doc: given var t : T? + .. code-block:: das var snap = wait_for_widget(d, "MY_WIN/:42:8", 15.0f) - t |> equal(find_widget(snap, "MY_WIN/:42:8")?["kind"] ?? "", "add_rect", ...) + t |> equal(find_widget(snap, "MY_WIN/:42:8")?["kind"] ?? "", + "add_rect", "the call site painted a rect") Because the synthesized key shifts on edits, tests typically enumerate drawlist entries by ``kind`` (``"add_rect"`` etc.) rather than hardcoding diff --git a/doc/source/reference/tutorials/imgui/dropdown_select.rst b/doc/source/reference/tutorials/imgui/dropdown_select.rst index de138ab4ae..59dc598ff8 100644 --- a/doc/source/reference/tutorials/imgui/dropdown_select.rst +++ b/doc/source/reference/tutorials/imgui/dropdown_select.rst @@ -10,6 +10,7 @@ rows. ``combo_getter`` serves a procedural list from a lambda; ``selectable_label`` lets the caller own the selected-flag (so you can wire it through a list_box for single-select): +.. das-doc: signatures .. code-block:: das combo(IDENT, (text = "..", items <- ["A", "B", ...])) // dropdown, items array @@ -33,6 +34,8 @@ Walkthrough Requires ======== +.. das-doc: given let FONTS = fixed_array("Serif", "Sans", "Mono", "Cursive") + Already in the baseline boost layer: * ``imgui/imgui_widgets_builtin`` — ``combo`` / ``combo_getter`` / diff --git a/doc/source/reference/tutorials/imgui/edit_tab_item.rst b/doc/source/reference/tutorials/imgui/edit_tab_item.rst index 60887d673f..be6c255990 100644 --- a/doc/source/reference/tutorials/imgui/edit_tab_item.rst +++ b/doc/source/reference/tutorials/imgui/edit_tab_item.rst @@ -15,9 +15,9 @@ button writes. var private g_tab_open : bool = true edit_tab_item(safe_addr(g_tab_open), (id = "TAB_A", - text = "alpha", - flags = ImGuiTabItemFlags.None)) { - + text = "alpha", + flags = ImGuiTabItemFlags.None)) { + text("alpha tab body") } When ``*p_open == false`` the tab is skipped entirely — no header diff --git a/doc/source/reference/tutorials/imgui/file_dialog.rst b/doc/source/reference/tutorials/imgui/file_dialog.rst index a917aa8b4a..d6f71109ae 100644 --- a/doc/source/reference/tutorials/imgui/file_dialog.rst +++ b/doc/source/reference/tutorials/imgui/file_dialog.rst @@ -62,6 +62,7 @@ One module on top of the harness: Public API ========== +.. das-doc: signatures .. code-block:: das enum FileDialogResult { none; confirmed; cancelled } diff --git a/doc/source/reference/tutorials/imgui/icons.rst b/doc/source/reference/tutorials/imgui/icons.rst index 9c34ad0d0e..68ae240014 100644 --- a/doc/source/reference/tutorials/imgui/icons.rst +++ b/doc/source/reference/tutorials/imgui/icons.rst @@ -107,6 +107,7 @@ exist for custom chrome: glyph, grouped by category, with its name. Regenerate the catalog images after adding a glyph: - ``daslang -project_root . utils/make_icon_doc.das``. + ``daslang -project_root . modules/dasImgui/utils/make_icon_doc.das`` + (runs from the repo root, and must run windowed — it drives a real ImGui frame). :ref:`Boost macros ` — the macro layer. diff --git a/doc/source/reference/tutorials/imgui/input_numeric.rst b/doc/source/reference/tutorials/imgui/input_numeric.rst index dc70345acc..c5d06e58da 100644 --- a/doc/source/reference/tutorials/imgui/input_numeric.rst +++ b/doc/source/reference/tutorials/imgui/input_numeric.rst @@ -9,6 +9,7 @@ focus, type, Enter to commit. Optional ``+`` / ``-`` step buttons turn scalar forms into discrete-step editors. Same call shape spans scalar / vector / double-precision — nine widgets, one mental model. +.. das-doc: signatures .. code-block:: das input_float(IDENT, (text = "..", step = 0.0f, step_fast = 0.0f, @@ -17,8 +18,8 @@ vector / double-precision — nine widgets, one mental model. flags = ImGuiInputTextFlags....)) input_double(IDENT, (text = "..", step = 0.0lf, step_fast = 0.0lf, format = "%.6f")) - input_float2 / input_float3 / input_float4 // vector — no step args - input_int2 / input_int3 / input_int4 + input_float2 / input_float3 / input_float4 // vector — format + flags, no step + input_int2 / input_int3 / input_int4 // vector — flags only No bounds. ``input_*`` is for **typed entry**; if you need clamped scrubbing, use :ref:`tutorial_drag` or :ref:`tutorial_slider`. @@ -38,12 +39,17 @@ Walkthrough Requires ======== +.. das-doc: given require daslib/safe_addr + Already in the baseline boost layer: * ``imgui/imgui_widgets_builtin`` — every ``input_*`` numeric rail. * ``imgui/imgui_boost_runtime`` — ``InputStateFloat`` / ``InputStateInt`` / ``InputStateDouble`` (+ vector variants) state structs. +The caller-owned form at the end of this page also needs +``daslib/safe_addr`` for ``safe_addr``. + Step buttons ============ @@ -70,15 +76,20 @@ Component-wise editing only. Format ====== -``format`` is the printf-style label format. Defaults are sane for most -cases; bump precision when the user needs to see it: +``format`` is the printf-style label format, on the **float and double** +forms only. Defaults are sane for most cases; bump precision when the +user needs to see it: .. code-block:: das - input_float(MASS, (text = "mass", format = "%.6f")) // 6 decimal places - input_int(LEVEL, (text = "level", format = "%03d")) // 003, 042, etc. + input_float(MASS, (text = "mass", format = "%.6f")) // 6 decimal places + input_float3(BOX, (text = "box", format = "%.1f")) // vectors take it too input_double(EPOCH, (text = "epoch", format = "%.9f")) // sub-ns precision +The ``input_int*`` forms take **no** ``format`` argument — ImGui's +``InputInt`` picks the format itself (``%d``, or ``%08X`` when +``CharsHexadecimal`` is set, see *Flags* below). + Vector forms ============ @@ -120,8 +131,8 @@ allowed), ``EscapeClearsAll``, etc. Composable via ``|``: .. code-block:: das input_int(HEX_ADDR, (text = "addr", - flags = ImGuiInputTextFlags.CharsHexadecimal, - format = "0x%08X")) + flags = ImGuiInputTextFlags.CharsHexadecimal)) + // ImGui renders and parses this field as %08X on its own Driving from outside ==================== diff --git a/doc/source/reference/tutorials/imgui/input_text.rst b/doc/source/reference/tutorials/imgui/input_text.rst index b372c58752..bc60132589 100644 --- a/doc/source/reference/tutorials/imgui/input_text.rst +++ b/doc/source/reference/tutorials/imgui/input_text.rst @@ -9,6 +9,7 @@ grow-on-overflow, callback-driven, and the inline filter editor. Six widgets, one ``InputTextState`` (text_filter uses its own ``TextFilterState``). +.. das-doc: signatures .. code-block:: das input_text(IDENT, (text = "..", flags = ImGuiInputTextFlags....)) @@ -38,6 +39,10 @@ Walkthrough Requires ======== +.. das-doc: given let LOG_LINES = fixed_array("[info] startup complete", "[error] disk full") +.. das-doc: given var LOG_LINE : table +.. das-doc: given def completion_cb(var data : ImGuiInputTextCallbackData) : int => 0 + Already in the baseline boost layer: * ``imgui/imgui_widgets_builtin`` — every ``input_text*`` rail + diff --git a/doc/source/reference/tutorials/imgui/layout.rst b/doc/source/reference/tutorials/imgui/layout.rst index 9135986e44..fd456e9cd5 100644 --- a/doc/source/reference/tutorials/imgui/layout.rst +++ b/doc/source/reference/tutorials/imgui/layout.rst @@ -33,6 +33,9 @@ moved nothing aborts the recording at teardown. Requires ======== +.. das-doc: given require imgui/imgui_scope_builtin +.. das-doc: given let LIPSUM = "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + Same backend + boost layer as :ref:`tutorial_widgets_tour`, with two extra modules pulled in: @@ -64,13 +67,18 @@ The panel composes three helpers, nested: .. code-block:: das - window(LAYOUT_WIN, (text = "IDE layout", ...)) { + window(LAYOUT_WIN, (text = "IDE layout", closable = false, + flags = ImGuiWindowFlags.None)) { dock_left(SIDEBAR, (init = 200.0f, bounds = (80.0f, 320.0f))) { - // sidebar contents + text("Sidebar") } split_v(SPLIT_VERT, (init = 0.65f, bounds = (0.1f, 0.9f)), - ${ split_h(SPLIT_MAIN, ...) { ... } }, - ${ /* bottom pane */ }) + ${ + split_h(SPLIT_MAIN, (init = 0.4f, bounds = (0.1f, 0.9f)), + ${ text("Files") }, + ${ text("Editor") }) + }, + ${ text("Output") }) } ``dock_left`` carves a fixed-width column off the left edge. Its @@ -127,10 +135,10 @@ no state — they read like inline scopes: // Indent / Unindent — nest content under a heading. with_indent(0.0f) { // 0.0f defers to style IndentSpacing - Text("Bullet child") + text("Bullet child") } with_indent(40.0f) { // explicit pixel offset - Text("Hard-indented") + text("Hard-indented") } // PushItemWidth / PopItemWidth — scope a widget-width override. @@ -142,8 +150,12 @@ no state — they read like inline scopes: } // PushTextWrapPos / PopTextWrapPos — scope where long text wraps. - with_text_wrap_pos(0.0f) { TextUnformatted(LIPSUM) } // window right edge - with_text_wrap_pos(200.0f) { TextUnformatted(LIPSUM) } // wrap at 200 px + with_text_wrap_pos(0.0f) { // window right edge + text_unformatted(WRAP_EDGE, (text = LIPSUM)) + } + with_text_wrap_pos(200.0f) { // wrap at 200 px + text_unformatted(WRAP_200, (text = LIPSUM)) + } Feature demos: ``modules/dasImgui/examples/features/with_indent.das``, ``modules/dasImgui/examples/features/with_item_width.das``, diff --git a/doc/source/reference/tutorials/imgui/layout_primitives.rst b/doc/source/reference/tutorials/imgui/layout_primitives.rst index d41a884cb1..b27c003c22 100644 --- a/doc/source/reference/tutorials/imgui/layout_primitives.rst +++ b/doc/source/reference/tutorials/imgui/layout_primitives.rst @@ -57,15 +57,20 @@ silently dropped out of the layout would abort the recording. Requires ======== -Baseline boost layer (``imgui/imgui_boost_v2`` re-exports -``imgui/imgui_layout_builtin``). No extra modules. +Baseline boost layer. All four rails live in +``imgui/imgui_widgets_builtin`` alongside the ordinary widgets — no extra +modules. (``imgui/imgui_layout_builtin`` is a different rail: the +``split_h`` / ``split_v`` / ``dock_left`` helpers of +:ref:`tutorial_layout`.) When to reach for each ====================== ``same_line`` is the workhorse — every multi-column row, every label-then-input -pattern uses it. Pass an explicit ``offset`` if the next widget needs a -column-aligned position; default packs against the previous item. +pattern uses it. Pass an explicit ``offset_from_start_x`` if the next widget +needs a column-aligned position; the default (``0.0f``) packs against the +previous item. A second ``spacing`` argument overrides the horizontal gap +(negative = the style's ``ItemSpacing.x``). ``spacing`` is a minimal 1-line gap — cheaper to read than ``dummy`` when you just want breathing room between sections. Stack three of them if you @@ -84,8 +89,10 @@ width. Snapshot shape ============== -Each layout marker registers an entry under its ident with kind -``"empty_marker"``: +Each layout marker registers an entry under its ident, with ``kind`` set +to the rail that fired — ``"same_line"``, ``"spacing"``, ``"new_line"``, +``"dummy"`` (``EmptyMarkerState`` is the state struct behind all four, +not the reported kind): .. code-block:: bash diff --git a/doc/source/reference/tutorials/imgui/live_reload.rst b/doc/source/reference/tutorials/imgui/live_reload.rst index 4fb84c5cc5..315bcc1089 100644 --- a/doc/source/reference/tutorials/imgui/live_reload.rst +++ b/doc/source/reference/tutorials/imgui/live_reload.rst @@ -64,8 +64,9 @@ A daslang-live reload runs in this order: in the OLD program. The ``live/live_vars`` module auto-generates one of these per ``@live`` global; the user can register more. 3. Typer + codegen run against the new source. If they fail, the - reload aborts and the old program keeps running — ``live_get_error`` - surfaces the diagnostic. + reload aborts and the old program keeps running — + ``get_last_error()`` surfaces the diagnostic in daslang, and the + ``last_error`` live command surfaces it over HTTP. 4. The new program is loaded. ``[after_reload]`` hooks run, restoring the saved state (``@live`` first, then user hooks). 5. The next ``update()`` call sees ``live_begin_frame() == true`` and @@ -240,9 +241,10 @@ external events the same way it responds to mouse clicks. Full source: :download:`modules/dasImgui/examples/tutorial/live_reload.das <../../../../../modules/dasImgui/examples/tutorial/live_reload.das>` - Framework module: ``live/live_host`` (the host itself), - ``live/live_commands`` (the ``[live_command]`` annotation), and - ``live/live_vars`` (the ``@live`` serializer). + Framework modules: ``live_host`` (the host itself — note the bare + name, it is the C++ module), ``live/live_commands`` (the + ``[live_command]`` annotation), and ``live/live_vars`` (the ``@live`` + serializer). ImGui-specific lifecycle: ``imgui/imgui_live.das`` — the ``[before_reload]`` / ``[after_reload]`` pair that preserves the diff --git a/doc/source/reference/tutorials/imgui/log_capture.rst b/doc/source/reference/tutorials/imgui/log_capture.rst index 388196ba29..5b93f258be 100644 --- a/doc/source/reference/tutorials/imgui/log_capture.rst +++ b/doc/source/reference/tutorials/imgui/log_capture.rst @@ -39,9 +39,10 @@ Walkthrough Requires ======== -``with_log`` is in ``imgui/imgui_scope_builtin`` (re-exported by -``imgui/imgui_boost_v2``). ``GetClipboardText`` and ``LogRenderedText`` are in -the carve-out of raw calls the lint allows. +``with_log`` is in ``imgui/imgui_scope_builtin`` — require it explicitly, +the way the companion does; the baseline boost layer does not pull it in. +``GetClipboardText`` and ``LogRenderedText`` are in the carve-out of raw +calls the lint allows. Behaviour ========= diff --git a/doc/source/reference/tutorials/imgui/main_menu_bar.rst b/doc/source/reference/tutorials/imgui/main_menu_bar.rst index bb3b34f0b8..e9170f0612 100644 --- a/doc/source/reference/tutorials/imgui/main_menu_bar.rst +++ b/doc/source/reference/tutorials/imgui/main_menu_bar.rst @@ -52,8 +52,10 @@ Walkthrough Requires ======== -Baseline boost layer (``imgui/imgui_boost_v2`` re-exports -``imgui/imgui_containers_builtin``). No extra modules. +Baseline boost layer, no extra modules — ``main_menu_bar`` / ``menu_bar`` +/ ``menu`` come from ``imgui/imgui_containers_builtin``, and +``menu_item`` / ``menu_label`` / ``separator`` from +``imgui/imgui_widgets_builtin``. main_menu_bar vs menu_bar ========================= diff --git a/doc/source/reference/tutorials/imgui/plot.rst b/doc/source/reference/tutorials/imgui/plot.rst index b4ba9eb2d1..305ecc278a 100644 --- a/doc/source/reference/tutorials/imgui/plot.rst +++ b/doc/source/reference/tutorials/imgui/plot.rst @@ -9,6 +9,7 @@ per-frame ``array`` and copy it synchronously; two **lambda-form** widgets call back once per sample, skipping the backing array entirely. +.. das-doc: signatures .. code-block:: das plot_lines(IDENT, "title", values, scale_min, scale_max, size) @@ -38,6 +39,10 @@ Walkthrough Requires ======== +.. das-doc: given var samples : array +.. das-doc: given var avg_ms : float = 0.0f +.. das-doc: given def sample_at(idx : int) : float => 0.0f + Already in the baseline boost layer: * ``imgui/imgui_widgets_builtin`` — both array and lambda rails. diff --git a/doc/source/reference/tutorials/imgui/popup_modal.rst b/doc/source/reference/tutorials/imgui/popup_modal.rst index b7c1dc5d0c..04d40c0ce4 100644 --- a/doc/source/reference/tutorials/imgui/popup_modal.rst +++ b/doc/source/reference/tutorials/imgui/popup_modal.rst @@ -8,6 +8,7 @@ Modal popups parent window, absorbs every click outside the modal, and routes ESC to the close path. The wrapper's signature: +.. das-doc: signatures .. code-block:: das popup_modal(IDENT, (text = "title", @@ -41,6 +42,8 @@ Walkthrough Requires ======== +.. das-doc: given var CONFIRM_RESULT : string = "(no answer yet)" + Already in the baseline boost layer: * ``imgui/imgui_containers_builtin`` — ``popup_modal`` plus ``window``. diff --git a/doc/source/reference/tutorials/imgui/popup_window.rst b/doc/source/reference/tutorials/imgui/popup_window.rst index caf255b644..faed1b17f4 100644 --- a/doc/source/reference/tutorials/imgui/popup_window.rst +++ b/doc/source/reference/tutorials/imgui/popup_window.rst @@ -44,6 +44,8 @@ trigger that failed to open or close the popup would abort the recording. Requires ======== +.. das-doc: given var in_region : bool = false + Already in the baseline boost layer: * ``imgui/imgui_containers_builtin`` — ``popup_window``, ``open_popup``, @@ -109,8 +111,10 @@ exactly that loop. State ===== -``PopupWindowState`` is empty — ImGui owns the open lifecycle by -``str_id`` and the boost wrapper just brackets the body. The state +``PopupWindowState`` carries no live fields — just an unused placeholder +bool that keeps the struct non-empty for ``[container]`` auto-emit. ImGui +owns the open lifecycle by ``str_id`` and the boost wrapper just brackets +the body. The state global is what the registry walks to find the widget by IDENT; the snapshot reports ``kind="popup_window"`` for every registered instance regardless of whether the popup is currently visible. diff --git a/doc/source/reference/tutorials/imgui/popups.rst b/doc/source/reference/tutorials/imgui/popups.rst index b368a2aa4c..71b56b3af0 100644 --- a/doc/source/reference/tutorials/imgui/popups.rst +++ b/doc/source/reference/tutorials/imgui/popups.rst @@ -10,6 +10,7 @@ item**; ``popup_context_window`` attaches to the **enclosing window**. Both are ``stateless_finalize`` — ImGui owns open/close internally, the body runs only while the popup is visible. +.. das-doc: signatures .. code-block:: das popup_context_item(IDENT, (str_id = "..", @@ -49,6 +50,9 @@ right-click the button / empty space yourself: Requires ======== +.. das-doc: given var g_item_action : string = "(right-click the button)" +.. das-doc: given var g_win_action : string = "(right-click empty area)" + * ``imgui/imgui_containers_builtin`` — both popup containers. * ``imgui/imgui_widgets_builtin`` — ``button`` + ``menu_item`` for the target + menu rows. @@ -67,8 +71,12 @@ popup: button(TARGET, (text = "Right-click me")) popup_context_item(CTX, (str_id = "target_ctx", flags = ImGuiPopupFlags.MouseButtonRight)) { - if (menu_item(RENAME, (text = "Rename", shortcut = "F2"))) { ... } - if (menu_item(DELETE, (text = "Delete", shortcut = "Del"))) { ... } + if (menu_item(RENAME, (text = "Rename", shortcut = "F2"))) { + g_item_action = "Rename" + } + if (menu_item(DELETE, (text = "Delete", shortcut = "Del"))) { + g_item_action = "Delete" + } } The order matters — ``popup_context_item`` calls ``IsItemHovered`` / @@ -90,16 +98,21 @@ and only empty space opens the window menu: .. code-block:: das - window(MAIN_WIN, (text = "...")) { + window(MAIN_WIN, (text = "popups", closable = false, + flags = ImGuiWindowFlags.None)) { button(TARGET, (text = "Right-click me")) popup_context_item(ITEM_CTX, (str_id = "item_ctx", flags = ImGuiPopupFlags.MouseButtonRight)) { - if (menu_item(RENAME, (text = "Rename"))) { ... } + if (menu_item(RENAME, (text = "Rename", shortcut = "F2"))) { + g_item_action = "Rename" + } } popup_context_window(WIN_CTX, (str_id = "win_ctx", flags = ImGuiPopupFlags.MouseButtonRight | ImGuiPopupFlags.NoOpenOverItems)) { - if (menu_item(REFRESH, (text = "Refresh"))) { ... } + if (menu_item(REFRESH, (text = "Refresh", shortcut = "F5"))) { + g_win_action = "Refresh" + } } } diff --git a/doc/source/reference/tutorials/imgui/selectable_hover.rst b/doc/source/reference/tutorials/imgui/selectable_hover.rst index 66aaff7204..2f418c3cbc 100644 --- a/doc/source/reference/tutorials/imgui/selectable_hover.rst +++ b/doc/source/reference/tutorials/imgui/selectable_hover.rst @@ -11,6 +11,8 @@ that: .. code-block:: das + var private SH_ROW : table + for (i in range(length(ROWS))) { let hovered = selectable_hover(SH_ROW[i], (text = "Row {i}: {ROWS[i]}")) if (hovered) { @@ -22,6 +24,11 @@ that: hover bool. The selected flag is fixed at false — clicks render a brief selection highlight but the row never persists as selected. +One widget per loop iteration means an **indexed** state table, and the +``[widget]`` macro does not auto-emit those — declare +``SH_ROW : table`` at module scope yourself (string +keys work too: ``table``). + Source: ``modules/dasImgui/examples/tutorial/selectable_hover.das``. ************ @@ -37,8 +44,10 @@ Walkthrough Requires ======== -Same baseline as ``selectable`` — already in -``imgui/imgui_widgets_builtin`` (re-exported by ``imgui/imgui_boost_v2``). +.. das-doc: given let ROWS = fixed_array("Arrow", "TextInput", "ResizeAll", "Hand", "NotAllowed") + +Same baseline as ``selectable`` — ``imgui/imgui_widgets_builtin``, which +every widget tutorial already requires. When to reach for it ==================== diff --git a/doc/source/reference/tutorials/imgui/slider.rst b/doc/source/reference/tutorials/imgui/slider.rst index 7d483adc2f..845e912e18 100644 --- a/doc/source/reference/tutorials/imgui/slider.rst +++ b/doc/source/reference/tutorials/imgui/slider.rst @@ -9,6 +9,7 @@ click the track to jump, drag the handle to scrub. Same call shape spans scalar / vector / vertical and float / int — ten widgets, one mental model. +.. das-doc: signatures .. code-block:: das slider_float(IDENT, (text = "..", format = "%.3f", @@ -38,12 +39,20 @@ Walkthrough Requires ======== +.. das-doc: given require imgui +.. das-doc: given require imgui/imgui_boost_v2 +.. das-doc: given require imgui/imgui_widgets_builtin +.. das-doc: given require daslib/safe_addr + Already in the baseline boost layer: * ``imgui/imgui_widgets_builtin`` — every ``slider_*`` / ``vslider_*`` rail. * ``imgui/imgui_boost_runtime`` — ``SliderStateFloat`` / ``SliderStateInt`` / vector state structs. +The caller-owned form at the end of this page also needs +``daslib/safe_addr`` for ``safe_addr``. + Bounds ====== diff --git a/doc/source/reference/tutorials/imgui/state_telemetry.rst b/doc/source/reference/tutorials/imgui/state_telemetry.rst index 1c6fbf03bd..ee9b64cc38 100644 --- a/doc/source/reference/tutorials/imgui/state_telemetry.rst +++ b/doc/source/reference/tutorials/imgui/state_telemetry.rst @@ -52,7 +52,7 @@ the matching global at module scope: .. code-block:: das // emitted automatically — no manual declaration - @live variable private SAVE_BTN : ClickState = ClickState() + var private @live SAVE_BTN : ClickState = ClickState() That's why there's no ``var SAVE_BTN`` at the top of the file. The state struct is owned by daslang — visible to ``grep``, walkable via @@ -67,7 +67,9 @@ Once emitted, the global behaves like any other daslang global — .. code-block:: das - if (button(SAVE_BTN, (text = "Save"))) { ... } + if (button(SAVE_BTN, (text = "Save"))) { + // clicked this frame — same information as SAVE_BTN.clicked + } text("SAVE_BTN.click_count = {SAVE_BTN.click_count}") Two distinct value channels are available: @@ -90,8 +92,8 @@ telemetry path uses only the bare identifier (``STATE_WIN/SPEED``, never ``STATE_WIN/SPEED.PUBLIC``) — flags never leak into the path or the ImGui hash. -* ``SPEED.PUBLIC`` — emit as ``variable public`` instead of the default - ``variable private``. Sibling modules requiring this one can then +* ``SPEED.PUBLIC`` — emit as ``var public`` instead of the default + ``var private``. Sibling modules requiring this one can then read ``SPEED.value`` directly. * ``VOLUME.NOTLIVE`` — skip the ``@live`` annotation on the emitted global. Useful when you change the slider bounds and want the @@ -109,7 +111,7 @@ text_show — the app-driven mirror ``text_show`` is the read-only counterpart to ``text_input`` — ``state.value`` is what the widget renders, and the value can be -written by the app (``STATUS_TEXT.value := "..."``) **or** by an +written by the app (``STATUS_TEXT.value = "..."``) **or** by an external driver (``imgui_force_set`` with a string value). Either way the snapshot exposes the current value under the standard ``payload.value`` field, so integration tests can assert on computed @@ -119,13 +121,14 @@ status strings the same way they assert slider values: text_show(STATUS_TEXT) if (button(BUMP_STATUS, (text = "bump status"))) { - STATUS_TEXT.value := "saved at frame {get_uptime()}" + STATUS_TEXT.value = "saved at frame {get_uptime()}" } -The ``:=`` clones the new string into the current context's heap — -required because ``daslang-live``'s HTTP handler runs in a different -context than the GLFW main loop. Plain ``=`` would assign a pointer -that becomes invalid the moment the request returns. +Plain ``=`` is right here: the interpolated string is freshly built on +the app's own heap, in the same context that renders the frame. The +external path never touches this assignment — +``imgui_force_set`` hands the JSON string to the widget dispatcher, +which stores it on the state struct for the next frame to render. Standalone vs live ================== diff --git a/doc/source/reference/tutorials/imgui/tab_bar.rst b/doc/source/reference/tutorials/imgui/tab_bar.rst index ee7dd645b0..e5c919e95d 100644 --- a/doc/source/reference/tutorials/imgui/tab_bar.rst +++ b/doc/source/reference/tutorials/imgui/tab_bar.rst @@ -58,8 +58,14 @@ only runs while that tab is active: flags = ImGuiTabItemFlags.None)) { checkbox(G_WIRE, (text = "Wireframe")) // only registered while active } - tab_item(AUDIO_TAB, ...) { ... } - tab_item(INFO_TAB, ...) { ... } + tab_item(AUDIO_TAB, (text = "Audio", closable = false, + flags = ImGuiTabItemFlags.None)) { + slider_int(A_VOLUME, (text = "Volume")) + } + tab_item(INFO_TAB, (text = "Info", closable = false, + flags = ImGuiTabItemFlags.None)) { + text("Tab metadata - read-only.") + } } A snapshot taken while ``AUDIO_TAB`` is active shows ``G_WIRE`` under @@ -112,8 +118,10 @@ Sharing open state across two surfaces ====================================== For the canonical "the same flag lives in a checkbox and a tab's X" -case, use ``edit_tab_item`` against a caller-owned bool pointer: +case, use ``edit_tab_item`` against a caller-owned bool pointer +(``require daslib/safe_addr`` for the ``safe_addr`` form): +.. das-doc: given require daslib/safe_addr .. code-block:: das var private DRAFT_TAB_OPEN : bool = true @@ -121,9 +129,9 @@ case, use ``edit_tab_item`` against a caller-owned bool pointer: // Checkbox row mirrors the X-button flag. edit_checkbox(safe_addr(DRAFT_TAB_OPEN), (id = "TAB_VISIBLE", text = "Show Draft tab")) - // ... - edit_tab_item(safe_addr(DRAFT_TAB_OPEN), "Draft", - ImGuiTabItemFlags.None) { + edit_tab_item(safe_addr(DRAFT_TAB_OPEN), (id = "DRAFT_TAB_EXT", + text = "Draft", + flags = ImGuiTabItemFlags.None)) { text("Draft body — same flag the checkbox flips") } @@ -143,13 +151,18 @@ Bar-level chrome: * ``NoCloseWithMiddleMouseButton`` — disable the middle-mouse shortcut for closable tabs. * ``NoTooltip`` — suppress hover tooltips on tab headers. -* ``FittingPolicyResizeDown`` / ``FittingPolicyScroll`` — what happens - when the strip doesn't fit horizontally. +* ``DrawSelectedOverline`` — draw an overline on the selected tab. +* ``FittingPolicyShrink`` / ``FittingPolicyScroll`` — what happens + when the strip doesn't fit horizontally (``FittingPolicyShrink`` is + the default; ``FittingPolicyMixed`` combines the two). Per-tab flags via ``ImGuiTabItemFlags``: ``UnsavedDocument`` (marks the header with a dot), ``SetSelected`` (a one-shot select-this-tab nudge), -``NoCloseButton`` (closable=true but no X), ``Leading``/``Trailing`` -(pin to bar edges), ``NoReorder``. +``Leading``/``Trailing`` (pin to bar edges), ``NoReorder``, +``NoPushId``, ``NoTooltip``, ``NoCloseWithMiddleMouseButton``, +``NoAssumedClosure``. There is no public "closable but no X" flag — +ImGui's ``NoCloseButton`` is internal bookkeeping and is not bound; drop +``closable`` instead. Standalone vs live ================== diff --git a/doc/source/reference/tutorials/imgui/texture_ref.rst b/doc/source/reference/tutorials/imgui/texture_ref.rst index b4f80ba5b7..226bc67475 100644 --- a/doc/source/reference/tutorials/imgui/texture_ref.rst +++ b/doc/source/reference/tutorials/imgui/texture_ref.rst @@ -11,19 +11,25 @@ texture, and pass that texture through an ``ImTextureRef``: .. code-block:: das + var private g_tex : uint = 0u // GL texture name + let PICTURE_PATH = "{get_das_root()}/modules/dasImgui/doc/source/_static/icons/cube.png" + // 1. decode a PNG to RGBA pixels (dasStbImage) var img : Image - img->load(path, 4) + let (loaded, err) = img->load(PICTURE_PATH, 4) // 4 channels => RGBA // 2. upload to a GL texture glGenTextures(1, safe_addr(g_tex)) glBindTexture(GL_TEXTURE_2D, g_tex) glTexImage2D(GL_TEXTURE_2D, 0, int(GL_RGBA), img.width, img.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, unsafe(addr(img.bytes[0]))) // 3. wrap the GL texture name in an ImTextureRef - var ref : ImTextureRef - ref._TexID = uint64(g_tex) - // 4. draw it - image(TR_PIC, (user_texture_id = ref, size = float2(w, h), ...)) + unsafe { + var ref : ImTextureRef + ref._TexID = uint64(g_tex) + // 4. draw it + image(TR_PIC, (user_texture_id = ref, + size = float2(float(img.width), float(img.height)))) + } ``_TexID`` is the **user slot** of ``ImTextureRef`` (an ``ImTextureID`` == the GL texture name here). With ``_TexData`` left null, the backend treats it as a @@ -58,9 +64,11 @@ Behaviour The picture is decoded + uploaded **once** in ``init`` (the GL context is live after ``live_imgui_init``), and the texture name is kept in a module global. Each frame ``image()`` draws it twice through a freshly built ``ImTextureRef`` — -once plain, once tinted + bordered — to show the same texture reused while -``tint_col`` / ``border_col`` vary per call. The texture is freed with -``glDeleteTextures`` on shutdown. +once plain, once tinted — to show the same texture reused while ``tint_col`` +varies per call. (``border_col`` is echoed into ``ImageState`` for snapshot +assertions only; ImGui's ``Image()`` takes no per-call border, it comes from the +``ImGuiCol_Border`` style.) The texture is freed with ``glDeleteTextures`` on +shutdown. Migration note ============== diff --git a/doc/source/reference/tutorials/imgui/toggles.rst b/doc/source/reference/tutorials/imgui/toggles.rst index 7887ae4a47..2746ca6764 100644 --- a/doc/source/reference/tutorials/imgui/toggles.rst +++ b/doc/source/reference/tutorials/imgui/toggles.rst @@ -9,6 +9,7 @@ one mental model. Click flips state; ``imgui_force_set`` writes it from outside. The three forms differ in glyph and in whether they share state across call sites: +.. das-doc: signatures .. code-block:: das checkbox(IDENT, (text = "..")) // single bool, square glyph @@ -102,18 +103,25 @@ The dispatcher (``[widget_dispatch]`` on ``ToggleState`` and Caller-owned variants ===================== -For sites where the value already lives on an external bool / int (not -a widget state struct), use the ``edit_*`` rails — they take a ``T?`` -pointer via ``safe_addr`` and skip the state-struct allocation: +For sites where the value already lives on an external bool (not a +widget state struct), use the ``edit_*`` rails — they take a ``bool?`` +pointer via ``safe_addr`` (``require daslib/safe_addr``) and skip the +state-struct allocation: +.. das-doc: given require daslib/safe_addr .. code-block:: das var g_enabled : bool = false edit_checkbox(safe_addr(g_enabled), (id = "EN", text = "Enabled")) - var g_mode : int = 0 - edit_radio_button_int(safe_addr(g_mode), (id = "MODE", - text = "Off", v_button = 0)) + var g_subscribed : bool = false + edit_radio_button(safe_addr(g_subscribed), (id = "SUB", text = "Sub")) + +Only the **bool** forms have caller-owned rails: ``edit_checkbox`` and +``edit_radio_button``. There is no ``edit_radio_button_int`` — the +grouped one-of-N form needs the shared ``RadioIntState`` that carries +the group's selected value, so declare a ``RadioIntState`` ident and use +``radio_button_int``. See :ref:`tutorial_edit_external_tour`. diff --git a/doc/source/reference/tutorials/imgui/tree_image_misc.rst b/doc/source/reference/tutorials/imgui/tree_image_misc.rst index 63856f7926..b961a938cf 100644 --- a/doc/source/reference/tutorials/imgui/tree_image_misc.rst +++ b/doc/source/reference/tutorials/imgui/tree_image_misc.rst @@ -8,6 +8,7 @@ Two leaf widgets the container-shaped variants don't cover. ``tree_node_ex`` is the explicit-control sibling of the ``tree_node`` container; ``image`` is the display-only sibling of ``image_button``. +.. das-doc: signatures .. code-block:: das tree_node_ex(IDENT, (text = "..", flags = ...)) // returns open bool @@ -96,9 +97,8 @@ icon strip), use ``image``. If it's an interactive surface, use .. code-block:: das let io & = unsafe(GetIO()) - let font_tex = io.Fonts.TexID - if (font_tex != null) { - image(AVATAR, (user_texture_id = font_tex, + if (io.Fonts.TexData != null) { + image(AVATAR, (user_texture_id = io.Fonts.TexRef, size = float2(128.0f, 128.0f), uv0 = float2(0.0f, 0.0f), uv1 = float2(1.0f, 1.0f), @@ -107,13 +107,16 @@ icon strip), use ``image``. If it's an interactive surface, use } ``uv0`` / ``uv1`` slice into the texture (use for sprite sheets); -``tint_col`` modulates the rendered face; ``border_col`` draws a -1-pixel frame around it (transparent for no border). - -The font atlas (``io.Fonts.TexID``) is always available and works as a -no-setup demo texture. Real apps load via ``stbi`` or whatever your -GL/Vulkan texture pipeline provides; pass the opaque ``ImTextureID`` -(typed ``void?`` on the daslang side) as ``texture``. +``tint_col`` modulates the rendered face. ``border_col`` is echoed into +``ImageState`` for snapshot assertions only — ImGui's ``Image()`` takes +no per-call border, it comes from the ``ImGuiCol_Border`` style. + +The font atlas is always available and works as a no-setup demo +texture: ``io.Fonts.TexRef`` is its ImGui 1.92 ``ImTextureRef``, and the +``io.Fonts.TexData != null`` guard skips the frames before the backend +has built the atlas. Real apps decode + upload their own texture and put +the handle in ``ref._TexID`` — see :ref:`tutorial_texture_ref` for the +full path. Driving from outside ==================== diff --git a/doc/source/reference/tutorials/imgui/tree_node.rst b/doc/source/reference/tutorials/imgui/tree_node.rst index a473b19669..8704f63370 100644 --- a/doc/source/reference/tutorials/imgui/tree_node.rst +++ b/doc/source/reference/tutorials/imgui/tree_node.rst @@ -8,6 +8,7 @@ Tree node ``TreeNodeEx``: branch headers that fold-and-expand with an auto-paired ``TreePop``. The wrapper's signature: +.. das-doc: signatures .. code-block:: das tree_node(IDENT, (text = "...", @@ -118,9 +119,10 @@ The third arg's bitfield. Most useful: * ``OpenOnArrow`` — only the chevron click toggles; clicking the label itself doesn't expand. Pair with a Selectable-row pattern. * ``OpenOnDoubleClick`` — single click selects, double click toggles. -* ``Leaf`` — render the row without a chevron. The body is required to - not exist (or be empty); use ``tree_node_ex`` (the [widget] leaf form) - for nodes that should be selectable rows without children. See +* ``Leaf`` — render the row without a chevron; ImGui reports it as + always-open, so a container body would run every frame. A leaf has no + children, so leave the body empty — or use ``tree_node_ex`` (the + [widget] leaf form) for nodes that should be selectable rows. See ``modules/dasImgui/examples/features/tree_node_ex.das``. * ``Bullet`` — bullet point instead of chevron. Pair with ``Leaf``. * ``Framed`` — solid background under the header strip. diff --git a/doc/source/reference/tutorials/imgui/visual_aids_tour.rst b/doc/source/reference/tutorials/imgui/visual_aids_tour.rst index f2039a78ff..b3bc795418 100644 --- a/doc/source/reference/tutorials/imgui/visual_aids_tour.rst +++ b/doc/source/reference/tutorials/imgui/visual_aids_tour.rst @@ -100,9 +100,9 @@ enabled: .. code-block:: das - mouse_trail(true) // on - mouse_trail(true, 0.45f) // 450ms fade - mouse_trail(false) // off + mouse_trail(true) // on, module defaults + mouse_trail(true, 0.45f) // faster fade (alpha factor, default 0.96f) + mouse_trail(false) // off — also clears the trail buffer The trail's value is mostly to make synth-cursor recordings parseable — without it, the cursor teleports between waypoints and the viewer diff --git a/doc/source/reference/tutorials/imgui/widgets_tour.rst b/doc/source/reference/tutorials/imgui/widgets_tour.rst index 54a7885674..62b9da57fe 100644 --- a/doc/source/reference/tutorials/imgui/widgets_tour.rst +++ b/doc/source/reference/tutorials/imgui/widgets_tour.rst @@ -45,9 +45,11 @@ Init and shutdown ================= ``init()`` opens a 1024x720 GLFW window via ``live_create_window`` and hands -the handle to ``live_imgui_init``. It also bumps ``io.FontGlobalScale`` to -``1.5`` so the recorded APNG reads at typical Sphinx HTML widths without -zooming. ``shutdown()`` mirrors the pair in reverse order. +the handle to ``live_imgui_init``. It also bumps +``GetStyle().FontScaleMain`` to ``1.5`` so the recorded APNG reads at typical +Sphinx HTML widths without zooming (ImGui 1.92 moved ``io.FontGlobalScale`` +onto the style as ``FontScaleMain``). ``shutdown()`` mirrors the pair in +reverse order. The frame loop ============== @@ -72,11 +74,14 @@ register at ``AUDIO_WIN/``: window(AUDIO_WIN, (text = "Audio settings", closable = false, flags = ImGuiWindowFlags.None)) { input_text(USER_NAME, (text = "Your name")) + VOLUME.bounds = (0.0f, 1.0f) slider_float(VOLUME, (text = "Master volume")) checkbox(MUTED, (text = "Mute")) combo(QUALITY, (text = "Quality", items <- ["Low", "Medium", "High", "Ultra"])) color_edit3(TINT, (text = "Accent color")) - if (button(SAVE_BTN, (text = "Save settings"))) { ... } + if (button(SAVE_BTN, (text = "Save settings"))) { + print("save clicked: vol={VOLUME.value} muted={MUTED.value}\n") + } } Each boost macro declares the named global the first time it expands and diff --git a/doc/source/reference/tutorials/imgui/window_size_constraints.rst b/doc/source/reference/tutorials/imgui/window_size_constraints.rst index 28e59c1859..1dae10eafd 100644 --- a/doc/source/reference/tutorials/imgui/window_size_constraints.rst +++ b/doc/source/reference/tutorials/imgui/window_size_constraints.rst @@ -12,17 +12,30 @@ invokes on every resize so the callback can re-shape the requested size The boost module ``imgui/imgui_window_constraints_builtin`` ships a daslang wrapper around the 3-arg form: an ``ImGuiSizeConstraints`` struct that wraps -a daslang lambda + the existing C++ trampoline (``das_invoke_lambda`` -at ``src/dasIMGUI.main.cpp:361``). End-user code passes the wrapper directly: +a daslang lambda + the existing C++ trampoline +(``SetNextWindowSizeConstraintsCallback`` → +``das_invoke_lambda`` in ``src/dasIMGUI.main.cpp``). End-user code +passes the wrapper: .. code-block:: das - let aspect = @ capture(= ratio) (var data : ImGuiSizeCallbackData) : void { + // Module scope: ImGui keeps the pointer for the NEXT Begin(), so the + // wrapper must outlive the SetNextWindowSizeConstraints call. + var private ASPECT_CN : ImGuiSizeConstraints + + let aspect_ratio = 16.0f / 9.0f + ASPECT_CN <- ImGuiSizeConstraints(@ capture(= aspect_ratio) + (var data : ImGuiSizeCallbackData) : void { data.DesiredSize = float2(data.DesiredSize.x, - data.DesiredSize.x / ratio) - } - SetNextWindowSizeConstraints(float2(0,0), float2(FLT_MAX, FLT_MAX), - ImGuiSizeConstraints(aspect)) + data.DesiredSize.x / aspect_ratio) + }) + SetNextWindowSizeConstraints(float2(0.0f, 0.0f), float2(FLT_MAX, FLT_MAX), + ASPECT_CN) + +Seed the wrapper once (``init``) and pass the same struct on every frame — +the lambda carries its own capture state. A stack local will not do: auto-fit +invokes the callback from inside ``Begin()``, after the enclosing function has +already returned. The 2-arg ``SetNextWindowSizeConstraints(min, max)`` form stays on the boost-surface allow-list — only require this module when you need the @@ -93,8 +106,8 @@ callback shapes: } Capture rules follow the standard daslang lambda surface — ``capture(= var)`` -for by-value, ``capture(& var)`` for by-reference. The ``@`` (no-capture) -form works for callbacks that read only ``data``. +for by-value, ``capture(& var)`` for by-reference. A plain ``@(...)`` with no +``capture`` clause is enough for callbacks that read only ``data``. Struct layout ============= diff --git a/doc/source/reference/tutorials/imgui/with_disabled.rst b/doc/source/reference/tutorials/imgui/with_disabled.rst index f855c1435d..df540a7fa2 100644 --- a/doc/source/reference/tutorials/imgui/with_disabled.rst +++ b/doc/source/reference/tutorials/imgui/with_disabled.rst @@ -12,14 +12,18 @@ no idents — they exist to subsume the raw ``Push*`` / ``Pop*`` pairs that v2's lint flags as invisible state changes. * ``with_disabled(disabled, blk)`` — ``BeginDisabled`` / ``EndDisabled``. -* ``with_font(font, blk)`` — ``PushFont`` / ``PopFont``. -* ``with_button_repeat(repeat, blk)`` — ``PushButtonRepeat`` / - ``PopButtonRepeat``. +* ``with_font(font, size, blk)`` — ``PushFont(font, size)`` / ``PopFont``; + ``size`` defaults to ``0.0f`` (keep the current size), so + ``with_font(f) { ... }`` is a pure font swap. +* ``with_button_repeat(repeat, blk)`` — + ``PushItemFlag(ImGuiItemFlags.ButtonRepeat, repeat)`` / ``PopItemFlag`` + (ImGui obsoleted the ``PushButtonRepeat`` shorthand in 1.91). * ``with_clip_rect(min, max, isect, blk)`` — ``PushClipRect`` / ``PopClipRect``. .. code-block:: das + checkbox(ENABLED_MASTER, (text = "Enable child group")) with_disabled(!ENABLED_MASTER.value) { button(CHILD_SAVE, (text = "Save")) button(CHILD_LOAD, (text = "Load")) @@ -113,7 +117,7 @@ with_clip_rect The fourth scope wrapper isn't exercised in this tutorial — see ``modules/dasImgui/examples/features/clip_rect.das`` for a per-frame clipping demo. -``with_clip_rect((min, max, isect), blk)`` is the safest way to install a +``with_clip_rect(min, max, isect, blk)`` is the safest way to install a custom clip rectangle around custom-rendered content (drawlist primitives, images, manual layout) since the scope guarantees the prior clip rect is restored on exit. @@ -125,6 +129,7 @@ The wrappers compose cleanly — nest them to layer scopes: .. code-block:: das + checkbox(FEATURE_ENABLED, (text = "Feature on")) with_disabled(!FEATURE_ENABLED.value) { with_button_repeat(true) { button(STEP_UP, (text = "+")) diff --git a/doc/source/reference/tutorials/imgui/with_style.rst b/doc/source/reference/tutorials/imgui/with_style.rst index 25bfd0e731..113fd6f4b8 100644 --- a/doc/source/reference/tutorials/imgui/with_style.rst +++ b/doc/source/reference/tutorials/imgui/with_style.rst @@ -70,7 +70,7 @@ scope is restored: .. code-block:: das with_style((ImGuiCol.Text, ImVec4(1.0f, 0.95f, 0.30f, 1.0f))) { - Text("Yellow text inherited from outer scope.") + text("Yellow text inherited from outer scope.") with_style((ImGuiCol.Button, ImVec4(0.20f, 0.45f, 0.85f, 1.0f)), (ImGuiStyleVar.FrameRounding, 4.0f)) { // Yellow text + blue button + 4-radius rounding all active. diff --git a/doc/source/reference/tutorials/imgui/with_tab_stop.rst b/doc/source/reference/tutorials/imgui/with_tab_stop.rst index 8b36085761..dad0f213d6 100644 --- a/doc/source/reference/tutorials/imgui/with_tab_stop.rst +++ b/doc/source/reference/tutorials/imgui/with_tab_stop.rst @@ -4,9 +4,11 @@ With tab stop ####################### -ImGui's ``PushTabStop`` / ``PopTabStop`` pair lets you control which widgets -participate in TAB / Shift+TAB focus cycling. The boost layer wraps it as a -stateless scope wrapper: +ImGui's ``NoTabStop`` item flag controls which widgets participate in TAB / +Shift+TAB focus cycling. The boost layer wraps the +``PushItemFlag(ImGuiItemFlags.NoTabStop, !tab_stop)`` / ``PopItemFlag`` pair +as a stateless scope wrapper (ImGui obsoleted the ``PushTabStop`` / +``PopTabStop`` shorthand in 1.91): .. code-block:: das @@ -34,8 +36,10 @@ Walkthrough Requires ======== -Same baseline as the other ``with_*`` wrappers — already in -``imgui/imgui_scope_builtin`` (re-exported by ``imgui/imgui_boost_v2``). +Same baseline as the other ``with_*`` wrappers, plus an explicit +``require imgui/imgui_scope_builtin`` — ``imgui/imgui_boost_v2`` re-exports +only ``imgui``, ``imgui/imgui_lint`` and ``imgui/imgui_boost_runtime``, so the +scope wrappers need their own ``require``. Behaviour ========= diff --git a/doc/source/reference/tutorials/integration_c_03_binding_types.rst b/doc/source/reference/tutorials/integration_c_03_binding_types.rst index efcf0807b0..f1f7fb53d6 100644 --- a/doc/source/reference/tutorials/integration_c_03_binding_types.rst +++ b/doc/source/reference/tutorials/integration_c_03_binding_types.rst @@ -47,7 +47,9 @@ non-zero means the enum's underlying storage is ``int64``; zero (the common case) means ``int``. The example above passes ``1``, so ``Color`` is int64-backed — pass ``0`` for the usual ``int`` storage. -In daslang: +In daslang, once the script has ``require tutorial_c_03``: + +.. das-doc: fragment .. code-block:: das @@ -71,7 +73,9 @@ and each field's offset and mangled type: das_structure_add_field(st, mod, lib, "y", "y", offsetof(Point2D, y), "f"); das_module_bind_structure(mod, st); -In daslang: +In daslang, once the script has ``require tutorial_c_03``: + +.. das-doc: fragment .. code-block:: das diff --git a/doc/source/reference/tutorials/integration_c_04_callbacks.rst b/doc/source/reference/tutorials/integration_c_04_callbacks.rst index eec4b9d5ea..c9f797b4b9 100644 --- a/doc/source/reference/tutorials/integration_c_04_callbacks.rst +++ b/doc/source/reference/tutorials/integration_c_04_callbacks.rst @@ -53,7 +53,10 @@ Calling a function pointer return ret; } -From daslang, pass a function pointer with ``@@``: +From daslang, once the script has ``require tutorial_c_04``, pass a function +pointer with ``@@``: + +.. das-doc: fragment .. code-block:: das @@ -94,6 +97,8 @@ must be the lambda itself** (its capture block): From daslang: +.. das-doc: fragment + .. code-block:: das var counter = 0 @@ -126,7 +131,9 @@ include the block itself**: return ret; } -From daslang: +From daslang (``counter`` is the same captured local as above): + +.. das-doc: fragment .. code-block:: das diff --git a/doc/source/reference/tutorials/integration_c_06_sandbox.rst b/doc/source/reference/tutorials/integration_c_06_sandbox.rst index f0a3218e30..f6db18dfa5 100644 --- a/doc/source/reference/tutorials/integration_c_06_sandbox.rst +++ b/doc/source/reference/tutorials/integration_c_06_sandbox.rst @@ -154,7 +154,13 @@ module_get — path resolution When a ``.das_project`` is active, the built-in ``daslib/`` resolution logic is **completely bypassed** — ``module_get`` is the sole path resolver. It must handle ``daslib/X`` paths as well as relative module -paths: +paths. The project file declares the ``module_info`` typedef itself and +requires ``strings`` plus ``daslib/strings_boost`` for ``split_by_chars`` +and ``join``: + +.. das-doc: given require strings +.. das-doc: given require daslib/strings_boost +.. das-doc: given typedef module_info = tuple const .. code-block:: das diff --git a/doc/source/reference/tutorials/integration_c_07_context_variables.rst b/doc/source/reference/tutorials/integration_c_07_context_variables.rst index b68c860285..5eb24dfd07 100644 --- a/doc/source/reference/tutorials/integration_c_07_context_variables.rst +++ b/doc/source/reference/tutorials/integration_c_07_context_variables.rst @@ -31,6 +31,8 @@ The daslang script The companion script declares four scalar globals: +.. das-doc: fragment + .. code-block:: das options gen2 diff --git a/doc/source/reference/tutorials/integration_c_11_type_introspection.rst b/doc/source/reference/tutorials/integration_c_11_type_introspection.rst index df41eac109..401a7a6c54 100644 --- a/doc/source/reference/tutorials/integration_c_11_type_introspection.rst +++ b/doc/source/reference/tutorials/integration_c_11_type_introspection.rst @@ -39,6 +39,8 @@ The daslang script The companion script defines a struct hierarchy, an enum, and exported functions for the C host to inspect: +.. das-doc: fragment + .. code-block:: das options gen2 diff --git a/doc/source/reference/tutorials/integration_c_12_ecs.rst b/doc/source/reference/tutorials/integration_c_12_ecs.rst index 6e92df0e09..cbc11fd632 100644 --- a/doc/source/reference/tutorials/integration_c_12_ecs.rst +++ b/doc/source/reference/tutorials/integration_c_12_ecs.rst @@ -32,7 +32,11 @@ The user script The user defines a **component struct** whose field names match the C-side array names, declares **host-provided globals** with ``@required``, and -writes ``[es]`` functions with **no arguments** -- the macro injects them: +writes ``[es]`` functions with **no arguments** -- the macro injects them. +``tutorial_c_12`` is the C-side module registered by the host, so this script +only compiles inside that host: + +.. das-doc: fragment .. code-block:: das @@ -90,6 +94,11 @@ The ``ecs_macro`` module provides the ``[es]`` function annotation: on module globals and generates ``ecs_register_global`` calls with ``addr()`` of the global variable. +The abridged macro body -- ``...`` marks omitted code; the full version is in +``ecs_macro.das``, and ``ecs_register`` comes from the C-side module: + +.. das-doc: fragment + .. code-block:: das [function_macro(name="es")] diff --git a/doc/source/reference/tutorials/integration_c_13_shared_module.rst b/doc/source/reference/tutorials/integration_c_13_shared_module.rst index 32d29a686a..ea39154e63 100644 --- a/doc/source/reference/tutorials/integration_c_13_shared_module.rst +++ b/doc/source/reference/tutorials/integration_c_13_shared_module.rst @@ -59,7 +59,10 @@ compiler promotes it to the **global module registry**. Subsequent compilations find it there -- no file needed. The promotion is a compile-time mechanism. A minimal "loader" script -triggers it: +triggers it (``my_helpers`` resolves through the host's file access, not +from disk): + +.. das-doc: fragment .. code-block:: das @@ -117,7 +120,7 @@ Part 1 — demonstrating the problem // 1b: Fresh FileAccess — module file not introduced das_fileaccess_introduce_file(fa2, "user_script.das", USER_SCRIPT, 0); das_program * program2 = das_program_compile("user_script.das", fa2, tout, libgrp); - // error[30901]: missing prerequisite 'my_helpers'; file not found + // error[20605]: missing prerequisite 'my_helpers'; file not found Part 2 — the shared module solution diff --git a/doc/source/reference/tutorials/integration_cpp_03_binding_functions.rst b/doc/source/reference/tutorials/integration_cpp_03_binding_functions.rst index 4ae71ab5ba..f5c12c92da 100644 --- a/doc/source/reference/tutorials/integration_cpp_03_binding_functions.rst +++ b/doc/source/reference/tutorials/integration_cpp_03_binding_functions.rst @@ -56,7 +56,9 @@ Binding constants addConstant(*this, "PI", 3.14159265358979323846f); addConstant(*this, "SQRT2", sqrtf(2.0f)); -In the script: +In the script (after ``require tutorial_03_cpp``): + +.. das-doc: fragment .. code-block:: das @@ -145,6 +147,8 @@ If a function takes a reference and modifies it, use In the script: +.. das-doc: fragment + .. code-block:: das var val = 21 @@ -162,7 +166,7 @@ script caller does **not** see these parameters: .. code-block:: cpp void print_stack_info(Context * ctx) { - printf("Stack size: %d bytes\n", ctx->stack.size()); + printf("Context stack size: %d bytes\n", ctx->stack.size()); } addExtern(*this, lib, "print_stack_info", @@ -170,6 +174,8 @@ script caller does **not** see these parameters: In the script the function takes zero arguments: +.. das-doc: fragment + .. code-block:: das print_stack_info() // prints "Context stack size: 16384 bytes" @@ -193,6 +199,8 @@ The host program must request the module before ``Module::Initialize()``: The script uses ``require`` to access the module: +.. das-doc: fragment + .. code-block:: das require tutorial_03_cpp diff --git a/doc/source/reference/tutorials/integration_cpp_04_binding_types.rst b/doc/source/reference/tutorials/integration_cpp_04_binding_types.rst index 92ff8d70b4..8920ec67e1 100644 --- a/doc/source/reference/tutorials/integration_cpp_04_binding_types.rst +++ b/doc/source/reference/tutorials/integration_cpp_04_binding_types.rst @@ -180,6 +180,11 @@ types. Creating a mutable local variable of such a type requires an Using bound types in daslang ============================== +``Vec2``, ``vec2_length``, ``make_vec2`` and the rest come from the C++ +module, so this script only compiles inside the tutorial host: + +.. das-doc: fragment + .. code-block:: das require tutorial_04_cpp @@ -203,6 +208,7 @@ Using bound types in daslang // Nested types and field access let r = make_rect(10.0, 20.0, 100.0, 50.0) print("rect area = {rect_area(r)}\n") + } Immutable locals created via ``let`` from factory functions work without ``unsafe``. Use ``unsafe { var ... }`` only when the variable must be diff --git a/doc/source/reference/tutorials/integration_cpp_05_binding_enums.rst b/doc/source/reference/tutorials/integration_cpp_05_binding_enums.rst index ba953812be..41b0cb298e 100644 --- a/doc/source/reference/tutorials/integration_cpp_05_binding_enums.rst +++ b/doc/source/reference/tutorials/integration_cpp_05_binding_enums.rst @@ -129,8 +129,10 @@ the enum lives in a deeply nested namespace), you can construct an pEnum->addIEx("Error", "Severity::Error", 3, LineInfo()); addEnumeration(pEnum); -You still need ``DAS_BASE_BIND_ENUM`` (or at least ``DAS_BIND_ENUM_CAST``) -for the ``typeFactory<>`` so that ``addExtern`` can match the type. +You still need ``DAS_BASE_BIND_ENUM`` (or the factory-only +``DAS_BASE_BIND_ENUM_GEN``) for the ``typeFactory<>`` so that ``addExtern`` +can match the type. ``DAS_BIND_ENUM_CAST`` is not a substitute — it only +defines the ``cast<>`` specialization, not the factory. Binding functions that use enums @@ -159,7 +161,11 @@ and return values automatically — no special treatment is required: Using bound enums in daslang ============================== -Enum values are accessed with dot syntax — ``EnumName.Value``: +Enum values are accessed with dot syntax — ``EnumName.Value``. ``Direction``, +``Severity`` and the helper functions come from the C++ module, so this script +only compiles inside the tutorial host: + +.. das-doc: fragment .. code-block:: das @@ -181,6 +187,7 @@ Enum values are accessed with dot syntax — ``EnumName.Value``: // Boolean result from enum logic print("Warning is severe? {is_severe(Severity.Warning)}\n") + } Name collision warning diff --git a/doc/source/reference/tutorials/integration_cpp_06_interop.rst b/doc/source/reference/tutorials/integration_cpp_06_interop.rst index e4bfc3fea2..e43f9a163a 100644 --- a/doc/source/reference/tutorials/integration_cpp_06_interop.rst +++ b/doc/source/reference/tutorials/integration_cpp_06_interop.rst @@ -154,7 +154,12 @@ Using interop functions from daslang ======================================= The interop functions look like regular functions in daslang — the -"any type" argument accepts any value: +"any type" argument accepts any value. ``describe_type``, +``struct_field_names``, ``call_site_info`` and ``make_particle`` are the +interop functions registered above, so this script only compiles inside the +tutorial host: + +.. das-doc: fragment .. code-block:: das @@ -182,6 +187,7 @@ The interop functions look like regular functions in daslang — the // Call-site info reports this script's file and line print("called from: {call_site_info()}\n") + } Building and running diff --git a/doc/source/reference/tutorials/integration_cpp_07_callbacks.rst b/doc/source/reference/tutorials/integration_cpp_07_callbacks.rst index e1af06d0d3..82ab6e3eff 100644 --- a/doc/source/reference/tutorials/integration_cpp_07_callbacks.rst +++ b/doc/source/reference/tutorials/integration_cpp_07_callbacks.rst @@ -139,8 +139,13 @@ And the accumulator/reduce pattern: Calling from daslang ======================= -Blocks use the ``<|`` pipe syntax with ``$()`` lambda prefix. -Function pointers use ``@@function_name``: +A block is written as a trailing argument after the call — +``func(args) $(a, b) { ... }``. Function pointers use +``@@function_name``. ``with_values``, ``call_function_twice``, +``for_each_fibonacci`` and ``reduce_range`` are the C++ functions bound +above, so this script only compiles inside the tutorial host: + +.. das-doc: fragment .. code-block:: das @@ -177,6 +182,7 @@ Function pointers use ``@@function_name``: return acc * i } print("10! = {fact}\n") + } Building and running diff --git a/doc/source/reference/tutorials/integration_cpp_08_methods.rst b/doc/source/reference/tutorials/integration_cpp_08_methods.rst index 4613f89f44..07a59846c5 100644 --- a/doc/source/reference/tutorials/integration_cpp_08_methods.rst +++ b/doc/source/reference/tutorials/integration_cpp_08_methods.rst @@ -109,7 +109,11 @@ Calling methods from daslang =============================== The bound methods are called as free functions. Because handled types -need mutable access under ``unsafe``, the pattern is: +need mutable access under ``unsafe``, the pattern is (this snippet +compiles only inside this tutorial's host — ``tutorial_08_cpp`` is the +C++ module registered by ``08_methods.cpp``): + +.. das-doc: fragment .. code-block:: das @@ -131,8 +135,10 @@ need mutable access under ``unsafe``, the pattern is: var c = make_counter(100, 10) c |> decrement() c |> decrement() - print("100 - 20 = {c |> get()}\n") // 80 + c |> decrement() + print("100 - 3*10 = {c |> get()}\n") // 70 } + } Building and running diff --git a/doc/source/reference/tutorials/integration_cpp_09_operators_and_properties.rst b/doc/source/reference/tutorials/integration_cpp_09_operators_and_properties.rst index d365765af1..f5bc143b7b 100644 --- a/doc/source/reference/tutorials/integration_cpp_09_operators_and_properties.rst +++ b/doc/source/reference/tutorials/integration_cpp_09_operators_and_properties.rst @@ -56,8 +56,12 @@ function with the operator symbol as its daslang name: SideEffects::none, "vec3_neg") ->args({"a"}); -Available operator names: ``+``, ``-``, ``*``, ``/``, ``%``, ``<<``, -``>>``, ``<``, ``>``, ``<=``, ``>=``, ``&``, ``|``, ``^``. +Operators bind purely by name — there is no whitelist. Registration +only checks that the name is punctuation-only (``isValidBuiltinName``), +so every operator the parser can produce is bindable: ``+ - * / % << +>> < > <= >= & | ^ ~ ! && ||``, the compound-assign family (``+= -= *= +/= %= <<= >>= &= |= ^=``), ``++`` / ``--``, and ``[]``. The special +names ``clone`` and ``finalize`` bind the same way. .. note:: @@ -115,7 +119,12 @@ getter methods on the C++ type, then register them in the annotation: } }; -In daslang, properties are accessed with dot syntax just like fields: +In daslang, properties are accessed with dot syntax just like fields. +Every daslang snippet below needs this tutorial's C++ host — ``Vec3``, +``Color``, ``SafeColor`` and ``make_vec3`` live in the +``tutorial_09_cpp`` module registered by ``09_operators_and_properties.cpp``: + +.. das-doc: fragment .. code-block:: das @@ -154,6 +163,8 @@ Register with explicit function-pointer types for both overloads: In daslang, the property value depends on the variable's mutability: +.. das-doc: fragment + .. code-block:: das let immutable_v = make_vec3(1.0, 2.0, 3.0) @@ -199,6 +210,8 @@ Consider a ``Color`` type with a non-trivial constructor: Without annotation overrides, local Color variables need workarounds: +.. das-doc: fragment + .. code-block:: das // ERROR: let c = Color() → error[30108] without unsafe! @@ -269,6 +282,8 @@ We define ``SafeColor`` as a separate struct with the same layout: Now scripts can use SafeColor without ``unsafe``: +.. das-doc: fragment + .. code-block:: das let sc = SafeColor() // works — annotation says it's safe @@ -282,6 +297,8 @@ Using operators in daslang All operators work naturally: +.. das-doc: fragment + .. code-block:: das options gen2 @@ -298,6 +315,7 @@ All operators work naturally: print("a == a: {a == a}\n") // true print("a != b: {a != b}\n") // true print("a.length = {a.length}\n") // property access + } Building and running diff --git a/doc/source/reference/tutorials/integration_cpp_10_custom_modules.rst b/doc/source/reference/tutorials/integration_cpp_10_custom_modules.rst index 79272df0bf..9de0d99161 100644 --- a/doc/source/reference/tutorials/integration_cpp_10_custom_modules.rst +++ b/doc/source/reference/tutorials/integration_cpp_10_custom_modules.rst @@ -240,7 +240,11 @@ name string, returning ``nullptr`` if not found. Using both modules from daslang ================================== -Scripts ``require`` each module independently: +Scripts ``require`` each module independently. Both ``math_types`` and +``math_utils`` are C++ modules registered by ``10_custom_modules.cpp``, +so this snippet compiles only inside this tutorial's host: + +.. das-doc: fragment .. code-block:: das @@ -261,6 +265,7 @@ Scripts ``require`` each module independently: let mid = lerp(a, make_vec2(1.0, 0.0), 0.5) print("mid = ({mid.x}, {mid.y})\n") + } Building and running diff --git a/doc/source/reference/tutorials/integration_cpp_11_context_variables.rst b/doc/source/reference/tutorials/integration_cpp_11_context_variables.rst index 0df91bafb1..3cd3333630 100644 --- a/doc/source/reference/tutorials/integration_cpp_11_context_variables.rst +++ b/doc/source/reference/tutorials/integration_cpp_11_context_variables.rst @@ -122,6 +122,11 @@ immediately after simulation. The daslang side =================== +``GameConfig`` is a handled type registered by ``11_context_variables.cpp``, +so this script needs that tutorial's C++ host (module ``tutorial_11_cpp``): + +.. das-doc: fragment + .. code-block:: das options gen2 @@ -134,8 +139,10 @@ The daslang side [export] def print_globals() { - print("score = {score}\n") // C++ changes are visible here + print("score = {score}\n") + print("config.gravity = {config.gravity}\n") + } Building and running diff --git a/doc/source/reference/tutorials/integration_cpp_12_smart_pointers.rst b/doc/source/reference/tutorials/integration_cpp_12_smart_pointers.rst index cdf50fd45a..6a827c1db4 100644 --- a/doc/source/reference/tutorials/integration_cpp_12_smart_pointers.rst +++ b/doc/source/reference/tutorials/integration_cpp_12_smart_pointers.rst @@ -43,10 +43,10 @@ and ``use_count()``: int32_t health; Entity() : name("unnamed"), x(0), y(0), health(100) { - printf(" Entity constructed\n"); + printf(" [C++] Entity('%s') constructed\n", name.c_str()); } ~Entity() { - printf(" Entity destroyed\n"); + printf(" [C++] Entity('%s') destroyed\n", name.c_str()); } // ... methods ... }; @@ -116,7 +116,12 @@ Using smart pointers in daslang ================================== Smart pointer variables must be declared with ``var inscope``, which -ensures ``delRef()`` is called when the variable goes out of scope: +ensures ``delRef()`` is called when the variable goes out of scope. +``Entity`` and ``make_entity`` come from ``tutorial_12_cpp``, the C++ +module registered by ``12_smart_pointers.cpp`` — this script needs that +host: + +.. das-doc: fragment .. code-block:: das @@ -143,6 +148,7 @@ ensures ``delRef()`` is called when the variable goes out of scope: fresh.health = 50 } // fresh destroyed here (delRef → ref_count==0 → delete) + } Key points: diff --git a/doc/source/reference/tutorials/integration_cpp_15_custom_annotations.rst b/doc/source/reference/tutorials/integration_cpp_15_custom_annotations.rst index 60eaad9741..2865dca243 100644 --- a/doc/source/reference/tutorials/integration_cpp_15_custom_annotations.rst +++ b/doc/source/reference/tutorials/integration_cpp_15_custom_annotations.rst @@ -176,6 +176,12 @@ Register annotations in the module constructor: Using from daslang ==================== +``[log_calls]`` and ``[add_field]`` are registered by +``15_custom_annotations.cpp``, so this script compiles only inside this +tutorial's C++ host (module ``tutorial_15_cpp``): + +.. das-doc: fragment + .. code-block:: das options gen2 diff --git a/doc/source/reference/tutorials/integration_cpp_16_sandbox.rst b/doc/source/reference/tutorials/integration_cpp_16_sandbox.rst index bc395c0064..3f4dd5fee3 100644 --- a/doc/source/reference/tutorials/integration_cpp_16_sandbox.rst +++ b/doc/source/reference/tutorials/integration_cpp_16_sandbox.rst @@ -88,7 +88,9 @@ Key policy flags: | ``threadlock_context`` | Adds context mutex for thread safety | +-----------------------------------+------------------------------------------+ -When a policy is violated, compilation fails with error ``40207``. +Each policy has its own diagnostic — ``no_unsafe`` reports +``error[31012]: unsafe function `` with the note "unsafe functions +are prohibited by CodeOfPolicies". Custom FileAccess — module restrictions @@ -184,10 +186,13 @@ Project file callbacks typedef module_info = tuple const var DAS_PAK_ROOT = "./" - // REQUIRED — resolve every `require X` to a file path + // REQUIRED — resolve every `require X` to a file path. + // Returns (moduleName, fileName, importName). [export] def module_get(req, from : string) : module_info { - // return (moduleName, fileName, importName) + let rs <- split_by_chars(req, "./") + let mod_name = rs[length(rs) - 1] + return (mod_name, "{get_das_root()}/daslib/{mod_name}.das", "") } // Whitelist which modules can be loaded @@ -233,6 +238,12 @@ Available callbacks: | ``include_get`` | Resolve ``include`` directives (optional) | +-------------------------------+-----------------------------------------------+ +Only ``module_get`` is mandatory; every other callback is optional and +falls back to "allow" when the project file does not export it. The +loader looks up a few more by name: ``with_module_unsafe``, +``can_module_be_required``, ``is_same_file_name``, ``option_blocked``, +``is_pod_in_scope_allowed`` and ``dyn_modules_folder``. + ``DAS_PAK_ROOT`` is set by the runtime to the directory containing the ``.das_project`` file, useful for resolving relative paths. @@ -297,7 +308,7 @@ Expected output:: --- Unsafe script with no_unsafe --- Compilation FAILED (expected in sandbox demo): - error[40207]: unsafe function test + error[31012]: unsafe function test ... === Demo 3: Memory limits === @@ -327,7 +338,7 @@ Expected output:: === Demo 6: .das_project blocks violations === --- Unsafe script under .das_project --- Compilation FAILED (expected in sandbox demo): - error[40207]: unsafe function test + error[31012]: unsafe function test ... --- Blocked module under .das_project --- diff --git a/doc/source/reference/tutorials/integration_cpp_19_class_adapters.rst b/doc/source/reference/tutorials/integration_cpp_19_class_adapters.rst index 3f8e0ebde0..28b2d727ec 100644 --- a/doc/source/reference/tutorials/integration_cpp_19_class_adapters.rst +++ b/doc/source/reference/tutorials/integration_cpp_19_class_adapters.rst @@ -157,7 +157,11 @@ The daslang side ================= Scripts derive from the exposed abstract class and use ``def override`` -to provide implementations: +to provide implementations. ``TutorialBaseClass``, ``add_object`` and +``tick`` all come from the ``tutorial_19`` module that +``19_class_adapters.cpp`` registers, so this script needs that C++ host: + +.. das-doc: fragment .. code-block:: das diff --git a/doc/source/reference/tutorials/integration_cpp_23_handle_registry.rst b/doc/source/reference/tutorials/integration_cpp_23_handle_registry.rst index d97e21cd42..322594ee37 100644 --- a/doc/source/reference/tutorials/integration_cpp_23_handle_registry.rst +++ b/doc/source/reference/tutorials/integration_cpp_23_handle_registry.rst @@ -116,10 +116,15 @@ One call registers: +--------------------------+--------------------------------------------------+ | ``destroy_actor`` | only if ``destroyFnName`` (the argument after | | | the type name) is non-empty — calls | -| | ``HandleRegistry::release(h)`` | +| | ``HandleRegistry::instance().release(h)`` | +--------------------------+--------------------------------------------------+ | leak-dump hook | wired automatically via | -| | ``handleRegistry_registerDump`` | +| | ``handleRegistry_registerDump(&dumpHandleLeaks | +| | )`` | ++--------------------------+--------------------------------------------------+ +| live-count hook | wired automatically via | +| | ``handleRegistry_registerCount(&countHandleLeaks | +| | )`` | +--------------------------+--------------------------------------------------+ @@ -173,10 +178,16 @@ Explicit destroy and ``is_alive`` ================================= The daslang name passed as ``destroyFnName`` becomes a script-callable -destructor that unregisters the handle: +destructor that unregisters the handle. ``make_actor``, ``destroy_actor`` +and ``is_alive`` come from the ``tutorial_23_cpp`` module registered by +``23_handle_registry.cpp``, so this snippet needs that C++ host: + +.. das-doc: fragment .. code-block:: das + require tutorial_23_cpp + var goblin = make_actor("Goblin", 10.0, 5.0) destroy_actor(goblin) assert(!is_alive(goblin)) diff --git a/doc/source/reference/tutorials/jsonrpc_02_dispatch_line.rst b/doc/source/reference/tutorials/jsonrpc_02_dispatch_line.rst index 7a6e2c5ddf..5f87ce5599 100644 --- a/doc/source/reference/tutorials/jsonrpc_02_dispatch_line.rst +++ b/doc/source/reference/tutorials/jsonrpc_02_dispatch_line.rst @@ -18,9 +18,10 @@ The dispatcher block ==================== ``dispatch_line(line, strict, dispatcher_block)`` parses a wire line and -invokes the dispatcher block for each non-notification request. The -block receives ``(method, params_json)`` and returns the raw JSON result -string; the library wraps the result in a JSON-RPC envelope. +invokes the dispatcher block once per well-formed request — notifications +included. The block receives ``(method, params_json)`` and returns the raw +JSON result string; the library wraps that result in a JSON-RPC envelope +for every request that carries an ``id``. .. code-block:: das @@ -73,10 +74,14 @@ When you need to emit specific error codes per method .. code-block:: das - let req = parse_request(line) - if (!empty(req.error_envelope)) return req.is_notification ? "" : req.error_envelope - if (req.method == "unknown") return error(req.id_str, METHOD_NOT_FOUND, "no such method") - return response(req.id_str, dispatch(req.method, req.params_json)) + def handle(line : string) : string { + let req = parse_request(line) + if (!empty(req.error_envelope)) return req.is_notification ? "" : req.error_envelope + if (req.method != "ping" && req.method != "echo") { + return error(req.id_str, METHOD_NOT_FOUND, "no such method: {req.method}") + } + return response(req.id_str, dispatch(req.method, req.params_json)) + } Running the tutorial ==================== diff --git a/doc/source/reference/tutorials/jsonrpc_03_batch.rst b/doc/source/reference/tutorials/jsonrpc_03_batch.rst index affca5ac9b..a7ef9b6ee4 100644 --- a/doc/source/reference/tutorials/jsonrpc_03_batch.rst +++ b/doc/source/reference/tutorials/jsonrpc_03_batch.rst @@ -85,11 +85,17 @@ directly: .. code-block:: das - let pb = parse_batch(wire) - if (!empty(pb.framing_error)) return pb.framing_error - for (req in pb.requests) { - if (!empty(req.error_envelope)) { /* per-entry error */ } - else { /* req.method, req.id_str, req.params, req.params_json available */ } + def handle_batch(wire : string) : string { + let pb = parse_batch(wire) + return pb.framing_error if (!empty(pb.framing_error)) + for (req in pb.requests) { + if (!empty(req.error_envelope)) { + // per-entry error — req.error_envelope is ready to go on the wire + } else { + // req.method, req.id_str, req.params, req.params_json available + } + } + return "" // assemble the response array from the per-entry results } Running the tutorial diff --git a/doc/source/reference/tutorials/macros/01_call_macro.rst b/doc/source/reference/tutorials/macros/01_call_macro.rst index a06b680367..1a0829be22 100644 --- a/doc/source/reference/tutorials/macros/01_call_macro.rst +++ b/doc/source/reference/tutorials/macros/01_call_macro.rst @@ -43,11 +43,24 @@ Key imports used by the module:: Section 1 — hello(): Minimal call macro ======================================== -A call macro is a class that extends ``AstCallMacro``, annotated with +A macro module needs a ``module`` declaration — without it the compiler +rejects the file with *"module Module_Name is required"*. A call macro is +a class that extends ``AstCallMacro``, annotated with ``[call_macro(name="...")]``: +.. das-doc: file call_macro_mod.das .. code-block:: das + options gen2 + + module call_macro_mod public + + require daslib/ast + require daslib/ast_boost + require daslib/templates_boost + require daslib/strings_boost + require daslib/macro_boost + [call_macro(name="hello")] class HelloMacro : AstCallMacro { def override visit(prog : ProgramPtr; mod : Module?; @@ -68,9 +81,13 @@ It returns an ``ExpressionPtr`` — the AST tree that replaces the call. ``qmacro(...)`` is a *reification* helper: you write normal daslang syntax inside it and it builds the corresponding AST at compile time. -Usage:: +Usage, from the file that requires the module: + +.. code-block:: das + + require call_macro_mod - hello() // → print("hello, call macro!\n") + hello() // → print("hello, call macro!\n") Section 2 — greet("name"): Argument validation @@ -79,6 +96,7 @@ Section 2 — greet("name"): Argument validation The ``greet`` macro validates its single argument and builds a string interpolation expression: +.. das-doc: file call_macro_mod.das .. code-block:: das [call_macro(name="greet")] @@ -125,6 +143,7 @@ Section 3 — printf(fmt, args...): Format-string parsing The ``printf`` macro parses a format string at compile time, replacing ``(N)`` placeholders with the corresponding argument expressions: +.. das-doc: given let score = 42 .. code-block:: das printf("player (1) scored (2) points\n", "Alice", score) @@ -144,6 +163,7 @@ looking for ``(`` ... ``)`` pairs. For each placeholder it: 2. Validates bounds with ``macro_verify`` 3. Inserts a ``clone_expression`` of the referenced argument +.. das-doc: file call_macro_mod.das .. code-block:: das [call_macro(name="printf")] diff --git a/doc/source/reference/tutorials/macros/02_when_macro.rst b/doc/source/reference/tutorials/macros/02_when_macro.rst index 6f32add7f9..534c0b1759 100644 --- a/doc/source/reference/tutorials/macros/02_when_macro.rst +++ b/doc/source/reference/tutorials/macros/02_when_macro.rst @@ -22,6 +22,21 @@ covered in :ref:`tutorial_macro_call_macro`. The macro transforms this into: +.. das-doc: given require daslib/ast_boost +.. das-doc: given require daslib/templates_boost +.. das-doc: given let x = 2 +.. das-doc: given let y = 42 +.. das-doc: given let a = 1 +.. das-doc: given let b = 2 +.. das-doc: given var cond : ExpressionPtr +.. das-doc: given var cond_type : TypeDeclPtr +.. das-doc: given var call_block : ExpressionPtr +.. das-doc: given var list : array +.. das-doc: given var blk : ExprBlock? +.. das-doc: given var tupl : ExprMakeTuple? +.. das-doc: given var is_default = false +.. das-doc: given var any_default = false +.. das-doc: given let arg_name = "__when_arg__" .. code-block:: das let result = invoke($(arg : int const) { @@ -77,6 +92,7 @@ fully error-checked until after the macro transforms them. ``canVisitArgument`` lets you decide per-argument: +.. das-doc: member AstCallMacro .. code-block:: das def override canVisitArgument(expr : ExprCallMacro?; @@ -100,6 +116,7 @@ Deferring return-type inference the enclosing function can be finalized while this macro call is still unexpanded: +.. das-doc: member AstCallMacro .. code-block:: das def override canFoldReturnResult( @@ -118,17 +135,18 @@ Building the statement list The core of the macro iterates over the block's statements. Each statement is an ``ExprMakeTuple`` (the ``=>`` operator creates tuples): +.. das-doc: fragment .. code-block:: das for (stmt, idx in blk.list, count()) { let tupl = stmt as ExprMakeTuple assume cond_value = tupl.values[0] - let is_default = (cond_value is ExprVar) - && (cond_value as ExprVar).name == "_" + let is_default = (cond_value is ExprVar) && (cond_value as ExprVar).name == "_" For each case, we build either a conditional return or an unconditional return using ``qmacro_block``: +.. das-doc: member AstCallMacro .. code-block:: das if (is_default) { @@ -155,6 +173,7 @@ Assembling the block After building the statement list, we need a typed block argument. ``clone_type`` copies the condition's inferred type, and we adjust flags: +.. das-doc: member AstCallMacro .. code-block:: das var cond_type = clone_type(cond._type) @@ -163,12 +182,11 @@ After building the statement list, we need a typed block argument. Then we assemble the block and mark its argument as shadowable: +.. das-doc: member AstCallMacro .. code-block:: das - var call_block = qmacro( - $($i(arg_name) : $t(cond_type)){ $b(list); }) - ((call_block as ExprMakeBlock)._block as ExprBlock) - .arguments[0].flags.can_shadow = true + var call_block = qmacro($($i(arg_name) : $t(cond_type)){ $b(list); }) + ((call_block as ExprMakeBlock)._block as ExprBlock).arguments[0].flags.can_shadow = true * **``$t(type)``** — injects a ``TypeDecl?`` into the reified AST * **``$b(list)``** — injects an ``array`` as the block body @@ -177,6 +195,7 @@ Then we assemble the block and mark its argument as shadowable: The final result is an ``invoke`` call: +.. das-doc: member AstCallMacro .. code-block:: das return qmacro(invoke($e(call_block), $e(cond))) @@ -190,6 +209,7 @@ return on some code paths — the compiler would reject it. The macro solves this by automatically generating a default that returns the type's default value (``""`` for strings, ``0`` for ints, etc.): +.. das-doc: member AstCallMacro .. code-block:: das if (!any_default) { diff --git a/doc/source/reference/tutorials/macros/03_function_macro.rst b/doc/source/reference/tutorials/macros/03_function_macro.rst index 3e4ebf6016..f8422cf77b 100644 --- a/doc/source/reference/tutorials/macros/03_function_macro.rst +++ b/doc/source/reference/tutorials/macros/03_function_macro.rst @@ -76,6 +76,13 @@ New concepts introduced: * **``expr.func._module.name``** — identifying a function’s source module +.. das-doc: given require function_macro_mod +.. das-doc: given require daslib/ast_boost +.. das-doc: given require daslib/templates_boost +.. das-doc: given var func : FunctionPtr +.. das-doc: given var call_sb : ExprStringBuilder? +.. das-doc: given var new_body : ExpressionPtr + Prerequisites ============= @@ -115,10 +122,24 @@ Part 1: [log_calls] — apply() ============================== The ``apply()`` method receives the function being compiled and can -modify its AST arbitrarily: +modify its AST arbitrarily. Macros live in their own module, so the file +opens with a ``module`` declaration — without one the compiler rejects it +with *"module Module_Name is required"*: +.. das-doc: file function_macro_mod.das .. code-block:: das + options gen2 + + module function_macro_mod public + + require daslib/ast + require daslib/ast_boost + require daslib/templates_boost + require daslib/strings_boost + require daslib/macro_boost + require strings public + [function_macro(name="log_calls")] class LogCallsMacro : AstFunctionAnnotation { def override apply(var func : FunctionPtr; @@ -142,6 +163,7 @@ string like ``"add(2, 3)\n"`` at runtime. This must be built as an ``ExprStringBuilder`` — a compile-time AST node that generates string interpolation code: +.. das-doc: member AstFunctionAnnotation .. code-block:: das var call_sb = new ExprStringBuilder(at = func.at) @@ -198,6 +220,7 @@ The heart of the macro builds a new function body using ``qmacro_block``. This generates a statement list (``ExprBlock``) rather than a single expression: +.. das-doc: member AstFunctionAnnotation .. code-block:: das var new_body = qmacro_block() { @@ -240,6 +263,7 @@ Replacing the function body Finally, we swap the function's body with our new block: +.. das-doc: member AstFunctionAnnotation .. code-block:: das func.body = new_body @@ -256,6 +280,7 @@ Public variables for shared state ``LOG_DEPTH`` is declared at module scope with ``var public``: +.. das-doc: file function_macro_mod.das .. code-block:: das var public LOG_DEPTH = 0 @@ -274,6 +299,7 @@ While ``apply()`` transforms the function at definition time, ``verifyCall()`` runs at every **call site** after type inference. It receives the call expression and can accept or reject it. +.. das-doc: file function_macro_mod.das .. code-block:: das [function_macro(name="expect_range")] @@ -306,6 +332,7 @@ Annotation arguments like ``[expect_range(value, min=0, max=255)]`` are stored in an ``AnnotationArgumentList``. Each entry has a ``name`` and typed value fields: +.. das-doc: fragment .. code-block:: das var arg_name = "" @@ -339,6 +366,7 @@ Extracting constant integer values To check whether a call-site argument is a compile-time constant and extract its value, we need a helper that navigates the AST: +.. das-doc: file function_macro_mod.das .. code-block:: das [macro_function] @@ -380,6 +408,7 @@ Reporting compile-time errors The error reporting pattern is straightforward — set the ``errors`` string and return ``false``: +.. das-doc: fragment .. code-block:: das var val = 0 @@ -429,6 +458,7 @@ types are resolved, overloads are selected, and the AST is ready to simulate. This makes it ideal for structural validation that needs complete type information. +.. das-doc: file function_macro_mod.das .. code-block:: das [function_macro(name="no_print")] @@ -463,6 +493,7 @@ To inspect the function body, ``lint()`` uses the **visitor pattern**. We define a class that inherits from ``AstVisitor`` and overrides ``preVisitExprCall`` to intercept function calls: +.. das-doc: file function_macro_mod.das .. code-block:: das [macro] @@ -521,6 +552,7 @@ Running the visitor from lint() The ``lint()`` method creates the visitor, adapts it with ``make_visitor``, and walks the function body with ``visit()``: +.. das-doc: fragment .. code-block:: das def override lint(...) : bool { @@ -622,6 +654,7 @@ Valid calls compile normally:: Out-of-range constants are rejected at compile time: +.. das-doc: fragment .. code-block:: das // set_channel("red", 300) // compile error! @@ -666,6 +699,7 @@ A natural extension is to add execution timing using same — the only addition is a local timing variable in the generated body: +.. das-doc: member AstFunctionAnnotation .. code-block:: das var new_body = qmacro_block() { @@ -720,7 +754,6 @@ Expected output:: green = 0 blue = 255 alpha = 200 - compute = 13 diff --git a/doc/source/reference/tutorials/macros/04_advanced_function_macro.rst b/doc/source/reference/tutorials/macros/04_advanced_function_macro.rst index 81b39148b8..0b1c3d2cb5 100644 --- a/doc/source/reference/tutorials/macros/04_advanced_function_macro.rst +++ b/doc/source/reference/tutorials/macros/04_advanced_function_macro.rst @@ -58,11 +58,15 @@ uses three of them in sequence: What patch() generates ======================= -For a function ``fib(n : int) : int``, ``patch()`` produces three things: +For a function ``fib(n : int) : int``, ``patch()`` produces three things. +The generated names start with a backtick, which no hand-written source can +spell — an identifier may *contain* backticks but must start with a letter or +underscore, so only a macro can mint these names: **A private copy of the original function** (without ``[memoize]``) so the wrapper can call it without triggering ``transform()`` again: +.. das-doc: skip .. code-block:: das def private `memoize`original`fib(n : int) : int { @@ -72,6 +76,17 @@ wrapper can call it without triggering ``transform()`` again: **A private global cache variable:** +.. das-doc: given require advanced_function_macro_mod +.. das-doc: given require daslib/ast_boost +.. das-doc: given require daslib/templates_boost +.. das-doc: given var fn : FunctionPtr +.. das-doc: given var astChanged = false +.. das-doc: given let cacheName = "cache" +.. das-doc: given let originalCopyName = "original" +.. das-doc: given var callArgs : array +.. das-doc: given var keyExpr : ExpressionPtr + +.. das-doc: skip .. code-block:: das var private `memoize`cache`fib : table @@ -79,6 +94,7 @@ wrapper can call it without triggering ``transform()`` again: **A private wrapper function** that checks the cache, calls the original on miss, and stores the result via ``insert_clone``: +.. das-doc: skip .. code-block:: das def private `memoize`fib(n : int) : int { @@ -102,8 +118,18 @@ Module file: ``advanced_function_macro_mod.das`` apply() — pre-inference validation ------------------------------------ +.. das-doc: file advanced_function_macro_mod.das .. code-block:: das + options gen2 + + module advanced_function_macro_mod public + + require daslib/ast + require daslib/ast_boost + require daslib/templates_boost + require daslib/strings_boost + [function_macro(name="memoize")] class MemoizeMacro : AstFunctionAnnotation { @@ -124,6 +150,9 @@ apply() — pre-inference validation return true } + // patch() and transform() follow, in the same class + } + ``apply()`` rejects functions at parse time — before the compiler has resolved types. The checks use pre-inference properties that are already available: ``isGeneric``, ``result.isVoid``, and ``length(arguments)``. @@ -135,6 +164,7 @@ patch() — code generation after inference The "already processed" guard ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. das-doc: fragment .. code-block:: das def override patch(var fn : FunctionPtr; var group : ModuleGroup; @@ -151,6 +181,7 @@ generate duplicate functions and hit an infinite loop. Mark as processed and trigger restart ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. das-doc: member AstFunctionAnnotation .. code-block:: das // Mark as processed and trigger inference restart @@ -168,6 +199,7 @@ so ``transform()`` can read it. Step 1 — clone the original function ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. das-doc: fragment .. code-block:: das var originalCopy <- clone_function(fn) @@ -194,6 +226,7 @@ unannotated copy. Step 2 — create the cache variable ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. das-doc: member AstFunctionAnnotation .. code-block:: das var retType = clone_type(fn.result) @@ -215,6 +248,7 @@ value types. ``clone_type(cacheType)`` is required because Step 4 — hash key computation ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. das-doc: member AstFunctionAnnotation .. code-block:: das var hashExprs : array @@ -239,6 +273,7 @@ lexical scope for the intermediate variable inside the loop. Step 6 — assemble the wrapper body ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. das-doc: member AstFunctionAnnotation .. code-block:: das var bodyExprs : array @@ -259,6 +294,7 @@ Each ``qmacro_expr`` generates one statement. The splicing operators: Step 7–8 — create and add the wrapper function ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. das-doc: fragment .. code-block:: das var wrapperFn <- qmacro_function(wrapperName) $($a(wrapperArgs)) : $t(wrapperRetType) { @@ -287,6 +323,7 @@ in the annotation arguments — ``transform()`` reads it on the next pass. transform() — call-site redirection -------------------------------------- +.. das-doc: member AstFunctionAnnotation .. code-block:: das def override transform(var call : ExprCallFunc?; @@ -372,6 +409,7 @@ Compile-time error examples The ``apply()`` method rejects invalid uses at compile time: +.. das-doc: fragment .. code-block:: das // ERROR: cannot memoize a void function — there is nothing to cache diff --git a/doc/source/reference/tutorials/macros/05_tag_function_macro.rst b/doc/source/reference/tutorials/macros/05_tag_function_macro.rst index bcf3acaf65..5c1db8f0b1 100644 --- a/doc/source/reference/tutorials/macros/05_tag_function_macro.rst +++ b/doc/source/reference/tutorials/macros/05_tag_function_macro.rst @@ -23,6 +23,12 @@ This tutorial builds a ``once()`` macro that executes a block only on the first call. Each call site gets its own auto-generated global boolean flag: +.. das-doc: given require tag_function_macro_mod +.. das-doc: given require daslib/ast_boost +.. das-doc: given require daslib/templates_boost +.. das-doc: given var call : ExprCallFunc? +.. das-doc: given let flag_name = "once_flag" +.. das-doc: given var stmts : array .. code-block:: das for (i in range(5)) { @@ -100,8 +106,17 @@ The module has two parts: the tagged function and the macro class. Part 1 — The tagged function ----------------------------- +.. das-doc: file tag_function_macro_mod.das .. code-block:: das + options gen2 + + module tag_function_macro_mod public + + require daslib/ast + require daslib/ast_boost + require daslib/templates_boost + [tag_function(once_tag)] def public once(blk : block) { invoke(blk) @@ -121,6 +136,7 @@ original body is never executed. Part 2 — The macro class -------------------------- +.. das-doc: file tag_function_macro_mod.das .. code-block:: das [tag_function_macro(tag="once_tag")] @@ -128,6 +144,7 @@ Part 2 — The macro class def override transform(var call : ExprCallFunc?; var errors : das_string) : ExpressionPtr { // ... rewrite every call to once() + return default } } @@ -150,6 +167,7 @@ It proceeds in four steps. Step 1 — Generate a unique flag name ------------------------------------- +.. das-doc: member AstFunctionAnnotation .. code-block:: das let flag_name = make_unique_private_name("__once_flag", call.at) @@ -163,6 +181,7 @@ same function are completely independent. Step 2 — Create the global flag --------------------------------- +.. das-doc: fragment .. code-block:: das if (!compiling_module() |> add_global_private_var(flag_name, call.at) <| quote(false)) { @@ -181,6 +200,7 @@ the function returns ``false`` and we report an error. Step 3 — Extract the block body --------------------------------- +.. das-doc: member AstFunctionAnnotation .. code-block:: das var block_clone = clone_expression(call.arguments[0]) @@ -204,6 +224,7 @@ expects ``array``, not an ``ExprBlock`` directly. Step 4 — Build the replacement -------------------------------- +.. das-doc: member AstFunctionAnnotation .. code-block:: das var replacement = qmacro_block() { @@ -232,8 +253,10 @@ The final expansion of: print("hello\n") } -is: +is (names starting with ``__`` are reserved in hand-written source — only a +macro can introduce one): +.. das-doc: skip .. code-block:: das if (!__once_flag_12_5) { diff --git a/doc/source/reference/tutorials/macros/06_structure_macro.rst b/doc/source/reference/tutorials/macros/06_structure_macro.rst index c3faaa2fed..0ce4623532 100644 --- a/doc/source/reference/tutorials/macros/06_structure_macro.rst +++ b/doc/source/reference/tutorials/macros/06_structure_macro.rst @@ -36,6 +36,13 @@ the compilation pipeline: | | Read-only — useful for diagnostics. | +---------------------+----------------------------------------------+ +.. das-doc: given require structure_macro_mod +.. das-doc: given require daslib/ast_boost +.. das-doc: given require daslib/templates_boost +.. das-doc: given var st : StructurePtr +.. das-doc: given var version = 1 +.. das-doc: given var astChanged = false + This tutorial builds a ``[serializable]`` annotation that: 1. Adds a ``_version`` field and generates a **stub** @@ -90,11 +97,21 @@ The module: ``structure_macro_mod.das`` Registration ------------ +.. das-doc: file structure_macro_mod.das .. code-block:: das + options gen2 + + module structure_macro_mod public + + require daslib/ast + require daslib/rtti + require daslib/ast_boost + require daslib/templates_boost + [structure_macro(name="serializable")] class SerializableMacro : AstStructureAnnotation { - ... + // apply(), patch() and finish() follow, in the same class } ``[structure_macro(name="serializable")]`` tells the compiler: @@ -116,6 +133,7 @@ an error string. It runs during parsing, before inference. Step 1 — Validate arguments ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. das-doc: fragment .. code-block:: das var version = 1 @@ -142,6 +160,7 @@ the error message stored in ``errors``. Step 2 — Add a field ^^^^^^^^^^^^^^^^^^^^^ +.. das-doc: member AstStructureAnnotation .. code-block:: das st |> add_structure_field("_version", @@ -159,6 +178,7 @@ type and default value. Step 3 — Generate a stub describe function ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. das-doc: member AstStructureAnnotation .. code-block:: das let funcName = "describe_{st.name}" @@ -217,6 +237,7 @@ non-serializable fields. Step 1 — Guard against re-patching ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. das-doc: fragment .. code-block:: das if (find_arg(args, "patched") is tBool) return true @@ -230,6 +251,7 @@ and check for it here — if present, the work is already done. Step 2 — Find the stub function ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. das-doc: member AstStructureAnnotation .. code-block:: das let funcName = "describe_{st.name}" @@ -244,6 +266,7 @@ this pointer affect the actual function. Step 3 — Get the body as ExprBlock ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. das-doc: fragment .. code-block:: das unsafe { @@ -258,6 +281,7 @@ pointer so we can access the ``list`` array of statements. Step 4 — Append field-printing statements ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. das-doc: fragment .. code-block:: das for (fld in st.fields) { @@ -284,6 +308,7 @@ the function where ``obj`` is a parameter. Step 5 — Mark as patched and trigger re-inference ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. das-doc: member AstStructureAnnotation .. code-block:: das for (ann in st.annotations) { @@ -303,6 +328,7 @@ the modified function body. On the next pass, ``find_arg(args, Inside ``finish()`` ------------------- +.. das-doc: fragment .. code-block:: das def override finish(var st : StructurePtr; var group : ModuleGroup; @@ -337,6 +363,7 @@ checked with ``is tInt`` / ``as tInt``. The usage file ============== +.. das-doc: fragment .. code-block:: das options gen2 diff --git a/doc/source/reference/tutorials/macros/07_block_macro.rst b/doc/source/reference/tutorials/macros/07_block_macro.rst index 94cf838d87..30c08c47f5 100644 --- a/doc/source/reference/tutorials/macros/07_block_macro.rst +++ b/doc/source/reference/tutorials/macros/07_block_macro.rst @@ -33,6 +33,11 @@ pipeline: | | resolved, useful for diagnostics. | +---------------------+----------------------------------------------+ +.. das-doc: given require daslib/ast_boost +.. das-doc: given require daslib/templates_boost +.. das-doc: given var blk : ExprBlock? +.. das-doc: given let lbl = "setup" + This tutorial builds a ``[traced(tag="X")]`` annotation that: 1. Prepends an enter-message and appends an exit-message (via @@ -59,6 +64,7 @@ Block annotation syntax Block annotations are placed between the ``$`` sigil and the parameter list (or body, for parameterless blocks): +.. das-doc: fragment .. code-block:: das // Parameterless block @@ -70,6 +76,7 @@ list (or body, for parameterless blocks): Multiple annotations can be comma-separated inside the brackets, just like function annotations: +.. das-doc: fragment .. code-block:: das $ [traced(tag="x"), REQUIRE(hp)] (v : int) { ... } @@ -102,11 +109,21 @@ The module: ``block_macro_mod.das`` Registration ------------ +.. das-doc: file block_macro_mod.das .. code-block:: das + options gen2 + + module block_macro_mod public + + require daslib/ast + require daslib/rtti + require daslib/ast_boost + require daslib/templates_boost + [block_macro(name="traced")] class TracedBlockMacro : AstBlockAnnotation { - ... + // apply() and finish() follow, in the same class } ``[block_macro(name="traced")]`` tells the compiler: @@ -130,6 +147,7 @@ an error string. It runs during parsing, before inference. Step 1 — Validate arguments ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. das-doc: fragment .. code-block:: das let labelArg = find_arg(args, "tag") @@ -148,6 +166,7 @@ we check ``is tString`` and cast with ``as tString``. Returning Step 2 — Prepend enter-print ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. das-doc: member AstBlockAnnotation .. code-block:: das var enterExpr = qmacro(print($v(">> {lbl}\n"))) @@ -164,6 +183,7 @@ value baked in) as a constant expression in the generated code. Step 3 — Append exit-print to ``finalList`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. das-doc: member AstBlockAnnotation .. code-block:: das var exitExpr = qmacro(print($v("<< {lbl}\n"))) @@ -184,6 +204,7 @@ the exit message prints even if the block has an early return. Inside ``finish()`` ------------------- +.. das-doc: member AstBlockAnnotation .. code-block:: das def override finish(var blk : ExprBlock?; var group : ModuleGroup; diff --git a/doc/source/reference/tutorials/macros/08_variant_macro.rst b/doc/source/reference/tutorials/macros/08_variant_macro.rst index 9c797d2da5..6099d75f66 100644 --- a/doc/source/reference/tutorials/macros/08_variant_macro.rst +++ b/doc/source/reference/tutorials/macros/08_variant_macro.rst @@ -47,6 +47,14 @@ compiler tries generic ``operator is``/``operator as`` overloads, and finally falls through to built-in ``variant`` type dispatching. +.. das-doc: given require daslib/ast_boost +.. das-doc: given require daslib/templates_boost +.. das-doc: given var expr : ExprSafeAsVariant? +.. das-doc: given let func_name = "getter" +.. das-doc: given var vtype : TypeDeclPtr +.. das-doc: given let iname = "IDrawable" +.. das-doc: given var st : Structure? + ``AstVariantMacro`` methods =========================== @@ -92,6 +100,7 @@ Type guard pattern Every visitor method starts with a *type guard* — a series of checks that decide whether this macro should handle the expression: +.. das-doc: fragment .. code-block:: das def override visitExprIsVariant(prog : ProgramPtr; mod : Module?; @@ -122,6 +131,7 @@ Once the guard passes, ``visitExprIsVariant`` looks for a ``get`IFoo`` field on the source struct. If found, the struct implements the interface → return ``true``. Otherwise → ``false``: +.. das-doc: member AstVariantMacro .. code-block:: das let getter_field = "get`{iname}" @@ -142,6 +152,7 @@ The result is a **compile-time constant** — no runtime cost at all. ``visitExprAsVariant`` generates a call to the getter function: +.. das-doc: member AstVariantMacro .. code-block:: das let func_name = "{st.name}`get`{iname}" @@ -161,6 +172,7 @@ returns an ``IDrawable?`` proxy. ``visitExprSafeAsVariant`` adds a null check before calling the getter: +.. das-doc: member AstVariantMacro .. code-block:: das return <- qmacro($e(expr.value) != null ? $c(func_name)(*$e(expr.value)) : null) @@ -226,12 +238,17 @@ handles all three operators automatically: // is — compile-time check print("w is IDrawable = {w is IDrawable}\n") // true - print("l is IResizable = {l is IResizable}\n") // false + print("w is IResizable = {w is IResizable}\n") // true + print("l is IDrawable = {l is IDrawable}\n") // true + print("l is IResizable = {l is IResizable}\n") // false // as — get interface proxy var drawable = w as IDrawable drawable->draw(10, 20) + var resizable = w as IResizable + resizable->resize(800, 600) + // ?as — null-safe access var maybe_draw = l ?as IDrawable if (maybe_draw != null) { 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 3b5b52220a..508f6e5e9f 100644 --- a/doc/source/reference/tutorials/macros/09_for_loop_macro.rst +++ b/doc/source/reference/tutorials/macros/09_for_loop_macro.rst @@ -32,6 +32,7 @@ daslang tables (``table``) are not directly iterable. The standard idiom to iterate a table requires the verbose ``keys()`` and ``values()`` built-in functions: +.. das-doc: given var tab : table .. code-block:: das for (k, v in keys(tab), values(tab)) { diff --git a/doc/source/reference/tutorials/macros/10_capture_macro.rst b/doc/source/reference/tutorials/macros/10_capture_macro.rst index 7f5bfb1e6f..5a30be974c 100644 --- a/doc/source/reference/tutorials/macros/10_capture_macro.rst +++ b/doc/source/reference/tutorials/macros/10_capture_macro.rst @@ -86,11 +86,22 @@ The module file ``audit_on_finalize`` 3. ``CaptureAuditMacro`` — the capture macro class (three hooks) +.. das-doc: given require daslib/ast_boost + The tag annotation ~~~~~~~~~~~~~~~~~~ +.. das-doc: file capture_macro_mod.das .. code-block:: das + options gen2 + options no_aot + + module capture_macro_mod + + require daslib/ast + require daslib/ast_boost + [structure_macro(name=audited)] class AuditedAnnotation : AstStructureAnnotation { def override apply(var st : StructurePtr; var group : ModuleGroup; @@ -129,6 +140,7 @@ captureExpression When an ``[audited]`` variable is captured, the macro wraps the capture expression in a call to ``audit_on_capture(value, "name")``: +.. das-doc: member AstCaptureMacro .. code-block:: das def override captureExpression(prog : Program?; mod : Module?; @@ -155,6 +167,7 @@ captureFunction For each ``[audited]`` field in the lambda struct, the macro appends a print call to the function body's ``finalList``: +.. das-doc: member AstCaptureMacro .. code-block:: das def override captureFunction(prog : Program?; mod : Module?; @@ -183,6 +196,7 @@ 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``: +.. das-doc: member AstCaptureMacro .. code-block:: das def override releaseFunction(prog : Program?; mod : Module?; diff --git a/doc/source/reference/tutorials/macros/11_reader_macro.rst b/doc/source/reference/tutorials/macros/11_reader_macro.rst index 54a6b350c5..a9f02694cd 100644 --- a/doc/source/reference/tutorials/macros/11_reader_macro.rst +++ b/doc/source/reference/tutorials/macros/11_reader_macro.rst @@ -80,7 +80,12 @@ This tutorial builds both patterns: The module file =============== -``reader_macro_mod.das`` defines two reader macros. +``reader_macro_mod.das`` defines three reader macros — the two below, +plus the inline ``%sum!`` variant covered at the end of this page. + +.. das-doc: given require daslib/ast_boost +.. das-doc: given require daslib/strings_boost +.. das-doc: given require strings The ``accept()`` idiom ~~~~~~~~~~~~~~~~~~~~~~ @@ -88,6 +93,7 @@ The ``accept()`` idiom Both macros share the same standard ``accept()`` implementation — the most common pattern in the standard library: +.. das-doc: member AstReaderMacro .. code-block:: das def override accept(prog : ProgramPtr; mod : Module?; @@ -116,6 +122,7 @@ CsvReader — visit pattern value, and uses ``convert_to_expression()`` from ``daslib/ast_boost`` to embed the resulting string array in the AST: +.. das-doc: member AstReaderMacro .. code-block:: das def override visit(prog : ProgramPtr; mod : Module?; @@ -142,6 +149,7 @@ BasicReader — suffix pattern overrides ``suffix()`` instead of ``visit()``. The method parses a tiny BASIC dialect and returns the equivalent daslang source code: +.. das-doc: member AstReaderMacro .. code-block:: das def override suffix(prog : ProgramPtr; mod : Module?; @@ -202,6 +210,7 @@ The usage file **Section 1** — CSV reader (visit pattern): +.. das-doc: fragment .. code-block:: das var data <- %csv~ Alice, 30, New York %% @@ -227,6 +236,7 @@ values are parsed and embedded at compile time, not at runtime. **Section 2** — BASIC transpiler (suffix pattern): +.. das-doc: fragment .. code-block:: das %basic~ @@ -240,6 +250,7 @@ This appears at **module level** (not inside a function). The suffix generates a function ``basic_hello()`` that the rest of the file can call: +.. das-doc: fragment .. code-block:: das [export] @@ -284,10 +295,22 @@ Inline suffix (expression level) The ``suffix`` pattern above injects text at **module level** — ``%basic~ … %%`` generates a top-level function. The same ``suffix`` hook can also rewrite **in expression position** when invoked with a ``!`` separator instead of ``~``. ``InlineSumReader`` (``[reader_macro(name=sum)]``) -demonstrates this — ``%sum! a, b, c %%`` rewrites to ``( a + b + c )``: +demonstrates this — ``%sum! a, b, c %%`` rewrites to ``( a + b + c )``. +It lives in the same module file: +.. das-doc: file reader_macro_mod.das .. code-block:: das + options gen2 + options no_aot + + module reader_macro_mod + + require daslib/ast + require strings + require daslib/ast_boost + require daslib/strings_boost + [reader_macro(name=sum)] class InlineSumReader : AstReaderMacro { def override accept ( prog:ProgramPtr; mod:Module?; var expr:ExprReader?; ch:int; info:LineInfo ) : bool { @@ -318,6 +341,8 @@ Used inline, the macro is itself an expression: .. code-block:: das + require reader_macro_mod + let total = %sum! 1, 2, 3, 4 %% // ( 1 + 2 + 3 + 4 ) == 10 let scaled = %sum! 10, 20 %% * 2 // ( 10 + 20 ) * 2 == 60 diff --git a/doc/source/reference/tutorials/macros/12_typeinfo_macro.rst b/doc/source/reference/tutorials/macros/12_typeinfo_macro.rst index 2af868996b..9f17bb189c 100644 --- a/doc/source/reference/tutorials/macros/12_typeinfo_macro.rst +++ b/doc/source/reference/tutorials/macros/12_typeinfo_macro.rst @@ -78,8 +78,18 @@ struct_info — returning a string ``typeinfo struct_info(type)`` builds a description string at compile time: +.. das-doc: file typeinfo_macro_mod.das .. code-block:: das + options gen2 + options no_aot + + module typeinfo_macro_mod + + require daslib/ast + require strings + require daslib/ast_boost + [typeinfo_macro(name="struct_info")] class TypeInfoGetStructInfo : AstTypeInfoMacro { def override getAstChange(expr : ExprTypeInfo?; @@ -126,6 +136,7 @@ enum_value_strings — returning an array ``typeinfo enum_value_strings(type)`` returns a fixed-size array of enum value names: +.. das-doc: file typeinfo_macro_mod.das .. code-block:: das [typeinfo_macro(name="enum_value_strings")] @@ -163,9 +174,9 @@ Key points: - ``expr.typeexpr.enumType.list`` iterates all ``EnumEntry`` nodes. - The bare block provides a lexical scope for the intermediate variable inside the loop. -- The result is a **dynamic array** (``array``), not a - fixed-size ``string[N]`` — ``ExprMakeArray`` always produces a - dynamic array. +- The result is a **fixed-size array** — ``ExprMakeArray`` with a + ``makeType`` produces ``string[N]``, where ``N`` is the number of + values pushed, not a dynamic ``array``. has_non_static_method — returning a bool with subtrait @@ -175,6 +186,7 @@ has_non_static_method — returning a bool with subtrait has a non-static method with the given name. The method name is passed via the ``subtrait`` parameter: +.. das-doc: file typeinfo_macro_mod.das .. code-block:: das [typeinfo_macro(name="has_non_static_method")] @@ -224,6 +236,7 @@ Full source: :download:`12_typeinfo_macro.das <../../../../../tutorials/macros/1 Section 1: struct_info ---------------------- +.. das-doc: given require typeinfo_macro_mod .. code-block:: das struct Vec3 { diff --git a/doc/source/reference/tutorials/macros/13_enumeration_macro.rst b/doc/source/reference/tutorials/macros/13_enumeration_macro.rst index 470cdf1387..e75cd07836 100644 --- a/doc/source/reference/tutorials/macros/13_enumeration_macro.rst +++ b/doc/source/reference/tutorials/macros/13_enumeration_macro.rst @@ -52,12 +52,22 @@ The module file — enum_total ============================ The macro module defines a single ``AstEnumerationAnnotation`` subclass -that adds a ``total`` entry to any enum. +that adds a ``total`` entry to any enum. Macros live in their own module, +so the file opens with a ``module`` declaration — without one the compiler +rejects it with *"module Module_Name is required"*. Full source: :download:`enum_macro_mod.das <../../../../../tutorials/macros/enum_macro_mod.das>` +.. das-doc: file enum_macro_mod.das .. code-block:: das + options gen2 + + module enum_macro_mod + + require daslib/ast + require daslib/ast_boost + [enumeration_macro(name="enum_total")] class EnumTotalAnnotation : AstEnumerationAnnotation { def override apply(var enu : EnumerationPtr; @@ -191,6 +201,7 @@ How string_to_enum works internally The ``EnumFromStringConstruction`` class in ``daslib/enum_trait.das`` demonstrates the **code generation** pattern for enumeration macros: +.. das-doc: fragment .. code-block:: das [enumeration_macro(name="string_to_enum")] diff --git a/doc/source/reference/tutorials/macros/14_pass_macro.rst b/doc/source/reference/tutorials/macros/14_pass_macro.rst index 469616e487..962030df58 100644 --- a/doc/source/reference/tutorials/macros/14_pass_macro.rst +++ b/doc/source/reference/tutorials/macros/14_pass_macro.rst @@ -22,7 +22,9 @@ single method: ``apply(prog : ProgramPtr; mod : Module?) → bool`` ``prog`` is the full program being compiled. - ``mod`` is the module that registered the macro. + ``mod`` is the module currently being compiled — every pass-macro call + site passes ``prog.thisModule``, the same module ``compiling_module()`` + returns. It is *not* the module that owns the macro. The return value depends on the annotation (see below). Five annotations control **when** the macro runs: @@ -47,7 +49,8 @@ Five annotations control **when** the macro runs: currently being compiled. * - ``[global_lint_macro]`` - Same as ``[lint_macro]`` but runs for **all** modules, not just - the one that requires it. + the one that requires it. The owning module must be ``shared``, + or the global sweep never reaches it. * - ``[optimization_macro]`` - Runs during the **optimization** loop, after built-in optimisations. Returning ``true`` continues the loop. @@ -65,14 +68,25 @@ 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. +Both macros live in a single module that the user requires. Macro modules +open with a ``module`` declaration — without one the compiler rejects the +file with *"module Module_Name is required"*. Section 1 — lint_macro (compile-time analysis) ---------------------------------------------- +.. das-doc: file pass_macro_mod.das .. code-block:: das + options gen2 + + module pass_macro_mod + + require daslib/ast + require daslib/ast_boost + require daslib/templates_boost + [lint_macro] class CodeStatsLint : AstPassMacro { def override apply(prog : ProgramPtr; mod : Module?) : bool { @@ -94,9 +108,10 @@ Key points: - ``[lint_macro]`` means this class runs **after inference succeeds**, during the read-only lint phase. -- ``compiling_module()`` returns the module being compiled right now. - This is **not** the same as ``mod``, which is the module that owns - the macro (``pass_macro_mod``). +- ``compiling_module()`` returns the module being compiled right now — + the same module the ``mod`` parameter carries. Neither one names the + module that owns the macro (``pass_macro_mod``); a macro that needs its + own module has to capture it at registration time. - ``for_each_function("")`` iterates the module's functions. The empty string means "all names". - The lint checks function body size: any function with more than @@ -120,6 +135,7 @@ body. It follows the same visitor pattern as ``daslib/heartbeat.das``. First, a helper function that the injected code will call: +.. das-doc: file pass_macro_mod.das .. code-block:: das def public _trace_enter(name : string) { @@ -128,6 +144,7 @@ First, a helper function that the injected code will call: The visitor walks the AST and modifies function bodies: +.. das-doc: file pass_macro_mod.das .. code-block:: das class TraceCallsVisitor : AstVisitor { @@ -190,6 +207,7 @@ Key visitor techniques: The pass macro creates the visitor and walks the full program: +.. das-doc: file pass_macro_mod.das .. code-block:: das [infer_macro] @@ -304,7 +322,10 @@ When the compiler processes the user's program: 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.) + (``[global_lint_macro]`` macros also run once per compiled module — + they just skip the visibility check, so they fire for modules that + never required them. Their module must be ``shared`` to be reachable + from that global sweep.) 6. **Execution** — the instrumented program runs. diff --git a/doc/source/reference/tutorials/macros/18_with_boost.rst b/doc/source/reference/tutorials/macros/18_with_boost.rst index 70b6dd3119..c6baed78e5 100644 --- a/doc/source/reference/tutorials/macros/18_with_boost.rst +++ b/doc/source/reference/tutorials/macros/18_with_boost.rst @@ -14,10 +14,11 @@ Macro Tutorial 18: ``with_`` — locked binding of container slots ergonomics problem: rebinding a struct field across an array or table element. The naive form is rejected by daslang's typer: +.. das-doc: expect error[31019] .. code-block:: das var arr = [A(f1 = 1, f2 = 2)] - var a : A& = arr[0] // error[31300]: local reference to non-local expression is unsafe + var a : A& = arr[0] // error[31019]: local reference to non-local expression is unsafe a.f1 = 99 Between binding ``a`` and writing through it, code could push/resize/erase @@ -28,12 +29,18 @@ Between binding ``a`` and writing through it, code could push/resize/erase push/erase/resize/clear inside the body panic at runtime instead of silently corrupting memory. -The single-arg form is a 1:1 replacement for the rejected pattern above: +The single-arg form is a 1:1 replacement for the rejected pattern above. +``A`` is the element struct every snippet on this page uses: .. code-block:: das require daslib/with_boost + struct A { + f1 : int + f2 : int + } + var arr = [A(f1 = 1, f2 = 2)] with_(arr[0]) { _.f1 = 99 // mutation persists in arr[0] @@ -145,25 +152,30 @@ message before exit for nicer logging, NOT to recover-and-continue. Section 6 — Refused container shapes ===================================== -``with_`` is intentionally narrow: +``with_`` is intentionally narrow. Two shapes are refused at +macro-expansion time, with the macro-error code ``50503`` and a message +naming the failing arg: * **Non-``ExprAt`` containers** (plain locals, struct fields on locals, function-call results, array literals) are refused. The macro needs - to ref-bind the container to a local, and only ExprVar-rooted - lvalue chains (variables, ``obj.field``, ``arr[i]``) have stable - addressable storage outside the expression. Use built-in ``with`` for - locals; for literal-or-call containers, hoist to a ``var`` first. + to ref-bind the container to a local, and only chains rooted in a + variable and reached through **field hops only** — ``arr``, + ``obj.field``, ``obj.a.b`` — have stable addressable storage outside + the expression. Index hops are not followed: ``with_(outer[i].inner[j])`` + is refused too, because locking ``inner`` would leave ``outer`` free to + reallocate from inside the body. Use built-in ``with`` for locals; for + literal-or-call containers, hoist to a ``var`` first. * **More than one table-keyed arg** is refused per the rehash hazard noted above. -* **Bodies that ``return`` a value** are refused at typecheck time — - the synthesized invoke target declares a ``: void`` block return. - ``with_`` is for in-place mutation; compute values via a local: - ``var v : T; with_(arr[0]) { v = _.f }``. +A third shape is rejected later, by the typer: -All refusals fire at macro-expansion time with the macro-error code -``50503`` and a message describing the failing arg. +* **Bodies that ``return`` a value** — the synthesized invoke target + declares a ``: void`` block return, so the ``return`` reports + ``error[31402]: not expecting a return value``. ``with_`` is for + in-place mutation; compute values via a local: + ``var v : T; with_(arr[0]) { v = _.f }``. Running the tutorial @@ -182,6 +194,10 @@ Expected output:: section 6: tab[k].f1 = 777 section 7: see comment for the lock-panic shape +The labels are the source file's own section numbering, which counts the +"why ``with_`` exists" preamble as section 1 — so its section *N* is this +page's Section *N-1*. + .. seealso:: diff --git a/doc/source/reference/tutorials/macros/19_add_module_option.rst b/doc/source/reference/tutorials/macros/19_add_module_option.rst index 025932370e..80b069e5c7 100644 --- a/doc/source/reference/tutorials/macros/19_add_module_option.rst +++ b/doc/source/reference/tutorials/macros/19_add_module_option.rst @@ -32,10 +32,20 @@ Full source: :download:`add_module_option_mod.das <../../../../../tutorials/macr ``add_module_option(module, name, type)`` records an option name and its type on a module. It must run while the module's macros are being compiled, so it lives in a ``macro_function`` guarded by -``is_compiling_macros_in_module``: +``is_compiling_macros_in_module``. Macro modules open with a ``module`` +declaration — without one the compiler rejects the file with *"module +Module_Name is required"*: +.. das-doc: file add_module_option_mod.das .. code-block:: das + options gen2 + + module add_module_option_mod + + require daslib/ast + require daslib/ast_boost + [_macro, macro_function] def register_options { if (is_compiling_macros_in_module("add_module_option_mod")) { @@ -55,6 +65,7 @@ A ``[lint_macro]`` runs once per module compiled after this one. It reads the flag off the program options and, when set, prints a per-module note at compile time: +.. das-doc: file add_module_option_mod.das .. code-block:: das [lint_macro] @@ -64,7 +75,7 @@ compile time: if (!on) return false let cm = compiling_module() var nfun = 0 - cm |> for_each_function("") $(var func : FunctionPtr) { + cm |> for_each_function("") $(var _func : FunctionPtr) { nfun++ } let name = empty(cm.name) ? "
" : string(cm.name) @@ -89,9 +100,22 @@ The usage file Full source: :download:`19_add_module_option.das <../../../../../tutorials/macros/19_add_module_option.das>` -.. literalinclude:: ../../../../../tutorials/macros/19_add_module_option.das - :language: das - :lines: 23-36 +.. code-block:: das + + require add_module_option_mod + options trace_compile = true + + def greet(name : string) { + print("Hello, {name}!\n") + } + + def add(a, b : int) : int => a + b + + [export] + def main() { + greet("world") + print("2 + 3 = {add(2, 3)}\n") + } The ``options trace_compile = true`` line is accepted only because ``add_module_option_mod`` registered the name — without the ``require``, the diff --git a/doc/source/reference/tutorials/macros/20_template_struct_instance.rst b/doc/source/reference/tutorials/macros/20_template_struct_instance.rst index 6dcce6080b..9d39bb9793 100644 --- a/doc/source/reference/tutorials/macros/20_template_struct_instance.rst +++ b/doc/source/reference/tutorials/macros/20_template_struct_instance.rst @@ -30,10 +30,21 @@ An instance is just a class that inherits from the template: The template ============ +Templates live in a module that the instances require, so the file opens +with a ``module`` declaration. ``require daslib/typemacro_boost public`` +re-exports the annotation, so instance files get it from this one require. + Full source: :download:`template_struct_instance_mod.das <../../../../../tutorials/macros/template_struct_instance_mod.das>` +.. das-doc: file template_struct_instance_mod.das .. code-block:: das + options gen2 + + module template_struct_instance_mod shared public + + require daslib/typemacro_boost public + [ |> template_struct_instance] class template public TopKeeperT { best : KT @@ -82,6 +93,8 @@ Full source: :download:`20_template_struct_instance.das <../../../../../tutorial .. code-block:: das + require template_struct_instance_mod + class FloatTop : TopKeeperT { // 1. type parameter typedef KT = float } @@ -114,8 +127,17 @@ The fourth pattern parameterizes a **free-function call**. The template calls a function by name; ``@template_call`` marks that name as rebindable — the field name is what the body spells, the init is where the call goes: +.. das-doc: file template_struct_instance_mod.das .. code-block:: das + def public dot_i(a, b : int) : int { + return a * b + } + + def public dot_i_scaled(a, b : int) : int { + return a * b * 100 + } + [ |> template_struct_instance] class template public MixT { acc : int = 0 @@ -126,6 +148,10 @@ field name is what the body spells, the init is where the call goes: } } +One instance keeps the default target, the other redirects it: + +.. code-block:: das + class MixPlain : MixT { // dot_i calls stay on the real dot_i } @@ -153,6 +179,7 @@ the whole class for that one field is what templates exist to avoid. ``@template_gate`` names a bool ``@template_constant``; the field exists only in stamps where that constant is true: +.. das-doc: file template_struct_instance_mod.das .. code-block:: das [ |> template_struct_instance] @@ -172,6 +199,10 @@ in stamps where that constant is true: } } +The two instances differ by that one axis: + +.. code-block:: das + class PlainSum : SumT { } diff --git a/doc/source/reference/tutorials/sql_01_hello.rst b/doc/source/reference/tutorials/sql_01_hello.rst index ab906bc157..2b4bba7971 100644 --- a/doc/source/reference/tutorials/sql_01_hello.rst +++ b/doc/source/reference/tutorials/sql_01_hello.rst @@ -23,6 +23,9 @@ Two requires:: require daslib/sql require sqlite/sqlite_boost +.. das-doc: given require daslib/sql +.. das-doc: given require sqlite/sqlite_boost + ``daslib/sql`` is the abstract layer — it re-exports ``Option`` and ``Result`` so user code only needs one import for those types. ``sqlite/sqlite_boost`` is the SQLite provider; it ships ``SqlRunner``, diff --git a/doc/source/reference/tutorials/sql_02_insert_data.rst b/doc/source/reference/tutorials/sql_02_insert_data.rst index a3bf6b9c32..88a4fa6d80 100644 --- a/doc/source/reference/tutorials/sql_02_insert_data.rst +++ b/doc/source/reference/tutorials/sql_02_insert_data.rst @@ -60,6 +60,8 @@ next to its type: ``sql_bind`` maps the value to a SQLite primitive, ``array``, ``bool``, and enums) ship with pairs. To support a custom type, add your own pair — no registration required: +.. das-doc: signatures + .. code-block:: das def sql_bind(v : MyType) : string { return ... } @@ -143,16 +145,18 @@ not eight: .. code-block:: das - db |> insert([ - Car(Id=1, Name="Audi", Price=52642), - Car(Id=2, Name="Mercedes", Price=57127), - Car(Id=3, Name="Skoda", Price=9000), - Car(Id=4, Name="Volvo", Price=29000), - Car(Id=5, Name="Bentley", Price=350000), - Car(Id=6, Name="Citroen", Price=21000), - Car(Id=7, Name="Hummer", Price=41400), - Car(Id=8, Name="Volkswagen", Price=21600) - ]) + with_sqlite("test.db") <| $(db) { + db |> insert([ + Car(Id=1, Name="Audi", Price=52642), + Car(Id=2, Name="Mercedes", Price=57127), + Car(Id=3, Name="Skoda", Price=9000), + Car(Id=4, Name="Volvo", Price=29000), + Car(Id=5, Name="Bentley", Price=350000), + Car(Id=6, Name="Citroen", Price=21000), + Car(Id=7, Name="Hummer", Price=41400), + Car(Id=8, Name="Volkswagen", Price=21600) + ]) + } All rows must agree on PK presence: the first row decides which ``INSERT`` shape to prepare. The transaction rolls back on any diff --git a/doc/source/reference/tutorials/sql_03_last_row_id.rst b/doc/source/reference/tutorials/sql_03_last_row_id.rst index 586267f991..bbd3c7b719 100644 --- a/doc/source/reference/tutorials/sql_03_last_row_id.rst +++ b/doc/source/reference/tutorials/sql_03_last_row_id.rst @@ -70,7 +70,9 @@ Reading the last rowid directly .. code-block:: das - let id = db |> last_insert_rowid() + with_sqlite(":memory:") <| $(db) { + let id = db |> last_insert_rowid() + } Use this when you need the most recent rowid in a context where the ``insert`` return value is no longer in scope. diff --git a/doc/source/reference/tutorials/sql_04_select_all.rst b/doc/source/reference/tutorials/sql_04_select_all.rst index ab23c9bddf..a43dc9c690 100644 --- a/doc/source/reference/tutorials/sql_04_select_all.rst +++ b/doc/source/reference/tutorials/sql_04_select_all.rst @@ -39,6 +39,9 @@ Reading every row The minimal chain ``select_from(type)`` reads every row of the table into ``array``: +.. das-doc: given [sql_table(name="Cars")] struct Car { @sql_primary_key Id : int; Name : string; Price : int } +.. das-doc: given var inscope db = open_sqlite(":memory:") + .. code-block:: das let cars <- _sql(db |> select_from(type)) diff --git a/doc/source/reference/tutorials/sql_05_parametrized.rst b/doc/source/reference/tutorials/sql_05_parametrized.rst index 44a3682df5..939711dd65 100644 --- a/doc/source/reference/tutorials/sql_05_parametrized.rst +++ b/doc/source/reference/tutorials/sql_05_parametrized.rst @@ -24,6 +24,9 @@ name in the chain as either a **column reference** (``_.Field``) or a ``?`` placeholders and bound automatically --- you never type ``?`` or ``:name``: +.. das-doc: given [sql_table(name="Cars")] struct Car { @sql_primary_key Id : int; Name : string; Price : int } +.. das-doc: given var inscope db = open_sqlite(":memory:") + .. code-block:: das let target = 3 @@ -62,7 +65,7 @@ variadic trailing args: The trailing args bind to ``?`` placeholders in declaration order. Mixed types are fine --- daslang's overload resolution dispatches to -the right ``sqlite_bind`` for each arg: +the right ``sql_bind_to_stmt`` for each arg: .. code-block:: das diff --git a/doc/source/reference/tutorials/sql_06_error_handling.rst b/doc/source/reference/tutorials/sql_06_error_handling.rst index dbccc27160..d6ae0a4749 100644 --- a/doc/source/reference/tutorials/sql_06_error_handling.rst +++ b/doc/source/reference/tutorials/sql_06_error_handling.rst @@ -36,6 +36,8 @@ Use ``try_open_sqlite`` when the path is user-supplied or otherwise unreliable. The strict ``open_sqlite`` / ``with_sqlite`` forms panic on the same failure: +.. das-doc: given [sql_table(name="Users")] struct User { @sql_primary_key Id : int; Name : string } + .. code-block:: das var open_result <- try_open_sqlite(":memory:") @@ -90,7 +92,8 @@ helper used and the return type: // _try_sql(... |> _first()) : Result // _try_sql(... |> _first_opt()) : Result, string> - // _try_sql(... |> count()) : Result + // _try_sql(... |> count()) : Result + // _try_sql(... |> long_count()) : Result let res = _try_sql(db |> select_from(type) |> _first()) if (res |> is_ok) { diff --git a/doc/source/reference/tutorials/sql_07_anatomy.rst b/doc/source/reference/tutorials/sql_07_anatomy.rst index f00b4cfcbd..5ce568691c 100644 --- a/doc/source/reference/tutorials/sql_07_anatomy.rst +++ b/doc/source/reference/tutorials/sql_07_anatomy.rst @@ -39,6 +39,9 @@ Inspecting the SQL with ``_sql_text`` string instead of running it. The ``?`` placeholders show where each bind goes: +.. das-doc: given [sql_table(name="Cars")] struct Car { @sql_primary_key Id : int; Name : string; Price : int } +.. das-doc: given var inscope db = open_sqlite(":memory:") + .. code-block:: das let sql1 = _sql_text(db |> select_from(type) @@ -66,6 +69,8 @@ parameterized either way is the safe-by-default behavior: Column-side detection is symmetric --- the analyzer recognizes ``_.Field`` as a column on whichever side of an operator it appears: +.. das-doc: skip + .. code-block:: das _where(_.Price > cutoff) // WHERE "Price" > ? diff --git a/doc/source/reference/tutorials/sql_08_where.rst b/doc/source/reference/tutorials/sql_08_where.rst index 908ff193e6..43034cd10e 100644 --- a/doc/source/reference/tutorials/sql_08_where.rst +++ b/doc/source/reference/tutorials/sql_08_where.rst @@ -57,6 +57,9 @@ Captured-variable equality The simplest pattern --- a free variable on one side, a column ref on the other: +.. das-doc: given [sql_table(name="Cars")] struct Car { @sql_primary_key Id : int; Name : string; Price : int } +.. das-doc: given var inscope db = open_sqlite(":memory:") + .. code-block:: das let target = 3 diff --git a/doc/source/reference/tutorials/sql_09_select.rst b/doc/source/reference/tutorials/sql_09_select.rst index de23e0e02b..27053b65c2 100644 --- a/doc/source/reference/tutorials/sql_09_select.rst +++ b/doc/source/reference/tutorials/sql_09_select.rst @@ -31,6 +31,9 @@ No ``_select`` --- the macro emits ``SELECT`` of every column declared in the ``[sql_table]`` struct, in declaration order, and materializes each row as the source struct: +.. das-doc: given [sql_table(name="Cars")] struct Car { @sql_primary_key Id : int; Name : string; Price : int } +.. das-doc: given var inscope db = open_sqlite(":memory:") + .. code-block:: das let cars <- _sql(db |> select_from(type)) @@ -68,10 +71,9 @@ name no longer matters at the use site: to_log(LOG_INFO, " {p.Name} (price={p.Price})\n") } -The recordNames live on the result tuple's ``TypeDecl.argNames``; -``build_row_builder`` constructs that ``recordType`` explicitly because -the ``ExprMakeTuple``'s own recordNames vector isn't bound to daslang -yet. +The chosen names live on the result tuple's ``TypeDecl.argNames`` --- +``build_row_builder`` sets that record type when it builds the row +reader. Renaming via named tuple ======================== @@ -84,7 +86,7 @@ match the domain language better than the SQL column names do: let renamed <- _sql(db |> select_from(type) |> _select((Identifier=_.Id, Label=_.Name))) - // emits: SELECT "Id", "Name" FROM "Cars" + // emits: SELECT "Id" AS "Identifier", "Name" AS "Label" FROM "Cars" // result: array> for (r in renamed) { @@ -97,14 +99,16 @@ Computed columns A named-tuple value can be any computed expression over the row's columns, not just a bare column reference. It is rendered into the ``SELECT`` list by the same translator that powers ``_where`` -predicates, so anything legal in a predicate works as a column. Result -fields map by position, so no SQL alias is emitted: +predicates, so anything legal in a predicate works as a column. The +macro adds an ``AS ""`` alias whenever the chosen name differs +from the source column name, and a computed column always gets one. +The row reader still maps result fields by position: .. code-block:: das let bonuses <- _sql(db |> select_from(type) |> _select((Name=_.Name, Bonus=_.Price / 10))) - // emits: SELECT "Name", ("Price") / (?) FROM "Cars" + // emits: SELECT "Name", ("Price") / (?) AS "Bonus" FROM "Cars" // result: array> A computed column composes with a computed ``_where`` --- the predicate diff --git a/doc/source/reference/tutorials/sql_10_order_by.rst b/doc/source/reference/tutorials/sql_10_order_by.rst index 3d531e9656..0205ce1a3b 100644 --- a/doc/source/reference/tutorials/sql_10_order_by.rst +++ b/doc/source/reference/tutorials/sql_10_order_by.rst @@ -27,6 +27,9 @@ Key shape Emitted SQL Single-column order =================== +.. das-doc: given [sql_table(name="Cars")] struct Car { @sql_primary_key Id : int; Name : string; Price : int } +.. das-doc: given var inscope db = open_sqlite(":memory:") + .. code-block:: das let by_price <- _sql(db |> select_from(type) diff --git a/doc/source/reference/tutorials/sql_11_take_skip.rst b/doc/source/reference/tutorials/sql_11_take_skip.rst index 89a8e86383..771076313f 100644 --- a/doc/source/reference/tutorials/sql_11_take_skip.rst +++ b/doc/source/reference/tutorials/sql_11_take_skip.rst @@ -26,6 +26,9 @@ Source shape Emitted SQL ``take(n)`` --- LIMIT ===================== +.. das-doc: given [sql_table(name="Cars")] struct Car { @sql_primary_key Id : int; Name : string; Price : int } +.. das-doc: given var inscope db = open_sqlite(":memory:") + .. code-block:: das let first_two <- _sql(db |> select_from(type) |> take(2)) diff --git a/doc/source/reference/tutorials/sql_12_distinct.rst b/doc/source/reference/tutorials/sql_12_distinct.rst index eae30a9244..28c2c53085 100644 --- a/doc/source/reference/tutorials/sql_12_distinct.rst +++ b/doc/source/reference/tutorials/sql_12_distinct.rst @@ -21,6 +21,9 @@ Full-row DISTINCT Without a ``_select``, every column is in the row --- DISTINCT deduplicates whole rows: +.. das-doc: given [sql_table(name="Cars")] struct Car { @sql_primary_key Id : int; Name : string; Price : int } +.. das-doc: given var inscope db = open_sqlite(":memory:") + .. code-block:: das let all_rows <- _sql(db |> select_from(type) |> distinct()) @@ -161,7 +164,7 @@ all source columns: |> _group_by(_.Name) |> _select((Brand = _._0, FirstCar = _._1 |> first()))) - // SELECT "Name", "Id", "Name", "Price" + // SELECT "Name" AS "Brand", "Id", "Name", "Price" // FROM (SELECT *, MIN("Id") FROM "Cars" GROUP BY "Name") AS "t0" // Output type: array<(Brand:string, FirstCar:Car)> diff --git a/doc/source/reference/tutorials/sql_12b_set_ops.rst b/doc/source/reference/tutorials/sql_12b_set_ops.rst index 33f3c5606c..89bf2fa24b 100644 --- a/doc/source/reference/tutorials/sql_12b_set_ops.rst +++ b/doc/source/reference/tutorials/sql_12b_set_ops.rst @@ -33,6 +33,10 @@ the sources have different schemas. Distinct tags from either table =============================== +.. das-doc: given [sql_table(name="Customers")] struct Customer { @sql_primary_key Id : int; Tier : int } +.. das-doc: given [sql_table(name="Prospects")] struct Prospect { @sql_primary_key Id : int; Tier : int } +.. das-doc: given var inscope db = open_sqlite(":memory:") + .. code-block:: das let all_tiers <- _sql((db |> select_from(type) |> _select(_.Tier)) @@ -44,8 +48,8 @@ Tags present in both tables .. code-block:: das - let shared <- _sql((db |> select_from(type) |> _select(_.Tier)) - |> intersect(db |> select_from(type) |> _select(_.Tier))) + let shared_tiers <- _sql((db |> select_from(type) |> _select(_.Tier)) + |> intersect(db |> select_from(type) |> _select(_.Tier))) Tags present only on the LHS ============================ diff --git a/doc/source/reference/tutorials/sql_13_aggregates.rst b/doc/source/reference/tutorials/sql_13_aggregates.rst index 3b40f1dc67..c6f76b523c 100644 --- a/doc/source/reference/tutorials/sql_13_aggregates.rst +++ b/doc/source/reference/tutorials/sql_13_aggregates.rst @@ -48,6 +48,9 @@ one-row counterpart when several scalar facts are needed together. ``count`` --- whole-source row count ==================================== +.. das-doc: given [sql_table(name="Cars")] struct Car { @sql_primary_key Id : int; Name : string; Price : int } +.. das-doc: given var inscope db = open_sqlite(":memory:") + .. code-block:: das let n = _sql(db |> select_from(type) |> count()) diff --git a/doc/source/reference/tutorials/sql_14_group_by.rst b/doc/source/reference/tutorials/sql_14_group_by.rst index 9671fd528f..f61dbc8406 100644 --- a/doc/source/reference/tutorials/sql_14_group_by.rst +++ b/doc/source/reference/tutorials/sql_14_group_by.rst @@ -52,13 +52,19 @@ body as ``.`` for any parameter name. Single-key grouping =================== +.. das-doc: given [sql_table(name="Users")] struct User { @sql_primary_key Id : int; Name : string; City : string; Age : int; Salary : int } +.. das-doc: given var inscope db = open_sqlite(":memory:") +.. das-doc: given let min_age = 25 +.. das-doc: given let min_count = 1 + .. code-block:: das let by_city <- _sql(db |> select_from(type) |> _group_by(_.City) |> _order_by(_._0) |> _select((City = _._0, N = _._1 |> length))) - // SELECT "City", COUNT(*) FROM "Users" GROUP BY "City" + // SELECT "City", COUNT(*) AS "N" FROM "Users" + // GROUP BY "City" ORDER BY "City" ASC Multiple aggregates in one projection ===================================== @@ -102,7 +108,7 @@ inside ``_having`` translate the same way as inside ``_select``: |> _group_by(_.City) |> _having(_._1 |> length > 1) |> _select((City = _._0, N = _._1 |> length))) - // SELECT "City", COUNT(*) FROM "Users" + // SELECT "City", COUNT(*) AS "N" FROM "Users" // GROUP BY "City" HAVING COUNT(*) > ? Multi-key grouping @@ -116,7 +122,7 @@ Pass a tuple to ``_group_by``. Each tuple field becomes its own _sql(db |> select_from(type) |> _group_by((_.City, _.Age)) |> _select((City = _._0._0, Age = _._0._1, N = _._1 |> length))) - // SELECT "City", "Age", COUNT(*) FROM "Users" + // SELECT "City", "Age", COUNT(*) AS "N" FROM "Users" // GROUP BY "City", "Age" Expression group keys @@ -131,7 +137,7 @@ shared by the SELECT and the GROUP BY clause: _sql(db |> select_from(type) |> _group_by(_.Age % 100) |> _select((K = _._0, N = _._1 |> length))) - // SELECT (("Age") % (100)), COUNT(*) FROM "Users" + // SELECT (("Age") % (100)) AS "K", COUNT(*) AS "N" FROM "Users" // GROUP BY (("Age") % (100)) A computed key can sit alongside a plain field key in a multi-key @@ -155,9 +161,9 @@ The chain mirrors SQL clause order one-for-one: AvgSalary = _._1 |> select($(u : User) => u.Salary) |> average)) |> take(10)) - // SELECT "City", COUNT(*), AVG("Salary") FROM "Users" - // WHERE "Age" >= ? GROUP BY "City" HAVING COUNT(*) >= ? - // ORDER BY "City" ASC LIMIT ? + // SELECT "City", COUNT(*) AS "N", AVG("Salary") AS "AvgSalary" + // FROM "Users" WHERE "Age" >= ? GROUP BY "City" + // HAVING COUNT(*) >= ? ORDER BY "City" ASC LIMIT ? .. seealso:: diff --git a/doc/source/reference/tutorials/sql_15_join.rst b/doc/source/reference/tutorials/sql_15_join.rst index f2b3503dde..babc98d7bf 100644 --- a/doc/source/reference/tutorials/sql_15_join.rst +++ b/doc/source/reference/tutorials/sql_15_join.rst @@ -20,6 +20,8 @@ Equi-join shape The ``on`` predicate is locked to the equi-join shape +.. das-doc: fragment + .. code-block:: das $(l : TA, r : TB) => l.X == r.Y @@ -41,13 +43,19 @@ WHERE, projection, and ON clauses qualifies with the matching alias. Single-source chains keep the unqualified shape --- only multi-source queries pay the alias-noise tax. +.. das-doc: given [sql_table(name="Users")] struct User { @sql_primary_key Id : int; Name : string; Active : bool } +.. das-doc: given [sql_table(name="Orders")] struct Order { @sql_primary_key Id : int; UserId : int; Total : int } +.. das-doc: given [sql_table(name="Cars")] struct Car { @sql_primary_key id : int; brand : string; price : int; dealer_id : int } +.. das-doc: given [sql_table(name="Dealers")] struct Dealer { @sql_primary_key id : int; name : string } +.. das-doc: given var inscope db = open_sqlite(":memory:") + .. code-block:: das let rows <- _sql(db |> select_from(type) |> _join(db |> select_from(type), $(u : User, o : Order) => u.Id == o.UserId, $(u : User, o : Order) => (UserName = u.Name, Total = o.Total))) - // SELECT "t0"."Name", "t1"."Total" + // SELECT "t0"."Name" AS "UserName", "t1"."Total" // FROM "Users" AS "t0" // INNER JOIN "Orders" AS "t1" // ON "t0"."Id" = "t1"."UserId" @@ -58,17 +66,21 @@ Filtering before / inside / after the join Pre-join ``_where`` filters the LEFT source. Column refs qualify with the left alias automatically: +.. das-doc: fragment + .. code-block:: das db |> select_from(type) |> _where(_.Active) |> _join(...) - // ... WHERE "t0"."Active" + // ... WHERE ("t0"."Active" <> 0) ``_where`` can also live inside the right-side chain --- filters there qualify with the right alias ``t1`` and emit into the JOIN's ``ON`` clause (not the outer ``WHERE``): +.. das-doc: fragment + .. code-block:: das _join(db |> select_from(type) |> _where(_.Total > 75), ...) @@ -112,6 +124,7 @@ to its qualified columns and reconstructs the exact nominal struct: // INNER JOIN "Orders" AS "t1" // ON "t0"."Id" = "t1"."UserId" // WHERE "t1"."Total" >= ? + // ORDER BY "t1"."Total" ASC The result type is ``array``, not a generated tuple. This form is useful when the other source only supplies filtering, ranking, or @@ -137,7 +150,7 @@ SQL composes cleanly with the JOIN: $(c : Car, d : Dealer) => (Brand = c.brand, Price = c.price)) |> _group_by(_.Brand) |> _select((Brand = _._0, N = _._1 |> count()))) - // SELECT ("t0"."brand"), COUNT(*) + // SELECT ("t0"."brand") AS "Brand", COUNT(*) AS "N" // FROM "Cars" AS "t0" INNER JOIN "Dealers" AS "t1" // ON "t0"."dealer_id" = "t1"."id" // GROUP BY ("t0"."brand") diff --git a/doc/source/reference/tutorials/sql_16_left_join.rst b/doc/source/reference/tutorials/sql_16_left_join.rst index 66d4955d3b..f46e60c376 100644 --- a/doc/source/reference/tutorials/sql_16_left_join.rst +++ b/doc/source/reference/tutorials/sql_16_left_join.rst @@ -57,13 +57,17 @@ Probe in projection Allowed on join kind Emitted LEFT JOIN ========= +.. das-doc: given [sql_table(name="Users")] struct User { @sql_primary_key Id : int; Name : string } +.. das-doc: given [sql_table(name="Orders")] struct Order { @sql_primary_key Id : int; UserId : int; Total : int } +.. das-doc: given var inscope db = open_sqlite(":memory:") + .. code-block:: das let rows <- _sql(db |> select_from(type) |> _left_join(db |> select_from(type), $(u : User, o : Order) => u.Id == o.UserId, $(u : User, o : Option) => (Name = u.Name, HasOrder = o |> is_some))) - // SELECT "t0"."Name", "t1"."UserId" IS NOT NULL + // SELECT "t0"."Name", "t1"."UserId" IS NOT NULL AS "HasOrder" // FROM "Users" AS "t0" // LEFT JOIN "Orders" AS "t1" // ON "t0"."Id" = "t1"."UserId" @@ -79,7 +83,7 @@ Mirror of LEFT --- every right row surfaces, left side is ``Option``. |> _right_join(db |> select_from(type), $(u : User, o : Order) => u.Id == o.UserId, $(u : Option, o : Order) => (HasUser = u |> is_some, OrderId = o.Id))) - // SELECT "t0"."Id" IS NOT NULL, "t1"."Id" + // SELECT "t0"."Id" IS NOT NULL AS "HasUser", "t1"."Id" AS "OrderId" // FROM "Users" AS "t0" // RIGHT JOIN "Orders" AS "t1" // ON "t0"."Id" = "t1"."UserId" @@ -98,7 +102,8 @@ Both sides are ``Option``. Probe either arg; the analyzer routes the |> _full_outer_join(db |> select_from(type), $(u : User, o : Order) => u.Id == o.UserId, $(u : Option, o : Option) => (HasUser = u |> is_some, HasOrder = o |> is_some))) - // SELECT "t0"."Id" IS NOT NULL, "t1"."UserId" IS NOT NULL + // SELECT "t0"."Id" IS NOT NULL AS "HasUser", + // "t1"."UserId" IS NOT NULL AS "HasOrder" // FROM "Users" AS "t0" // FULL OUTER JOIN "Orders" AS "t1" // ON "t0"."Id" = "t1"."UserId" @@ -115,7 +120,7 @@ inner-join-shaped result. let rows <- _sql(db |> select_from(type) |> _cross_join(db |> select_from(type), $(u : User, o : Order) => (UserName = u.Name, OrderId = o.Id))) - // SELECT "t0"."Name", "t1"."Id" + // SELECT "t0"."Name" AS "UserName", "t1"."Id" AS "OrderId" // FROM "Users" AS "t0" // CROSS JOIN "Orders" AS "t1" diff --git a/doc/source/reference/tutorials/sql_17_subqueries.rst b/doc/source/reference/tutorials/sql_17_subqueries.rst index af703606fc..47ad4b4dad 100644 --- a/doc/source/reference/tutorials/sql_17_subqueries.rst +++ b/doc/source/reference/tutorials/sql_17_subqueries.rst @@ -46,6 +46,10 @@ IN / NOT IN with a single-column subquery For IN-style subqueries, project a single column with ``_select(_.Col)`` so the IN list shape matches. +.. das-doc: given [sql_table(name="Users")] struct User { @sql_primary_key Id : int; Name : string; Active : bool } +.. das-doc: given [sql_table(name="Orders")] struct Order { @sql_primary_key Id : int; UserId : int; Total : int } +.. das-doc: given var inscope db = open_sqlite(":memory:") + .. code-block:: das let with_orders <- _sql(db |> select_from(type) diff --git a/doc/source/reference/tutorials/sql_18_null_handling.rst b/doc/source/reference/tutorials/sql_18_null_handling.rst index f2f96439ff..28134a3411 100644 --- a/doc/source/reference/tutorials/sql_18_null_handling.rst +++ b/doc/source/reference/tutorials/sql_18_null_handling.rst @@ -70,6 +70,8 @@ INSERT/UPDATE bind code branches per field type at compile time. ``sqlite3_bind_null``. SELECT readers check ``sqlite3_column_type == SQLITE_NULL`` and wrap accordingly. +.. das-doc: given var inscope db = open_sqlite(":memory:") + .. code-block:: das db |> insert(User( @@ -143,11 +145,12 @@ steers users away from that footgun: use ``_.Col |> is_none()`` (emits ``IS NULL``) or ``_.Col |> unwrap_or(d) == x`` (emits ``COALESCE`` then compare). -Direct ``_.Col == none()`` in a predicate is intentionally not -translated this chunk. A future revision may either lower it to -``IS NULL`` automatically or raise a ``macro_error`` with a fix-it -pointing to ``|> is_none()`` --- the leaning is toward the explicit -diagnostic so the user has to confront three-valued logic head-on. +Direct ``_.Col == none()`` in a predicate is refused outright: the +macro raises ``error[50503]`` with a fix-it pointing to +``|> is_none()`` (or ``is_some()`` for the negation). The explicit +diagnostic is deliberate --- equality against ``none()`` would +silently bind an empty placeholder, so the user has to confront +three-valued logic head-on. ``_try_sql`` composes ===================== diff --git a/doc/source/reference/tutorials/sql_19_update.rst b/doc/source/reference/tutorials/sql_19_update.rst index 5170da31aa..07417adaf4 100644 --- a/doc/source/reference/tutorials/sql_19_update.rst +++ b/doc/source/reference/tutorials/sql_19_update.rst @@ -45,11 +45,14 @@ site. RETURNING is macro-only; there is no plain By-PK whole-row replace ======================= +.. das-doc: given [sql_table(name="Users")] struct User { @sql_primary_key Id : int; Name : string; Email : string; Active : bool; LastSeen : int64 } +.. das-doc: given var inscope db = open_sqlite(":memory:") + .. code-block:: das let n1 = db |> update(User(Id = 1, Name = "alice", Email = "alice@new.com", Active = true, LastSeen = 100l)) - // UPDATE "Users" SET "Name"=?, "Email"=?, "Active"=?, "LastSeen"=? + // UPDATE "Users" SET "Name" = ?, "Email" = ?, "Active" = ?, "LastSeen" = ? // WHERE "Id" = ? A non-matching PK returns 0 rows-affected. Not an error. diff --git a/doc/source/reference/tutorials/sql_20_delete.rst b/doc/source/reference/tutorials/sql_20_delete.rst index 4cd8933bf6..8810973f77 100644 --- a/doc/source/reference/tutorials/sql_20_delete.rst +++ b/doc/source/reference/tutorials/sql_20_delete.rst @@ -46,6 +46,10 @@ By-PK from a row Useful when you already have the row loaded (e.g. from a SELECT). +.. das-doc: given [sql_table(name="Users")] struct User { @sql_primary_key Id : int; Name : string; Active : bool } +.. das-doc: given [sql_table(name="Orders")] struct Order { @sql_primary_key Id : int; UserId : int; Total : int } +.. das-doc: given var inscope db = open_sqlite(":memory:") + .. code-block:: das let n1 = db |> delete_(User(Id = 2, Name = "", Active = false)) diff --git a/doc/source/reference/tutorials/sql_21_upsert.rst b/doc/source/reference/tutorials/sql_21_upsert.rst index 5ffd5472ad..1641105f1c 100644 --- a/doc/source/reference/tutorials/sql_21_upsert.rst +++ b/doc/source/reference/tutorials/sql_21_upsert.rst @@ -39,6 +39,8 @@ INSERT OR IGNORE Silent no-op on PK / UNIQUE conflict. Returns rows-affected: 1 if inserted, 0 if ignored. +.. das-doc: given var inscope db = open_sqlite(":memory:") + .. code-block:: das let n = db |> insert_or_ignore(WordHit(Id = 2, Word = "world", Hits = 1, Last = 200l)) @@ -78,7 +80,7 @@ ON CONFLICT ... DO UPDATE --- the proper merge (Hits = _.Hits + 1, Last = _excluded.Last)) // INSERT INTO "WordHits" (...) VALUES (?,?,?,?) // ON CONFLICT("Id") DO UPDATE SET - // "Hits" = ("Hits") + (?), + // "Hits" = ("WordHits"."Hits") + (?), // "Last" = excluded."Last" Auto-assigned integer primary keys @@ -162,7 +164,7 @@ practice; the array shape mirrors ``_sql_update_returning``). _.Id, (Hits = _.Hits + 1)) // INSERT INTO "WordHits" (...) VALUES (?,?,?,?) - // ON CONFLICT("Id") DO UPDATE SET "Hits" = ("Hits") + (?) + // ON CONFLICT("Id") DO UPDATE SET "Hits" = ("WordHits"."Hits") + (?) // RETURNING "Id", "Word", "Hits", "Last" Non-panic ``try_`` variants diff --git a/doc/source/reference/tutorials/sql_22_transactions.rst b/doc/source/reference/tutorials/sql_22_transactions.rst index 2eaf1bde8f..d02323f962 100644 --- a/doc/source/reference/tutorials/sql_22_transactions.rst +++ b/doc/source/reference/tutorials/sql_22_transactions.rst @@ -46,6 +46,9 @@ site. Same shape for ``try_transaction``. Canonical form ============== +.. das-doc: given [sql_table(name="Friends")] struct Friend { @sql_primary_key Id : int; Name : string } +.. das-doc: given var inscope db = open_sqlite(":memory:") + .. code-block:: das db |> with_transaction() { diff --git a/doc/source/reference/tutorials/sql_23_foreign_keys.rst b/doc/source/reference/tutorials/sql_23_foreign_keys.rst index 6ec148bc04..3e6ee7a02c 100644 --- a/doc/source/reference/tutorials/sql_23_foreign_keys.rst +++ b/doc/source/reference/tutorials/sql_23_foreign_keys.rst @@ -50,6 +50,8 @@ appear and CASCADE silently no-ops. dasSQLITE turns it on always. CASCADE delete ============== +.. das-doc: given var inscope db = open_sqlite(":memory:") + .. code-block:: das [sql_table(name = "Users")] diff --git a/doc/source/reference/tutorials/sql_24_indexes.rst b/doc/source/reference/tutorials/sql_24_indexes.rst index fab14d1acc..783c288ee9 100644 --- a/doc/source/reference/tutorials/sql_24_indexes.rst +++ b/doc/source/reference/tutorials/sql_24_indexes.rst @@ -25,6 +25,8 @@ Sibling-annotation shape Both annotations live in the **same** bracket pair, comma-separated, with ``[sql_table]`` first. +.. das-doc: given var inscope db = open_sqlite(":memory:") + .. code-block:: das [sql_table(name = "Users"), @@ -52,8 +54,8 @@ Argument Meaning / default ============================================== =================================================== ``[sql_table]`` validates every field name against the struct's -fields at macro-expansion time. Misspellings produce a compile error -listing the valid columns. +fields at macro-expansion time. A misspelling fails the macro with +``error[20800]`` naming the offending field and the struct. DDL emitted =========== @@ -82,6 +84,8 @@ generated name ``uq__``. It goes through column DDL, so the same annotation is enforced by SQLite, DuckDB, and PostgreSQL. +.. das-doc: fragment + .. code-block:: das struct User { diff --git a/doc/source/reference/tutorials/sql_25_defaults_computed.rst b/doc/source/reference/tutorials/sql_25_defaults_computed.rst index 53d719fa1a..b7d74b8207 100644 --- a/doc/source/reference/tutorials/sql_25_defaults_computed.rst +++ b/doc/source/reference/tutorials/sql_25_defaults_computed.rst @@ -51,6 +51,8 @@ writes. Default to ``VIRTUAL`` otherwise. Schema example ============== +.. das-doc: given var inscope db = open_sqlite(":memory:") + .. code-block:: das [sql_table(name = "Items")] @@ -87,8 +89,8 @@ them itself, so any value the struct holds for ``DoubleQty`` / db |> insert(Item(Id = 1, Name = "thing", Active = true, Quantity = 7, Tag = "t", CreatedAt = "", DoubleQty = 9999, QtyPlusOne = 9999)) - // INSERT INTO "Items" ("Id","Name","Active","Quantity","Tag","CreatedAt") - // VALUES (?,?,?,?,?,?) + // INSERT INTO "Items" ("Id", "Name", "Active", "Quantity", "Tag", "CreatedAt") + // VALUES (?, ?, ?, ?, ?, ?) Defaults fire when the column is omitted ======================================== diff --git a/doc/source/reference/tutorials/sql_26_custom_types.rst b/doc/source/reference/tutorials/sql_26_custom_types.rst index 38ed6edfc6..7884278b1a 100644 --- a/doc/source/reference/tutorials/sql_26_custom_types.rst +++ b/doc/source/reference/tutorials/sql_26_custom_types.rst @@ -23,6 +23,8 @@ step. The two-function pair ===================== +.. das-doc: signatures + .. code-block:: das def sql_bind (v : T) : P // T -> primitive @@ -56,7 +58,8 @@ every ``sql_bind`` overload in scope at the call site --- including the user's type-specific pair --- participates in overload resolution. Same mechanism as ``_::clone`` and ``_::finalize``. -Built-in adapters ship in ``sqlite_boost`` for: +Built-in adapters ship in ``daslib/sql_boost`` (re-exported by +``sqlite/sqlite_boost``) for: * The four primitives (passthrough). * Stdlib widenings: ``int`` / ``int8`` / ``int16`` / ``uint`` / @@ -177,8 +180,10 @@ Missing-adapter compile error ============================= If a ``[sql_table]`` field has no ``sql_bind`` / ``sql_extract`` pair -in scope, overload resolution fails at the macro-emitted ``_::sql_bind`` -call: +in scope, the macro-emitted ``_::sql_bind`` call lands on the catch-all +``auto`` overload, whose ``concept_assert`` fires as ``error[31400]``: + +.. das-doc: expect error[31400] .. code-block:: das @@ -190,7 +195,8 @@ call: Bg : Color // no sql_bind(Color) - compile error } -Compiler message names the offending struct + field type. No runtime +The message spells the fix, and the instantiation trail names the +offending field type and the struct it came from. No runtime "type not registered" error --- this is all compile-time. .. seealso:: diff --git a/doc/source/reference/tutorials/sql_27_blob.rst b/doc/source/reference/tutorials/sql_27_blob.rst index e2097730d7..20414f770e 100644 --- a/doc/source/reference/tutorials/sql_27_blob.rst +++ b/doc/source/reference/tutorials/sql_27_blob.rst @@ -106,7 +106,10 @@ Future: streaming BLOB I/O SQLite offers ``sqlite3_blob_open`` / ``sqlite3_blob_read`` / ``sqlite3_blob_write`` for incremental access without materializing -the whole blob in daslang's heap. Likely shape: +the whole blob in daslang's heap. Likely shape (not implemented --- +sketch only): + +.. das-doc: fragment .. code-block:: das diff --git a/doc/source/reference/tutorials/sql_28_json.rst b/doc/source/reference/tutorials/sql_28_json.rst index 110cc81e67..7488b08964 100644 --- a/doc/source/reference/tutorials/sql_28_json.rst +++ b/doc/source/reference/tutorials/sql_28_json.rst @@ -53,6 +53,8 @@ daslang refuses to guess. @sql_json: TEXT-backed, queryable ================================= +.. das-doc: given var inscope db = open_sqlite(":memory:") + .. code-block:: das [sql_table(name = "Users")] @@ -65,6 +67,8 @@ daslang refuses to guess. The macro emits, at module scope: +.. das-doc: fragment + .. code-block:: das def sql_bind (v : tuple<...>) : string { @@ -96,7 +100,7 @@ SELECT projections. The leaf type drives result-side adapter dispatch // WHERE descent let dark_users <- _sql(db |> select_from(type) |> _where(_.Prefs.theme == "dark")) - // SELECT Id, Name, Prefs FROM "Users" + // SELECT "Id", "Name", "Prefs" FROM "Users" // WHERE json_extract("Prefs", '$.theme') = ? // SELECT projection descent @@ -107,7 +111,7 @@ SELECT projections. The leaf type drives result-side adapter dispatch // Mixed projection (plain column + JSON path) let mixed <- _sql(db |> select_from(type) |> _select((Name = _.Name, Theme = _.Prefs.theme))) - // SELECT "Name", json_extract("Prefs", '$.theme') FROM "Users" + // SELECT "Name", json_extract("Prefs", '$.theme') AS "Theme" FROM "Users" Descent is arbitrary depth --- nested struct paths concatenate dotted: @@ -124,7 +128,7 @@ Descent is arbitrary depth --- nested struct paths concatenate dotted: let in_ny <- _sql(db |> select_from(type) |> _where(_.Profile.Addr.City == "NY")) - // SELECT Id, Profile FROM "Accounts" + // SELECT "Id", "Profile" FROM "Accounts" // WHERE json_extract("Profile", '$.Addr.City') = ? @sql_blob: opaque binary archive @@ -150,6 +154,8 @@ is ``const`` because the catch-all binder passes ``v`` non-\ ``var``; ``mem_archive_save`` / ``mem_archive_load`` need a mutable reference, so the body clones into a local through ``clone_to_move``: +.. das-doc: fragment + .. code-block:: das def sql_bind (v : T) : array { diff --git a/doc/source/reference/tutorials/sql_29_column_names.rst b/doc/source/reference/tutorials/sql_29_column_names.rst index 1e7b0fbb4e..b8fe50b84f 100644 --- a/doc/source/reference/tutorials/sql_29_column_names.rst +++ b/doc/source/reference/tutorials/sql_29_column_names.rst @@ -40,6 +40,8 @@ Band 1 --- ``column_info(type)`` API. ``[sql_table]`` already walks struct fields at compile time to emit DDL and bind/column code; ``column_info`` is a view over that same walk: +.. das-doc: signatures + .. code-block:: das enum SqlType { @@ -55,10 +57,14 @@ DDL and bind/column code; ``column_info`` is a view over that same walk: is_pk : bool is_nullable : bool default_expr : string // "" if none + is_computed : bool // @sql_computed - GENERATED ALWAYS AS column } ``SqlType`` is **abstract** --- it lives in ``daslib/sql``. Provider -helpers render the dialect-specific spelling: +helpers render the dialect-specific spelling (``sqlite_sql_type`` ships +in ``sqlite/sqlite_provider``, re-exported by ``sqlite/sqlite_boost``): + +.. das-doc: signatures .. code-block:: das @@ -114,7 +120,8 @@ comprehensions, ``for``-in, filters) without further ceremony: ``@sql_json`` and ``@sql_blob`` short-circuit the witness lookup --- ``column_info`` reports ``SqlType.Text`` for JSON columns and ``SqlType.Blob`` for archive columns regardless of the daslang field -type. Computed columns appear in the array with empty ``default_expr``. +type. Computed columns appear in the array with ``is_computed = true`` +and an empty ``default_expr``. Band 3 --- raw PRAGMA via ``query`` ==================================== @@ -122,6 +129,8 @@ Band 3 --- raw PRAGMA via ``query`` ``[sql_table]`` on a read-only row shape is the idiomatic way to opt into the typed materializer without committing to a CREATE TABLE: +.. das-doc: given var inscope db = open_sqlite(":memory:") + .. code-block:: das [sql_table(name = "pragma_columns")] diff --git a/doc/source/reference/tutorials/sql_30_list_tables.rst b/doc/source/reference/tutorials/sql_30_list_tables.rst index e3f6f9e96f..b8eabb6563 100644 --- a/doc/source/reference/tutorials/sql_30_list_tables.rst +++ b/doc/source/reference/tutorials/sql_30_list_tables.rst @@ -41,6 +41,8 @@ End-to-end ``[sql_table]`` on a read-only row shape opts into the materializer rail without claiming a real underlying table: +.. das-doc: given var inscope db = open_sqlite(":memory:") + .. code-block:: das [sql_table(name = "sqlite_master_rows")] diff --git a/doc/source/reference/tutorials/sql_31_views.rst b/doc/source/reference/tutorials/sql_31_views.rst index bb98d47730..a53c1c2f63 100644 --- a/doc/source/reference/tutorials/sql_31_views.rst +++ b/doc/source/reference/tutorials/sql_31_views.rst @@ -57,6 +57,10 @@ view to see the new state. End-to-end ========== +.. das-doc: given var inscope db = open_sqlite(":memory:") +.. das-doc: given [sql_view(name="HugeOrders")] struct HugeOrder { Id : int; CustomerId : int; Amount : int; Status : string } +.. das-doc: given [sql_view(name="MaxOrders")] struct MaxOrder { Id : int; CustomerId : int; Amount : int; Status : string } + .. code-block:: das [sql_table(name = "Customers")] @@ -89,9 +93,9 @@ End-to-end db |> create_table(type) // ... insert rows ... - // CREATE VIEW BigOrders(Id, CustomerId, Amount, Status) AS - // SELECT Id, CustomerId, Amount, Status FROM Orders - // WHERE Amount >= 100 + // CREATE VIEW "BigOrders"("Id", "CustomerId", "Amount", "Status") AS + // SELECT "Id", "CustomerId", "Amount", "Status" FROM "Orders" + // WHERE "Amount" >= 100 db |> _create_view(type, db |> select_from(type) |> _where(_.Amount >= 100)) @@ -135,7 +139,7 @@ struct fields, function calls, arithmetic over the above: .. code-block:: das - let cutoff = 100 + var cutoff = 100 db |> _create_view(type, db |> select_from(type) |> _where(_.Amount >= cutoff)) // sqlite_schema now stores: ... WHERE "Amount" >= 100 @@ -155,7 +159,7 @@ does not update the view. To re-bake, drop and re-create: .. code-block:: das db |> drop_view_if_exists(type) - let cutoff = 200 + cutoff = 200 db |> _create_view(type, db |> select_from(type) |> _where(_.Amount >= cutoff)) @@ -186,13 +190,28 @@ precedence over the default set. .. code-block:: das - enum Status { Pending = 1; Shipped = 2 } + enum Status { Pending = 1, Shipped = 2 } def to_sql_literal(s : Status) : string => "{int(s)}" + [sql_table(name = "Shipments")] + struct Shipment { + @sql_primary_key Id : int + StatusVal : Status + Amount : int + } + + [sql_view(name = "ShippedOrders")] + struct ShippedOrder { + Id : int + StatusVal : Status + Amount : int + } + let want = Status.Shipped - db |> _create_view(type, - db |> select_from(type) |> _where(_.StatusVal == want)) + db |> _create_view(type, + db |> select_from(type) |> _where(_.StatusVal == want)) + // sqlite_schema now stores: ... WHERE "StatusVal" = 2 Types with no overload fail at compile time with the catch-all's ``concept_assert`` message: *to_sql_literal: unsupported type for diff --git a/doc/source/reference/tutorials/sql_32_sql_functions.rst b/doc/source/reference/tutorials/sql_32_sql_functions.rst index 752ec63215..48916bdadb 100644 --- a/doc/source/reference/tutorials/sql_32_sql_functions.rst +++ b/doc/source/reference/tutorials/sql_32_sql_functions.rst @@ -23,6 +23,8 @@ scalar would go. The contract ============ +.. das-doc: signatures + .. code-block:: das db |> register_function(name : string, fn : @@<...>; @@ -86,6 +88,8 @@ SQL helpers, all marked deterministic so SQLite is allowed to factor them out of inner loops or use them as the indexed expression in a ``CREATE INDEX``. +.. das-doc: given var inscope db = open_sqlite(":memory:") + .. code-block:: das require sqlite/sqlite_boost @@ -187,7 +191,7 @@ predicate or projection just like a built-in scalar. } [sql_function(name="event_id")] - def sql_event(tag : string) : int { ... } + def sql_event(tag : string) : int => length(tag) The annotation does two things at compile time: diff --git a/doc/source/reference/tutorials/sql_33_pragma.rst b/doc/source/reference/tutorials/sql_33_pragma.rst index bb87777670..8811b3d140 100644 --- a/doc/source/reference/tutorials/sql_33_pragma.rst +++ b/doc/source/reference/tutorials/sql_33_pragma.rst @@ -30,11 +30,13 @@ The contract Three typed ``value`` overloads cover the common shapes: +.. das-doc: signatures + .. code-block:: das - set_pragma(db; name; value : string) // 'WAL', 'NORMAL', 'utf8' - set_pragma(db; name; value : int64) // 5000, 50000, 4096 - set_pragma(db; name; value : bool) // ON / OFF + def set_pragma(db : SqlRunner; name : string; value : string) : void // 'WAL', 'NORMAL', 'utf8' + def set_pragma(db : SqlRunner; name : string; value : int64) : void // 5000, 50000, 4096 + def set_pragma(db : SqlRunner; name : string; value : bool) : void // ON / OFF Each has a ``try_set_pragma`` sibling returning ``SqlError`` for non-panic recovery. PRAGMA values can't be bound with ``?`` --- @@ -46,6 +48,8 @@ user input), so this is fine. Reading PRAGMAs back uses the typed ``query_scalar`` rail (:ref:`tut 13 `): +.. das-doc: given var inscope db = open_sqlite(":memory:") + .. code-block:: das let mode = db |> query_scalar("PRAGMA journal_mode", type) diff --git a/doc/source/reference/tutorials/sql_34_backup_vacuum.rst b/doc/source/reference/tutorials/sql_34_backup_vacuum.rst index f48f6f8f6c..3ae2efad9f 100644 --- a/doc/source/reference/tutorials/sql_34_backup_vacuum.rst +++ b/doc/source/reference/tutorials/sql_34_backup_vacuum.rst @@ -42,6 +42,8 @@ returning ``SqlError`` / ``Result<...>`` for non-panic recovery. End-to-end ========== +.. das-doc: given var inscope db = open_sqlite(":memory:") + .. code-block:: das require daslib/sql diff --git a/doc/source/reference/tutorials/sql_35_streaming.rst b/doc/source/reference/tutorials/sql_35_streaming.rst index 28a62a60b9..ec89f7c48c 100644 --- a/doc/source/reference/tutorials/sql_35_streaming.rst +++ b/doc/source/reference/tutorials/sql_35_streaming.rst @@ -40,6 +40,8 @@ is reclaimed only as part of that teardown, not a recoverable cleanup.) End-to-end ========== +.. das-doc: given var inscope db = open_sqlite(":memory:") + .. code-block:: das require daslib/sql diff --git a/doc/source/reference/tutorials/sql_36_attach.rst b/doc/source/reference/tutorials/sql_36_attach.rst index 297a646e14..b1d57d20ef 100644 --- a/doc/source/reference/tutorials/sql_36_attach.rst +++ b/doc/source/reference/tutorials/sql_36_attach.rst @@ -39,6 +39,8 @@ uses that so it has no external file dependencies. End-to-end ========== +.. das-doc: given var inscope db = open_sqlite(":memory:") + .. code-block:: das require daslib/sql diff --git a/doc/source/reference/tutorials/sql_37_bulk_operations.rst b/doc/source/reference/tutorials/sql_37_bulk_operations.rst index 68cddcdca1..00d2e44fab 100644 --- a/doc/source/reference/tutorials/sql_37_bulk_operations.rst +++ b/doc/source/reference/tutorials/sql_37_bulk_operations.rst @@ -33,6 +33,9 @@ The default autocommit makes every INSERT a separate transaction and the whole batch is one ``fsync``. ~50x speedup on cold WAL is typical: +.. das-doc: given [sql_table(name="Events")] struct Event { @sql_primary_key Id : int; Kind : string; PayloadBytes : int } +.. das-doc: given var inscope db = open_sqlite(":memory:") + .. code-block:: das db |> with_transaction <| $() { @@ -66,6 +69,13 @@ outer ``with_transaction`` for the fsync win. Reach for an outer with **other** statements --- it adds no extra fsync benefit by itself. +One constraint on the array overload: every row must agree on +primary-key presence. The batch prepares one statement from +``rows[0]`` --- with or without the PK column --- so a mixed +array is rejected before the transaction opens (``try_insert`` +returns ``Err``, ``insert`` panics). Partition by "PK set / +PK unset" and call once per partition. + ``INSERT ... SELECT`` ===================== diff --git a/doc/source/reference/tutorials/sql_38_concurrency.rst b/doc/source/reference/tutorials/sql_38_concurrency.rst index a750e12599..ce3c881bc4 100644 --- a/doc/source/reference/tutorials/sql_38_concurrency.rst +++ b/doc/source/reference/tutorials/sql_38_concurrency.rst @@ -41,13 +41,15 @@ Each worker opens its own ``with_sqlite(...)``. The underlying file is shared; the handle is per-thread. WAL guarantees readers see a consistent snapshot while another thread writes: +.. das-doc: given [sql_table(name="Counters")] struct Counter { @sql_primary_key Id : int; Tally : int } + .. code-block:: das - def worker_read(path : string) { + def worker_read(path : string; worker : int) { with_sqlite(path) $(db) { // separate handle, separate prepared-statement cache let n = _sql(db |> select_from(type) |> count) - to_log(LOG_INFO, "thread {get_thread_id()} sees n={n}") + to_log(LOG_INFO, "worker {worker} sees n={n}") } } diff --git a/doc/source/reference/tutorials/sql_39_schema_from.rst b/doc/source/reference/tutorials/sql_39_schema_from.rst index 2cfc90e5bf..465ab3fff8 100644 --- a/doc/source/reference/tutorials/sql_39_schema_from.rst +++ b/doc/source/reference/tutorials/sql_39_schema_from.rst @@ -11,7 +11,7 @@ SQL-39 --- ``schema_from``: struct mirrors the DB single: Tutorial; check_schema ``[sql_table(schema_from = "path.db")]`` opens the .db at compile -time, reads ``pragma_table_info``, and populates the struct's +time, reads ``PRAGMA table_xinfo``, and populates the struct's fields from the actual schema. The struct mirrors the database --- which means schema drift becomes a **compile error** at the exact lines that need updating. No reflection, no migration metadata, @@ -86,14 +86,14 @@ route through the existing custom-types adapter rail. // Annotation override: @sql_json on a TEXT column tells the macro // to bind/extract via JSON encoding for a structured payload. - struct Note { + struct NoteBody { title : string rank : int } [sql_table(name = "Items", - schema_from = "items.db")] + schema_from = "tests/dasSQLITE/test_data/schema_from_nullable.db")] struct Item { - @sql_json Meta : Option // TEXT column on disk; JSON-encoded daslang side + @sql_json Note : Option // nullable TEXT column; JSON-encoded daslang side } What you cannot do via partial body: @@ -146,7 +146,7 @@ today is what the script reads/writes. ETL between two DBs, archival readers, admin tooling --- all good fits. For "the DB grows over time, run versioned schema migrations at -startup", the ``daslib/sqlite_migrate`` module ships +startup", the ``sqlite/sqlite_migrate`` module ships ``[sql_migration(version=N)]`` + a runtime runner (see :ref:`tutorial_sql_migrations`). The two are orthogonal: ``schema_from`` gives compile-time contract checks diff --git a/doc/source/reference/tutorials/sql_40_fts5.rst b/doc/source/reference/tutorials/sql_40_fts5.rst index 26236bd0e9..4fb9f8396e 100644 --- a/doc/source/reference/tutorials/sql_40_fts5.rst +++ b/doc/source/reference/tutorials/sql_40_fts5.rst @@ -20,8 +20,8 @@ builds an inverted index over text columns and exposes relevance ranking + Boolean / phrase / prefix / NEAR query operators. -API surface added in this chunk -=============================== +API surface used here +===================== * ``[sql_fts5(name = "...")]`` --- struct annotation: the type is materialized as a SQLite FTS5 virtual table. @@ -34,13 +34,15 @@ API surface added in this chunk emits ``col MATCH ?``. Compile error on a non-FTS5 column, with a fix-it pointing at ``contains`` or ``[sql_fts5]``. -The same ``text_match`` is also a Phase 0.4 ``daslib/fts5_query`` -predicate (full FTS5-grammar parser, in-memory matcher); user -code gets in-memory and SQL-side matching from one call site. +The same ``text_match`` is also a ``daslib/fts5_query`` predicate +(full FTS5-grammar parser, in-memory matcher); user code gets +in-memory and SQL-side matching from one call site. Declaring the virtual table =========================== +.. das-doc: given var inscope db = open_sqlite(":memory:") + .. code-block:: das require daslib/sql @@ -96,8 +98,8 @@ ascending BM25 score (lower = more relevant first): |> _where(_.Body |> text_match("quick fox")) |> _order_by(_.Rank)) // emits: - // SELECT "Body", rank FROM "docs_idx" - // WHERE "Body" MATCH ? ORDER BY rank + // SELECT "Id", "Body", rank FROM "docs_idx" + // WHERE "Body" MATCH ? ORDER BY "Rank" ASC Typed predicate DELETE ====================== @@ -136,6 +138,8 @@ non-FTS5 column --- it has no way to give useful behavior. The error message points at ``contains`` (LIKE-based substring) or adding ``[sql_fts5]``: +.. das-doc: expect error[50503] + .. code-block:: das [sql_table(name = "Articles")] @@ -223,6 +227,8 @@ the bound value via SQL ``ESCAPE '\'``. A user typing ``"50%"`` matches literal ``"50%"`` (not "50" + anything). User code never sees escape sequences: +.. das-doc: given [sql_table(name="Items")] struct Item { @sql_primary_key Id : int; Code : string } + .. code-block:: das let prefix = "50%" diff --git a/doc/source/reference/tutorials/sql_41_triggers.rst b/doc/source/reference/tutorials/sql_41_triggers.rst index 2b8872c51c..bfc7297bd8 100644 --- a/doc/source/reference/tutorials/sql_41_triggers.rst +++ b/doc/source/reference/tutorials/sql_41_triggers.rst @@ -38,6 +38,9 @@ A common use case: stamp ``UpdatedAt`` and write an audit-log row whenever an ``Articles`` row changes. Two triggers, one for INSERT and one for UPDATE: +.. das-doc: given [sql_table(name="Articles")] struct Article { @sql_primary_key Id : int; Title : string; UpdatedAt : int64 } +.. das-doc: given var inscope db = open_sqlite(":memory:") + .. code-block:: das db |> exec( @@ -91,20 +94,40 @@ side effect outside the DB, commit and queue the work in your application code on the path that wrote the row --- don't try to do it from a trigger. -Trigger recursion (gotcha) -========================== +Trigger cascades (gotcha) +========================= + +``PRAGMA recursive_triggers`` (OFF by default) controls one thing +only: whether a trigger may re-enter **itself**. It does *not* stop +a trigger body from firing a **different** trigger on the same +table. The two triggers above cascade: the ``AFTER INSERT`` trigger +updates ``Articles`` to stamp ``UpdatedAt``, that nested UPDATE +fires ``articles_audit_update``, and every INSERT therefore lands +**two** audit rows --- the nested ``UPDATE`` row first, then the +``INSERT`` row. -SQLite's default is ``PRAGMA recursive_triggers = OFF`` --- a write -performed inside a trigger body does **not** fire other triggers on -the same table. The audit-log example above relies on that default: -the ``AFTER INSERT`` trigger updates ``Articles`` (to stamp -``UpdatedAt``), and that nested update is intentionally swallowed -so the ``AFTER UPDATE`` trigger does not also write a row. +Filter the cascade in the trigger itself. A ``WHEN`` clause that +only fires on a real content change makes each statement write +exactly one audit row, while ``UpdatedAt`` still gets stamped on +both paths: + +.. code-block:: das + + db |> exec( + "CREATE TRIGGER articles_audit_update + AFTER UPDATE ON \"Articles\" + WHEN OLD.\"Title\" IS NOT NEW.\"Title\" + BEGIN + UPDATE \"Articles\" SET \"UpdatedAt\" = strftime('%s', 'now') + WHERE \"Id\" = NEW.\"Id\"; + INSERT INTO \"AuditLog\" (\"Op\", \"ArticleId\", \"AtUnix\") + VALUES ('UPDATE', NEW.\"Id\", strftime('%s', 'now')); + END") -If you opt into ``PRAGMA recursive_triggers = ON`` you must design -the trigger body so it doesn't re-touch the source table, or filter -the recursive case explicitly --- otherwise the audit log doubles -(or loops) on every write. +The trigger's own stamping UPDATE does not re-fire it --- that +*is* self-recursion, which the default ``recursive_triggers = OFF`` +suppresses. Turning the pragma ON removes that last guard too, so a +self-touching trigger body then needs its own ``WHEN`` filter. .. seealso:: diff --git a/doc/source/reference/tutorials/sql_43_migrations.rst b/doc/source/reference/tutorials/sql_43_migrations.rst index 3be17317fa..9b868f7cee 100644 --- a/doc/source/reference/tutorials/sql_43_migrations.rst +++ b/doc/source/reference/tutorials/sql_43_migrations.rst @@ -135,6 +135,8 @@ Inspection: pure reads, no side effects Three functions answer "what's the migration state?" without running anything: +.. das-doc: given var inscope db = open_sqlite(":memory:") + .. code-block:: das db |> current_schema_version() // int (0 if no audit table yet) @@ -161,8 +163,11 @@ apply. Typed ALTER: when daslang has enough info to validate ====================================================== -Migrations 2 and 3 above use the typed surface in -``daslib/sqlite_boost``: +Migrations 2 and 3 above use the provider-neutral typed ALTER +surface from ``daslib/sql_boost`` (``drop_index_if_exists`` is +provider-side, in ``sqlite/sqlite_boost``): + +.. das-doc: signatures .. code-block:: das @@ -171,10 +176,13 @@ Migrations 2 and 3 above use the typed surface in db |> create_unique_index(type, ... same shape ...) db |> drop_index_if_exists("name") -Field selectors are **string literals**, not ``.Field`` syntax: -gen2's parser only accepts ``.Field`` inside an ``_sql {...}`` -block. Plain call sites pass explicit string field names --- -the same convention ``[sql_index(fields="A")]`` already uses. +Field selectors are **string literals**, not ``_.Field`` +placeholders --- the ``_.`` row placeholder only has meaning +inside the ``_sql(...)`` chain macros. These call sites pass +explicit string field names, the same convention +``[sql_index(fields="A")]`` already uses. The macro resolves +each name against the struct at compile time, so a typo is a +compile error. What the typed forms buy you: @@ -307,9 +315,9 @@ The rebuild runs inside ``migrate_to_latest``'s big transaction [struct_convert] def my_v6_to_v7(old : UserV6; var dst : User) { - dst.Email = (old.LegacyEmail != "") + dst.Email = ((old.LegacyEmail != "") ? old.LegacyEmail - : "unknown@example.com" + : "unknown@example.com") } [sql_migration(version = 7, description = "restructure users")] diff --git a/doc/source/reference/utils/daslang_live.rst b/doc/source/reference/utils/daslang_live.rst index 0ff9df71b2..5239992f0b 100644 --- a/doc/source/reference/utils/daslang_live.rst +++ b/doc/source/reference/utils/daslang_live.rst @@ -44,8 +44,9 @@ Five requirements every live-reloadable application must handle: 3. **Runtime exception handling.** A crash in ``update()`` should not corrupt persistent state. The - host clears the store on exception, so the application must handle - starting from scratch gracefully. + host clears the store's data entries on exception and stops calling + ``update()``, so the application must handle starting from scratch + gracefully. 4. **State persistence.** GPU resources (windows, buffers, shaders) and game state (entities, @@ -75,9 +76,13 @@ save --- the window stays open and the new code takes effect. Here is the full ``hello/main.das``: +.. das-doc: file hello_main.das +.. das-doc: given require hello_main .. code-block:: das options gen2 + options persistent_heap + options gc require live/glfw_live require opengl/opengl_boost @@ -156,6 +161,7 @@ Here is the full ``hello/main.das``: init() while (!exit_requested()) { update() + maybe_collect_gc() } shutdown() } @@ -166,7 +172,9 @@ function drives the loop; under ``daslang-live.exe`` the host calls ``init()``, ``update()``, and ``shutdown()`` directly. For a stdin/stdout JSON-RPC transport instead of HTTP, swap one require -line and use ``examples/daslive/hello_stdio/``:: +line and use ``examples/daslive/hello_stdio/``: + +.. code-block:: das require live/live_api_stdio // instead of live/live_api @@ -204,14 +212,20 @@ Lifecycle 6. ``init()`` is called in the new context. **Failed reload:** -The host reverts to the old context, pauses execution, and stores the -compilation error. Retrieve it via ``GET /error``, the -``last_error`` stdio command, or ``get_last_error()``. The next -successful reload unpauses automatically. +The host reverts to the old context, re-runs ``[after_reload]`` and +``init()`` on it, pauses execution, and stores the compilation error. +Retrieve it via ``GET /error``, the ``last_error`` stdio command, or +``get_last_error()``. The next successful reload unpauses +automatically. **Runtime exception:** -The host pauses, clears the persistent store (potentially corrupted), -and sets the error. +The host stores the exception message, pauses, and clears the store's +data entries (``__live_vars_*`` and ``__decs_*``; infrastructure keys +such as the GLFW window handle and audio handles are preserved). The +context is then marked dead: ``update()`` and the ``[before_update]`` +hooks are skipped from that point on even if you unpause --- only a +reload or a reset recovers. An exception thrown inside a +``[live_command]`` does the same. Core API @@ -231,7 +245,10 @@ Lifecycle and timing * - ``is_live_mode() : bool`` - True when running under ``daslang-live.exe``. * - ``is_reload() : bool`` - - True during the first frame after a reload. + - True from the start of a reload cycle (it is set before + ``shutdown()``) until the process exits. Use it in ``init()`` / + ``shutdown()`` to tell a reload from a cold start --- it is not a + one-frame pulse. * - ``request_exit()`` - Signal the host to exit after the current frame. * - ``exit_requested() : bool`` @@ -246,7 +263,7 @@ Lifecycle and timing - Current frames per second. * - ``is_paused() : bool`` - True when execution is paused. - * - ``set_paused(v : bool)`` + * - ``set_paused(paused : bool)`` - Pause or unpause execution. Persistent store @@ -262,8 +279,19 @@ Persistent store - Store a byte array under a string key. Survives reloads. * - ``live_load_bytes(key, data) : bool`` - Load a byte array. Returns ``false`` if the key is not found. + * - ``live_store_string(key, value)`` + - Same, for a string value. + * - ``live_load_string(key, value) : bool`` + - Load a string. Returns ``false`` if the key is not found. * - ``get_last_error() : string`` - - Last compilation error (empty string if none). + - Last compilation error **or** runtime exception message (empty + string if none). + * - ``request_reset()`` + - Re-simulate the already-compiled program into a fresh context + (no recompile); clears ``@live`` vars and store data. + * - ``get_reload_generation() : uint64`` + - Bumped on every terminal reload / reset outcome, success or + failure. Also reported by ``GET /status``. Reload annotations @@ -280,8 +308,10 @@ Reload annotations * - ``[after_reload]`` - Called after recompile, before ``init()``. Restore state here. * - ``[before_update]`` - - Called every frame before ``update()``. Used internally by - ``live_api``, ``live_api_stdio``, and other transport agents. + - Called every frame before ``update()``. Used by + ``live/glfw_live`` (synthetic input playback) and + ``live/opengl_live`` (capture). Transport agents do **not** use + it --- they run on the debug-agent tick. The host discovers annotated functions by name prefix (``__before_reload_*``, ``__after_reload_*``, ``__before_update_*``). @@ -300,6 +330,8 @@ Tag globals with ``@live`` and the macro auto-generates ``[before_reload]``/``[after_reload]`` handlers that serialize them via ``Archive``. No manual save/restore needed: +.. das-doc: file live_vars_main.das +.. das-doc: given require live_vars_main .. code-block:: das options gen2 @@ -333,8 +365,13 @@ discarded and the new default takes effect --- safe format migration. Works with POD types, strings, enums, arrays, tables, and structs with serializable fields. -Full reload (``request_reload(true)`` or ``POST /reload/full``) clears -all ``@live`` entries. +``@live`` also works on **struct fields**: a non-``@live`` global whose +struct has ``@live`` fields is preserved field by field, and the +non-marked fields take their source defaults on reload. ``@live`` on a +``let`` is a compile error --- a constant has nothing to restore. + +Full reload (``request_reload(true)`` or ``POST /reload/full``) and +``POST /reset`` clear all ``@live`` entries. Manual serialization -------------------- @@ -357,13 +394,18 @@ Helper modules * - ``live/glfw_live`` - GLFW window that persists across reloads + synthetic mouse driver. * - ``live/opengl_live`` - - OpenGL screenshot + APNG video recording commands. + - OpenGL screenshot + APNG video recording commands, plus the + ``gl_stats`` command. * - ``live/decs_live`` - Auto-serialization of DECS entities across reloads. * - ``live/live_commands`` - ``[live_command]`` annotation for transport-callable functions. * - ``live/live_vars`` - ``@live`` variable macro (auto-persistence). + * - ``live/live_gc`` + - ``maybe_collect_gc()`` for standalone ``main`` loops (a no-op + under the host, which drives GC itself). Re-exported by + ``live/glfw_live``. * - ``live/live_watch`` - File watcher (auto-reload on save). * - ``live/live_watch_boost`` @@ -383,6 +425,11 @@ Helper modules * - ``live/audio_live`` - Audio state persistence across reloads. +The ``live/`` prefix is a registered module alias, not a directory: +``live/glfw_live``, ``live/opengl_live`` and ``live/audio_live`` live in +``modules/dasGlfw/``, ``modules/dasOpenGL/`` and ``modules/dasAudio/`` +respectively; the rest are in ``modules/dasLiveHost/live/``. + ``live/glfw_live`` ------------------ @@ -441,13 +488,28 @@ re-record APNG tours from a JSON timeline. - Stop playback and clear the queue. * - ``mouse_status`` - Playback status: ``playing``, ``elapsed_ms``, ``cursor_x``, - ``cursor_y``, ``queue_idx``, ``queue_total``. + ``cursor_y``, ``queue_idx``, ``queue_total``, ``held_count``, + ``cursor_owned``. ``get_synth_cursor() : tuple`` returns -``(active, x, y)``. Overlays that draw a cursor sprite or motion trail -should consult this — when ``active`` the synthetic driver owns the -position, and ``ImGui_ImplGlfw``'s per-frame poll would otherwise -overwrite ``io.MousePos`` with the real OS cursor on focused windows. +``(active, x, y)``. ``active`` goes true on the first synthetic event and +stays true for the rest of the session. Overlays that draw a cursor +sprite or motion trail should consult this — when ``active`` the +synthetic driver owns the position, and ``ImGui_ImplGlfw``'s per-frame +poll would otherwise overwrite ``io.MousePos`` with the real OS cursor on +focused windows. + +Synthetic keyboard driver +^^^^^^^^^^^^^^^^^^^^^^^^^ + +The same module carries a keyboard timeline built the same way: +``key_press`` / ``key_release`` (one key by GLFW keycode), ``key_char`` +(one codepoint through the char callback), ``key_type`` (type a whole +string), ``key_chord`` (modifiers held around a key), ``key_play`` / +``key_stop`` (scripted timeline), and ``key_status``. +``get_synth_keys() : tuple`` reports whether the +synthetic keyboard is active, and ``live_apply_control_badge(locked)`` +draws the "input is scripted" affordance. ``live/decs_live`` ------------------ @@ -456,6 +518,9 @@ Require this module and all DECS entities auto-persist across reloads. Guard ``decs::restart()`` with ``is_reload()`` to avoid wiping restored entities: +.. das-doc: given require live/decs_live +.. das-doc: given require daslib/decs +.. das-doc: given require live_host .. code-block:: das if (!is_reload()) { @@ -511,7 +576,9 @@ For ``daslang-live`` (the binary), pass ``--live-port N`` to the binary itself --- the C++ side scans the same full argv and keys its single-instance lock on the resolved port, so two daslang-live instances on different ports coexist on the same host. Invalid values -(non-numeric, out of ``[1, 65535]``) fall through to the default. +(non-numeric, out of ``[1, 65535]``) fall through to the default, and +the **last** ``--live-port`` on the command line wins unconditionally --- +``--live-port 19090 --live-port abc`` resolves to 9090, not 19090. Endpoints ^^^^^^^^^ @@ -525,19 +592,25 @@ Endpoints - Description * - GET - ``/status`` - - JSON: ``fps``, ``uptime``, ``paused``, ``dt``, ``has_error``. + - JSON: ``fps``, ``uptime``, ``paused``, ``dt``, ``has_error``, + ``generation``. * - GET - ``/error`` - - Plain text: last compilation error. + - ``text/plain`` with the last error; JSON ``null`` when there is + none. * - POST - ``/reload`` - Incremental reload. * - POST - ``/reload/full`` - Full recompile (clears ``@live`` vars). + * - POST + - ``/reset`` + - Re-simulate the compiled program into a fresh context (no + recompile); clears ``@live`` vars and store data. * - POST - ``/pause`` - - Pause execution. Returns 503 if compile error active. + - Pause execution. * - POST - ``/unpause`` - Resume execution. @@ -555,9 +628,16 @@ Endpoints response is a JSON array of per-entry results in input order. Continue-on-error: malformed entries surface ``{"error":...}`` in their slot. - * - ANY - - ``*`` - - JSON help with all endpoints and curl examples. + * - GET + - ``/`` + - JSON help with all endpoints and curl examples. There is no + catch-all route --- any other path returns 404. + +While a compilation error is active, ``/pause``, ``/unpause``, +``/command`` and ``/commands`` return 503 with +``{"error":…,"hint":"POST /reload to retry"}``. ``/reload``, +``/reload/full``, ``/reset``, ``/shutdown``, ``/status`` and ``/error`` +stay reachable. curl examples ^^^^^^^^^^^^^ @@ -660,7 +740,8 @@ Available methods (built-ins from ``live/live_api_builtins``): * - ``status`` - JSON: ``fps``, ``uptime``, ``paused``, ``dt``, ``has_error``. * - ``last_error`` - - Last compilation error string (or JSON ``null`` if none). + - Last compilation error or runtime exception string (or JSON + ``null`` if none). * - ``reload`` - Incremental reload. * - ``reload_full`` @@ -671,9 +752,17 @@ Available methods (built-ins from ``live/live_api_builtins``): - Resume execution. * - ``shutdown`` - Graceful shutdown. + * - ``help`` + - JSON object mapping every registered command name to its + description. Works over both transports. Any user-defined ``[live_command]`` is also callable by name. +An **unknown** method is not a ``-32601``: the dispatcher returns a +successful envelope whose ``result`` is the string +``"unknown command: "``. The ``-32700`` / ``-32600`` / ``-32602`` +codes only ever come from the envelope layer. + .. warning:: ``stdout`` is the response channel. Scripts that use this transport @@ -707,8 +796,9 @@ CLI reference * - ``-project_root `` - Project root --- the parent of ``modules/`` for daspkg-style module resolution. Equivalent to passing ``project_root`` to - MCP tools. - * - ``-load_module `` + MCP tools. ``-project-root`` is accepted too. Defaults to the + script's own directory. + * - ``-load_module `` (or ``-load-module``) - Directly load a single dynamic-module folder (the one containing ``.das_module``); repeatable. Bypasses the ``/modules/`` scan and shadows same-basename @@ -729,7 +819,7 @@ CLI reference * - ``--dump-leaks`` / ``--no-dump-leaks`` - Toggle JobStatus / HandleRegistry leak dumps at exit (default: dump). - * - ``--live-port `` + * - ``--live-port `` (or ``--live-port=N``) - REST API port. Default 9090; range ``[1, 65535]``. The single-instance lock is keyed on this value, so two binaries on different ports coexist on the same host. @@ -761,6 +851,12 @@ Examples - Full breakout game: DECS, audio, particles, 30+ live commands. * - ``examples/games/sequence/`` - Card board game: multi-module, bot AI, tournament runner. + * - ``examples/games/asteroids/`` + - Arcade shooter: GLFW + live commands + ``@live`` state. + * - ``examples/games/pacman/`` + - Maze game on DECS with audio and live commands. + * - ``examples/games/river_run/`` + - Scrolling arcade game. * - ``examples/daslive/tank_game/`` - 3D tank combat with dynamic lighting. * - ``examples/daslive/live_vars_demo/`` @@ -788,8 +884,11 @@ Tips and gotchas ``is_reload()`` to detect reloads. - Debug agents persist across reloads; their code is **not** updated on reload (restart required to pick up agent code changes). -- ``[live_command]`` functions cannot be defined in the same module - that registers them --- use a separate module. +- ``[live_command]`` cannot be used inside ``live/live_commands`` + itself --- the ``[function_macro]`` that implements it is not + available while its own module compiles. That is why + ``live_watch_boost`` is a separate file from ``live_watch``. Any + other module defines and registers its commands in one place. - Failed reload pauses the host --- check ``GET /error`` or ``get_last_error()``. - A single-instance lock prevents running two ``daslang-live.exe`` diff --git a/doc/source/reference/utils/daspkg.rst b/doc/source/reference/utils/daspkg.rst index 8d33bf26c9..6ea976129e 100644 --- a/doc/source/reference/utils/daspkg.rst +++ b/doc/source/reference/utils/daspkg.rst @@ -89,22 +89,32 @@ Commands - Upgrade one or all packages to the latest version. * - ``list`` - List installed packages. - * - ``search `` - - Search the package index. + * - ``search [query]`` + - Search the package index. An empty query lists every entry. * - ``build`` - - Build all C/C++ packages (cmake). + - Build all C/C++ packages (cmake). ``build --wasm`` builds the + wasm64 runtime and module archives instead. * - ``check`` - Verify installed packages are present and have ``.das_module``. + * - ``cleanup`` + - Remove ``modules/`` and ``daspkg.lock`` for a fresh install. + With ``--global`` it also requires ``--force``. * - ``doctor`` - Check environment (git, cmake, gh). + * - ``release`` + - Bundle the project as a redistributable standalone (exe + + shared modules + assets) under ``<--out>//``. + ``release wasm`` cross-compiles to a standalone web app instead. + * - ``update-index`` + - Refresh every index entry's metadata from its source repo. * - ``introduce [url]`` - Submit a package to the index via PR (requires ``gh`` CLI). * - ``withdraw `` - Remove a package from the index via PR (requires ``gh`` CLI). All commands that operate on packages (``install``, ``remove``, -``update``, ``upgrade``, ``list``, ``check``, ``build``) accept the -``--global`` flag. +``update``, ``upgrade``, ``list``, ``check``, ``build``, ``cleanup``) +accept the ``--global`` flag. Options: @@ -112,11 +122,20 @@ Options: - ``--force`` -- force reinstall even if already installed. - ``--global``, ``-g`` -- operate on global modules in ``{das_root}/modules/`` (see :ref:`daspkg_global_modules`). +- ``--branch ``, ``-b`` -- install from a branch instead of a tag. - ``--color`` / ``--no-color`` -- enable/disable ANSI colored output. - ``--verbose``, ``-v`` -- print debug details (git commands, resolve steps, file operations). - ``--json`` -- machine-readable JSON output (``search``, ``list``, ``check``). +- ``--out `` -- output directory for ``release`` (default: + current directory). +- ``--paranoid`` / ``--quick`` -- ``release`` tuning budget: re-mint the + ``[tune]`` sidecar with the paranoid budget, or accept a complete + existing sidecar instead of re-minting. +- ``--wasm`` -- on ``build``, target wasm64 (memory64). +- ``--wasm-lib-dir `` -- directory holding the wasm64 archives; + default ``/web/output64/lib``. Package sources @@ -200,10 +219,16 @@ Manifest functions - ``package_author(author)`` -- author name - ``package_description(desc)`` -- short description - ``package_source(url)`` -- canonical source URL + - ``package_license(license)`` -- license identifier + - ``package_tag(tag)`` / ``package_tags(tags)`` -- index search tags + (both append) + - ``package_min_sdk(version)`` -- declared minimum daslang SDK + version (index metadata; not enforced at install time) ``resolve(sdk_version, version : string)`` - Optional. Receives the daslang SDK version and the user-requested - version string, and calls one of: + Optional. Receives the user-requested version string -- the leading + ``v`` is stripped, so ``install foo@v1.0`` arrives as ``"1.0"`` -- and + calls one of: .. list-table:: :header-rows: 1 @@ -218,9 +243,13 @@ Manifest functions * - ``download_redirect("github.com/org/new-repo", "v2.0")`` - Re-clone from a different repository - If no ``resolve()`` function exists, the version string (from - ``install foo@v1.0``) is used as a git tag directly. If no - version is specified, the default branch is used. + The first parameter, ``sdk_version``, is reserved -- daspkg passes an + empty string for it today, so a manifest cannot yet branch on the SDK + release. + + If no ``resolve()`` function exists, the version string is used as a + git tag directly, retrying with a ``v`` prefix if that tag is + missing. If no version is specified, the default branch is used. ``dependencies(version : string)`` Optional. Declares dependencies on other packages: @@ -244,6 +273,14 @@ Manifest functions If omitted, no build step runs (pure-daslang package). +``release()`` + Optional. Declares what ``daspkg release`` ships for this package: + the entry script (``release_main``), the bundle name + (``release_name``), asset globs (``release_include`` / + ``release_exclude`` / ``release_include_from``), force-included + dylibs (``release_shared_module``), and the wasm-specific hooks used + by ``release wasm``. See ``daslib/daspkg.das`` for the full set. + Install flow ============ @@ -252,15 +289,21 @@ When you run ``daspkg install github.com/user/repo@v1.0``: 1. Shallow-clone the package from the default branch into a temp directory. -2. If ``.das_package`` exists and has ``resolve()`` -- call it with - ``sdk_version`` and ``"v1.0"``. Checkout the resolved tag/branch. - On redirect, discard the clone and re-clone from the new URL. +2. If ``.das_package`` exists and has ``resolve()`` -- call it with the + requested version, ``"1.0"`` (the leading ``v`` is stripped). + Checkout the resolved tag/branch. On redirect, discard the clone and + re-clone from the new URL. 3. If resolve returned nothing and a version was requested -- checkout - the version string as a git tag. + the version string as a git tag, retrying with a ``v`` prefix. 4. Move to ``modules//``. -5. Record in ``daspkg.lock``. -6. Install transitive dependencies (from ``dependencies()``). -7. Auto-build if ``.das_package`` has ``build()``. +5. Install transitive dependencies (from ``dependencies()``). +6. Auto-build if ``.das_package`` has ``build()``. +7. Record in ``daspkg.lock`` and add ``modules/`` to + ``.gitignore`` (local installs only). + +The lock file is written **last**: if dependency installation or the +build step fails, the partial install is removed and nothing is +recorded. .. _daspkg_global_modules: @@ -314,7 +357,7 @@ A package can exist both locally and globally. The C++ runtime - If the same module directory exists in both ``{das_root}/modules/`` and ``{project_root}/modules/``, the **local version wins**. - A warning is printed: - ``Warning: local 'dasVulkan' shadows global -- using local`` + ``Warning: local 'dasVulkan' shadows global - using local`` - This is safe -- removing the local copy seamlessly falls back to the global one. @@ -507,10 +550,11 @@ Package with dependencies } -SDK-aware version resolution ------------------------------ +Version aliases +--------------- -A package that ships different versions for different SDK releases: +A package that maps loose version requests (``latest``, ``1.x``) onto +concrete tags: .. code-block:: das @@ -601,8 +645,9 @@ Three version axes: - **Package version** -- semver of the package itself. - **daslang version** -- which SDK release the package is compatible - with. The ``resolve()`` function receives ``sdk_version`` and can - return different tags for different SDK versions. + with. Declared with ``package_min_sdk()`` and carried in the index; + ``resolve()`` reserves an ``sdk_version`` parameter for branching on + it, but daspkg passes an empty string there today. - **Dependencies** -- other packages with their own version constraints. diff --git a/doc/source/stdlib/handmade/module-algorithm.rst b/doc/source/stdlib/handmade/module-algorithm.rst index a558b52b45..a2a26c9a93 100644 --- a/doc/source/stdlib/handmade/module-algorithm.rst +++ b/doc/source/stdlib/handmade/module-algorithm.rst @@ -23,17 +23,17 @@ Example: require daslib/algorithm - [export] - def main() { - var arr <- [3, 1, 4, 1, 5, 9, 2, 6, 5] - sort_unique(arr) - print("sort_unique: {arr}\n") - print("has 4: {binary_search(arr, 4)}\n") - print("has 7: {binary_search(arr, 7)}\n") - print("lower_bound(4): {lower_bound(arr, 4)}\n") - print("upper_bound(4): {upper_bound(arr, 4)}\n") - let er = equal_range(arr, 5) - print("equal_range(5): {er}\n") - print("min index: {min_element(arr)}\n") - print("is_sorted: {is_sorted(arr)}\n") - } + [export] + def main() { + var arr <- [3, 1, 4, 1, 5, 9, 2, 6, 5] + sort_unique(arr) + print("sort_unique: {arr}\n") + print("has 4: {binary_search(arr, 4)}\n") + print("has 7: {binary_search(arr, 7)}\n") + print("lower_bound(4): {lower_bound(arr, 4)}\n") + print("upper_bound(4): {upper_bound(arr, 4)}\n") + let er = equal_range(arr, 5) + print("equal_range(5): {er}\n") + print("min index: {min_element(arr)}\n") + print("is_sorted: {is_sorted(arr)}\n") + } diff --git a/doc/source/stdlib/handmade/module-apply.rst b/doc/source/stdlib/handmade/module-apply.rst index 482531f36f..6f8ea948e1 100644 --- a/doc/source/stdlib/handmade/module-apply.rst +++ b/doc/source/stdlib/handmade/module-apply.rst @@ -15,23 +15,23 @@ Example: require daslib/apply - struct Foo { - a : int - b : float - c : string + struct Foo { + a : int + b : float + c : string + } + + [export] + def main() { + var foo = Foo(a = 42, b = 3.14, c = "hello") + apply(foo) $(name, field) { + print("{name} = {field}\n") } - - [export] - def main() { - var foo = Foo(a = 42, b = 3.14, c = "hello") - apply(foo) $(name, field) { - print("{name} = {field}\n") - } - } - // output: - // a = 42 - // b = 3.14 - // c = hello + } + // output: + // a = 42 + // b = 3.14 + // c = hello When the block has no function-escaping ``return``, it runs inline once per field — no helper function and no per-field block invoke — so it is cheap enough for hot paths like serialization. A block that diff --git a/doc/source/stdlib/handmade/module-archive.rst b/doc/source/stdlib/handmade/module-archive.rst index bcea2a0e7e..10e863423d 100644 --- a/doc/source/stdlib/handmade/module-archive.rst +++ b/doc/source/stdlib/handmade/module-archive.rst @@ -15,12 +15,19 @@ For example this is how DECS implements component serialization: .. code-block:: das - def public serialize ( var arch:Archive; var src:Component ) + require daslib/decs + + def public serialize(var arch : Archive; var src : Component) { arch |> serialize(src.name) arch |> serialize(src.hash) arch |> serialize(src.stride) arch |> serialize(src.info) - invoke(src.info.serializer, arch, src.data) + if (src.info.serializer != null) { + invoke(src.info.serializer, arch, src.data, src.name) + } else { + panic("decs: unable to serialize component '{src.name}'") + } + } Example: @@ -28,19 +35,19 @@ Example: require daslib/archive - struct Foo { - a : float - b : string - } - - [export] - def main() { - var original = Foo(a = 3.14, b = "hello") - var data <- mem_archive_save(original) - var loaded : Foo - data |> mem_archive_load(loaded) - delete data - print("a = {loaded.a}, b = {loaded.b}\n") - } - // output: - // a = 3.14, b = hello + struct Foo { + a : float + b : string + } + + [export] + def main() { + var original = Foo(a = 3.14, b = "hello") + var data <- mem_archive_save(original) + var loaded : Foo + data |> mem_archive_load(loaded) + delete data + print("a = {loaded.a}, b = {loaded.b}\n") + } + // output: + // a = 3.14, b = hello diff --git a/doc/source/stdlib/handmade/module-base64.rst b/doc/source/stdlib/handmade/module-base64.rst index 526c8231d6..c6f49c1598 100644 --- a/doc/source/stdlib/handmade/module-base64.rst +++ b/doc/source/stdlib/handmade/module-base64.rst @@ -14,13 +14,13 @@ Example: require daslib/base64 - [export] - def main() { - let encoded = base64_encode("Hello, daslang!") - print("encoded: {encoded}\n") - let decoded = base64_decode(encoded) - print("decoded: {decoded.text}\n") - } - // output: - // encoded: SGVsbG8sIGRhU2NyaXB0IQ== - // decoded: Hello, daslang! + [export] + def main() { + let encoded = base64_encode("Hello, daslang!") + print("encoded: {encoded}\n") + let decoded = base64_decode(encoded) + print("decoded: {decoded.text}\n") + } + // output: + // encoded: SGVsbG8sIGRhc2xhbmch + // decoded: Hello, daslang! diff --git a/doc/source/stdlib/handmade/module-contracts.rst b/doc/source/stdlib/handmade/module-contracts.rst index b0e9720103..fcf5b754bc 100644 --- a/doc/source/stdlib/handmade/module-contracts.rst +++ b/doc/source/stdlib/handmade/module-contracts.rst @@ -17,22 +17,22 @@ Example: require daslib/contracts - [!expect_dim(a)] - def process(a) { - return "scalar" - } - - [expect_dim(a)] - def process(a) { - return "array" - } - - [export] - def main() { - var arr : int[3] - print("{process(42)}\n") - print("{process(arr)}\n") - } - // output: - // scalar - // array + [expect_any_numeric(a)] + def process(a) { + return "scalar" + } + + [expect_any_array(a)] + def process(a) { + return "array" + } + + [export] + def main() { + var arr : int[3] + print("{process(42)}\n") + print("{process(arr)}\n") + } + // output: + // scalar + // array diff --git a/doc/source/stdlib/handmade/module-coroutines.rst b/doc/source/stdlib/handmade/module-coroutines.rst index ee278f6087..dc76ce89bd 100644 --- a/doc/source/stdlib/handmade/module-coroutines.rst +++ b/doc/source/stdlib/handmade/module-coroutines.rst @@ -18,29 +18,29 @@ Example: require daslib/coroutines - [coroutine] - def fibonacci() : int { - var a = 0 - var b = 1 - while (true) { - yield a - let next = a + b - a = b - b = next - } + [coroutine] + def fibonacci() : int { + var a = 0 + var b = 1 + while (true) { + yield a + let next = a + b + a = b + b = next } - - [export] - def main() { - var count = 0 - for (n in fibonacci()) { - print("{n} ") - count ++ - if (count >= 10) { - break - } + } + + [export] + def main() { + var count = 0 + for (n in fibonacci()) { + print("{n} ") + count ++ + if (count >= 10) { + break } - print("\n") } - // output: - // 0 1 1 2 3 5 8 13 21 34 + print("\n") + } + // output: + // 0 1 1 2 3 5 8 13 21 34 diff --git a/doc/source/stdlib/handmade/module-decs_boost.rst b/doc/source/stdlib/handmade/module-decs_boost.rst index 16fa202d5f..1cfb93f61c 100644 --- a/doc/source/stdlib/handmade/module-decs_boost.rst +++ b/doc/source/stdlib/handmade/module-decs_boost.rst @@ -16,19 +16,19 @@ Example: .. code-block:: das options persistent_heap = true - require daslib/decs_boost - - [export] - def main() { - restart() - create_entity() @(eid, cmp) { - cmp |> set("pos", float3(1, 2, 3)) - cmp |> set("name", "hero") - } - commit() - query() $(pos : float3; name : string) { - print("{name} at {pos}\n") - } + require daslib/decs_boost + + [export] + def main() { + restart() + create_entity() @(eid, cmp) { + cmp |> set("pos", float3(1, 2, 3)) + cmp |> set("name", "hero") + } + commit() + query() $(pos : float3; name : string) { + print("{name} at {pos}\n") } - // output: - // hero at 1,2,3 + } + // output: + // hero at 1,2,3 diff --git a/doc/source/stdlib/handmade/module-defer.rst b/doc/source/stdlib/handmade/module-defer.rst index 78d3f9d583..30d89f7bb2 100644 --- a/doc/source/stdlib/handmade/module-defer.rst +++ b/doc/source/stdlib/handmade/module-defer.rst @@ -14,15 +14,15 @@ Example: require daslib/defer - [export] - def main() { - print("start\n") - defer() { - print("cleanup runs last\n") - } - print("middle\n") + [export] + def main() { + print("start\n") + defer() { + print("cleanup runs last\n") } - // output: - // start - // middle - // cleanup runs last + print("middle\n") + } + // output: + // start + // middle + // cleanup runs last diff --git a/doc/source/stdlib/handmade/module-enum_trait.rst b/doc/source/stdlib/handmade/module-enum_trait.rst index a3ba0e2702..6fe04b405e 100644 --- a/doc/source/stdlib/handmade/module-enum_trait.rst +++ b/doc/source/stdlib/handmade/module-enum_trait.rst @@ -15,21 +15,21 @@ Example: require daslib/enum_trait - enum Color { - red - green - blue - } - - [export] - def main() { - print("{Color.green}\n") - let c = to_enum(type, "blue") - print("{c}\n") - let bad = to_enum(type, "purple", Color.red) - print("fallback = {bad}\n") - } - // output: - // green - // blue - // fallback = red + enum Color { + red + green + blue + } + + [export] + def main() { + print("{Color.green}\n") + let c = to_enum(type, "blue") + print("{c}\n") + let bad = to_enum(type, "purple", Color.red) + print("fallback = {bad}\n") + } + // output: + // green + // blue + // fallback = red diff --git a/doc/source/stdlib/handmade/module-fio.rst b/doc/source/stdlib/handmade/module-fio.rst index 4966f8be47..cf85a91ef9 100644 --- a/doc/source/stdlib/handmade/module-fio.rst +++ b/doc/source/stdlib/handmade/module-fio.rst @@ -1,7 +1,7 @@ The FIO module implements file input/output and filesystem operations. It provides functions for reading and writing files (``fopen``, ``fread``, ``fwrite``), -directory management (``mkdir``, ``dir``), path manipulation (``join_path``, -``basename``, ``dirname``), and file metadata queries (``stat``, ``file_time``). +directory management (``mkdir``, ``dir``), path manipulation (``path_join``, +``base_name``, ``dir_name``), and file metadata queries (``stat``, ``file_size``). All functions and symbols are in "fio" module, use require to get access to it. @@ -15,20 +15,20 @@ Example: require daslib/fio - [export] - def main() { - let fname = "_test_fio_tmp.txt" - fopen(fname, "wb") $(f) { - fwrite(f, "hello, daslang!") - } - fopen(fname, "rb") $(f) { - let content = fread(f) - print("{content}\n") - } - remove(fname) + [export] + def main() { + let fname = "_test_fio_tmp.txt" + fopen(fname, "wb") $(f) { + fwrite(f, "hello, daslang!") + } + fopen(fname, "rb") $(f) { + let content = fread(f) + print("{content}\n") } - // output: - // hello, daslang! + remove(fname) + } + // output: + // hello, daslang! Glob and pattern matching ------------------------- @@ -53,6 +53,8 @@ CLI tools that take a user-supplied pattern or a comma/newline-separated list of directories, and globs into a flat list of paths. Strips whitespace, skips empty entries, literal entries pass through, glob entries go through ``expand_glob``. Also **appends** to ``result``. +.. das-doc: fresh + .. code-block:: das require daslib/fio diff --git a/doc/source/stdlib/handmade/module-flat_hash_table.rst b/doc/source/stdlib/handmade/module-flat_hash_table.rst index 5a65a701d4..7b02bac56e 100644 --- a/doc/source/stdlib/handmade/module-flat_hash_table.rst +++ b/doc/source/stdlib/handmade/module-flat_hash_table.rst @@ -1,6 +1,6 @@ -The FLAT_HASH_TABLE module implements a flat (open addressing) hash table. -It stores all entries in a single contiguous array, providing cache-friendly -access patterns and good performance for small to medium-sized tables. +The FLAT_HASH_TABLE module implements a flat hash table — open addressing with +linear probing. ``TFlatHashTable`` keeps keys, hashes, and values in parallel +arrays, which is cache-friendly and performs well for small to medium tables. All functions and symbols are in "flat_hash_table" module, use require to get access to it. @@ -14,20 +14,20 @@ Example: require daslib/flat_hash_table public - typedef IntMap = TFlatHashTable - - [export] - def main() { - var m <- IntMap() - m[1] = "one" - m[2] = "two" - m[3] = "three" - print("length = {m.data_length}\n") - print("m[2] = {m[2]}\n") - m.clear() - print("after clear: {m.data_length}\n") - } - // output: - // length = 3 - // m[2] = two - // after clear: 0 + typedef IntMap = TFlatHashTable + + [export] + def main() { + var m <- IntMap() + m[1] = "one" + m[2] = "two" + m[3] = "three" + print("length = {m.data_length}\n") + print("m[2] = {m[2]}\n") + m.clear() + print("after clear: {m.data_length}\n") + } + // output: + // length = 3 + // m[2] = two + // after clear: 0 diff --git a/doc/source/stdlib/handmade/module-functional.rst b/doc/source/stdlib/handmade/module-functional.rst index ab205b5313..258b9010ac 100644 --- a/doc/source/stdlib/handmade/module-functional.rst +++ b/doc/source/stdlib/handmade/module-functional.rst @@ -19,14 +19,14 @@ Example: require daslib/functional - [export] - def main() { - var src <- [iterator for (x in range(6)); x] - var evens <- filter(src, @(x : int) : bool { return x % 2 == 0; }) - for (v in evens) { - print("{v} ") - } - print("\n") + [export] + def main() { + var src <- [iterator for (x in range(6)); x] + var evens <- filter(src, @(x : int) : bool { return x % 2 == 0; }) + for (v in evens) { + print("{v} ") } - // output: - // 0 2 4 + print("\n") + } + // output: + // 0 2 4 diff --git a/doc/source/stdlib/handmade/module-interfaces.rst b/doc/source/stdlib/handmade/module-interfaces.rst index 112d06d3ea..c9ccb5e325 100644 --- a/doc/source/stdlib/handmade/module-interfaces.rst +++ b/doc/source/stdlib/handmade/module-interfaces.rst @@ -7,7 +7,7 @@ dispatch without class inheritance. - Interface inheritance (``class IChild : IParent``) - Default method implementations (non-abstract methods) -- Compile-time completeness checking (error 30111 on missing methods) +- Compile-time completeness checking (error 30926 on missing methods) - ``is``/``as``/``?as`` operators via the ``InterfaceAsIs`` variant macro - Const-only interfaces — when all methods are ``def const``, ``as``/``?as``/``is`` work on const pointers diff --git a/doc/source/stdlib/handmade/module-jobque.rst b/doc/source/stdlib/handmade/module-jobque.rst index a82b444970..32ecdfb364 100644 --- a/doc/source/stdlib/handmade/module-jobque.rst +++ b/doc/source/stdlib/handmade/module-jobque.rst @@ -1,7 +1,8 @@ The JOBQUE module provides low-level job queue and threading primitives. -It includes thread-safe channels for inter-thread communication, lock boxes -for shared data access, job status tracking, and fine-grained thread -management. For higher-level job abstractions, see ``jobque_boost``. +It includes thread-safe ``Channel`` and ``Stream`` types for inter-thread +communication, ``LockBox`` for shared data access, ``Atomic32`` / ``Atomic64`` +counters, ``JobStatus`` tracking, and fine-grained thread management. For +higher-level job abstractions, see ``jobque_boost``. See :ref:`tutorial_jobque` for a hands-on tutorial. @@ -17,18 +18,18 @@ Example: require jobque - [export] - def main() { - with_atomic32() $(counter) { - counter |> set(10) - print("value = {counter |> get}\n") - let after_inc = counter |> inc - print("after inc = {after_inc}\n") - let after_dec = counter |> dec - print("after dec = {after_dec}\n") - } + [export] + def main() { + with_atomic32() $(counter) { + counter |> set(10) + print("value = {counter |> get}\n") + let after_inc = counter |> inc + print("after inc = {after_inc}\n") + let after_dec = counter |> dec + print("after dec = {after_dec}\n") } - // output: - // value = 10 - // after inc = 11 - // after dec = 10 + } + // output: + // value = 10 + // after inc = 11 + // after dec = 10 diff --git a/doc/source/stdlib/handmade/module-jobque_boost.rst b/doc/source/stdlib/handmade/module-jobque_boost.rst index 03a11d345d..d260135b29 100644 --- a/doc/source/stdlib/handmade/module-jobque_boost.rst +++ b/doc/source/stdlib/handmade/module-jobque_boost.rst @@ -1,6 +1,10 @@ The JOBQUE_BOOST module provides high-level job queue abstractions built on -the low-level ``jobque`` primitives. It includes ``with_job``, ``with_job_status``, -and channel-based patterns for simplified concurrent programming. +the low-level ``jobque`` primitives: ``new_job`` / ``new_thread`` (which capture +a lambda and clone the context for the worker), ``with_wait_group`` / ``done``, +``parallel_for`` and the ``team_parallel_*`` family, and typed ``push`` / +``pop`` / ``gather`` over a ``Stream``. It requires ``jobque`` publicly, so the +builtins — ``with_job_que``, ``with_job_status``, ``Channel``, ``LockBox`` — +are visible through it as well. See also :doc:`jobque` for the low-level job queue primitives. See :ref:`tutorial_jobque` for a hands-on tutorial. @@ -17,17 +21,17 @@ Example: require daslib/jobque_boost - [export] - def main() { - with_job_status(1) $(status) { - new_thread() @() { - print("from thread\n") - status |> notify_and_release() - } - status |> join() - print("thread done\n") + [export] + def main() { + with_job_status(1) $(status) { + new_thread() @() { + print("from thread\n") + status |> notify_and_release() } + status |> join() + print("thread done\n") } - // output: - // from thread - // thread done + } + // output: + // from thread + // thread done diff --git a/doc/source/stdlib/handmade/module-json.rst b/doc/source/stdlib/handmade/module-json.rst index 9602eb4421..a1ce0c5ad4 100644 --- a/doc/source/stdlib/handmade/module-json.rst +++ b/doc/source/stdlib/handmade/module-json.rst @@ -1,7 +1,8 @@ The JSON module implements JSON parsing and serialization. It provides ``read_json`` for parsing JSON text into a ``JsonValue`` tree, ``write_json`` for serializing back to text, and ``JV`` helpers for constructing -JSON values from daslang types. +JSON values from daslang types. ``write_json`` pretty-prints (one element per +line, tab-indented); ``write_json_compact`` emits the same tree on a single line. See also :doc:`json_boost` for automatic struct-to-JSON conversion and the ``%json~`` reader macro. See :ref:`tutorial_json` for a hands-on tutorial. @@ -18,15 +19,21 @@ Example: require daslib/json - [export] - def main() { - let data = "[1, 2, 3]" - var error = "" - var js <- read_json(data, error) - print("json: {write_json(js)}\n") - unsafe { - delete js - } + [export] + def main() { + let data = "[1, 2, 3]" + var error = "" + var js <- read_json(data, error) + print("compact: {write_json_compact(js)}\n") + print("pretty: {write_json(js)}\n") + unsafe { + delete js } - // output: - // json: [1,2,3] + } + // output: + // compact: [1, 2, 3] + // pretty: [ + // 1, + // 2, + // 3 + // ] diff --git a/doc/source/stdlib/handmade/module-json_boost.rst b/doc/source/stdlib/handmade/module-json_boost.rst index c172a168a5..79be79742d 100644 --- a/doc/source/stdlib/handmade/module-json_boost.rst +++ b/doc/source/stdlib/handmade/module-json_boost.rst @@ -1,6 +1,9 @@ The JSON_BOOST module extends JSON support with operator overloads for convenient -field access (``?[]``), null-coalescing (``??``), and automatic struct-to-JSON -conversion macros (``from_JsValue``, ``to_JsValue``). +field access (``?.`` / ``?[]``), null-coalescing (``??``), and generic +conversions in both directions: ``JV(value)`` builds a ``JsonValue?`` tree from +a struct, tuple, array, table, or vector, and ``from_JV(js, default)`` reads one +back into a typed value. Field annotations on the struct steer both directions +(see below), and the builtin ``sprint_json`` honours the same annotations. See also :doc:`json` for core JSON parsing and writing. See :ref:`tutorial_json` for a hands-on tutorial. @@ -17,24 +20,24 @@ Example: require daslib/json_boost - [export] - def main() { - let data = "\{ \"name\": \"Alice\", \"age\": 30 \}" - var error = "" - var js <- read_json(data, error) - if (error == "") { - let name = js?.name ?? "?" - print("name = {name}\n") - let age = js?.age ?? -1 - print("age = {age}\n") - } - unsafe { - delete js - } + [export] + def main() { + let data = "\{ \"name\": \"Alice\", \"age\": 30 \}" + var error = "" + var js <- read_json(data, error) + if (error == "") { + let name = js?.name ?? "?" + print("name = {name}\n") + let age = js?.age ?? -1 + print("age = {age}\n") } - // output: - // name = Alice - // age = 30 + unsafe { + delete js + } + } + // output: + // name = Alice + // age = 30 Field annotations ----------------- @@ -45,8 +48,9 @@ parsed by :ref:`parse_json_annotation ` and stored in a ``static_let`` cache so each field is parsed only once. -``sprint_json`` requires ``options rtti`` for annotations to take effect at -runtime. +``sprint_json`` reads the same annotations from the runtime ``TypeInfo`` of the +value it is handed (``src/simulate/json_print.cpp``), so they apply with no extra +module options. .. list-table:: :header-rows: 1 @@ -59,9 +63,10 @@ runtime. empty string, empty array, empty table, null pointer). * - ``@rename="json_key"`` - Use *json_key* instead of the daslang field name in JSON output and - when looking up keys during ``from_JV`` deserialization. The annotation - value must be a string (``@rename="name"``). A bare ``@rename`` with - no string value is silently ignored. + when looking up keys during ``from_JV`` deserialization. A bare + ``@rename`` with no string value strips one leading underscore from the + field name — ``_type`` is written as ``"type"`` — and does nothing to a + name that does not start with ``_``. * - ``@embed`` - Treat a ``string`` field as raw JSON — embed it without extra quoting. During ``JV`` conversion the string is parsed with ``read_json`` and @@ -75,9 +80,16 @@ runtime. Example with ``sprint_json``: +.. das-doc: fresh + .. code-block:: das - options rtti + require daslib/json_boost + + enum Priority { + low + high + } struct Config { name : string @@ -88,6 +100,13 @@ Example with ``sprint_json``: @enum_as_int level : Priority // integer, not string } - let json_str = sprint_json(cfg, false) + [export] + def main() { + let cfg = Config(name = "app", _type = "service", raw = "[1,2]", + path = "c:\\tmp", level = Priority.high) + print("{sprint_json(cfg, false)}\n") + } + // output: + // {"name":"app","type":"service","raw":[1,2],"path":"c:\tmp","level":1} See :ref:`tutorial_json` for runnable examples of every annotation. diff --git a/doc/source/stdlib/handmade/module-linq.rst b/doc/source/stdlib/handmade/module-linq.rst index 4734ce2263..a4da0574b3 100644 --- a/doc/source/stdlib/handmade/module-linq.rst +++ b/doc/source/stdlib/handmade/module-linq.rst @@ -18,14 +18,14 @@ Example: require daslib/linq - [export] - def main() { - var src <- [iterator for (x in range(10)); x] - var evens <- where_(src, $(x : int) : bool { return x % 2 == 0; }) - for (v in evens) { - print("{v} ") - } - print("\n") + [export] + def main() { + var src <- [iterator for (x in range(10)); x] + var evens <- where_(src, $(x : int) : bool { return x % 2 == 0; }) + for (v in evens) { + print("{v} ") } - // output: - // 0 2 4 6 8 + print("\n") + } + // output: + // 0 2 4 6 8 diff --git a/doc/source/stdlib/handmade/module-linq_boost.rst b/doc/source/stdlib/handmade/module-linq_boost.rst index f56bf73a8d..866dc7e9e2 100644 --- a/doc/source/stdlib/handmade/module-linq_boost.rst +++ b/doc/source/stdlib/handmade/module-linq_boost.rst @@ -16,16 +16,16 @@ Example: .. code-block:: das require daslib/linq - require daslib/linq_boost - - [export] - def main() { - var src <- [iterator for (x in range(10)); x] - var evens <- _where(src, _ % 2 == 0) - for (v in evens) { - print("{v} ") - } - print("\n") + require daslib/linq_boost + + [export] + def main() { + var src <- [iterator for (x in range(10)); x] + var evens <- _where(src, _ % 2 == 0) + for (v in evens) { + print("{v} ") } - // output: - // 0 2 4 6 8 + print("\n") + } + // output: + // 0 2 4 6 8 diff --git a/doc/source/stdlib/handmade/module-lpipe.rst b/doc/source/stdlib/handmade/module-lpipe.rst index edf9199ffd..e96497e11f 100644 --- a/doc/source/stdlib/handmade/module-lpipe.rst +++ b/doc/source/stdlib/handmade/module-lpipe.rst @@ -14,20 +14,20 @@ Example: require daslib/lpipe - def take2(a, b : block) { - invoke(a) - invoke(b) + def take2(a, b : block) { + invoke(a) + invoke(b) + } + + [export] + def main() { + take2() { + print("first\n") } - - [export] - def main() { - take2() { - print("first\n") - } - lpipe() { - print("second\n") - } + lpipe() { + print("second\n") } - // output: - // first - // second + } + // output: + // first + // second diff --git a/doc/source/stdlib/handmade/module-match.rst b/doc/source/stdlib/handmade/module-match.rst index f30a548bc0..cdf604228d 100644 --- a/doc/source/stdlib/handmade/module-match.rst +++ b/doc/source/stdlib/handmade/module-match.rst @@ -1,7 +1,15 @@ The MATCH module implements pattern matching on variants, structs, tuples, -arrays, and scalar values. Supports variable capture, wildcards, guard -expressions, and alternation. ``static_match`` enforces exhaustive matching -at compile time. +arrays, and scalar values. Supports variable capture (``$v(name)``), wildcards +(``_``), guard expressions (``&&``), and alternation (``||``). + +``match`` is a **statement**, not an expression — write the arms so each one +assigns or returns, rather than expecting the ``match`` itself to produce a value. +Arms are tried in source order and the first one that matches wins; a pattern +that cannot apply to the subject type is a compile error. ``static_match`` +drops such arms silently instead of erroring, which is what makes it usable in +generic code where only some arms apply per instantiation. +``multi_match`` / ``static_multi_match`` run **every** matching arm instead of +stopping at the first. See :ref:`tutorial_pattern_matching` for a hands-on tutorial. @@ -17,28 +25,28 @@ Example: require daslib/match - enum Color { - red - green - blue + enum Color { + red + green + blue + } + + def describe(c : Color) : string { + match (c) { + if (Color.red) { return "red"; } + if (Color.green) { return "green"; } + if (_) { return "other"; } } - - def describe(c : Color) : string { - match (c) { - if (Color.red) { return "red"; } - if (Color.green) { return "green"; } - if (_) { return "other"; } - } - return "?" - } - - [export] - def main() { - print("{describe(Color.red)}\n") - print("{describe(Color.green)}\n") - print("{describe(Color.blue)}\n") - } - // output: - // red - // green - // other + return "?" + } + + [export] + def main() { + print("{describe(Color.red)}\n") + print("{describe(Color.green)}\n") + print("{describe(Color.blue)}\n") + } + // output: + // red + // green + // other diff --git a/doc/source/stdlib/handmade/module-math.rst b/doc/source/stdlib/handmade/module-math.rst index 53884790d1..4043d02432 100644 --- a/doc/source/stdlib/handmade/module-math.rst +++ b/doc/source/stdlib/handmade/module-math.rst @@ -28,28 +28,28 @@ Example: require math - [export] - def main() { - print("sin(PI/2) = {sin(PI / 2.0)}\n") - print("cos(0) = {cos(0.0)}\n") - print("sqrt(16) = {sqrt(16.0)}\n") - print("abs(-5) = {abs(-5)}\n") - print("clamp(15, 0, 10) = {clamp(15, 0, 10)}\n") - print("min(3, 7) = {min(3, 7)}\n") - print("max(3, 7) = {max(3, 7)}\n") - print("tanh(1) = {tanh(1.0)}\n") - print("log10(1000) = {log10(1000.0)}\n") - let v = float3(1, 0, 0) - print("length = {length(v)}\n") - } - // output: - // sin(PI/2) = 1 - // cos(0) = 1 - // sqrt(16) = 4 - // abs(-5) = 5 - // clamp(15, 0, 10) = 10 - // min(3, 7) = 3 - // max(3, 7) = 7 - // tanh(1) = 0.7615942 - // log10(1000) = 3 - // length = 1 + [export] + def main() { + print("sin(PI/2) = {sin(PI / 2.0)}\n") + print("cos(0) = {cos(0.0)}\n") + print("sqrt(16) = {sqrt(16.0)}\n") + print("abs(-5) = {abs(-5)}\n") + print("clamp(15, 0, 10) = {clamp(15, 0, 10)}\n") + print("min(3, 7) = {min(3, 7)}\n") + print("max(3, 7) = {max(3, 7)}\n") + print("tanh(1) = {tanh(1.0)}\n") + print("log10(1000) = {log10(1000.0)}\n") + let v = float3(1, 0, 0) + print("length = {length(v)}\n") + } + // output: + // sin(PI/2) = 1 + // cos(0) = 1 + // sqrt(16) = 4 + // abs(-5) = 5 + // clamp(15, 0, 10) = 10 + // min(3, 7) = 3 + // max(3, 7) = 7 + // tanh(1) = 0.7615942 + // log10(1000) = 3 + // length = 1 diff --git a/doc/source/stdlib/handmade/module-math_bits.rst b/doc/source/stdlib/handmade/module-math_bits.rst index 4d7f2ec49b..51532b0794 100644 --- a/doc/source/stdlib/handmade/module-math_bits.rst +++ b/doc/source/stdlib/handmade/module-math_bits.rst @@ -1,7 +1,8 @@ -The MATH_BITS module provides bit manipulation functions for floating point -numbers, including type punning between integer and float representations, -and efficient integer math operations like ``int_bits_to_float`` and -``float_bits_to_int``. +The MATH_BITS module provides bit-level reinterpretation between integer and +floating point representations — ``int_bits_to_float``, ``uint_bits_to_float``, +``float_bits_to_int``, ``float_bits_to_uint`` (plus the 64-bit ``double`` +forms and 2/3/4-lane vector overloads) — as well as the ``cast_to_*`` helpers +that pack and unpack values through a ``float4`` payload. All functions and symbols are in "math_bits" module, use require to get access to it. @@ -15,13 +16,13 @@ Example: require daslib/math_bits - [export] - def main() { - let f = uint_bits_to_float(0x3F800000u) - print("uint_bits_to_float(0x3F800000) = {f}\n") - let back = float_bits_to_uint(1.0) - print("float_bits_to_uint(1.0) = {back}\n") - } - // output: - // uint_bits_to_float(0x3F800000) = 1 - // float_bits_to_uint(1.0) = 0x3f800000 + [export] + def main() { + let f = uint_bits_to_float(0x3F800000u) + print("uint_bits_to_float(0x3F800000) = {f}\n") + let back = float_bits_to_uint(1.0) + print("float_bits_to_uint(1.0) = {back}\n") + } + // output: + // uint_bits_to_float(0x3F800000) = 1 + // float_bits_to_uint(1.0) = 0x3f800000 diff --git a/doc/source/stdlib/handmade/module-math_boost.rst b/doc/source/stdlib/handmade/module-math_boost.rst index 780a4f0a4b..44dd680a1e 100644 --- a/doc/source/stdlib/handmade/module-math_boost.rst +++ b/doc/source/stdlib/handmade/module-math_boost.rst @@ -1,7 +1,11 @@ The MATH_BOOST module adds geometric types (``AABB``, ``AABR``, ``Ray``), -angle conversion (``degrees``, ``radians``), intersection tests, color space -conversion (``linear_to_SRGB``, ``RGBA_TO_UCOLOR``), and view/projection -matrix construction (``look_at_lh``, ``perspective_rh``). +intersection tests (``is_intersecting``), plane helpers (``plane_dot``, +``plane_normalize``, ``plane_from_point_normal``, ``planar_shadow``), color +space conversion (``linear_to_SRGB``, ``RGBA_TO_UCOLOR``, ``UCOLOR_TO_RGBA``), +and view/projection matrix construction (``look_at_lh``, ``look_at_rh``, +``perspective_rh``, ``ortho_rh``). It requires ``math`` publicly, so requiring +``math_boost`` also brings in the whole scalar/vector math surface +(``degrees``, ``radians``, ``sin``, ``length``, ...). All functions and symbols are in "math_boost" module, use require to get access to it. @@ -15,14 +19,18 @@ Example: require daslib/math_boost - [export] - def main() { - print("degrees(PI) = {degrees(PI)}\n") - print("radians(180) = {radians(180.0)}\n") - var box = AABB(min = float3(0), max = float3(10)) - print("box = ({box.min}) - ({box.max})\n") - } - // output: - // degrees(PI) = 180 - // radians(180) = 3.1415927 - // box = (0,0,0) - (10,10,10) + [export] + def main() { + let box = AABB(min = float3(0), max = float3(10)) + let other = AABB(min = float3(5), max = float3(15)) + print("boxes intersect = {is_intersecting(box, other)}\n") + let ray = Ray(origin = float3(-1, 5, 5), dir = float3(1, 0, 0)) + print("ray hits box = {is_intersecting(ray, box, 0.0, 100.0)}\n") + print("linear_to_SRGB(0.5) = {linear_to_SRGB(0.5)}\n") + print("RGBA_TO_UCOLOR(red) = {RGBA_TO_UCOLOR(1.0, 0.0, 0.0, 1.0)}\n") + } + // output: + // boxes intersect = true + // ray hits box = true + // linear_to_SRGB(0.5) = 0.73535705 + // RGBA_TO_UCOLOR(red) = 0xff0000ff diff --git a/doc/source/stdlib/handmade/module-openai_common.rst b/doc/source/stdlib/handmade/module-openai_common.rst index 449359a46a..f6e37bd1e8 100644 --- a/doc/source/stdlib/handmade/module-openai_common.rst +++ b/doc/source/stdlib/handmade/module-openai_common.rst @@ -27,8 +27,9 @@ Every consumer root MUST set ``options rtti`` — otherwise the ``@optional`` / options rtti require openai/openai_chat + require daslib/fio - let client = openai_client("https://api.openai.com/v1", get_env("OPENAI_API_KEY")) + let client = openai_client("https://api.openai.com/v1", get_env_variable("OPENAI_API_KEY")) print("{chat_text(client, "gpt-4o-mini", "Say hi in one sentence.")}\n") See :ref:`tutorial_dasOPENAI_first_chat` for a hands-on tutorial, or the rest of diff --git a/doc/source/stdlib/handmade/module-peg.rst b/doc/source/stdlib/handmade/module-peg.rst index 3228c69587..ec7494ee7b 100644 --- a/doc/source/stdlib/handmade/module-peg.rst +++ b/doc/source/stdlib/handmade/module-peg.rst @@ -112,25 +112,25 @@ Example: require peg/peg - def parse_greeting(input : string; - blk : block<(val : string; err : array) : void>) { - parse(input) { - var greeting : string - rule("Hello, ", "{+letter}" as name, "!", EOF) { - return name - } - var letter : void? - rule(set('a'..'z', 'A'..'Z')) { - return null - } + def parse_greeting(input : string; + blk : block<(val : string; err : array) : void>) { + parse(input) { + var greeting : string + rule("Hello, ", "{+letter}" as name, "!", EOF) { + return name + } + var letter : void? + rule(set('a'..'z', 'A'..'Z')) { + return null } } + } - [export] - def main() { - parse_greeting("Hello, World!") $(val; err) { - print("name = {val}\n") - } + [export] + def main() { + parse_greeting("Hello, World!") $(val; err) { + print("name = {val}\n") } - // output: - // name = World + } + // output: + // name = World diff --git a/doc/source/stdlib/handmade/module-random.rst b/doc/source/stdlib/handmade/module-random.rst index d44b22634b..3af3c4c657 100644 --- a/doc/source/stdlib/handmade/module-random.rst +++ b/doc/source/stdlib/handmade/module-random.rst @@ -15,14 +15,14 @@ Example: require daslib/random - [export] - def main() { - var seed = random_seed(12345) - print("int: {random_int(seed)}\n") - print("float: {random_float(seed)}\n") - print("float: {random_float(seed)}\n") - } - // output: - // int: 7584 - // float: 0.5848567 - // float: 0.78722495 + [export] + def main() { + var seed = random_seed(12345) + print("int: {random_int(seed)}\n") + print("float: {random_float(seed)}\n") + print("float: {random_float(seed)}\n") + } + // output: + // int: 7584 + // float: 0.5848567 + // float: 0.78722495 diff --git a/doc/source/stdlib/handmade/module-regex.rst b/doc/source/stdlib/handmade/module-regex.rst index 9857fe09c6..f6ab03efe3 100644 --- a/doc/source/stdlib/handmade/module-regex.rst +++ b/doc/source/stdlib/handmade/module-regex.rst @@ -66,20 +66,20 @@ Example: .. code-block:: das require daslib/regex - require strings - - [export] - def main() { - var re <- regex_compile("[0-9]+") - let m = regex_match(re, "123abc") - print("match length = {m}\n") - let text = "age 25, height 180" - regex_foreach(re, text) $(r) { - print("found: {slice(text, r.x, r.y)}\n") - return true - } + require strings + + [export] + def main() { + var re <- regex_compile("[0-9]+") + let m = regex_match(re, "123abc") + print("match length = {m}\n") + let text = "age 25, height 180" + regex_foreach(re, text) $(r) { + print("found: {slice(text, r.x, r.y)}\n") + return true } - // output: - // match length = 3 - // found: 25 - // found: 180 + } + // output: + // match length = 3 + // found: 25 + // found: 180 diff --git a/doc/source/stdlib/handmade/module-regex_boost.rst b/doc/source/stdlib/handmade/module-regex_boost.rst index 8ca48d63f1..edc95e3ee0 100644 --- a/doc/source/stdlib/handmade/module-regex_boost.rst +++ b/doc/source/stdlib/handmade/module-regex_boost.rst @@ -23,20 +23,20 @@ Example: .. code-block:: das require daslib/regex_boost - require strings - - [export] - def main() { - var inscope re <- %regex~\d+%% - let m = regex_match(re, "123abc") - print("match length = {m}\n") - let text = "age 25, height 180" - regex_foreach(re, text) $(r) { - print("found: {slice(text, r.x, r.y)}\n") - return true - } + require strings + + [export] + def main() { + var re <- %regex~\d+%% + let m = regex_match(re, "123abc") + print("match length = {m}\n") + let text = "age 25, height 180" + regex_foreach(re, text) $(r) { + print("found: {slice(text, r.x, r.y)}\n") + return true } - // output: - // match length = 3 - // found: 25 - // found: 180 + } + // output: + // match length = 3 + // found: 25 + // found: 180 diff --git a/doc/source/stdlib/handmade/module-static_let.rst b/doc/source/stdlib/handmade/module-static_let.rst index 56eb1dda65..bd49788e54 100644 --- a/doc/source/stdlib/handmade/module-static_let.rst +++ b/doc/source/stdlib/handmade/module-static_let.rst @@ -1,7 +1,9 @@ The STATIC_LET module implements the ``static_let`` pattern — local variables that persist across function calls, similar to C ``static`` variables. The -variable is initialized once on first call and retains its value in subsequent -invocations. +declaration is promoted to module scope under a mangled name, so it is +initialized once when the context starts (not lazily on first call) and retains +its value across calls. ``static_let_finalize`` additionally deletes the +variable on context shutdown. All functions and symbols are in "static_let" module, use require to get access to it. @@ -15,21 +17,21 @@ Example: require daslib/static_let - def counter() : int { - static_let() { - var count = 0 - } - count ++ - return count + def counter() : int { + static_let() { + var count = 0 } - - [export] - def main() { - print("{counter()}\n") - print("{counter()}\n") - print("{counter()}\n") - } - // output: - // 1 - // 2 - // 3 + count ++ + return count + } + + [export] + def main() { + print("{counter()}\n") + print("{counter()}\n") + print("{counter()}\n") + } + // output: + // 1 + // 2 + // 3 diff --git a/doc/source/stdlib/handmade/module-strings_boost.rst b/doc/source/stdlib/handmade/module-strings_boost.rst index f2a9424b25..f1d6dc4dd4 100644 --- a/doc/source/stdlib/handmade/module-strings_boost.rst +++ b/doc/source/stdlib/handmade/module-strings_boost.rst @@ -1,5 +1,7 @@ -The STRINGS_BOOST module extends string handling with splitting, joining, -padding, character replacement, and edit distance computation. +The STRINGS_BOOST module extends string handling with splitting (``split``, +``split_by_chars``), joining (``join``), padding (``wide``), multi-substring +replacement (``replace_multiple``), and edit distance (``levenshtein_distance``). +It re-exports ``strings``, so the built-in string surface is available too. All functions and symbols are in "strings_boost" module, use require to get access to it. @@ -13,16 +15,16 @@ Example: require daslib/strings_boost - [export] - def main() { - let parts = split("one,two,three", ",") - print("split: {parts}\n") - print("join: {join(parts, " | ")}\n") - print("[{wide("hello", 10)}]\n") - print("distance: {levenshtein_distance("kitten", "sitting")}\n") - } - // output: - // split: [[ one; two; three]] - // join: one | two | three - // [hello ] - // distance: 3 + [export] + def main() { + let parts = split("one,two,three", ",") + print("split: {parts}\n") + print("join: {join(parts, " | ")}\n") + print("[{wide("hello", 10)}]\n") + print("distance: {levenshtein_distance("kitten", "sitting")}\n") + } + // output: + // split: [ one, two, three] + // join: one | two | three + // [hello ] + // distance: 3 diff --git a/doc/source/stdlib/handmade/module-unroll.rst b/doc/source/stdlib/handmade/module-unroll.rst index 727fbd83b0..6a232542ec 100644 --- a/doc/source/stdlib/handmade/module-unroll.rst +++ b/doc/source/stdlib/handmade/module-unroll.rst @@ -14,16 +14,16 @@ Example: require daslib/unroll - [export] - def main() { - unroll() { - for (i in range(4)) { - print("step {i}\n") - } + [export] + def main() { + unroll() { + for (i in range(4)) { + print("step {i}\n") } } - // output: - // step 0 - // step 1 - // step 2 - // step 3 + } + // output: + // step 0 + // step 1 + // step 2 + // step 3 diff --git a/modules/dasImgui/examples/tutorial/state_telemetry.das b/modules/dasImgui/examples/tutorial/state_telemetry.das index 99a6f08049..d296f63bd3 100644 --- a/modules/dasImgui/examples/tutorial/state_telemetry.das +++ b/modules/dasImgui/examples/tutorial/state_telemetry.das @@ -89,7 +89,7 @@ def update() { // ---- Dotted flags ---- - // SPEED.PUBLIC: emit the global `variable public` (not the default private) + // SPEED.PUBLIC: emit the global `var public` (not the default private) // so requiring modules can read SPEED.value. Path stays "SPEED" — flags // don't leak into the registry path. diff --git a/modules/dasImgui/widgets/imgui_boost_runtime.das b/modules/dasImgui/widgets/imgui_boost_runtime.das index ab1f372c00..a75418bd7d 100644 --- a/modules/dasImgui/widgets/imgui_boost_runtime.das +++ b/modules/dasImgui/widgets/imgui_boost_runtime.das @@ -1079,7 +1079,7 @@ def private check_unique_render(path_key : string; kind : string) { // radio_button_int intentionally shares state across N per-frame calls; other kinds panic on duplicate render. return if (kind == "radio_button_int") if (key_exists(g_rendered_this_frame, path_key)) { - let msg = "dasImgui: widget '{path_key}' ({kind}) rendered twice in frame {g_frame}. Single-global widgets must render exactly once per frame. For multiple instances, declare a table variable private : table> and call with subscript: {kind}([i], (...))" + let msg = "dasImgui: widget '{path_key}' ({kind}) rendered twice in frame {g_frame}. Single-global widgets must render exactly once per frame. For multiple instances, declare a table var private : table> and call with subscript: {kind}([i], (...))" panic(msg) } g_rendered_this_frame |> insert(path_key) diff --git a/modules/dasImgui/widgets/imgui_boost_v2.das b/modules/dasImgui/widgets/imgui_boost_v2.das index c78ae7c3e7..1ee6f0d893 100644 --- a/modules/dasImgui/widgets/imgui_boost_v2.das +++ b/modules/dasImgui/widgets/imgui_boost_v2.das @@ -669,7 +669,7 @@ class WidgetCallMacro : AstCallMacro { var indexExpr : ExpressionPtr) : ExpressionPtr { let tableVar = find_variable(mod, bareName) if (tableVar == null) { - let msg = "{kind_name}({bareName}[k]): table global '{bareName}' is not declared at module scope. Declare it as: variable private {bareName} : table (or table for string keys)." + let msg = "{kind_name}({bareName}[k]): table global '{bareName}' is not declared at module scope. Declare it as: var private {bareName} : table (or table for string keys)." macro_error(prog, expr.at, msg) return <- default } diff --git a/plans/doc-sweep.md b/plans/doc-sweep.md new file mode 100644 index 0000000000..43a1991cf2 --- /dev/null +++ b/plans/doc-sweep.md @@ -0,0 +1,267 @@ +# 0.6.4 documentation sweep — authored RST verification + +Goal: every authored RST page (NOT das2rst-generated) carries code that compiles and prose +that matches the implementation, verified mechanically where possible, by agents where not — +and a nightly lane that keeps it that way. + +## Scope + +- `doc/source/reference/tutorials/**` (313 pages incl. imgui/macros/opengl subdirs) +- `doc/source/reference/language/` (42) +- `doc/source/reference/embedding/` (6) — cpp blocks, separate rail +- `doc/source/reference/utils/` (12) +- `doc/source/stdlib/handmade/**` — code blocks only (152 das blocks); prose corpus out of scope +- OUT: `doc/source/stdlib/generated/**` (das2rst output), `external_modules/` (0 das blocks) + +Census at plan time: ~2,260 `code-block:: das` + 152 `code-block:: cpp`. + +## Rule 0 — the checker binary carries the corpus + +Before any verdict is minted, the tool emits one require-probe per module family referenced +by the corpus and compiles each. Any probe failure = "binary-gap" bucket; the audit REFUSES +to report page verdicts while that bucket is non-empty. This catches both a lean binary and +a wrong require spelling — the dry-run produced false drift verdicts from both causes. + +## Mechanical model — page as literate program + +Each page's das blocks concatenate in order into one synthetic module: + +- `require`/`options`/`module` lines hoisted (deduped); preamble taken from the page's + companion `.das` (`tutorials//NN_x.das`, hyphen variant too) when one exists — + never from a hand table when a companion is present +- decl chunks (def/struct/class/enum/...) at module scope; stmt chunks appended in order + into one function so locals flow block-to-block +- rename-on-redeclare: daslang bans shadowing; a narrative re-declaration becomes `name__N` + from that point on +- class-redecl whose body STARTS with `...` merges into the original class (elision = + "previous members here"); other redecls rename as independent examples +- `...` in statement position → `pass` (indentation preserved); in decl-body position → merge/drop + +Markers — RST comments `.. das-doc: ` directly above a block; invisible in rendered +output, no Sphinx extension: + +| marker | meaning | +|---|---| +| `skip` | not code (wire formats, output dumps, pseudo-code) | +| `fresh` | start a new synthetic program at this block | +| `given ` | inject context the prose assumes (free vars, tiny base classes) | +| `signatures` | API-surface listing — compiler skips; agents verify against the real API | +| `expect error[NNNNN]` | block must FAIL to compile with that code (verified by appending to the page program and compiling) | + +## Predictions (filed 2026-08-10, BEFORE the full scan) + +1. 30–45% of pages red under auto mode (no markers yet); ~25% of red pages carry a genuine + doc bug, the rest need markers/context only. +2. Worst areas: `reference/language/` (gen1 remnants) and fast-moving module families + (clargs/Result-era tutorials, sql, macros). Best: numbered language tutorials with + compile-gated companions; `stdlib/handmade` ≥95% green. +3. ≥5 more gen1-syntax remnants tree-wide (first language page examined already had one). +4. ≥1 more page documenting an API that no longer exists in that shape (clargs pattern). +5. The 135 error-talk blocks: most convert to `expect error[NNNNN]` markers cleanly; ≤10% + quote error codes/messages that are themselves stale. + +Dry-run priors (6 pages): 3 green after harness fixes, 1 one-marker-away, 2 with genuine +bugs (53_clargs: string→Result drift + invalid one-line enum; classes.rst:275: gen1 +braceless `class sealed`). Born-wrong exists (embedding cpp_api.rst ManagedVectorAnnotation). + +## Phases + +1. **Tool** (`utils/doc-verify/`, das) — extractor, emitter, Rule-0 probes, compile driver + (fresh `bin/daslang -compile-only` per page, MCP-subtool isolation pattern), JSON+md report. +2. **Full mechanical scan** → red-list with buckets: genuine-bug / needs-marker / binary-gap. +3. **Mini Opus fan-out** (gate, Boris-mandated): one bounded family + calibration pages with + known findings; validates agent fix quality and marker discipline before scale. +4. **Full Opus fan-out** — fix pages, add markers, converge to green. Prose claims verified + per page against implementation (probe access), adversarial verify on prose findings. +5. **cpp-block rail** — one generated TU per embedding page compiled against headers. +6. **Nightly lane** — `verify_docs_and_examples`: doc-verify + /examples + /tutorials + compile/run/lint. NIGHTLY ONLY (regular per-PR cycle stays untouched). preflight mirror entry. + WIRED (2026-08-11): extended_checks.yml nightly cron + workflow_dispatch, posix cells, + step after "Run tutorial dry-runs". No build change was needed — the extended main build + already applies ci/release_modules.txt (dasHV et al. ON); the rest of the corpus modules + are default-enabled, and rule 0 verifies the full set on every run. preflight.md mirror + row added. The /examples+/tutorials lint-clean half of the lane remains its own + follow-up wave. +7. **Procedure doc** — `skills/doc_sweep.md` (repo-only), written LAST, once the procedure + survives the audit; each-release cadence, prose re-sweeps scoped to pages whose subject + changed since the last sweep tag. + +## Ledger + +- (2026-08-10) 🐞→✅ FIXED same day: temp-string reclaim + passthrough aliasing + use-after-free. `slice` is [temp_string_result]-flagged → reclaim queues its result; + `trim` with no trailing whitespace returns an INTERIOR POINTER into that argument typed + as a REGULAR string (module_builtin_string.cpp:713 → rtrim `return s`); queue advances → + cell freed → persistent SHOE reissues it SAME-SIZE-CLASS (why small repros missed: churn + must match the victim's size class). Fix (Boris-approved option: fail-safe gate, not + always-allocate): both reclaim phases now skip a consuming call that returns a string + unless its callee is tempStringResult-flagged — flagged = always-fresh = provably cannot + alias an argument into its result (MarkTempStrings preVisit(ExprCall) + VarUseClassifier + in ast_allocate_stack.cpp). Failing-test-first: 2 new rows in + tests/strings/temp_string_reclaim.das (direct-arg + let-form, deterministic 3/6-key + corruption pre-fix); post-fix 22/22 + full tests/strings 394/394. Independent second + reproduction: the sql fan-out agent caught doc-verify's OWN marker strings being eaten + (parse_marker trim/slice temps) pre-fix, stable post-fix. doc-verify's clone_string + workaround removed. + +## Harness hardening backlog (mini fan-out output, 2026-08-10) + +From the sql (13/13 green) and classes (green) agents — fix BEFORE the full fan-out: + +1. Companion lookup is slug-only — `sql_01_hello.rst` never finds `01-version.das`; add a + number-prefix fallback (`NN-*.das` / `NN_*.das` glob on the number). +2. `given` is page-wide, injected at top of `_doc_main` — needs (a) module-scope routing + for `var`/`let` givens (finalizer-counter globals; also on-page module-scope vars are + chunked as stmts), (b) renamer seeding (a given `class Base` + an on-page `class Base` + should rename to Base__2, not error 20512), (c) documented ordering (module-decl givens + must precede others; hoists always precede givens). +3. `fresh` discards accumulated decls — cannot express "alternative spelling of the same + member" (def finalize vs def operator delete). Need an `alt` marker: compile the block + against the page program in ISOLATION (also the right semantics for `expect` — see 5). +4. Elision-merge drops a re-declared header whose MODIFIER SET differs (`class sealed + Foo3D` merged into `Foo3D`, contributing zero lines — the page's gen1 bug was invisible + to the harness). Differing modifiers = new example, not merge. +5. `expect error[NNNNN]` must compile the block in ISOLATION with a stated require set — a + negative example can pass/fail for the wrong reason via page-hoisted requires. +6. `def` re-declarations are not renamed (only type decls) — narrative def evolution + collides. +7. Marker-vocabulary: a `fragment` marker distinct from `skip` ("valid syntax, no chain + root / no context") so `skip` stays auditable as "never code". +8. Contract note: requires stated in `::` literal blocks are invisible to the extractor — + pages must use `code-block:: das` for anything the checker should see (docs-authoring + rule for skills/doc_sweep.md). + +## Engine/daslib finds from the fan-out (Boris decision) + +RULED by Boris 2026-08-11 — all follow-up work AFTER the sweep PR: + +- 🐞 FIX (bug, no discussion): tuple destructuring bypasses the shadowing check — + `let x = 1; let x = 2` is error[30704], but `let (ok, a) = p1(); let (ok, b) = p2()` + compiles and SILENTLY rebinds `ok`. Failing test first; sweep in-tree .das for repeated + destructure names to size breakage; several doc pages with repeated `let (ok, err)` + narratives will need a coordinated pass. +- 🐞 FIX (bug, "historical reasons" — double-check then fix): a class whose NAME matches a + class in a required module fails its own generated-method resolution — + `class MacroMacro : ...` in a module requiring daslib/ast_boost → + `error[30810] function not found _::MacroMacro'__finalize` (ambiguous with + ast_boost::MacroMacro's). Generated class-method calls should pin to the defining + module, not `_::` open resolution (the `_::` convention is for clone/finalize OVERLOAD + dispatch, not a class's own generated members). Also unblocks doc excerpts of required + modules. +- ✅ AS-DESIGNED (not a hole): init-move from a smart-pointer value (`var b <- f(p)`) + needing no unsafe while the statement form fires 31021. Rationale (Boris): the + statement-form unsafe exists because move-ASSIGN overwrites whatever live smart pointer + `b` held; an init has nothing to overwrite. Only 3-4 residual smart_ptr classes remain. +- 🔍 INVESTIGATE: `..` field-bypass surface syntax must STAY writable (needed in generic + code, not only macro-built AST). Docs now teach the reachable `sp. .x` spelling; explore + whether the lexer/grammar can be made to parse `sp..x` directly (DOTDOT currently wins). +- Re-confirmed open #3678 tail: `let s = match (...)` still yields the misleading + `error[30231]` instead of the return-match hint. +- Historical note: tables.rst TAUGHT `unsafe { tab[k] = v }` — the likely origin of the + `unsafe(tab[k])` residue CLAUDE.md warns about; now fixed to state the default. +- modules/dasStbImage/src/dasRaster.cpp:346 — the comment above rast_blend_pixel states a + `+128` blend formula the code three lines below contradicts (exact /255 via + `(x + 1 + (x>>8)) >> 8`); page 05's wrong formula was copied from it — fix together. +- FIXED in-tree: imgui_boost_v2.das:672 diagnostic suggested invalid `variable private` + spelling (now `var private`); state_telemetry.das:92 comment likewise. Still open + (report-only): modules/dasImgui/tests/record_layout_primitives.das:14 "empty_marker" + kind comment; examples/features/internal_log_capture.das:13 pre-1.92 ImGuiLogType + header comment; daslib/strings_boost + daslib/enum_trait docstrings FIXED in-tree. +- imgui family-wide phrase to sweep post-fan-out: "re-exported by imgui_boost_v2" — v2 + re-exports ONLY imgui, imgui_lint, imgui_boost_runtime; four pages carried the false + claim, more may exist on GREEN pages the fan-out never touched. + +- strudel `!N` is implemented as `fast(n)` (`strudel_mini.das:267-272`) with the comment + "approximation since patterns aren't copyable" — lambdas ARE copyable now, so the + justification is stale; docs (page 03) now describe actual behaviour. If the engine gets + the real replicate-into-parent-slots semantics, rewrite page 03 Part D back. +- strudel `end_pos` naming rationale ("`end` is a reserved word") is false — `end` is not + reserved; stale in `strudel_event.das:78-79` and tutorial 13 comments. +- `tutorials/daStrudel/daStrudel_03_*.das:85-88` + `daStrudel_13_*.das:135,183-192` carry + the same false claims the RST pages had (companions are compile-gated but their comments + are not). +- `tutorials/sql/06-error_handling.das:66` count-returns-int64 comment — FIXED in-tree. +- `daslib/sql_boost.das:597` — `[sql_index]` bad-field error omits the `Available: {field_names}` + suffix its sibling path at :475 has; making them match would let sql_24's original "lists + the valid columns" prose come back (currently rewritten to match the terse truth). +- More stale companion SQL comments (predate the projection aliaser): tutorials/sql/ + 14-group_by.das:122,137, 15-join.das:96-100, 19-update.das:54, 21-upsert.das:85,128. +- tutorials/daStrudel/daStrudel_16_live_reloading.das:132,138 — persistent-store key says + "tutorial15_reload_count" inside tutorial 16 (renumbering leftover); RST kept matching + the companion — fix both together. +- examples/daStrudel/sfx_lab/main.das:3-4 — header still says "Layers/Editor/Mix" + + "(Reference target + save/load land in later slices)"; all shipped since. +- Marker-vocabulary note for post-sweep polish: a page that legitimately SHOWS a + module-scope `var` in a block can't both display it and compile it (block top-level + var chunks as a statement; a given seeds the renamer so the visible duplicate renames). + Needs a `global`-ish per-block marker if it recurs. +- Post-sweep tool polish: (a) hoist companion module-scope `let` constants as implicit + givens (hand-written `given TWO_PI = ...` duplicates compile-gated ground truth and can + drift); (b) replace_ident rewrites inside `//` comments and def parameter lists — restrict + renames to code text and don't apply page aliases inside a decl chunk's own param scope. +- report_page truncates compiler output at 24 lines and surfaces cascade noise before the + actionable `can't locate variable` — rank 30838 first (sql agent suggestion); also hint + when ALL errors sit in a required module (peg agent: block likely lacks context). +- `wrap ` marker idea (peg agent): synthesize a `def f(input; blk) { parse(input) {` shell + around macro-DSL excerpts so grammar rules compile instead of going fragment. +- `alt` on a hoist-only block is a vacuous pass (checked++ fires, flush early-returns empty) — + make alt refuse to count an empty segment. `expect` blocks append unrenamed — run them + through the page renamer (or the isolation redesign). `given member ` idea (macros + agent): inject a FIELD into the member-marker subclass for base-method-parameter context + (das_string/AnnotationArgumentList can't be globals). Backlog item 6 (def re-decl renaming) + is DONE for exact headers; modifier-differing headers still collide — extend def_header_key. +- dasLLAMA report-only finds: tutorials/dasLLAMA/04_sessions_and_memory.das:56 hardcodes + 4 bytes/KV-entry (2× under the f16 default — its two prints disagree); + modules/dasLLAMA/dasllama/dasllama.das:3-8 facade arch roll-call omits GLM-4-MoE and + Mistral-3 (page 01 copies it verbatim); tutorials/dasLLAMA/07_speech_to_text.das:21-26 + ASR family list missing gemma4a/canary/qwen3omni. +- Marker-idiom note for skills/doc_sweep.md: when a name is declared inside a `with_...()` + block and a follow-up snippet uses it at top level, `alt` (fresh renamer seeded only by + givens) is THE idiom — a page-wide given cannot carry the name past the re-declaration. +- Stale companion comments: tutorials/dasPEG/06_debugging.das:76-77 (commit does NOT gate + error emission — probe-disproved, RST fixed); tutorials/sql/41-triggers.das:84 (wrong + audit-row count), :24/:104 (wrong module path + tutorial number); + tutorials/sql/39-schema_from.das:93 ("coming soon" long shipped); + tutorials/sql/38-concurrency.das:48 (nonexistent get_thread_id in comment). + +- ⚠ SPATIAL-AUDIO CONVENTION CONTRADICTION (needs a listening test): companion + tutorials/dasAudio/04_spatial_audio.das:60-73 + `g_head_direction` default (+Y, + audio_boost.das:662) vs the engine's own pan math (`pan = nrxy.y`, MIT HRTF azimuth + sign, volume_mixer.h pan law) and the HRTF demo's "-Y = forward, +X = right" comment — + a +X source pans LEFT for a +Y-facing listener by the math, RIGHT per the companion. + One of the two is wrong. The RST (dasAudio_04) now follows the engine math + demo. +- audio_boost.das:1528 `set_position(sid; pos; dir : float3)` — third param named `dir` + but assigned to `velocity`; misleading for named-arg callers. +- dasHV family-level doc gap: the STREAM/HttpResponseWriter streaming rail and SERVE_FILE + are documented on no RST page (page 07 covers buffered SSE only). + +- daspkg: `resolve(sdk_version, ...)` hook's first parameter is DEAD — both production + call sites (utils/daspkg/commands.das:404,:853) pass "". Doc now says "reserved; daspkg + passes an empty string today"; if it's a bug, fix daspkg and revert the doc line. +- dasLiveHost stale-comment cluster (report-only, fix as a batch): main.cpp:440 "lockbox + dispatch" (none exists), :839 contradicted by find_live_port_in_argv; live_commands.das:12, + :44; live_api.das:16-28 endpoint list omissions; live_api_builtins.das:20; + live_watch_boost.das:11-12; dasLiveHost.cpp:326 `live_collect_string_gc` is byte-identical + to live_collect_gc (name promises a string-only collect it does not do). +- Checker-rule idea (numbered agent): flag an `// output:` block whose chunk contains no + `print` — caught three false output claims on one page. +- More stale companion comments: tutorials/language/50_soa.das:98 (push CLONES, not moves) + + :8/:20; 52_option_and_result.das:29,:70; daslib/option.das:29 (no mutators), + daslib/result.das:30-33,:184,:236; daslib/delegate.das:16-21. + +## Regen traps (Boris decision) + +- `doc/reflections/gen_module_examples.py` generated the stdlib/handmade module-*.rst + fragments and still contains the defects the fan-out fixed (double-indent, stale + base64 output, wrong-module contracts example); it emits `::` literal blocks (invisible + to the checker) and is unrunnable as-is (hardcoded `d:\Work\daslang` path). Re-running + a fixed version would silently revert the batch. Delete it, or regenerate it FROM the + corrected RST before it bites. + +- (2026-08-10) Rule-0 probe on M1: zero true binary gaps; all scares were require-spelling + errors (dasSQLITE → sqlite/sqlite_boost, openai → openai/openai_chat, peg → peg/peg, + strudel → strudel/strudel). llvm/vulkan/anthropic referenced by no authored RST. +- (2026-08-10) Confirmed doc bugs pre-scan: 53_clargs.rst (whole-page parse_args drift + + invalid enum one-liner), classes.rst:275 (gen1), embedding/cpp_api.rst:188 (born-wrong + ManagedVectorAnnotation + plain addAnnotation; correct: + addVectorAnnotation>(this, lib, "IntVector")). diff --git a/skills/clargs_usage.md b/skills/clargs_usage.md index 3934ab9e8e..2365f747a2 100644 --- a/skills/clargs_usage.md +++ b/skills/clargs_usage.md @@ -12,7 +12,9 @@ renderer, and uniform behavior across every tool. flag is a real field with a real type (`string`, `int`, `bool`, `array`, enum). The compiler enforces the schema. - `print_help` renders complete usage text — short flags, long flags, - doc strings, defaults — with no duplicated string templates. + doc strings, env-twin/repeat/mutex markers — with no duplicated string + templates. Field defaults are NOT rendered (`default_doc` is populated + by `[EnvConfig]` only). - Required flags, repeatable flags (`array`), and enum validation all just work. diff --git a/skills/doc_sweep.md b/skills/doc_sweep.md new file mode 100644 index 0000000000..4d6b19b668 --- /dev/null +++ b/skills/doc_sweep.md @@ -0,0 +1,98 @@ +# Doc sweep — verifying authored RST code blocks (repo-only) + +Read this before running the each-release documentation sweep, before adding or editing +`.. das-doc:` markers on a doc page, and before extending `utils/doc-verify/`. Repo-only: +it is about `doc/source/`, the checker tool, and the audit procedure. + +The problem it solves: authored RST pages carry hand-written das code blocks that nothing +compiles, so they drift (the API moves under them) or are born wrong. `doc/reflections/` +das2rst output is generated and out of scope; everything hand-written under +`doc/source/reference/{tutorials,language,utils}` and `doc/source/stdlib/handmade` is in. + +## Rule 0 — the checker binary carries the corpus + +Before any page verdict counts, every module the corpus requires must load in the checker +binary. The tool probes this itself: companion-sourced requires that fail are a **binary +gap and abort the audit**; page-sourced requires that fail only red their page. Never +`--no-probes` an audit run. A "missing module" scare is as often a wrong require spelling +as a lean binary — the probe catches both. + +## Running + +```bash +bin/daslang utils/doc-verify/main.das # full corpus, report.json + exit code +bin/daslang utils/doc-verify/main.das -- --page 53_clargs # one page (substring filter — + # suffix with .rst to disambiguate) +``` + +Default out dir `build/doc_verify/`; use a private `--out` when iterating so parallel runs +don't clobber each other, and `rm` the report before a run you intend to read (a crashed +run leaves a stale report silently). Per-page synthetic modules land in `/pages/` — +read the emitted `.das` to understand an error. Cascade tip: a missing `given` can surface +as macro exceptions (`error[31206]`) listed before the real +`error[30838] can't locate variable` — scan down the list first. + +## The page model + +A page is a literate program: its das blocks concatenate in document order into one +synthetic module. Declarations (def/struct/class/enum/variant/typedef/bitfield/tuple, and +`var private`/`let private`/shared/public globals) go to module scope; statements go into +one function so locals flow block to block; `require`/`options`/`module` lines hoist +(module lines first). The preamble comes from the page's companion tutorial +(`tutorials//…`, several naming conventions, or the page's own directory for subdir +families) — companions are compile-gated ground truth. + +Narrative re-declarations rename (`name__2`) — daslang bans shadowing. Typedefs and +exact-duplicate `def` headers rename too (annotations join a GENERIC def's identity, so +contract-differentiated overloads stay distinct); a class re-declared with a leading `...` +merges into the original when its modifiers match. String-literal text is never renamed +but `{interpolation}` regions are. `...` on a line of its own elides (`pass` in statement +position, dropped in decl bodies); inline `{ ... }` compiles as `{ pass }`; any other +inline `...` reaches the compiler — expand it to the companion's real form or mark. + +## Markers + +RST comment on its own line directly above the block; invisible when rendered. + +| marker | meaning | +|---|---| +| `.. das-doc: skip` | not code at all (output dumps, wire formats, generated internals with unspellable `__`/backtick names) | +| `.. das-doc: fragment` | real syntax deliberately without context. Prefer given/member/alt — compiling code finds bugs that fragment hides (the dasHV family found 4 the moment its fragments became `member`) | +| `.. das-doc: signatures` | API-surface listing; compiler skips it, so YOU must verify each signature against the implementation before marking | +| `.. das-doc: fresh` | start a new program at this block (independent example; second `[export] def main`) | +| `.. das-doc: given ` | page-wide context. `var`/`let` givens become module-scope globals (`inscope` stripped; block params may shadow them); type/def givens go to module scope; other statements run at the top of the main function. Givens seed the renamer, so an on-page decl of the same name renames — a follow-up block that needs the *given* binding back uses `alt` | +| `.. das-doc: alt` | compile this block as its own isolated program (hoists + givens + block only): equivalent-to restatements, alternative spellings, follow-ups reusing an inner-scope name | +| `.. das-doc: member ` | block content lives inside the named class: hoists route out, `def override` bodies splice as members of a synthesized subclass, statements wrap in a `: auto` helper (so `return ` tails compile) | +| `.. das-doc: file ` | the block IS a sibling module ("put this in helpers.das") — written next to the synthetic page so a bare same-dir require resolves it; repeated markers with the same name append (a literate tangle). A minted module shadows the companion's require of the real one — per page pick mint (page text verified) or companion (real macros verified), whichever compiles more | +| `.. das-doc: expect error[NNNNN]` | deliberate error demo appended to the page program; must FAIL with that code. Limits: page-hoisted requires apply (an error that depends on a require's absence can't be expressed — fragment it), and expect blocks are not renamed — give their defs unique names | + +## Authoring rules + +- Checkable code goes in `.. code-block:: das` — `::` literal blocks and `literalinclude` + are invisible to the checker. Shell in `bash`, C++ in `cpp`, CMake in `cmake`, XML in + `xml`: retag mislabeled blocks instead of marking them. +- Name example params/fields distinctly from page locals: the renamer can rewrite block + parameter names, named-argument labels, and struct member names that collide with an + aliased page local. +- `// output:` comments are claims — run the example and make them match. An output block + whose snippet contains no `print` is lying. +- Quoted error codes and messages are claims too — probe them; renumbered diagnostics were + one of the most common drift classes found. +- Two-line lead-in + indented list is a Sphinx `Unexpected indentation` error; keep + lead-ins to one line or add a blank line. + +## The each-release procedure + +1. Full run of doc-verify; it must exit 0 (Rule 0 gates first). Triage any red: GENUINE + (fix the RST — verify the correct form against the companion and module source, probe + uncertain syntax before writing it) / MARKER (scaffolding) / HARNESS GAP (fix the tool; + never mark around a tool defect). +2. Prose re-sweep: mechanical green does not verify prose. Scope the agent-read pass to + pages whose subject area changed since the last sweep (git-dateable), with probe access + and the same GENUINE/MARKER discipline. Companions' comments are not compile-gated — + when a page and its companion carry the same wrong claim, fix both. +3. Regen traps: check `plans/doc-sweep.md`'s ledger before re-running any doc generator — + `doc/reflections/gen_module_examples.py` in particular would revert the handmade + fragments wholesale. + +Backlog, ledger, and the wiring spec for the nightly lane live in `plans/doc-sweep.md`. diff --git a/skills/filesystem.md b/skills/filesystem.md index f9e3946e2e..06c5ed0474 100644 --- a/skills/filesystem.md +++ b/skills/filesystem.md @@ -222,6 +222,7 @@ See [skills/daspkg.md](skills/daspkg.md#L224) for the bundle-shipping side of th - `fread(file)` requires **binary mode** (`"rb"`). Text mode causes a partial-read error. - `fopen(path, mode, blk)` (3-arg block form) auto-closes on block exit; the 2-arg `FILE?` form needs explicit `fclose`. Prefer the 3-arg form unless you need the file handle to outlive the call. - `dir(path)` callback yields `.` and `..` on POSIX — skip them. +- **`dir_rec(path)` callback yields paths RELATIVE to the walked root** (`std::filesystem::relative(entry, root)` — source-verified `builtin_fs_dir_rec`; a direct child yields just its filename, a nested entry yields `sub/dir/file`), never absolute. `path_join(root, name)` before any `fread`/`stat` — a bare `fread(name)` silently returns `""` from the wrong cwd. - `dir_name`/`base_name` use platform-specific code (POSIX `dirname`/Windows `_splitpath`); `parent`/`stem`/`extension` use C++17 `std::filesystem` and are uniform across platforms. All are exposed; either set is correct. - `getcwd()` and `chdir()` are no-ops on Emscripten — guard if your code runs there. - **Out of scope for this skill** (same C++ module, but different concerns): `popen`, `popen_timeout`, `system`, `exit`, `get_env_variable`, `has_env_variable`, `sleep`, `get_clock`, `mktime`, `register_dynamic_module`. Use them directly from `fio`; they're not the focus here. (`run_and_capture` is the exception — it is a `daslib/fio` function and is listed above, because it's the shell-free way to run a child process and capture its output.) diff --git a/skills/preflight.md b/skills/preflight.md index c85c5547d7..39a89bc7bc 100644 --- a/skills/preflight.md +++ b/skills/preflight.md @@ -150,6 +150,7 @@ cmake -B build -DDAS_HV_DISABLED=OFF -DDAS_LLVM_DISABLED=OFF -DDAS_AUDIO_DISABLE | daslang_static sweep | `cmake --build build --config Release --target daslang_static`, then `bin/Release/daslang_static.exe dastest/dastest.das -- --color --failures-only --test tests` | rarely built locally; catches static-registration / no-dynamic-modules divergence | | Ser/deser sweep | ` dastest/dastest.das -- --test tests --ser serialized.bin` then `... --deser serialized.bin` | run after touching AST serialization (`ast_serializer.cpp`, flag-bit additions) | | AST verify sweep — **not a PR gate** | `find tests -name '*.das' ! -name 'cant_*' ! -name 'failed_*' ! -name 'invalid_*' -print0 \| xargs -0 -P8 -n1 --ast-verify -compile-only` — only an `AST verify` line is a failure; compile errors are expected (many tests assert one) | Runs on `extended_checks.yml`'s 04:00 cron, not per PR: ~23 min, one daslang process per test file. Force it early with `gh workflow run extended_checks.yml`. Run it locally after touching macro or AST-building code — `skills/das_macros.md` | +| Authored-doc code blocks — **not a PR gate** | ` utils/doc-verify/main.das` (exit 0 = every authored RST page's das blocks compile; report at `build/doc_verify/report.json`) | Nightly cron + `workflow_dispatch`, posix cells only: ~35 min, one daslang spawn per page. Run it locally after editing `doc/source/reference/**` or `doc/source/stdlib/handmade/**`, or after daslib/module API changes that docs quote — `skills/doc_sweep.md` | | MCP tools test | ` dastest/dastest.das -- --color --failures-only --test utils/mcp/test_tools.das` | linux-only in CI but runs anywhere; MCP signature changes break it silently — run after editing `utils/mcp/` | | dasImgui build | nothing to install — dasImgui is in-tree (`modules/dasImgui`) and builds in this lane like any other default-ON module | the old `daspkg install dasImgui` externals-coupling gate is gone; the external ABI canaries (dasImguiImplot, dasImguiNodeEditor + the rest of the daspkg-index) now run in `nightly_daspkg_index.yml`. See `skills/abi_break_sweep.md` | | Coverage | ` dastest/dastest.das -- --cov-path coverage.lcov --color --test tests/language --timeout 1800` + `dascov` | rarely needed locally | diff --git a/src/ast/ast_allocate_stack.cpp b/src/ast/ast_allocate_stack.cpp index 92aa4f16e5..65d0ecb4d6 100644 --- a/src/ast/ast_allocate_stack.cpp +++ b/src/ast/ast_allocate_stack.cpp @@ -1153,6 +1153,11 @@ namespace das { if ( efun->policyBased || efun->invoke || efun->captureString ) return; if ( efun->mayQueueTempString ) return; // the callee's own body could flush the parked temp while still reading it if ( !efun->builtIn && !efun->knownSideEffects ) return; // uncomputed das function - its flags cannot be trusted + // a string-returning callee that is not itself always-fresh may PASSTHROUGH an + // argument into its result (trim/rtrim/replace return the input, trim even an + // interior pointer into it) - a queued temp laundered through the return outlives + // the call, so no argument of such a call may become a queue site + if ( expr->type && expr->type->isString() && !efun->tempStringResult ) return; for ( int ai=int(expr->arguments.size())-1; ai>=0; ai-- ) { auto & arg = expr->arguments[ai]; bool eligible = arg->rtti_isStringBuilder() || isWrapperCall(arg) @@ -1196,9 +1201,11 @@ namespace das { // counts references to one variable inside a statement, and how many are SAFE: // a safe reference is a plain read that is the DIRECT argument of a non-capturing, - // non-invoke, non-policy call - what a nested call returns is a different value, so - // only the immediate consumer of the reference matters. any reference inside a block - // literal, and any other shape (return, store, addr, operator operand), stays unsafe + // non-invoke, non-policy call whose result cannot alias the argument - a call is only + // alias-free when it returns a non-string, or is itself [temp_string_result] (always + // fresh); passthrough callees (trim/rtrim/replace) return the input, so a use there + // extends the value's live range past the call. any reference inside a block literal, + // and any other shape (return, store, addr, operator operand), stays unsafe class VarUseClassifier : public Visitor { public: VarUseClassifier ( Variable * v ) : var(v) {} @@ -1229,6 +1236,7 @@ namespace das { if ( !f || f->captureString || f->invoke || f->policyBased ) return; if ( f->mayQueueTempString ) return; // its body could flush the parked temp mid-read if ( !f->builtIn && !f->knownSideEffects ) return; // uncomputed das function - flags untrusted + if ( call->type && call->type->isString() && !f->tempStringResult ) return; // passthrough may alias the arg into the result auto barg = peelR2V(arg); if ( barg->rtti_isVar() && static_cast(barg)->variable==var ) safe ++; } diff --git a/tests/strings/temp_string_reclaim.das b/tests/strings/temp_string_reclaim.das index 475e372498..83f913b155 100644 --- a/tests/strings/temp_string_reclaim.das +++ b/tests/strings/temp_string_reclaim.das @@ -164,6 +164,51 @@ def test_temp_string_reclaim_propagation(t : T?) { } } +[test] +def test_temp_string_reclaim_passthrough_alias(t : T?) { + t |> run("trim passthrough must not launder a queued slice temp (direct-arg)") @@(t) { + var stored : array + stored |> reserve(6) + for (i in range(6)) { + let ln = "require mod_{i}/sub_{i}" + // trim with no trailing whitespace returns an INTERIOR POINTER into its + // argument; if slice's temp is queued here, stored aliases a freed cell + stored |> push(trim(slice(ln, length("require ")))) + } + // same-size churn: the persistent shoe reissues a freed cell to the next + // allocation of the same size class, so the corruption is deterministic + var churn = 0 + for (i in range(50)) { + churn += length(trim(slice("require xod_{i % 10}/yub_{i % 10}", 8))) + } + t |> success(churn > 0) + for (i in range(6)) { + let expected_elem = "mod_{i}/sub_{i}" + t |> equal(stored[i], expected_elem) + } + } + + t |> run("trim passthrough must not launder a wrapped let-local slice (let-form)") @@(t) { + var stored : array + stored |> reserve(6) + for (i in range(6)) { + let ln = "require mod_{i}/sub_{i}" + let sliced = slice(ln, 8) + let trimmed = trim(sliced) // "safe" use of sliced that aliases it + stored |> push(trimmed) + } + var churn = 0 + for (i in range(50)) { + churn += length(trim(slice("require xod_{i % 10}/yub_{i % 10}", 8))) + } + t |> success(churn > 0) + for (i in range(6)) { + let expected_elem = "mod_{i}/sub_{i}" + t |> equal(stored[i], expected_elem) + } + } +} + [test] def test_temp_string_reclaim_let_local(t : T?) { t |> run("f2s shape: let-local conversion is reclaimed") @@(t) { diff --git a/tutorials/language/53_clargs.das b/tutorials/language/53_clargs.das index ef63cc67a6..9b9326ab5f 100644 --- a/tutorials/language/53_clargs.das +++ b/tutorials/language/53_clargs.das @@ -335,7 +335,8 @@ def test_help_rendering() { print("\n=== Help rendering ===\n") let info <- get_command_info(type) print(format_help(info, "demo")) - // output: + // output (usage line is host-dependent; under the script host it reads + // "Usage: daslang demo -- [flags]"): // Usage: demo [flags] // // Flags: diff --git a/tutorials/sql/06-error_handling.das b/tutorials/sql/06-error_handling.das index 45134a6fda..882bad133e 100644 --- a/tutorials/sql/06-error_handling.das +++ b/tutorials/sql/06-error_handling.das @@ -63,7 +63,7 @@ def main() { // Returns the SAME shape as _sql but wrapped in Result<..., string>. // _try_sql(... |> _first()) returns Result // _try_sql(... |> _first_opt()) returns Result, string> - // _try_sql(... |> count()) returns Result + // _try_sql(... |> count()) returns Result (long_count() for int64) let res = _try_sql(db |> select_from(type) |> _first()) if (res |> is_ok) { let u = res |> unwrap diff --git a/utils/doc-verify/main.das b/utils/doc-verify/main.das new file mode 100644 index 0000000000..08c887af54 --- /dev/null +++ b/utils/doc-verify/main.das @@ -0,0 +1,1258 @@ +options gen2 +options persistent_heap +options indenting = 4 + +// doc-verify — compile-checks the das code blocks of authored RST pages. +// +// Model: each page is a literate program. Blocks concatenate in document order into +// one synthetic module — decls at module scope, statements into one function so locals +// flow block to block. daslang bans shadowing, so a narrative re-declaration renames +// (`name__N`) from that point on; a class re-declared with a leading `...` merges into +// the original (elision means "previous members here"). Preamble (require/options) +// comes from the page's companion tutorial .das when one exists. +// +// Markers, written as RST comments directly above a block (invisible when rendered): +// .. das-doc: skip -- not code (wire formats, output dumps) +// .. das-doc: fresh -- start a new synthetic program here +// .. das-doc: given -- inject context the prose assumes +// .. das-doc: signatures -- API listing; compiler skips, agents verify +// .. das-doc: expect error[NNNNN] -- block must FAIL to compile with this code +// +// Rule 0: before any page verdict, every require the corpus depends on is probed +// against the checker binary. A companion-sourced require that fails to load is a +// binary gap and ABORTS the audit; a page-sourced require that fails only reds +// that page (the page itself may be wrong). + +require daslib/clargs +require daslib/fio +require daslib/strings_boost +require strings +require math + +let AREAS = [ + "doc/source/reference/tutorials", + "doc/source/reference/language", + "doc/source/reference/utils", + "doc/source/stdlib/handmade" +] + +let ELIDE = "__DOC_ELIDE__" + +[CommandLineArgs] +struct Config { + @clarg_doc = "Repository root (default: current directory)" + root : string = "." + + @clarg_doc = "Output directory for synthetic modules and reports" + out : string = "build/doc_verify" + + @clarg_doc = "Path to the daslang binary used for compile checks" + daslang : string = "bin/daslang" + + @clarg_short = "p" + @clarg_doc = "Only process pages whose path contains this substring" + page : string + + @clarg_doc = "Path for the JSON report (default: /report.json)" + json : string + + @clarg_doc = "Skip rule-0 module probes (NOT for audit runs)" + no_probes : bool + + @clarg_short = "v" + @clarg_doc = "Per-page progress" + verbose : bool +} + +// ===== RST extraction ===== + +struct RstBlock { + rst_line : int + marker : string + marker_arg : string + body : array +} + +struct PageSource { + path : string + blocks : array + givens : array +} + +def leading_spaces(s : string) : int { + // tabs count as indentation too — a tab-indented block line must not read as + // column 0, or the whole block silently drops from extraction + var n = 0 + peek_data(s) $(arr) { + for (c in arr) { + break if (int(c) != ' ' && int(c) != '\t') + n++ + } + } + return n +} + +def parse_marker(trimmed : string; var marker : string&; var marker_arg : string&) { + let rest = trim(slice(trimmed, length(".. das-doc:"))) + let sp = find(rest, ' ') + if (sp < 0) { + marker = rest + marker_arg = "" + } else { + marker = slice(rest, 0, sp) + marker_arg = trim(slice(rest, sp + 1)) + } +} + +def parse_page(path : string) : PageSource { + var page = PageSource(path = path) + let lines <- split(fread(path), "\n") + var pending_marker = "" + var pending_arg = "" + var i = 0 + while (i < length(lines)) { + let trimmed = trim(lines[i]) + if (trimmed |> starts_with(".. das-doc:")) { + var m, a : string + parse_marker(trimmed, m, a) + if (m == "given") { + page.givens |> push(a) + } else { + pending_marker = m + pending_arg = a + } + i++ + continue + } + if (!(trimmed |> starts_with(".. code-block:: das")) || trimmed != ".. code-block:: das") { + i++ + continue + } + let base_indent = leading_spaces(lines[i]) + var j = i + 1 + while (j < length(lines) && (empty(trim(lines[j])) || (trim(lines[j]) |> starts_with(":")))) { + j++ + } + var blk = RstBlock(rst_line = j + 1, marker = pending_marker, marker_arg = pending_arg) + pending_marker = "" + pending_arg = "" + while (j < length(lines)) { + let ln = lines[j] + if (empty(trim(ln))) { + blk.body |> push("") + j++ + continue + } + break if (leading_spaces(ln) <= base_indent) + blk.body |> push(ln) + j++ + } + while (!empty(blk.body) && empty(blk.body[length(blk.body) - 1])) { + blk.body |> pop() + } + if (!empty(blk.body)) { + dedent(blk.body) + page.blocks |> emplace(blk) + } + i = j + } + return <- page +} + +def dedent(var body : array) { + var min_ind = 1000 + for (l in body) { + continue if (empty(trim(l))) + min_ind = min(min_ind, leading_spaces(l)) + } + return if (min_ind == 0 || min_ind == 1000) + for (l in body) { + l = empty(trim(l)) ? "" : slice(l, min_ind) + } +} + +// ===== chunking ===== + +let DECL_STARTERS = [ + "def ", "struct ", "class ", "enum ", "variant ", "typedef ", "bitfield ", "tuple " +] + +def is_decl_start(line : string) : bool { + for (s in DECL_STARTERS) { + return true if (line |> starts_with(s)) + } + // `private`/`shared` var-decls are module-scope-only grammar — a col-0 plain `var` + // stays a narrative statement, but these can only be globals + for (s in ["var private ", "let private ", "var shared ", "let shared ", "var public ", "let public "]) { + return true if (line |> starts_with(s)) + } + return line |> starts_with("[") || line |> starts_with("@") +} + +def is_hoist(line : string) : bool { + return (line |> starts_with("require ")) || (line |> starts_with("options ")) || (line |> starts_with("module ")) +} + +struct Chunk { + kind : string // "hoist" | "decl" | "stmt" + lines : array +} + +def flush_chunk(var chunks : array; var cur : array; var kind : string&) { + if (!empty(cur)) { + chunks |> emplace(Chunk(kind = kind, lines <- cur)) + } + kind = "" +} + +def last_nonblank(lines : array) : string { + var i = length(lines) - 1 + while (i >= 0) { + return lines[i] if (!empty(trim(lines[i]))) + i-- + } + return "" +} + +def chunks_of(body : array) : array { + var chunks : array + var cur : array + var kind = "" + for (ln in body) { + if (empty(trim(ln))) { + if (!empty(cur)) { + cur |> push(ln) + } + continue + } + let top = leading_spaces(ln) == 0 + if (top && is_hoist(ln)) { + flush_chunk(chunks, cur, kind) + chunks |> emplace(Chunk(kind = "hoist", lines <- [ln])) + continue + } + if (top && is_decl_start(ln)) { + let glue = kind == "decl" && !empty(cur) && ((trim(last_nonblank(cur)) |> starts_with("[")) || (trim(last_nonblank(cur)) |> starts_with("@"))) + if (glue) { + cur |> push(ln) + continue + } + flush_chunk(chunks, cur, kind) + kind = "decl" + cur |> push(ln) + continue + } + if (top && kind == "decl" && (ln |> starts_with("}"))) { + cur |> push(ln) + continue + } + if (top && kind != "stmt") { + flush_chunk(chunks, cur, kind) + kind = "stmt" + } elif (empty(kind)) { + kind = "stmt" + } + cur |> push(ln) + } + flush_chunk(chunks, cur, kind) + return <- chunks +} + +// ===== rename-on-redeclare ===== + +struct Renamer { + counts : table + aliases : table + def_headers : table // exact-duplicate def headers rename; overloads keep the name +} + +def is_ident_char(c : int) : bool { + return is_alpha(c) || is_number(c) || c == '_' +} + +def take_ident(s : string) : string { + var n = 0 + peek_data(s) $(arr) { + for (c in arr) { + break if (!is_ident_char(int(c))) + n++ + } + } + return slice(s, 0, n) +} + +let TYPE_KEYWORDS = ["class ", "struct ", "enum ", "variant ", "bitfield ", "tuple ", "typedef "] +let TYPE_MODIFIERS = ["private ", "public ", "sealed ", "abstract ", "shared ", "distinct "] + +def type_decl_name(line : string) : string { + var mods : string + return type_decl_name_mods(line, mods) +} + +def type_decl_name_mods(line : string; var mods : string&) : string { + mods = "" + for (kw in TYPE_KEYWORDS) { + continue if (!(line |> starts_with(kw))) + var rest = trim(slice(line, length(kw))) + var again = true + var mod_list : array + while (again) { + again = false + for (m in TYPE_MODIFIERS) { + if (rest |> starts_with(m)) { + mod_list |> push(m) + rest = trim(slice(rest, length(m))) + again = true + } + } + } + mods = join(mod_list, "") + return take_ident(rest) + } + return "" +} + +// a def with an untyped parameter segment is a GENERIC — contract annotations then +// participate in overload resolution, so annotated same-header generics are distinct +def def_is_generic(line : string) : bool { + let open = find(line, '(') + return false if (open < 0) + let close = find(line, ')') + return false if (close <= open + 1) + for (seg in split(slice(line, open + 1, close), ";")) { + return true if (find(seg, ':') < 0) + } + return false +} + +// the def name for duplicate-header renaming; "" for operators and non-def lines +def def_decl_name(line : string) : string { + return "" if (!(line |> starts_with("def "))) + var rest = trim(slice(line, length("def "))) + for (m in ["private ", "public ", "static ", "abstract ", "const "]) { + if (rest |> starts_with(m)) { + rest = trim(slice(rest, length(m))) + } + } + let name = take_ident(rest) + return name == "operator" ? "" : name +} + +def def_header_key(line : string) : string { + var cut = length(line) + let b = find(line, "\{") + if (b >= 0) { + cut = min(cut, b) + } + let a = find(line, "=>") + if (a >= 0) { + cut = min(cut, a) + } + var k = trim(slice(line, 0, cut)) + // `def main {` and `def main() {` are the same header + if (k |> ends_with("()")) { + k = trim(slice(k, 0, length(k) - 2)) + } + return k +} + +def var_decl_names(line : string) : array { + var t = trim(line) + var names : array + if ((t |> starts_with("var ")) || (t |> starts_with("let "))) { + t = trim(slice(t, 4)) + var again = true + while (again) { + again = false + for (m in ["inscope ", "private ", "shared ", "public "]) { + if (t |> starts_with(m)) { + t = trim(slice(t, length(m))) + again = true + } + } + } + } elif ((t |> starts_with("for (")) || (t |> starts_with("for("))) { + let open = find(t, '(') + let in_pos = find(t, " in ") + return <- names if (in_pos < 0) + t = trim(slice(t, open + 1, in_pos)) + } else { + return <- names + } + // tuple destructure: `let (ok, err) = ...` — names live inside the parens + if (t |> starts_with("(")) { + let close = find(t, ')') + return <- names if (close < 0) + for (piece in split(slice(t, 1, close), ",")) { + let name = take_ident(trim(piece)) + if (!empty(name)) { + names |> push(name) + } + } + return <- names + } + // truncate at the first initializer/type delimiter — only the name list splits on commas + var cut = length(t) + for (d in [find(t, ':'), find(t, '='), find(t, '<'), find(t, '&')]) { + if (d >= 0) { + cut = min(cut, d) + } + } + t = slice(t, 0, cut) + for (piece in split(t, ",")) { + let name = take_ident(trim(piece)) + if (!empty(name)) { + names |> push(name) + } + } + return <- names +} + +def declare(var ren : Renamer; names : array) { + for (n in names) { + let gen = (ren.counts?[n] ?? 0) + 1 + ren.counts[n] = gen + if (gen > 1) { + ren.aliases[n] = "{n}__{gen}" + } + } +} + +def replace_ident(line : string; base : string; alias_name : string) : string { + return line if (find(line, base) < 0) + var out = "" + let blen = length(base) + peek_data(line) $(arr) { + // string-literal TEXT is not code — "\n" must not become "\n__2" when `n` + // aliases — but an unescaped {…} interpolation region inside a string IS + // code and must rename with the rest of the line + var in_str : array + in_str |> resize(length(arr)) + var q = false + var depth = 0 + for (i in range(length(arr))) { + let c = int(arr[i]) + let esc = i > 0 && int(arr[i - 1]) == '\\' + if (!q) { + if (c == '"' && !esc) { + q = true + in_str[i] = true + } else { + in_str[i] = false + } + } elif (depth == 0 && c == '"' && !esc) { + q = false + in_str[i] = true + } elif (c == '{' && !esc) { + depth++ + in_str[i] = true + } elif (c == '}' && !esc && depth > 0) { + depth-- + in_str[i] = true + } else { + in_str[i] = depth == 0 + } + } + out = build_string() $(var writer) { + var start = 0 + while (true) { + let pos = find(line, base, start) + if (pos < 0) { + writer |> write(slice(line, start)) + break + } + let before_ok = pos == 0 || (!is_ident_char(int(arr[pos - 1])) && int(arr[pos - 1]) != '.') + let after_pos = pos + blen + let after_ok = after_pos >= length(arr) || !is_ident_char(int(arr[after_pos])) + writer |> write(slice(line, start, pos)) + writer |> write((before_ok && after_ok && !in_str[pos]) ? alias_name : base) + start = after_pos + } + } + } + return out +} + +def apply_renames(ren : Renamer; line : string) : string { + var out = line + for (base, alias_name in keys(ren.aliases), values(ren.aliases)) { + out = replace_ident(out, base, alias_name) + } + return out +} + +def rename_line(var ren : Renamer; line : string) : string { + return rename_line_ex(ren, line, true) +} + +def rename_line_ex(var ren : Renamer; line : string; declare_ok : bool) : string { + if (declare_ok) { + let tname = type_decl_name(line) + if (!empty(tname)) { + declare(ren, [tname]) + } else { + declare(ren, var_decl_names(line)) + } + } + return apply_renames(ren, line) +} + +// ===== page emission ===== + +struct DeclChunk { + name : string + mods : string // modifier words of the header; a differing set is a new example, not a merge + lines : array +} + +struct Segment { + decls : array + stmts : array +} + +struct EmittedPage { + files : array + checked : int + skipped : int + fragments : int + expects : array + require_lines : array + companion_requires : table // lines that came from the companion preamble + file_written : table // sibling modules minted by `file` markers this page + companion_backed : bool +} + +def emit_alt_block(blk : RstBlock; hoists : array; givens : array; out_base : string; var files : array) { + // `alt` compiles the block as its own program (hoists + givens + this block only) — + // the way to show an alternative spelling of a member the page program already has + var aseg = Segment() + var aren = Renamer() + seed_renamer(aren, givens) + var body := blk.body + mark_elisions(body) + var ahoists := hoists + let tag = "// --- alt block at rst:{blk.rst_line}" + for (chunk in chunks_of(body)) { + if (chunk.kind == "hoist") { + for (h in chunk.lines) { + let t = trim(h) + continue if (t == ELIDE) + ahoists |> push(t) + } + } elif (chunk.kind == "decl") { + append_decl(aseg, aren, chunk, tag) + } else { + append_stmts(aseg, aren, chunk, tag) + } + } + flush_segment(aseg, ahoists, givens, "{out_base}__alt_{blk.rst_line}.das", files) +} + +def strip_comment(line : string) : string { + let c = find(line, "//") + return c < 0 ? line : trim(slice(line, 0, c)) +} + +// a same-dir companion require (`require tutorial_server`) only resolves from the +// companion's own directory — rewrite it file-relative to the synthetic pages dir +def rewrite_sibling_require(line : string; comp_dir : string; pages_dir : string) : string { + let t = strip_comment(line) + return line if (!(t |> starts_with("require "))) + var target = trim(slice(t, length("require "))) + var suffix = "" + if (target |> ends_with(" public")) { + suffix = " public" + target = trim(trim_suffix(target, " public")) + } + return line if (find(target, "/") >= 0 || find(target, ".") >= 0) + let sibling = path_join(comp_dir, target + ".das") + return line if (!fexist(sibling)) + var err : string + let rel = relative(comp_dir, pages_dir, err) + return line if (!empty(err)) + return "require {to_generic_path(path_join(rel, target))}.das{suffix}" +} + +def companion_preamble(rst_path : string; root : string; pages_dir : string) : array { + var pre : array + let base = stem(rst_path) + var cands : array + let us = find(base, '_') + if (us > 0 && !is_number(first_character(base))) { + let fam = slice(base, 0, us) + let tail = slice(base, us + 1) + let fam_dir = path_join(root, path_join("tutorials", fam)) + cands |> push(path_join(fam_dir, tail + ".das")) + cands |> push(path_join(fam_dir, replace(tail, "_", "-") + ".das")) + // sql-style naming: hyphen after the number only (02-insert_data.das) + let t2 = find(tail, '_') + if (t2 > 0) { + cands |> push(path_join(fam_dir, slice(tail, 0, t2) + "-" + slice(tail, t2 + 1) + ".das")) + } + // number-prefix fallback: the slug can differ entirely (sql_01_hello.rst + // vs tutorials/sql/01-version.das) — any companion sharing the number wins + var dig = 0 + peek_data(tail) $(arr) { + for (c in arr) { + break if (!is_number(int(c))) + dig++ + } + } + if (dig > 0) { + var globbed : array + expand_glob(path_join(fam_dir, slice(tail, 0, dig) + "*.das"), globbed) + cands |> push_from(globbed) + } + } + if (length(base) > 2 && is_number(first_character(base))) { + cands |> push(path_join(root, path_join("tutorials/language", base + ".das"))) + } + // some families keep companions under the page's own basename, or in a module tree + let gpath = to_generic_path(rst_path) + if (find(gpath, "/tutorials/imgui/") >= 0) { + cands |> push(path_join(root, path_join("modules/dasImgui/examples/tutorial", base + ".das"))) + } + if (us > 0 && !is_number(first_character(base))) { + let fam2 = slice(base, 0, us) + cands |> push(path_join(root, path_join("tutorials", path_join(fam2, base + ".das")))) + } + // subdir families (tutorials/macros/NN_x.rst): the RST's own directory names the + // companion family — this is what lets usage blocks compile against the REAL + // sibling macro modules via the companion's rewritten requires + let parent = base_name(dir_name(rst_path)) + if (parent != "tutorials") { + cands |> push(path_join(root, path_join("tutorials", path_join(parent, base + ".das")))) + } + for (c in cands) { + continue if (!fexist(c)) + for (ln in split(fread(c), "\n")) { + let t = trim(ln) + if (t |> starts_with("require ")) { + pre |> push(rewrite_sibling_require(t, dir_name(c), pages_dir)) + } elif ((t |> starts_with("options ")) && find(t, "indenting") < 0) { + pre |> push(t) + } + break if (((t |> starts_with("def ")) || (t |> starts_with("["))) && !empty(pre)) + } + break if (!empty(pre)) + } + return <- pre +} + +def mark_elisions(var body : array) { + for (l in body) { + let t = trim(l) + if (t == "..." || t == "// ...") { + l = slice(l, 0, leading_spaces(l)) + ELIDE + continue + } + // inline elided bodies: `if (cond) { ... }` — a brace pair whose sole + // content is `...` compiles as `{ pass }` + if (find(l, "\{ ... \}") >= 0) { + l = replace(l, "\{ ... \}", "\{ pass \}") + } + if (find(l, "\{...\}") >= 0) { + l = replace(l, "\{...\}", "\{ pass \}") + } + } +} + +// duplicate exact def headers rename; overloads keep the name. For GENERICS the +// annotations join the identity (contracts participate in overload resolution), +// so annotated same-header generics are distinct, not duplicates +def register_def_header(var ren : Renamer; chunk : Chunk) { + var ann_parts : array + for (l in chunk.lines) { + let t = trim(l) + if ((t |> starts_with("[")) || (t |> starts_with("@"))) { + ann_parts |> push(t) + continue + } + let dname = def_decl_name(l) + continue if (empty(dname)) + let hdr = (def_is_generic(l) ? join(ann_parts, "") : "") + def_header_key(l) + if (key_exists(ren.def_headers, hdr)) { + declare(ren, [dname]) + } else { + ren.def_headers |> insert(hdr) + // claim generation 1 so the FIRST duplicate already aliases to __2 + if ((ren.counts?[dname] ?? 0) == 0) { + declare(ren, [dname]) + } + } + break + } +} + +def append_decl(var seg : Segment; var ren : Renamer; chunk : Chunk; tag : string) { + let header = chunk.lines[0] + var mods : string + let tname = type_decl_name_mods(header, mods) + // exact-duplicate def header = narrative re-declaration -> rename; overloads keep the name + if (empty(tname)) { + register_def_header(ren, chunk) + } + var merge_idx = -1 + if (!empty(tname) && (ren.counts?[tname] ?? 0) > 0) { + // merge only when the body opens with an elision — "previous members here" — + // AND the modifier set matches (a `sealed` re-declaration is a new example) + for (i in range(1, length(chunk.lines))) { + let t = trim(chunk.lines[i]) + continue if (empty(t)) + if (t == ELIDE) { + for (di in range(length(seg.decls))) { + if (seg.decls[di].name == tname && seg.decls[di].mods == mods) { + merge_idx = di + } + } + } + break + } + } + if (merge_idx >= 0) { + var members : array + for (i in range(1, length(chunk.lines))) { + let t = trim(chunk.lines[i]) + continue if (t == ELIDE || (i == length(chunk.lines) - 1 && t == "}")) + members |> push(chunk.lines[i]) + } + var target & = unsafe(seg.decls[merge_idx]) + var close = length(target.lines) - 1 + while (close > 0 && trim(target.lines[close]) != "}") { + close-- + } + target.lines |> resize(length(target.lines) + length(members)) + var i = length(target.lines) - 1 + while (i >= close + length(members)) { + target.lines[i] = target.lines[i - length(members)] + i-- + } + for (k, m in count(), members) { + target.lines[close + k] = m + } + return + } + var dc = DeclChunk(name = tname, mods = mods) + dc.lines |> push(tag) + for (l in chunk.lines) { + continue if (trim(l) == ELIDE) + // indented lines inside a decl body are function-local — they get alias + // APPLICATION but must not claim their names page-wide + dc.lines |> push(rename_line_ex(ren, l, leading_spaces(l) == 0)) + } + seg.decls |> emplace(dc) +} + +def append_stmts(var seg : Segment; var ren : Renamer; chunk : Chunk; tag : string) { + // statement lines declare page-wide regardless of indentation: daslang bans + // shadowing even in nested scopes, so an indented re-declaration must rename, + // and the alias persisting page-wide is the model the corpus is authored + // against (a page that needs a name back after a nested binding uses `alt` + // or renames the local — see the plan ledger) + seg.stmts |> push(" " + tag) + for (l in chunk.lines) { + if (trim(l) == ELIDE) { + seg.stmts |> push(slice(l, 0, leading_spaces(l)) + " pass") + continue + } + let r = rename_line_ex(ren, l, true) + seg.stmts |> push(empty(trim(r)) ? "" : " " + r) + } + seg.stmts |> push("") +} + +def is_global_decl(line : string) : bool { + return (line |> starts_with("var ")) || (line |> starts_with("let ")) +} + +def seed_renamer(var ren : Renamer; givens : array) { + for (g in givens) { + let tname = type_decl_name(g) + if (!empty(tname)) { + declare(ren, [tname]) + } else { + declare(ren, var_decl_names(g)) + } + } +} + +def flush_segment(var seg : Segment; hoists : array; givens : array; fname : string; var files : array) { + return if (empty(seg.decls) && empty(seg.stmts)) + var out : array + out |> reserve(length(hoists) + length(seg.stmts) + 16) + // a `module X` line (hoisted from a block or given) must be the first declaration + var seen : table + for (g in givens) { + if ((g |> starts_with("module ")) && !key_exists(seen, g)) { + seen |> insert(g) + out |> push(g) + } + } + for (hpass in range(2)) { + for (h in hoists) { + continue if ((h |> starts_with("module ")) != (hpass == 0) || key_exists(seen, h)) + seen |> insert(h) + out |> push(h) + } + } + out |> push("") + var given_stmts : array + for (g in givens) { + continue if ((g |> starts_with("module ")) || key_exists(seen, g)) + if (is_hoist(g)) { + seen |> insert(g) + } + if (is_decl_start(g) || is_hoist(g) || is_global_decl(g)) { + // var/let givens become globals: page-wide context (a finalizer counter, + // a db handle) that block parameters may legally shadow. `inscope` is + // statement-only grammar — drop it on the way to module scope + out |> push(replace(g, " inscope ", " ")) + } else { + given_stmts |> push(" " + g) + } + } + for (d in seg.decls) { + out |> push_from(d.lines) + out |> push("") + } + if (!empty(seg.stmts) || !empty(given_stmts)) { + out |> push("def _doc_main() \{") + out |> push_from(given_stmts, seg.stmts) + out |> push("\}") + } + fwrite(fname, join(out, "\n") + "\n") + files |> push(fname) + // clear, NOT delete: these arrays hold borrowed string pointers (rename_line + // returns the original line when nothing renamed), and delete on array + // frees the strings — leaving every alias in hoists/require tables dangling + seg.decls |> clear() + seg.stmts |> clear() +} + +// `member `: the block's content lives inside the named class — chunk-aware: +// hoist chunks route to the page hoists, decl chunks (def override ... ) splice as +// members of a synthesized subclass, statement chunks wrap in a helper method so +// unqualified calls resolve against the class surface (HvWebServer routes, adapters, ...) +def append_member_block(var seg : Segment; var ren : Renamer; blk : RstBlock; var hoists : array) { + var body := blk.body + mark_elisions(body) + var members : array + var stmts : array + for (chunk in chunks_of(body)) { + if (chunk.kind == "hoist") { + for (h in chunk.lines) { + let t = trim(h) + continue if (t == ELIDE) + hoists |> push(t) + } + continue + } + // member content is class/method-scoped: aliases apply, names stay local + for (l in chunk.lines) { + if (trim(l) == ELIDE) { + continue if (chunk.kind == "decl") + stmts |> push(" pass") + continue + } + let r = rename_line_ex(ren, l, false) + if (chunk.kind == "decl") { + members |> push(empty(trim(r)) ? "" : " " + r) + } else { + stmts |> push(empty(trim(r)) ? "" : " " + r) + } + } + } + var dc = DeclChunk(name = "_doc_member_{blk.rst_line}", lines <- [ + "// --- member block at rst:{blk.rst_line}", + "class _doc_member_{blk.rst_line} : {blk.marker_arg} \{" + ]) + dc.lines |> push_from(members) + if (!empty(stmts)) { + // `: auto` lets method-TAIL excerpts (`return ` in a member block) compile; + // a void body still infers void + dc.lines |> push(" def _doc_body_{blk.rst_line}() : auto \{") + dc.lines |> push_from(stmts) + dc.lines |> push(" \}") + } + dc.lines |> push("\}") + seg.decls |> emplace(dc) +} + +// true when the marker fully consumed the block (fresh falls through — it needs segment state) +def handle_marker(blk : RstBlock; var em : EmittedPage; hoists : array; givens : array; out_base : string; pages_dir : string) : bool { + if (blk.marker == "skip" || blk.marker == "signatures" || blk.marker == "fragment") { + em.skipped++ + if (blk.marker == "fragment") { + em.fragments++ + } + return true + } + if (blk.marker == "expect") { + em.expects |> push_clone(blk) + return true + } + if (blk.marker == "alt") { + em.checked++ + emit_alt_block(blk, hoists, givens, out_base, em.files) + return true + } + if (blk.marker == "file") { + // multi-file literate page: the block IS a sibling module ("put this in + // helpers.das") — written next to the synthetic page so the page's own + // bare `require helpers` resolves same-dir; compiled transitively. + // Repeated `file` markers with the same name APPEND (a literate tangle), + // and whole-line `...` elisions are dropped like any decl-body elision + em.checked++ + let fname = base_name(blk.marker_arg) + var body := blk.body + mark_elisions(body) + let content = build_string() $(var writer) { + for (l in body) { + continue if (trim(l) == ELIDE) + writer |> write(l) + writer |> write("\n") + } + } + let fpath = path_join(pages_dir, fname) + if (key_exists(em.file_written, fname)) { + fwrite(fpath, fread(fpath) + content) + } else { + em.file_written |> insert(fname) + fwrite(fpath, content) + } + return true + } + return false +} + +// a module the page mints via a `file` marker shadows the companion's require of the +// real one — both would declare the same module name; the page's text wins +def add_companion_hoists(var hoists : array; comp : array; page : PageSource) { + var minted : table + for (blk in page.blocks) { + if (blk.marker == "file" && !empty(blk.marker_arg)) { + let s = stem(base_name(blk.marker_arg)) + if (!key_exists(minted, s)) { + minted |> insert(s) + } + } + } + for (c in comp) { + let t = strip_comment(c) + if (t |> starts_with("require ")) { + var target = trim(slice(t, length("require "))) + if (target |> ends_with(" public")) { + target = trim(trim_suffix(target, " public")) + } + continue if (key_exists(minted, stem(base_name(target)))) + } + hoists |> push(c) + } +} + +def emit_page(page : PageSource; cfg : Config) : EmittedPage { + var em = EmittedPage() + var hoists : array + let comp <- companion_preamble(page.path, cfg.root, path_join(cfg.out, "pages")) + em.companion_backed = !empty(comp) + add_companion_hoists(hoists, comp, page) + // only requires that genuinely came FROM the companion are binary-gap-fatal; + // a page-text require on a companion-backed page is the page's own problem + for (h in hoists) { + if ((h |> starts_with("require ")) && !key_exists(em.companion_requires, h)) { + em.companion_requires |> insert(h) + } + } + if (!em.companion_backed && (base_name(page.path) |> starts_with("dasAudio_"))) { + hoists |> push("require audio/audio_boost") + hoists |> push("require math") + } + var seg = Segment() + var ren = Renamer() + seed_renamer(ren, page.givens) + var seg_no = 0 + let out_base = path_join(path_join(cfg.out, "pages"), stem(page.path) + "_doc") + for (blk in page.blocks) { + continue if (handle_marker(blk, em, hoists, page.givens, out_base, path_join(cfg.out, "pages"))) + if (blk.marker == "member") { + em.checked++ + append_member_block(seg, ren, blk, hoists) + continue + } + if (blk.marker == "fresh") { + let fname = seg_no == 0 ? out_base + ".das" : "{out_base}__{seg_no + 1}.das" + flush_segment(seg, hoists, page.givens, fname, em.files) + seg_no++ + // clear, not delete: key strings may alias page lines (identity slices) + ren.counts |> clear() + ren.aliases |> clear() + ren.def_headers |> clear() + seed_renamer(ren, page.givens) + } + em.checked++ + var body := blk.body + mark_elisions(body) + let tag = "// --- {base_name(page.path)}:{blk.rst_line}" + for (chunk in chunks_of(body)) { + if (chunk.kind == "hoist") { + for (h in chunk.lines) { + let t = trim(h) + continue if (trim(t) == ELIDE) + hoists |> push(t) + if (t |> starts_with("require ")) { + em.require_lines |> push(t) + } + } + } elif (chunk.kind == "decl") { + append_decl(seg, ren, chunk, tag) + } else { + append_stmts(seg, ren, chunk, tag) + } + } + } + let last_fname = seg_no == 0 ? out_base + ".das" : "{out_base}__{seg_no + 1}.das" + flush_segment(seg, hoists, page.givens, last_fname, em.files) + for (h in hoists) { + if (h |> starts_with("require ")) { + em.require_lines |> push(h) + } + } + return <- em +} + +// ===== compile driver ===== + +def compile_file_rc(cfg : Config; path : string; var output : string&) : int { + let args <- [normalize(cfg.daslang), "-compile-only", normalize(path)] + return run_and_capture(args, output, 120.0) +} + +def first_lines(s : string; n : int) : array { + var out : array + for (l in split(s, "\n")) { + break if (length(out) >= n) + continue if (empty(trim(l))) + out |> push(l) + } + return <- out +} + +// ===== reporting ===== + +struct PageReport { + page : string + status : string + das_blocks : int + checked : int + skipped : int + fragments : int + expect_checked : int + companion_backed : bool + errors : array +} + +struct SweepReport { + total_pages : int + green : int + red : int + no_blocks : int + probe_failures : array + pages : array +} + +def check_expects(em : EmittedPage; cfg : Config; base_file : string; var rep : PageReport) { + for (ex in em.expects) { + var body := ex.body + mark_elisions(body) + let ex_file = replace(base_file, ".das", "__expect_{ex.rst_line}.das") + var lines <- split(fread(base_file), "\n") + for (chunk in chunks_of(body)) { + if (chunk.kind == "decl" || chunk.kind == "hoist") { + lines |> push_from(chunk.lines) + } else { + lines |> reserve(length(lines) + length(chunk.lines) + 2) + lines |> push("def _doc_expect_{ex.rst_line}() \{") + for (l in chunk.lines) { + lines |> push(" " + l) + } + lines |> push("\}") + } + } + fwrite(ex_file, join(lines, "\n") + "\n") + var out : string + let rc = compile_file_rc(cfg, ex_file, out) + if (rc == 0) { + rep.status = "red" + rep.errors |> push("expect block at :{ex.rst_line} compiled clean (expected {ex.marker_arg})") + } elif (!empty(ex.marker_arg) && find(out, ex.marker_arg) < 0) { + rep.status = "red" + rep.errors |> push("expect block at :{ex.rst_line} failed with a different error (expected {ex.marker_arg})") + } else { + rep.expect_checked++ + } + } +} + +def scan_pages(cfg : Config) : array { + var pages : array + for (area in AREAS) { + let area_path = path_join(cfg.root, area) + continue if (!fexist(area_path)) + dir_rec(area_path) $(name, isdir) { + // dir_rec yields paths relative to the walked root + let full = path_join(area_path, name) + return if (isdir || extension(full) != ".rst" || (!empty(cfg.page) && find(to_generic_path(full), cfg.page) < 0)) + pages |> push(full) + } + } + pages |> sort() + return <- pages +} + +// module mounts only: file-relative requires (./x.das, ../y.das, rewritten siblings) +// and on-page modules are the page's own problem, not a binary gap +def probe_target(req : string) : string { + var t = strip_comment(req) + return "" if (!(t |> starts_with("require "))) + t = trim(slice(t, length("require "))) + if (t |> ends_with(" public")) { + t = trim(trim_suffix(t, " public")) + } + return "" if (empty(t) || find(t, ".") >= 0 || (t |> starts_with("daslib/"))) + return t +} + +def run_probes(require_lines : table; file_mods : table; cfg : Config) : array { + var failures : array + let probe_dir = path_join(cfg.out, "probes") + mkdir_rec(probe_dir) + var probed : table // target -> from_companion (OR-ed) + for (req, from_companion in keys(require_lines), values(require_lines)) { + let target = probe_target(req) + // a module minted by a `file` marker exists only beside its synthetic page + continue if (empty(target) || key_exists(file_mods, target)) + let prev = probed?[target] ?? false + probed[target] = prev || from_companion + } + // write ALL probe files before the first popen, then compile and rebuild + // messages from file contents — string handles derived from table keys read + // back corrupted after run_and_capture, so the filesystem is the stable store + var origins : array + for (target, from_companion in keys(probed), values(probed)) { + fwrite(path_join(probe_dir, "probe_{length(origins)}.das"), "require {target}\n") + origins |> push(from_companion) + } + for (idx in range(length(origins))) { + let pfile = path_join(probe_dir, "probe_{idx}.das") + var out : string + if (compile_file_rc(cfg, pfile, out) != 0) { + failures |> push((origins[idx] ? "[companion] " : "[page] ") + trim(fread(pfile))) + } + } + return <- failures +} + +def report_page(page_path : string; cfg : Config) : PageReport { + let page <- parse_page(page_path) + var rep = PageReport(page = to_generic_path(page_path), das_blocks = length(page.blocks)) + if (empty(page.blocks)) { + rep.status = "no_blocks" + return <- rep + } + var em <- emit_page(page, cfg) + rep.checked = em.checked + rep.skipped = em.skipped + rep.fragments = em.fragments + rep.companion_backed = em.companion_backed + rep.status = "green" + for (f in em.files) { + var out : string + if (compile_file_rc(cfg, f, out) != 0) { + rep.status = "red" + rep.errors |> push_from(first_lines(out, 24)) + } + } + // expects append to the main page program, not an alt program + var expect_base = "" + for (f in em.files) { + if (find(f, "__alt_") < 0) { + expect_base = f + break + } + } + if (rep.status == "green" && !empty(expect_base)) { + check_expects(em, cfg, expect_base, rep) + } + return <- rep +} + +[export] +def main() : int { + var cfg = Config() + let rc = parse_args_with_help(cfg, "doc-verify") + return rc if (rc >= 0) + mkdir_rec(path_join(cfg.out, "pages")) + let pages <- scan_pages(cfg) + to_log(LOG_INFO, "doc-verify: {length(pages)} pages\n") + + // pass 1: parse+emit everything, collecting the corpus require set for rule 0 + var require_origin : table // require line -> seen-from-companion + var file_mods : table // modules minted by `file` markers + for (p in pages) { + if (cfg.verbose) { + to_log(LOG_INFO, " emit {p}\n") + } + let page <- parse_page(p) + continue if (empty(page.blocks)) + var em <- emit_page(page, cfg) + for (r in em.require_lines) { + let prev = require_origin?[r] ?? false + require_origin[r] = prev || key_exists(em.companion_requires, r) + } + for (f in keys(em.file_written)) { + let s = stem(f) + if (!key_exists(file_mods, s)) { + file_mods |> insert(s) + } + } + } + + if (!cfg.no_probes) { + let failures <- run_probes(require_origin, file_mods, cfg) + var fatal = false + for (f in failures) { + to_log(LOG_ERROR, "rule-0 probe failed: {f}\n") + fatal ||= f |> starts_with("[companion]") + } + if (fatal) { + to_log(LOG_ERROR, "doc-verify: binary gap — companion-backed requires failed to load; build the missing modules and re-run\n") + return 2 + } + } + + // pass 2: verdicts + var sweep = SweepReport(total_pages = length(pages)) + sweep.pages |> reserve(length(pages)) + for (i, p in count(), pages) { + var rep <- report_page(p, cfg) + if (rep.status == "green") { + sweep.green++ + } elif (rep.status == "red") { + sweep.red++ + } else { + sweep.no_blocks++ + } + if (cfg.verbose || rep.status == "red") { + to_log(LOG_INFO, " [{rep.status}] {rep.page}\n") + } + if ((i + 1) % 25 == 0) { + to_log(LOG_INFO, " ... {i + 1}/{length(pages)}\n") + } + sweep.pages |> emplace(rep) + } + + let json_path = empty(cfg.json) ? path_join(cfg.out, "report.json") : cfg.json + fwrite(json_path, sprint_json(sweep, true)) + to_log(LOG_INFO, "doc-verify: {sweep.green} green, {sweep.red} red, {sweep.no_blocks} without das blocks; report: {json_path}\n") + return sweep.red > 0 ? 1 : 0 +}