Skip to content
Draft
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -199,3 +199,6 @@ modules/dasLLAMA/benchmarks/asr/_pybench_rows.txt
# python bytecode, anywhere
__pycache__/
site/files/profile_results_*.json

# examples/c_api_library builds its three libraries here
examples/c_api_library/_out/
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ Task-specific instructions are split into skill files under `skills/`. You MUST
| `skills/internal/documentation_rst.md` | Editing RST in `doc/source/`, `//!` doc-comments in `daslib/*.das`, tutorial RST pages |
| `skills/internal/tutorials.md` | Anything that looks like a tutorial - they live under `/tutorials/<area>/`, NEVER `modules/<X>/tutorial/` |
| `skills/internal/tutorial_prose.md` | WRITING or revising general-reader doc/tutorial prose (`documentation_rst.md` is mechanics, this is the words) |
| `skills/cpp_integration.md` | Embedding daslang in C++; binding types/functions/enums; shipping without the compiler (`libDaScriptNano`, or a standalone context on the full runtime) |
| `skills/cpp_integration.md` | Embedding daslang in C++; binding types/functions/enums; shipping without the compiler (`libDaScriptNano`, or a standalone context on the full runtime); calling daslang from C (`daslang -lib`) |
| `skills/internal/cpp_codebase_notes.md` | Working on daslang's own C++ - where inference/builtins/errors/parser live, AST function flags |
| `skills/internal/clang_bind_build.md` | Enabling `dasClangBind` / bumping the libclang SDK / running any `bind_*.das` self-binder |
| `skills/daslib_modules.md` | Working with `daslib/` modules or extending the stdlib |
Expand Down
3 changes: 2 additions & 1 deletion daslib/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
Design rationale a maintainer cannot recover from the code alone. One numbered section per
module; entries are anchored to symbols.

Three companions carry a concern each; a section number is unique across all four files.
Four companions carry a concern each; a section number is unique across all five files.

- `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_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.

## 8. ast_verify
Expand Down
68 changes: 68 additions & 0 deletions daslib/ARCHITECTURE_CAPI.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# daslib architecture notes - the generated C API

Companion to `ARCHITECTURE.md` in this folder; section numbers are unique across the family.
The header this section describes is the one BOTH backends write: `aot_standalone` for a
standalone context, `inject_lib` for `daslang -lib`.

## 30. c_api_header

- **One module writes every generated header, and the C API is the only calling path.** The C
declarations come first; the C++ half (`CppApi`) is appended under `#ifdef __cplusplus`, each of
its functions an `inline` call to the C entry point beside it - so both hosts include one file,
neither sees the other's half, one implementation stays correct, and no `aot_cpp` is needed: a
proxy only spells a C-representable signature, whose C++ form is mechanical. `aot_standalone`
emits the matching `extern "C"` bodies and `inject_lib` the matching thunks, both off this
describer, so JIT and AOT cannot disagree about what crosses or what it is called.
- **`Function.flags.exports` is the whole selection truth.** `[export_c]`
(`ExportCAnnotation`, `daslib/export_c.das` - a das `[function_macro]`, so the C surface is
decided entirely in daslang) sets it, and
`policies.export_public_functions` sets it for every public entry-module function under
`-lib-export-all` (`MarkSymbolUse::exportPublicFunctions`, `src/ast/ast_export.cpp`). The
selection forms are therefore one bit read several ways: `-lib` alone accepts only what carries
the annotation, `--jit-lib-export-marked` and a standalone context accept the bit however it was
set (so `[export]` selects), and `-lib-export-all` is that same acceptance plus the marking
policy. Accepting the bit however set is what licenses skipping an unspellable signature with a
warning; one that ASKED for C with `[export_c]` and cannot cross is an error. Whether a signature
CAN cross is decided here, after infer, because argument types do not exist at annotation time.
- **`[export_c]` reaches a library source with no `require` because `daslib/export_c` is
`!inscope`.** That marker sets `visibleEverywhere`, which `Module::isVisibleDirectly` honors
ahead of the require map - the mechanism that makes `daslib/builtin.das` universal. Being visible
still needs the module LOADED, and `daslib/just_in_time.das` - injected whenever the JIT is on -
requires it, so `-lib`, `-jit` and `-exe` carry the annotation for free; a compile with no JIT
needs `require daslib/export_c`. It lives in its own module rather than in `c_api_header` because
that require costs `ast_boost` alone, not the header emitter's whole graph.
- **Refusal is per stage, not per module**: `collect_c_exports` returns its rejections and logs
its skips. An `[export_c]` that cannot cross comes back for the caller to report - `macro_error`
during compilation, the jit error log during codegen - so this module needs no `ProgramPtr` and
no reporting policy of its own.
- **The scalar widths and the vector layouts are C++-side facts this emitter mirrors.**
`bool` is one byte (the `static_assert(sizeof(bool)==1)` in `getTypeBaseSize`,
`src/simulate/debug_info.cpp`), so das `bool` meets C as `bool`. `float3` is `{x, y, z}` at
12 bytes and 4-byte alignment, because `vec3<TT>` (`include/daScript/misc/vectypes.h`)
is a plain three-field struct with no `alignas` - the 16-byte vec4f shape is the JIT's
register ABI, not the memory layout a header has to mirror. So no vector or structure carries
an alignment attribute, and every declared struct carries a size assert plus one offset assert
per field. `Structure.sizeOf` is already rounded to the struct's alignment
(`Structure::getSizeOf`, `src/ast/ast.cpp`), so `sizeof` in C matches it directly.
- **A bound value type crosses as the wrap type its annotation carries.** A
`ManagedValueAnnotation` is not a ref type and holds `makeValueType()` - the `WrapType<OT>::type`
das moves the value through, reachable from das as `get_underlying_value_type`. The header
typedefs that shape under the das type's name and the body assigns the das type through it; a C++
host still gets the real type, because the C++ half carries the module's `aotRequire` include. It
is the one C type with an alignment attribute - a handle's alignment is not its wrap type's
(`BigEntityId` is 16-aligned, four floats are 4-aligned) - on ONE declarator, since the attribute
applies per declarator. Size and alignment are both asserted. A ref-type handle stays `void *`.
- **An enumeration is a typedef of its base integer plus loose enumerators, never a C
`enum`** - a C enum's underlying type is implementation-defined, which would break the
size assert on the 8/16/64-bit bases and on negative values. The values are read off the
entry's folded constant, so no smart pointer is needed to reach `find_enum_value`.
- **Types are emitted only when a signature reaches them, in post-order.** A by-value
field's structure is defined before the structure holding it; every structure also gets a
forward typedef ahead of all definitions, which is what lets a self-referential
(`Node?`) field compile. A pointer's target only has to be NAMEABLE, so one whose fields C
cannot spell is forward-declared and never DEFINED - defining it emits fields with no type at
all - while a representable target is defined, so a host can read through the pointer. A cycle
terminates either way, and a pointer to something C cannot even name degrades to `void *`
rather than refusing the function.
- **A `fixed_array` argument crosses (as `const T *`, which the das ABI already passes) but a
`fixed_array` result does not** - that is a CMRES of an array, a pointer nothing in C sizes.
22 changes: 22 additions & 0 deletions daslib/ARCHITECTURE_EMIT.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,30 @@ Companion to `ARCHITECTURE.md` in this folder; section numbers are unique across
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
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
`describeCppType` resolves those types through `g_aot_main_module_name`. Where one emitter writes
both - `das_safe_navigation<T, F, &T::field>` - they must agree, or the type argument names the
context's namespace while the member pointer names nothing. Only a standalone context sets a
main-module name, so regular AOT never sees it.

