From 19f62ef0b14c9463616c12f7b8aad62821235229 Mon Sep 17 00:00:00 2001 From: Boris Batkin Date: Wed, 18 Feb 2026 06:22:29 -0800 Subject: [PATCH 1/5] docuemntation bugs --- .../reference/tutorials/20_lifetime.rst | 4 +- .../standalone_context.das.cpp | 177 ------------------ .../standalone_context.das.h | 17 -- tutorials/language/20_lifetime.das | 4 +- 4 files changed, 6 insertions(+), 196 deletions(-) delete mode 100644 tutorials/integration/cpp/_standalone_ctx_generated/standalone_context.das.cpp delete mode 100644 tutorials/integration/cpp/_standalone_ctx_generated/standalone_context.das.h diff --git a/doc/source/reference/tutorials/20_lifetime.rst b/doc/source/reference/tutorials/20_lifetime.rst index 3e06f9b55e..1b140248ab 100644 --- a/doc/source/reference/tutorials/20_lifetime.rst +++ b/doc/source/reference/tutorials/20_lifetime.rst @@ -96,7 +96,9 @@ For class instances created with ``new``, ``delete`` requires ``unsafe``:: Or use ``var inscope`` for automatic cleanup:: - var inscope p = new MyClass() + unsafe { // needs 'unsafe', because deleting classes is unsafe + var inscope p = new MyClass() + } // deleted automatically at end of scope When to use what diff --git a/tutorials/integration/cpp/_standalone_ctx_generated/standalone_context.das.cpp b/tutorials/integration/cpp/_standalone_ctx_generated/standalone_context.das.cpp deleted file mode 100644 index 88901a4ab6..0000000000 --- a/tutorials/integration/cpp/_standalone_ctx_generated/standalone_context.das.cpp +++ /dev/null @@ -1,177 +0,0 @@ -#include "daScript/simulate/standalone_ctx_utils.h" - // require builtin -#include "daScript/simulate/bin_serializer.h" -#include "daScript/simulate/runtime_profile.h" -#include "standalone_context.das.h" - -#if defined(_MSC_VER) -#pragma warning(push) -#pragma warning(disable:4100) // unreferenced formal parameter -#pragma warning(disable:4189) // local variable is initialized but not referenced -#pragma warning(disable:4244) // conversion from 'int32_t' to 'float', possible loss of data -#pragma warning(disable:4114) // same qualifier more than once -#pragma warning(disable:4623) // default constructor was implicitly defined as deleted -#pragma warning(disable:4946) // reinterpret_cast used between related classes -#pragma warning(disable:4269) // 'const' automatic data initialized with compiler generated default constructor produces unreliable results -#pragma warning(disable:4555) // result of expression not used -#endif -#if defined(__EDG__) -#pragma diag_suppress 826 -#elif defined(__GNUC__) && !defined(__clang__) -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wunused-parameter" -#pragma GCC diagnostic ignored "-Wunused-variable" -#pragma GCC diagnostic ignored "-Wunused-function" -#pragma GCC diagnostic ignored "-Wwrite-strings" -#pragma GCC diagnostic ignored "-Wreturn-local-addr" -#pragma GCC diagnostic ignored "-Wignored-qualifiers" -#pragma GCC diagnostic ignored "-Wsign-compare" -#pragma GCC diagnostic ignored "-Wsubobject-linkage" -#endif -#if defined(__clang__) -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wunused-parameter" -#pragma clang diagnostic ignored "-Wwritable-strings" -#pragma clang diagnostic ignored "-Wunused-variable" -#pragma clang diagnostic ignored "-Wunused-but-set-variable" -#pragma clang diagnostic ignored "-Wunsequenced" -#pragma clang diagnostic ignored "-Wunused-function" -#endif - -namespace das { -namespace _anon_3883122988366833237 { - -extern TypeInfo __type_info__af90fe4c864e9d52; -extern TypeInfo __type_info__af63e44c8601fa24; -extern TypeInfo __type_info__af63eb4c86020609; - -FuncInfo __func_info__7f4e3d0819c1de70 = {"test", "", nullptr, 0, 64, &__type_info__af63eb4c86020609, nullptr,0,UINT64_C(0x7f4e3d0819c1de70), 0x0 }; -TypeInfo __type_info__af90fe4c864e9d52 = { Type::tString, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, 0, 0, nullptr, 16420, 8, UINT64_C(0xaf90fe4c864e9d52) }; -TypeInfo __type_info__af63e44c8601fa24 = { Type::tInt, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, 0, 0, nullptr, 28, 4, UINT64_C(0xaf63e44c8601fa24) }; -TypeInfo __type_info__af63eb4c86020609 = { Type::tVoid, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, 0, 0, nullptr, 28, 0, UINT64_C(0xaf63eb4c86020609) }; - -static void resolveTypeInfoAnnotations() -{ - vector annotations = {}; - for (auto& ann : annotations) { - ann.resolveAnnotation(); - } -} - -TypeInfo * __tinfo_0[3] = { &__type_info__af90fe4c864e9d52, &__type_info__af63e44c8601fa24, &__type_info__af90fe4c864e9d52 }; - -inline void test_40a425e56ca28455 ( Context * __context__ ); - -void __init_script ( Context * __context__, bool __init_shared ) -{ -} - -inline void test_40a425e56ca28455 ( Context * __context__ ) -{ - int32_t __total_rename_at_9_0 = 0; - { - bool __need_loop_10 = true; - // i: int const - das_iterator __i_iterator(range(0,10)); - int32_t __i_rename_at_10_1; - __need_loop_10 = __i_iterator.first(__context__,(__i_rename_at_10_1)) && __need_loop_10; - for ( ; __need_loop_10 ; __need_loop_10 = __i_iterator.next(__context__,(__i_rename_at_10_1)) ) - { - __total_rename_at_9_0 += __i_rename_at_10_1; - } - __i_iterator.close(__context__,(__i_rename_at_10_1)); - }; - builtin_print(das_string_builder_temp(__context__,SimNode_AotInterop<3>(__tinfo_0, cast::from(((char *) "Sum of 0..9 = ")), cast::from(__total_rename_at_9_0), cast::from(((char *) "\n")))),__context__,((LineInfoArg *)(&LineInfo::g_LineInfoNULL))); -} -} // namespace _anon_3883122988366833237 -using namespace _anon_3883122988366833237; -namespace standalone_context { -static vec4f __wrap_test_40a425e56ca28455 ( Context * __context__ ) { - test_40a425e56ca28455(__context__); - return v_zero(); -} - -#pragma optimize("", off) -struct AotFunction { uint64_t hash; bool is_cmres; void * fn; vec4f (*wrappedFn)(Context*); }; -static AotFunction functions[] = { - { 0x580c190f73ac3d2, false, (void*)&test_40a425e56ca28455, &__wrap_test_40a425e56ca28455 }, -}; -#pragma optimize("", on) - -static void registerAotFunctions ( AotLibrary & aotLib ) { - for (const auto &[hash, cmres, fn1, fn2] : functions) { - aotLib.emplace(hash, AotFactory(cmres, fn1, fn2)); - } - resolveTypeInfoAnnotations(); -} - -static AotListBase impl(registerAotFunctions); -auto Standalone::test ( ) -> void { - return test_40a425e56ca28455(this); -} - -Standalone::Standalone() { - auto & context = *this; - CodeOfPolicies policies; policies.debugger = false /*policies.debugger*/; - policies.persistent_heap = false; - policies.heap_size_hint = 65536; - policies.string_heap_size_hint = 65536; - context.setup(0/*totalVariables*/, 20 /*globalStringHeapSize*/, policies, {}); - // start totalVariables - // end totalVariables - - context.allocateGlobalsAndShared(); - context.totalVariables = 0/*totalVariables*/; - context.functions = (SimFunction *) context.code->allocate( 1/*totalFunctions*/*sizeof(SimFunction) ); - context.totalFunctions = 1/*totalFunctions*/; - bool anyPInvoke = false; - if ( anyPInvoke || false/*(policies.threadlock_context || policies.debugger)*/ ) { - context.contextMutex = new recursive_mutex; - } - context.tabMnLookup = make_shared>(); - context.tabMnLookup->clear(); - // start totalFunctions - struct FunctionStorage { int idx; FunctionInfo funcInfo; FuncInfo* debugInfo; }; - FunctionStorage usedFunctions[] = { - {0, FunctionInfo("test", "test", 0x7f4e3d0819c1de70, 0x580c190f73ac3d2, 64, false, false, false, false, false, false), &__func_info__7f4e3d0819c1de70}, - }; - // end totalFunctions - vector> id_to_funcs; - for (const auto& [index, func_info, debug_info]: usedFunctions) { - InitAotFunction(context, &context.functions[index], func_info); - context.functions[index].debugInfo = debug_info; - (*context.tabMnLookup)[func_info.mnh] = context.functions + index; - id_to_funcs.emplace_back(func_info.aotHash, &context.functions[index]); - anyPInvoke |= func_info.pinvoke; - } - context.tabGMnLookup = make_shared>(); - context.tabGMnLookup->clear(); - for ( int i=0, is=context.totalVariables; i!=is; ++i ) { - auto mnh = context.globalVariables[i].mangledNameHash; - (*context.tabGMnLookup)[mnh] = context.globalVariables[i].offset; - } - context.tabAdLookup = make_shared>(); - FillFunction(context, getGlobalAotLibrary(), id_to_funcs); - context.runInitScript(); -} -#ifdef STANDALONE_CONTEXT_TESTS -static Context * registerStandaloneTest ( ) { - auto ctx = new StandaloneContext(); - return ctx; -} -StandaloneContextNode node(registerStandaloneTest); -#endif -} // namespace standalone_context -} // namespace das - -#if defined(_MSC_VER) -#pragma warning(pop) -#endif -#if defined(__EDG__) -#pragma diag_default 826 -#elif defined(__GNUC__) && !defined(__clang__) -#pragma GCC diagnostic pop -#endif -#if defined(__clang__) -#pragma clang diagnostic pop -#endif diff --git a/tutorials/integration/cpp/_standalone_ctx_generated/standalone_context.das.h b/tutorials/integration/cpp/_standalone_ctx_generated/standalone_context.das.h deleted file mode 100644 index 87e0a2a3d5..0000000000 --- a/tutorials/integration/cpp/_standalone_ctx_generated/standalone_context.das.h +++ /dev/null @@ -1,17 +0,0 @@ -#include "daScript/misc/platform.h" - -#include "daScript/simulate/simulate.h" -#include "daScript/simulate/aot.h" -#include "daScript/simulate/aot_library.h" - -namespace das { -namespace standalone_context { - - -class Standalone : public Context { -public: - Standalone(); - auto test ( ) -> void; -}; -} // namespace standalone_context -} // namespace das diff --git a/tutorials/language/20_lifetime.das b/tutorials/language/20_lifetime.das index 3146cdc416..ca50b05781 100644 --- a/tutorials/language/20_lifetime.das +++ b/tutorials/language/20_lifetime.das @@ -111,7 +111,9 @@ def main { // unsafe { delete p } // // Or use 'var inscope' for automatic cleanup: - // var inscope p = new MyClass() + // unsafe { // needs 'unsafe', because deleting classes is unsafe + // var inscope p = new MyClass() + // } // // deleted automatically at end of scope // === When to use what === From 965ee725b20c3226021c67fae00a847900b3c022 Mon Sep 17 00:00:00 2001 From: Iskander Sharipov Date: Wed, 18 Feb 2026 18:43:36 +0400 Subject: [PATCH 2/5] dastest: use inscope var instead of explicit delete Keeping the less obvious case with unsafe+defer for now --- dastest/dastest.das | 3 +-- dastest/suite.das | 15 +++++---------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/dastest/dastest.das b/dastest/dastest.das index 7ecb2a5c72..48a49273fd 100644 --- a/dastest/dastest.das +++ b/dastest/dastest.das @@ -28,7 +28,7 @@ def private collect_input_paths(args : array; var paths : array) def private collect_files(input_paths : array; var files : array) : bool { - var cache : table + var inscope cache : table for (path in input_paths) { if (path |> ends_with(".das")) { if (!cache |> key_exists(path)) { @@ -42,7 +42,6 @@ def private collect_files(input_paths : array; var files : array } } } - delete cache return true } diff --git a/dastest/suite.das b/dastest/suite.das index ac30cb4216..fd11a5b11b 100644 --- a/dastest/suite.das +++ b/dastest/suite.das @@ -95,9 +95,8 @@ def private match_test_name(name : string; ctx : SuiteCtx) { def test_file(file_name : string; ctx : SuiteCtx) : SuiteResult { - var fileCtx <- internalFileCtx(ctx) + var inscope fileCtx <- internalFileCtx(ctx) var res = test_file(file_name, ctx, fileCtx) - delete fileCtx return res } @@ -207,9 +206,8 @@ def test_file(file_name : string; ctx : SuiteCtx; file_ctx : FileCtx) : SuiteRes def test_module(var mod : rtti::Module; var context : rtti::Context; suite_ctx : SuiteCtx) : SuiteResult { - var fileCtx <- internalFileCtx(suite_ctx) + var inscope fileCtx <- internalFileCtx(suite_ctx) var res = test_module(mod, context, suite_ctx, fileCtx) - delete fileCtx return res } @@ -219,7 +217,7 @@ def test_module(var mod : rtti::Module; var context : rtti::Context; suite_ctx : return default } var res : SuiteResult - var fileCtx := file_ctx + var inscope fileCtx := file_ctx var modPtr : Module? unsafe { fileCtx.context = addr(context) @@ -249,7 +247,6 @@ def test_module(var mod : rtti::Module; var context : rtti::Context; suite_ctx : } } } - delete fileCtx return res } @@ -296,19 +293,17 @@ def private test_any_lambda(name : string; func : lambda; args_num : int; var co [export] def private sub_test_any_func(name : string; func : function; args_num : int; var context : FileCtx; var res : SuiteResult&) { - var subContext := context + var inscope subContext := context subContext.indenting = "\t" + subContext.indenting test_any(name, func, args_num, subContext, res) - delete subContext } [export] def private sub_test_any_lambda(name : string; func : lambda; args_num : int; var context : FileCtx; var res : SuiteResult&) { - var subContext := context + var inscope subContext := context subContext.indenting = "\t" + subContext.indenting test_any(name, func, args_num, subContext, res) - delete subContext } From b29bdcd9df25cca979f6c1568c4b4e7166d0de48 Mon Sep 17 00:00:00 2001 From: Boris Batkin Date: Wed, 18 Feb 2026 07:01:36 -0800 Subject: [PATCH 3/5] skills --- .github/copilot-instructions.md | 498 ++------------------------------ CLAUDE.md | 498 ++------------------------------ skills/cpp_integration.md | 251 ++++++++++++++++ skills/das_formatting.md | 24 ++ skills/daslib_modules.md | 33 +++ skills/documentation_rst.md | 118 ++++++++ skills/writing_tests.md | 39 +++ 7 files changed, 511 insertions(+), 950 deletions(-) create mode 100644 skills/cpp_integration.md create mode 100644 skills/das_formatting.md create mode 100644 skills/daslib_modules.md create mode 100644 skills/documentation_rst.md create mode 100644 skills/writing_tests.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 25c4b3e90f..f797e66f07 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,6 +1,6 @@ # daslang Project Instructions -> **Keep in sync:** This file and `CLAUDE.md` (repo root) share identical content. When updating one, copy the changes to the other so both GitHub Copilot and Claude Code see the same project instructions. +> **Keep in sync:** This file and `CLAUDE.md` (repo root) share identical content. Both reference skill files in the `skills/` directory at repo root — skill files are shared, not duplicated. ## Project Overview @@ -15,30 +15,19 @@ This is the [daslang](https://dascript.org/) programming language repository (Ga - **Run a script:** `bin/Release/daslang.exe path/to/script.das` - **Run tests:** `bin/Release/daslang.exe dastest/dastest.das -- --test path/to/test.das` -## Code Formatting (REQUIRED) +## Skill Files (REQUIRED) -After creating or modifying any `.das` file that is part of the project (daslib modules, tutorials, tests, etc.), run the source formatter on it. Do NOT format temporary/scratch files that will be deleted. +Task-specific instructions are split into skill files under `skills/`. You MUST read the relevant skill file(s) before performing the corresponding task. -**Formatter tool:** `utils/dasCodeFormatter/main.das` +| Skill file | Read BEFORE... | +|---|---| +| `skills/das_formatting.md` | Creating or modifying any `.das` file (tutorials, tests, daslib modules, utilities) | +| `skills/writing_tests.md` | Writing or editing test files under `tests/` | +| `skills/documentation_rst.md` | Editing RST files in `doc/source/`, editing `//!` doc-comments in `daslib/*.das`, or writing tutorial RST pages | +| `skills/cpp_integration.md` | Writing or editing C++ files in `src/`, `modules/`, or `tutorials/integration/cpp/` | +| `skills/daslib_modules.md` | Working with `daslib/` modules (linq, json, regex, functional, match, etc.) or extending the standard library | -**Procedure:** - -1. **Back up** the file before formatting: copy it to `.das.bak` -2. **Run the formatter:** `bin/Release/daslang.exe utils/dasCodeFormatter/main.das -- path/to/file.das` -3. **Verify** the formatted file still compiles: `bin/Release/daslang.exe path/to/file.das` - - For test files (no `main`), compile-check with: `bin/Release/daslang.exe dastest/dastest.das -- --test path/to/test.das` - - For module files (no `main`), verify by running a file that requires them -4. **Remove the backup** if formatting succeeded: delete `.das.bak` -5. **Restore from backup** if formatting broke the file: copy `.das.bak` back over the `.das` file, delete the backup, and report the issue - -**When to format:** -- New `.das` files: tutorials, tests, daslib modules, utilities -- Modified `.das` files: after any edits to existing project files - -**When NOT to format:** -- Temporary/scratch files that will be deleted immediately -- Files you are only reading, not modifying -- C++ source files, RST docs, Python scripts, etc. (only `.das` files) +Multiple skill files may apply to a single task. For example, creating a new daslib module requires reading `skills/das_formatting.md`, `skills/daslib_modules.md`, and possibly `skills/documentation_rst.md`. ## daslang Language — Gen2 Syntax (REQUIRED) @@ -115,6 +104,18 @@ All code examples and documentation MUST use gen2 syntax (add `options gen2` at - When writing a library generic that should dispatch to user-provided overloads, use `_::` prefix - `mem_archive_save` does NOT use `_::serialize` — so user serialize overloads are invisible; use manual `Archive` + `MemSerializer` pattern instead +### Table operations + +- `table[key]` **inserts** a new default entry if `key` is missing — use only when you want insert-on-access +- `table[key]` requires `unsafe` by default; add `options unsafe_table_lookup = false` to allow safe `[]` access +- `table?[key] ?? default_value` — safe lookup with fallback, does NOT insert missing keys +- `key_exists(table, key)` — check if a key is present without inserting +- `table |> insert(key, value)` — insert into `table`; `table |> insert(key)` — insert into set `table` +- `table |> erase(key)` — remove a key +- **Never use two `[]` lookups on the same table in one expression** (e.g. `tab[k1] = tab[k2]`) — tables are unboxed containers and re-hashing on insert can invalidate the first reference +- `find(table, key) <| $(pval) { ... }` — block-based lookup; block receives pointer to value if found +- `get(table, key, blk)` — similar block-based access (see `daslib/builtin.das`) + ### Common gotchas - Lambda params can shadow function params — use distinct names (e.g., `$(lhs, rhs)` not `$(a, b)` when `a` is already in scope) @@ -155,459 +156,6 @@ All code examples and documentation MUST use gen2 syntax (add `options gen2` at - `tests/regex/` — Regex module tests (8 test files, 278 tests) - `modules/` — External plugin modules -## Standard Library Documentation - -The stdlib docs live in `doc/source/stdlib/` and are generated from `//!` doc-comments in `daslib/*.das`. - -### Documentation pipeline - -1. `daslib/*.das` files contain `//!` comments for each module, struct, function -2. `doc/reflections/rst_comment.das` extracts comments into `doc/source/stdlib/detail/*.rst` -3. `doc/reflections/das2rst.das` + `doc/reflections/rst.das` combine detail RST with handmade content -4. **`doc/source/stdlib/handmade/`** — manually written module descriptions and examples (2,001 files) -5. Final RST output goes to `doc/source/stdlib/*.rst` - -### Key documentation tools - -- `doc/reflections/gen_module_examples.py` — generates/updates all 86 module-*.rst files with descriptions and compilable examples (31 modules have examples) -- `doc/reflections/test_new_examples.py` — tests all example snippets by running them through `daslang.exe` -- `doc/reflections/fix_short_docs.py` — fixes terse function documentation -- Validate: `bin/Release/daslang.exe doc/reflections/das2rst.das` (exit code 0 = success) - -### Function grouping in generated docs - -Functions in each module's RST are organized into named groups (e.g. "Compilation and validation", "Access", "Match & replace"). Groups are defined in `doc/reflections/das2rst.das` via `group_by_regex("Group Name", mod, %regex~(func1|func2)$%%)`. Any public function not matched by a group regex ends up in an **"Uncategorized"** section. After adding new public functions to a module: - -1. Add the function name to the appropriate `group_by_regex` call in the module's `document_module_*` function in `das2rst.das` -2. Create or update the handmade doc file in `doc/source/stdlib/handmade/` (replace `// stub` with a description) -3. Regenerate: `bin/Release/daslang.exe doc/reflections/das2rst.das` -4. Verify no "Uncategorized" section remains: search for `Uncategorized` in the generated `doc/source/stdlib/*.rst` - -### Adding a module example - -1. Add `example="""..."""` to the `reg()` call in `gen_module_examples.py` -2. Test it: add to `test_new_examples.py` and run `python doc/reflections/test_new_examples.py` -3. Regenerate: `python doc/reflections/gen_module_examples.py` -4. Validate: `bin/Release/daslang.exe doc/reflections/das2rst.das` - -## Documentation Conventions - -When editing RST files in `doc/source/reference/language/`: - -- All code blocks use `.. code-block:: das` with gen2 syntax -- Include `// output:` comments showing expected output for runnable examples -- Add `require` statements when examples need imports -- Use `:ref:` cross-references to link between pages (labels: `_structs`, `_classes`, `_functions`, `_statements`, `_expressions`, `_arrays`, `_tables`, `_iterators`, `_generators`, `_lambdas`, `_blocks`, `_tuples`, `_variants`, `_bitfields`, `_aliases`, `_modules`, `_options`, `_unsafe`, `_enumerations`, `_generic_programming`, `_pattern-matching`, `_comprehensions`, `_string_builder`, `_macros`, `_reification`, `_finalizers`, `_clone`, `_temporary`, `_move_copy_clone`, `_annotations`, `_program_structure`, `_type_conversions`, `_contexts`, `_locks`, `_datatypes_and_values`, `_pointers`) -- Verify examples compile: `bin/Release/daslang.exe example.das` - -### RST table rules - -RST uses two table formats — **grid tables** and **simple tables**. Both are fragile: - -- **Grid tables** (`+---+---+`): Every row line must be exactly the same width as every separator line. Off-by-one spaces cause Sphinx errors. -- **Simple tables** (`=== ===`): The `=` separator defines column widths. Content in non-last columns must NOT extend past its column's `=` boundary. Headers must start at or after the column's start position (not in the gap). The gap between columns must be at least 2 spaces. -- After creating or editing any RST table, verify the file with a Sphinx build (see below). - -### Documentation workflow (REQUIRED) - -After creating or modifying any RST files, stdlib documentation, or `daslib/*.das` module doc-comments: - -1. **Regenerate stdlib docs** (if `daslib/*.das` files or `doc/reflections/das2rst.das` were changed): - ``` - bin/Release/daslang.exe doc/reflections/das2rst.das - ``` - -2. **Clean Sphinx build** — MUST delete cache; cached builds hide errors: - ``` - cd doc - Remove-Item -Recurse -Force sphinx-build # delete doctree cache - Remove-Item -Recurse -Force ../site/doc # delete HTML output - sphinx-build -b html -d sphinx-build source ../site/doc - ``` - On Linux/Mac: - ``` - cd doc - rm -rf sphinx-build ../site/doc - sphinx-build -b html -d sphinx-build source ../site/doc - ``` - -3. **Verify no new errors or warnings**: Check the build output for `ERROR` and `WARNING`. The build must introduce **no new** Sphinx errors or warnings compared to the baseline. - -**When to run the workflow:** -- New or modified RST files (language docs, tutorials, stdlib docs) -- New or modified `//!` doc-comments in `daslib/*.das` files -- Changes to `doc/reflections/das2rst.das` or `doc/reflections/rst.das` -- New public functions added to any `daslib/*.das` module (also update `group_by_regex` in `das2rst.das`) - -### Tutorial RST conventions - -Tutorial RST files live in `doc/source/reference/tutorials/` with companion `.das` files in `tutorials/language/`. - -- Each RST starts with a label: `.. _tutorial_name:` (e.g., `.. _tutorial_linq:`) -- Include `.. index::` directive with relevant `single: Tutorial; Topic` entries -- Code blocks use `.. code-block:: das` with gen2 syntax -- End each RST with a `.. seealso::` block containing: - - Full source as `:download:` link: `Full source: :download:\`tutorials/language/XX_name.das <../../../../tutorials/language/XX_name.das>\`` - - Next tutorial link (except last): `Next tutorial: :ref:\`tutorial_next_name\`` - - Related language reference links via `:ref:` -- Toctree is in `doc/source/reference/tutorials.rst` — add new tutorials there - -### C++ integration tutorial RST conventions - -C++ integration tutorial RST files live in `doc/source/reference/tutorials/` with `.cpp` and `.das` files in `tutorials/integration/cpp/`. - -- Label pattern: `.. _tutorial_integration_cpp_:` (e.g., `.. _tutorial_integration_cpp_binding_types:`) -- Index entries: `single: Tutorial; C++ Integration; ` -- Code blocks: `.. code-block:: cpp` for C++ code, `.. code-block:: das` for daslang code -- Build & run section with `cmake --build` command and expected output -- End with `.. seealso::` containing: - - `:download:` links for both `.cpp` and `.das` source files - - Previous/Next tutorial links via `:ref:` -- Each tutorial is one self-contained `.cpp` file with embedded `main()` — no separate build infrastructure needed beyond CMake target -- Tutorial CMake targets: `integration_cpp_01` through `integration_cpp_NN` (defined in `tutorials/integration/cpp/CMakeLists.txt`) - -- Tutorial labels for cross-references: `tutorial_hello_world`, `tutorial_variables`, `tutorial_operators`, `tutorial_control_flow`, `tutorial_functions`, `tutorial_arrays`, `tutorial_strings`, `tutorial_structs`, `tutorial_enumerations`, `tutorial_tables`, `tutorial_tuples_and_variants`, `tutorial_function_pointers`, `tutorial_blocks`, `tutorial_lambdas`, `tutorial_iterators_and_generators`, `tutorial_modules`, `tutorial_move_copy_clone`, `tutorial_classes`, `tutorial_generics`, `tutorial_lifetime`, `tutorial_error_handling`, `tutorial_unsafe`, `tutorial_string_format`, `tutorial_pattern_matching`, `tutorial_annotations`, `tutorial_contracts`, `tutorial_testing`, `tutorial_linq`, `tutorial_functional`, `tutorial_json`, `tutorial_regex`, `tutorial_operator_overloading`, `tutorial_pointers`, `tutorial_utility_patterns`, `tutorial_random`, `tutorial_dynamic_type_checking`, `tutorial_coroutines`, `tutorial_serialization`, `tutorial_testing_tools` -- C++ integration tutorial labels: `tutorial_integration_cpp_hello_world`, `tutorial_integration_cpp_calling_functions`, `tutorial_integration_cpp_binding_functions`, `tutorial_integration_cpp_binding_types`, `tutorial_integration_cpp_binding_enums`, `tutorial_integration_cpp_interop`, `tutorial_integration_cpp_callbacks`, `tutorial_integration_cpp_methods`, `tutorial_integration_cpp_operators_and_properties` -- C++ integration tutorial plan (remaining): 10 Custom Modules, 11 Context Variables, 12 Smart Pointers & GC, 13 AOT, 14 Serialization, 15 Custom Annotations, 16 Sandbox - -## C++ Integration Patterns - -These patterns are used in C++ host applications that embed daslang. - -### Host program boilerplate - -Every C++ host that runs daslang scripts follows this pattern: - -```cpp -#include "daScript/daScript.h" -using namespace das; - -void run_script() { - TextPrinter tout; - ModuleGroup dummyLibGroup; - auto fAccess = make_smart(); - auto program = compileDaScript(getDasRoot() + "/path/to/script.das", - fAccess, tout, dummyLibGroup); - if (program->failed()) { /* report errors */ return; } - Context ctx(program->getContextStackSize()); - if (!program->simulate(ctx, tout)) { /* report errors */ return; } - auto fn = ctx.findFunction("test"); - ctx.evalWithCatch(fn, nullptr); -} - -int main(int, char * []) { - NEED_ALL_DEFAULT_MODULES; - NEED_MODULE(Module_MyModule); // custom modules - Module::Initialize(); - run_script(); - Module::Shutdown(); - return 0; -} -``` - -### Creating a module - -Derive from `Module`, register types/functions/enums in the constructor, then use `REGISTER_MODULE`: - -```cpp -class Module_MyMod : public Module { -public: - Module_MyMod() : Module("my_module_name") { - ModuleLibrary lib(this); - lib.addBuiltInModule(); - // addAnnotation, addExtern, addEnumeration, addConstant ... - } -}; -REGISTER_MODULE(Module_MyMod); -``` - -The host uses `NEED_MODULE(Module_MyMod)` before `Module::Initialize()`. Scripts access it via `require my_module_name`. - -### Callbacks — `TBlock<>`, `TFunc<>`, `TLambda<>`, `das_invoke*` - -Three closure types exist, each with a typed template and an invocation helper: - -| Type | Template | Invocation | Lifetime | -|------|----------|------------|----------| -| Block | `TBlock` | `das_invoke::invoke(ctx, at, blk, args...)` | Stack-bound — valid only during the call | -| Func | `TFunc` (or untyped `Func`) | `das_invoke_function::invoke(ctx, at, fn, args...)` | Context-bound — storable | -| Lambda | `TLambda` (or untyped `Lambda`) | `das_invoke_lambda::invoke(ctx, at, lmb, args...)` | Heap-allocated — captures variables | - -**Typed vs untyped**: `TBlock` maps to `block<(arg:int):int>` in daslang — the compiler checks signatures. Untyped `Lambda` maps to `lambda<>` and will **not** match typed lambdas like `lambda<(x:int):int>`. Prefer typed templates. - -**Block callback example**: - -```cpp -void with_values(int32_t a, int32_t b, - const TBlock & blk, - Context * context, LineInfoArg * at) { - das_invoke::invoke(context, at, blk, a, b); -} - -addExtern(*this, lib, "with_values", - SideEffects::invoke, "with_values") - ->args({"a", "b", "blk", "context", "at"}); -``` - -Use `SideEffects::invoke` for any function that invokes script callbacks. - -In daslang: blocks use `<|` with `$()` prefix, function pointers use `@@func_name`, lambdas use `@(args) { body }`. - -### Calling daslang functions from C++ — `das_invoke_function` - -The high-level `das_invoke_function::invoke(ctx, at, fnPtr, arg1, arg2, ...)` handles argument marshalling automatically. Preferred over raw `cast<>` + `evalWithCatch`. - -### Binding C++ functions — `addExtern` + `DAS_BIND_FUN` - -```cpp -addExtern(*this, lib, "das_name", - SideEffects::none, "cpp_function") - ->args({"param1", "param2"}); -``` - -`SideEffects` flags: `none` (pure), `modifyExternal` (stdout/files), `modifyArgument` (mutates ref params), `accessGlobal` (reads shared state), `invoke` (calls daslang), `worstDefault` (safe fallback). - -### Binding C++ types — `MAKE_TYPE_FACTORY` + `ManagedStructureAnnotation` - -1. **`MAKE_TYPE_FACTORY(DasName, CppType)`** at file scope — creates `typeFactory` + `typeName` -2. **`ManagedStructureAnnotation`** — describe fields with `addField("name", "name")` -3. **`addAnnotation(make_smart(lib))`** in the module — order matters: if type B contains type A, register A first -4. Functions returning bound types by value require **`SimNode_ExtFuncCallAndCopyOrMove`** template parameter in `addExtern` - -**Handled types are reference types** — important consequences for scripts: -- Mutable local variables (`var`) of handled types require `unsafe` blocks -- Immutable locals (`let`) returned from factory functions work without `unsafe` -- **Factory function pattern**: provide `make_xxx()` functions returning by value so scripts can create instances ergonomically with `let x = make_xxx(...)` — no `unsafe` needed -- POD structs (no default member initializers, no virtual functions) work best with `ManagedStructureAnnotation` - -### Binding C++ methods — `DAS_CALL_MEMBER` + `DAS_CALL_METHOD` - -daslang has no member functions — "methods" are free functions where the first argument is `self`. Pipe syntax (`obj |> method()`) provides method-call ergonomics. - -```cpp -// Step 1: Create wrapper aliases -using method_increment = DAS_CALL_MEMBER(Counter::increment); -using method_get = DAS_CALL_MEMBER(Counter::get); - -// Step 2: Register with addExtern -addExtern(*this, lib, "increment", - SideEffects::modifyArgument, - DAS_CALL_MEMBER_CPP(Counter::increment)) - ->args({"self"}); - -addExtern(*this, lib, "get", - SideEffects::none, - DAS_CALL_MEMBER_CPP(Counter::get)) - ->args({"self"}); -``` - -- **Non-const methods**: `SideEffects::modifyArgument` (they mutate the object) -- **Const methods**: `SideEffects::none` -- `DAS_CALL_MEMBER_CPP(Class::method)` provides the AOT-compatible name string - -### Binding operators and properties - -**Operators**: register functions with the operator symbol as the daslang name: - -```cpp -addExtern( - *this, lib, "+", SideEffects::none, "vec3_add")->args({"a", "b"}); -// Unary: addExtern<...>(*this, lib, "-", ...)->args({"a"}); -``` - -Available operator names: `+`, `-`, `*`, `/`, `%`, `<<`, `>>`, `<`, `>`, `<=`, `>=`, `&`, `|`, `^`. - -**Equality**: `addEquNeq(*this, lib)` binds both `==` and `!=` (requires `operator==` and `operator!=` on T). - -**Properties**: method calls disguised as field access in `ManagedStructureAnnotation`: - -```cpp -addProperty("length", "length"); -``` - -In daslang, `v.length` calls `Vec3::length()` — looks like a field, calls a method. - -### Binding C++ enums — `DAS_BASE_BIND_ENUM` - -```cpp -// MUST come BEFORE `using namespace das` to avoid name collisions -DAS_BASE_BIND_ENUM(CppEnum, DasName, Value1, Value2, Value3) - -using namespace das; - -// In module constructor: -addEnumeration(make_smart()); -``` - -- `DAS_BASE_BIND_ENUM` creates class `EnumerationDasName` + `typeFactory` -- `DAS_BIND_ENUM_CAST(CppEnum)` — explicit `cast<>` specialization (often not needed, SFINAE default suffices) -- `DAS_BASE_BIND_ENUM_98` — for unscoped (C-style) enums -- **Critical**: place enum macros BEFORE `using namespace das` — the macros define names inside `namespace das` that collide with global enum names -- **Name collision pitfall**: `das::LogLevel` is defined internally in `include/daScript/misc/string_writer.h` — do NOT name your enum `LogLevel` when `using namespace das` -- Manual construction alternative: `make_smart("Name")` + `pEnum->addIEx("Value", "CppEnum::Value", intValue, LineInfo())` - -### Low-level interop — `addInterop` - -`addInterop` binds a C++ function that receives raw simulation-level arguments (`vec4f *`), the call node (`SimNode_CallBase *`), and the context. Unlike `addExtern`, it supports **"any type" arguments** — when a template parameter is `vec4f`, it means the argument can be any daslang type. The function inspects `call->types[i]` (`TypeInfo *`) at runtime to determine what was actually passed. - -**Signature**: the C++ function must match `InteropFunction`: - -```cpp -// typedef vec4f (*InteropFunction)(Context &, SimNode_CallBase *, vec4f *); -vec4f my_interop(Context & context, SimNode_CallBase * call, vec4f * args) { - TypeInfo * ti = call->types[0]; // type info for first argument - // ... inspect ti->type, ti->structType, etc. - return v_zero(); -} -``` - -**Registration**: - -```cpp -addInterop( - *this, lib, "das_name", SideEffects::none, "my_interop"); -``` - -Where `vec4f` as an `ArgType` means "any type" — the argument accepts any daslang value. Concrete types (e.g. `int32_t`, `const char *`, `const Block &`) are also valid and work like `addExtern`. - -**Key capabilities** (vs `addExtern`): -- Access to `call->types[]` — per-argument `TypeInfo` with full type metadata -- Access to `call->debugInfo` — source location of the call site -- `vec4f` argument type = "any" — accept arguments of any daslang type -- Used internally for `sprint`, `hash`, `write`, `binary_save/load`, `invoke_in_context` - -**TypeInfo union warning**: `TypeInfo` has a union — `structType`, `enumType`, and `annotation_or_name` share the same memory. Which member is valid depends on `ti->type`: -- `tStructure` → `ti->structType` (StructInfo *) -- `tEnumeration` / `tEnumeration8` / `tEnumeration16` → `ti->enumType` (EnumInfo *) -- `tHandle` → use `ti->getAnnotation()` (resolves tagged pointer safely) - -Accessing the wrong union member is **undefined behavior**. `das_to_string(Type::tHandle)` returns an empty string — use `ti->getAnnotation()->name` for handled type names. - -**Example** — `new_and_init` allocates and initializes any struct: - -```cpp -vec4f new_and_init(Context & context, SimNode_CallBase * call, vec4f * args) { - TypeInfo * typeInfo = call->types[0]; - if (typeInfo->type != Type::tStructure) - context.throw_error_at(call->debugInfo, "expected struct"); - auto size = getTypeSize(typeInfo); - auto data = context.allocate(size, &call->debugInfo); - if (typeInfo->structType && typeInfo->structType->init_mnh) { - auto fn = context.fnByMangledName(typeInfo->structType->init_mnh); - context.callWithCopyOnReturn(fn, nullptr, data, 0); - } else { - memset(data, 0, size); - } - return cast::from(data); -} - -addInterop(*this, lib, "new_and_init", - SideEffects::none, "new_and_init"); -``` - -## C++ Codebase Notes - -- Main type inference: `src/ast/ast_infer_type.cpp` (implementation) + `include/daScript/ast/ast_infer_type.h` (class declarations for `CaptureLambda` and `InferTypes`) -- Builtin runtime functions: `src/builtin/module_builtin_runtime.cpp` -- Smart pointer builtins: `move`, `move_new`, `smart_ptr_clone`, `smart_ptr_use_count` -- Compilation errors: `include/daScript/ast/compilation_errors.h` (error codes 10001–40214) -- Lexer: `src/parser/ds2_lexer.lpp` -- Parser: `src/parser/ds2_parser.ypp` - -### Key AST function flags - -- `func.flags.isClassMethod` — function is a struct/class method (set after inference) -- `func.moreFlags.isStaticClassMethod` — static method (declared with `def static`); name is `StructName\`methodName`, self is explicit first argument -- Non-static methods (`isClassMethod=true`, `isStaticClassMethod=false`) — self is added implicitly during inference; name stays unqualified (e.g. `finalize`, `[]`) -- `func.moreFlags.propertyFunction` — property accessor (name starts with `.\``) -- `func.classParent` — pointer to the struct/class that owns the method - -### Table operations - -- `table[key]` **inserts** a new default entry if `key` is missing — use only when you want insert-on-access -- `table[key]` requires `unsafe` by default; add `options unsafe_table_lookup = false` to allow safe `[]` access -- `table?[key] ?? default_value` — safe lookup with fallback, does NOT insert missing keys -- `key_exists(table, key)` — check if a key is present without inserting -- `table |> insert(key, value)` — insert into `table`; `table |> insert(key)` — insert into set `table` -- `table |> erase(key)` — remove a key -- **Never use two `[]` lookups on the same table in one expression** (e.g. `tab[k1] = tab[k2]`) — tables are unboxed containers and re-hashing on insert can invalidate the first reference -- `find(table, key) <| $(pval) { ... }` — block-based lookup; block receives pointer to value if found -- `get(table, key, blk)` — similar block-based access (see `daslib/builtin.das`) - -## Testing Conventions (dastest) - -Tests use the `dastest` framework. Test files live in `tests/` with per-module subfolders. - -### Test file structure - -```das -options gen2 -require dastest/testing_boost public -require daslib/module_under_test - -[test] -def test_something(t : T?) { - t |> run("description") <| @(t : T?) { - t |> equal(actual, expected) - t |> success() - } -} -``` - -### Key test functions - -- `t |> equal(actual, expected)` — value equality assertion -- `t |> success()` — mark subtest as passed -- `t |> run("name") <| @(t : T?) { ... }` — named subtest -- `t |> equal(actual, true)` / `t |> equal(actual, false)` — boolean assertions (there is no `expect_true`/`expect_false`) -- `t |> strictEqual(actual, expected)` — strict equality assertion - -### Common test options - -- `options no_unused_function_arguments = false` — suppress warnings for test params -- `options no_unused_block_arguments = false` — suppress warnings for block params -- Shared test helpers go in `_common.das` module files (e.g., `tests/linq/_common.das`) - -### Running tests - -- Single file: `bin/Release/daslang.exe dastest/dastest.das -- --test tests/linq/test_linq_aggregation.das` -- Directory: `bin/Release/daslang.exe dastest/dastest.das -- --test tests/linq/` -- All tests: `bin/Release/daslang.exe dastest/dastest.das -- --test tests/` - -## Standard Library Module Conventions - -### Base + boost pattern - -Many modules come in pairs: `daslib/foo.das` (core) + `daslib/foo_boost.das` (macro layer): - -- **Base module** (`linq.das`, `json.das`, `regex.das`, etc.): pure functional API, runtime functions, iterator implementations -- **Boost module** (`linq_boost.das`, `json_boost.das`, etc.): macro-based sugar, compile-time optimizations, pipe-syntax rewrites -- All boost modules re-export their base module publicly (`require daslib/foo public`), so only `require daslib/foo_boost` is needed — do NOT add a separate `require daslib/foo` -- `regex.das` also has `require strings public`, so `require daslib/regex` (or `require daslib/regex_boost`) makes `slice`, `starts_with`, etc. available -- Example: `linq.das` provides `where`, `select`, `order_by` functions; `linq_boost.das` adds `_fold` macro that rewrites iterator chains into imperative loops - -### Iterator implementation pattern - -Many daslib functions follow this convention for iterator-based operations: - -- `foo_impl` — internal generator function (yields values) -- `foo` — public function returning `iterator` (calls `_impl`) -- `foo_to_array` — convenience wrapper returning `array` (pipes through `to_array`) -- Inplace `foo` on arrays — overload taking `var arr : array` and modifying in place - -### Key daslib modules - -- `daslib/linq.das` — LINQ-style queries (where, select, order_by, group_by, zip, etc.) -- `daslib/linq_boost.das` — `_fold` optimization macro, pipe-syntax macros -- `daslib/match.das` — pattern matching on variants and types -- `daslib/templates_boost.das` — template/reification infrastructure for AST macros; `apply_template` rewrites AST nodes -- `daslib/functional.das` — lazy iterator adapters and higher-order function utilities (filter, map, reduce, fold, scan, enumerate, chain, pairwise, iterate, find, find_index, partition, tap, for_each, flat_map, sorted, repeat, cycle, islice, echo, sum, any, all). Uses lambdas/functions for generator-returning functions (blocks cannot be captured into generators). Non-generator functions (reduce, fold, for_each, find, find_index, partition) also accept blocks. -- `daslib/strings_boost.das` — string manipulation helpers -- `daslib/json.das` / `daslib/json_boost.das` — JSON parsing/generation. Core: `JsValue` variant (7 types: `_object`, `_array`, `_string`, `_number`, `_longint`, `_bool`, `_null`), `JsonValue` struct wrapper, `read_json`, `write_json`, `JV()` constructors, `JVNull()`. Boost: safe access (`?.`, `?[]`, `??`), `from_JV`/`JV` generic struct↔JSON conversion, `%json~...%%` reader macro, `BetterJsonMacro` (`is`/`as` on `JsonValue?`). Settings: `set_no_trailing_zeros`, `set_no_empty_arrays`, `set_allow_duplicate_keys`. `try_fixing_broken_json` repairs LLM output. Key gotcha: `js?.value` accesses `JsonValue.value` field (returns `JsValue`), not a JSON key named "value" — use `js?["value"]` for that. -- `daslib/regex.das` / `daslib/regex_boost.das` — regular expressions. Re-exports `strings` publicly (`require strings public`), so `require daslib/regex` makes `slice`, `starts_with`, etc. available. Core: recursive-descent parser building `ReNode` AST, function-pointer-driven backtracking matcher. `Regex` struct, `regex_compile(pattern, case_insensitive=false, dot_all=false)`, `regex_match(re, str, offset=0)` → end position or -1, `regex_search(re, str, offset=0)` → `int2(start, end)` or `int2(-1,-1)` (finds first match anywhere), `regex_group(re, group_num, str)` → captured substring, `regex_group_by_name(re, name, str)` → named group substring, `re[index]` → `range` for group by int index (1-based), `re["name"]` → `range` for named group (returns `range(0,0)` if not found), `regex_foreach(re, str, block)` iterates all matches passing `range` values, `regex_replace(re, str, block)` replaces matches via block, `regex_replace(re, str, replacement)` replaces matches with template string (`$0`/`$&` for whole match, `$1`-`$9` for numbered groups, `${name}` for named groups, `$$` for literal `$`), `regex_split(re, str)` → `array` of substrings between matches, `regex_match_all(re, str)` → `array` of all match ranges, `is_valid(re)` checks compilation. Supports: `.` (any char except newline — use `dot_all=true` to also match `\n`), `^` (BOL), `$` (EOL), `+` `*` `?` quantifiers (greedy), `+?` `*?` `??` quantifiers (lazy), `{n}` `{n,}` `{n,m}` counted quantifiers (greedy), `{n}?` `{n,}?` `{n,m}?` counted quantifiers (lazy), `(...)` capturing groups, `(?:...)` non-capturing groups, `(?P...)` named capturing groups, `(?=...)` positive lookahead, `(?!...)` negative lookahead, `|` alternation, `[abc]` `[a-z]` `[^...]` character sets, `\w` `\W` `\d` `\D` `\s` `\S` classes, `\b` `\B` word boundaries, `\t` `\n` `\r` `\f` `\v` escapes, `\xHH` hex escapes. ASCII only (256-bit CharSet). Flags: `case_insensitive=true` for case-insensitive matching (ASCII only), `dot_all=true` for dot matching newline. Boost: `%regex~pattern%%` reader macro (compile-time, no double-escaping); flags via `%regex~pattern~flags%%` where `i`=case-insensitive, `s`=dotAll. Key gotchas: `{` must be escaped as `\{` in daslang strings for counted quantifiers (`"\\d\{3}"`), but reader macro takes literal text (`%regex~\d{3}%%`). `regex_match` always matches from position 0 (or offset) — it does NOT search for the pattern; use `regex_search` for first occurrence or `regex_foreach`/`regex_match_all` to find all occurrences. Nested groups have limited support — prefer sequential groups. `-` is only special inside `[...]` character sets. Quantifiers on lookaheads are not allowed. -- `daslib/flat_hash_table.das` — template-based open-addressing hash table (`TFlatHashTable`) with methods: `empty`, `length`, `clear`, `grow`, `rehash`, `reserve`, `key_index`, `key_exists`, `get`, `erase`, `foreach`, `keys`, `values`, `operator[]`, `operator?[]` -- `daslib/builtin.das` — core builtins like `to_array`, `to_table` - ## Keywords Reference `aka` — variable name alias (`var a aka alpha = 42`, `for (x aka element in arr)`) diff --git a/CLAUDE.md b/CLAUDE.md index cbb1f68529..f797e66f07 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,6 @@ # daslang Project Instructions -> **Keep in sync:** This file and `.github/copilot-instructions.md` share identical content. When updating one, copy the changes to the other so both GitHub Copilot and Claude Code see the same project instructions. +> **Keep in sync:** This file and `CLAUDE.md` (repo root) share identical content. Both reference skill files in the `skills/` directory at repo root — skill files are shared, not duplicated. ## Project Overview @@ -15,30 +15,19 @@ This is the [daslang](https://dascript.org/) programming language repository (Ga - **Run a script:** `bin/Release/daslang.exe path/to/script.das` - **Run tests:** `bin/Release/daslang.exe dastest/dastest.das -- --test path/to/test.das` -## Code Formatting (REQUIRED) +## Skill Files (REQUIRED) -After creating or modifying any `.das` file that is part of the project (daslib modules, tutorials, tests, etc.), run the source formatter on it. Do NOT format temporary/scratch files that will be deleted. +Task-specific instructions are split into skill files under `skills/`. You MUST read the relevant skill file(s) before performing the corresponding task. -**Formatter tool:** `utils/dasCodeFormatter/main.das` +| Skill file | Read BEFORE... | +|---|---| +| `skills/das_formatting.md` | Creating or modifying any `.das` file (tutorials, tests, daslib modules, utilities) | +| `skills/writing_tests.md` | Writing or editing test files under `tests/` | +| `skills/documentation_rst.md` | Editing RST files in `doc/source/`, editing `//!` doc-comments in `daslib/*.das`, or writing tutorial RST pages | +| `skills/cpp_integration.md` | Writing or editing C++ files in `src/`, `modules/`, or `tutorials/integration/cpp/` | +| `skills/daslib_modules.md` | Working with `daslib/` modules (linq, json, regex, functional, match, etc.) or extending the standard library | -**Procedure:** - -1. **Back up** the file before formatting: copy it to `.das.bak` -2. **Run the formatter:** `bin/Release/daslang.exe utils/dasCodeFormatter/main.das -- path/to/file.das` -3. **Verify** the formatted file still compiles: `bin/Release/daslang.exe path/to/file.das` - - For test files (no `main`), compile-check with: `bin/Release/daslang.exe dastest/dastest.das -- --test path/to/test.das` - - For module files (no `main`), verify by running a file that requires them -4. **Remove the backup** if formatting succeeded: delete `.das.bak` -5. **Restore from backup** if formatting broke the file: copy `.das.bak` back over the `.das` file, delete the backup, and report the issue - -**When to format:** -- New `.das` files: tutorials, tests, daslib modules, utilities -- Modified `.das` files: after any edits to existing project files - -**When NOT to format:** -- Temporary/scratch files that will be deleted immediately -- Files you are only reading, not modifying -- C++ source files, RST docs, Python scripts, etc. (only `.das` files) +Multiple skill files may apply to a single task. For example, creating a new daslib module requires reading `skills/das_formatting.md`, `skills/daslib_modules.md`, and possibly `skills/documentation_rst.md`. ## daslang Language — Gen2 Syntax (REQUIRED) @@ -115,6 +104,18 @@ All code examples and documentation MUST use gen2 syntax (add `options gen2` at - When writing a library generic that should dispatch to user-provided overloads, use `_::` prefix - `mem_archive_save` does NOT use `_::serialize` — so user serialize overloads are invisible; use manual `Archive` + `MemSerializer` pattern instead +### Table operations + +- `table[key]` **inserts** a new default entry if `key` is missing — use only when you want insert-on-access +- `table[key]` requires `unsafe` by default; add `options unsafe_table_lookup = false` to allow safe `[]` access +- `table?[key] ?? default_value` — safe lookup with fallback, does NOT insert missing keys +- `key_exists(table, key)` — check if a key is present without inserting +- `table |> insert(key, value)` — insert into `table`; `table |> insert(key)` — insert into set `table` +- `table |> erase(key)` — remove a key +- **Never use two `[]` lookups on the same table in one expression** (e.g. `tab[k1] = tab[k2]`) — tables are unboxed containers and re-hashing on insert can invalidate the first reference +- `find(table, key) <| $(pval) { ... }` — block-based lookup; block receives pointer to value if found +- `get(table, key, blk)` — similar block-based access (see `daslib/builtin.das`) + ### Common gotchas - Lambda params can shadow function params — use distinct names (e.g., `$(lhs, rhs)` not `$(a, b)` when `a` is already in scope) @@ -155,459 +156,6 @@ All code examples and documentation MUST use gen2 syntax (add `options gen2` at - `tests/regex/` — Regex module tests (8 test files, 278 tests) - `modules/` — External plugin modules -## Standard Library Documentation - -The stdlib docs live in `doc/source/stdlib/` and are generated from `//!` doc-comments in `daslib/*.das`. - -### Documentation pipeline - -1. `daslib/*.das` files contain `//!` comments for each module, struct, function -2. `doc/reflections/rst_comment.das` extracts comments into `doc/source/stdlib/detail/*.rst` -3. `doc/reflections/das2rst.das` + `doc/reflections/rst.das` combine detail RST with handmade content -4. **`doc/source/stdlib/handmade/`** — manually written module descriptions and examples (2,001 files) -5. Final RST output goes to `doc/source/stdlib/*.rst` - -### Key documentation tools - -- `doc/reflections/gen_module_examples.py` — generates/updates all 86 module-*.rst files with descriptions and compilable examples (31 modules have examples) -- `doc/reflections/test_new_examples.py` — tests all example snippets by running them through `daslang.exe` -- `doc/reflections/fix_short_docs.py` — fixes terse function documentation -- Validate: `bin/Release/daslang.exe doc/reflections/das2rst.das` (exit code 0 = success) - -### Function grouping in generated docs - -Functions in each module's RST are organized into named groups (e.g. "Compilation and validation", "Access", "Match & replace"). Groups are defined in `doc/reflections/das2rst.das` via `group_by_regex("Group Name", mod, %regex~(func1|func2)$%%)`. Any public function not matched by a group regex ends up in an **"Uncategorized"** section. After adding new public functions to a module: - -1. Add the function name to the appropriate `group_by_regex` call in the module's `document_module_*` function in `das2rst.das` -2. Create or update the handmade doc file in `doc/source/stdlib/handmade/` (replace `// stub` with a description) -3. Regenerate: `bin/Release/daslang.exe doc/reflections/das2rst.das` -4. Verify no "Uncategorized" section remains: search for `Uncategorized` in the generated `doc/source/stdlib/*.rst` - -### Adding a module example - -1. Add `example="""..."""` to the `reg()` call in `gen_module_examples.py` -2. Test it: add to `test_new_examples.py` and run `python doc/reflections/test_new_examples.py` -3. Regenerate: `python doc/reflections/gen_module_examples.py` -4. Validate: `bin/Release/daslang.exe doc/reflections/das2rst.das` - -## Documentation Conventions - -When editing RST files in `doc/source/reference/language/`: - -- All code blocks use `.. code-block:: das` with gen2 syntax -- Include `// output:` comments showing expected output for runnable examples -- Add `require` statements when examples need imports -- Use `:ref:` cross-references to link between pages (labels: `_structs`, `_classes`, `_functions`, `_statements`, `_expressions`, `_arrays`, `_tables`, `_iterators`, `_generators`, `_lambdas`, `_blocks`, `_tuples`, `_variants`, `_bitfields`, `_aliases`, `_modules`, `_options`, `_unsafe`, `_enumerations`, `_generic_programming`, `_pattern-matching`, `_comprehensions`, `_string_builder`, `_macros`, `_reification`, `_finalizers`, `_clone`, `_temporary`, `_move_copy_clone`, `_annotations`, `_program_structure`, `_type_conversions`, `_contexts`, `_locks`, `_datatypes_and_values`, `_pointers`) -- Verify examples compile: `bin/Release/daslang.exe example.das` - -### RST table rules - -RST uses two table formats — **grid tables** and **simple tables**. Both are fragile: - -- **Grid tables** (`+---+---+`): Every row line must be exactly the same width as every separator line. Off-by-one spaces cause Sphinx errors. -- **Simple tables** (`=== ===`): The `=` separator defines column widths. Content in non-last columns must NOT extend past its column's `=` boundary. Headers must start at or after the column's start position (not in the gap). The gap between columns must be at least 2 spaces. -- After creating or editing any RST table, verify the file with a Sphinx build (see below). - -### Documentation workflow (REQUIRED) - -After creating or modifying any RST files, stdlib documentation, or `daslib/*.das` module doc-comments: - -1. **Regenerate stdlib docs** (if `daslib/*.das` files or `doc/reflections/das2rst.das` were changed): - ``` - bin/Release/daslang.exe doc/reflections/das2rst.das - ``` - -2. **Clean Sphinx build** — MUST delete cache; cached builds hide errors: - ``` - cd doc - Remove-Item -Recurse -Force sphinx-build # delete doctree cache - Remove-Item -Recurse -Force ../site/doc # delete HTML output - sphinx-build -b html -d sphinx-build source ../site/doc - ``` - On Linux/Mac: - ``` - cd doc - rm -rf sphinx-build ../site/doc - sphinx-build -b html -d sphinx-build source ../site/doc - ``` - -3. **Verify no new errors or warnings**: Check the build output for `ERROR` and `WARNING`. The build must introduce **no new** Sphinx errors or warnings compared to the baseline. - -**When to run the workflow:** -- New or modified RST files (language docs, tutorials, stdlib docs) -- New or modified `//!` doc-comments in `daslib/*.das` files -- Changes to `doc/reflections/das2rst.das` or `doc/reflections/rst.das` -- New public functions added to any `daslib/*.das` module (also update `group_by_regex` in `das2rst.das`) - -### Tutorial RST conventions - -Tutorial RST files live in `doc/source/reference/tutorials/` with companion `.das` files in `tutorials/language/`. - -- Each RST starts with a label: `.. _tutorial_name:` (e.g., `.. _tutorial_linq:`) -- Include `.. index::` directive with relevant `single: Tutorial; Topic` entries -- Code blocks use `.. code-block:: das` with gen2 syntax -- End each RST with a `.. seealso::` block containing: - - Full source as `:download:` link: `Full source: :download:\`tutorials/language/XX_name.das <../../../../tutorials/language/XX_name.das>\`` - - Next tutorial link (except last): `Next tutorial: :ref:\`tutorial_next_name\`` - - Related language reference links via `:ref:` -- Toctree is in `doc/source/reference/tutorials.rst` — add new tutorials there - -### C++ integration tutorial RST conventions - -C++ integration tutorial RST files live in `doc/source/reference/tutorials/` with `.cpp` and `.das` files in `tutorials/integration/cpp/`. - -- Label pattern: `.. _tutorial_integration_cpp_:` (e.g., `.. _tutorial_integration_cpp_binding_types:`) -- Index entries: `single: Tutorial; C++ Integration; ` -- Code blocks: `.. code-block:: cpp` for C++ code, `.. code-block:: das` for daslang code -- Build & run section with `cmake --build` command and expected output -- End with `.. seealso::` containing: - - `:download:` links for both `.cpp` and `.das` source files - - Previous/Next tutorial links via `:ref:` -- Each tutorial is one self-contained `.cpp` file with embedded `main()` — no separate build infrastructure needed beyond CMake target -- Tutorial CMake targets: `integration_cpp_01` through `integration_cpp_NN` (defined in `tutorials/integration/cpp/CMakeLists.txt`) - -- Tutorial labels for cross-references: `tutorial_hello_world`, `tutorial_variables`, `tutorial_operators`, `tutorial_control_flow`, `tutorial_functions`, `tutorial_arrays`, `tutorial_strings`, `tutorial_structs`, `tutorial_enumerations`, `tutorial_tables`, `tutorial_tuples_and_variants`, `tutorial_function_pointers`, `tutorial_blocks`, `tutorial_lambdas`, `tutorial_iterators_and_generators`, `tutorial_modules`, `tutorial_move_copy_clone`, `tutorial_classes`, `tutorial_generics`, `tutorial_lifetime`, `tutorial_error_handling`, `tutorial_unsafe`, `tutorial_string_format`, `tutorial_pattern_matching`, `tutorial_annotations`, `tutorial_contracts`, `tutorial_testing`, `tutorial_linq`, `tutorial_functional`, `tutorial_json`, `tutorial_regex`, `tutorial_operator_overloading`, `tutorial_pointers`, `tutorial_utility_patterns`, `tutorial_random`, `tutorial_dynamic_type_checking`, `tutorial_coroutines`, `tutorial_serialization`, `tutorial_testing_tools` -- C++ integration tutorial labels: `tutorial_integration_cpp_hello_world`, `tutorial_integration_cpp_calling_functions`, `tutorial_integration_cpp_binding_functions`, `tutorial_integration_cpp_binding_types`, `tutorial_integration_cpp_binding_enums`, `tutorial_integration_cpp_interop`, `tutorial_integration_cpp_callbacks`, `tutorial_integration_cpp_methods`, `tutorial_integration_cpp_operators_and_properties` -- C++ integration tutorial plan (remaining): 10 Custom Modules, 11 Context Variables, 12 Smart Pointers & GC, 13 AOT, 14 Serialization, 15 Custom Annotations, 16 Sandbox - -## C++ Integration Patterns - -These patterns are used in C++ host applications that embed daslang. - -### Host program boilerplate - -Every C++ host that runs daslang scripts follows this pattern: - -```cpp -#include "daScript/daScript.h" -using namespace das; - -void run_script() { - TextPrinter tout; - ModuleGroup dummyLibGroup; - auto fAccess = make_smart(); - auto program = compileDaScript(getDasRoot() + "/path/to/script.das", - fAccess, tout, dummyLibGroup); - if (program->failed()) { /* report errors */ return; } - Context ctx(program->getContextStackSize()); - if (!program->simulate(ctx, tout)) { /* report errors */ return; } - auto fn = ctx.findFunction("test"); - ctx.evalWithCatch(fn, nullptr); -} - -int main(int, char * []) { - NEED_ALL_DEFAULT_MODULES; - NEED_MODULE(Module_MyModule); // custom modules - Module::Initialize(); - run_script(); - Module::Shutdown(); - return 0; -} -``` - -### Creating a module - -Derive from `Module`, register types/functions/enums in the constructor, then use `REGISTER_MODULE`: - -```cpp -class Module_MyMod : public Module { -public: - Module_MyMod() : Module("my_module_name") { - ModuleLibrary lib(this); - lib.addBuiltInModule(); - // addAnnotation, addExtern, addEnumeration, addConstant ... - } -}; -REGISTER_MODULE(Module_MyMod); -``` - -The host uses `NEED_MODULE(Module_MyMod)` before `Module::Initialize()`. Scripts access it via `require my_module_name`. - -### Callbacks — `TBlock<>`, `TFunc<>`, `TLambda<>`, `das_invoke*` - -Three closure types exist, each with a typed template and an invocation helper: - -| Type | Template | Invocation | Lifetime | -|------|----------|------------|----------| -| Block | `TBlock` | `das_invoke::invoke(ctx, at, blk, args...)` | Stack-bound — valid only during the call | -| Func | `TFunc` (or untyped `Func`) | `das_invoke_function::invoke(ctx, at, fn, args...)` | Context-bound — storable | -| Lambda | `TLambda` (or untyped `Lambda`) | `das_invoke_lambda::invoke(ctx, at, lmb, args...)` | Heap-allocated — captures variables | - -**Typed vs untyped**: `TBlock` maps to `block<(arg:int):int>` in daslang — the compiler checks signatures. Untyped `Lambda` maps to `lambda<>` and will **not** match typed lambdas like `lambda<(x:int):int>`. Prefer typed templates. - -**Block callback example**: - -```cpp -void with_values(int32_t a, int32_t b, - const TBlock & blk, - Context * context, LineInfoArg * at) { - das_invoke::invoke(context, at, blk, a, b); -} - -addExtern(*this, lib, "with_values", - SideEffects::invoke, "with_values") - ->args({"a", "b", "blk", "context", "at"}); -``` - -Use `SideEffects::invoke` for any function that invokes script callbacks. - -In daslang: blocks use `<|` with `$()` prefix, function pointers use `@@func_name`, lambdas use `@(args) { body }`. - -### Calling daslang functions from C++ — `das_invoke_function` - -The high-level `das_invoke_function::invoke(ctx, at, fnPtr, arg1, arg2, ...)` handles argument marshalling automatically. Preferred over raw `cast<>` + `evalWithCatch`. - -### Binding C++ functions — `addExtern` + `DAS_BIND_FUN` - -```cpp -addExtern(*this, lib, "das_name", - SideEffects::none, "cpp_function") - ->args({"param1", "param2"}); -``` - -`SideEffects` flags: `none` (pure), `modifyExternal` (stdout/files), `modifyArgument` (mutates ref params), `accessGlobal` (reads shared state), `invoke` (calls daslang), `worstDefault` (safe fallback). - -### Binding C++ types — `MAKE_TYPE_FACTORY` + `ManagedStructureAnnotation` - -1. **`MAKE_TYPE_FACTORY(DasName, CppType)`** at file scope — creates `typeFactory` + `typeName` -2. **`ManagedStructureAnnotation`** — describe fields with `addField("name", "name")` -3. **`addAnnotation(make_smart(lib))`** in the module — order matters: if type B contains type A, register A first -4. Functions returning bound types by value require **`SimNode_ExtFuncCallAndCopyOrMove`** template parameter in `addExtern` - -**Handled types are reference types** — important consequences for scripts: -- Mutable local variables (`var`) of handled types require `unsafe` blocks -- Immutable locals (`let`) returned from factory functions work without `unsafe` -- **Factory function pattern**: provide `make_xxx()` functions returning by value so scripts can create instances ergonomically with `let x = make_xxx(...)` — no `unsafe` needed -- POD structs (no default member initializers, no virtual functions) work best with `ManagedStructureAnnotation` - -### Binding C++ methods — `DAS_CALL_MEMBER` + `DAS_CALL_METHOD` - -daslang has no member functions — "methods" are free functions where the first argument is `self`. Pipe syntax (`obj |> method()`) provides method-call ergonomics. - -```cpp -// Step 1: Create wrapper aliases -using method_increment = DAS_CALL_MEMBER(Counter::increment); -using method_get = DAS_CALL_MEMBER(Counter::get); - -// Step 2: Register with addExtern -addExtern(*this, lib, "increment", - SideEffects::modifyArgument, - DAS_CALL_MEMBER_CPP(Counter::increment)) - ->args({"self"}); - -addExtern(*this, lib, "get", - SideEffects::none, - DAS_CALL_MEMBER_CPP(Counter::get)) - ->args({"self"}); -``` - -- **Non-const methods**: `SideEffects::modifyArgument` (they mutate the object) -- **Const methods**: `SideEffects::none` -- `DAS_CALL_MEMBER_CPP(Class::method)` provides the AOT-compatible name string - -### Binding operators and properties - -**Operators**: register functions with the operator symbol as the daslang name: - -```cpp -addExtern( - *this, lib, "+", SideEffects::none, "vec3_add")->args({"a", "b"}); -// Unary: addExtern<...>(*this, lib, "-", ...)->args({"a"}); -``` - -Available operator names: `+`, `-`, `*`, `/`, `%`, `<<`, `>>`, `<`, `>`, `<=`, `>=`, `&`, `|`, `^`. - -**Equality**: `addEquNeq(*this, lib)` binds both `==` and `!=` (requires `operator==` and `operator!=` on T). - -**Properties**: method calls disguised as field access in `ManagedStructureAnnotation`: - -```cpp -addProperty("length", "length"); -``` - -In daslang, `v.length` calls `Vec3::length()` — looks like a field, calls a method. - -### Binding C++ enums — `DAS_BASE_BIND_ENUM` - -```cpp -// MUST come BEFORE `using namespace das` to avoid name collisions -DAS_BASE_BIND_ENUM(CppEnum, DasName, Value1, Value2, Value3) - -using namespace das; - -// In module constructor: -addEnumeration(make_smart()); -``` - -- `DAS_BASE_BIND_ENUM` creates class `EnumerationDasName` + `typeFactory` -- `DAS_BIND_ENUM_CAST(CppEnum)` — explicit `cast<>` specialization (often not needed, SFINAE default suffices) -- `DAS_BASE_BIND_ENUM_98` — for unscoped (C-style) enums -- **Critical**: place enum macros BEFORE `using namespace das` — the macros define names inside `namespace das` that collide with global enum names -- **Name collision pitfall**: `das::LogLevel` is defined internally in `include/daScript/misc/string_writer.h` — do NOT name your enum `LogLevel` when `using namespace das` -- Manual construction alternative: `make_smart("Name")` + `pEnum->addIEx("Value", "CppEnum::Value", intValue, LineInfo())` - -### Low-level interop — `addInterop` - -`addInterop` binds a C++ function that receives raw simulation-level arguments (`vec4f *`), the call node (`SimNode_CallBase *`), and the context. Unlike `addExtern`, it supports **"any type" arguments** — when a template parameter is `vec4f`, it means the argument can be any daslang type. The function inspects `call->types[i]` (`TypeInfo *`) at runtime to determine what was actually passed. - -**Signature**: the C++ function must match `InteropFunction`: - -```cpp -// typedef vec4f (*InteropFunction)(Context &, SimNode_CallBase *, vec4f *); -vec4f my_interop(Context & context, SimNode_CallBase * call, vec4f * args) { - TypeInfo * ti = call->types[0]; // type info for first argument - // ... inspect ti->type, ti->structType, etc. - return v_zero(); -} -``` - -**Registration**: - -```cpp -addInterop( - *this, lib, "das_name", SideEffects::none, "my_interop"); -``` - -Where `vec4f` as an `ArgType` means "any type" — the argument accepts any daslang value. Concrete types (e.g. `int32_t`, `const char *`, `const Block &`) are also valid and work like `addExtern`. - -**Key capabilities** (vs `addExtern`): -- Access to `call->types[]` — per-argument `TypeInfo` with full type metadata -- Access to `call->debugInfo` — source location of the call site -- `vec4f` argument type = "any" — accept arguments of any daslang type -- Used internally for `sprint`, `hash`, `write`, `binary_save/load`, `invoke_in_context` - -**TypeInfo union warning**: `TypeInfo` has a union — `structType`, `enumType`, and `annotation_or_name` share the same memory. Which member is valid depends on `ti->type`: -- `tStructure` → `ti->structType` (StructInfo *) -- `tEnumeration` / `tEnumeration8` / `tEnumeration16` → `ti->enumType` (EnumInfo *) -- `tHandle` → use `ti->getAnnotation()` (resolves tagged pointer safely) - -Accessing the wrong union member is **undefined behavior**. `das_to_string(Type::tHandle)` returns an empty string — use `ti->getAnnotation()->name` for handled type names. - -**Example** — `new_and_init` allocates and initializes any struct: - -```cpp -vec4f new_and_init(Context & context, SimNode_CallBase * call, vec4f * args) { - TypeInfo * typeInfo = call->types[0]; - if (typeInfo->type != Type::tStructure) - context.throw_error_at(call->debugInfo, "expected struct"); - auto size = getTypeSize(typeInfo); - auto data = context.allocate(size, &call->debugInfo); - if (typeInfo->structType && typeInfo->structType->init_mnh) { - auto fn = context.fnByMangledName(typeInfo->structType->init_mnh); - context.callWithCopyOnReturn(fn, nullptr, data, 0); - } else { - memset(data, 0, size); - } - return cast::from(data); -} - -addInterop(*this, lib, "new_and_init", - SideEffects::none, "new_and_init"); -``` - -## C++ Codebase Notes - -- Main type inference: `src/ast/ast_infer_type.cpp` (implementation) + `include/daScript/ast/ast_infer_type.h` (class declarations for `CaptureLambda` and `InferTypes`) -- Builtin runtime functions: `src/builtin/module_builtin_runtime.cpp` -- Smart pointer builtins: `move`, `move_new`, `smart_ptr_clone`, `smart_ptr_use_count` -- Compilation errors: `include/daScript/ast/compilation_errors.h` (error codes 10001–40214) -- Lexer: `src/parser/ds2_lexer.lpp` -- Parser: `src/parser/ds2_parser.ypp` - -### Key AST function flags - -- `func.flags.isClassMethod` — function is a struct/class method (set after inference) -- `func.moreFlags.isStaticClassMethod` — static method (declared with `def static`); name is `StructName\`methodName`, self is explicit first argument -- Non-static methods (`isClassMethod=true`, `isStaticClassMethod=false`) — self is added implicitly during inference; name stays unqualified (e.g. `finalize`, `[]`) -- `func.moreFlags.propertyFunction` — property accessor (name starts with `.\``) -- `func.classParent` — pointer to the struct/class that owns the method - -### Table operations - -- `table[key]` **inserts** a new default entry if `key` is missing — use only when you want insert-on-access -- `table[key]` requires `unsafe` by default; add `options unsafe_table_lookup = false` to allow safe `[]` access -- `table?[key] ?? default_value` — safe lookup with fallback, does NOT insert missing keys -- `key_exists(table, key)` — check if a key is present without inserting -- `table |> insert(key, value)` — insert into `table`; `table |> insert(key)` — insert into set `table` -- `table |> erase(key)` — remove a key -- **Never use two `[]` lookups on the same table in one expression** (e.g. `tab[k1] = tab[k2]`) — tables are unboxed containers and re-hashing on insert can invalidate the first reference -- `find(table, key) <| $(pval) { ... }` — block-based lookup; block receives pointer to value if found -- `get(table, key, blk)` — similar block-based access (see `daslib/builtin.das`) - -## Testing Conventions (dastest) - -Tests use the `dastest` framework. Test files live in `tests/` with per-module subfolders. - -### Test file structure - -```das -options gen2 -require dastest/testing_boost public -require daslib/module_under_test - -[test] -def test_something(t : T?) { - t |> run("description") <| @(t : T?) { - t |> equal(actual, expected) - t |> success() - } -} -``` - -### Key test functions - -- `t |> equal(actual, expected)` — value equality assertion -- `t |> success()` — mark subtest as passed -- `t |> run("name") <| @(t : T?) { ... }` — named subtest -- `t |> equal(actual, true)` / `t |> equal(actual, false)` — boolean assertions (there is no `expect_true`/`expect_false`) -- `t |> strictEqual(actual, expected)` — strict equality assertion - -### Common test options - -- `options no_unused_function_arguments = false` — suppress warnings for test params -- `options no_unused_block_arguments = false` — suppress warnings for block params -- Shared test helpers go in `_common.das` module files (e.g., `tests/linq/_common.das`) - -### Running tests - -- Single file: `bin/Release/daslang.exe dastest/dastest.das -- --test tests/linq/test_linq_aggregation.das` -- Directory: `bin/Release/daslang.exe dastest/dastest.das -- --test tests/linq/` -- All tests: `bin/Release/daslang.exe dastest/dastest.das -- --test tests/` - -## Standard Library Module Conventions - -### Base + boost pattern - -Many modules come in pairs: `daslib/foo.das` (core) + `daslib/foo_boost.das` (macro layer): - -- **Base module** (`linq.das`, `json.das`, `regex.das`, etc.): pure functional API, runtime functions, iterator implementations -- **Boost module** (`linq_boost.das`, `json_boost.das`, etc.): macro-based sugar, compile-time optimizations, pipe-syntax rewrites -- All boost modules re-export their base module publicly (`require daslib/foo public`), so only `require daslib/foo_boost` is needed — do NOT add a separate `require daslib/foo` -- `regex.das` also has `require strings public`, so `require daslib/regex` (or `require daslib/regex_boost`) makes `slice`, `starts_with`, etc. available -- Example: `linq.das` provides `where`, `select`, `order_by` functions; `linq_boost.das` adds `_fold` macro that rewrites iterator chains into imperative loops - -### Iterator implementation pattern - -Many daslib functions follow this convention for iterator-based operations: - -- `foo_impl` — internal generator function (yields values) -- `foo` — public function returning `iterator` (calls `_impl`) -- `foo_to_array` — convenience wrapper returning `array` (pipes through `to_array`) -- Inplace `foo` on arrays — overload taking `var arr : array` and modifying in place - -### Key daslib modules - -- `daslib/linq.das` — LINQ-style queries (where, select, order_by, group_by, zip, etc.) -- `daslib/linq_boost.das` — `_fold` optimization macro, pipe-syntax macros -- `daslib/match.das` — pattern matching on variants and types -- `daslib/templates_boost.das` — template/reification infrastructure for AST macros; `apply_template` rewrites AST nodes -- `daslib/functional.das` — lazy iterator adapters and higher-order function utilities (filter, map, reduce, fold, scan, enumerate, chain, pairwise, iterate, find, find_index, partition, tap, for_each, flat_map, sorted, repeat, cycle, islice, echo, sum, any, all). Uses lambdas/functions for generator-returning functions (blocks cannot be captured into generators). Non-generator functions (reduce, fold, for_each, find, find_index, partition) also accept blocks. -- `daslib/strings_boost.das` — string manipulation helpers -- `daslib/json.das` / `daslib/json_boost.das` — JSON parsing/generation. Core: `JsValue` variant (7 types: `_object`, `_array`, `_string`, `_number`, `_longint`, `_bool`, `_null`), `JsonValue` struct wrapper, `read_json`, `write_json`, `JV()` constructors, `JVNull()`. Boost: safe access (`?.`, `?[]`, `??`), `from_JV`/`JV` generic struct↔JSON conversion, `%json~...%%` reader macro, `BetterJsonMacro` (`is`/`as` on `JsonValue?`). Settings: `set_no_trailing_zeros`, `set_no_empty_arrays`, `set_allow_duplicate_keys`. `try_fixing_broken_json` repairs LLM output. Key gotcha: `js?.value` accesses `JsonValue.value` field (returns `JsValue`), not a JSON key named "value" — use `js?["value"]` for that. -- `daslib/regex.das` / `daslib/regex_boost.das` — regular expressions. Re-exports `strings` publicly (`require strings public`), so `require daslib/regex` makes `slice`, `starts_with`, etc. available. Core: recursive-descent parser building `ReNode` AST, function-pointer-driven backtracking matcher. `Regex` struct, `regex_compile(pattern, case_insensitive=false, dot_all=false)`, `regex_match(re, str, offset=0)` → end position or -1, `regex_search(re, str, offset=0)` → `int2(start, end)` or `int2(-1,-1)` (finds first match anywhere), `regex_group(re, group_num, str)` → captured substring, `regex_group_by_name(re, name, str)` → named group substring, `re[index]` → `range` for group by int index (1-based), `re["name"]` → `range` for named group (returns `range(0,0)` if not found), `regex_foreach(re, str, block)` iterates all matches passing `range` values, `regex_replace(re, str, block)` replaces matches via block, `regex_replace(re, str, replacement)` replaces matches with template string (`$0`/`$&` for whole match, `$1`-`$9` for numbered groups, `${name}` for named groups, `$$` for literal `$`), `regex_split(re, str)` → `array` of substrings between matches, `regex_match_all(re, str)` → `array` of all match ranges, `is_valid(re)` checks compilation. Supports: `.` (any char except newline — use `dot_all=true` to also match `\n`), `^` (BOL), `$` (EOL), `+` `*` `?` quantifiers (greedy), `+?` `*?` `??` quantifiers (lazy), `{n}` `{n,}` `{n,m}` counted quantifiers (greedy), `{n}?` `{n,}?` `{n,m}?` counted quantifiers (lazy), `(...)` capturing groups, `(?:...)` non-capturing groups, `(?P...)` named capturing groups, `(?=...)` positive lookahead, `(?!...)` negative lookahead, `|` alternation, `[abc]` `[a-z]` `[^...]` character sets, `\w` `\W` `\d` `\D` `\s` `\S` classes, `\b` `\B` word boundaries, `\t` `\n` `\r` `\f` `\v` escapes, `\xHH` hex escapes. ASCII only (256-bit CharSet). Flags: `case_insensitive=true` for case-insensitive matching (ASCII only), `dot_all=true` for dot matching newline. Boost: `%regex~pattern%%` reader macro (compile-time, no double-escaping); flags via `%regex~pattern~flags%%` where `i`=case-insensitive, `s`=dotAll. Key gotchas: `{` must be escaped as `\{` in daslang strings for counted quantifiers (`"\\d\{3}"`), but reader macro takes literal text (`%regex~\d{3}%%`). `regex_match` always matches from position 0 (or offset) — it does NOT search for the pattern; use `regex_search` for first occurrence or `regex_foreach`/`regex_match_all` to find all occurrences. Nested groups have limited support — prefer sequential groups. `-` is only special inside `[...]` character sets. Quantifiers on lookaheads are not allowed. -- `daslib/flat_hash_table.das` — template-based open-addressing hash table (`TFlatHashTable`) with methods: `empty`, `length`, `clear`, `grow`, `rehash`, `reserve`, `key_index`, `key_exists`, `get`, `erase`, `foreach`, `keys`, `values`, `operator[]`, `operator?[]` -- `daslib/builtin.das` — core builtins like `to_array`, `to_table` - ## Keywords Reference `aka` — variable name alias (`var a aka alpha = 42`, `for (x aka element in arr)`) diff --git a/skills/cpp_integration.md b/skills/cpp_integration.md new file mode 100644 index 0000000000..aab0bee6db --- /dev/null +++ b/skills/cpp_integration.md @@ -0,0 +1,251 @@ +# C++ Integration Patterns + +These patterns are used in C++ host applications that embed daslang. + +## Host program boilerplate + +Every C++ host that runs daslang scripts follows this pattern: + +```cpp +#include "daScript/daScript.h" +using namespace das; + +void run_script() { + TextPrinter tout; + ModuleGroup dummyLibGroup; + auto fAccess = make_smart(); + auto program = compileDaScript(getDasRoot() + "/path/to/script.das", + fAccess, tout, dummyLibGroup); + if (program->failed()) { /* report errors */ return; } + Context ctx(program->getContextStackSize()); + if (!program->simulate(ctx, tout)) { /* report errors */ return; } + auto fn = ctx.findFunction("test"); + ctx.evalWithCatch(fn, nullptr); +} + +int main(int, char * []) { + NEED_ALL_DEFAULT_MODULES; + NEED_MODULE(Module_MyModule); // custom modules + Module::Initialize(); + run_script(); + Module::Shutdown(); + return 0; +} +``` + +## Creating a module + +Derive from `Module`, register types/functions/enums in the constructor, then use `REGISTER_MODULE`: + +```cpp +class Module_MyMod : public Module { +public: + Module_MyMod() : Module("my_module_name") { + ModuleLibrary lib(this); + lib.addBuiltInModule(); + // addAnnotation, addExtern, addEnumeration, addConstant ... + } +}; +REGISTER_MODULE(Module_MyMod); +``` + +The host uses `NEED_MODULE(Module_MyMod)` before `Module::Initialize()`. Scripts access it via `require my_module_name`. + +## Callbacks — `TBlock<>`, `TFunc<>`, `TLambda<>`, `das_invoke*` + +Three closure types exist, each with a typed template and an invocation helper: + +| Type | Template | Invocation | Lifetime | +|------|----------|------------|----------| +| Block | `TBlock` | `das_invoke::invoke(ctx, at, blk, args...)` | Stack-bound — valid only during the call | +| Func | `TFunc` (or untyped `Func`) | `das_invoke_function::invoke(ctx, at, fn, args...)` | Context-bound — storable | +| Lambda | `TLambda` (or untyped `Lambda`) | `das_invoke_lambda::invoke(ctx, at, lmb, args...)` | Heap-allocated — captures variables | + +**Typed vs untyped**: `TBlock` maps to `block<(arg:int):int>` in daslang — the compiler checks signatures. Untyped `Lambda` maps to `lambda<>` and will **not** match typed lambdas like `lambda<(x:int):int>`. Prefer typed templates. + +**Block callback example**: + +```cpp +void with_values(int32_t a, int32_t b, + const TBlock & blk, + Context * context, LineInfoArg * at) { + das_invoke::invoke(context, at, blk, a, b); +} + +addExtern(*this, lib, "with_values", + SideEffects::invoke, "with_values") + ->args({"a", "b", "blk", "context", "at"}); +``` + +Use `SideEffects::invoke` for any function that invokes script callbacks. + +In daslang: blocks use `<|` with `$()` prefix, function pointers use `@@func_name`, lambdas use `@(args) { body }`. + +## Calling daslang functions from C++ — `das_invoke_function` + +The high-level `das_invoke_function::invoke(ctx, at, fnPtr, arg1, arg2, ...)` handles argument marshalling automatically. Preferred over raw `cast<>` + `evalWithCatch`. + +## Binding C++ functions — `addExtern` + `DAS_BIND_FUN` + +```cpp +addExtern(*this, lib, "das_name", + SideEffects::none, "cpp_function") + ->args({"param1", "param2"}); +``` + +`SideEffects` flags: `none` (pure), `modifyExternal` (stdout/files), `modifyArgument` (mutates ref params), `accessGlobal` (reads shared state), `invoke` (calls daslang), `worstDefault` (safe fallback). + +## Binding C++ types — `MAKE_TYPE_FACTORY` + `ManagedStructureAnnotation` + +1. **`MAKE_TYPE_FACTORY(DasName, CppType)`** at file scope — creates `typeFactory` + `typeName` +2. **`ManagedStructureAnnotation`** — describe fields with `addField("name", "name")` +3. **`addAnnotation(make_smart(lib))`** in the module — order matters: if type B contains type A, register A first +4. Functions returning bound types by value require **`SimNode_ExtFuncCallAndCopyOrMove`** template parameter in `addExtern` + +**Handled types are reference types** — important consequences for scripts: +- Mutable local variables (`var`) of handled types require `unsafe` blocks +- Immutable locals (`let`) returned from factory functions work without `unsafe` +- **Factory function pattern**: provide `make_xxx()` functions returning by value so scripts can create instances ergonomically with `let x = make_xxx(...)` — no `unsafe` needed +- POD structs (no default member initializers, no virtual functions) work best with `ManagedStructureAnnotation` + +## Binding C++ methods — `DAS_CALL_MEMBER` + `DAS_CALL_METHOD` + +daslang has no member functions — "methods" are free functions where the first argument is `self`. Pipe syntax (`obj |> method()`) provides method-call ergonomics. + +```cpp +// Step 1: Create wrapper aliases +using method_increment = DAS_CALL_MEMBER(Counter::increment); +using method_get = DAS_CALL_MEMBER(Counter::get); + +// Step 2: Register with addExtern +addExtern(*this, lib, "increment", + SideEffects::modifyArgument, + DAS_CALL_MEMBER_CPP(Counter::increment)) + ->args({"self"}); + +addExtern(*this, lib, "get", + SideEffects::none, + DAS_CALL_MEMBER_CPP(Counter::get)) + ->args({"self"}); +``` + +- **Non-const methods**: `SideEffects::modifyArgument` (they mutate the object) +- **Const methods**: `SideEffects::none` +- `DAS_CALL_MEMBER_CPP(Class::method)` provides the AOT-compatible name string + +## Binding operators and properties + +**Operators**: register functions with the operator symbol as the daslang name: + +```cpp +addExtern( + *this, lib, "+", SideEffects::none, "vec3_add")->args({"a", "b"}); +// Unary: addExtern<...>(*this, lib, "-", ...)->args({"a"}); +``` + +Available operator names: `+`, `-`, `*`, `/`, `%`, `<<`, `>>`, `<`, `>`, `<=`, `>=`, `&`, `|`, `^`. + +**Equality**: `addEquNeq(*this, lib)` binds both `==` and `!=` (requires `operator==` and `operator!=` on T). + +**Properties**: method calls disguised as field access in `ManagedStructureAnnotation`: + +```cpp +addProperty("length", "length"); +``` + +In daslang, `v.length` calls `Vec3::length()` — looks like a field, calls a method. + +## Binding C++ enums — `DAS_BASE_BIND_ENUM` + +```cpp +// MUST come BEFORE `using namespace das` to avoid name collisions +DAS_BASE_BIND_ENUM(CppEnum, DasName, Value1, Value2, Value3) + +using namespace das; + +// In module constructor: +addEnumeration(make_smart()); +``` + +- `DAS_BASE_BIND_ENUM` creates class `EnumerationDasName` + `typeFactory` +- `DAS_BIND_ENUM_CAST(CppEnum)` — explicit `cast<>` specialization (often not needed, SFINAE default suffices) +- `DAS_BASE_BIND_ENUM_98` — for unscoped (C-style) enums +- **Critical**: place enum macros BEFORE `using namespace das` — the macros define names inside `namespace das` that collide with global enum names +- **Name collision pitfall**: `das::LogLevel` is defined internally in `include/daScript/misc/string_writer.h` — do NOT name your enum `LogLevel` when `using namespace das` +- Manual construction alternative: `make_smart("Name")` + `pEnum->addIEx("Value", "CppEnum::Value", intValue, LineInfo())` + +## Low-level interop — `addInterop` + +`addInterop` binds a C++ function that receives raw simulation-level arguments (`vec4f *`), the call node (`SimNode_CallBase *`), and the context. Unlike `addExtern`, it supports **"any type" arguments** — when a template parameter is `vec4f`, it means the argument can be any daslang type. The function inspects `call->types[i]` (`TypeInfo *`) at runtime to determine what was actually passed. + +**Signature**: the C++ function must match `InteropFunction`: + +```cpp +// typedef vec4f (*InteropFunction)(Context &, SimNode_CallBase *, vec4f *); +vec4f my_interop(Context & context, SimNode_CallBase * call, vec4f * args) { + TypeInfo * ti = call->types[0]; // type info for first argument + // ... inspect ti->type, ti->structType, etc. + return v_zero(); +} +``` + +**Registration**: + +```cpp +addInterop( + *this, lib, "das_name", SideEffects::none, "my_interop"); +``` + +Where `vec4f` as an `ArgType` means "any type" — the argument accepts any daslang value. Concrete types (e.g. `int32_t`, `const char *`, `const Block &`) are also valid and work like `addExtern`. + +**Key capabilities** (vs `addExtern`): +- Access to `call->types[]` — per-argument `TypeInfo` with full type metadata +- Access to `call->debugInfo` — source location of the call site +- `vec4f` argument type = "any" — accept arguments of any daslang type +- Used internally for `sprint`, `hash`, `write`, `binary_save/load`, `invoke_in_context` + +**TypeInfo union warning**: `TypeInfo` has a union — `structType`, `enumType`, and `annotation_or_name` share the same memory. Which member is valid depends on `ti->type`: +- `tStructure` → `ti->structType` (StructInfo *) +- `tEnumeration` / `tEnumeration8` / `tEnumeration16` → `ti->enumType` (EnumInfo *) +- `tHandle` → use `ti->getAnnotation()` (resolves tagged pointer safely) + +Accessing the wrong union member is **undefined behavior**. `das_to_string(Type::tHandle)` returns an empty string — use `ti->getAnnotation()->name` for handled type names. + +**Example** — `new_and_init` allocates and initializes any struct: + +```cpp +vec4f new_and_init(Context & context, SimNode_CallBase * call, vec4f * args) { + TypeInfo * typeInfo = call->types[0]; + if (typeInfo->type != Type::tStructure) + context.throw_error_at(call->debugInfo, "expected struct"); + auto size = getTypeSize(typeInfo); + auto data = context.allocate(size, &call->debugInfo); + if (typeInfo->structType && typeInfo->structType->init_mnh) { + auto fn = context.fnByMangledName(typeInfo->structType->init_mnh); + context.callWithCopyOnReturn(fn, nullptr, data, 0); + } else { + memset(data, 0, size); + } + return cast::from(data); +} + +addInterop(*this, lib, "new_and_init", + SideEffects::none, "new_and_init"); +``` + +## C++ Codebase Notes + +- Main type inference: `src/ast/ast_infer_type.cpp` (implementation) + `include/daScript/ast/ast_infer_type.h` (class declarations for `CaptureLambda` and `InferTypes`) +- Builtin runtime functions: `src/builtin/module_builtin_runtime.cpp` +- Smart pointer builtins: `move`, `move_new`, `smart_ptr_clone`, `smart_ptr_use_count` +- Compilation errors: `include/daScript/ast/compilation_errors.h` (error codes 10001–40214) +- Lexer: `src/parser/ds2_lexer.lpp` +- Parser: `src/parser/ds2_parser.ypp` + +### Key AST function flags + +- `func.flags.isClassMethod` — function is a struct/class method (set after inference) +- `func.moreFlags.isStaticClassMethod` — static method (declared with `def static`); name is `StructName\`methodName`, self is explicit first argument +- Non-static methods (`isClassMethod=true`, `isStaticClassMethod=false`) — self is added implicitly during inference; name stays unqualified (e.g. `finalize`, `[]`) +- `func.moreFlags.propertyFunction` — property accessor (name starts with `.\``) +- `func.classParent` — pointer to the struct/class that owns the method diff --git a/skills/das_formatting.md b/skills/das_formatting.md new file mode 100644 index 0000000000..97fdf6dd20 --- /dev/null +++ b/skills/das_formatting.md @@ -0,0 +1,24 @@ +# Code Formatting (REQUIRED) + +After creating or modifying any `.das` file that is part of the project (daslib modules, tutorials, tests, etc.), run the source formatter on it. Do NOT format temporary/scratch files that will be deleted. + +**Formatter tool:** `utils/dasCodeFormatter/main.das` + +**Procedure:** + +1. **Back up** the file before formatting: copy it to `.das.bak` +2. **Run the formatter:** `bin/Release/daslang.exe utils/dasCodeFormatter/main.das -- path/to/file.das` +3. **Verify** the formatted file still compiles: `bin/Release/daslang.exe path/to/file.das` + - For test files (no `main`), compile-check with: `bin/Release/daslang.exe dastest/dastest.das -- --test path/to/test.das` + - For module files (no `main`), verify by running a file that requires them +4. **Remove the backup** if formatting succeeded: delete `.das.bak` +5. **Restore from backup** if formatting broke the file: copy `.das.bak` back over the `.das` file, delete the backup, and report the issue + +**When to format:** +- New `.das` files: tutorials, tests, daslib modules, utilities +- Modified `.das` files: after any edits to existing project files + +**When NOT to format:** +- Temporary/scratch files that will be deleted immediately +- Files you are only reading, not modifying +- C++ source files, RST docs, Python scripts, etc. (only `.das` files) diff --git a/skills/daslib_modules.md b/skills/daslib_modules.md new file mode 100644 index 0000000000..4be547ebba --- /dev/null +++ b/skills/daslib_modules.md @@ -0,0 +1,33 @@ +# Standard Library Module Conventions + +## Base + boost pattern + +Many modules come in pairs: `daslib/foo.das` (core) + `daslib/foo_boost.das` (macro layer): + +- **Base module** (`linq.das`, `json.das`, `regex.das`, etc.): pure functional API, runtime functions, iterator implementations +- **Boost module** (`linq_boost.das`, `json_boost.das`, etc.): macro-based sugar, compile-time optimizations, pipe-syntax rewrites +- All boost modules re-export their base module publicly (`require daslib/foo public`), so only `require daslib/foo_boost` is needed — do NOT add a separate `require daslib/foo` +- `regex.das` also has `require strings public`, so `require daslib/regex` (or `require daslib/regex_boost`) makes `slice`, `starts_with`, etc. available +- Example: `linq.das` provides `where`, `select`, `order_by` functions; `linq_boost.das` adds `_fold` macro that rewrites iterator chains into imperative loops + +## Iterator implementation pattern + +Many daslib functions follow this convention for iterator-based operations: + +- `foo_impl` — internal generator function (yields values) +- `foo` — public function returning `iterator` (calls `_impl`) +- `foo_to_array` — convenience wrapper returning `array` (pipes through `to_array`) +- Inplace `foo` on arrays — overload taking `var arr : array` and modifying in place + +## Key daslib modules + +- `daslib/linq.das` — LINQ-style queries (where, select, order_by, group_by, zip, etc.) +- `daslib/linq_boost.das` — `_fold` optimization macro, pipe-syntax macros +- `daslib/match.das` — pattern matching on variants and types +- `daslib/templates_boost.das` — template/reification infrastructure for AST macros; `apply_template` rewrites AST nodes +- `daslib/functional.das` — lazy iterator adapters and higher-order function utilities (filter, map, reduce, fold, scan, enumerate, chain, pairwise, iterate, find, find_index, partition, tap, for_each, flat_map, sorted, repeat, cycle, islice, echo, sum, any, all). Uses lambdas/functions for generator-returning functions (blocks cannot be captured into generators). Non-generator functions (reduce, fold, for_each, find, find_index, partition) also accept blocks. +- `daslib/strings_boost.das` — string manipulation helpers +- `daslib/json.das` / `daslib/json_boost.das` — JSON parsing/generation. Core: `JsValue` variant (7 types: `_object`, `_array`, `_string`, `_number`, `_longint`, `_bool`, `_null`), `JsonValue` struct wrapper, `read_json`, `write_json`, `JV()` constructors, `JVNull()`. Boost: safe access (`?.`, `?[]`, `??`), `from_JV`/`JV` generic struct↔JSON conversion, `%json~...%%` reader macro, `BetterJsonMacro` (`is`/`as` on `JsonValue?`). Settings: `set_no_trailing_zeros`, `set_no_empty_arrays`, `set_allow_duplicate_keys`. `try_fixing_broken_json` repairs LLM output. Key gotcha: `js?.value` accesses `JsonValue.value` field (returns `JsValue`), not a JSON key named "value" — use `js?["value"]` for that. +- `daslib/regex.das` / `daslib/regex_boost.das` — regular expressions. Re-exports `strings` publicly (`require strings public`), so `require daslib/regex` makes `slice`, `starts_with`, etc. available. Core: recursive-descent parser building `ReNode` AST, function-pointer-driven backtracking matcher. `Regex` struct, `regex_compile(pattern, case_insensitive=false, dot_all=false)`, `regex_match(re, str, offset=0)` → end position or -1, `regex_search(re, str, offset=0)` → `int2(start, end)` or `int2(-1,-1)` (finds first match anywhere), `regex_group(re, group_num, str)` → captured substring, `regex_group_by_name(re, name, str)` → named group substring, `re[index]` → `range` for group by int index (1-based), `re["name"]` → `range` for named group (returns `range(0,0)` if not found), `regex_foreach(re, str, block)` iterates all matches passing `range` values, `regex_replace(re, str, block)` replaces matches via block, `regex_replace(re, str, replacement)` replaces matches with template string (`$0`/`$&` for whole match, `$1`-`$9` for numbered groups, `${name}` for named groups, `$$` for literal `$`), `regex_split(re, str)` → `array` of substrings between matches, `regex_match_all(re, str)` → `array` of all match ranges, `is_valid(re)` checks compilation. Supports: `.` (any char except newline — use `dot_all=true` to also match `\n`), `^` (BOL), `$` (EOL), `+` `*` `?` quantifiers (greedy), `+?` `*?` `??` quantifiers (lazy), `{n}` `{n,}` `{n,m}` counted quantifiers (greedy), `{n}?` `{n,}?` `{n,m}?` counted quantifiers (lazy), `(...)` capturing groups, `(?:...)` non-capturing groups, `(?P...)` named capturing groups, `(?=...)` positive lookahead, `(?!...)` negative lookahead, `|` alternation, `[abc]` `[a-z]` `[^...]` character sets, `\w` `\W` `\d` `\D` `\s` `\S` classes, `\b` `\B` word boundaries, `\t` `\n` `\r` `\f` `\v` escapes, `\xHH` hex escapes. ASCII only (256-bit CharSet). Flags: `case_insensitive=true` for case-insensitive matching (ASCII only), `dot_all=true` for dot matching newline. Boost: `%regex~pattern%%` reader macro (compile-time, no double-escaping); flags via `%regex~pattern~flags%%` where `i`=case-insensitive, `s`=dotAll. Key gotchas: `{` must be escaped as `\{` in daslang strings for counted quantifiers (`"\\d\{3}"`), but reader macro takes literal text (`%regex~\d{3}%%`). `regex_match` always matches from position 0 (or offset) — it does NOT search for the pattern; use `regex_search` for first occurrence or `regex_foreach`/`regex_match_all` to find all occurrences. Nested groups have limited support — prefer sequential groups. `-` is only special inside `[...]` character sets. Quantifiers on lookaheads are not allowed. +- `daslib/flat_hash_table.das` — template-based open-addressing hash table (`TFlatHashTable`) with methods: `empty`, `length`, `clear`, `grow`, `rehash`, `reserve`, `key_index`, `key_exists`, `get`, `erase`, `foreach`, `keys`, `values`, `operator[]`, `operator?[]` +- `daslib/builtin.das` — core builtins like `to_array`, `to_table` diff --git a/skills/documentation_rst.md b/skills/documentation_rst.md new file mode 100644 index 0000000000..f925f9800e --- /dev/null +++ b/skills/documentation_rst.md @@ -0,0 +1,118 @@ +# Documentation Conventions + +## Standard Library Documentation + +The stdlib docs live in `doc/source/stdlib/` and are generated from `//!` doc-comments in `daslib/*.das`. + +### Documentation pipeline + +1. `daslib/*.das` files contain `//!` comments for each module, struct, function +2. `doc/reflections/rst_comment.das` extracts comments into `doc/source/stdlib/detail/*.rst` +3. `doc/reflections/das2rst.das` + `doc/reflections/rst.das` combine detail RST with handmade content +4. **`doc/source/stdlib/handmade/`** — manually written module descriptions and examples (2,001 files) +5. Final RST output goes to `doc/source/stdlib/*.rst` + +### Key documentation tools + +- `doc/reflections/gen_module_examples.py` — generates/updates all 86 module-*.rst files with descriptions and compilable examples (31 modules have examples) +- `doc/reflections/test_new_examples.py` — tests all example snippets by running them through `daslang.exe` +- `doc/reflections/fix_short_docs.py` — fixes terse function documentation +- Validate: `bin/Release/daslang.exe doc/reflections/das2rst.das` (exit code 0 = success) + +### Function grouping in generated docs + +Functions in each module's RST are organized into named groups (e.g. "Compilation and validation", "Access", "Match & replace"). Groups are defined in `doc/reflections/das2rst.das` via `group_by_regex("Group Name", mod, %regex~(func1|func2)$%%)`. Any public function not matched by a group regex ends up in an **"Uncategorized"** section. After adding new public functions to a module: + +1. Add the function name to the appropriate `group_by_regex` call in the module's `document_module_*` function in `das2rst.das` +2. Create or update the handmade doc file in `doc/source/stdlib/handmade/` (replace `// stub` with a description) +3. Regenerate: `bin/Release/daslang.exe doc/reflections/das2rst.das` +4. Verify no "Uncategorized" section remains: search for `Uncategorized` in the generated `doc/source/stdlib/*.rst` + +### Adding a module example + +1. Add `example="""..."""` to the `reg()` call in `gen_module_examples.py` +2. Test it: add to `test_new_examples.py` and run `python doc/reflections/test_new_examples.py` +3. Regenerate: `python doc/reflections/gen_module_examples.py` +4. Validate: `bin/Release/daslang.exe doc/reflections/das2rst.das` + +## RST Editing Conventions + +When editing RST files in `doc/source/reference/language/`: + +- All code blocks use `.. code-block:: das` with gen2 syntax +- Include `// output:` comments showing expected output for runnable examples +- Add `require` statements when examples need imports +- Use `:ref:` cross-references to link between pages (labels: `_structs`, `_classes`, `_functions`, `_statements`, `_expressions`, `_arrays`, `_tables`, `_iterators`, `_generators`, `_lambdas`, `_blocks`, `_tuples`, `_variants`, `_bitfields`, `_aliases`, `_modules`, `_options`, `_unsafe`, `_enumerations`, `_generic_programming`, `_pattern-matching`, `_comprehensions`, `_string_builder`, `_macros`, `_reification`, `_finalizers`, `_clone`, `_temporary`, `_move_copy_clone`, `_annotations`, `_program_structure`, `_type_conversions`, `_contexts`, `_locks`, `_datatypes_and_values`, `_pointers`) +- Verify examples compile: `bin/Release/daslang.exe example.das` + +### RST table rules + +RST uses two table formats — **grid tables** and **simple tables**. Both are fragile: + +- **Grid tables** (`+---+---+`): Every row line must be exactly the same width as every separator line. Off-by-one spaces cause Sphinx errors. +- **Simple tables** (`=== ===`): The `=` separator defines column widths. Content in non-last columns must NOT extend past its column's `=` boundary. Headers must start at or after the column's start position (not in the gap). The gap between columns must be at least 2 spaces. +- After creating or editing any RST table, verify the file with a Sphinx build (see below). + +### Documentation workflow (REQUIRED) + +After creating or modifying any RST files, stdlib documentation, or `daslib/*.das` module doc-comments: + +1. **Regenerate stdlib docs** (if `daslib/*.das` files or `doc/reflections/das2rst.das` were changed): + ``` + bin/Release/daslang.exe doc/reflections/das2rst.das + ``` + +2. **Clean Sphinx build** — MUST delete cache; cached builds hide errors: + ``` + cd doc + Remove-Item -Recurse -Force sphinx-build # delete doctree cache + Remove-Item -Recurse -Force ../site/doc # delete HTML output + sphinx-build -b html -d sphinx-build source ../site/doc + ``` + On Linux/Mac: + ``` + cd doc + rm -rf sphinx-build ../site/doc + sphinx-build -b html -d sphinx-build source ../site/doc + ``` + +3. **Verify no new errors or warnings**: Check the build output for `ERROR` and `WARNING`. The build must introduce **no new** Sphinx errors or warnings compared to the baseline. + +**When to run the workflow:** +- New or modified RST files (language docs, tutorials, stdlib docs) +- New or modified `//!` doc-comments in `daslib/*.das` files +- Changes to `doc/reflections/das2rst.das` or `doc/reflections/rst.das` +- New public functions added to any `daslib/*.das` module (also update `group_by_regex` in `das2rst.das`) + +## Tutorial RST conventions + +Tutorial RST files live in `doc/source/reference/tutorials/` with companion `.das` files in `tutorials/language/`. + +- Each RST starts with a label: `.. _tutorial_name:` (e.g., `.. _tutorial_linq:`) +- Include `.. index::` directive with relevant `single: Tutorial; Topic` entries +- Code blocks use `.. code-block:: das` with gen2 syntax +- End each RST with a `.. seealso::` block containing: + - Full source as `:download:` link: `Full source: :download:\`tutorials/language/XX_name.das <../../../../tutorials/language/XX_name.das>\`` + - Next tutorial link (except last): `Next tutorial: :ref:\`tutorial_next_name\`` + - Related language reference links via `:ref:` +- Toctree is in `doc/source/reference/tutorials.rst` — add new tutorials there + +## C++ integration tutorial RST conventions + +C++ integration tutorial RST files live in `doc/source/reference/tutorials/` with `.cpp` and `.das` files in `tutorials/integration/cpp/`. + +- Label pattern: `.. _tutorial_integration_cpp_:` (e.g., `.. _tutorial_integration_cpp_binding_types:`) +- Index entries: `single: Tutorial; C++ Integration; ` +- Code blocks: `.. code-block:: cpp` for C++ code, `.. code-block:: das` for daslang code +- Build & run section with `cmake --build` command and expected output +- End with `.. seealso::` containing: + - `:download:` links for both `.cpp` and `.das` source files + - Previous/Next tutorial links via `:ref:` +- Each tutorial is one self-contained `.cpp` file with embedded `main()` — no separate build infrastructure needed beyond CMake target +- Tutorial CMake targets: `integration_cpp_01` through `integration_cpp_NN` (defined in `tutorials/integration/cpp/CMakeLists.txt`) + +## Cross-reference labels + +- Tutorial labels for cross-references: `tutorial_hello_world`, `tutorial_variables`, `tutorial_operators`, `tutorial_control_flow`, `tutorial_functions`, `tutorial_arrays`, `tutorial_strings`, `tutorial_structs`, `tutorial_enumerations`, `tutorial_tables`, `tutorial_tuples_and_variants`, `tutorial_function_pointers`, `tutorial_blocks`, `tutorial_lambdas`, `tutorial_iterators_and_generators`, `tutorial_modules`, `tutorial_move_copy_clone`, `tutorial_classes`, `tutorial_generics`, `tutorial_lifetime`, `tutorial_error_handling`, `tutorial_unsafe`, `tutorial_string_format`, `tutorial_pattern_matching`, `tutorial_annotations`, `tutorial_contracts`, `tutorial_testing`, `tutorial_linq`, `tutorial_functional`, `tutorial_json`, `tutorial_regex`, `tutorial_operator_overloading`, `tutorial_pointers`, `tutorial_utility_patterns`, `tutorial_random`, `tutorial_dynamic_type_checking`, `tutorial_coroutines`, `tutorial_serialization`, `tutorial_testing_tools` +- C++ integration tutorial labels: `tutorial_integration_cpp_hello_world`, `tutorial_integration_cpp_calling_functions`, `tutorial_integration_cpp_binding_functions`, `tutorial_integration_cpp_binding_types`, `tutorial_integration_cpp_binding_enums`, `tutorial_integration_cpp_interop`, `tutorial_integration_cpp_callbacks`, `tutorial_integration_cpp_methods`, `tutorial_integration_cpp_operators_and_properties` +- C++ integration tutorial plan (remaining): 10 Custom Modules, 11 Context Variables, 12 Smart Pointers & GC, 13 AOT, 14 Serialization, 15 Custom Annotations, 16 Sandbox diff --git a/skills/writing_tests.md b/skills/writing_tests.md new file mode 100644 index 0000000000..a903251dc5 --- /dev/null +++ b/skills/writing_tests.md @@ -0,0 +1,39 @@ +# Testing Conventions (dastest) + +Tests use the `dastest` framework. Test files live in `tests/` with per-module subfolders. + +## Test file structure + +```das +options gen2 +require dastest/testing_boost public +require daslib/module_under_test + +[test] +def test_something(t : T?) { + t |> run("description") <| @(t : T?) { + t |> equal(actual, expected) + t |> success() + } +} +``` + +## Key test functions + +- `t |> equal(actual, expected)` — value equality assertion +- `t |> success()` — mark subtest as passed +- `t |> run("name") <| @(t : T?) { ... }` — named subtest +- `t |> equal(actual, true)` / `t |> equal(actual, false)` — boolean assertions (there is no `expect_true`/`expect_false`) +- `t |> strictEqual(actual, expected)` — strict equality assertion + +## Common test options + +- `options no_unused_function_arguments = false` — suppress warnings for test params +- `options no_unused_block_arguments = false` — suppress warnings for block params +- Shared test helpers go in `_common.das` module files (e.g., `tests/linq/_common.das`) + +## Running tests + +- Single file: `bin/Release/daslang.exe dastest/dastest.das -- --test tests/linq/test_linq_aggregation.das` +- Directory: `bin/Release/daslang.exe dastest/dastest.das -- --test tests/linq/` +- All tests: `bin/Release/daslang.exe dastest/dastest.das -- --test tests/` From 13ee7866ddd2d557d9e636b779ec5fc3dced18c4 Mon Sep 17 00:00:00 2001 From: Boris Batkin Date: Wed, 18 Feb 2026 07:03:17 -0800 Subject: [PATCH 4/5] and that --- tutorials/language/20_lifetime.das | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tutorials/language/20_lifetime.das b/tutorials/language/20_lifetime.das index ca50b05781..692c6d0f96 100644 --- a/tutorials/language/20_lifetime.das +++ b/tutorials/language/20_lifetime.das @@ -39,6 +39,10 @@ def make_data() : array { return <- result } +class MyClass { + dummy : int +} + [export] def main { @@ -111,9 +115,9 @@ def main { // unsafe { delete p } // // Or use 'var inscope' for automatic cleanup: - // unsafe { // needs 'unsafe', because deleting classes is unsafe - // var inscope p = new MyClass() - // } + unsafe { // needs 'unsafe', because deleting classes is unsafe + var inscope p = new MyClass() + } // // deleted automatically at end of scope // === When to use what === From 7b28196f9b928803be761c1ae55897ff2f5d7be5 Mon Sep 17 00:00:00 2001 From: Boris Batkin Date: Wed, 18 Feb 2026 09:16:16 -0800 Subject: [PATCH 5/5] my first macro tutorial --- .github/copilot-instructions.md | 3 +- CLAUDE.md | 3 +- doc/source/reference/tutorials.rst | 25 +- .../tutorials/39_dynamic_type_checking.rst | 20 +- .../tutorials/macros/01_call_macro.rst | 216 ++++++++++++++++++ doc/source/stdlib/rtti.rst | 156 ++++++------- skills/documentation_rst.md | 15 ++ tutorials/macros/01_call_macro.das | 61 +++++ tutorials/macros/call_macro_mod.das | 146 ++++++++++++ 9 files changed, 553 insertions(+), 92 deletions(-) create mode 100644 doc/source/reference/tutorials/macros/01_call_macro.rst create mode 100644 tutorials/macros/01_call_macro.das create mode 100644 tutorials/macros/call_macro_mod.das diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index f797e66f07..cad9b71fcd 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -147,8 +147,9 @@ All code examples and documentation MUST use gen2 syntax (add `options gen2` at - `doc/source/reference/language/` — RST language documentation (36 files) - `doc/source/stdlib/` — RST standard library documentation (auto-generated + handmade) - `doc/reflections/` — Documentation generation tools (das2rst.das, rst.das, gen_module_examples.py) -- `tutorials/language/` — Language tutorial `.das` files (32 progressive tutorials) +- `tutorials/language/` — Language tutorial `.das` files (42 progressive tutorials) - `tutorials/integration/cpp/` — C++ integration tutorials (embedding daslang in C++ host applications) +- `tutorials/macros/` — Macro tutorials (call macros, reader macros, etc.) - `doc/source/reference/tutorials/` — RST companion pages for each tutorial - `tests/linq/` — LINQ module tests (15 test files, ~500 tests) - `tests/functional/` — Functional module tests diff --git a/CLAUDE.md b/CLAUDE.md index f797e66f07..cad9b71fcd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -147,8 +147,9 @@ All code examples and documentation MUST use gen2 syntax (add `options gen2` at - `doc/source/reference/language/` — RST language documentation (36 files) - `doc/source/stdlib/` — RST standard library documentation (auto-generated + handmade) - `doc/reflections/` — Documentation generation tools (das2rst.das, rst.das, gen_module_examples.py) -- `tutorials/language/` — Language tutorial `.das` files (32 progressive tutorials) +- `tutorials/language/` — Language tutorial `.das` files (42 progressive tutorials) - `tutorials/integration/cpp/` — C++ integration tutorials (embedding daslang in C++ host applications) +- `tutorials/macros/` — Macro tutorials (call macros, reader macros, etc.) - `doc/source/reference/tutorials/` — RST companion pages for each tutorial - `tests/linq/` — LINQ module tests (15 test files, ~500 tests) - `tests/functional/` — Functional module tests diff --git a/doc/source/reference/tutorials.rst b/doc/source/reference/tutorials.rst index 562f6c49d7..6e03a8dcf2 100644 --- a/doc/source/reference/tutorials.rst +++ b/doc/source/reference/tutorials.rst @@ -4,11 +4,12 @@ Tutorials ***************************** -This section provides hands-on tutorials organized into three groups: +This section provides hands-on tutorials organized into four groups: * **Language Tutorials** — learn daslang syntax and standard library features * **C Integration Tutorials** — embed daslang in a C host using the ``daScriptC.h`` API * **C++ Integration Tutorials** — embed daslang in a C++ host using the native ``daScript.h`` API +* **Macro Tutorials** — write compile-time code transformations using the daslang macro system .. _tutorials_language: @@ -123,4 +124,24 @@ and a companion ``.das`` script in ``tutorials/integration/cpp/``. tutorials/integration_cpp_17_coroutines.rst tutorials/integration_cpp_18_dynamic_scripts.rst tutorials/integration_cpp_19_class_adapters.rst - tutorials/integration_cpp_20_standalone_contexts.rst \ No newline at end of file + tutorials/integration_cpp_20_standalone_contexts.rst + +.. _tutorials_macros: + +Macro Tutorials +=============== + +These tutorials teach daslang's compile-time macro system: call macros, +reader macros, function macros, and AST manipulation. Each tutorial has +**two** source files — a module (``.das``) that defines the macros and a +usage file that exercises them — because macros cannot be used in the same +module that defines them. + +Run any tutorial from the project root:: + + daslang.exe tutorials/macros/01_call_macro.das + +.. toctree:: + :maxdepth: 1 + + tutorials/macros/01_call_macro.rst \ No newline at end of file diff --git a/doc/source/reference/tutorials/39_dynamic_type_checking.rst b/doc/source/reference/tutorials/39_dynamic_type_checking.rst index cef611f62f..d702835164 100644 --- a/doc/source/reference/tutorials/39_dynamic_type_checking.rst +++ b/doc/source/reference/tutorials/39_dynamic_type_checking.rst @@ -189,16 +189,16 @@ Combine ``is`` and ``as`` to process a heterogeneous collection of shapes: Summary ======= -================================== ================================================ -Function / Syntax Description -================================== ================================================ -``is_instance_of(ptr, type)`` ``true`` if ptr is instance of T via RTTI -``dynamic_type_cast(ptr, T)`` Returns ``T?`` or ``null`` on failure -``force_dynamic_type_cast(ptr, T)`` Returns ``T?`` or panics on failure -``ptr is ClassName`` Syntactic sugar for ``is_instance_of`` -``ptr as ClassName`` Syntactic sugar for force cast -``ptr ?as ClassName`` Syntactic sugar for safe cast -================================== ================================================ +====================================== ================================================ +Function / Syntax Description +====================================== ================================================ +``is_instance_of(ptr, type)`` ``true`` if ptr is instance of T via RTTI +``dynamic_type_cast(ptr, T)`` Returns ``T?`` or ``null`` on failure +``force_dynamic_type_cast(ptr, T)`` Returns ``T?`` or panics on failure +``ptr is ClassName`` Syntactic sugar for ``is_instance_of`` +``ptr as ClassName`` Syntactic sugar for force cast +``ptr ?as ClassName`` Syntactic sugar for safe cast +====================================== ================================================ .. seealso:: diff --git a/doc/source/reference/tutorials/macros/01_call_macro.rst b/doc/source/reference/tutorials/macros/01_call_macro.rst new file mode 100644 index 0000000000..781324d1e8 --- /dev/null +++ b/doc/source/reference/tutorials/macros/01_call_macro.rst @@ -0,0 +1,216 @@ +.. _tutorial_macro_call_macro: + +.. index:: + single: Tutorial; Macros; Call Macros + +==================================== + Macro Tutorial 1: Call Macros +==================================== + +Call macros intercept function-call syntax at compile time and replace it with +arbitrary AST. When the compiler sees ``hello()`` or ``printf("...", args)``, it +invokes your macro's ``visit`` method instead of looking for a function — +giving you full control over what code is generated. + +This tutorial builds three progressively complex call macros: + +1. ``hello()`` — the simplest possible macro (no arguments) +2. ``greet("name")`` — argument validation and string builder construction +3. ``printf(fmt, args...)`` — format-string parsing with argument reordering + +.. note:: + + Macros cannot be used in the module that defines them. Every macro tutorial + therefore has **two** source files: a *module* file containing the macro + definitions and a *usage* file that requires the module and exercises the + macros. + +Prerequisites +============= + +Familiarity with daslang basics (functions, strings, control flow) is assumed. +No prior macro experience is required — concepts are introduced one at a time. + +Key imports used by the module:: + + require daslib/ast // AST node types (ExprConstString, etc.) + require daslib/ast_boost // AST helpers and ExpressionPtr + require daslib/templates_boost // qmacro, $e() reification + require daslib/strings_boost // ExprStringBuilder + require daslib/macro_boost // [call_macro] annotation, macro_verify + + +Section 1 — hello(): Minimal call macro +======================================== + +A call macro is a class that extends ``AstCallMacro``, annotated with +``[call_macro(name="...")]``: + +.. code-block:: das + + [call_macro(name="hello")] + class HelloMacro : AstCallMacro { + def override visit(prog : ProgramPtr; mod : Module?; + var expr : smart_ptr) : ExpressionPtr { + macro_verify(length(expr.arguments) == 0, prog, expr.at, + "hello() takes no arguments") + return <- qmacro(print("hello, call macro!\n")) + } + } + +The ``visit`` method receives: + +* **prog** — the program being compiled (used for error reporting) +* **mod** — the module where the call appears +* **expr** — the call expression (with ``.arguments`` and ``.at`` for source location) + +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:: + + hello() // → print("hello, call macro!\n") + + +Section 2 — greet("name"): Argument validation +=============================================== + +The ``greet`` macro validates its single argument and builds a string +interpolation expression: + +.. code-block:: das + + [call_macro(name="greet")] + class GreetMacro : AstCallMacro { + def override visit(prog : ProgramPtr; mod : Module?; + var expr : smart_ptr) : ExpressionPtr { + macro_verify(length(expr.arguments) == 1, prog, expr.at, + "greet() requires exactly one string argument") + macro_verify(expr.arguments[0] is ExprConstString, prog, expr.at, + "greet() argument must be a string literal") + var inscope sbuilder <- new ExprStringBuilder(at = expr.at) + sbuilder.elements |> emplace_new <| new ExprConstString( + value := "hello, ", at = expr.at) + sbuilder.elements |> emplace_new <| clone_expression(expr.arguments[0]) + sbuilder.elements |> emplace_new <| new ExprConstString( + value := "!\n", at = expr.at) + return <- qmacro(print($e(sbuilder))) + } + } + +Key techniques: + +* **``expr.arguments[0] is ExprConstString``** — compile-time type check on the + AST node to verify the argument is a string literal. +* **``macro_verify``** — emits a compile error and returns an empty expression + if the condition is false. +* **``ExprStringBuilder``** — the AST node for string interpolation + (``"hello, {name}!\n"``). Its ``.elements`` array holds literal strings and + interpolated expressions. +* **``clone_expression``** — duplicates an AST node. Always clone arguments + before inserting them into new AST — the original may be used elsewhere. +* **``$e(expr)``** inside ``qmacro`` — splices an expression node into the + reified AST. + +Usage:: + + greet("world") // → print("hello, world!\n") + greet("daslang") // → print("hello, daslang!\n") + + +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: + +.. code-block:: das + + printf("player (1) scored (2) points\n", "Alice", score) + // → print("player {\"Alice\"} scored {score} points\n") + +Arguments can be **reordered** and **repeated**: + +.. code-block:: das + + printf("result: (2) from (1)\n", "source", 100) + printf("(1) and (1) and (1)\n", "echo") + +The implementation iterates over the format string character by character, +looking for ``(`` ... ``)`` pairs. For each placeholder it: + +1. Extracts the number with ``chop`` and converts it with ``to_int`` +2. Validates bounds with ``macro_verify`` +3. Inserts a ``clone_expression`` of the referenced argument + +.. code-block:: das + + [call_macro(name="printf")] + class PrintfMacro : AstCallMacro { + def override visit(prog : ProgramPtr; mod : Module?; + var expr : smart_ptr) : ExpressionPtr { + macro_verify(length(expr.arguments) >= 1, prog, expr.at, + "printf requires at least a format string argument") + macro_verify(expr.arguments[0] is ExprConstString, prog, expr.at, + "first argument to printf must be a constant string") + let totalArgs = length(expr.arguments) + var inscope sbuilder <- new ExprStringBuilder(at = expr.at) + let format = string((expr.arguments[0] as ExprConstString).value) + var pos = 0 + while (pos < length(format)) { + var open = find(format, '(', pos) + if (open == -1) { + let tail = format.chop(pos, length(format) - pos) + sbuilder.elements |> emplace_new <| new ExprConstString( + value := tail, at = expr.at) + break + } + if (open > pos) { + let text = format.chop(pos, open - pos) + sbuilder.elements |> emplace_new <| new ExprConstString( + value := text, at = expr.at) + } + var close = find(format, ')', open + 1) + macro_verify(close != -1, prog, expr.at, + "unmatched '(' in format string") + var argNumStr = format.chop(open + 1, close - open - 1) + var argNum = to_int(argNumStr) + macro_verify(argNum >= 1, prog, expr.at, + "argument number must be >= 1") + macro_verify(argNum < totalArgs, prog, expr.at, + "argument index out of range") + sbuilder.elements |> emplace_new <| clone_expression( + expr.arguments[argNum]) + pos = close + 1 + } + return <- qmacro(print($e(sbuilder))) + } + } + + +Running the tutorial +==================== + +:: + + daslang.exe tutorials/macros/01_call_macro.das + +Expected output:: + + hello, call macro! + hello, world! + hello, daslang! + player Alice scored 42 points + result: 100 from source + echo and echo and echo + pi is approximately 3.14, or roughly 3 + +.. seealso:: + + Full source: + :download:`call_macro_mod.das <../../../../../tutorials/macros/call_macro_mod.das>`, + :download:`01_call_macro.das <../../../../../tutorials/macros/01_call_macro.das>` + + Language reference: :ref:`Macros ` — full macro system documentation + diff --git a/doc/source/stdlib/rtti.rst b/doc/source/stdlib/rtti.rst index bed3f11898..3dae432921 100644 --- a/doc/source/stdlib/rtti.rst +++ b/doc/source/stdlib/rtti.rst @@ -629,161 +629,161 @@ Handled structures .. das:attribute:: CodeOfPolicies -Object which holds compilation and simulation settings and restrictions. +:Fields: * **aot** : bool - Object which holds compilation and simulation settings and restrictions. -:Fields: * **aot** : bool - Whether ahead-of-time compilation is enabled. + * **aot_lib** : bool - Whether ahead-of-time compilation is enabled. - * **aot_lib** : bool - AOT library mode. + * **standalone_context** : bool - AOT library mode. - * **standalone_context** : bool - Whether standalone context AOT compilation is enabled. + * **aot_module** : bool - Whether standalone context AOT compilation is enabled. - * **aot_module** : bool - Specifies to AOT if we are compiling a module, or a final program. + * **aot_macros** : bool - Specifies to AOT if we are compiling a module, or a final program. - * **aot_macros** : bool - Enables AOT of macro code (like 'qmacro_block' etc). + * **paranoid_validation** : bool - Enables AOT of macro code (like 'qmacro_block' etc). - * **paranoid_validation** : bool - Whether paranoid validation is enabled (extra checks, no optimizations). + * **cross_platform** : bool - Whether paranoid validation is enabled (extra checks, no optimizations). - * **cross_platform** : bool - Whether cross-platform AOT is enabled (if not, we generate code for the current platform). + * **aot_result** : :ref:`das_string ` - Whether cross-platform AOT is enabled (if not, we generate code for the current platform). - * **aot_result** : :ref:`das_string ` - File name for AOT output (if not set, we generate a temporary file). + * **completion** : bool - File name for AOT output (if not set, we generate a temporary file). - * **completion** : bool - If we are in code completion mode. + * **export_all** : bool - If we are in code completion mode. - * **export_all** : bool - Export all functions and global variables. + * **serialize_main_module** : bool - Export all functions and global variables. - * **serialize_main_module** : bool - If not set, we recompile main module each time. + * **keep_alive** : bool - If not set, we recompile main module each time. - * **keep_alive** : bool - Keep context alive after main function. + * **very_safe_context** : bool - Keep context alive after main function. - * **very_safe_context** : bool - Whether to use very safe context (delete of data is delayed, to avoid table[foo]=table[bar] lifetime bugs). + * **always_report_candidates_threshold** : int - Whether to use very safe context (delete of data is delayed, to avoid table[foo]=table[bar] lifetime bugs). - * **always_report_candidates_threshold** : int - Threshold for reporting candidates for function calls. If less than this number, we always report them. + * **max_infer_passes** : int - Threshold for reporting candidates for function calls. If less than this number, we always report them. - * **max_infer_passes** : int - Maximum number of inference passes. + * **stack** : uint - Maximum number of inference passes. - * **stack** : uint - Stack size. + * **intern_strings** : bool - Stack size. - * **intern_strings** : bool - Whether to intern strings. + * **persistent_heap** : bool - Whether to intern strings. - * **persistent_heap** : bool - Whether to use persistent heap (or linear heap). + * **multiple_contexts** : bool - Whether to use persistent heap (or linear heap). - * **multiple_contexts** : bool - Whether multiple contexts are allowed (pinvokes between contexts). + * **heap_size_hint** : uint - Whether multiple contexts are allowed (pinvokes between contexts). - * **heap_size_hint** : uint - Heap size hint. + * **string_heap_size_hint** : uint - Heap size hint. - * **string_heap_size_hint** : uint - String heap size hint. + * **solid_context** : bool - String heap size hint. - * **solid_context** : bool - Whether to use solid context (global variables are cemented at locations, can't be called from other contexts via pinvoke). + * **macro_context_persistent_heap** : bool - Whether to use solid context (global variables are cemented at locations, can't be called from other contexts via pinvoke). - * **macro_context_persistent_heap** : bool - Whether macro context uses persistent heap. + * **macro_context_collect** : bool - Whether macro context uses persistent heap. - * **macro_context_collect** : bool - Whether macro context does garbage collection. + * **max_static_variables_size** : uint64 - Whether macro context does garbage collection. - * **max_static_variables_size** : uint64 - Maximum size of static variables. + * **max_heap_allocated** : uint64 - Maximum size of static variables. - * **max_heap_allocated** : uint64 - Maximum heap allocated. + * **max_string_heap_allocated** : uint64 - Maximum heap allocated. - * **max_string_heap_allocated** : uint64 - Maximum string heap allocated. + * **rtti** : bool - Maximum string heap allocated. - * **rtti** : bool - Whether to enable RTTI. + * **unsafe_table_lookup** : bool - Whether to enable RTTI. - * **unsafe_table_lookup** : bool - Whether to allow unsafe table lookups (via [] operator). + * **relaxed_pointer_const** : bool - Whether to allow unsafe table lookups (via [] operator). - * **relaxed_pointer_const** : bool - Whether to relax pointer constness rules. + * **version_2_syntax** : bool - Whether to relax pointer constness rules. - * **version_2_syntax** : bool - Allows use of version 2 syntax. + * **gen2_make_syntax** : bool - Allows use of version 2 syntax. - * **gen2_make_syntax** : bool - Whether to use gen2 make syntax. + * **relaxed_assign** : bool - Whether to use gen2 make syntax. - * **relaxed_assign** : bool - Allows relaxing of the assignment rules. + * **no_unsafe** : bool - Allows relaxing of the assignment rules. - * **no_unsafe** : bool - Disables all unsafe operations. + * **local_ref_is_unsafe** : bool - Disables all unsafe operations. - * **local_ref_is_unsafe** : bool - Local references are considered unsafe. + * **no_global_variables** : bool - Local references are considered unsafe. - * **no_global_variables** : bool - Disallows global variables in this context (except for generated). + * **no_global_variables_at_all** : bool - Disallows global variables in this context (except for generated). - * **no_global_variables_at_all** : bool - Disallows global variables at all in this context. + * **no_global_heap** : bool - Disallows global variables at all in this context. - * **no_global_heap** : bool - Disallows global heap in this context. + * **only_fast_aot** : bool - Disallows global heap in this context. - * **only_fast_aot** : bool - Only fast AOT, no C++ name generation. + * **aot_order_side_effects** : bool - Only fast AOT, no C++ name generation. - * **aot_order_side_effects** : bool - Whether to consider side effects during AOT ordering. + * **no_unused_function_arguments** : bool - Whether to consider side effects during AOT ordering. - * **no_unused_function_arguments** : bool - Errors on unused function arguments. + * **no_unused_block_arguments** : bool - Errors on unused function arguments. - * **no_unused_block_arguments** : bool - Errors on unused block arguments. + * **allow_block_variable_shadowing** : bool - Errors on unused block arguments. - * **allow_block_variable_shadowing** : bool - Allows block variable shadowing. + * **allow_local_variable_shadowing** : bool - Allows block variable shadowing. - * **allow_local_variable_shadowing** : bool - Allows local variable shadowing. + * **allow_shared_lambda** : bool - Allows local variable shadowing. - * **allow_shared_lambda** : bool - Allows shared lambdas. + * **ignore_shared_modules** : bool - Allows shared lambdas. - * **ignore_shared_modules** : bool - Ignore shared modules during compilation. + * **default_module_public** : bool - Ignore shared modules during compilation. - * **default_module_public** : bool - Default module mode is public. + * **no_deprecated** : bool - Default module mode is public. - * **no_deprecated** : bool - Disallows use of deprecated features. + * **no_aliasing** : bool - Disallows use of deprecated features. - * **no_aliasing** : bool - Disallows aliasing (if aliasing is allowed, temporary lifetimes are extended). + * **strict_smart_pointers** : bool - Disallows aliasing (if aliasing is allowed, temporary lifetimes are extended). - * **strict_smart_pointers** : bool - Enables strict smart pointer checks. + * **no_init** : bool - Enables strict smart pointer checks. - * **no_init** : bool - Disallows use of 'init' in structures. + * **strict_unsafe_delete** : bool - Disallows use of 'init' in structures. - * **strict_unsafe_delete** : bool - Enables strict unsafe delete checks. + * **no_members_functions_in_struct** : bool - Enables strict unsafe delete checks. - * **no_members_functions_in_struct** : bool - Disallows member functions in structures. + * **no_local_class_members** : bool - Disallows member functions in structures. - * **no_local_class_members** : bool - Disallows local class members. + * **report_invisible_functions** : bool - Disallows local class members. - * **report_invisible_functions** : bool - Report invisible functions. + * **report_private_functions** : bool - Report invisible functions. - * **report_private_functions** : bool - Report private functions. + * **strict_properties** : bool - Report private functions. - * **strict_properties** : bool - Enables strict property checks. + * **no_optimizations** : bool - Enables strict property checks. - * **no_optimizations** : bool - Disables all optimizations. + * **fail_on_no_aot** : bool - Disables all optimizations. - * **fail_on_no_aot** : bool - Fails compilation if AOT is not available. + * **fail_on_lack_of_aot_export** : bool - Fails compilation if AOT is not available. - * **fail_on_lack_of_aot_export** : bool - Fails compilation if AOT export is not available. + * **log_compile_time** : bool - Fails compilation if AOT export is not available. - * **log_compile_time** : bool - Log compile time. + * **log_total_compile_time** : bool - Log compile time. - * **log_total_compile_time** : bool - Log total compile time. + * **no_fast_call** : bool - Log total compile time. - * **no_fast_call** : bool - Disables fast call optimization. + * **scoped_stack_allocator** : bool - Disables fast call optimization. - * **scoped_stack_allocator** : bool - Reuse stack memory after variables go out of scope. + * **force_inscope_pod** : bool - Reuse stack memory after variables go out of scope. - * **force_inscope_pod** : bool - Force in-scope for POD-like types. + * **log_inscope_pod** : bool - Force in-scope for POD-like types. - * **log_inscope_pod** : bool - Log in-scope for POD-like types. + * **debugger** : bool - Log in-scope for POD-like types. - * **debugger** : bool - Enables debugger support. + * **debug_infer_flag** : bool - Enables debugger support. - * **debug_infer_flag** : bool - Enables debug inference flag. + * **debug_module** : :ref:`das_string ` - Enables debug inference flag. - * **debug_module** : :ref:`das_string ` - Sets debug module (module which will be loaded when IDE connects). + * **profiler** : bool - Sets debug module (module which will be loaded when IDE connects). - * **profiler** : bool - Enables profiler support. + * **profile_module** : :ref:`das_string ` - Enables profiler support. - * **profile_module** : :ref:`das_string ` - Sets profile module (module which will be loaded when profiler connects). + * **threadlock_context** : bool - Sets profile module (module which will be loaded when profiler connects). - * **threadlock_context** : bool - Enables threadlock context. + * **jit_enabled** : bool - Enables threadlock context. - * **jit_enabled** : bool - JIT enabled - if enabled, JIT will be used to compile code at runtime. + * **jit_module** : :ref:`das_string ` - JIT enabled - if enabled, JIT will be used to compile code at runtime. - * **jit_module** : :ref:`das_string ` - JIT module - module loaded when -jit is specified. + * **jit_jit_all_functions** : bool - JIT module - module loaded when -jit is specified. - * **jit_jit_all_functions** : bool - JIT all functions - if enabled, JIT will compile all functions in the module. + * **jit_debug_info** : bool - JIT all functions - if enabled, JIT will compile all functions in the module. - * **jit_debug_info** : bool - JIT debug info - if enabled, JIT will generate debug info for JIT compiled code. + * **jit_use_dll_mode** : bool - JIT debug info - if enabled, JIT will generate debug info for JIT compiled code. - * **jit_use_dll_mode** : bool - JIT dll mode - if enabled, JIT will generate DLL's into JIT output folder and load them from there. + * **emit_prologue** : bool - JIT dll mode - if enabled, JIT will generate DLL's into JIT output folder and load them from there. * **jit_output_folder** : :ref:`das_string ` - JIT output folder (where JIT compiled code will be stored). diff --git a/skills/documentation_rst.md b/skills/documentation_rst.md index f925f9800e..d72ca286e0 100644 --- a/skills/documentation_rst.md +++ b/skills/documentation_rst.md @@ -111,8 +111,23 @@ C++ integration tutorial RST files live in `doc/source/reference/tutorials/` wit - Each tutorial is one self-contained `.cpp` file with embedded `main()` — no separate build infrastructure needed beyond CMake target - Tutorial CMake targets: `integration_cpp_01` through `integration_cpp_NN` (defined in `tutorials/integration/cpp/CMakeLists.txt`) +## Macro tutorial RST conventions + +Macro tutorial RST files live in `doc/source/reference/tutorials/macros/` with `.das` files in `tutorials/macros/`. + +- Each macro tutorial has **two** source files: a module file (`_mod.das`) with definitions and a usage file (`NN_.das`) that requires the module +- Module files do NOT have numeric prefixes (so `require` resolution works); only usage files are numbered +- Label pattern: `.. _tutorial_macro_:` (e.g., `.. _tutorial_macro_call_macro:`) +- Index entries: `single: Tutorial; Macros; ` +- Code blocks: `.. code-block:: das` with gen2 syntax +- End with `.. seealso::` containing: + - `:download:` links for **both** module and usage source files + - Next tutorial link (except last) +- Toctree is the "Macro Tutorials" section (label `tutorials_macros`) in `doc/source/reference/tutorials.rst` + ## Cross-reference labels - Tutorial labels for cross-references: `tutorial_hello_world`, `tutorial_variables`, `tutorial_operators`, `tutorial_control_flow`, `tutorial_functions`, `tutorial_arrays`, `tutorial_strings`, `tutorial_structs`, `tutorial_enumerations`, `tutorial_tables`, `tutorial_tuples_and_variants`, `tutorial_function_pointers`, `tutorial_blocks`, `tutorial_lambdas`, `tutorial_iterators_and_generators`, `tutorial_modules`, `tutorial_move_copy_clone`, `tutorial_classes`, `tutorial_generics`, `tutorial_lifetime`, `tutorial_error_handling`, `tutorial_unsafe`, `tutorial_string_format`, `tutorial_pattern_matching`, `tutorial_annotations`, `tutorial_contracts`, `tutorial_testing`, `tutorial_linq`, `tutorial_functional`, `tutorial_json`, `tutorial_regex`, `tutorial_operator_overloading`, `tutorial_pointers`, `tutorial_utility_patterns`, `tutorial_random`, `tutorial_dynamic_type_checking`, `tutorial_coroutines`, `tutorial_serialization`, `tutorial_testing_tools` - C++ integration tutorial labels: `tutorial_integration_cpp_hello_world`, `tutorial_integration_cpp_calling_functions`, `tutorial_integration_cpp_binding_functions`, `tutorial_integration_cpp_binding_types`, `tutorial_integration_cpp_binding_enums`, `tutorial_integration_cpp_interop`, `tutorial_integration_cpp_callbacks`, `tutorial_integration_cpp_methods`, `tutorial_integration_cpp_operators_and_properties` - C++ integration tutorial plan (remaining): 10 Custom Modules, 11 Context Variables, 12 Smart Pointers & GC, 13 AOT, 14 Serialization, 15 Custom Annotations, 16 Sandbox +- Macro tutorial labels: `tutorial_macro_call_macro` diff --git a/tutorials/macros/01_call_macro.das b/tutorials/macros/01_call_macro.das new file mode 100644 index 0000000000..54bbe389b0 --- /dev/null +++ b/tutorials/macros/01_call_macro.das @@ -0,0 +1,61 @@ +// Macro Tutorial 1: Call Macros +// +// This tutorial teaches how to write call macros — compile-time +// transformations that intercept function-call syntax and replace +// it with arbitrary AST. +// +// Covers: +// - [call_macro] annotation and AstCallMacro base class +// - The visit() method: inspecting arguments, returning AST +// - macro_verify for compile-time error reporting +// - ExprStringBuilder for building string interpolation AST +// - qmacro and $e() for AST reification +// - clone_expression for argument reuse +// +// The macros are defined in call_macro_mod.das (a separate module, +// because macros cannot be used in the module that defines them). +// +// Run: daslang.exe tutorials/macros/01_call_macro.das + +options gen2 + +require call_macro_mod + +[export] +def main() { + + // Section 1: hello() — simplest call macro + // The macro replaces hello() with print("hello, call macro!\n") + hello() + + // Section 2: greet("name") — macro with argument validation + // The macro reads the string argument and builds: + // print("hello, {name}!\n") + greet("world") + greet("daslang") + + // Section 3: printf(fmt, args...) — format string macro + // (N) in the format string refers to the N-th argument (1-based). + // The macro builds a string interpolation expression at compile time. + var score = 42 + printf("player (1) scored (2) points\n", "Alice", score) + + // Arguments can be reordered — (2) before (1) + printf("result: (2) from (1)\n", "source", 100) + + // Arguments can be repeated + printf("(1) and (1) and (1)\n", "echo") + + // Mixed types work because string interpolation handles conversion + let pi = 3.14 + printf("pi is approximately (1), or roughly (2)\n", pi, int(pi)) +} + +// output: +// hello, call macro! +// hello, world! +// hello, daslang! +// player Alice scored 42 points +// result: 100 from source +// echo and echo and echo +// pi is approximately 3.14, or roughly 3 diff --git a/tutorials/macros/call_macro_mod.das b/tutorials/macros/call_macro_mod.das new file mode 100644 index 0000000000..6c73c1f7fa --- /dev/null +++ b/tutorials/macros/call_macro_mod.das @@ -0,0 +1,146 @@ +// Macro Tutorial 1: Call Macros — Module +// +// This module defines three progressively complex call macros: +// Section 1: hello() — simplest possible call macro +// Section 2: greet("name") — call macro with argument validation +// Section 3: printf(fmt, args...) — format-string macro with reordering +// +// Call macros intercept function-call-like syntax at compile time and +// replace it with arbitrary AST. They cannot be used in the module +// that defines them — see 01_call_macro.das for usage. + +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 + +// ============================================================================ +// Section 1: Minimal call macro — hello() +// +// A call macro is a class that extends AstCallMacro, annotated with +// [call_macro(name="...")]. When the compiler sees a call to that name, +// it invokes the macro's `visit` method instead of looking for a function. +// +// The visit method receives: +// prog — the program being compiled (for error reporting) +// mod — the module where the call appears +// expr — the call expression (arguments, source location, etc.) +// +// It must return an ExpressionPtr — the AST that replaces the call. +// Returning an empty ExpressionPtr signals an error. +// +// qmacro(...) is a reification helper that builds AST from daslang syntax. +// ============================================================================ + +[call_macro(name="hello")] +class HelloMacro : AstCallMacro { + def override visit(prog : ProgramPtr; mod : Module?; var expr : smart_ptr) : ExpressionPtr { + // The simplest macro: ignore arguments, return print("hello\n") + macro_verify(length(expr.arguments) == 0, prog, expr.at, + "hello() takes no arguments") + return <- qmacro(print("hello, call macro!\n")) + } +} + +// ============================================================================ +// Section 2: Call macro with arguments — greet("name") +// +// expr.arguments is an array of ExpressionPtr — one per argument passed +// to the macro call. We can inspect their type at compile time using `is`: +// expr.arguments[0] is ExprConstString — checks if it's a string literal +// +// macro_verify(condition, prog, at, message) is a helper that emits +// a compile error and returns an empty ExpressionPtr if condition is false. +// +// ExprStringBuilder is the AST node for string interpolation ("{...}"). +// We build one manually by filling its `elements` array with: +// - ExprConstString nodes for literal text +// - clone_expression(arg) for interpolated values +// +// The $e(expr) escape inside qmacro inserts an expression node. +// ============================================================================ + +[call_macro(name="greet")] +class GreetMacro : AstCallMacro { + def override visit(prog : ProgramPtr; mod : Module?; var expr : smart_ptr) : ExpressionPtr { + // Validate: exactly one argument, must be a string literal + macro_verify(length(expr.arguments) == 1, prog, expr.at, + "greet() requires exactly one string argument") + macro_verify(expr.arguments[0] is ExprConstString, prog, expr.at, + "greet() argument must be a string literal") + // Build: print("hello, {name}!\n") using ExprStringBuilder + var inscope sbuilder <- new ExprStringBuilder(at = expr.at) + sbuilder.elements |> emplace_new <| new ExprConstString(value := "hello, ", at = expr.at) + sbuilder.elements |> emplace_new <| clone_expression(expr.arguments[0]) + sbuilder.elements |> emplace_new <| new ExprConstString(value := "!\n", at = expr.at) + return <- qmacro(print($e(sbuilder))) + } +} + +// ============================================================================ +// Section 3: Format-string macro — printf(fmt, arg1, arg2, ...) +// +// This macro parses a format string containing (N) placeholders where N +// is a 1-based argument index. It builds an ExprStringBuilder with +// literal text segments interleaved with argument expressions. +// +// Example: printf("(2) said (1)\n", name, greeting) +// → print("{greeting} said {name}\n") +// +// Key techniques: +// - String parsing at compile time (the format string is a constant) +// - to_int() to convert placeholder numbers +// - Bounds checking with macro_verify +// - clone_expression() to duplicate argument AST nodes (since an +// argument might be referenced multiple times in the format string) +// ============================================================================ + +[call_macro(name="printf")] +class PrintfMacro : AstCallMacro { + def override visit(prog : ProgramPtr; mod : Module?; var expr : smart_ptr) : ExpressionPtr { + // Validate: at least a format string + macro_verify(length(expr.arguments) >= 1, prog, expr.at, + "printf requires at least a format string argument") + macro_verify(expr.arguments[0] is ExprConstString, prog, expr.at, + "first argument to printf must be a constant string") + let totalArgs = length(expr.arguments) + var inscope sbuilder <- new ExprStringBuilder(at = expr.at) + let format = string((expr.arguments[0] as ExprConstString).value) + var pos = 0 + while (pos < length(format)) { + // Find the next (N) placeholder + var open = find(format, '(', pos) + if (open == -1) { + // No more placeholders — add remaining text + let tail = format.chop(pos, length(format) - pos) + sbuilder.elements |> emplace_new <| new ExprConstString(value := tail, at = expr.at) + break + } + // Add literal text before the placeholder + if (open > pos) { + let text = format.chop(pos, open - pos) + sbuilder.elements |> emplace_new <| new ExprConstString(value := text, at = expr.at) + } + // Find matching closing paren + var close = find(format, ')', open + 1) + macro_verify(close != -1, prog, expr.at, + "unmatched '(' in format string") + // Parse the argument number + var argNumStr = format.chop(open + 1, close - open - 1) + var argNum = to_int(argNumStr) + macro_verify(argNum >= 1, prog, expr.at, + "argument number must be >= 1, got '({argNumStr})'") + macro_verify(argNum < totalArgs, prog, expr.at, + "argument index ({argNum}) out of range, printf has {totalArgs - 1} argument(s)") + // Insert a clone of the referenced argument expression + sbuilder.elements |> emplace_new <| clone_expression(expr.arguments[argNum]) + pos = close + 1 + } + return <- qmacro(print($e(sbuilder))) + } +}