Skip to content
Open
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ doc/source/stdlib/generated/
utils/internal/ast-fuzz/__*.das
utils/internal/ast-fuzz/__*.log
utils/internal/ast-fuzz/_ast_synth_all.das
utils/internal/ast-fuzz/__probes/
# Tutorial recordings: APNG intermediates land here from modules/dasImgui/tests
# record_* drivers; the MP4 deliverables are staged here from the docs-assets
# GitHub release by utils/internal/docs-assets/fetch — neither is tracked.
Expand Down
7 changes: 5 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -326,10 +326,10 @@ MACRO(DAS_LLVM_AOT_LIB input_files genList mainTarget)
if(_batch_n EQUAL 32 OR _llvm_aot_idx EQUAL _llvm_aot_total)
ADD_CUSTOM_COMMAND(
OUTPUT ${_batch_objs}
DEPENDS daslang ${_batch_srcs} ${PROJECT_SOURCE_DIR}/utils/internal/jit/main.das ${_das_llvm_aot_codegen}
DEPENDS daslang ${_batch_srcs} ${PROJECT_SOURCE_DIR}/utils/jit/main.das ${_das_llvm_aot_codegen}
WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}
COMMENT "LLVM-AOT compiling ${_batch_n} files"
COMMAND daslang ${PROJECT_SOURCE_DIR}/utils/internal/jit/main.das -- ${_batch_rels} --aot-object --aot-object-prefix ${mainTarget}
COMMAND daslang ${PROJECT_SOURCE_DIR}/utils/jit/main.das -- ${_batch_rels} --aot-object --aot-object-prefix ${mainTarget}
)
set(_batch_rels "")
set(_batch_srcs "")
Expand Down Expand Up @@ -1903,6 +1903,9 @@ install(FILES ${PROJECT_SOURCE_DIR}/include/fast_float/LICENSE DESTINATION ${DAS
# Install aot tool (the AOT generation driver the integration scaffolds invoke)
install(FILES ${PROJECT_SOURCE_DIR}/utils/aot/main.das DESTINATION utils/aot)

# Install jit tool (the LLVM compile driver: dll cache prewarm, -exe, native .o)
install(FILES ${PROJECT_SOURCE_DIR}/utils/jit/main.das DESTINATION utils/jit)

# Install fix-lint-errors tool (mechanical lint auto-fixer)
install(FILES ${PROJECT_SOURCE_DIR}/utils/fix-lint-errors/main.das DESTINATION utils/fix-lint-errors)

Expand Down
1 change: 1 addition & 0 deletions ci/smoke_test_bundle.sh
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ COMPILE_TESTS=(
"daspkg|utils/daspkg/main.das"
"detect-dupe|utils/detect-dupe/main.das"
"fix-lint-errors|utils/fix-lint-errors/main.das"
"jit|utils/jit/main.das"
"jobque-timeline|utils/jobque-timeline/main.das"
"lint|utils/lint/main.das"
"mcp|utils/mcp/main.das"
Expand Down
38 changes: 38 additions & 0 deletions daslib/ast_verify.das
Original file line number Diff line number Diff line change
Expand Up @@ -801,10 +801,41 @@ class private AstVerifyVisitor : AstVisitor {
}
}

// Neither base overrides visit, so a walk reaching one asserts in Expression::visit - this
// verifier's own walk included. Hence the gc list rather than a descent, and the early out.
def private verify_no_abstract_nodes(mod : Module?) : int {
var n = 0
for_each_gc_expression(mod) $(e) {
if (e is ExprConst || e is ExprMakeLocal) {
n++
let loc = describe(e.at)
let msg = "AST verify: {e.__rtti} is a base class, not a node - build a concrete subclass"
to_log(LOG_ERROR, empty(loc) ? "{msg}\n" : "{msg} at {loc}\n")
compiling_program() |> macro_sticky_error(e.at, msg)
}
}
return n
}

// verify_function has no module to scan, so it asks the program being compiled for one.
def private verify_no_abstract_nodes_being_compiled() : int {
var total = 0
program_for_each_module(compiling_program()) $(m) {
if (empty(m.name)) {
total += verify_no_abstract_nodes(m)
}
}
return total
}

def public verify_module(prog : ProgramPtr; mod : Module?) : int {
//! Verifies one module in place, emitting a sticky compile error per violation and
//! returning the count. Call it from a macro's ``apply()`` right after building
//! AST — the repo's ast-fuzz suite README covers when each entry point applies.
let abstract_nodes = verify_no_abstract_nodes(mod)
if (abstract_nodes > 0) {
return abstract_nodes
}
var v = new AstVerifyVisitor(check_unique = prog._options |> find_arg("_ast_verify_unique") ?as tBool ?? true)
make_visitor(*v) $(adapter) {
v.adapter = adapter
Expand Down Expand Up @@ -838,6 +869,9 @@ def public verify_function(var fn : FunctionPtr) : int {
//! Verifies one function IN PLACE, signature included. For a macro that BUILDS a
//! function: add_function mangles the signature at once, so a malformed result or
//! argument type crashes there, before [[verify_module]] could see it.
if (verify_no_abstract_nodes_being_compiled() > 0) {
return 1
}
var v = new AstVerifyVisitor()
make_visitor(*v) $(adapter) {
visit(fn, adapter)
Expand Down Expand Up @@ -1356,6 +1390,10 @@ def public verify_module_after_infer(prog : ProgramPtr; mod : Module?) : int {
//! Verifies one module is ready for codegen, returning the violation count.
//! Separate from [[verify_module]]: a half-inferred tree legitimately violates
//! these, so they can only be asserted here.
let abstract_nodes = verify_no_abstract_nodes(mod)
if (abstract_nodes > 0) {
return abstract_nodes
}
var v = new AstPostInferVerifyVisitor()
make_visitor(*v) $(adapter) {
visit_module(prog, adapter, mod)
Expand Down
1 change: 1 addition & 0 deletions doc/source/reference/utils.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ built-in leak-detection mechanism.
utils/daspkg.rst
utils/benchctl.rst
utils/aot.rst
utils/jit.rst
utils/mcp.rst
utils/lsp.rst
utils/detect_dupe.rst
Expand Down
94 changes: 94 additions & 0 deletions doc/source/reference/utils/jit.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
.. _utils_jit:

.. index::
single: Utils; jit
single: Utils; JIT
single: Utils; LLVM

==================================================
jit --- LLVM Compile Driver
==================================================

The jit tool compiles ``.das`` files through the LLVM backend without
running them. One run produces, depending on the mode, a cached shared
library (the JIT dll cache), a standalone executable, or a native ``.o``
for static linking into a host binary.

It accepts files and directories, so a whole tree can be compiled in one
go; a directory is walked recursively, skipping ``_``-prefixed entries
and the ``cant_`` / ``failed_`` / ``invalid_`` expected-failure tests.

Requires a daslang built with the LLVM backend.

.. contents::
:local:
:depth: 2


Quick start
===========

Prewarm the JIT dll cache for a tree, one worker per hardware thread::

daslang utils/jit/main.das -- path/to/dir --parallel 0

Build a standalone executable from a script::

daslang utils/jit/main.das -- my_script.das --exe -o my_script

Emit a native object that registers its functions into the AOT library::

daslang utils/jit/main.das -- my_script.das --aot-object -o my_script


Modes
=====

Shared library (default)
The target is compiled JIT-aware and the generated ``.dll`` is written
under ``.jitted_scripts/<namespace>/``, content-hashed, unless
``--output`` names a path. A later run of the same script loads the
cached library instead of generating code again.

``--exe``
Emits a standalone executable. ``--jit-target`` cross-compiles (for
example ``wasm32-unknown-emscripten``), and
``--jit-register-all-modules`` registers every builtin native module at
startup, which a standalone compiler-driver binary needs in order to
recompile arbitrary daslang at runtime.

``--aot-object``
Emits a native ``.o`` covering the used function set, with a load
constructor that registers those functions into the AOT library.
``--aot-object-prefix`` derives one object per input under
``<dir>/_llvm_aot_generated/``, so a single process can emit many
objects and amortize the dasLLVM load cost.


Parallel compilation
====================

``--parallel N`` spawns *N* worker subprocesses; ``--parallel 0`` (or the
flag with no value) picks the hardware thread count. Omitting the flag
compiles sequentially in-process. Workers are full subprocesses because
the compile pipeline is not thread-safe within one process.

``--parallel`` is rejected together with ``--output`` — every file would
collide on the one path.

``--exclude <name>`` skips directories by their own name, at any depth,
and is repeatable.


Tuning flags
============

The JIT tuning flags are accepted alongside the tool's own — among them
``--jit-opt-level``, ``--jit-size-level``, ``--jit-debug``, ``--jit-stack``,
``--jit-dump``, ``--jit-split-modules``, ``--jit-path-to-linker`` and
``--jit-linker-string``. ``daslang utils/jit/main.das -- -?`` prints both
sets with their descriptions.

.. seealso::

:ref:`utils_aot` -- the C++ AOT generation driver
1 change: 1 addition & 0 deletions install/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ For path/filename ops use `fio` helpers (`base_name`/`dir_name`/`path_join`/...)
- `utils/daspkg/`, `utils/dascov/` - package manager; code coverage
- `utils/aot/` - AOT generation driver (`bin/daslang utils/aot/main.das -- -aot <in.das> <out.cpp>`; `-ctx` emits a standalone context dir - the integration tutorial scaffolds invoke it)
- `utils/fix-lint-errors/` - auto-fixer for mechanical lint findings (`--dry-run` to preview)
- `utils/jit/` - LLVM compile driver (`bin/daslang utils/jit/main.das -- <files-or-dirs>`): prewarms the JIT dll cache, `--exe` builds a standalone binary, `--aot-object` emits a native `.o`
- `utils/benchctl/` - benchmark result database + statistical comparison (needs the sqlite module)
- `utils/dasllama-server/` - OpenAI-compatible dasLLAMA inference server (JIT-only; `deploy-jit.ps1` builds a standalone bundle)
- `utils/dasllama-convert/` - offline GGUF -> `.dlim` model prep
Expand Down
4 changes: 2 additions & 2 deletions skills/internal/aot_testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ tests/aot/
| `DAS_AOT_EXT(files, genList, target, tool, extra)` | Core macro - others call this | `extra` alone (no implicit `-aot`; `DAS_AOT` passes `-aot` as the extra) |
| `DAS_AOT_CTX(files, genList, target, tool)` | AOT with custom context | `-ctx` |
| `DAS_AOT_STANDALONE(files, genList, target, tool, extra)` | AOT for a standalone binary | `extra` |
| `DAS_LLVM_AOT_LIB(files, genList, target)` | LLVM-backend AOT - emits native `.o`, not C++; runs `utils/internal/jit/main.das` in batches of 32 | `--aot-object` |
| `DAS_LLVM_AOT_LIB(files, genList, target)` | LLVM-backend AOT - emits native `.o`, not C++; runs `utils/jit/main.das` in batches of 32 | `--aot-object` |

**Target name collision**: `DAS_AOT_EXT` creates a custom target named `${mainTarget}_genaot`. Multiple calls with the same `mainTarget` will collide. Use distinct target names (e.g., `test_aot_testing` and `test_aot_tests`).

Expand Down Expand Up @@ -379,7 +379,7 @@ All `_aot_generated/` directories are covered by a single broad pattern in `.git

### The LLVM-AOT rail (`test_llvm_aot`)

There is a third binary beside `test_aot` / `test_aot_subset`. `test_llvm_aot` (LLVM-only, `EXCLUDE_FROM_ALL`, opt-in - not in ALL and not in CI) compiles each `.das` through the **LLVM backend** into a self-registering native `.o` (a `das_aot_register` load ctor), linked straight into the binary; `-use-aot` then binds each function as a `SimNode_Jit` via `linkCppAot`. It is built by `DAS_LLVM_AOT_LIB` (which drives `utils/internal/jit/main.das --aot-object`, not the C++ AOT tool) and run through the `run_tests_llvm_aot` target in `tests/CMakeLists.txt`.
There is a third binary beside `test_aot` / `test_aot_subset`. `test_llvm_aot` (LLVM-only, `EXCLUDE_FROM_ALL`, opt-in - not in ALL and not in CI) compiles each `.das` through the **LLVM backend** into a self-registering native `.o` (a `das_aot_register` load ctor), linked straight into the binary; `-use-aot` then binds each function as a `SimNode_Jit` via `linkCppAot`. It is built by `DAS_LLVM_AOT_LIB` (which drives `utils/jit/main.das --aot-object`, not the C++ AOT tool) and run through the `run_tests_llvm_aot` target in `tests/CMakeLists.txt`.

**Its corpus is derived, so registering a suite enrols it here too.** `LLVM_AOT_TEST_FILES` is the accumulated `TEST_AOT_ALL_DAS` (every `reg`-flavor suite's test bodies) plus `tests/jit_tests/*.das`, minus `_`-prefixed files and a filter list (`cant_`, `llvm_tune`, `llvm_code`, `llvm_compile_only`, `dll_cache`, `jit_fastpath`, `typeinfo`, and all of `tests/msl/` + `tests/metal/`, which decline the JIT via `lattice_fallback`). Adding a directory to `DAS_AOT_SUITES` therefore silently adds it to the LLVM-AOT corpus - if it can't survive that rail, it needs a filter entry as well.

Expand Down
2 changes: 1 addition & 1 deletion utils/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ set(DAS_UTILS
das-fmt
detect-dupe
lint
jit
internal/hygiene
internal/jit
)

if(NOT DAS_SQLITE_DISABLED)
Expand Down
101 changes: 100 additions & 1 deletion utils/internal/ast-fuzz/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,47 @@ Four axes are opt-in, because each leaves parser-shaped AST behind:
| `--synth-bind` | binds resolved pointers directly (`ExprVar.variable`, `ExprCall.func`, `ExprAddr.func`, `ExprConstEnumeration.enumType`, `ExprField.fieldIndex`) the way a macro does, rather than leaving the name for inference |
| `--synth-pretype` | pre-sets `Expression._type`; infer treats a typed node as already inferred, so a wrong type reaches codegen |

### Typed mode (`--synth-typed`)

The four axes above all generate *ill-typed* AST on purpose, which is what finds
crashes - but it also means inference stops at the first error, so 85-90% of runs
end in a compiler error and the deep half of the type checker is never reached.
Typed mode is the other half of the tool: it builds each expression bottom-up
from a requested type, so the program is **well-typed by construction**, every
seed compiles clean, and inference runs to the end - through overload resolution,
generic instantiation, make-local and codegen.

```
daslang utils/internal/ast-fuzz/main.das -- --bin bin/daslang --threads 16 \
--seeds 4800 --synth-typed --synth-funcs 4 --synth-size 8 --synth-depth 4
```

A compiler **error** in typed mode is a generator bug, not a finding: the whole
contract is that these programs are legal. What still counts is a crash, a hang,
or a verifier report.

The type universe is closed and addressed by an int code, so a `TypeDecl` is
rebuilt fresh at every use and no node is ever shared between two slots:

| Code | Types |
|---|---|
| basic | `int` `uint` `float` `double` `bool` `string` `int64` `uint64`, the int/float 2-3-4 vectors, `void` |
| `T_ARRAY + c` | `array<c>` |
| `T_TABLE + c` | `table<string; c>` |
| `T_STRUCT + i` | a synthesized structure whose fields are all basic codes |
| `T_PTR + i` | a pointer to one |

What it reaches that random mode cannot: `[[S f0=...]]` make-struct literals and
CMRES returns, field reads, `new S` / `delete p`, `p ?? [[S ...]]` and
`p?.f0 ?? lit`, `t["k"] = v` writes with `t?["k"] ?? lit` reads, `a := b` and
`a <- b`, `invoke($ : T { return v })` closure blocks, string builders, `unsafe`
and `try`/`recover` blocks, and two **generics** (`def f(a) { return a }` and
`def f(a, b) { return a + b }`, both `auto`-argument and `auto`-result, filed via
`add_generic`) that instantiate afresh at every call site.

Function results are real types with a matching `return`, and each function may
call the ones added before it, which keeps the call graph a DAG.

Handled-type annotations come from a fixed list of real types (`HANDLED_TYPES` in
`_ast_synth.das`) rather than being enumerated: `module_find_type_annotation`
`static_cast`s whatever `findAnnotation` returns, so asking it for `export` would
Expand Down Expand Up @@ -93,7 +134,65 @@ A run is `CRASH` only on a definitive abnormal exit (a `CRASH:` banner, signal,
or abort). A **timeout** is reported separately, because it is ambiguous: a real
hang *or* just a slow compile. Re-run with a larger `--timeout` to tell them
apart. Hitting the `--memcap` ceiling is reported as `resource`: unbounded
allocation on a pathological input is not a compiler crash.
allocation on a pathological input is not a compiler crash. A timeout keeps a
`__slow.seed<N>.das` repro, the same way a crash keeps `__crash.seed<N>.das`.

## Mutation of real programs

`--mutate <file>` / `--mutate-dir <dir>` re-infers an existing program with edits
applied to its AST during inference. Every edit is a shape source could also
spell, so a crash from one is a compiler bug rather than a malformed-tree report.

`--mut-kind K` applies one kind at node `--mut-at`; `--mut-count N` applies N
random edits per run. Kinds: 0 op2 operator, 1 int literal, 2 call argument
order, 3 variable name, 4 let type, 5 let type to `auto`, 6 let const, 7 let type
wrapped in `array`, 8 copy to move/clone, 9 statement into unsafe/try/scope, 10
statement into a closure, 11 statement duplicated, 12 argument const, 13 default
value on the last argument, 14 result type to `auto`, 15 let type nested
`--mut-depth` deep, 16 statement nested `--mut-depth` closures deep.

A timeout in this mode is measured against the **wrapper**, not the file. Requiring the
macro module costs about a second in a Release build and about ten in a Debug one,
before any edit is applied, so a bare-file baseline makes every slow victim look like a
compile-time blowup. Time a no-op wrapper compile of the same file first; only the
excess over that is the mutation's.

Two properties keep this honest. `--verify` must report **zero**
verifier-caught runs: one means the mutator is building trees source cannot
produce, not programs. And only the module being compiled can be mutated - a
required module is already inferred when the macro runs, so edits to it are
never re-checked and every run comes back clean.

## Source probes (`probe.das`)

The generator builds trees no source can produce, so its findings are verifier checks by
construction. `probe.das` asks the same question about **ordinary source text**, where a
crash is a compiler bug with a repro anyone can paste:

```
daslang utils/internal/ast-fuzz/probe.das -- --bin bin/daslang --threads 12
```

Three matrices, each a cross product written out as one tiny program per cell:

| matrix | what it varies | expected |
|---|---|---|
| `--mismatch` | 23 declared types x 10 wrong initializers x 30 uses | compiler ERRORS; a crash is a bug |
| `--legal` | 20 legally typed values x the same 30 uses | nothing at all - this is the control |
| `--depth` | nesting to 8192 (parens, unary, `array<>`, `+` chains) | nothing at all |

A compiler error is the *expected* outcome of the mismatch matrix - the probes are
deliberately ill-typed. Only an abnormal exit, a `CRASH:` banner or a timeout is reported,
and each one is written out as `__probes/crash_<name>.das`.

The mismatch matrix is what found the const-folding crash: a local `let` whose declared
type and initializer disagree propagates its constant to the variable's uses, and folding
then reads the literal's bits through the declared type's policy - `SIGSEGV at 0x2` for the
literal `2`, `0x40200000` for `2.5`. That crash is fixed - a mismatched initializer is no longer substituted for the declared
type (`ast_const_folding.cpp`, `tests/language/failed_const_init_type_folding.das`) - so all
three matrices are gated as tests: `test_probe_mismatch_matrix_never_crashes`,
`test_probe_legal_matrix_is_clean`, `test_probe_depth_matrix_is_clean`. The mismatch gate
takes `--sample 5` to stay affordable; drop `--sample` for the full 6900-cell sweep.

## Verifier (`daslib/ast_verify`)

Expand Down
Loading