## 6. aot_standalone

- **The entry module's structures are visited SORTED.** `visitModule` takes `sortStructures`,
which runs `topoSortStructures` so a by-value field's structure is complete before the structure
holding it; regular AOT passes it through `visit(program, adapter, true)` and a standalone
context, which visits the entry module by itself, has to ask for it too. Declaration order is the
author's, and nothing else re-derives it.
- **The context name is an identifier; the file stem is not the same string.** A stem reaches C++
as a namespace and C as a symbol prefix, so `while.das` or `3d-math.das` would open
`namespace while {`. `context_name` is the stem through `cpp_context_ident`, while `file_stem`
keeps the raw name - a build system predicts the generated file names from the input path and
cannot be told they were sanitized.
- **The generated constructor IS the init protocol** - a standalone context never calls
`Context::runInitScript`, so the ctor reproduces its observable semantics inline:
`memset(context.globals, 0, context.getGlobalSize())` mirrors runInitScript's globals
Expand Down
5 changes: 3 additions & 2 deletions daslib/aot_cpp.das
Original file line number Diff line number Diff line change
Expand Up @@ -854,6 +854,7 @@ class public AotDebugInfoHelper {
def str() {
return build_string() $(var writer) {
verify(info2Name.empty() && info2TypeName.empty())
annInfoNames |> clear()
helper |> debug_helper_iter_structs($(_name, ti) {
write(writer, "extern StructInfo {structInfoName(ti)};\n");
});
Expand Down Expand Up @@ -896,7 +897,6 @@ class public AotDebugInfoHelper {
write(writer, "\}\n\n")
info2Name.clear();
info2TypeName.clear();
annInfoNames |> clear()
}
}

Expand Down Expand Up @@ -2479,7 +2479,8 @@ class public CppAot : AstVisitor {
}
write(*ss, ", {vtype.get_variant_field_offset(field.fieldIndex)}, {field.fieldIndex}>::get(");
} else {
let mod_name = (vtype.structType._module.name.empty() ? "" : string(vtype.structType._module.name) + "::");
let ns = aotModuleName(vtype.structType._module);
let mod_name = (ns |> empty() ? "" : "{ns}::");
write(*ss, ",&{mod_name}{aotStructName(vtype.structType)}::{aotFieldName(string(field.name))}>::get(");
}
}
Expand Down
Loading