Skip to content

fix: analyzer false positives, MCP cross-file diagnostics, macro expansion, and semantic audit#262

Merged
sam-at-luther merged 26 commits intomainfrom
fix/258-260-analysis-and-mcp-diagnostics
Apr 3, 2026
Merged

fix: analyzer false positives, MCP cross-file diagnostics, macro expansion, and semantic audit#262
sam-at-luther merged 26 commits intomainfrom
fix/258-260-analysis-and-mcp-diagnostics

Conversation

@sam-at-luther
Copy link
Copy Markdown
Member

@sam-at-luther sam-at-luther commented Apr 1, 2026

Summary

Fixes 4 issues + semantic audit gaps + workspace macro loading for embedders.

Issue fixes

Semantic audit

  • macrolet scope handling (new ScopeMacrolet)
  • qualified-symbol argument skipped (data, not code)
  • thread-first/thread-last explicit dispatch

Workspace macro loading

  • Preamble forms (in-package, use-package, export, defmacro, defun, set) collected per file in source order
  • LoadWorkspaceMacros replays preamble into env, mirroring runtime's (load) behavior
  • DFS load-order from buildLoadTree ensures definitions precede macros that use them
  • Package resolution: bare files inherit load-tree context or fall back to DefaultPackage
  • Auto-creates workspace packages + imports lang builtins
  • ExpandMacro(form, pkg) resolves macros in the file's package context
  • Thread-safe via sync.Mutex + notMacro cache with package-qualified keys

Embedder API

lintCfg := &lint.LintConfig{
    Workspace: root,
    Registry:  env.Runtime.Registry,
    Env:       env,  // just add this line — elps handles the rest
}

Test plan

  • 40+ new test functions across analysis, expander, workspace, and MCP packages
  • go test ./... — all pass
  • make static-checks — 0 issues
  • CI: Lint & Test — pass
  • CI: Benchmark Comparison — pass (no regressions)
  • 4 QA professor passes + 3 code reviews — all findings addressed

Fixes #258, Fixes #259, Fixes #260, Fixes #261

…analyzers

- Add analyzeCond to skip resolving bare 'else'/'true' in cond test
  position, matching the runtime's special handling (Fixes #258)

