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
1 change: 1 addition & 0 deletions CHANGELIST.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ A public site and service for the `[tune]` ecosystem: upload a tuned box's sidec
- **Position-based liveness** (#3744, #3746) - every expression gets a preorder frame position and locals carry integer liveness intervals, replacing the source-range gate that made eight families of scopes GC-invisible (class-ctor `self`, comprehension accumulators, inliner tails, one-line blocks - locals were swept while alive); `frame_position()` is new das surface
- **Attributable frames only** (#3739) - inline splices stamp host call-site positions, functions reaching `heap_collect` are statically denied fastcall, the collector refuses chains it cannot attribute, and `Context::fastCallDepth` is deleted (C++ ABI break; the hot dispatch path gets lighter)
- **Scratch opt-out** (#3745) - `very_safe_context` gains per-container `set_scratch` so trusted internal buffers free eagerly on growth instead of deferring to GC
- **Opt-in fastcall depth guard** - `options max_fast_call_depth = N` / `CodeOfPolicies::max_fast_call_depth` turns unbounded fastcall recursion from a native stack overflow into a `recover`-able panic: a second `FastCallChecked` node family (fused one- and two-argument shapes included) is emitted only when the cap is set, so unprotected programs run the same nodes as before; a recovered panic restores the counter at every catch point (C++ ABI: `Context` gains two trailing members, `fastCallDepth` - the name #3739 deleted, back as the guard's counter - and `maxFastCallDepth`; `DAS_POLICIES_VERSION` moves to 2 because the new policy field lands in tail padding); found on the way: AOT `TTable::moveT` skipped `tombstones`, so a table moved by value could rehash mid-iteration on stack garbage - it moves the whole header now

#### Automatic Inlining (#3389, #3393, #3396, #3441, #3445, #3462)

Expand Down
2 changes: 1 addition & 1 deletion daslib/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Design rationale not evident in code; numbered module sections are anchored to s

Companion documents carry separate concerns.
- `ARCHITECTURE_LINT.md` - sec. 1-4: perf_lint, lint_config, lint, style_lint.
- `ARCHITECTURE_EMIT.md` - sec. 5-7, 28-29: aot_cpp, aot_standalone, flatten, the shader rails.
- `ARCHITECTURE_EMIT.md` - sec. 5-6: aot_cpp, aot_standalone; `ARCHITECTURE_SHADER.md` - sec. 7, 28-29: flatten, the shader rails.
Comment thread
borisbat marked this conversation as resolved.
- `ARCHITECTURE_CAPI.md` - sec. 30: c_api_header, the C surface both backends emit.
- `ARCHITECTURE_LINQ.md` - sec. 11-17, 33, 37: the linq family, sql_linq, sql_migrate.
- `ARCHITECTURE_CURSOR.md` - sec. 40: ast_cursor, the cursor module the LSP and MCP navigation tools share.
Expand Down
86 changes: 11 additions & 75 deletions daslib/ARCHITECTURE_EMIT.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# daslib architecture notes - emission: AOT C++, standalone contexts, shaders
# daslib architecture notes - emission: AOT C++, standalone contexts

Companion to `ARCHITECTURE.md` in this folder; section numbers are unique across the family.

Expand All @@ -16,6 +16,12 @@ Companion to `ARCHITECTURE.md` in this folder; section numbers are unique across
nothing when its destructor fires during an unwind
(`include/daScript/simulate/ARCHITECTURE.md#aot-finally-unwind`). The pair moves together;
only `test_aot` on a `DAS_ENABLE_EXCEPTIONS` build fails on a mismatch.
- **A `try`/`recover` restores what the interpreter's handler restores**: the emitter writes
the pair as `das_try_recover(__context__, [&](){...}, [&](){...})`, whose catch path puts
back `fastCallDepth` before the recover body runs, then `abiArg`, `abiCMRES` and the stack
watermark after it - the same state `SimNode_TryCatch` and `jit_try_recover` restore
(`include/daScript/simulate/ARCHITECTURE.md#fastcall-depth-guard`). The three move together;
only `test_aot` over a capped program whose recursion stays interpreted fails on a mismatch.
- **C++ identifier mangling**: `aotSuffixNameEx` prepends `_S`/`_E`/`_V`/`_f_` when a das
name is a C++ keyword, holds a non-alnum char, or is `DELETE` (winnt.h). Structs and
enums share ONE C++ namespace while daslang keeps separate tables, so `struct X` +
Expand Down Expand Up @@ -69,11 +75,10 @@ Companion to `ARCHITECTURE.md` in this folder; section numbers are unique across
module constructors `Module::require` earlier ones by name (`fio_core` takes `strings`,
dasHV takes `rtti_core`). A module missing from the daslib list still registers, only in
the dependencies-first pass that follows; a module added to the C++ side joins the list.

- **The AnnotationInfo table resets at the START of the debug-info dump, not its end.** The
globals' `VarInfo`s are written after that dump and a handled global's info refers to an
`AnnotationInfo` by the name the dump minted, so clearing on the way out left `&` with nothing
after it. Only that walk reaches a global's annotation - `writeHandledAnnotations` iterates
`AnnotationInfo` by the name the dump minted, so clearing on the way out would leave `&` with
nothing after it. Only that walk reaches a global's annotation - `writeHandledAnnotations` iterates
types, structs and functions.
- **A member pointer is qualified with `aotModuleName`, never the raw module name.** The main
module is unnamed, so `_module.name` is empty for every type a script declares itself, while
Expand Down Expand Up @@ -160,7 +165,7 @@ Companion to `ARCHITECTURE.md` in this folder; section numbers are unique across
`preVisitExprAddr` never asks. The dependency dump's second `CppAot` emits no expression
and needs no table.
- **Every used `[init]` is called from the ctor, whatever module declares it.** The TU holds
every used function of every module (below), so a required module's `[init]` has an AOT body
every used function of every module (above), so a required module's `[init]` has an AOT body
like any other and needs no special case; the call order is the simulated context's own, read
back through rtti. A `[no_aot]` one stays a collected emit error, because there is no body to
call. An engine that registers itself from its modules - dasLLAMA's architecture registry is
Expand Down Expand Up @@ -191,7 +196,7 @@ Companion to `ARCHITECTURE.md` in this folder; section numbers are unique across
the set, because module constructors `Module::require` those by name (dasHV takes
`rtti_core` this way). A default C++ module the program never reaches stays out, however
the compiler loaded it: a macro module's `daslib/ast` brings `rtti_core` and `ast_core`
into the compiler, and a context that registered them ran two constructors and carried
into the compiler, and a context that registered them would run two constructors and carry
their code for nothing. The pruned modules (`compile time only, not linked`) get no
`aotRequire` include and no registration.
`standaloneModuleRegistration` orders the C++ subset and ranks it: `DEFAULT_MODULE_ORDER`
Expand Down Expand Up @@ -228,72 +233,3 @@ Companion to `ARCHITECTURE.md` in this folder; section numbers are unique across
`#pragma once` and the required modules' `aotRequire` includes so it stands alone in
an embedder TU; two DIFFERENT contexts' headers sharing a das dependency still cannot
be included in one TU (the shared types have no per-type guards).

## 7. flatten

- **Predicated lowering carries one live-mask per exit flavor** - `__flat_live` for
return, a per-loop break mask (persists across unrolled copies) and continue mask
(re-minted per copy). A write's predicate ANDs every active mask plus the structural
predicate; a narrow term excludes its own mask so it self-cancels. An inlined callee
gets a fresh live mask and lowers with `ctx.loopMasks` moved OUT, so its break/continue
can never reach the caller's loops.
- **`flatten_preshade_cse` is a joint fixpoint, not a pipeline** - extraction, regroup,
CSE and alias elimination mutually enable each other; the `_preshader_`/`_cse_` counters
are owned by that loop and re-seeded from surviving suffixes (per-call numbering
re-mints a live name).
- **A CSE/regroup tally counts exactly the regions its rewrite can change** - a duplicate
counted where the rewrite cannot reach never drops below 2 and runs the fixpoint to its
iteration cap.
- **`__flat_ret` carries `safeWhenUninitialized` only while every write is a
self-referential select** - a lowering change that makes the bare-decl read observable
turns the flag into a real uninitialized read.
- **CSE is local value numbering over one converged basic block, and it is complete** -
pure subtrees keyed by `describe()`; value-stability = reads no reassigned name; a store
through index/field/swizzle destabilizes its base; an unrecognized node fails closed as
mutable-reading. Uniform duplicates route to the preshader.
- **The copy-prop/CSE walks stay O(size)** - one name-to-statement index, one structural
walk. A `string` materialized per `ExprVar` in a visitor callback breaks that: each
`describe()` allocates a string that lives to the end of the pass, so the walk goes
quadratic in heap bytes, not only in time.
- **`MutCollect` is what CSE trusts to say whether a name is stable, so it counts every
store spelling, not the one the lowering emits.** CSE treats a name outside its set as
constant for the whole block; a missed store is a shared subexpression across a mutation.
Copies are only the visible half - `<-` also zeroes its SOURCE, `:=` lowers to a
`builtin`clone`(dst, src)` CALL rather than an `ExprClone`, `++`/`+=` are their own
nodes, and a by-reference
argument writes with no assignment node anywhere. Hence the argument arm keys on the
callee's parameter type (non-const and `ref` or a ref type), not on a node kind.
- **`delete` on a container of `ExpressionPtr` frees the BUFFER, never the nodes** -
`delete array<T?>` frees the pointees only for das-heap `T`, and `Expression` is a
handled C++ type whose instances are not heap chunks at all (the
measurement: an `array<S?>` of das structs returns its pointees to `heap_bytes_allocated`,
an `array<ExpressionPtr>` returns only the buffer and the nodes surface in the exit GC
report). That is why `make_float_ctor`'s const-fold early return may leave its lanes
un-consumed while the ctor path `emplace`s them away, why every `unsafe { delete args }`
after it is sound over borrowed tree nodes, and why a struct field holding a borrowed
node needs no `@do_not_delete`. Node lifetime belongs to the AST GC: a lane the const
fold drops is unreachable and collected at the enclosing `ast_gc_guard`.
- **The whitelist admits value-returning primitives only.** `lower_stmt`'s fall-through arm
lowers an unrecognized statement for its lifted sub-lets and drops the statement itself,
which is correct exactly while every surviving call is pure - so `lift_expr` refuses a
whitelisted call that writes through a by-reference argument (`sincos`) rather than let
the drop delete the store. Predicating such a write would need per-out-param temps the
lowering does not own.

## 28. shader_block_layout

- **Two rails, deliberately separate** - the LAYOUT rail admits int64/uint64 as block
members (`compute_block_layout` special-cases them) while the ARITHMETIC rail rejects
64-bit INT (`arith_width_ok` allows width 64 only for floats); `cpu_only_lattice_width`
keys both emitters' fail-closed diagnostic.

## 29. shader_lingua_franca {#shader-lingua-franca}

- **Every symbol is either an exact CPU mirror of its GPU semantics or a `[sideeffects]`
dummy every rail lowers by name** - the dummies return zero on the host, so a CPU replay
reproduces GPU semantics only for the real-bodied set. Unsigned overloads never fold into
signed twins (glslang picks the unsigned opcode).
- **A width-variant of a lowered-by-name symbol is one more overload here, never an emitter
arm.** `unpack8` carries `int16 -> byte2` and `uint16 -> ubyte2` beside the 32-bit pair; every
overload is the same `reinterpret` on the host and the same single `OpBitcast` on the SPIR-V
rail, so the emitter matches the name and reads the width off the operand type.
72 changes: 72 additions & 0 deletions daslib/ARCHITECTURE_SHADER.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# daslib architecture notes - the shader rails: flatten, block layout, lingua franca

Companion to `ARCHITECTURE.md` in this folder; section numbers are unique across the family.

## 7. flatten

- **Predicated lowering carries one live-mask per exit flavor** - `__flat_live` for
return, a per-loop break mask (persists across unrolled copies) and continue mask
(re-minted per copy). A write's predicate ANDs every active mask plus the structural
predicate; a narrow term excludes its own mask so it self-cancels. An inlined callee
gets a fresh live mask and lowers with `ctx.loopMasks` moved OUT, so its break/continue
can never reach the caller's loops.
- **`flatten_preshade_cse` is a joint fixpoint, not a pipeline** - extraction, regroup,
CSE and alias elimination mutually enable each other; the `_preshader_`/`_cse_` counters
are owned by that loop and re-seeded from surviving suffixes (per-call numbering
re-mints a live name).
- **A CSE/regroup tally counts exactly the regions its rewrite can change** - a duplicate
counted where the rewrite cannot reach never drops below 2 and runs the fixpoint to its
iteration cap.
- **`__flat_ret` carries `safeWhenUninitialized` only while every write is a
self-referential select** - a lowering change that makes the bare-decl read observable
turns the flag into a real uninitialized read.
- **CSE is local value numbering over one converged basic block, and it is complete** -
pure subtrees keyed by `describe()`; value-stability = reads no reassigned name; a store
through index/field/swizzle destabilizes its base; an unrecognized node fails closed as
mutable-reading. Uniform duplicates route to the preshader.
- **The copy-prop/CSE walks stay O(size)** - one name-to-statement index, one structural
walk. A `string` materialized per `ExprVar` in a visitor callback breaks that: each
`describe()` allocates a string that lives to the end of the pass, so the walk goes
quadratic in heap bytes, not only in time.
- **`MutCollect` is what CSE trusts to say whether a name is stable, so it counts every
store spelling, not the one the lowering emits.** CSE treats a name outside its set as
constant for the whole block; a missed store is a shared subexpression across a mutation.
Copies are only the visible half - `<-` also zeroes its SOURCE, `:=` lowers to a
`builtin`clone`(dst, src)` CALL rather than an `ExprClone`, `++`/`+=` are their own
nodes, and a by-reference
argument writes with no assignment node anywhere. Hence the argument arm keys on the
callee's parameter type (non-const and `ref` or a ref type), not on a node kind.
- **`delete` on a container of `ExpressionPtr` frees the BUFFER, never the nodes** -
`delete array<T?>` frees the pointees only for das-heap `T`, and `Expression` is a
handled C++ type whose instances are not heap chunks at all (the
measurement: an `array<S?>` of das structs returns its pointees to `heap_bytes_allocated`,
an `array<ExpressionPtr>` returns only the buffer and the nodes surface in the exit GC
report). That is why `make_float_ctor`'s const-fold early return may leave its lanes
un-consumed while the ctor path `emplace`s them away, why every `unsafe { delete args }`
after it is sound over borrowed tree nodes, and why a struct field holding a borrowed
node needs no `@do_not_delete`. Node lifetime belongs to the AST GC: a lane the const
fold drops is unreachable and collected at the enclosing `ast_gc_guard`.
- **The whitelist admits value-returning primitives only.** `lower_stmt`'s fall-through arm
lowers an unrecognized statement for its lifted sub-lets and drops the statement itself,
which is correct exactly while every surviving call is pure - so `lift_expr` refuses a
whitelisted call that writes through a by-reference argument (`sincos`) rather than let
the drop delete the store. Predicating such a write would need per-out-param temps the
lowering does not own.

## 28. shader_block_layout

- **Two rails, deliberately separate** - the LAYOUT rail admits int64/uint64 as block
members (`compute_block_layout` special-cases them) while the ARITHMETIC rail rejects
64-bit INT (`arith_width_ok` allows width 64 only for floats); `cpu_only_lattice_width`
keys both emitters' fail-closed diagnostic.

## 29. shader_lingua_franca {#shader-lingua-franca}

- **Every symbol is either an exact CPU mirror of its GPU semantics or a `[sideeffects]`
dummy every rail lowers by name** - the dummies return zero on the host, so a CPU replay
reproduces GPU semantics only for the real-bodied set. Unsigned overloads never fold into
signed twins (glslang picks the unsigned opcode).
- **A width-variant of a lowered-by-name symbol is one more overload here, never an emitter
arm.** `unpack8` carries `int16 -> byte2` and `uint16 -> ubyte2` beside the 32-bit pair; every
overload is the same `reinterpret` on the host and the same single `OpBitcast` on the SPIR-V
rail, so the emitter matches the name and reads the width off the operand type.
4 changes: 2 additions & 2 deletions daslib/shader_lingua_franca.das
Original file line number Diff line number Diff line change
Expand Up @@ -231,10 +231,10 @@ def public unpack8(x : int) : byte4 => unsafe(reinterpret<byte4>(x))

def public unpack8(x : uint) : ubyte4 => unsafe(reinterpret<ubyte4>(x))

[arch(at="ARCHITECTURE_EMIT.md#shader-lingua-franca")]
[arch(at="ARCHITECTURE_SHADER.md#shader-lingua-franca")]
def public unpack8(x : int16) : byte2 => unsafe(reinterpret<byte2>(x))

[arch(at="ARCHITECTURE_EMIT.md#shader-lingua-franca")]
[arch(at="ARCHITECTURE_SHADER.md#shader-lingua-franca")]
def public unpack8(x : uint16) : ubyte2 => unsafe(reinterpret<ubyte2>(x))

def public pack32(v : byte4) : int => unsafe(reinterpret<int>(v))
Expand Down
8 changes: 8 additions & 0 deletions doc/source/reference/language/options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,14 @@ Optimization
- bool
- false
- Disables the fastcall optimization.
* - ``max_fast_call_depth``
- int
- 0
- Interpreter only. Caps how deep fastcall (frameless) calls may nest; past the cap the
call panics with ``stack overflow, max_fast_call_depth <cap> exceeded while calling
<function>``, which ``recover`` can catch, instead of exhausting the native stack. ``0``
leaves fastcall unchecked and costs nothing. Calls through function pointers, lambdas
and class methods push a regular frame and are already bounded by ``stack``.

--------------------
Memory
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,4 @@ JIT size optimization level for compiled code (0-3).
Path to shared library, which is used in JIT.
Path to linker, which is used in JIT.
compile_file from a script reads and refreshes the default module cache around this compile (keyed by the file, the running binary, the host arguments and these policies).
Interpreter only: caps how deep fastcall (frameless) calls may nest, panicking past the cap instead of exhausting the native stack; 0 leaves fastcall unchecked. Host-side counterpart of ``options max_fast_call_depth`` (the option overrides the policy).
2 changes: 1 addition & 1 deletion include/daScript/ast/ast_serializer.h
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ namespace das {
AstSerializer & serializeModule ( Module & module, bool already_exists );

static constexpr uint32_t getVersion () {
return 220; // 220: (a type back-reference stays inside its module's record vs annotation numeric payloads retain 64 bits)
return 221; // 221: max_fast_call_depth joins the module-cache policy stream
}

void serializeProgram ( ProgramPtr program, ModuleGroup & libGroup ) noexcept;
Expand Down
Loading
Loading