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
31 changes: 31 additions & 0 deletions .codex/config.toml.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Project-local Codex MCP setup.
# Copy this file to .codex/config.toml from the repository root:
# cp .codex/config.toml.example .codex/config.toml
# The MCP commands and working directory are relative to this checkout.

[mcp_servers.daslang]
command = "python3"
args = ["utils/mcp/mcp_supervisor.py", "--repo-root", "."]
cwd = "."
enabled = true
required = true
startup_timeout_sec = 20
tool_timeout_sec = 120

[mcp_servers.daslang-lsp]
command = "python3"
args = ["utils/lsp/mcp_bridge.py", "--repo-root", "."]
cwd = "."
enabled = true
required = true
startup_timeout_sec = 20
tool_timeout_sec = 120

[mcp_servers.daslang-dap]
command = "python3"
args = ["utils/dap/mcp_bridge.py", "--repo-root", "."]
cwd = "."
enabled = true
required = true
startup_timeout_sec = 20
tool_timeout_sec = 120
12 changes: 9 additions & 3 deletions .github/workflows/extended_checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -473,8 +473,8 @@ jobs:
- name: "Test ser/deser"
if: matrix.role != 'core'
run: |
$BIN/daslang _dasroot_/dastest/dastest.das -- --color --failures-only --test ./tests --ser serialized.bin
$BIN/daslang _dasroot_/dastest/dastest.das -- --color --failures-only --test ./tests --deser serialized.bin
$BIN/daslang _dasroot_/dastest/dastest.das -- --color --failures-only --test ./tests --ser serialized.bin --timeout 1200
$BIN/daslang _dasroot_/dastest/dastest.das -- --color --failures-only --test ./tests --deser serialized.bin --timeout 1200

- name: "Compile tests/ with --ast-verify-batch"
# NIGHTLY ONLY (cron or manual dispatch), because of what it costs: the cost is
Expand Down Expand Up @@ -530,6 +530,13 @@ jobs:
$BIN/daslang _dasroot_/dastest/dastest.das -- --color --failures-only --test ./utils/mcp/test_tools.das
$BIN/daslang _dasroot_/dastest/dastest.das -- --color --failures-only --test ./utils/mcp/test_crosstree_guard.das

- name: "Test DAP MCP bridge (python)"
if: matrix.target == 'linux'
run: |
set -eux
PYTHONDONTWRITEBYTECODE=1 python3 utils/dap/test_mcp_bridge.py
DAS_TEST_STEPPING=1 PYTHONDONTWRITEBYTECODE=1 python3 utils/dap/test_mcp_bridge.py

- name: "Test LSP cross-tree guard (python)"
if: matrix.role != 'modules'
run: |
Expand Down Expand Up @@ -760,4 +767,3 @@ jobs:
run: |
$BIN/daslang dastest/dastest.das -- --cov-path coverage.lcov --color --test ./tests/language --timeout 1800
$BIN/dascov.exe -- coverage.lcov --exclude tests/language

3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ benchdata.db

