Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
501 changes: 25 additions & 476 deletions .github/copilot-instructions.md

Large diffs are not rendered by default.

501 changes: 25 additions & 476 deletions CLAUDE.md

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions dastest/dastest.das
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def private collect_input_paths(args : array<string>; var paths : array<string>)


def private collect_files(input_paths : array<string>; var files : array<string>) : bool {
var cache : table<string; void?>
var inscope cache : table<string; void?>
for (path in input_paths) {
if (path |> ends_with(".das")) {
if (!cache |> key_exists(path)) {
Expand All @@ -42,7 +42,6 @@ def private collect_files(input_paths : array<string>; var files : array<string>
}
}
}
delete cache
return true
}

Expand Down
15 changes: 5 additions & 10 deletions dastest/suite.das
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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
}

Expand All @@ -219,7 +217,7 @@ def test_module(var mod : rtti::Module; var context : rtti::Context; suite_ctx :
return default<SuiteResult>
}
var res : SuiteResult
var fileCtx := file_ctx
var inscope fileCtx := file_ctx
var modPtr : Module?
unsafe {
fileCtx.context = addr(context)
Expand Down Expand Up @@ -249,7 +247,6 @@ def test_module(var mod : rtti::Module; var context : rtti::Context; suite_ctx :
}
}
}
delete fileCtx
return res
}

Expand Down Expand Up @@ -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
}


Expand Down
25 changes: 23 additions & 2 deletions doc/source/reference/tutorials.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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
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
4 changes: 3 additions & 1 deletion doc/source/reference/tutorials/20_lifetime.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 10 additions & 10 deletions doc/source/reference/tutorials/39_dynamic_type_checking.rst
Original file line number Diff line number Diff line change
Expand Up @@ -189,16 +189,16 @@ Combine ``is`` and ``as`` to process a heterogeneous collection of shapes:
Summary
=======

================================== ================================================
Function / Syntax Description
================================== ================================================
``is_instance_of(ptr, type<T>)`` ``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<T>)`` ``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::

Expand Down
216 changes: 216 additions & 0 deletions doc/source/reference/tutorials/macros/01_call_macro.rst
Original file line number Diff line number Diff line change
@@ -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<ExprCallMacro>) : 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<ExprCallMacro>) : 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<ExprCallMacro>) : 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 <macros>` — full macro system documentation

Loading