This is the daslang programming language repository (GaijinEntertainment/daScript). daslang (formerly daScript) is a high-performance statically-typed scripting language for games and real-time applications. The repository and many C++ API names still use the old "daScript" spelling.
For the why — design principles, three-tier execution model, the macros-as-design-lens rule — read skills/project_overview.md. The full long-form rationale lives in skills/design_philosophy.md, which is also the single source doc/source/reference/design_philosophy.rst includes for daslang.io — edit the .md, never a copy.
CMake-based build, supported on Windows / Linux / macOS / iOS / Android / WASM (CI runs the full matrix). Quick start: cmake --build build --config Release -j 64, then run bin/Release/daslang path/to/script.das (Windows MSVC layout) or build/daslang ... (single-config Make/Ninja). Builds are slow (15-25 min clean, 2-10 min incremental) — always pass timeout: 0 to long cmake --build commands, do NOT assume "no output" means failure.
Full reference (per-platform generator commands, build flags, AOT debugging, exit code meanings, runtime crash diagnostics): skills/build_and_debug.md.
Clean up files created for diagnostics, staging, and one-off tests before handing work back. This
includes temporary release trees, formatter backups, extracted audio, generated __pycache__
directories, and ad-hoc stdout/stderr logs. Keep intentional application logs, databases, configs,
rollback bundles, and user-owned artifacts. In deployed application directories, do not leave
unrelated diagnostic files beside the program.
- Use GitHub MCP tools (
mcp__github__*) for all GitHub operations (creating PRs, listing issues, reading PRs, etc.) — they avoid shell escaping issues entirely - Fallback: If MCP tools are unavailable, use
ghCLI with--body-filefor any text containing backticks (they're shell escape characters in every supported shell)
Before reaching for Bash/Grep/Read to find a symbol or trace usages in this repo, ToolSearch the daslang MCP tool that answers the question and call it. The tools are deferred (schemas not in turn-start context) so the workflow is two calls: ToolSearch select:mcp__daslang__<tool> → invoke. (defer_loading: false in .mcp.json is the documented knob to flip this, but it's currently broken upstream — set it anyway, then operate as if deferred.)
| Question | Tool |
|---|---|
Where is symbol X defined? (.das) |
find_symbol — pass with_cpp_source=true for builtins / handled types to bridge daslang→C++ in one call |
Where is symbol X defined? (.cpp) |
cpp_find_symbol |
Where is X used? (.das) |
grep_usage |
Where is X used? (.cpp) |
cpp_grep_usage |
| What's in this file? | outline (.das) / cpp_outline (.cpp) |
| Goto definition at cursor | goto_definition (.das) / cpp_goto_definition (.cpp) |
All references to X (.das) |
find_references |
Same applies to lint/format: mcp__daslang__lint / format_file, not shell bin/daslang invocations. Same applies to GitHub operations (see above): mcp__github__*, not gh shell.
Fall back to Bash/Grep/Read only when the MCP tool reports an error or the question is genuinely outside MCP coverage (RST prose, CMake, Python tooling).
Cross-tree guard (#3392): when you work in a git worktree, MCP tool results and LSP diagnostics prepend a CROSS-TREE WARNING if you query a file outside the server's own tree — that answer used the wrong tree's module sources AND that binary's compiled-in C++ bindings, so it's likely stale (missing symbols, outdated signatures). Heed it, don't trust cross-tree output: run the session inside the worktree (its checked-in .claude/skills/daslang-lsp gives correct diagnostics; daslang utils/mcp/setup.das -- --root <worktree> bootstraps its MCP tools). A main-tree-rooted session can never give correct diagnostics for a worktree file.
Task-specific instructions are split into skill files under skills/. You MUST read the relevant skill file(s) before performing the corresponding task.
| Skill file | Read BEFORE... |
|---|---|
skills/project_overview.md |
First significant task — design philosophy, three execution tiers, macros-as-design-lens |
skills/build_and_debug.md |
Build flags, AOT build commands, exit-code/crash diagnosis, options log_infer_passes |
skills/mcp_tools.md |
Full MCP tool table + live-API reference |
skills/das_formatting.md |
Creating or modifying any .das file |
skills/writing_tests.md |
Writing or editing any dastest test (ships in the SDK; applies everywhere) |
skills/tests_in_repo.md |
Adding/moving tests in this repo — AOT registration for a new tests/ dir, the tests/.das_test gating filter, deep-engine model-test rules. Repo-only; deliberately not shipped |
skills/writing_cpp_tests.md |
Writing or editing C++ tests under tests-cpp/ (doctest, leak guards, ctest wiring) |
skills/documentation_rst.md |
Editing RST in doc/source/, //! doc-comments in daslib/*.das, tutorial RST pages |
skills/tutorials.md |
Anything that looks like a tutorial — they live under /tutorials/<area>/, NEVER modules/<X>/tutorial/ |
skills/tutorial_prose.md |
WRITING or revising any general-reader doc/tutorial prose — the voice/vocabulary/structure rules (ESL-plain, flip-it, no forward promises, term discipline, no glossaries); documentation_rst.md is mechanics, this is the words |
skills/cpp_integration.md |
Writing/editing C++ in src/, modules/, tutorials/integration/cpp/ |
skills/clang_bind_build.md |
Enabling the dasClangBind module / installing or bumping the libclang SDK / running any bind_*.das self-binder (bind_clangbind, bind_imgui, …) to regenerate native bindings |
skills/daslib_modules.md |
Working with daslib/ modules or extending the stdlib |
skills/das_macros.md |
Compile-time macros, AST manipulation, qmacro/quote, gc_node patterns |
skills/writing_benchmarks.md |
Writing/running benchmarks/ files |
skills/daspkg.md |
Running daspkg commands, .das_package manifests |
skills/dynamic_modules.md |
.das_module descriptors, adding modules under modules/ |
skills/external_module_debugging.md |
Working on an external daslang module (dasImgui, dasPUGIXML, dasSQLITE, etc.) locally — need to run/lint/test from a standalone daslang.exe or via MCP before push-to-CI. Covers the <DummyRoot>/modules/<your-module> junction pattern + project_root MCP arg |
skills/install_instructions.md |
Updating install/CLAUDE.md or install/skills/ for the shipped SDK |
skills/writing_skills.md |
Adding a skills/*.md file, moving content between skills, or reviewing a skill change — the audience decision (SDK vs repo-only), the shipping gate, and the review checklist for what the gate can't check |
skills/aot_testing.md |
AOT test files, test_aot binary, Module::aotRequire(), AOT hash mismatches |
skills/llvm_tune.md |
The [tune] kernel-tuning framework — [tune_perm]/[tune_scope]/[tune_policy]/--tune, per-box manifests, the runtime-tune-and-re-exec model, the AOT/-exe gates, adding a kernel family (modules/dasLLVM/daslib/llvm_tune.das) |
skills/visitor_gen_bind.md |
Adding Visitor virtual methods / canVisit* gates / gen_bind.das regen |
skills/daslang_live.md |
daslang-live, live-reload lifecycle, [live_command], [before_reload]/[after_reload] |
skills/daslang_lsp.md |
Working on utils/lsp/ (the LSP server for Claude Code / stdio clients) — locked architecture, coordinate conventions, CC wire facts, headless dev rig, protocol tests |
utils/dasHerd/dasherder.md |
Running INSIDE a dasHerd-managed agent session (any DASHERD_SESSION_ID env var set) — the session cooperation contract: inbox/outbox mailbox, declaring participating repositories, Review Bundles, the dasherd.ps1 CLI |
skills/imgui_ui_debugging.md |
CRITICAL UI SKILL — diagnosing/fixing ANY dasImgui UI or interaction bug. The discipline: reproduce + screenshot → make it observable in imgui_snapshot (fix the inspection if it isn't) → fix → prove via snapshot + test → 'after' screenshot. UI is hard; never claim a UI fix works from logic or a screenshot — only from structured snapshot state. |
skills/environment_variables.md |
Reading or ADDING any environment variable — the full daslang set, plus the read-once/typed-default rules a new one must follow. dasLLAMA's own ~130 knobs are generated into modules/dasLLAMA/ENVIRONMENT.md |
skills/perf_lint.md |
Adding rules to daslib/perf_lint.das — and before declaring any hot path off-limits to allocation: [hot_path] / [no_alloc] / [no_env] / [no_io] contracts (PERF026-028), [cold_path] to prune, @scratch to declare a reused buffer |
skills/style_lint.md |
Adding rules to daslib/style_lint.das |
skills/strings.md |
Any .das string operation — find/replace/split/parsing/build_string/peek_data (covers strings, daslib/strings_boost, daslib/strings_convert) |
skills/regex.md |
Writing regular expressions in .das code |
skills/glob.md |
Writing or reviewing any glob/wildcard pattern handling — file selection, include/exclude masks, pattern-match-on-paths (* / ? / ** / [abc]) |
skills/gc_migration.md |
Migrating external/archived code from smart_ptr<T> AST patterns to gc_node (in-tree migration is complete) |
skills/version_update.md |
Bumping the daslang version number |
skills/doc_archiving.md |
Archiving a completed arc's design/plan/audit docs into /history — the archive-vs-stay test, reference-update discipline, area-index notes, the history/README.md ledger |
skills/jobque_debugging.md |
Channel/LockBox/JobStatus/Feature leaks (--track-job-status, DumpJobQueLeaks) |
skills/memory_leak_detection.md |
Any leak report at exit — master index of all six leak-detection mechanisms (gc_node, --das-profiler-leaks, -track-allocations, smart_ptr tracking, jobque, HandleRegistry) and which to reach for |
skills/make_pr.md |
Creating a pull request (lint, test, AOT, format checklist) |
skills/preflight.md |
Pushing a non-trivial branch or reproducing a red CI lane — maps every PR-triggered CI lane to its exact local mirror command (or an honest "not mirrorable") |
skills/abi_break_sweep.md |
Changing public C++ API, AST node layout, or daslib generic signatures that external module repos compile against — both-worlds spellings, externals-merge-first ordering, daspkg-index scope |
skills/wsl_ci_repro.md |
Reproducing a Linux-only CI failure (sanitizers, POSIX divergence, headless timing) in the WSL CI-mirror distro — verbatim-CI recipe and its traps |
skills/babysit.md |
Babysitting an open PR through CI failures and Copilot/human review feedback after the PR is created (the post-open loop) |
skills/strudel_port.md |
Porting strudel.cc patterns into daslang |
skills/clargs_usage.md |
Writing or editing any tool that parses command-line flags — declarative argv parsing via daslib/clargs, plus migration discipline for legacy get_command_line_arguments() callers |
skills/json.md |
Reading/writing JSON in .das code (sprint_json/sscan_json, JV, manual JsonValue?) |
skills/xml.md |
XML via dasPUGIXML/PUGIXML_boost (RAII parsing, builder, XPath, struct round-trip) |
skills/sql.md |
SQL via daslib/sql_linq + providers (dasSQLITE / dasDuckDB / dasPostgreSQL) — [sql_table] / [sql_view] / [sql_fts5] / [sql_function], the _sql(...) LINQ-to-SQL flagship + _each_sql / _sql_update / _sql_delete / _sql_upsert / _create_view / _distinct_by, per-provider capability gates, custom-type adapter rail, @sql_json / @sql_blob columns, transactions, migrations ([sql_migration] + with_latest_sqlite, SQLite-only) |
skills/dashv.md |
Any .das that serves HTTP, streams SSE, makes outbound HTTP requests, or tests a server — HvWebServer (routes GET/POST/STREAM/…), buffered vs streaming responses, handle-first SSE writer ops, the client API (GET/POST/request/with_http_request/get_body_bytes), the with_test_server harness, and the per-context-module-global / options stack gotchas. Worked example: utils/dasllama-server/ |
skills/filesystem.md |
Any .das path/filename/filesystem op — must use fio helpers, never rfind/slice |
skills/detect_dupe.md |
Duplicate-function detection (corpus build, MCP tools export_corpus/detect_duplicates, CLI under utils/detect-dupe/) |
skills/find_dupe.md |
AI-judging a detect-dupe report via Claude (MCP tools judge_duplicates/find_dupe, CLI under utils/find-dupe/); cost guardrails (--dry-run, --max-clusters, --positives-only) |
skills/linq.md |
Filter/map/sort/group/aggregate transforms — preference order: comprehension → linq_boost → plain for. Avoid daslib/functional for new code |
skills/decs.md |
Programming with daslib/decs / decs_boost — entities, components, queries, [decs_template], stages, bulk creation, from_decs linq bridge |
skills/aot_hash_desync_debugging.md |
error[50101]: AOT link failed — semantic-hash desync diagnostics |
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.
Formatter reminder: Use the MCP format_file tool to format .das files. It calls daslib/das_source_formatter directly. Do NOT use utils/dasFormatter/ (that is the v1→v2 syntax converter, not a code formatter).
When you discover something new about daslang syntax, semantics, or conventions — whether through compiler errors, user corrections, or experimentation — update this file with the new knowledge. If it relates to a specific skill area, update the relevant skills/*.md file instead.
Syntax and factual corrections are fix-in-place, always. If a compiler error, probe, or user correction shows that a claim in CLAUDE.md or skills/*.md is wrong, incomplete, or stale, fix it in the same session and flag the edit in the end-of-turn summary — never defer it to a proposal. Verify the corrected claim before writing it (grammar truth is src/parser/ds2_parser.ypp; behavior truth is a probe-compile with the current binary).
Doc improvements at stopping points. Propose-first applies only to what's left: restructuring, removing existing guidance, or proposing a new skill file when you see a recurring pattern that no existing skill covers. Doc edits direct future Claude behavior, so structural diffs still get review — but factual drift must be self-healing, not queued behind it.
Read skills/writing_skills.md first — it carries the full checklist. The three things that matter most:
- Decide the audience before writing. Skills named in
install/skills.listare copied verbatim into the SDK bundle, wheresrc/,tests/,benchmarks/,doc/source/andmodules/*/srcdo not exist. A skill that mixes SDK-usable content with repo plumbing serves neither audience and is painful to split later. Ship it and push repo bits into a(repo-only)section, or keep the whole file repo-only and leave it off the list. - Never fix an audience mismatch by shipping
src/ortests/. Mark the linerepo-onlyinstead (works on a line, or on a heading to cover a whole section).python3 ci/check_shipped_skills.py <bundle> install/skills.listenforces this per-PR viaci/smoke_test_bundle.sh; it also catchesbin/Release/…paths,daslang.exeinvocations, machine-local paths, dead relative links, and references to skills that aren't shipped. - Register it in all the places.
skills/<name>.md, plus a row in the top-levelCLAUDE.mdtable; if shipped, alsoinstall/skills.listand a row ininstall/CLAUDE.md. A skill with no trigger row is a skill nobody opens.
gen2 is the DEFAULT parser — every .das file parses as gen2 unless it explicitly opts out with options gen2 = false (the only gen1 discriminator; the options gen2 markers around the tree are historical no-ops — NEVER infer gen1 from their absence). All code MUST use gen2 syntax; house style still adds options gen2 at the top of new files. Key rules:
- Parentheses on control flow:
if (x > 0),for (i in range(10)),while (running) - Braces on all blocks:
def foo() { ... },if (x) { ... } - Construction:
new Type(field=val)— NOTnew [[Type() field=val]] - Enum access:
EnumName.EnumValuewith dot — NOTEnumName EnumValue - Array literals:
[1, 2, 3]— NOT[[int 1; 2; 3]]. Createsarray<int>; usefixed_array(1, 2, 3)for fixed-size - Struct init:
Foo(a=1, b=2)— NOT[[Foo() a=1, b=2]]. Move-init:Foo(a=1, b <- expr)for non-copyable fields - Table literals:
{ "k" => v, "k2" => v2 }— NOT{{ "k" => v; "k2" => v2 }} - Bare blocks:
{ var x = 1; ... }at statement level creates a lexical scope (NOT a table literal). Supportsfinally:{ ... } finally { ... } with (module foo/bar) { ... }: compile-time resolution scope — names inside resolve as if written in module foo/bar (incl. its PRIVATES + its require graph; the enclosing module's own symbols are NOT visible inside);_::nameescapes back to the enclosing module; locals stay lexical and win; nested forms don't combine — innermost wins outright; erased after infer, zero runtime. Policywith_module_is_unsafe(or.das_projectwith_module_unsafe(mod, file)) makes user-written ones requireunsafe- Named arguments: bare
foo(pos, name = value)— named args after positionals, no brackets needed (0.6.4+, #3410–#3415; probe-verified 2026-07-24); works on method calls too (obj.m(name = v)/obj->m(name = v)). The bracketed formfoo([name = value])still parses; there, multiple named arguments share one bracket group:foo(positional, [first = a, second = b]); separate groups such asfoo([first = a], [second = b])are a syntax error - Block arguments: block/lambda after
func()pipes as last arg. No$for parameterless blocks:defer() { ... }. With params:build_string() $(var writer) { ... }. Lambdas:emplace() @(x : int) { ... }. Arrow shorthand for single-expression blocks:arr |> sort() $(a, b) => a < b. Defaulted parameters sitting between the explicit args and a trailing block are padded automatically — don't spell them out - Lambda:
@(args) { body }or@@(args) { body }(no-capture). Inline arrow form:@(x) => expr(capture lambda) and@@(x) => expr(no-capture function pointer) — preferred for short transforms passed as arguments:sometimes(pat, @@(x) => fast(x, 2.0lf)) - Function/method arrow body:
def add(a, b : int) : int => a + b— single-expression body, return type optional (def add(a, b : int) => a + binfers). Works on class methods too:def get() : int => count + 2. The body must START on the=>line —def f() : int64 =>followed by a newline iserror[30151](probe-verified 2026-07-12); wrap a long body by opening(on the=>line and breaking inside the parens - Generator:
$() { yield value; }or$ { yield value; } - Tuple
=>:a => bcreatestuple<auto;auto> typeinfo:typeinfo trait_name(type<T>)— trait name outside parensstatic_if:static_if (condition) { ... }— parentheses required- Type function call:
take(type<int>, 1, 2)— NOTtake < int > (1, 2) - Casts require call-style parens:
cast<T>(x)/upcast<T>(x)/reinterpret<T>(x)— the parenthesized operand is mandatory and self-delimiting, soreinterpret<uint8?>(p) + 1means(reinterpret<uint8?>(p)) + 1, like the call it looks like. The old juxtaposition formcast<T> xis a syntax error (it swallowed trailing<< >> + - * / % ??into the operand, silently running the arithmetic onx's original type — stride 0 forvoid?, i.e. dropping the add). Related: pointer arithmetic onvoid?is a compile error (30950); do byte math viaintptrand reinterpret once - Newlines inside
(...),[...],{...}are free — long pipe chains, multi-arg calls, array/table literals can wrap freely. Statement-level (no surrounding bracket) still requires one statement per line, so wrap the RHS in(...)if alet x = a |> b |> cneeds to break across lines. DANGER — silent, no error: without the parens, a continuation line starting with a unary-capable operator (+,-) parses as a separate statement —+ bis unary plus, pure, so the optimizer silently deletes it.let x = a⏎+ b⏎+ cbecomes justlet x = a(the+ b/+ clines vanish) — wrong result, no diagnostic (verified:xisa, nota+b+c). A non-unary operator like|> f()can't begin a statement, so it errors loudly instead — it's+/-that bite silently. Always wrap a multi-line arithmetic RHS in(...) - Inline literals over temp-var-and-push — for short arrays consumed in one expression, write
stack([a, b, c])rather thanvar xs : array<T>; xs |> emplace(a); xs |> emplace(b); stack(xs). Faster in interpreted mode and easier to read; same applies to table literals and other bracketed constructors. Threshold: while it stays readable
const in das is purely a type qualifier — mutability lives entirely in the type, fixed at declaration. There is no separate "const variable" concept, and there is one declaration node (let); the let/var keyword only flags whether const is appended to the type.
let(and a parameter with novar) appendsconstto the declared type — whatever you spelled or left to inference. Solet a = x≡let a : auto const = x;var a = x≡var a = x(nothing appended).def f(a : T)≡def f(a : T const);def f(var a : T)≡def f(a : T).varmeans only "do not append the trailingconst." Uniform for every type —int, structs, pointers alike. Solet/varon a local and the presence/absence ofvaron a parameter are the same one mechanism.- Dereference / index / field access appends the handle's const to the result. Unlike C++ (where
*p/p[i]yields the pointee type as-is, so aT* conststill derefs to a mutableT&), das flows the const of the thing you dereference onto the result.a : float? const⟹a[2]isfloat const(notfloat),*aisfloat const,a.fieldis<field> const. Alet/non-varhandle therefore gives const access to everything reachable through it; only avarhandle yields mutable access. (This is the same rule as "take the paramvarfor mutable field access.") - A pointer type has two independent
constpositions:float const?= the pointee is const;float? const= the pointer is const (this is the trailing one thatlet/no-varadds). To write through a pointer parameter you need both non-const →var x : float?. Anything less makesx[i]afloat const&and the write failserror[30952] can't write to a constant value. - The optimizer trusts the type, so stripping const to write is a lie that miscompiles — not a style nit. A
constpointee is a promise to the compiler that that memory is never written; the JIT/AOT may then mark the parameterreadonly, treat anoaliaswrite as dead, or DCE the whole call.var p = reinterpret<float?>(somethingConst); p[i] = …retypes theconstaway, but the original const type already licensed those optimizations — the write silently vanishes, in interp / JIT / AOT alike, with no diagnostic. This is the exact root of thenoaliaswrite-elision and the forwarded-const?elision (issue #3311). Never take a writable pointer asT const?andreinterpret-strip it — declare the parametervar T?. - Idiom for a writable pointer kernel (including fork-callable pointer cores): the output parameter is
var T?(writeo[i]directly, no strip); genuinely read-only inputs areT const?. Callers passaddr(arr[0])straight in — it binds tovar T?(output) and toT const?(input, add-const is implicit), with no launderingreinterpret. At a local,let p = addr(x)is a const handle (p[i]is const — can't write);var p = addr(x)is writable. A hoistedvar pcaptured into aparallel_for/maybe_parallel_forworker stays writable — capture freezes the binding, not the pointee, andp + offis still a mutableT?.
==conston a parameter type — propagates the caller's constness (NOT "always non-const"):def foo(self : MyStruct ==const)accepts eitherMyStructorMyStruct const, and inside the bodyself's constness matches what the caller passed. Use plainFoo?for non-const-only,Foo const?for const-only,Foo? ==constwhen you want the callee to accept either and inherit the caller's view-constremoves aconstfrom a type expression (withreinterpret):unsafe(reinterpret<MyStruct? -const>(addr(self))). Caveat (see The const model above): this is the genuine-interior-mutability escape hatch for a const handle you cannot re-declarevar(e.g. a==const-propagatedself), and it carries the same readonly/noalias-elision risk the optimizer is licensed to take. For a parameter you control that you simply need to write through, the fix isvar T?, not a-const/reinterpret strip — don't reach for-constto paper over a missingvar- Function pointer with explicit type:
@@<(var self : T) : RetT> funcName— specifies the exact parameter/return types of a function pointer literal - OR types in params (
T1 | T2 | …) — a parameter may list alternative accepted types:int | float | double, or heterogeneous forms likearray<int> | table<auto> | auto(NT). This is a generic "OR" type, NOT a runtime tagged variant — the function is monomorphized per the concrete argument type that matches one alternative, so each instantiation sees that concrete type with no per-call dispatch or unpacking cost. Don't "hoist the union cast out of the loop" — there is no union value; a cast likefloat(n)inside the body is just a trivial concrete cast in each instantiation. Use it to widen an overload set in one signature (e.g.def fast(n : int | float | double)accepting bare ints, floats, and doubles at the same call site)
int[10] is a structural type (Type::tFixedArray), not a qualifier vector on the element: one node per dimension, element in firstType, size in fixedDim, outermost first (int[3][4] = FA(3, FA(4, int))); ref/const/temporary live on the chain head. Operations act on the outermost level only (the one-peel rule).
- Generic binding:
auto(TT)binds the WHOLE array (TT = int[3][4]);auto(TT)[]peels one level (TT = int[4], parameter constness inherited);TT - []in a return/alias removes ONE level — pre-0.6.3 docs saying "removes all dims" describe the deleted flattened world safe_addr(arr)on a fixed array returns a pointer to the FIRST ELEMENT (C-style decay; multi-dim peels one level:int[2][3]→int[3]?#) — this is what makesglGetBooleanv(what, safe_addr(flags4))-style C interop work- Typedefs compose:
typedef M4 = float[4]thenM4[10]isfloat[10][4]— array-ness survives aliasing and generic substitution (the pre-0.6.3 flattening bugs are gone) - Runtime
TypeInfois still flattened (dim[]/dimSize) — only the AST is structural. C++TypeDecl::isArray()means "is a fixed array" in both pre- and post-rework daslang (useful for external modules spanning versions) - Macro authors: das-side
TypeDeclfields arefixedDim/fixedDimExpr(NOTdim/dimExpr— deleted); typemacro payloads live intypeMacroExpr; build chains withmake_fixed_array_type(total, element)fromdaslib/ast_boost— details inskills/das_macros.md
- No implicit type promotion:
int + floatis a compile error — both sides must match - No
bool(int)cast — usex != 0; nostring(bool)— use"{flag}" lengthandemptyon a string need norequire— both live in the base module, solength(s)compiles in a bare file (the rest of the string surface still needsrequire strings). 64-bit forms:long_length(s). Theint-returninglengthpanics past 2^31 rather than wrapping — same always-on guard array/table length already carryint("123")does NOT work — useto_intfromrequire strings.to_intsilently returns0on garbage (to_int("foo")→0,to_int("12abc")→12). When you need to validate user/external input — including any string that flows into a shell command, file path, or system call — usetry_to_int/try_to_floatfromdaslib/strings_convertinstead. Those returnResult<T; ConversionError>distinguishinginvalid_argument/out_of_range/trailing_garbage, so";rm -rf;"rejects cleanly instead of becoming0. Same forto_float→try_to_float- Hex literals are
uintby default — useint(0x3F)for int - 16/8-bit type lattice:
float16scalar (half= builtin alias; literal suffix1.5h) + packed vectorshalf2/3/4/8,short2/3/4/8,ushort2/3/4/8,byte2/3/4/8/16(byte = SIGNED int8),ubyte2/3/4/8/16. Tightly packed (half3 = 6B), element-aligned, pass by value. fp16 family has closed arithmetic (per-op promote-to-float-round-back — bit-identical to native fp16); int families are storage + converts only (byte4 + byte4is a compile error; widen viaint4(b4), narrow via ctor = C-truncate orshort4_sat(i4)-style saturating forms)..s-hex swizzles on every vector (v.s3210,b16.sf),.lo/.hion 8/16-lane forms;xyzwstays ≤4 lanes. Conformance harness:tests/type_lattice/(GENERATED — regen viadaslang utils/dasgen/gen_type_conformance.das). Lattice vectors have NO ExprConst nodes by design (isFoldablefalse;Program::makeConstreturns null — callers must handle) default<T>— the zero value ofT. Body of the called function CAN use it.type<T>vsdefault<T>as a witness argument:type<T>is a no-stack tag (compile error if body reads it; annotate[unused_argument(t)]);default<T>is a real zero value (body can read/pass it). Pick by whether the body needs to touch the param. If you want to read atype<T>param, switch the caller todefault<T>— don't rewrite the function.typedecl(expr)— compile-time type-of, usable insidedefault<>:default<typedecl(field)>.- Bitfields:
bitfield Name : uint8 { ... }(alsouint16/uint64; defaultuint, always unsigned). From an int:bitfield64(1ul << 13ul)(alsobitfield8/bitfield16). - Distinct types (nominal newtype, PR #3424):
typedef distinct Foo = int(privacy:typedef private distinct Foo = int) — a unique nominal type over any workhorse underlying, ABI-identical and fully erased at runtime (print/RTTI/debugger show the underlying value). No interconversion in either direction; two distincts over the same underlying are unrelated; overloads onFoovsintcoexist. The only way IN isFoo(expr)(exactly one arg of exactly the underlying type, zero-cost relabel); the only way OUT is*a(peels one distinct level, yields underlying as ref — const flows from the handle; for distinct-over-pointer it peels the distinct, not the pointer). Only==/!=are borrowed (same distinct both sides); all other operators are user-defined (def operator + (a, b : Foo)). NOT a table key by design.default<Foo>/var a : Foozero-init.distinctis NOT a keyword (validated in the grammar action — identifiers like linq'sdistinct()are unaffected). C++ side:DistinctTypeAnnotation+MAKE_DISTINCT_TYPE_FACTORY(TYPE,CTYPE)+makeType-registered externs (worked example:NativeIdin dasUnitTest). Reference:doc/source/reference/language/distinct.rst.
- Structs/arrays/tables always pass by reference — no
&needed. - Only workhorse types (
int,float,bool,string, …,isWorkhorseTypeon the C++ side) pass by value. - AST pointers (gc_node) pass by value — copying the pointer, no refcount, no allocation.
def foo(p : ExpressionPtr)shares the node;var plets you reassign locally;var p : ExpressionPtr&propagates reassignment back. For mutable field access, take the param asvar. - Lambdas are copyable. A
lambda<…>is a fat pointer to a heap-allocated capture frame;=and pass-by-value copy the pointer (creates an alias), andpush/array storage works withoutpush_clone.delete lamrequiresunsafesince other aliases may still be live — same rule as raw pointer / classdelete. The unsafe-delete rule cascades:array<lambda<…>>, structs with a lambda field, tuple/variant containing a lambda — all inherit the unsafe-delete requirement. deleteonarray<T?>(any container of raw pointers) FREES THE POINTEES — it finalizes and heap-frees every element, then the buffer. On borrowed pointers this is heap corruption: interp reportsdeleting <ptr>, which is not a chunk pointer; Release+JIT corrupts SILENTLY and crashes at some later unrelated alloc/free (probe-verified 2026-07-09). For non-owning pointer containers: struct fields take@do_not_delete(canonical:daslib/aot_cpp.das@do_not_delete stack : array<ExprBlock?>— pointees survive, buffer still freed); locals doarr |> clear()beforedelete arr(cleardoes NOT finalize elements — probe-verified).- Strings:
var s : stringis a writable local copy (no propagation).var s : string&propagates.:=clones into current context's heap (required across contexts); plain=copies the pointer. - Residual
smart_ptrtypes (ProgramPtr,ContextPtr,FileAccessPtr,DebugAgentPtr,VisitorAdapterPtr) still use refcount semantics — variables holding them needvar inscope. AST types do NOT — see below.
AST types (TypeDecl, Expression, Function, Structure, Enumeration, Variable, MakeFieldDecl, MakeStruct, every Annotation subclass) are plain raw pointers tracked by gc_node. The only types still using smart_ptr are Program, Context, FileAccess, plus a couple of internal helpers (DebugAgentPtr, VisitorAdapterPtr).
Quick rules:
- AST nodes have unique ownership — don't insert the same pointer into two parents; use
clone_type/clone_expression/clone_function/clone_variable/clone_structureto duplicate. - AST pointers use plain
=and pass-by-value. Novar inscope, no<-for them.var inscopeis for OWNED locals that should finalize at scope exit (smart_ptr types, plain arrays/structs) — never for gc_node AST pointers. - Tools that build AST at runtime (outside the compile pipeline) must wrap their scope in
ast_gc_guard() { ... }fromdaslib/ast, or the leak detector reportsGC APP LEAKat exit. - daslang has garbage collection, but plain
var arr : array<T>does NOT finalize on scope exit. Either declare withvar inscope(finalize-at-scope-exit — serves plain arrays/structs as well as the smart_ptr types; Boris-confirmed + compile-verified 2026-07-26), calldeleteexplicitly, or move out via<-. Per-frame leaks in hot paths usually trace to a localvar arrnever deleted.
Full migration table (when reading older docs that say var inscope or <- for AST types): skills/gc_migration.md.
new Foo()allocates on the current context's heap — each context has its own heap- Contexts cannot retain data from other contexts — only copy. A pointer into context A's heap is invalid in context B
- Threads run in separate contexts —
new_thread() <| @{ ... }creates a new context. Data must be copied/cloned when crossing thread boundaries clone_string(s)clones a string into the current context's heap — required when passing strings across contexts:=on strings does a clone (new allocation in current context);=copies the pointer (unsafe across contexts)- Channel data: when sending data through channels, the receiving context gets a temporary reference (
#) — clone/copy what you need before the callback returns - Implication for threaded audio: parsed data (arrays, structs) created on the main thread cannot be referenced by pointer from the audio thread. Either clone into the audio thread's context, or use C++-side shared memory that lives outside any daScript context
unsafe(expr)— narrow-scope unsafe, preferred overunsafe { block }. Limits unsafe to the exact expression that needs it. Lint backs this: STYLE024 flagsunsafewraps with no descendant needing unsafe; STYLE025 flags blocks where exactly one statement needs unsafe (narrow to expression form); STYLE026 flags nestedunsafe { ... }- The expression form does NOT propagate into nested call arguments.
unsafe(f(addr(x)))still fails witherror[31000] address of reference requires unsafeat the inneraddr— the wrap only authorizes the unsafe op at the top ofexpr, not a sub-expression buried in an argument. Wrap the exact unsafe op instead:f(unsafe(addr(x))). The block formunsafe { f(addr(x)) }does cover the nested op (whole scope), so it's the fallback when several nested ops need it. The one sanctioned exception isaddr<T?>(x)below — its desugared reinterpret propagates the wrap down to its own generated addr.
- The expression form does NOT propagate into nested call arguments.
addr<T?>(x)— sugar forreinterpret<T?>(addr(x))with ONE unsafe gate:unsafe(addr<T?>(x))covers both halves (the desugared reinterpret carriescastFlags.fromAddrSugar; infer preVisit pushesalwaysSafedown onto the generated inner addr). Target must be a pointer type —addr<int>(x)iserror[30133], always a typo foraddr<int?>. The exact-type form (addr<int?>(i)whereaddr(i)is alreadyint?) folds to plainaddrbut still demands unsafe via the addr gate. Prefer over the double-wrappedunsafe(reinterpret<T?>(unsafe(addr(x))))— STYLE034 flags the long form (pointer targets only; the sugar's own output is exempt)- Local reference binding is unsafe:
let blk & = exprrequiresunsafewhenever it creates a local reference to a non-local expression —let blk & = unsafe(expr) - Variant
asread access is safe:(v as _field).memberworks withoutunsafeafter anischeck - Variant field assignment is always unsafe:
v._field = valueandset_variant_index(v, N)requireunsafe reinterpret<T>(expr)requiresunsafe— used for const-stripping on regular pointers:unsafe(reinterpret<Foo?>(const_ptr)). Since casts require call-style parens (#3420), the cast is self-delimiting:reinterpret<float?>(b) + 1adds on the CAST result's stride (probe-verified 2026-07-24; the old prefix-form operand-swallow trap is gone — that form is now a syntax error)typeinfo is_unsafe_when_uninitialized(type<T>)— the trait that gates unsafe-ness per type in generic code. Pairs with field-level@safe_when_uninitializedand struct-level[safe_when_uninitialized]annotations. Canonical use indaslib/builtin.das: declarevar x : TTinsidestatic_if (typeinfo is_unsafe_when_uninitialized(type<TT>)) { unsafe { ... } } else { ... }— the unsafe block needs// nolint:STYLE025on theunsafe {line because STYLE025 sees only one statement needing unsafe at any instantiation and can't reason across the static_if branches
try/recover— NOTtry/catch(recoveris the keyword)panic("message"),assert(condition),verify(condition)(stays in release)assert/verifymessage must be a string CONSTANT —assert(cond, "msg")rejects an interpolated"...{x}..."witherror[30117]"assert comment must be string constant". Use a constant message; when you need the runtime value in the diagnostic, guard instead:if (!cond) panic("...{x}...")(panictakes an interpolated string).- Postfix conditional:
return expr if (cond),break if (cond),continue if (cond)— early-exit guard on one line - Braceless early-exit: prefer
if (cond) return X(or postfixreturn X if (cond)) overif (cond) { return X }— STYLE005 flags the braced single-terminator form as noise - Panic is fatal, not an exception. daslang has no C++/JS-style exception model. A
panic(or failedassert/verify) means the program is broken — the only correct response is to print diagnostics and exit.try/recoverexists to capture the message before exit so you can log it nicely, NOT to recover-and-continue. Do not write code that relies on continuing afterrecover; do not design APIs around panic-as-control-flow. Corollary:{ body } finally { cleanup }deliberately skipscleanupon panic (the cleanup can't run safely on a broken program); this is not a bug. Don't try to "fix" it; don't usefinallyfor cleanup that needs to run on panic. If you need post-statements that run after a block in the normal path, just put them after the block — panic skips everything, and that's the design.
_::foo(x): resolves in the calling module — caller’s overloads visible. Use in library generics.- Unqualified
foo(x): resolves in the defining module — caller’s overloads NOT visible. - This is why
:=anddeleteemit_::clone/_::finalize
A generic that should accept array<T>, array<array<T>>, … (any nesting) — e.g. a flattening push_from(dest, src) — uses an untyped source param (def f(var dest : array<auto(T)>; src), NOT array<auto(TT)>) plus static_if (typeinfo is_array(src)) { for (x in src) f(dest, x) } else { <base> }. ADD it alongside the concrete single-level overload (array<numT>); the concrete one out-specializes a bare untyped param, so flat sources take the fast/bulk path and the recursive form fires only for deeper nesting — zero change/risk to the existing overload. (Live example: variadic push_from/push_clone_from in daslib/builtin.das.)
array<numT>andarray<auto(TT)>are EQUALLY specialized → two such same-name overloads are ambiguous (error[30341] too many matching functions). A bare untypedsrcis less specialized thanarray<numT>, so the concrete one wins — that's why the recursive param is untyped, notarray<auto(TT)>. (Whether an alias should out-specialize a non-alias is an open compiler question.)- Compile-time type equality: there is no
typeinfo is_same_type(A, B)(typeinfo traits take one arg). Usetypeinfo stripped_typename(x) == typeinfo stripped_typename(y)(pattern fromdaslib/algorithm.das). -#/==conston ≥2 same-numTarray params breaks matching (rejects a plainarray<int>&) — use plainarray<numT>for fixed-arity multi-source params (f(dest; a : array<numT>; b : array<numT>)).
a.foo(b) is sugar for foo(a, b) — but only when a is a struct/class value (chains: a.foo().bar(x) ≡ bar(foo(a), x)).
- Works on: struct / class values (incl. by-ref).
- Does NOT work on: primitives (
let n = 5; n.double()→ "can't get field 'double' of int const&"), tuples/arrays, and lambda typedefs — most importantly strudel'sPatterntype (typedef Pattern = lambda<...>);s("bd").fast(2.0lf)fails. Pattern chains must use|>(or direct call). - When telling someone "use pipe here": check the receiver type — for structs
.method()is idiomatic, for lambdas only|>works. Don't say "daslang uses pipes instead of method chains" without qualification.
table[key]inserts a default entry if missing — usetable?[key] ?? defaultfor safe lookupkey_exists(table, key)— check without insertingtable |> insert(key, value)/table |> erase(key)- Never use two
[]lookups on the same table in one expression — re-hashing can invalidate references table[key](read or assign) is safe — do NOT wrap inunsafe(...). Some legacy daslib code hasunsafe(tab[k]); do not propagate that pattern- Move-assign table literal:
tab <- { "k" => v }works for bothvar tab <- { ... }declarations andtab <- { ... }reassignment to existing variables - Table comprehension move-assign:
tab <- { for(x in range(5)); x => x*x }— same move-assign rules apply table<T>(one type param) is the set type — value type elided.var s : table<int>; s |> insert(5); key_exists(s, 5). Distinct fromtable<K; V>(the map form); both shapes coexist. Set-literal init:let STOP_WORDS : table<string> <- { "a", "an", "the" }— value-less braces, comma-separated. Use this instead of declaringvar X : table<T>and populating in an[init]function.
[unsafe_outside_of_for] def each(x) : iterator<T>makes a type iterable inforloops- When the iterator is named
each, the call can be omitted:for (v in each(x))is identical tofor (v in x) - Other iterator names (e.g.
filter,map) cannot be omitted - Iterator element-const variance is pointer-like:
iterator<T -&>flows intoiterator<T const -&>(mut → const), not the reverse. So a generic param declared asiterator<auto(TT) const>takes botheach(array<T>)(yieldsiterator<T -&>) and iterator-comprehension (yieldsiterator<T const -&>) sources via a single instantiation. Theconstqualifier alone is enough — do NOT add&(that's a separate ref-form modifier, not what you want for variance) - Generic-mangling pitfall: instantiations of the same generic that differ only in inner element-const (
iterator<int -&>vsiterator<int const -&>) currently hash-collide in the instance registry, producingerror[50609]: multiple instances of …when both arise in one module. Workaround at the library level: declare the iterator overload asiterator<auto(TT) const>instead ofiterator<auto(TT)>— both source flavors then converge on a single instance per the variance rule above. Caveat: the constify makesitinside the body const, so the body must not move from / mutate / call non-const operators onit. linq.das only constifiesallandcontainsfor this reason; the rest stay vulnerable until the mangler is fixed upstream
peek_data(str) $(arr) { ... }— safe O(1) per-element read access to string asarray<uint8> const#. Onestrlencall total. Preferred overcharacter_atfor iteration.modify_data(str) $(var arr) { ... }— returns a modified copy; allocates new string, opens as mutablearray<uint8>. Use for character-level transformations.character_at(s, i)— O(n) per call (strlen+ bounds check). Fine for isolated checks, but usepeek_datain loops or hot paths.- Pointer-based string access (
reinterpret<uint8?>) is for core library implementations only — user code should usepeek_data/modify_datafor safety.
- Lambda params can shadow function params — use distinct names
whereandsharedare reserved words too —whereis the comprehension filter keyword andsharedthe module modifier; both are a syntax error as a variable name (let where = ...,let shared = ...). Rename (sink_pos,shared_node); hit while writing PERF026-028label,expect,pass, andexplicitare reserved words (lexer keywordsDAS_LABEL/DAS_EXPECT/DAS_PASS/DAS_EXPLICIT—passis the no-op statement,explicitthe generic-parameter modifier you see inarray<T> explicitsignatures) — using any as a parameter/variable name is a syntax error; rename (tag,want,cpass,declared)range/urange/range64/urange64are lexer TYPE tokens (DAS_TRANGEetc., likeint) — unusable as struct-field, parameter, or annotation-argument names (@range = ...is a syntax error); the grammar whitelists them back to a plain name only in call position, which is whyrange(10)works. Rename (rng,span); grammar-verified 2026-07-23- Literal
{/}in string literals must be escaped\{/\}— unescaped{...}is interpolation. Bites when embedding shader/C source as inline strings. String literals may span multiple lines (raw newlines are legal); probe-verified 2026-07-11 - String builder requires
unsafeoroptions persistent_heapif returned - DANGER — silent JIT miscompile: a comprehension used inline as a
<-move-init argument inside a struct constructor (Foo(a <- [for (x in src); expr], b <- local)) yields an empty array under-jit(interp is fine; a plain-local<-field beside it is unaffected). No diagnostic. Hoist the comprehension to avarlocal first, thenFoo(a <- local_comp, b <- local). (This is exactly the fix applied inmodules/dasLLAMA/performance/gen_asr_profile.das.) - Tuple field access:
t._0,t._1,t._2 - Annotations:
[export],[test];options no_aot,options rtti optionsare MODULE-LOCAL for pass-macros ([lint_macro]/AstPassMacro). The macro fires once per module in the require chain, readingprog._optionsfrom THAT module's options table — not the program-root's. Sooptions _my_lint_off = trueinfoo.dassuppresses YOUR lint infoo, butrequire foofrombar.dasdoes not inherit the flag —bargets linted unless it sets its own. Don't confuse with runtime options (gc,multiple_contexts,persistent_heap,rtti) which DO unify across the program codegen and effectively cascade up to consumersoptions stackis MAIN-MODULE-ONLY — it does NOT unify up from required modules (probe-verified 2026-07-09: a 1MBoptions stackin a required shared module left the program's context at the 16K default and it still overflowed). A deep library (dasLLAMA's forward chain, llvm_jit) cannot declare its own stack need — every PROGRAM (test file, example, tool) that drives it must carryoptions stack = 65536(or whatever it needs) itself;llvm_jit.das's 4MB works only because those files run as the program root- Visibility is a prefix keyword, not an annotation:
def private foo(),struct private Foo { ... },enum private E { ... },variable private x = 0,alias private X = Y. There is no[private]annotation — it's a grammar error - Field/variable annotations use
@nameonly:@safe_when_uninitialized at : LineInfo,@sql_primary_key id : int64,@do_not_delete ctx : Context?. The[name]form is reserved for struct/function/global-level annotations and does NOT parse on a struct field requireuses forward slash:require daslib/linq— NOT backslashrequire foo public— re-exportsfootransitivelyrequirepath resolution: barerequire nameresolves a module-root mount (daslib/x, a registered module) or a same-directoryname.das(this is howrequire tutorial_serverpicks uptutorials/dasHV/tutorial_server.das). To require a file elsewhere in the tree by path, use a file-relative path with the explicit.dasextension —require ../../foo/bar.das. Dropping the.das, or using a non-mounted root-relative path likerequire tutorials/x/y, fails witherror[20605] missing prerequisite … file not found. A require path isNAMEtokens joined by/.%..(grammarrequire_module_name) — so it CANNOT contain a hyphen: a dir likeutils/dasllama-server/is unreachable by path (the parser stops at the-, e.g.require ../../utils/dasllama-server/x.dasresolves../../utils/dasllamaand fails). This is why hyphenated-dir tools (detect-dupe,find-dupe,dasllama-server) keep their[test]files inside the dir andrequirethe sibling module by bare name. Upshot: a test needing a shared fixture should keep the fixture in its own directory (bare same-dir require) rather than reaching across the tree[export] def main()defaults to returningvoid, but you can declare it asdef main() : int { ... return rc }when you need to surface a non-zero process exit code (e.g. CLI tools where callers — MCP wrappers, CI, parent shells — branch on exit). Seedastest/dastest.dasfor the canonical pattern. Don't reach forpanicjust to force a non-zero exit; declare: intandreturn rcinstead.pushcopies (fails for non-copyable types),emplacemoves (zeros source),push_cloneclones (preserves source)- Non-copyable types (
array<T>,table<K;V>): use:=,push_clone, or<-. (Lambdas are copyable — see above.) - Blocks cannot be stored/returned/captured — use lambdas or function pointers
- Class methods:
def const,def abstract const,def static; call syntaxobj.method(),obj->method(),obj |> method() is/ason handled types checks EXACT type, not C++ inheritance —expr is ExprFieldisfalsewhenexprisExprSafeField.ason wrong type crashes. Must handle each concrete type explicitly.#pragma optimizein AOT-generated code must be wrapped in#ifdef _MSC_VER— Clang warns on unknown pragmas- Macro-generated
var x : $t(st)(no init) tripserror[31016]"uninitialized variable is unsafe" for any struct result/local type — field defaults do not exempt it. Fixes:= default<$t(st)>when the type is default-constructible; but for handled/backend typesdefault<>iserror[50503] unsupported variable type— then settd.flags.safeWhenUninitialized = trueon the (cloned) decl type when the uninitialized read is intentional and discarded (canonical: the[flatten]return accumulator indaslib/flatten.das) printis for user-facing scripts only. Intests/,daslib/,utils/: useto_log(LOG_INFO|LOG_WARNING|LOG_ERROR)— same stdout, but level-tagged and filterable. Canonical example:utils/detect-dupe/main.das
| Don't write | Write instead | Why |
|---|---|---|
string(x.__rtti) == "ExprFoo" |
x is ExprFoo |
is works directly on AST pointers |
get_ptr(x) == null / get_ptr(x).field |
x == null / x.field |
AST pointers auto-dereference; get_ptr is smart_ptr-era residue |
string(das_str) == "lit", !empty(string(das_str)) |
drop the string(...) cast |
das_string compares with string directly; empty() works on it |
let v = string(x.name); $i(v) / var copy = val; $v(copy) |
$i(x.name) / $v(val) |
qmacro tags accept das_string, let vars, loop vars directly |
6 qmacro arms differing only in the call target (if isTry { qmacro(_::try_run_select(…)) } elif … { … }) |
let fname = (isTry ? "try_run_select" : "run_select") + suffix; qmacro($c(fname)(…)) |
$c(stringVar) splices a function name; resolution at splice site uses user's require chain. Note: _::$c(…) is a parse error — drop _:: |
if (true) { ... } |
{ ... } |
bare blocks create lexical scope in gen2 |
var inscope r <- expr; return <- r |
return <- expr |
direct return avoids intermediate |
unsafe { (reinterpret<ExprBlock?>(blk)).list } / unsafe(reinterpret<T?>(x)) |
make param var + plain x.list |
var param gives non-const field access without reinterpret |
if (cond) { return X } (or { break } / { continue }) |
if (cond) return X or postfix return X if (cond) |
STYLE005: braces around a single-statement early-exit are noise |
for (i in range(length(arr))) { ... arr[i] ... } where i is used only as arr[i] |
for (c in arr) { ... c ... } |
PERF018: direct iteration drops the index variable |
from_JV(v, type<int>, 13) |
v ?? 13 |
STYLE020: json_boost provides operator ?? for every scalar from_JV overload |
var args : table<string; JsonValue?>; args |> insert("k1", JV(v1)); args |> insert("k2", JV(v2)) |
var args = JV((k1=v1, k2=v2)) |
STYLE021: named-tuple JV form (json_boost.das:638) is one line instead of N |
int(BfT.a) | int(BfT.b) (same bitfield, or enum with operator |) |
int(BfT.a | BfT.b) |
PERF019: collapse two int casts to one. Const-foldable forms only surface under lint policies |
int64(a) where a : int64 (or any of the 15 workhorse casts: int*/uint*/float/double/string/bitfield*) |
a |
PERF020: same-type workhorse cast is a no-op ExprCall. Match is baseType-strict, so widening/narrowing/signedness/float↔int still fire as genuine work. User-named bitfield/enum ctors (MyBitfield(x)) and vector ctors (int2(x,y)) are out of scope |
foo |= BfT.m / foo &= ~BfT.m (bitfield foo, single named bit) |
foo.m = true / foo.m = false |
STYLE022: bitfield-as-field assignment reads bit-name-first, drops the ~ for clears |
uint(bf & BfT.m) != 0u / int(bf & BfT.m) == 0 (bitfield bf, single named bit) |
bf.m / !bf.m |
STYLE023: bitfield-as-field read; drop the int cast + != 0 / == 0 compare |
unsafe(x + y) / unsafe { let d = x + y } where nothing inside requires unsafe |
drop the wrap | STYLE024: redundant unsafe — flagged when no descendant matches a known inherently-unsafe shape (reinterpret/upcast cast, delete, addr, table-index, variant-write, ExprCallFunc with unsafeOperation). Macro-generated subtrees (genFlags.generated == true) skipped per design |
unsafe { stmt1; stmt2; stmt_needing_unsafe } (only ONE stmt actually needs unsafe) |
stmt1; stmt2; unsafe(<sub-expr>) |
STYLE025: narrow block-form unsafe to expression-form on the single unsafe-needing statement. Silent when ≥2 statements need unsafe (block is justified) |
unsafe { ...; unsafe { ... }; ... } (nested unsafe { } block) |
drop the inner wrap | STYLE026: outer unsafe already covers the whole inner scope, so the inner block is pure noise. Closure / lambda / generator bodies are NOT nested for this rule — they execute in a separate context where the outer wrap does not propagate |
for (s in A) { B |> push(s) } / push_clone(s) (iter-var only) |
B |> push_from(A) / push_clone_from(A) |
PERF022: the bulk overload in builtin.das reserves combined capacity up front. Single name push/push_clone is overloaded between single-element and bulk (ambiguous when destination is array<T[]>); the _from suffix names the bulk intent. Source must be array<T> or C-array — range/iterator sources are not flagged. emplace is out of scope (const iter-var can't be moved) |
var a : array<T>; for (x in SRC) { if (COND) { a |> push(EXPR) } } (or table<K;V> + insert/a[k]=v) |
var a <- [for (x in SRC); EXPR; where COND] (or \{for (...); k => v; where ...\}) |
STYLE027: var with empty default-init followed by a for-loop that only push/insert into it. Accepts depth ≤ 2 nested fors and if-filters at any depth. emplace excluded — move-source-zeroing differs from comprehension element-construction. Iterator-comprehension form ([$f ...]) NOT suggested |
var t : table<K;V>; t |> insert(k1, v1); t |> insert(k2, v2) (or t[k] = v runs, or 2-arg set inserts) |
var t <- { k1 => v1, k2 => v2 } (set: var s <- { k1, k2 }) |
STYLE031: ≥ 2 contiguous inserts/[]= after an empty table decl collapse to a literal move-assign. Computed keys fine; runs with a duplicate CONST key stay silent (literal duplicates are error[30706], inserts overwrite). table<string; JsonValue?> const-key runs get STYLE021's JV((k1=...)) form instead |
var w : array<T>; w |> push_from(SRC) (empty decl + single bulk copy from an array<T>) |
var w := SRC (clone-assign); if then return <- w, return clone_to_move(SRC) |
STYLE032 (the init half) + PERF009-clone (the return half) — they compose. A bulk push_from/push_clone_from into a fresh-empty array IS a clone-init. Array source only; a C-array source stays silent (var w := cArray ≠ array<T>). Only the statement immediately after the decl is inspected — a reserve/guard between keeps it quiet. PERF009 clone variant: var x := src; return <- x → return clone_to_move(src), NOT return <- src (would move/destroy the source) |
var X = clone_expression(E); ... $e(X) ... (only-uses-are-qmacro-splice) |
drop the pre-clone, inline $e(E) at each splice site |
PERF023: qmacro/qmacro_block/qmacro_expr/qmacro_block_to_array go through apply_template (templates_boost.das:418), whose substitution visitor calls clone_expression on every substitution input (templates_boost.das:252). Pre-cloning is wasted work. Detection: post-expansion $e(X) becomes add_ptr_ref(X) inside an ExprMakeBlock; visitor tracks splice-wrapper depth via preVisitExprCall/visitExprCall counter on add_ptr_ref, classifies each candidate ExprVar reference as "safe" when depth>0. Fires only when ALL uses are safe AND ≥1 is observed. Multi-clone-of-same-source flagged too — apply_template clones each substitution independently |
hand-rolled is X / as X / null-guard / ExprRef2Value-peel ladders in macro code |
qmatch(e, $e(a) + $e(b)) for source-syntax shapes; match (e) { if (ExprField(name = "key", value = ExprVar(...))) { ... } } for node-class shapes |
both matchers peel ExprRef2Value automatically; || alternation, && guards, and match_expr(local) cover most ladders. Limits + the qmatch↔match division of labor: skills/das_macros.md "match (daslib/match)" |
unsafe(reinterpret<T?>(unsafe(addr(x)))) (reinterpret-of-addr, pointer target) |
unsafe(addr<T?>(x)) |
STYLE034: same AST, one unsafe gate instead of two. Non-pointer puns (reinterpret<uint64>(addr(x))) have no addr<T?> spelling and stay silent; the sugar's own desugared output is exempt via castFlags.fromAddrSugar |
b == T('(') where b : T and T is a non-int built-in numeric scalar (also !=, ranges, and Yoda forms) |
make b : int, then write b == '(' |
STYLE035: character literals are int; changing the plain variable once removes every repeated numeric cast. The warning is deduplicated at the declaration. |
int64(length(x)) / uint64(capacity(x)) (also count, find_index, fread, fwrite) |
long_length(x) / long_capacity(x) / … |
LINT017: the inner call returns int, so the 2^31 limit is hit before the widening cast — as a wrap, or as the panic guard on array/table/string length — and the cast buys nothing. Pair table is hardcoded (an exists-check is meaningless for user functions, since a macro can add/remove them) and gated on receiver type, which keeps same-named user overloads and the fixed-array length generic (no long_ twin) silent |
memcpy(d, s, int(n)) / memcmp(d, s, int(n)) where n is int64/uint64/uint |
memcpy(d, s, n) |
LINT018: memcpy/memcmp now carry uint/int64/uint64 size overloads, so the int(...) narrowing is pure loss — above 2^31 it copies the wrong byte count |
cast<T -const>(x) / reinterpret<T -#>(x) / addr<void? -const>(x) — any of -const -& -[] -# ==const ==& on a concrete cast target |
drop the contract | STYLE036: these are substitution contracts — they act only while a generic binds, and infer clears them once consumed, so a flag still set at lint time proves it did nothing. auto/unresolved-alias targets are excluded (there substitution hasn't run and the contract is real); a concrete typedef is NOT excluded (reinterpret<CI? -const> with typedef CI = int const keeps the const) |
For path/filename ops use fio helpers (base_name/dir_name/path_join/etc.) — see skills/filesystem.md. Never hand-roll rfind("/") / slice — misses Windows separators.
Minimize unsafe: Most unsafe(reinterpret<T?>) in macro code exists to strip const from raw-pointer field access. Fix the root cause: make the function parameter var so field access returns non-const pointers. Reserve unsafe for genuinely unsafe operations (pointer arithmetic, reinterpret across unrelated types).
Comment hygiene. Comments are 1–2 lines max. Strict rules:
- No banner comments above a documented function. When a function carries
//!inside its body, drop the// ===== name — desc =====block above. The banner duplicates the doc. - No multi-paragraph architectural prose at the head of a section. Don't write 10–30 line preambles explaining design decisions, surface examples, NULL handling, panic semantics, etc. above
// Section name. Code reads well; design docs (plans,API_REWORK.md, RST tutorials) carry the WHY. If a reader genuinely needs that context, it goes in those docs, not the source. - Private functions and types don't get public-style docs.
//!///!<is for tooling-visible public API. Ondef private,struct private,enum private,variant private, drop the docstring entirely — the symbol isn't exported, so no doc generator ever sees it, and the docstring just restates the function name / field name to a reader who already has them. If a function or field genuinely needs a 1-line WHY (non-obvious invariant, surprising behavior), write a plain// ...line, not//!. The bar for keeping any comment on a private symbol is "a maintainer reading the symbol alone would be surprised." - What stays: terse 2-line section dividers (
// ===== Section name =====),//!docstrings on PUBLIC functions/types (visible to tooling), and inline//comments that flag a non-obvious WHY at the exact line — a workaround for a specific bug, a subtle invariant, behavior that would surprise a reader. Don't restate what the code says. - When in doubt: delete. If reading the code + the relevant docstring(s) doesn't make the WHY clear, the comment was load-bearing. Otherwise it was noise.
Most layout is obvious from ls. Non-obvious ones worth knowing:
daslib/aot_cpp.das— AOT C++ emitter lives here, NOT in C++ (src/ast/ast_aot_cpp.cppwas deleted; only the header remains)tests/aot/CMakeLists.txt— register new test directories here for AOT compilation. Two AOT binaries:test_aot_subset(tests/language only, in ALL — the per-PR CI compile gate) and fulltest_aot(EXCLUDE_FROM_ALL, ~1080 AOT TUs — nightly CI +preflight --fullonly, via--target test_aot/run_tests_aot)dastest/— test framework (used by bothtests/and external repos)utils/detect-dupe/(in-repo dupe finder) andutils/find-dupe/(Claude-based judge that needsdaspkg install --root utils/find-dupe+ANTHROPIC_API_KEY) — both also exposed as MCP toolsutils/mcp/,utils/daslang-live/,utils/daspkg/— in-tree tools (most also have skills underskills/)tutorials/language/(language tour) vstutorials/<area>/(per-area) — never put tutorials inmodules/<X>/tutorial/- Array/dim/vector indexing lives across 5 tiers — bug fixes (bounds checks, neg-index detection, width-aware bounds) usually need parallel edits in all of them: AOT C++ (include/daScript/simulate/aot.h), interpreter non-fused (include/daScript/simulate/runtime_array.h + include/daScript/simulate/simulate_nodes.h), interpreter fused (src/simulate/simulate_fusion_at_array.cpp + src/simulate/simulate_fusion_at.cpp), JIT (modules/dasLLVM/daslib/llvm_jit.das), and AST const-fold (src/ast/ast_simulate.cpp). Debug builds bypass the fused permutations — a fix that lands only in the fused path passes Release CI and trips Debug-ARM. Bump
LLVM_JIT_CODEGEN_VERSIONinmodules/dasLLVM/daslib/llvm_jit_run.dasafter JIT emitter/ABI changes to invalidate cached.dlls (the cache hash also folds per-function AOT hashes, so AST-level changes self-invalidate without a bump)
The daslang MCP server (utils/mcp/main.das) exposes compiler diagnostics, program introspection, and live-reload control. Prefer MCP tools over manual compilation and grep — grep_usage is parse-aware (tree-sitter), find_references resolves cross-module symbols, and live_* tools talk to daslang-live directly instead of curl.
Fresh worktree/clone with no daslang MCP tools? .mcp.json, sgconfig.yml, bin/, and the tree-sitter grammar lib are all gitignored, so a git worktree add (or clone) starts with zero daslang tools. Bootstrap it from any existing daslang binary: daslang utils/mcp/setup.das -- --root <worktree> builds a worktree-local binary (+ grammar), copies the platform sgconfig.yml, and merges a daslang entry into .mcp.json (writes no secrets, preserves github etc.). --no-build wires config to an already-built binary. Restart the session in the worktree to pick it up. Details: skills/mcp_tools.md.
Full tool table (including detect_duplicates/judge_duplicates/find_dupe), live-API caveats, and .mcp.json configuration: skills/mcp_tools.md.