# build artifacts
web/build_pt/
.codex/
.codex/*
!.codex/config.toml.example
.agents/
build/
build-ninja/
Expand Down
6 changes: 6 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2031,6 +2031,12 @@ install(DIRECTORY ${PROJECT_SOURCE_DIR}/skills/daslang/
)
install(FILES ${PROJECT_SOURCE_DIR}/.claude/agents/dragon.md DESTINATION .claude/agents)

install(FILES
${PROJECT_SOURCE_DIR}/utils/dap/mcp_bridge.py
${PROJECT_SOURCE_DIR}/utils/dap/README.md
DESTINATION utils/dap
)

# Install dascov (code coverage)
install(FILES ${PROJECT_SOURCE_DIR}/utils/dascov/main.das DESTINATION utils/dascov)
install(FILES ${PROJECT_SOURCE_DIR}/utils/dascov/README.md DESTINATION utils/dascov)
Expand Down
26 changes: 26 additions & 0 deletions daslib/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,32 @@ Three companions carry a concern each; a section number is unique across all fou
- **`g_installed_agents` is the GC root for every installed agent** - the C++ adapter holds
a raw classPtr the das GC cannot see.

### 24.1 Debugger readiness query {#debugger-readiness-query}

- **Non-allocation debug-agent callbacks and cross-context invocations share the debug-agent
context mutex** - callbacks can run on debuggee or debugger worker threads, while pinvokes can
arrive on the main thread. Allocation instrumentation callbacks remain direct because they run
inside allocator operations, where re-entering the same allocation path cannot be serialized.
- **The debugger-ready query uses an explicit pinvoke** - generated `apply_in_context`
verification would acquire the agent registry while holding the context mutex, opposite to the
debugger tick's registry-to-context order.

### 24.2 Debugger worker startup {#debugger-worker-startup}

- **The statement-debugger worker executes its lambda only after the source context reaches
`onSimulateContext`** - its cloned context shares the finalized SimNode graph, so executing the
lambda while `Program::simulate` is still hashing that graph is a data race. With
`--das-wait-debugger`, the main thread pumps DAP requests until client configuration completes;
without that flag, source-context simulation proceeds while the worker waits for it to finish.
- **Only one statement-debugger worker may be active** - a second launch fails immediately rather
than blocking the source thread that must deliver the first worker's readiness notification.
Destroying that source context cancels the pending worker so its raw identity cannot outlive it.

### 24.3 Statement breakpoint lookup {#statement-breakpoint-lookup}

- **Statement stepping releases a breakpoint table borrow before entering the stopped command
pump** - requests handled while stopped may replace that table entry.

## 25. typemacro_boost {#typemacro-boost}

- **The parser does not run annotation `apply` for macro-added functions** - the add/erase
Expand Down
101 changes: 66 additions & 35 deletions daslib/debug.das
Original file line number Diff line number Diff line change
Expand Up @@ -832,6 +832,13 @@ struct private DABreakpoint {
typedef DABreakpoints = table<string; array<DABreakpoint>>


def private delete_breakpoint_arrays(var breakpoints : DABreakpoints) {
for (it in values(breakpoints)) {
delete it
}
}


struct private DAContext {
//! Internal context state for the DAP debugger.
id : uint64
Expand Down Expand Up @@ -1151,6 +1158,19 @@ class private DAgent : DapiDebugAgent {
})
}

[arch(at="ARCHITECTURE.md#statement-breakpoint-lookup")]
def findBreakpointId(file : string; line : uint) : uint64 {
for (fileBr, brs in keys(breakpoints), values(breakpoints)) {
if (!compare_path(fileBr, file)) continue
for (br in brs) {
if (br.line == line) return br.id
}
break
}
return -1ul
}

[arch(at="ARCHITECTURE.md#statement-breakpoint-lookup")]
def override onSingleStep(var ctx : Context; at : LineInfo) : void {
//! Handles single-step events, processing pause, step-in, step-over requests, and breakpoint checks.
if (at.fileInfo == null || server == null) return
Expand Down Expand Up @@ -1203,30 +1223,12 @@ class private DAgent : DapiDebugAgent {
ctx |> set_single_step(ctxData.stepInRequested || ctxData.stepRequestedDepth > 0)
return
}
var i = 0
while (i < length(breakpoints)) {
var fileBr : string
for (j, it in range(i + 1), keys(breakpoints)) {
if (j == i) {
fileBr = it
break
}
}
i++
if (!compare_path(fileBr, file)) continue
breakpoints |> get(file) $(brs) {
var brIdx = 0
while (brIdx < length(brs)) {
let br & = unsafe((brs)[brIdx++])
if (br.line != at.line) continue
sendStopped(ctx, ctxData, file, at, "breakpoint", "", br.id)

ctxData.continueRequested = false
wait_for_resume(ctxData)
afterPause()
break
}
}
let breakpointHit = findBreakpointId(file, at.line)
if (breakpointHit != -1ul) {
sendStopped(ctx, ctxData, file, at, "breakpoint", "", breakpointHit)
ctxData.continueRequested = false
wait_for_resume(ctxData)
afterPause()
}
})
}
Expand Down Expand Up @@ -1343,10 +1345,10 @@ class private DAgent : DapiDebugAgent {
}
}

def invalidateBreakpoints(var ctx : Context) : void {
//! Re-instruments breakpoints for a context after code changes, updating their verification state.
def invalidateBreakpoints(var ctx : Context; configuring : bool = false) : void {
//! Instruments configured breakpoints for a context and updates their verification state.
if (!withInstruments
|| server == null || !server.configurationDone
|| server == null || (!server.configurationDone && !configuring)
|| ctx.category.debug_context || ctx.category.debugger_tick) return

for (brs in values(breakpoints)) {
Expand Down Expand Up @@ -1418,6 +1420,11 @@ class private DAgent : DapiDebugAgent {
invalidateBreakpoints(ctx)
}

[arch(at="ARCHITECTURE.md#debugger-worker-startup")]
def override onSimulateContext(var ctx : Context) : void {
debugger_thread_context_ready(ctx)
}

def override onDestroyContext(var ctx : Context) : void {
//! Handles context destruction by unregistering it and updating breakpoint instrumentation.
removeContext(ctx)
Expand Down Expand Up @@ -1662,6 +1669,11 @@ class private DAServer : Server {

def reqConfigurationDone(seq : double; command : string; data : JsonValue?) {
//! Handles the DAP configurationDone request, marking client configuration as complete.
for (ctx in agent.contexts) {
if (ctx.ctx != null) {
agent->invalidateBreakpoints(*ctx.ctx, true)
}
}
configurationDone = true
sendSuccessResponse(seq, command, null)
}
Expand Down Expand Up @@ -1732,9 +1744,7 @@ class private DAServer : Server {
ctx |> reset_debug_flags()
ctx.continueRequested = true
}
for (it in values(agent.breakpoints)) {
delete it
}
delete_breakpoint_arrays(agent.breakpoints)
agent.breakpoints |> clear()
disconnected = true
}
Expand Down Expand Up @@ -2217,12 +2227,33 @@ def private tick_debugger() {

var private g```dAgent : DAgent?

[apply_in_context(name="~debug")]
[arch(at="ARCHITECTURE.md#debugger-readiness-query"), export, pinvoke]
def private query_debugger_ready(var ready : bool?) {
unsafe {
if (g```dAgent == null || g```dAgent.server == null) {
*ready = false
} else {
*ready = !g```dAgent.waitConnection || (g```dAgent.server.configurationDone && g```dAgent.server.threadsDone)
}
}
}


[arch(at="ARCHITECTURE.md#debugger-readiness-query")]
def private debugger_is_ready() : bool {
var ready = false
unsafe {
invoke_in_context(get_debug_agent_context("~debug"), @@query_debugger_ready, addr(ready))
}
return ready
}


[arch(at="ARCHITECTURE.md#debugger-worker-startup")]
def wait_for_debugger() : bool {
//! Blocks until the debug agent is connected and the client configuration is complete.
while (g```dAgent == null || g```dAgent.server == null
|| (g```dAgent.waitConnection && (!g```dAgent.server.configurationDone || !g```dAgent.server.threadsDone))) {
sleep(10u)
//! Blocks until the debug agent reports readiness for the selected connection mode.
while (!debugger_is_ready()) {
tick_debugger()
}
return true
}
Expand Down
9 changes: 8 additions & 1 deletion dastest/dastest.das
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ struct FailedFile {

var private fileTimings : array<FileTiming>
var private failedFiles : array<FailedFile>
var private currentTestFile : string
var private timingOutliers : int = 0
var private maxFileTime : float = 0.0
var private jsonFilePath : string
Expand Down Expand Up @@ -444,6 +445,10 @@ def deserialize_path(var ctx : SuiteCtx, _files : array<string>, in_file : strin
return
}
return if (program._options |> find_arg("rtti") ?as tBool ?? false)
let program_module = program |> get_this_module()
let program_file = program_module != null ? string(program_module.fileName) : "<no module>"
log::info("deser {i + 1:d}/{count:d}: {program_file}")
currentTestFile = program_file
simulate(program) $(sok, context, serrors) {
if (!sok) {
log::error("Failed to simulate program {i}\n{serrors}")
Expand All @@ -460,6 +465,7 @@ def deserialize_path(var ctx : SuiteCtx, _files : array<string>, in_file : strin
}
}
delete_ast_serializer(ser)
log::info("Deserialized run finished: {count:d} programs, {res.total:d} tests, {res.errors:d} error(s)")
}
return res
}
Expand Down Expand Up @@ -590,6 +596,7 @@ def main() : int { // nolint:STYLE037,STYLE038 - CLI dispatch, one arm per mode
for (file in files) {
let uri = ctx.uriPaths ? file_name_to_uri(file) : file
let fileTime = ref_time_ticks()
currentTestFile = file
let status = suite::test_file(file, ctx)
let fileDt = get_time_nsec(fileTime) / 1000
if (status.errors + status.failed == 0) {
Expand Down Expand Up @@ -769,7 +776,7 @@ def main() : int { // nolint:STYLE037,STYLE038 - CLI dispatch, one arm per mode
def timeout_tests(var res : SuiteResult; start_time : int64; timeout_sec : float) {
res.errors++
res.total++
log::error("Test timed out after {timeout_sec}s")
log::error("Test timed out after {timeout_sec}s while running {currentTestFile}")
// timeout fires from a worker thread; fio::exit is the cross-thread
// cancellation mechanism (bypasses shutdown dump — acceptable for this path).
unsafe {
Expand Down
2 changes: 1 addition & 1 deletion doc/reflections/das2rst.das
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,7 @@ def document_module_debugapi(_root : string) {
group_by_regex("Agent lifecycle", mod, %regex~(fork_debug_agent_context|install_debug_agent|install_debug_agent_thread_local|install_new_debug_agent|install_new_thread_local_debug_agent|has_debug_agent_context|get_debug_agent_context|delete_debug_agent_context|is_in_debug_agent_creation|lock_debug_agent)$%%),
group_by_regex("Cross-context invocation", mod, %regex~(invoke_in_context|invoke_debug_agent_method|invoke_debug_agent_function)$%%),
group_by_regex("Agent construction", mod, %regex~(make_debug_agent|make_data_walker|make_stack_walker)$%%),
group_by_regex("Agent tick and state collection", mod, %regex~(tick_debug_agent|collect_debug_agent_state|on_breakpoints_reset|report_context_state|debug_agent_command|debugger_stop_requested)$%%),
group_by_regex("Agent tick and state collection", mod, %regex~(tick_debug_agent|collect_debug_agent_state|on_breakpoints_reset|report_context_state|debug_agent_command|debugger_stop_requested|debugger_thread_context_ready)$%%),
group_by_regex("Instrumentation", mod, %regex~(instrument_node|instrument_function|instrument_all_functions|instrument_all_functions_thread_local|instrument_context_allocations|clear_instruments|set_single_step)$%%),
group_by_regex("Data and stack walking", mod, %regex~(walk_data|walk_stack|stackwalk|get_stackwalk|stack_depth)$%%),
group_by_regex("Context inspection", mod, %regex~(get_context_global_variable|has_function|get_heap_stats)$%%),
Expand Down
37 changes: 37 additions & 0 deletions doc/source/reference/tutorials/45_debug_agents.rst
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,43 @@ debuggers show custom watch variables and application diagnostics:
// output:
// Diagnostics: collection_count = 1

When the built-in DAP debugger stops, it calls
``collect_debug_agent_state`` for the paused context automatically. Each
``category`` reported by an agent becomes an additional DAP scope in the
top stack frame. Each ``name`` becomes a variable in that scope, and
structures, arrays, and other compound values can be expanded with the
normal DAP ``variables`` request. A DAP client should therefore enumerate
*all* scopes returned for a frame instead of assuming that only ``Locals``,
``Arguments``, and ``Globals`` exist.

These state-reporting modules are sometimes called **debugger macros**. A
program opts in by requiring the module; its init code installs a
``DapiDebugAgent``, and categories reported by the module appear as extra
scopes when execution pauses.
The DAP transport needs no module-specific support.

Two modules provide ready-made examples:

================================================ ============================================================
Module Scopes
================================================ ============================================================
``require opengl/opengl_state`` ``OPENGL``, ``OPENGL program``, ``OPENGL arrays``, and others
``require daslib/decs_state`` ``DECS archetype`` and ``DECS requests``
================================================ ============================================================

``opengl/opengl_boost`` already requires ``opengl_state``. The OpenGL
agent reports state only for a paused context marked as an OpenGL context,
where querying the current GL state is valid. ``daslib/decs_boost`` already
requires ``decs_state`` and therefore enables DECS inspection without an
additional ``require``. Code that uses only the lower-level ``opengl`` or
``decs`` module can require the corresponding state module explicitly.

Keep ``onCollect`` bounded and minimize changes to application state. It runs
synchronously while the debuggee is paused, so expensive collection directly
increases debugger latency. A collector may consume diagnostic state when
the underlying API requires it; for example, reading OpenGL errors clears the
error flag, while reading debug messages removes the returned messages.


Agent existence checks
=======================
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 @@ -24,6 +24,7 @@ built-in leak-detection mechanism.
utils/benchctl.rst
utils/aot.rst
utils/mcp.rst
utils/dap.rst
utils/lsp.rst
utils/detect_dupe.rst
utils/find_dupe.rst
Expand Down
Loading
Loading