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
17 changes: 13 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,10 +292,19 @@ Full tool table (including `detect_duplicates`/`judge_duplicates`/`find_dupe`),

## MOUSE FIRST (hard rule)

`mouse__ask` is the **first** tool on any "how do I X?" / "what's the right pattern for Y?" / "why does Z behave this way?" question — before Explore, before Grep, before Read, before any agent.
**The trigger:** the moment you are about to call `Grep`, `Read`, `Explore`, or launch any agent to answer a "how do I X?" / "what's the right pattern for Y?" / "why does Z behave this way?" question. **Stop. Call `mouse__ask` first.** This is not a courtesy and not a fallback — `mouse__ask` is the *first* research tool. Every time.

If you catch yourself mid-research thinking "I should have asked mouse first" — stop, ask now. If the cache had the answer, you just saved minutes; if it didn't, finish the research and `mouse__add` the answer before moving on. Either way the cache gets a little smarter.
If you find yourself reaching for a research tool with a how/why/pattern-shaped question in mind and you haven't asked mouse, you are violating the rule. The trigger fires every time, not just the first time in a session.

Cache misses you skipped never show up in `mouse log --misses` — the only safety net is the wrap-up curation pass. After every meaningful chunk of work, run `skills/task_wrap_up.md` to sweep both the recorded misses and the un-asked questions you researched the long way.
**Reject these rationalizations** — they are exactly the failure modes that have demoted past sessions:

The cost of an extra `mouse__ask` is ~50ms; the cost of redoing research that's already cached is minutes. The asymmetry is the whole point. Treat this as a hard rule, not a hint — a session that researches without asking mouse first is a session leaking time.
- *"I know this codebase, I'll just grep."* — Your model is stale; the cache is current. If mouse has the answer, you should read THAT, not your own re-derivation.
- *"It's a small/quick question, mouse is overkill."* — 50ms ask vs minutes of grep-then-re-derive when the cache had the answer. The asymmetry is the whole point of the rule. Small questions are exactly when the cost of asking is cheapest.
- *"I'll mouse__ask if grep doesn't find it."* — Backwards. Mouse short-circuits grep; it doesn't backstop it. By the time grep fails, you've already paid the cost the rule was designed to avoid.
- *"The mouse MCP just disconnected, I'll skip this round."* — When the MCP reconnects (the system reminder will tell you), re-anchor immediately. Your *next* how/why moment is a `mouse__ask`, not a free pass.

**Mid-stream recovery is non-negotiable.** Two consecutive `Grep` / `Read` / `Glob` / `Agent` calls on the same topic without a `mouse__ask` between them = warning sign. **Stop. Ask mouse now.** Don't promise yourself you'll do it "after this one more grep." The longer you research without asking, the harder sunk-cost makes it to ask.

**Cache-miss discipline.** If `mouse__ask` returns nothing useful, immediately call `mouse__bad` with the `query_id` from the response (signals: BM25 matched on tokens, no real answer in corpus). Finish the research the long way. Then `mouse__add` the answer you found before moving on to the next task. Misses you skip never show up in `mouse log --misses`; the wrap-up curation pass (`skills/task_wrap_up.md`) is the only safety net, and it only fires if you run it.

A session that does research without `mouse__ask` is leaking time *and* losing the chance to make the cache smarter for the next session. Treat this as a load-bearing constraint on every tool call, not a hint to remember at session start.
7 changes: 6 additions & 1 deletion daslib/json_boost.das
Original file line number Diff line number Diff line change
Expand Up @@ -546,7 +546,12 @@ def JV(value : auto(TT)) : JsonValue? {
return _::JV(arr)
}
static_if (typeinfo typename(value) == "json::JsonValue?" || typeinfo typename(value) == "json::JsonValue? const") {
return value
// Pass-through: JsonValue? IS a JsonValue?. Const-strip the pointer
// so callers walking const containers (apply on a const struct/tuple
// hands fields as const) round-trip cleanly.
unsafe {
return reinterpret<JsonValue?>(value)
}
} static_elif (typeinfo is_pointer(value)) {
return value == null ? default<JsonValue?> : _::JV(*value)
} static_elif (typeinfo is_enum(value)) {
Expand Down
2 changes: 1 addition & 1 deletion include/daScript/simulate/simulate_nodes.h
Original file line number Diff line number Diff line change
Expand Up @@ -2551,7 +2551,7 @@ SIM_NODE_AT_VECTOR(Float, float)
} else {
res = (char *) ptr->iter;
if ( !res ) {
context.throw_error_at(debugInfo,"iterator is empty or already consumed%s", errorMessage);
context.throw_error_at(debugInfo,"iterator is empty or already consumed by a prior for-in (use `next(it, value)` + `empty(it)` for step-by-step advance)%s", errorMessage);
} else {
ptr->iter = nullptr;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---
slug: how-do-i-build-a-jsonvalue-object-inline-without-declaring-a-struct-or-hand-writing-a-table-i-e-what-s-the-laziest-jv-form-for-a
title: How do I build a JsonValue object inline without declaring a struct or hand-writing a table — i.e. what's the laziest JV form for a one-off object?
created: 2026-05-11
last_verified: 2026-05-11
links: []
---

## Use the named-tuple form

```das
return JV((kind = meta.kind, rendered = false, payload = invoke(meta.serializer)))
// → {"kind": "...", "rendered": false, "payload": {...}}
```

`(key = value, ...)` is a named-tuple constructor in daslang gen2 — produces a `tuple<key:T; ...>`. `JV(auto)` in `daslib/json_boost` has an `is_tuple` branch (around line 633) that walks the named fields via `apply()` and emits a JSON object using each name as the key. One expression, no `insert` ceremony, no intermediate `table<string; JsonValue?>`.

## What it replaces

```das
// Before — 4 lines + intermediate table
var tab : table<string; JsonValue?>
tab |> insert("kind", JV(meta.kind))
tab |> insert("rendered", JV(false))
tab |> insert("payload", invoke(meta.serializer))
return JV(tab)

// After — one expression
return JV((kind = meta.kind, rendered = false, payload = invoke(meta.serializer)))
```

## Limits — when to fall back

- **No `@optional` / `@rename` / `@embed`** — field annotations only attach to real struct fields, not named-tuple fields. Every key is unconditionally emitted; the JSON key always matches the daslang name. If you need conditional skip-when-zero, declare a small struct with `@optional` instead.
- **All keys are static**. If the keys themselves vary at runtime (mixed-type maps, schema-by-runtime), fall back to manual `table<string; JsonValue?>`.

## Const-pass-through gotcha (fixed 2026-05-10)

If your named tuple contains a `JsonValue?` field (e.g. `payload = invoke(serializer)`), `apply()` hands it to the recursive `JV(field)` as `JsonValue? const`. Pre-fix, `JV(JsonValue? const)` errored with `error[30343]: can't copy constant to non-constant pointer` because the typename pass-through at `daslib/json_boost.das:548` did `return value` without const-stripping.

Fixed in PR #2626: the pass-through now const-strips via `unsafe { return reinterpret<JsonValue?>(value) }`. Safe because `JsonValue?` is a pointer; the cast is a no-op at runtime.

If you hit that error on an older daslang, the workaround is the manual table form — until you can rebuild against post-PR-#2626 daslib.

## Tutorial / source pointers

- Skill: [skills/json.md](https://github.com/GaijinEntertainment/daScript/blob/master/skills/json.md) — "Inline named-tuple JV" section
- Implementation: [daslib/json_boost.das](https://github.com/GaijinEntertainment/daScript/blob/master/daslib/json_boost.das) — `is_tuple` branch in `def JV(value : auto(TT))`

## Questions
- How do I build a JsonValue object inline without declaring a struct or hand-writing a table — i.e. what's the laziest JV form for a one-off object?
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
slug: how-do-i-test-that-a-function-panics-in-daslang-pytest-raises-equivalent
title: How do I test that a function panics in daslang? (pytest.raises equivalent)
created: 2026-05-11
last_verified: 2026-05-11
links: []
---

**Short answer: you don't.** daslang's `try { ... } recover { ... }` is for catching **panic**, not for exception-handling-style test assertions. It's banned for testing-panic-UX use.

**Why:**
- `panic` in daslang is "bad things just happened" — runtime equivalent of a C++ exception. It carries exception-unwinding cost, AOT footprint, perf weight. Using `try/recover` to verify "this helper panics on bad input" misuses the panic machinery.
- `recover` does **NOT** expose the panic message anyway — you can only detect that *some* panic fired, not what it said. So even the "did this raise" form has poor fidelity (no message-shape assertion).
- A lint rule is incoming that will warn on every `try/recover` use; suppression will require an explicit annotation. Don't add new sites.

**What to do instead:**
1. **For helpers that intentionally panic on timeout / bad input** (e.g. `expect_value` / `expect_render` / `await_quiescent` in dasImgui's playwright module): you don't unit-test the panic path. The happy-path test that DEPENDS on the helper failing-loudly will surface regressions — if the helper stops panicking, downstream assertions will see stale state and fail anyway.
2. **For functions where soft-failure IS a legitimate API**: design a non-panicking variant (`try_foo : Option<T>` or `try_foo : Result<T; E>`) and have the panic-throwing wrapper call it. Then unit-test the `Result` variant with normal `t |> equal(...)` calls. Match the existing convention: `try_*` for fallible primitive, `*_or` for unwrap-with-default sugar, plain `foo` for strict.
3. **Negative-path coverage** for a panic-on-bad-input helper becomes: a developer running dastest can VISUALLY confirm the panic message format on any genuine failure. dastest prints the panic + stack as part of the FAIL line.

**Anti-patterns to refuse:**
- `try { expect_value(...) } recover { caught = true }; t |> success(caught, ...)` — wrong tool. There is no daslang pytest.raises.
- `try { x = to_int(s) } recover { x = -1 }` — parse failure is normal; use `try_to_int` from `daslib/strings_convert`.
- `try { v = tab[k] } recover { v = default }` — use `tab?[k] ?? default` or `key_exists`.

**See also:** `feedback_no_try_recover_for_soft_fail` auto-memory entry; `daslib/strings_convert` for the `try_to_*` / `to_*_or` family pattern; `modules/dasImgui/widgets/imgui_playwright.das` for `expect_value` as a real "intentionally panic on timeout" helper.

**Found 2026-05-10**, dasImgui Phase 3 retro — attempted to use `try/recover` to test `expect_value` panic UX; Boris flagged the misuse and announced lint rule.

## Questions
- How do I test that a function panics in daslang? (pytest.raises equivalent)
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ last_verified: 2026-05-10
links: []
---

**Pattern:** spawn `daslang-live <feature>.das` as a subprocess via `popen_argv`, talk to its `live_api` HTTP server (default port 9090) using dashv's fire-and-forget client, and `POST /shutdown` when done. Wrap the lifecycle in a `with_live_app(path) <| $(d) { ... }` helper so test bodies just see a `LiveDriver`.
**Pattern:** spawn `daslang-live <feature>.das` as a subprocess via `popen_argv`, talk to its `live_api` HTTP server (default port 9090) using a `Transport` lambda (or dashv directly for lifecycle endpoints), and `POST /shutdown` when done. Wrap the lifecycle in a `with_imgui_app(path) <| $(app) { ... }` helper so test bodies just see an `ImguiApp`.

Reference impl: `modules/dasImgui/tests/integration/live_driver.das`. Tests in same dir.
Reference impl: `modules/dasImgui/widgets/imgui_playwright.das` (Phase 3 — public sibling module). Tests in `modules/dasImgui/tests/integration/test_*.das`. Spec example: `modules/dasImgui/examples/save_demo/`.

Historical note: through Phase 2 this lived privately at `tests/integration/live_driver.das` with type `LiveDriver` and `with_live_app`. Phase 3 promoted to public `require imgui/imgui_playwright`, renamed to `ImguiApp` / `with_imgui_app`, and added the assertion family below.

**Subprocess primitives** (`daslib/fio`):
- `popen_argv(args : array<string>; timeout_sec : float; scope : block<(f : FILE?) : void>) : int` — runs the body while subprocess is alive; on body return, drains pipe, waits for exit, returns exit code. `popen_timed_out` is the timeout sentinel.
Expand All @@ -25,16 +27,24 @@ Reference impl: `modules/dasImgui/tests/integration/live_driver.das`. Tests in s
**The playwright analogy is exact:**
| Playwright | dasImgui playwright |
|---|---|
| `await page.click("#btn")` | `imgui_click {"target":"SAVE_BTN"}` |
| `await page.fill("input", "v")` | `imgui_set {"target":"SPEED","value":7}` |
| `await page.waitForSelector(...)` | `wait_for_widget(d, ident, timeout)` |
| `await expect(span).toHaveText("1")` | `wait_for_int_value(d, target, field, expected, timeout)` |
| `await page.waitForFunction(...)` | `wait_until(d, timeout) $(snap) => predicate` |
| `await page.reload()` | `reload(d)` (POST /reload + poll /status until has_error=false) |
| `await page.click("#btn")` | `click(app, "SAVE_BTN")` (→ `imgui_click`) |
| `await page.fill("input", "v")` | `set_value(app, "SPEED", JV(7))` (→ `imgui_set`) |
| `await page.type("input", "x")` | `type_text(app, "NAME", "hello")` — auto-focuses target first |
| `await page.waitForSelector(...)` | `wait_for_widget(app, ident, timeout)` |
| `await expect(span).toHaveText("1")` | `wait_for_int_value(app, target, field, expected, timeout)` — returns bool |
| `expect(...).toBe(...)` (hard-fail) | `expect_value(app, target, field, expected, timeout)` — panics on timeout |
| `expect(elem).toBeVisible()` | `expect_render(app, ident, timeout)` — panics with registered-widget list |
| `await page.waitForFunction(...)` | `wait_until(app, timeout) $(snap) => predicate` |
| `await page.waitForLoadState("networkidle")` | `await_quiescent(app, timeout)` — panics on timeout |
| `await page.reload()` | `reload(app)` (POST /reload + poll /status until has_error=false) |

**Never write `sleep()` in tests.** The internal poll loop sleeps inside `wait_until` (50 ms) — that's invisible to test authors. Test bodies use `wait_for_int_value` / `expect_value` to converge on observable state, with a per-call timeout. Auto-retry beats fixed-sleep on every axis.

**`expect_*` vs `wait_for_*`** — the `expect_*` family (Phase 3) panics on timeout with a focused failure message (want / got / widget kind / rendered). The `wait_for_*` family (Phase 2) returns bool. Pick `expect_*` when a failure means "the test is broken"; pick `wait_for_*` when you want soft-success boolean control flow (`if (wait_for_int_value(...)) { ... } else { ... }`).

**Never write `sleep()` in tests.** The internal poll loop sleeps inside `wait_until` (50 ms) — that's invisible to test authors. Test bodies use `wait_for_int_value` etc. to converge on observable state, with a per-call timeout. Auto-retry beats fixed-sleep on every axis (correctness, speed, robustness).
**`type_text` auto-focus** — Phase 3's `type_text` posts `imgui_focus` first, polls `globals[target].focus == true`, then sends UTF-8 chars via `imgui_type_text`. **Feature apps using `type_text` (or `drag`) MUST call `advance_coroutines()` each frame** — the char-streaming is a Phase 2 coroutine. Without that, chars sit in queue forever and `expect_value` after will fail with `got: ""`. See `examples/features/io_synth_text.das` for the per-frame pattern.

**Module declaration gotcha:** `module live_driver shared public` REQUIRES all transitive `require`s be `shared`. `daslib/command_line` isn't shared — drop `shared` and use plain `module live_driver public` for test harnesses.
**Module declaration gotcha:** `module imgui_playwright shared public` REQUIRES all transitive `require`s be `shared`. `daslib/command_line` isn't shared — drop `shared` and use plain `module imgui_playwright public` for test harnesses. The error surfaces at full-suite link time (`error[20115]: Shared module ... has incorrect dependency type. Can't require X because its not shared`), NOT at single-file `compile_check`. Re-bit me at Phase 3.6.

**JSON traversal gotcha:** `snap?["globals"]?["MISSING_KEY"]` returns `JV(null)`, not literal-null pointer. So `result == null` is false even when the key is absent. Use a `widget_exists` helper that checks for a known field (e.g. `kind`) instead of a null-pointer test.

Expand Down
Loading
Loading