Skip to content

Commit 1ff9a53

Browse files
authored
Merge pull request GaijinEntertainment#2671 from GaijinEntertainment/bbatkin/mouse-cards-may15
mouse: 5 cards from dasImgui PR #29 lint detour + PR #32 Example Console port
2 parents 9690ecb + b3b8bea commit 1ff9a53

5 files changed

Lines changed: 276 additions & 0 deletions
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
---
2+
slug: how-do-i-embed-a-bound-cpp-struct-as-a-daslang-state-field-and-what-binding-overrides-do-i-need
3+
title: How do I embed a bound C++ struct (e.g. ImGuiTextFilter) as an inline field of a daslang state struct so [widget] auto-emit can hold one in a module-scope global?
4+
created: 2026-05-15
5+
last_verified: 2026-05-15
6+
links: []
7+
---
8+
9+
By default a `ManagedStructureAnnotation<T>` rejects most embed paths daslang needs for module-scope `var private` state globals — you'll hit one error at a time until **four** virtual overrides are flipped on the C++ side, then **two** more on the daslang side.
10+
11+
## C++ overrides (e.g. `src/dasIMGUI.struct.class.inc`)
12+
13+
```cpp
14+
struct ImGuiTextFilter_GeneratedAnnotation : ManagedStructureAnnotation<ImGuiTextFilter> {
15+
virtual bool isLocal() const override { return true; }
16+
virtual bool canBePlacedInContainer() const override { return true; }
17+
virtual bool hasNonTrivialCtor() const override { return false; }
18+
virtual bool canCopy() const override { return true; }
19+
...
20+
};
21+
```
22+
23+
Why each one:
24+
- **`isLocal()=true`** — daslang structs can embed this as a field. Default is false; without it, error 30239 *"contains Handled type, where isLocal() returned false"* on first field declaration.
25+
- **`canBePlacedInContainer()=true`** — daslang containers (tables, arrays) can hold this. `isLocal` alone isn't enough.
26+
- **`hasNonTrivialCtor()=false`** — daslang trusts zero-init bytes as a valid default. Default is true; without it, error 30175 *"global variable X can't be initialized at all"* once the containing struct goes module-scope. Only safe to flip when the C++ struct's zero-bytes form is a valid object (e.g. `ImGuiTextFilter` zeros → empty `InputBuf` + empty `ImVector Filters` + `CountGrep=0`).
27+
- **`canCopy()=true`** — needed whenever the type is bound with `addCtorAndUsing<T, ...>` (i.e. has a daslang-visible factory). Without it, daslang's `BuiltInFn` registration refuses with *"can't be bound. It returns values which can't be copied or moved"* because the factory returns `T` by value.
28+
29+
## Binder side (the `bind/*.das` policy file)
30+
31+
`cbind_boost` only emits **the first two** overrides for entries in `local_type_names <- { ... }`. The trivial-ctor + canCopy overrides have to be re-applied manually after every regen. Add a comment block above the struct flagging this — `cbind_boost.das:1131` is the only emitter.
32+
33+
## Daslang overrides (where the state struct lives, e.g. a `shared` boost module)
34+
35+
Even with the C++ side correct, two daslang-side generic walkers fail on bound C++ fields:
36+
37+
1. **`daslib/json_boost`'s `JV<T>(value : T) : JsonValue?`** walks every field and calls `_::JV(field)`. The v1 `register_widgets` chain auto-instantiates this for every state struct, so JV-failing field = whole-module compile fail. Add an explicit override:
38+
```das
39+
def public JV(value : MyState) : JsonValue? {
40+
return JV((field1 = value.field1, ...)) // serialize what makes sense
41+
}
42+
```
43+
44+
2. **`daslib/archive`'s generic `serialize<T>(arch, value : T)`** walks every field and calls `_::serialize(arch, field)`. The `LiveVarsPass` (in `modules/dasLiveHost/live/live_vars.das`) auto-discovers every module-scope global marked `@live` (which **includes every [widget]-auto-emitted state global** in dasImgui) and emits archive calls into `__before_reload_live_vars`. Same opt-out — add an explicit override:
45+
```das
46+
def public serialize(var arch : Archive; var value : MyState) {
47+
arch |> _::serialize(value.field_to_persist)
48+
// skip the bound C++ field; it regenerates on first frame after reload
49+
}
50+
```
51+
52+
Both overrides go next to the state struct in the boost rail (`widgets/imgui_boost_runtime.das` is the dasImgui slot). Make them `def public` so transitive consumers see them.
53+
54+
## Reality check
55+
56+
If the bound C++ struct's **fields** are also walked by some downstream generic (rare, but a hand-bound nested field that itself contains another bound type might trigger it), you may need a per-field override too. Surface as the error appears — there's no exhaustive list. The four C++ + two daslang overrides above cover dasImgui's [widget]/[container] auto-emit chain (verified 2026-05-15 on `ImGuiTextFilter` in dasImgui PR landing the `app_console.das` port).
57+
58+
## Pure-daslang alternative
59+
60+
If you don't actually need the C++ struct's methods (e.g. `ImGuiTextFilter::Draw` rendering an InputText editor), and you're just consuming a predicate, reimplement in daslang. ~30 LOC for the comma-split include/exclude logic; no C++ rebuild, no generic-walker overrides. Pick this when the C++ struct's only value is convenience.
61+
62+
## See also
63+
- `dasimgui-widget-state-struct-field-defaults-fire` — companion: how state struct field defaults map to the auto-emitted global
64+
- `how-do-i-preserve-a-c-owned-resource-across-a-daslang-live-reload` — the live-reload context for why archive::serialize gets walked at all
65+
66+
## Questions
67+
- How do I embed a bound C++ struct (e.g. ImGuiTextFilter) as an inline field of a daslang state struct so [widget] auto-emit can hold one in a module-scope global?
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
slug: how-do-i-rebuild-a-loaded-daslang-shared-module-when-linker-fails-lnk1104-dll-locked
3+
title: How do I rebuild dasModuleImgui (or another loaded daslang shared module) when the linker fails with LNK1104 because MCP / daslang-live has the DLL mapped?
4+
created: 2026-05-15
5+
last_verified: 2026-05-15
6+
links: []
7+
---
8+
9+
**Rename the locked output, then build.** Windows lets you rename a file that's been `LoadLibrary`'d (the open handle keeps using the old path's inode), so MSBuild can write a fresh file at the original name. Existing daslang processes keep using the old code until they restart.
10+
11+
```powershell
12+
cd D:/Work/daScript/modules/dasImgui
13+
mv dasModuleImgui.shared_module dasModuleImgui.shared_module.locked
14+
cmake --build build --config Release --target dasModuleImgui -j 8
15+
# new dasModuleImgui.shared_module exists; .locked stays until MCP restarts
16+
rm dasModuleImgui.shared_module.locked # optional, after MCP restart
17+
```
18+
19+
**Important caveats:**
20+
21+
- The MCP server (and any other daslang process that did a `require imgui`-chain compile) keeps the old code in its address space. **Fresh daslang subprocesses load the new code** — so `bin/Release/daslang.exe -compile-only path/to/file.das` from a shell sees the new behavior, but `mcp__daslang__compile_check` (in-process to MCP) still sees the old. Restart MCP to refresh.
22+
- Test by spawning a fresh subprocess first to confirm the rebuild took effect, *then* ask the user to restart MCP — saves a kill cycle if the build actually regressed.
23+
- Don't try to delete the `.locked` file before MCP restarts. Windows blocks `rm` of a memory-mapped file until every loader process releases it. To identify which process is holding a given DLL: `Get-Process | Where-Object { $_.Modules.FileName -like "*dasModuleImgui*" }` (uses the `System.Diagnostics.Process.Modules` collection, NOT the `Win32_Process` CIM class — the latter doesn't expose loaded modules).
24+
25+
**Alternative: ask the user to kill MCP first.** Cleaner but ends the session — every subsequent `mcp__*` call fails until they restart. Use the rename trick when you want to keep working while iterating on a C++ change.
26+
27+
**Default arg pre-check.** Before kicking off a multi-minute rebuild, run `Get-CimInstance Win32_Process -Filter "name='daslang.exe'" | Select ProcessId, CommandLine | Format-List` to see who's holding modules. Often it's only `utils/mcp/main.das` + `utils/mouse/main.das`; both can be restarted by the user. If a `daslang-live` is open, the rename trick is the *only* path that doesn't kick the user out of their live session.
28+
29+
**Found 2026-05-15** during dasImgui ImGuiTextFilter binding patch work — needed three rebuild cycles (one per discovered override), MCP was holding the DLL each time.
30+
31+
## See also
32+
- `feedback_kill_before_build.md` — companion: which processes to kill if you go the kill-first route
33+
- `feedback_never_rm_build.md` — DON'T nuke `build/` to "fix" a lock; libhv + OpenSSL take an hour
34+
35+
## Questions
36+
- How do I rebuild dasModuleImgui (or another loaded daslang shared module) when the linker fails with LNK1104 because MCP / daslang-live has the DLL mapped?
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
---
2+
slug: imgui2rst-strict-struct-fields-docstring-count
3+
title: imgui2rst CI panics "has less documentation than values, expected at least N lines" — what's the requirement?
4+
created: 2026-05-15
5+
last_verified: 2026-05-15
6+
links: []
7+
---
8+
9+
# imgui2rst strict_struct_fields — N //! lines required per N fields
10+
11+
Symptom: dasImgui docs CI panics with:
12+
13+
```
14+
|detail/class-<module>-<ClassName>| has less documentation than values.
15+
Expected at least N lines ([ field1, field2, field3 ]), got M.
16+
PANIC: invalid documentation
17+
```
18+
19+
(Real example from PR #29: `EditExternalCallMacro` had 1 `//!` line above 3 fields → "Expected at least 3 lines, got 1".)
20+
21+
## Rule
22+
23+
A class/struct documented with a `//!` block above the declaration must have **at least one `//!` line per public field**. The check lives at `daslib/rst.das:835` (the `strict_struct_fields` rule), and `utils/imgui2rst.das` enables it. It does NOT count blank `//!` lines specially — each field needs its own descriptive line.
24+
25+
## Fix shape
26+
27+
Per-field `//!` lines BETWEEN the class-level docstring and the field declarations:
28+
29+
```das
30+
class EditExternalCallMacro : AstCallMacro {
31+
//! Call macro installed by ``[edit_widget]``. Recognizes ...
32+
//! Name of the [edit_widget] function — used as the rewritten call's target.
33+
//! Underlying [edit_widget] function pointer; lives in the user module, read at visit time.
34+
//! User-facing args (positions 2..N after ``ptr`` + injected ``widget_ident``); cloned from render_fn.
35+
kind_name : string
36+
render_fn : FunctionPtr
37+
render_fn_args : array<VariablePtr>
38+
}
39+
```
40+
41+
Order matches field declaration order. First `//!` line is the class-level summary; the next N lines map 1:1 to the N fields.
42+
43+
## Why CI catches it but local builds usually don't
44+
45+
Most local dev runs skip the imgui2rst step. The Sphinx docs build (CI `docs.yml`) is where it surfaces, often hours into a long PR cycle. To reproduce locally:
46+
47+
```
48+
daslang utils/imgui2rst.das -- --detail_output doc/source/stdlib/generated
49+
```
50+
51+
No output = pass. PANIC = the rule fired.
52+
53+
## Applies to
54+
55+
- Any `class` or `struct` in `modules/dasImgui/widgets/*.das` with a `//!` block — `[widget]`/`[container]`/`[call_macro]` infrastructure classes especially. Private classes are exempt (no public docs generated).
56+
- Same rule fires on `enum` and `variant` field counts.
57+
58+
If you don't care to document a field, the rule still requires a `//!` line — give it a one-line "internal — see <function>" pointer. Don't delete the class-level docstring to dodge the rule; that just makes the class undocumented in the published index.
59+
60+
## Questions
61+
- imgui2rst CI panics "has less documentation than values, expected at least N lines" — what's the requirement?
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
---
2+
slug: lint-macro-opt-in-via-options-find-arg
3+
title: How do I make a [lint_macro] opt-in via an options flag like _comment_hygiene?
4+
created: 2026-05-15
5+
last_verified: 2026-05-15
6+
links: []
7+
---
8+
9+
# Opt-in [lint_macro] via `prog._options |> find_arg("...")`
10+
11+
The `daslib/style_lint.das` pattern for `_comment_hygiene` / `_no_imgui_legacy` style options: the lint registers via `[lint_macro]` (so the runner always loads it), but checks an options flag in `apply()` and returns early when unset. Off-by-default, opt-in per file via `options _flag = true`.
12+
13+
```das
14+
[lint_macro]
15+
class MyLintMacro : AstPassMacro {
16+
def override apply(prog : ProgramPtr; mod : Module?) : bool {
17+
let enabled = prog._options |> find_arg("_my_flag") ?as tBool ?? false
18+
if (!enabled) return true
19+
var v = new MyLintVisitor()
20+
make_visitor(*v) $(adapter) {
21+
visit_module(prog, adapter, prog.getThisModule)
22+
}
23+
unsafe { delete v; }
24+
return true
25+
}
26+
}
27+
```
28+
29+
User opts in at the top of their consumer file:
30+
31+
```das
32+
options _my_flag = true
33+
require my_module/my_lint
34+
```
35+
36+
## Pieces
37+
38+
- **`prog._options`** is the program-level options bag (annotation-arg list).
39+
- **`find_arg("name")`** returns `AnnotationArgument | null`.
40+
- **`?as tBool`** extracts the bool case from the variant; `?? false` gives the absent-or-not-bool default.
41+
- **Always return `true` from `apply`** unless you want to abort the compile. The lint emits errors via `compiling_program() |> macro_error(at, "...")` and that's what fails the build — `return false` would be a hard abort with no diagnostic.
42+
- **`visit_module(prog, adapter, prog.getThisModule)`** scopes the walk to the consumer file only. Transitively-required modules don't get walked (so library code with deliberate "forbidden" patterns inside it doesn't self-flag).
43+
44+
## Why the always-load + check-flag-in-apply shape
45+
46+
`[lint_macro]` is registered at module-require time, before the consumer's `options` are parsed. You can't skip registration based on an option. So the macro always loads, but exits cheaply when the flag is absent. Cost when disabled: one `find_arg` lookup per compile.
47+
48+
## Reference implementations
49+
50+
- `daslib/style_lint.das``_comment_hygiene` family.
51+
- `modules/dasImgui/widgets/imgui_lint.das``_no_imgui_legacy` (PR #29).
52+
53+
## Questions
54+
- How do I make a [lint_macro] opt-in via an options flag like _comment_hygiene?
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
---
2+
slug: lint-macro-skip-qmacro-synth-calls-via-fileinfo
3+
title: How do I filter macro-synthesized ExprCalls from a [lint_macro] AST visitor?
4+
created: 2026-05-15
5+
last_verified: 2026-05-15
6+
links: []
7+
---
8+
9+
# Lint-macro AST walkers must skip macro-synthesized calls
10+
11+
Symptom: your `[lint_macro]` walks `prog.getThisModule` and fires on calls the user never wrote. Example: `imgui_lint` flagged raw `PushID`/`PopID` calls inside the user's file — but those calls came from inside a `[container] with_id(...)` macro expansion, not from user source.
12+
13+
Root cause: qmacro emits `ExprCall` nodes whose `at` field points at the **macro template's source file** (e.g. `widgets/imgui_boost.das`), not the consumer's. The visitor sees them while walking the function body in the user's module, because the qmacro splice baked them into that body during infer.
14+
15+
## Fix — combined skip in preVisitExprCall
16+
17+
Track the containing function (`preVisitFunction` / `visitFunction`), then in `preVisitExprCall` skip when EITHER:
18+
19+
1. The containing function is generated (`current_function.flags.generated`) — these are macro-generated function bodies, not user code.
20+
2. The call's `at.fileInfo` doesn't match the containing function's `at.fileInfo` — these are spliced-in macro bodies.
21+
22+
```das
23+
class MyLintVisitor : AstVisitor {
24+
@do_not_delete current_function : Function?
25+
26+
def override preVisitFunction(var fn : FunctionPtr) : void {
27+
current_function = fn
28+
}
29+
def override visitFunction(var fn : FunctionPtr) : FunctionPtr {
30+
current_function = null
31+
return <- fn
32+
}
33+
def override preVisitExprCall(var expr : ExprCall?) : void {
34+
if (expr.func == null || expr.func._module == null
35+
|| (current_function != null
36+
&& (current_function.flags.generated
37+
|| (current_function.at.fileInfo != null
38+
&& expr.at.fileInfo != current_function.at.fileInfo)))) return
39+
// ... real lint logic here
40+
}
41+
}
42+
```
43+
44+
## Why both checks
45+
46+
- `flags.generated` catches helper functions the macro inserts whole (e.g. AOT-emitted thunks). The function itself never existed in user source.
47+
- `fileInfo` mismatch catches the more common case: a user-authored function whose body got spliced bits during infer. The function `at` is user source, but individual `ExprCall` nodes inside it carry the macro-template source's fileInfo.
48+
49+
Without the `fileInfo` check, lint passes fire on every `[widget]` or `[container]` expansion inside the user's function.
50+
51+
## Cross-references
52+
53+
- `modules/dasImgui/widgets/imgui_lint.das` is the canonical reference implementation (PR #29, merged 2026-05-14).
54+
- The pattern is the same one used by `daslib/style_lint.das` for `_comment_hygiene` and friends.
55+
- Scope the walk via `make_visitor(*v) $(adapter) { visit_module(prog, adapter, prog.getThisModule) }` so transitively-required modules aren't walked (the boost itself legitimately calls raw `imgui::*`).
56+
57+
## Questions
58+
- How do I filter macro-synthesized ExprCalls from a [lint_macro] AST visitor?

0 commit comments

Comments
 (0)