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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ Full migration table (when reading older docs that say `var inscope` or `<-` for
- `table[key]` (read or assign) is **safe** — do NOT wrap in `unsafe(...)`. Some legacy daslib code has `unsafe(tab[k])`; do not propagate that pattern
- **Move-assign table literal:** `tab <- { "k" => v }` works for both `var tab <- { ... }` declarations and `tab <- { ... }` 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 from `table<K; V>` (the map form); both shapes coexist.
- **`table<T>` (one type param) is the set type** — value type elided. `var s : table<int>; s |> insert(5); key_exists(s, 5)`. Distinct from `table<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 declaring `var X : table<T>` and populating in an `[init]` function.

### Iterators and `each`

Expand Down
9 changes: 9 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1573,6 +1573,15 @@ install(DIRECTORY ${PROJECT_SOURCE_DIR}/utils/mouse/tests/
FILES_MATCHING PATTERN "*.das" PATTERN "*.md"
)

# Install utils/common (git-aware staleness signature shared between
# utils/mcp/tools/cpp_common and utils/mouse/index).
file(GLOB DAS_UTILS_COMMON_FILES ${PROJECT_SOURCE_DIR}/utils/common/*.das)
install(FILES ${DAS_UTILS_COMMON_FILES} DESTINATION utils/common)
install(DIRECTORY ${PROJECT_SOURCE_DIR}/utils/common/tests/
DESTINATION utils/common/tests
FILES_MATCHING PATTERN "*.das"
)

# Install daspkg (package manager)
file(GLOB DAS_DASPKG_FILES ${PROJECT_SOURCE_DIR}/utils/daspkg/*.das)
install(FILES ${DAS_DASPKG_FILES} DESTINATION utils/daspkg)
Expand Down
23 changes: 23 additions & 0 deletions daslib/fio.das
Original file line number Diff line number Diff line change
Expand Up @@ -704,3 +704,26 @@ def rmdir_rec_result(path : string) : fs_result_bool {
}
return fs_result_bool(value = res)
}

def run_and_capture(args : array<string>; var output : string&; timeout_sec : float = 0.0) : int {
//! Run an external command and capture its stdout+stderr (merged into one
//! pipe by the underlying ``popen_argv``). Returns the process exit code;
//! ``output`` is filled with whatever the child wrote to either stream.
//!
//! ``args[0]`` is the executable; remaining elements are positional arguments.
//! ``timeout_sec > 0`` kills the process tree after that many seconds (returns
//! ``popen_timed_out``); ``timeout_sec <= 0`` means no timeout.
//!
//! Argv-based: bypasses the shell entirely (Windows: CreateProcess; Unix:
//! fork+execvp). No ``cmd.exe`` quote-stripping, no ``/bin/sh`` ``$()``/backtick
//! expansion — every argv element reaches the child verbatim. Callers don't
//! (and shouldn't) quote arguments themselves.
var captured : string
let exit_code = unsafe(popen_argv(args, timeout_sec, $(f) {
if (f != null) {
captured := unsafe(fread_to_eof(f))
}
}))
output := captured
return exit_code
}
23 changes: 10 additions & 13 deletions daslib/json_boost.das
Original file line number Diff line number Diff line change
Expand Up @@ -155,20 +155,11 @@ def operator ?. value(var a : JsonValue? ==const) : JsValue? {
[macro_function]
def private is_json_ptr_value(td : TypeDeclPtr) {
//! Checks if the type is a pointer to json::JsonValue
if (td.baseType != Type.tPointer) {
return false
}
if (td.firstType == null) {
return false
}
if (td.firstType.baseType != Type.tStructure) {
if (td.baseType != Type.tPointer || td.firstType == null || td.firstType.baseType != Type.tStructure) {
return false
}
let st = td.firstType.structType
if (st.name != "JsonValue" && st._module.name != "json") {
return false
}
return true
return st.name == "JsonValue" && st._module.name == "json"
}


Expand Down Expand Up @@ -438,7 +429,7 @@ def public parse_json_annotation(name : string; annotation : array<tuple<name :
if (ann.name == "rename") {
if (ann.data is tString) {
fieldState.argName = ann.data as tString
} elif (ann.data is tBool && length(name) > 0 && first_character(name) == '_') {
} elif (ann.data is tBool && !empty(name) && first_character(name) == '_') {
fieldState.argName = slice(name, 1)
}
} elif (ann.name == "enum_as_int" && ann.data is tBool) {
Expand Down Expand Up @@ -602,7 +593,13 @@ def JV(value : auto(TT)) : JsonValue? {
if (field == null) { return ; }
} else {
static_if (typeinfo is_workhorse(field)) {
if (field == default<typedecl(field)>) { return ; }
// bool path uses `!field` (avoids STYLE018 on `bool == false` after
// generic instantiation); numeric workhorses keep the generic-zero compare.
static_if (typeinfo stripped_typename(field) == "bool") {
if (!field) { return ; }
} else {
if (field == default<typedecl(field)>) { return ; }
}
}
}
}
Expand Down
31 changes: 31 additions & 0 deletions daslib/strings_boost.das
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,37 @@ def levenshtein_distance_fast(s, t : string implicit) : int {
return v0[tLen]
}

def public jaccard(a : table<string>; b : table<string>) : float {
//! Jaccard similarity over two string-sets: ``|intersection| / |union|`` in 0..1.
//! Empty either side returns 0.0. Use ``table<string>`` (the set form) so
//! the intersect lookup is O(1).
if (empty(a) || empty(b)) {
return 0.0f
}
var intersect = 0
for (k in keys(a)) {
if (key_exists(b, k)) {
intersect ++
}
}
let unionSize = length(a) + length(b) - intersect
return float(intersect) / float(unionSize)
}

def public jaccard(a, b : array<string>) : float {
//! Jaccard similarity over two string arrays. Builds two ``table<string>``
//! sets and delegates — convenient when callers don't already have sets.
var sa : table<string>
for (x in a) {
sa |> insert(x)
}
var sb : table<string>
for (x in b) {
sb |> insert(x)
}
return jaccard(sa, sb)
}

def replace_multiple(source : string; replaces : array<tuple<text : string; replacement : string>>) {
//! replaces occurances of multiple strings in a string. does not support overlap
if (empty(source) || empty(replaces)) {
Expand Down
4 changes: 2 additions & 2 deletions doc/reflections/das2rst.das
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ def document_module_fio(root : string) {
group_by_regex("Directory manipulation", mod, %regex~(dir|dir_rec|mkdir|mkdir_rec|mkdir_result|rmdir|rmdir_rec|rmdir_rec_result|rmdir_result|chdir|getcwd)$%%),
group_by_regex("Glob and pattern matching", mod, %regex~(match_glob|glob|glob_filtered|is_glob_pattern|expand_glob|parse_file_list)$%%),
group_by_regex("Filesystem queries", mod, %regex~(temp_directory|temp_directory_result|create_temp_file|create_temp_file_result|create_temp_directory|create_temp_directory_result|disk_space)$%%),
group_by_regex("OS specific routines", mod, %regex~(sleep|exit|system|popen|popen_binary|popen_timeout|popen_argv|popen_timed_out|get_env_variable|sanitize_command_line|has_env_variable)$%%),
group_by_regex("OS specific routines", mod, %regex~(sleep|exit|system|popen|popen_binary|popen_timeout|popen_argv|popen_timed_out|run_and_capture|get_env_variable|sanitize_command_line|has_env_variable)$%%),
group_by_regex("Dynamic modules", mod, %regex~(register_dynamic_module|register_native_path)$%%)
)
documents("File input output library", mod, "fio.rst", groups)
Expand Down Expand Up @@ -376,7 +376,7 @@ def document_module_strings_boost(root : string) {
group_by_regex("Search and match", mod, %regex~(last_index_of|glob_match|text_match)$%%),
group_by_regex("Replace", mod, %regex~(replace_multiple)$%%),
group_by_regex("Prefix and suffix", mod, %regex~(trim_prefix|trim_suffix)$%%),
group_by_regex("Levenshtein distance", mod, %regex~(levenshtein_distance|levenshtein_distance_fast)$%%),
group_by_regex("String similarity", mod, %regex~(levenshtein_distance|levenshtein_distance_fast|jaccard)$%%),
group_by_regex("Character traits", mod, %regex~(is_hex|is_tab_or_space)$%%))
document("Boost package for string manipulation library", mod, "strings_boost.rst", groups)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Jaccard similarity over two string-sets, returning ``|intersection| / |union|`` in 0..1. Empty either side returns 0. Pass two ``table<string>`` (the set form) for O(1) intersect lookup, or two ``array<string>`` and the array overload will build the sets internally.
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
slug: das2rst-emits-a-stub-for-my-new-public-daslib-function-even-though-i-added-a-doc-comment-what-s-the-right-fix
title: das2rst emits a // stub for my new public daslib function even though I added a //! doc-comment — what's the right fix?
created: 2026-05-09
last_verified: 2026-05-09
links: []
---

Two things are going on; the `documentation_rst.md` skill rolls them together as "add `//!` instead of filling the stub" but the real story has a wrinkle.

**1. `//!` placement — must be INSIDE the function body, not before `def`.**
For pure-daslang functions in `daslib/*.das`, `rst_comment.das` extracts `//!` comments only when they appear as the first line(s) of the function body:

```daslang
def public foo(x : int) : int {
//! Docs go HERE — first lines of the body.
//! Multi-line continues like this.
return x + 1
}
```

`//!` placed *before* `def` is silently ignored. Symptom: regen still produces a `// stub` placeholder under `doc/source/stdlib/handmade/function-<module>-<name>-<hash>.rst`. The fix is to move the doc-comment inside the body and re-run `das2rst`.

**2. Some daslib modules expect BOTH a `//!` body comment AND a per-symbol `handmade/*.rst`.**
Modules like `strings_boost`, `fio`, and other long-established ones have a per-symbol `handmade/*.rst` for *every* function — see e.g. `function-strings_boost-levenshtein_distance-0xbb5a4a3017b240a5.rst`. When you add a new public function to one of those modules, `das2rst` will emit a fresh `// stub` even with correctly-placed `//!`. The convention there is: keep the `//!` (it lands in `detail/`) **and** fill the stub with a 1-2 sentence handmade description. Don't delete the stub — re-running `das2rst` recreates it.

Newer modules like `archive`, `json_boost`, `command_line` don't have per-symbol handmade entries; for those, `//!` alone is enough.

**How to apply:**
- New daslib public function → add `//!` inside body first.
- Run `bin/daslang doc/reflections/das2rst.das`.
- `grep -rln "// stub" doc/source/stdlib/handmade/` — if your function appears, fill that file with a short description (plain text, no RST directives). If your function doesn't appear, you're done.
- Re-run `das2rst` to confirm clean.
- Verify `grep -c Uncategorized doc/source/stdlib/generated/*.rst | grep -v ':0$'` is empty (means the function is in a `group_by_regex` group in `das2rst.das`).

## Questions
- das2rst emits a // stub for my new public daslib function even though I added a //! doc-comment — what's the right fix?
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
slug: how-do-i-capture-a-function-param-var-state-t-struct-ref-by-reference-into-a-daslang-lambda
title: How do I capture a function param (var state : T struct ref) by reference into a daslang lambda so the lambda can mutate the underlying module global from outside the function?
created: 2026-05-09
last_verified: 2026-05-09
links: []
---

## Pattern

The capture clause goes BEFORE `<|`, the `&` marks each ref-captured name, and the WHOLE move into the storage requires `unsafe`:

```das
def register_widget(var state : State; ident : string) {
unsafe {
var d <- @ capture(& state) (payload : int) {
state.pending = true
state.value = payload
}
g_dispatchers[ident] <- d
}
}
```

## Three syntax landmines

1. **`@ <| capture(& x) (args) { ... }` is a parse error.** Capture clause attaches to `@`, not to `<|`. Correct form: `@ capture(& x) (args) { ... }` — drop the `<|`.
2. **`g_dispatchers[ident] <- @capture(...) (args) {...}` errors as `error[30941]: can't move from a constant value`.** The lambda literal is const-typed; you can't move directly into a table-indexing lvalue. Move into a temp `var d <- ...` first, then `<- d` into the table.
3. **`error[31003]: capture by reference requires unsafe`.** Wrap the move-and-store in `unsafe { ... }`. Capture-by-reference is unsafe because the lambda outlives the function frame; daslang requires you to opt in.

## Why this works for module-global state

When the caller passes a module global (`register_widget(G, "G")`), the function param `var state : State` is a stable ref to G. `capture(& state)` smuggles that ref into the closure. Since G lives forever (module-global), the captured ref stays valid for the lambda's lifetime — invoking the lambda from anywhere mutates G in place.

Verified end-to-end (2026-05-09, daslang Opus 4.7 session): module-global state mutation through a lambda stored in a module-global `table<string; lambda<(int) : void>>`, invoked from outside the registering function.

## Source

Canonical patterns in `dastest/dastest.das:306` (`new_thread <| @capture(& res, & mainCtx) {...}`) and `dastest/suite.das:451-475` (multiple `testing.onFail <- @ capture(& testing, & failed, ...)` shapes — note `<-` works for struct-field assignment without unsafe; only table-indexing assignment needs the `unsafe` wrapper).

## Questions
- How do I capture a function param (var state : T struct ref) by reference into a daslang lambda so the lambda can mutate the underlying module global from outside the function?
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
---
slug: how-do-i-make-an-mcp-tool-spawn-a-fresh-daslang-subprocess-so-each-call-gets-clean-macro-state
title: How do I make an MCP tool spawn a fresh daslang subprocess so each call gets a clean macro state, and what is the cold-start cost?
created: 2026-05-09
last_verified: 2026-05-09
links: []
---

When an MCP tool runs `compile_file()` on user code that registers `[function_macro]` / `[call_macro]` annotations, the C++-side Annotation pointers persist for the rest of the daslang process's lifetime. In a long-lived MCP server, this means **subsequent edits to the macro source are invisible** until the server restarts. Symptoms: identical input that worked an hour ago now hits a stale macro version; restarting the MCP fixes it.

The fix is to make each MCP tool that compiles user code shell out to a fresh `daslang.exe` subprocess. Macro state is process-local, so every call starts clean.

## Pattern

`utils/mcp/tools/<tool>.das` becomes a thin wrapper:

```das
options gen2
require common public

def do_compile_check(file : string; project : string = ""; json : bool = false) : string {
return run_mcp_subtool("compile_check", [file, project, json ? "true" : "false"])
}
```

The real logic lives at `utils/mcp/subtools/<tool>.das`:

```das
options gen2
require ../tools/common.das public
// ... real work ...

[export]
def main {
let raw <- get_command_line_arguments()
let args <- subtool_user_args(raw)
if (length(args) < 3) {
print(make_tool_result("compile_check subtool: expected 3 args, got {length(args)}", true))
return
}
let file = string(args[0])
let project = string(args[1])
let json = string(args[2]) == "true"
print(run_compile_check(file, project, json))
}
```

`run_mcp_subtool` (in `utils/mcp/tools/common.das`) handles the popen, the `--` separator, timeout, and exit-code translation:

```das
def run_mcp_subtool(subtool_name : string; args : array<string>; timeout_sec : float = 120.0) : string {
let exe = get_daslang_exe()
let subtool_path = path_join(get_das_root(), "utils/mcp/subtools/{subtool_name}.das")
var argv <- [exe, subtool_path, "--"] // `--` is critical
argv |> reserve(length(argv) + length(args))
for (a in args) { argv |> push(a) }
var output : string
let exit_code = run_and_capture(argv, output, timeout_sec)
if (exit_code == popen_timed_out) {
return make_tool_result("MCP subtool '{subtool_name}' timed out after {timeout_sec}s:\n{output}", true)
}
if (exit_code != 0) {
return make_tool_result("MCP subtool '{subtool_name}' failed (exit {exit_code}):\n{output}", true)
}
return output
}
```

## The `--` separator gotcha

Without `--`, daslang treats positional argv past the script path as additional `.das` files to load AND auto-runs each. With `--`, daslang stops parsing its own options and exposes the rest via `get_command_line_arguments()`. The subtool then uses `subtool_user_args(raw)` (also in `tools/common.das`) to skip past the interpreter+script-path prefix and the `--` to get just the user args.

## Cold-start cost

Every subprocess pays the daslang.exe boot + subtool-script compile + require-chain compile cost. Measured on Windows (Release):

| Subtool | Fast-fail wall time |
|---|---|
| `daslang.exe --version` | ~35 ms |
| `compile_check.das` / `find_symbol.das` / `list_module_api.das` | ~0.7 s |
| `aot.das` | ~1.9 s (extra ~1.2 s for `daslib/aot_cpp`) |

So per-MCP-call overhead is 0.7–1.9 s before the actual work. For a 270-test MCP test suite, totalling ~89 s. Acceptable for offline runs. The dasImgui PR #2620 has the full benchmark + the deferred-speedup backlog.

## When to apply

- Any MCP tool that calls `compile_file()` on user code (i.e., the user's macros may register).
- Any tool that walks RTTI of compiled programs (the same `compile_file` underlies it).

When NOT to apply:
- Tools that only walk the C++ AST index (`cpp_*` family) — no daslang macro state involved.
- `live_*` tools (talk to live HTTP host).
- Tools that already shell out to external processes (run_script, run_test, format_file, ast-grep wrappers).

Verified 2026-05-09 in PR #2620.

## Questions
- How do I make an MCP tool spawn a fresh daslang subprocess so each call gets a clean macro state, and what is the cold-start cost?
Loading
Loading