- Fix analyzeSet to model runtime set semantics: PutGlobal always
  writes to package scope. In nested scopes, reference the existing
  global binding instead of creating a spurious local one (Fixes #260)
Replace ScanWorkspaceAllWithConfig with PrescanWorkspace in
buildWorkspaceState to populate all config fields (PackageSymbols,
PackageImports, DefaultPackage, WorkspaceRefs) — matching the LSP
server's buildWorkspaceIndex. Also copy all fields in loadDocument
so per-file analysis has full workspace context.

Fixes #259
@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 1, 2026

Benchmark Comparison (main baseline vs PR)

Click to expand benchstat output
goos: linux
goarch: amd64
pkg: github.com/luthersystems/elps/lisp
cpu: AMD EPYC 7763 64-Core Processor                
                             │     base     │                 pr                 │
                             │    sec/op    │    sec/op     vs base              │
EnvGet-4                       10.97m ± ∞ ¹   10.90m ± ∞ ¹       ~ (p=0.548 n=5)
EnvFunCallBuiltin-4            729.3µ ± ∞ ¹   730.0µ ± ∞ ¹       ~ (p=0.690 n=5)
EnvFunCallRecursion-4          8.568m ± ∞ ¹   8.872m ± ∞ ¹       ~ (p=0.548 n=5)
EnvFunCallTailRec-4            3.856m ± ∞ ¹   3.706m ± ∞ ¹  -3.88% (p=0.008 n=5)
EnvFunCallPos-4                1.810m ± ∞ ¹   1.753m ± ∞ ¹  -3.14% (p=0.008 n=5)
EnvFunCallKey-4                2.108m ± ∞ ¹   2.044m ± ∞ ¹  -3.02% (p=0.008 n=5)
EnvFunCallOpt-4                1.792m ± ∞ ¹   1.738m ± ∞ ¹  -3.01% (p=0.008 n=5)
EnvFunCallVar-4                2.263m ± ∞ ¹   2.148m ± ∞ ¹       ~ (p=0.151 n=5)
FormatStringSimple-4           773.3µ ± ∞ ¹   741.2µ ± ∞ ¹       ~ (p=0.056 n=5)
FormatStringMultiple-4         829.4µ ± ∞ ¹   845.9µ ± ∞ ¹       ~ (p=0.690 n=5)
FormatStringNoPlaceholders-4   666.4µ ± ∞ ¹   661.7µ ± ∞ ¹       ~ (p=0.690 n=5)
FormatStringManyTokens-4       1.055m ± ∞ ¹   1.045m ± ∞ ¹       ~ (p=0.548 n=5)
FormatStringMixed-4            1.091m ± ∞ ¹   1.075m ± ∞ ¹       ~ (p=0.151 n=5)
FormatStringPositional-4       851.1µ ± ∞ ¹   830.4µ ± ∞ ¹  -2.44% (p=0.032 n=5)
FormatStringConcat-4           744.4µ ± ∞ ¹   732.4µ ± ∞ ¹  -1.61% (p=0.032 n=5)
MacroDefun-4                   43.05µ ± ∞ ¹   41.29µ ± ∞ ¹       ~ (p=0.056 n=5)
geomean                        1.284m         1.261m        -1.81%
¹ need >= 6 samples for confidence interval at level 0.95

                             │     base      │                 pr                  │
                             │     B/op      │     B/op       vs base              │
EnvGet-4                       10.20Mi ± ∞ ¹   10.20Mi ± ∞ ¹       ~ (p=0.413 n=5)
EnvFunCallBuiltin-4            829.4Ki ± ∞ ¹   829.4Ki ± ∞ ¹       ~ (p=0.119 n=5)
EnvFunCallRecursion-4          3.768Mi ± ∞ ¹   3.768Mi ± ∞ ¹       ~ (p=0.151 n=5)
EnvFunCallTailRec-4            3.454Mi ± ∞ ¹   3.454Mi ± ∞ ¹       ~ (p=0.984 n=5)
EnvFunCallPos-4                1.813Mi ± ∞ ¹   1.813Mi ± ∞ ¹       ~ (p=0.452 n=5)
EnvFunCallKey-4                1.889Mi ± ∞ ¹   1.889Mi ± ∞ ¹       ~ (p=0.421 n=5)
EnvFunCallOpt-4                1.813Mi ± ∞ ¹   1.813Mi ± ∞ ¹       ~ (p=0.246 n=5)
EnvFunCallVar-4                2.385Mi ± ∞ ¹   2.385Mi ± ∞ ¹       ~ (p=0.444 n=5)
FormatStringSimple-4           727.9Ki ± ∞ ¹   727.9Ki ± ∞ ¹       ~ (p=1.000 n=5)
FormatStringMultiple-4         798.2Ki ± ∞ ¹   798.2Ki ± ∞ ¹       ~ (p=1.000 n=5)
FormatStringNoPlaceholders-4   696.6Ki ± ∞ ¹   696.6Ki ± ∞ ¹       ~ (p=0.484 n=5)
FormatStringManyTokens-4       1.008Mi ± ∞ ¹   1.008Mi ± ∞ ¹       ~ (p=0.071 n=5)
FormatStringMixed-4            817.7Ki ± ∞ ¹   817.7Ki ± ∞ ¹       ~ (p=0.111 n=5)
FormatStringPositional-4       798.2Ki ± ∞ ¹   798.2Ki ± ∞ ¹       ~ (p=0.651 n=5)
FormatStringConcat-4           727.9Ki ± ∞ ¹   727.9Ki ± ∞ ¹       ~ (p=0.333 n=5)
MacroDefun-4                   33.31Ki ± ∞ ¹   33.31Ki ± ∞ ¹       ~ (p=0.611 n=5)
geomean                        1.145Mi         1.145Mi        -0.00%
¹ need >= 6 samples for confidence interval at level 0.95

                             │     base     │                  pr                  │
                             │  allocs/op   │  allocs/op    vs base                │
EnvGet-4                       128.0k ± ∞ ¹   128.0k ± ∞ ¹       ~ (p=1.000 n=5)
EnvFunCallBuiltin-4            8.015k ± ∞ ¹   8.015k ± ∞ ¹       ~ (p=1.000 n=5) ²
EnvFunCallRecursion-4          40.08k ± ∞ ¹   40.08k ± ∞ ¹       ~ (p=1.000 n=5) ²
EnvFunCallTailRec-4            44.07k ± ∞ ¹   44.07k ± ∞ ¹       ~ (p=1.000 n=5) ²
EnvFunCallPos-4                22.06k ± ∞ ¹   22.06k ± ∞ ¹       ~ (p=1.000 n=5) ²
EnvFunCallKey-4                23.06k ± ∞ ¹   23.06k ± ∞ ¹       ~ (p=1.000 n=5) ²
EnvFunCallOpt-4                22.06k ± ∞ ¹   22.06k ± ∞ ¹       ~ (p=1.000 n=5) ²
EnvFunCallVar-4                29.06k ± ∞ ¹   29.06k ± ∞ ¹       ~ (p=1.000 n=5) ²
FormatStringSimple-4           9.015k ± ∞ ¹   9.015k ± ∞ ¹       ~ (p=1.000 n=5) ²
FormatStringMultiple-4         9.015k ± ∞ ¹   9.015k ± ∞ ¹       ~ (p=1.000 n=5) ²
FormatStringNoPlaceholders-4   8.015k ± ∞ ¹   8.015k ± ∞ ¹       ~ (p=1.000 n=5) ²
FormatStringManyTokens-4       9.015k ± ∞ ¹   9.015k ± ∞ ¹       ~ (p=1.000 n=5) ²
FormatStringMixed-4            10.02k ± ∞ ¹   10.02k ± ∞ ¹       ~ (p=1.000 n=5) ²
FormatStringPositional-4       9.015k ± ∞ ¹   9.015k ± ∞ ¹       ~ (p=1.000 n=5) ²
FormatStringConcat-4           9.015k ± ∞ ¹   9.015k ± ∞ ¹       ~ (p=1.000 n=5) ²
MacroDefun-4                    475.0 ± ∞ ¹    475.0 ± ∞ ¹       ~ (p=1.000 n=5) ²
geomean                        13.58k         13.58k        +0.00%
¹ need >= 6 samples for confidence interval at level 0.95
² all samples are equal

pkg: github.com/luthersystems/elps/lisp/lisplib/libjson
                              │     base     │                 pr                 │
                              │    sec/op    │    sec/op     vs base              │
Encode-4                        18.87µ ± ∞ ¹   18.68µ ± ∞ ¹       ~ (p=0.151 n=5)
Encode_stringNumbers-4          2.895µ ± ∞ ¹   2.834µ ± ∞ ¹  -2.11% (p=0.008 n=5)
Package/$load-4                 1.375m ± ∞ ¹   1.340m ± ∞ ¹  -2.50% (p=0.008 n=5)
Package/load-object-4           2.266m ± ∞ ¹   2.246m ± ∞ ¹       ~ (p=0.056 n=5)
Package/load-array-4            2.835m ± ∞ ¹   2.782m ± ∞ ¹       ~ (p=0.095 n=5)
Package/load-nested-4           3.391m ± ∞ ¹   3.309m ± ∞ ¹       ~ (p=0.151 n=5)
Package/load-github-4           81.41m ± ∞ ¹   79.23m ± ∞ ¹  -2.69% (p=0.032 n=5)
Package/get-nested-baseline-4   45.75m ± ∞ ¹   44.58m ± ∞ ¹       ~ (p=0.056 n=5)
Package/dump-object-4           1.855m ± ∞ ¹   1.788m ± ∞ ¹  -3.61% (p=0.008 n=5)
Package/dump-array-4            1.077m ± ∞ ¹   1.030m ± ∞ ¹  -4.35% (p=0.008 n=5)
Package/dump-nested-4           2.858m ± ∞ ¹   2.829m ± ∞ ¹       ~ (p=0.421 n=5)
Package/dump-github-4           50.89m ± ∞ ¹   46.61m ± ∞ ¹  -8.42% (p=0.008 n=5)
geomean                         1.865m         1.813m        -2.81%
¹ need >= 6 samples for confidence interval at level 0.95

                              │     base      │                  pr                   │
                              │     B/op      │     B/op       vs base                │
Encode-4                        10.05Ki ± ∞ ¹   10.05Ki ± ∞ ¹       ~ (p=1.000 n=5)
Encode_stringNumbers-4          1.297Ki ± ∞ ¹   1.297Ki ± ∞ ¹       ~ (p=1.000 n=5) ²
Package/$load-4                 939.5Ki ± ∞ ¹   939.5Ki ± ∞ ¹       ~ (p=0.095 n=5)
Package/load-object-4           1.437Mi ± ∞ ¹   1.437Mi ± ∞ ¹       ~ (p=0.476 n=5)
Package/load-array-4            2.223Mi ± ∞ ¹   2.223Mi ± ∞ ¹  +0.00% (p=0.032 n=5)
Package/load-nested-4           2.147Mi ± ∞ ¹   2.147Mi ± ∞ ¹       ~ (p=0.833 n=5)
Package/load-github-4           32.73Mi ± ∞ ¹   32.73Mi ± ∞ ¹       ~ (p=0.063 n=5)
Package/get-nested-baseline-4   34.62Mi ± ∞ ¹   34.62Mi ± ∞ ¹       ~ (p=0.222 n=5)
Package/dump-object-4           1.735Mi ± ∞ ¹   1.735Mi ± ∞ ¹       ~ (p=0.222 n=5)
Package/dump-array-4            807.5Ki ± ∞ ¹   807.5Ki ± ∞ ¹       ~ (p=0.683 n=5)
Package/dump-nested-4           2.835Mi ± ∞ ¹   2.835Mi ± ∞ ¹       ~ (p=0.976 n=5)
Package/dump-github-4           51.03Mi ± ∞ ¹   51.03Mi ± ∞ ¹       ~ (p=0.095 n=5)
geomean                         1.270Mi         1.270Mi        +0.00%
¹ need >= 6 samples for confidence interval at level 0.95
² all samples are equal

                              │     base     │                  pr                  │
                              │  allocs/op   │  allocs/op    vs base                │
Encode-4                         214.0 ± ∞ ¹    214.0 ± ∞ ¹       ~ (p=1.000 n=5) ²
Encode_stringNumbers-4           30.00 ± ∞ ¹    30.00 ± ∞ ¹       ~ (p=1.000 n=5) ²
Package/$load-4                 22.74k ± ∞ ¹   22.74k ± ∞ ¹       ~ (p=1.000 n=5)
Package/load-object-4           25.03k ± ∞ ¹   25.03k ± ∞ ¹       ~ (p=1.000 n=5) ²
Package/load-array-4            41.03k ± ∞ ¹   41.03k ± ∞ ¹       ~ (p=1.000 n=5) ²
Package/load-nested-4           39.03k ± ∞ ¹   39.03k ± ∞ ¹       ~ (p=1.000 n=5) ²
Package/load-github-4           457.0k ± ∞ ¹   457.0k ± ∞ ¹  +0.00% (p=0.048 n=5)
Package/get-nested-baseline-4   504.1k ± ∞ ¹   504.1k ± ∞ ¹       ~ (p=0.302 n=5)
Package/dump-object-4           23.04k ± ∞ ¹   23.04k ± ∞ ¹       ~ (p=1.000 n=5) ²
Package/dump-array-4            10.03k ± ∞ ¹   10.03k ± ∞ ¹       ~ (p=1.000 n=5) ²
Package/dump-nested-4           37.06k ± ∞ ¹   37.06k ± ∞ ¹       ~ (p=1.000 n=5) ²
Package/dump-github-4           387.5k ± ∞ ¹   387.5k ± ∞ ¹       ~ (p=0.095 n=5)
geomean                         20.15k         20.15k        -0.00%
¹ need >= 6 samples for confidence interval at level 0.95
² all samples are equal

pkg: github.com/luthersystems/elps/parser/rdparser
                                       │     base     │                 pr                 │
                                       │    sec/op    │    sec/op     vs base              │
Parser/approx.lisp-4                     198.8µ ± ∞ ¹   194.8µ ± ∞ ¹  -2.04% (p=0.008 n=5)
Parser/complex.lisp-4                    554.3µ ± ∞ ¹   547.8µ ± ∞ ¹       ~ (p=0.095 n=5)
Parser/diff.lisp-4                       235.1µ ± ∞ ¹   226.0µ ± ∞ ¹  -3.88% (p=0.008 n=5)
Parser/scheme-math.lisp-4                1.305m ± ∞ ¹   1.260m ± ∞ ¹       ~ (p=0.151 n=5)
Parser/sicp.lisp-4                       852.2µ ± ∞ ¹   837.8µ ± ∞ ¹       ~ (p=0.310 n=5)
Parser/stream.lisp-4                     762.9µ ± ∞ ¹   743.3µ ± ∞ ¹  -2.57% (p=0.008 n=5)
ParserFaultTolerant/approx.lisp-4        201.1µ ± ∞ ¹   193.2µ ± ∞ ¹  -3.90% (p=0.008 n=5)
ParserFaultTolerant/complex.lisp-4       567.2µ ± ∞ ¹   542.2µ ± ∞ ¹  -4.41% (p=0.008 n=5)
ParserFaultTolerant/diff.lisp-4          236.2µ ± ∞ ¹   227.4µ ± ∞ ¹  -3.71% (p=0.008 n=5)
ParserFaultTolerant/scheme-math.lisp-4   1.332m ± ∞ ¹   1.264m ± ∞ ¹  -5.10% (p=0.008 n=5)
ParserFaultTolerant/sicp.lisp-4          883.4µ ± ∞ ¹   858.0µ ± ∞ ¹       ~ (p=0.222 n=5)
ParserFaultTolerant/stream.lisp-4        783.6µ ± ∞ ¹   766.7µ ± ∞ ¹  -2.15% (p=0.032 n=5)
geomean                                  534.7µ         518.2µ        -3.09%
¹ need >= 6 samples for confidence interval at level 0.95

                                       │     base      │                 pr                  │
                                       │     B/op      │     B/op       vs base              │
Parser/approx.lisp-4                     239.6Ki ± ∞ ¹   239.6Ki ± ∞ ¹       ~ (p=0.889 n=5)
Parser/complex.lisp-4                    462.4Ki ± ∞ ¹   462.4Ki ± ∞ ¹       ~ (p=0.437 n=5)
Parser/diff.lisp-4                       269.3Ki ± ∞ ¹   269.3Ki ± ∞ ¹       ~ (p=1.000 n=5)
Parser/scheme-math.lisp-4                905.7Ki ± ∞ ¹   905.7Ki ± ∞ ¹       ~ (p=0.952 n=5)
Parser/sicp.lisp-4                       679.2Ki ± ∞ ¹   679.2Ki ± ∞ ¹       ~ (p=0.548 n=5)
Parser/stream.lisp-4                     593.5Ki ± ∞ ¹   593.5Ki ± ∞ ¹       ~ (p=0.706 n=5)
ParserFaultTolerant/approx.lisp-4        239.6Ki ± ∞ ¹   239.6Ki ± ∞ ¹  -0.00% (p=0.016 n=5)
ParserFaultTolerant/complex.lisp-4       462.4Ki ± ∞ ¹   462.4Ki ± ∞ ¹       ~ (p=0.595 n=5)
ParserFaultTolerant/diff.lisp-4          269.3Ki ± ∞ ¹   269.3Ki ± ∞ ¹       ~ (p=0.794 n=5)
ParserFaultTolerant/scheme-math.lisp-4   905.7Ki ± ∞ ¹   905.7Ki ± ∞ ¹       ~ (p=0.381 n=5)
ParserFaultTolerant/sicp.lisp-4          679.2Ki ± ∞ ¹   679.2Ki ± ∞ ¹       ~ (p=0.286 n=5)
ParserFaultTolerant/stream.lisp-4        593.5Ki ± ∞ ¹   593.5Ki ± ∞ ¹       ~ (p=0.500 n=5)
geomean                                  470.8Ki         470.8Ki        -0.00%
¹ need >= 6 samples for confidence interval at level 0.95

                                       │     base     │                  pr                  │
                                       │  allocs/op   │  allocs/op    vs base                │
Parser/approx.lisp-4                     3.566k ± ∞ ¹   3.566k ± ∞ ¹       ~ (p=1.000 n=5) ²
Parser/complex.lisp-4                    10.75k ± ∞ ¹   10.75k ± ∞ ¹       ~ (p=1.000 n=5) ²
Parser/diff.lisp-4                       4.232k ± ∞ ¹   4.232k ± ∞ ¹       ~ (p=1.000 n=5) ²
Parser/scheme-math.lisp-4                24.81k ± ∞ ¹   24.81k ± ∞ ¹       ~ (p=1.000 n=5)
Parser/sicp.lisp-4                       16.65k ± ∞ ¹   16.65k ± ∞ ¹       ~ (p=1.000 n=5) ²
Parser/stream.lisp-4                     14.63k ± ∞ ¹   14.63k ± ∞ ¹       ~ (p=1.000 n=5) ²
ParserFaultTolerant/approx.lisp-4        3.566k ± ∞ ¹   3.566k ± ∞ ¹       ~ (p=1.000 n=5) ²
ParserFaultTolerant/complex.lisp-4       10.75k ± ∞ ¹   10.75k ± ∞ ¹       ~ (p=1.000 n=5) ²
ParserFaultTolerant/diff.lisp-4          4.232k ± ∞ ¹   4.232k ± ∞ ¹       ~ (p=1.000 n=5) ²
ParserFaultTolerant/scheme-math.lisp-4   24.81k ± ∞ ¹   24.81k ± ∞ ¹       ~ (p=1.000 n=5)
ParserFaultTolerant/sicp.lisp-4          16.65k ± ∞ ¹   16.65k ± ∞ ¹       ~ (p=1.000 n=5) ²
ParserFaultTolerant/stream.lisp-4        14.63k ± ∞ ¹   14.63k ± ∞ ¹       ~ (p=1.000 n=5) ²
geomean                                  9.968k         9.968k        +0.00%
¹ need >= 6 samples for confidence interval at level 0.95
² all samples are equal

                                       │     base      │                 pr                  │
                                       │      B/s      │      B/s       vs base              │
Parser/approx.lisp-4                     8.373Mi ± ∞ ¹   8.545Mi ± ∞ ¹  +2.05% (p=0.008 n=5)
Parser/complex.lisp-4                    9.146Mi ± ∞ ¹   9.260Mi ± ∞ ¹       ~ (p=0.095 n=5)
Parser/diff.lisp-4                       7.200Mi ± ∞ ¹   7.486Mi ± ∞ ¹  +3.97% (p=0.008 n=5)
Parser/scheme-math.lisp-4                8.821Mi ± ∞ ¹   9.136Mi ± ∞ ¹       ~ (p=0.151 n=5)
Parser/sicp.lisp-4                       8.116Mi ± ∞ ¹   8.259Mi ± ∞ ¹       ~ (p=0.278 n=5)
Parser/stream.lisp-4                     8.898Mi ± ∞ ¹   9.127Mi ± ∞ ¹  +2.57% (p=0.008 n=5)
ParserFaultTolerant/approx.lisp-4        8.278Mi ± ∞ ¹   8.612Mi ± ∞ ¹  +4.03% (p=0.008 n=5)
ParserFaultTolerant/complex.lisp-4       8.945Mi ± ∞ ¹   9.356Mi ± ∞ ¹  +4.58% (p=0.008 n=5)
ParserFaultTolerant/diff.lisp-4          7.162Mi ± ∞ ¹   7.439Mi ± ∞ ¹  +3.86% (p=0.008 n=5)
ParserFaultTolerant/scheme-math.lisp-4   8.650Mi ± ∞ ¹   9.108Mi ± ∞ ¹  +5.29% (p=0.008 n=5)
ParserFaultTolerant/sicp.lisp-4          7.830Mi ± ∞ ¹   8.059Mi ± ∞ ¹       ~ (p=0.206 n=5)
ParserFaultTolerant/stream.lisp-4        8.659Mi ± ∞ ¹   8.850Mi ± ∞ ¹  +2.20% (p=0.032 n=5)
geomean                                  8.315Mi         8.578Mi        +3.17%
¹ need >= 6 samples for confidence interval at level 0.95

pkg: github.com/luthersystems/elps/parser/regexparser
                          │     base     │                 pr                 │
                          │    sec/op    │    sec/op     vs base              │
Parser/approx.lisp-4        1.402m ± ∞ ¹   1.380m ± ∞ ¹       ~ (p=0.151 n=5)
Parser/complex.lisp-4       4.107m ± ∞ ¹   4.080m ± ∞ ¹       ~ (p=0.421 n=5)
Parser/diff.lisp-4          1.828m ± ∞ ¹   1.750m ± ∞ ¹  -4.29% (p=0.008 n=5)
Parser/scheme-math.lisp-4   10.73m ± ∞ ¹   10.52m ± ∞ ¹       ~ (p=0.095 n=5)
Parser/sicp.lisp-4          7.184m ± ∞ ¹   7.161m ± ∞ ¹       ~ (p=0.222 n=5)
Parser/stream.lisp-4        6.053m ± ∞ ¹   5.875m ± ∞ ¹  -2.94% (p=0.008 n=5)
geomean                     4.123m         4.042m        -1.97%
¹ need >= 6 samples for confidence interval at level 0.95

                          │     base      │                 pr                  │
                          │     B/op      │     B/op       vs base              │
Parser/approx.lisp-4        861.6Ki ± ∞ ¹   860.8Ki ± ∞ ¹       ~ (p=0.222 n=5)
Parser/complex.lisp-4       2.395Mi ± ∞ ¹   2.395Mi ± ∞ ¹       ~ (p=0.690 n=5)
Parser/diff.lisp-4          1.122Mi ± ∞ ¹   1.121Mi ± ∞ ¹       ~ (p=0.548 n=5)
Parser/scheme-math.lisp-4   5.555Mi ± ∞ ¹   5.559Mi ± ∞ ¹       ~ (p=0.841 n=5)
Parser/sicp.lisp-4          4.047Mi ± ∞ ¹   4.028Mi ± ∞ ¹       ~ (p=0.421 n=5)
Parser/stream.lisp-4        3.328Mi ± ∞ ¹   3.334Mi ± ∞ ¹       ~ (p=0.841 n=5)
geomean                     2.352Mi         2.350Mi        -0.06%
¹ need >= 6 samples for confidence interval at level 0.95

                          │     base     │                 pr                 │
                          │  allocs/op   │  allocs/op    vs base              │
Parser/approx.lisp-4        12.08k ± ∞ ¹   12.07k ± ∞ ¹       ~ (p=0.206 n=5)
Parser/complex.lisp-4       35.82k ± ∞ ¹   35.82k ± ∞ ¹       ~ (p=1.000 n=5)
Parser/diff.lisp-4          16.38k ± ∞ ¹   16.38k ± ∞ ¹       ~ (p=1.000 n=5)
Parser/scheme-math.lisp-4   84.05k ± ∞ ¹   84.05k ± ∞ ¹       ~ (p=0.722 n=5)
Parser/sicp.lisp-4          60.87k ± ∞ ¹   60.87k ± ∞ ¹       ~ (p=0.413 n=5)
Parser/stream.lisp-4        49.91k ± ∞ ¹   49.91k ± ∞ ¹       ~ (p=1.000 n=5)
geomean                     34.91k         34.91k        -0.00%
¹ need >= 6 samples for confidence interval at level 0.95

                          │     base      │                 pr                  │
                          │      B/s      │      B/s       vs base              │
Parser/approx.lisp-4        1.183Mi ± ∞ ¹   1.202Mi ± ∞ ¹       ~ (p=0.167 n=5)
Parser/complex.lisp-4       1.230Mi ± ∞ ¹   1.240Mi ± ∞ ¹       ~ (p=0.444 n=5)
Parser/diff.lisp-4          947.3Ki ± ∞ ¹   986.3Ki ± ∞ ¹  +4.12% (p=0.008 n=5)
Parser/scheme-math.lisp-4   1.078Mi ± ∞ ¹   1.097Mi ± ∞ ¹       ~ (p=0.103 n=5)
Parser/sicp.lisp-4          986.3Ki ± ∞ ¹   986.3Ki ± ∞ ¹       ~ (p=0.278 n=5)
Parser/stream.lisp-4        1.125Mi ± ∞ ¹   1.154Mi ± ∞ ¹  +2.54% (p=0.008 n=5)
geomean                     1.078Mi         1.098Mi        +1.80%
¹ need >= 6 samples for confidence interval at level 0.95

Compared against cached baseline from the latest main push.

Add MacroExpander interface and EnvMacroExpander that uses a live LEnv
to expand user-defined macro calls during static analysis. When an
expander is available, the analyzer expands macro calls and walks the
expanded AST, resolving symbols introduced by the macro (e.g. lambda
parameters from quasiquote templates). Expansion failures fall back
gracefully to the existing opaque-macro behavior.

Wired into both the LSP server and MCP server — when WithEnv() provides
a runtime environment, macro expansion activates automatically.

Fixes #261
@sam-at-luther sam-at-luther changed the title fix: resolve analyzer false positives and MCP cross-file diagnostics fix: resolve analyzer false positives, MCP cross-file diagnostics, and macro expansion Apr 2, 2026
… macros

QA review findings addressed:
- Strengthen expander test to verify expanded AST structure (not just type)
- Add reference-based assertions for macro-expanded lambda params
- Add negative test: cross-file symbols flagged without workspace_root
- Add test: genuinely undefined symbols in expanded code get InsideMacroCall
- Add test: expansion error returns nil gracefully
- Add test: empty form returns nil
- Strengthen set-in-lambda test with global scope and reference assertions

Fix: analyzeCall now tries MacroExpander even when the macro isn't in the
analysis scope (e.g. macros from the embedder's registry or cross-file).
Previously, scope.Lookup had to succeed AND return SymMacro before expansion
was attempted.
[P0] Add not-a-macro cache to EnvMacroExpander — avoids repeated
env.Get() on every unresolved function call when expander is set.

[P1] Fix analyzeSet to look up in root scope only — scope.LookupInPackage
walked the full chain and could find let-local shadows, but PutGlobal
always targets the package scope.

[P1] Add sync.Mutex to EnvMacroExpander — MacroCall mutates shared
Runtime state (call stack, package pointer). Serializes concurrent
expansion calls from LSP handlers.

[P2] Add expansion depth limit (64) to prevent stack overflow on
self-expanding macros.

[P2] Fix copyright year to 2026 on new files. Add cross-reference
comment between isCondDefaultSymbol and lint's isCondDefault.
- Add depth limit test with self-expanding macro (loop-forever)
- Add reference assertions to nested macro expansion test
- Strengthen set-creates-global test with Kind and negative scope checks
- Rename PanicRecovery test to ExpansionErrorGracefulReturn (honest naming)
…notMacro cache test

TestAnalyze_MacroExpansion_WhenMacro previously used a defun parameter
(x) which resolves without expansion, making the test partially
redundant. Changed to use a with-default macro that introduces a let
binding — default-val only exists after expansion, proving the
expander actually contributes to resolution.

Added TestEnvMacroExpander_NotMacroCached to verify that the notMacro
negative-lookup cache on EnvMacroExpander is populated after the first
call and persists on subsequent calls.
…forms

Add three missing cases to the analysis package's `analyzeExpr` switch:

- `macrolet`: Creates a ScopeMacrolet scope, registers bindings as SymMacro,
  skips analyzing macro template bodies (like defmacro), and analyzes the
  body in the new scope. Fixes false "undefined symbol" for local macros.

- `qualified-symbol`: Skips resolving the argument since it is data (a name),
  not a variable reference. Fixes false positives on bare symbol arguments.

- `thread-first`/`thread-last`: Explicit pass-through to analyzeCall with
  comment explaining symbol resolution is unaffected by threading.
- Add nested macrolet test (inner scope sees outer + inner macros)
- Add qualified-symbol extra-args test (args beyond Cells[1] analyzed)
- Add thread-last undefined-symbol negative test (matches thread-first)
- Sharpen depth limit assertion to check for 'loop-forever' specifically
LintConfig was missing a MacroExpander field, so embedders that boot
a full environment (e.g. shirotester) had no way to enable macro
expansion through the CLI lint path. The MCP and LSP servers already
set it, but the lint package was the missing link.
Collect raw defmacro AST nodes during workspace prescan (MacroDefs
field on WorkspacePrescan). Add LoadWorkspaceMacros helper that evals
these into a runtime env so EnvMacroExpander can expand them.

Wire through all three paths:
- LintConfig.Env: embedders just pass their env, BuildAnalysisConfig
  handles LoadWorkspaceMacros + expander creation automatically
- LSP buildWorkspaceIndex: loads workspace macros into s.env
- MCP buildWorkspaceState: loads workspace macros into s.env

Embedder usage is now one line: set Env on LintConfig and workspace-
defined macros like def-case-verification expand correctly.
[P1] Log LoadWorkspaceMacros errors in all callers:
- LSP: window/logMessage warnings
- MCP: slog.Warn
- lint BuildAnalysisConfig: slog.Warn

[P1] Add Reset() method on EnvMacroExpander to clear notMacro cache.
Document cache lifetime assumption.

[P2] Fix package context for workspace macro loading. MacroDef struct
pairs each defmacro AST with its source package (from in-package
tracking). LoadWorkspaceMacros calls env.InPackage before each eval
so macros register in the correct package scope.

[P2] Fix misleading cond comment — runtime only special-cases 'else';
'true' works because it evaluates to a truthy value, not because it's
a keyword.
- Add TestEnvMacroExpander_Reset_ClearsCache: proves stale cache
  prevents expansion, Reset() fixes it (the key mutation-resistant test)
- Add TestLoadWorkspaceMacros_MultiplePackages: two macros in different
  packages in one call, verifies each registers in its own package
- Add TestPrescanWorkspace_MacroDefs: integration test verifying prescan
  collects defmacro ASTs with correct package context from in-package
- Rename ErrorIncludesMacroName to ErrorWithNonSymbolName (honest name,
  verifies <unknown> appears for non-symbol macro names)
Workspace packages (e.g. "acre") don't exist in the boot env — they're
defined by workspace code. LoadWorkspaceMacros now calls DefinePackage
and UsePackage(lang) before InPackage, so workspace-only packages are
created on demand with lang builtins imported.

Added TestLoadWorkspaceMacros_PackageAutoCreated to verify this works
with a package that is NOT pre-defined in the env.
…cros

Workspace macro bodies may reference functions from imported packages
(e.g. flatten from utils). LoadWorkspaceMacros now accepts packageImports
from prescan and applies use-package for each workspace package's imports
before eval'ing its macros. Also ensures imported packages are created
via DefinePackage (they may be workspace-only packages).

The preparation phase runs once per unique package across all macro defs,
then each macro is eval'd in its prepared package context.
Instead of manually reconstructing package state from MacroDef structs
and packageImports, collect all preamble forms (in-package, use-package,
export, defmacro) from workspace files in source order and eval them
sequentially. This mirrors the runtime's (load) behavior — package
context, imports, and macro definitions build up naturally.

Changes:
- WorkspacePrescan.Preamble replaces MacroDefs (flat []*LVal in order)
- LoadWorkspaceMacros takes []*LVal, saves/restores env package
- evalPreambleForm auto-creates workspace packages + imports lang
- Drop MacroDef struct, preparePackage, packageImports parameter
- All callers simplified: just pass prescan.Preamble
- Tests rewritten to use preamble sequences (in-package + defmacro)
- New tests: PackageRestored, UsePackageImports
ExpandMacro now accepts a pkg parameter — the analyzer's currentPkg.
The env is temporarily switched to this package before env.Get so that
unqualified symbol resolution matches the file's package scope, the
same way the runtime resolves symbols during eval.

This fixes workspace macros defined in non-default packages (e.g. acre)
not being found by the expander, which was in the user package after
LoadWorkspaceMacros restored it.

The notMacro cache key is now package-qualified (pkg\x00name) since
the same symbol name may be a macro in one package but not another.
Macros that call workspace-defined functions (e.g. flatten) failed
during expansion because defun forms weren't loaded into the env.
defun is safe to eval — it captures a lambda without evaluating the
body, creating only a package-scope binding with no side effects.

Added end-to-end test: workspace defun + defmacro that calls it,
verified expansion resolves lambda params correctly.
At runtime, (load-file "template.lisp") inherits the caller's package.
Files without in-package should have their definitions placed in the
workspace's DefaultPackage (from main.lisp), not "user".

PrescanWorkspace now prepends a synthetic (in-package 'defaultPkg)
to bare files' preambles before aggregation, so LoadWorkspaceMacros
places their defun/defmacro forms in the correct package.

Also confirmed recursive defun works correctly in preamble loading —
defun binds the name before the body executes, so self-references
resolve at call time. Added test for both cases.
At runtime, (load-file "bare.lisp") inherits the caller's package at
the call site. If main.lisp switches packages between load-file calls,
each bare file gets a different package.

Use the prescan's loadTree (which already tracks per-file package
context from load-file call sites) instead of the global defaultPkg.
Falls back to defaultPkg for files not in the load tree.

Added tests: BareFileInheritsLoadContext (single package) and
BareFileInheritsPackageSwitch (package changes between load-file calls).
Preamble forms were aggregated in filepath.Walk order (alphabetical),
but the runtime loads files in the order specified by load-file calls
in main.lisp. This means a helper function in a.lisp could be eval'd
after the macro in b.lisp that depends on it.

buildLoadTree now returns the DFS visit order alongside the package
map. PrescanWorkspace uses this to emit preamble forms in load order:
first files from the load tree (in DFS traversal order), then files
not in the load tree. This ensures macros see all definitions in the
same order as the runtime.
- Add set to preamble forms — bare template files using (set 'name ...)
  now load into the correct package, matching runtime behavior
- Fix stale LoadWorkspaceMacros docstring to list all 6 preamble forms
- Fix stale scanFileFull preamble comment
- Add TestLoadWorkspaceMacros_LoadOrderMatters — proves DFS order matters
  (helpers loaded before macros that call them)
- Add TestLoadWorkspaceMacros_SetInBareFile — proves set globals from
  bare template files land in the inherited package
- Update TestPrescanWorkspace_Preamble to verify set is collected
The existing DefaultPackage remapping already handles set definitions
in bare template files — they get remapped from user to the load-tree's
package. Added end-to-end test: prescan a workspace with a bare
template.lisp containing (set 'default-template ...), then analyze a
file in myapp that references default-template. Confirms it resolves.
…file

The shouldRemap guard required loadTree to be non-empty, which meant
bare files not referenced by load-file in main.lisp kept their
definitions in user instead of DefaultPackage. This caused set globals
in template files to be invisible to files in the workspace's main
package.

Two fixes:
- Remove len(loadTree) > 0 guard — remap whenever DefaultPackage is set
- Bare files not in the load tree fall back to DefaultPackage instead
  of being skipped entirely

Added test: bare template.lisp with set, NOT loaded via load-file,
verifies it remaps to myapp and resolves cross-file.
…tion

Expanded the shouldRemap block comment to explain why DefaultPackage
is the workspace's "real" default package, and the three rules:
(1) bare files in load tree → load-tree package context
(2) bare files NOT in load tree → DefaultPackage (e.g. template files
    loaded conditionally inside function bodies)
(3) non-bare files → only defs before first in-package
@sam-at-luther sam-at-luther changed the title fix: resolve analyzer false positives, MCP cross-file diagnostics, and macro expansion fix: analyzer false positives, MCP cross-file diagnostics, macro expansion, and semantic audit Apr 3, 2026
@sam-at-luther sam-at-luther merged commit 6d794d7 into main Apr 3, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant