fix(packages): generalize go list diagnostic preference for Go sources - #2265
fix(packages): generalize go list diagnostic preference for Go sources#2265MeteorsLiu wants to merge 4 commits into
Conversation
There was a problem hiding this comment.
Review summary
Rewrites the go/types diagnostic name X not exported by package … into the gc-style undefined: pkg.X so the go1.26 errorcheck case runtime.go passes, and drops the corresponding xfail.yaml entry. The mechanism is sound: keying by err.Pos (== sel.Sel.Pos()) is exact and collision-free, closure-capture ordering is safe (the map is populated before NewChecker(...).Files(...) runs), and the message-prefix guard makes an unintended rewrite effectively impossible since that message is package-exclusive in go/types.
One correctness concern worth addressing (inline), plus a minor efficiency note below.
Minor: eager, unconditional AST walk on the per-package load path
internal/packages/load.go (~line 373): the ast.Inspect walk and map[token.Pos]string are built for every package on every load, even when the package type-checks cleanly and no message ever needs rewriting. It also records all ident-qualified selectors (v.Field, x.Method, …), not just import-qualified ones, so the map is larger than the single use case requires. This is pure overhead for the common (no-error) case. Since it runs once per package before type-checking, the cost is likely modest relative to type-checking itself — but building the map lazily inside the types.Error branch (only when the target message is actually seen), or restricting insertion to import-qualified selectors, would eliminate it. Non-blocking.
There was a problem hiding this comment.
Review: link-time DCE for ABI method slots + per-package frontend options
Thanks for this — the DCE analysis is well-structured, the cl.Options refactor that replaces the process-wide Enable* globals is clean and consistently threaded, the sync.Once around llssa.Initialize is safe (init flags are a compile-time constant), and the RewriteMainPrefix removal left no dangling references. Comprehensive tests accompany the new packages.
Findings below, ordered by severity. The correctness-class items (a live method being pruned, or a panic on valid input) matter most because they surface only under -deadcodedrop.
Correctness / robustness
-
internal/dcepass/dcepass.go:147—panicon an unsupported global value is reachable.cloneGlobalValuehandles onlyIsAFunctionandIsAGlobalVariable; any otherGlobalValue(GlobalAlias,GlobalIFunc) hitspanic("dcepass: unsupported global value"). llgo does emit aliases (ssa/decl.go:148llvm.AddAlias), so if a pruned ABI type descriptor ever references one — directly or through a cloned field — the whole build aborts. Prefer rebinding aliases by name to a dst declaration (as functions are handled) or keeping the value, rather than panicking. (inline) -
internal/dcepass/dcepass.go:114-127—cloneConstdoes not recurse intoConstantExpr. It recurses only throughConstantStruct; aConstantExpr(GEP / bitcast / inttoptr, all produced elsewhere in codegen) falls through toreturn v, leaving the dst-module initializer pointing back into the source module's value graph. Latent today under opaque pointers where the observed fields are plain global refs, but a typed-pointer build or a codegen change that materializes a field as a constant-expression would produce a cross-module reference. Consider recursing throughConstantExpr/ConstantArrayoperands, or explicitly asserting the field shapes you support. (inline) -
internal/dcepass/dcepass.go:68-76— method-array element read as a 4-operand struct without validation.methodArrayvalidates the element type has 4 fields, but the drop branch unconditionally readsorig.Operand(0..3)(andOperand(2/3).Name()when verbose) on each element value. AConstantAggregateZeroelement hasOperandsCount()==0, soOperand(i)would be out of range. Safe today because llgo always emits non-null ifn/tfn, but it is an unguarded assumption on IR shape. (inline) -
internal/deadcode/analyze.go:46-52/internal/build/build.go(dceEntryRootCandidates) — missing entry roots are silently dropped.Analyzeskips any root name absent from the summary. If a mandatory entry root such asmain.mainever fails to resolve (e.g. a metadata-collection regression), the pass silently treats far more slots as dead and prunes live code with no diagnostic. A cheap guard/verbose-warning whenmain.main/main.initresolve to nothing would turn a silent miscompile into a visible error.
Performance
-
internal/deadcode/analyze.go:279-284—popWorkis O(n) per dequeue → O(n²) flood.copy(d.workQueue, d.workQueue[1:])shifts the entire remaining slice on every pop, so draining N enqueued symbols costs O(N²) over a whole-program analysis. The loop runs to a fixpoint, so FIFO vs LIFO is irrelevant to correctness — popping from the tail (d.workQueue[len-1]; d.workQueue = d.workQueue[:len-1]) is O(1), or use a head index. Single highest-value change in the analysis. (inline) -
internal/meta/global.go:264-272—OrdinaryEdgesgrows viaappendwith no pre-size on the hot flood path, unlike the structurally identicalTypeChildren(line 282) which usesmake([]Symbol, len(local)). Pre-size tolen(edges)for consistency. Separately,markUsedInIfacere-enqueues an already-reachablesymbol, causingfloodto re-walk its ordinary edges + demands purely to reach theusedInIfacebranch — a separate "already flooded" guard would avoid that duplicate work.
Minor / docs
-
internal/dcepass/dcepass.go:20-21— doc says "clears IFn/TFn" but the code replaces both with theunreachableMethodstub (the slots stay populated, pointing at a trap). "redirects IFn/TFn to an unreachable stub" describes it accurately. (inline) -
internal/dcepass/dcepass.go:53— redundant map write.emitTypeOverridesetse.values[srcType] = dstType, butensureOverrideGlobal(called the line above) already assigns the same key. The line-53 write is dead. -
cl/compile.go:2117-2119— comment steers multi-package drivers to a now-deprecated function. The block comment says multi-package drivers "useNewPackageExWithEmbed", which this PR marksDeprecated: use NewPackageExWithEmbedMetaOptions. Point the guidance at the new entry point.
No blocking issues; the panic-robustness and the O(n²) dequeue are the two I'd most want addressed before this runs on large programs.
Findings without inline locations
internal/dcepass/dcepass.go:147:cloneGlobalValuehandles only functions and global variables; any otherGlobalValue(GlobalAlias,GlobalIFunc) reaches thispanic. llgo does emit aliases (ssa/decl.go:148), so if a pruned ABI type descriptor references one — directly or via a cloned field — the build aborts on valid input. Consider rebinding aliases by name to a dst declaration (like functions above) or falling back to keeping the value, rather than panicking.internal/dcepass/dcepass.go:120:cloneConstrecurses only throughConstantStruct. AConstantExpr(GEP/bitcast/inttoptr) falls through toreturn vhere, leaving the dst-module initializer referencing the source module's value graph. Latent under opaque pointers (observed fields are plain global refs), but a typed-pointer build or a codegen change that materializes a field as a constant-expression would produce a cross-module reference. Consider recursing throughConstantExpr/ConstantArrayoperands, or asserting the shapes you support.internal/dcepass/dcepass.go:70: The drop branch unconditionally readsorig.Operand(0..3)(andOperand(2/3).Name()when verbose).methodArrayvalidated the element type has 4 fields, but not that each element value is a materialized 4-operandConstantStruct— aConstantAggregateZeroelement hasOperandsCount()==0and would fault here. Safe today since ifn/tfn are always non-null, but it's an unguarded assumption on IR shape.internal/deadcode/analyze.go:283:copy(d.workQueue, d.workQueue[1:])shifts the whole remaining slice on every pop, makingflood()O(n²) over a whole-program analysis. The loop runs to a fixpoint so ordering is irrelevant — popping from the tail is O(1):internal/dcepass/dcepass.go:20: Nit: "clears IFn/TFn" — the code replaces both fields with theunreachableMethodstub (slots stay populated, pointing at a trap) rather than clearing them. "redirects IFn/TFn to an unreachable stub" would be accurate.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Review (corrected): align unexported-package-member diagnostics
⚠️ My earlier review on this PR reviewed the wrong diff — it compared the head branch against the currentmaintip and so pulled in dozens of already-merged, unrelated commits (the DCE feature, thecl.Optionsrefactor, etc.). Please disregard that review. This PR's real base is2310ff87and it changes only 3 files. This review supersedes it.
The actual change makes llgo report undefined: alias.hidden (matching gc) instead of name hidden not exported by package ... when source references an unexported member of another package, and removes the now-passing runtime.go go1.26 errorcheck case from xfail.yaml. It's focused and well-tested (load_test.go asserts both the new message and the absence of the old one).
Only minor observations — nothing blocking.
Observations
-
internal/packages/load.go:291— the rewrite is coupled to go/types' exact error wording. The trigger isstrings.HasPrefix(err.Msg, "name "+name+" not exported by package "). If a future Go toolchain rephrases that diagnostic, the rewrite silently stops firing and the original (non-gc) message resurfaces. The failure mode is graceful (you fall back to the upstream message, and the goroot errorcheck test would flag the regression), so this is acceptable — but a brief comment noting the dependency on the go/types string would help the next reader understand why the match is so literal. -
internal/packages/load.go:383-385—packageSelectorsrecords every ident-qualified selector, not just package qualifiers (x.field,v.Method, andpkg.Nameall land in the map). This is harmless because the rewrite at line 289-293 only activates on thename X not exported by packagemessage, which go/types emits exclusively for package-qualified access to an unexported identifier — so a non-package qualifier can never reach the rewrite. Worth being aware of if that message guard is ever loosened; today it is safe by construction. -
internal/packages/load.go:260,289— nil-map read is safe.packageSelectorsis declared nil at line 260 and only populated at line 376, after parse/driver errors but before type-checking. The line-289 lookup runs only fortypes.Error(emitted during type-checking, after population), and a read from a nil map returns the zero value regardless — so there is no ordering hazard or panic risk. Noting it only to confirm it was checked.
Overall this looks correct and appropriately scoped. No changes required.
LLGo baseline benchmarks
Program measurements
Core language and compiler benchmarks
Compared with |
5e23576 to
eef7e54
Compare
eef7e54 to
b94d36f
Compare
There was a problem hiding this comment.
Review summary
This PR rewrites the go/types diagnostic name X not exported by package … into the gc-style undefined: qualifier.X, adds a regression test, and drops the now-reproduced go1.26 errorcheck runtime.go entry from notapplicable.yaml. The approach is sound and correctly gated: the rewrite runs only for locally generated types.Errors (the hasCompilerSyntaxError/hasCompilerDiagnostics early-returns still win), and the packageSelectors map is only built after the NeedTypes == 0 return, so it is skipped when types aren't requested. Cross-file token.Pos collisions are not possible since both the map key (sel.Sel.Pos()) and the lookup key (err.Pos) come from the shared ld.Fset.
A few points worth addressing — see inline comments. None are blocking.
Additional note (no reliable inline anchor):
- Overlap with the test harness normalizer.
test/goroot/runner_test.goalready hasnormalizeGoTypesDiagnosticMessage, which reshapes several go/types diagnostics for GOROOT errorcheck matching and is explicitly documented as affecting only the test harness. This PR instead moves an equivalent transform into the production loader path (lpkg.Errors), which is a user-facing behavior change. Please confirm the two paths don't double-transform this message and that the placement (production vs. harness) is intentional and consistent with the existing convention.
Findings without inline locations
internal/packages/load.go:405:sel.Xis only checked to be an*ast.Ident; it is not verified to be a package qualifier. This records everyident.memberselector (local vars, receivers, params, field access), not just package-qualified references. Correctness is preserved today only because the rewrite is additionally gated on the"name X not exported by package "message prefix, which go/types emits solely for package-member access. It's worth a short comment stating that the message-prefix check — not the AST classification — is the real filter, so a future reader (or a future go/types wording that reused similar phrasing) doesn't get a misleadingundefined: x.fieldrewrite for a struct-field selector.internal/packages/load.go:311: This hard-codes the exact go/types wording"name %s not exported by package %s", which is a compiler-internal diagnostic string with no stability guarantee and has changed across Go versions. If the wording changes, the rewrite silently becomes a no-op (graceful, but invisible) and only the new test would catch it. Please add a comment anchoring this to the go/types version whose wording it matches — matching the version-coupling comments already present elsewhere in this file (e.g. the go1.16 /go listskew note) and theintentionally affects only the test harnessnote inrunner_test.go.internal/packages/load.go:310:nameis re-derived by splitting the stored"qualifier.Sel"string (selector[strings.LastIndexByte(selector, '.')+1:]) that was itself built by joiningqualifier.Name + "." + sel.Sel.Name. That's a join-then-resplit round-trip. Storing a small struct{qualifier, sel string}(or two maps) in the inspect loop would drop the re-parse and make intent clearer. Safe today since identifiers can't contain., but needlessly indirect.internal/packages/load.go:396: The full-AST walk and map allocation run unconditionally for every type-checked package, on the common no-error path, purely to service a rare error-message rewrite.loadPackageExruns per package across the whole transitive import graph (concurrent fan-out), so this adds a second whole-program AST traversal plus a map holding a large fraction of each package's selectors, almost always thrown away unused. Consider building the map lazily on the first"not exported by package "error, or resolving just the single failingerr.Poson demand. Not blocking, but the eager index is speculative work on the hot path.
b94d36f to
b13b29c
Compare
| continue | ||
| } | ||
| for _, compiledGoFile := range compiledGoFiles { | ||
| if sameDiagnosticFile(diagnosticFile, compiledGoFile) { |
There was a problem hiding this comment.
NOTE: BAD performance
|
/review |
|
@MeteorsLiu Thanks for the trigger request. This repository currently enforces member-only triggers, and your account does not have repository membership access, so Fenno cannot run for this request. To proceed, you can:
|
1 similar comment
|
@MeteorsLiu Thanks for the trigger request. This repository currently enforces member-only triggers, and your account does not have repository membership access, so Fenno cannot run for this request. To proceed, you can:
|
There was a problem hiding this comment.
Review Summary
This replaces the fragile substring-based hasCompilerSyntaxError check with a positional match (hasGoSourceListDiagnostics + sameDiagnosticFile) that confirms a go list ListError diagnostic actually targets one of the package's CompiledGoFiles. The design is a clear improvement — anchoring "authoritative go list diagnostic" to the compiled Go source correctly avoids false positives from forwarded child-tool failures (e.g. an asm.s error being attributed to an asm.go package). Doc comments are thorough and the table tests cover absolute/relative paths, cgo output, unpositioned errors, and non-list errors.
Findings are all low-severity; none are blocking. The most substantive is the package-wide granularity of error suppression (see inline). Documentation accuracy: no issues found.
Additional notes (no reliable inline anchor)
internal/packages/load.go— emptycompiledGoFilesis untested. WhencompiledGoFilesis empty,hasGoSourceListDiagnosticsreturnsfalseand follow-on errors are (correctly) not suppressed. This safe default is not exercised byTestHasGoSourceListDiagnostics; consider adding a case to lock it in against regressions.internal/packages/load.go— format-fragility, fail-open (informational). Matching depends on the exact"# "header prefix and the": "separator instrings.Cut. If a future Go toolchain changes go list's block header or spacing, detection silently returnsfalseand suppression stops — real errors would then be shown rather than hidden, so this degrades gracefully. Worth a test pinning the expected go list format.
|
@MeteorsLiu Thanks for the trigger request. This repository currently enforces member-only triggers, and your account does not have repository membership access, so Fenno cannot run for this request. To proceed, you can:
|
There was a problem hiding this comment.
Review summary
This is a solid, well-tested refactor. It replaces the narrow ": syntax error: " substring match with a more general check (hasGoSourceListDiagnostics) that treats a positioned go list diagnostic tied to one of the package's compiled Go files as authoritative, suppressing local parser/type follow-ons. The helpers are small, single-purpose, and covered by TestHasGoSourceListDiagnostics (absolute/relative/different-file/cgo/unpositioned/metadata/non-list cases). The removed test/goroot xfail/notapplicable entries are consistent with the broadened suppression and leave no dangling references. Operator precedence and filepath.IsAbs handling in sameDiagnosticFile are correct.
One substantive robustness finding is left inline. The rest are minor/informational and non-blocking:
- Behavior broadening (
load.go:263,:283/:296): The newhasCompilerDiagnosticssuppresses bothscanner.ErrorListandtypes.Errorfollow-ons whenevergo listhas any positioned diagnostic on any compiled Go file in the package — not just syntax errors and not just the specific file. Previously only syntax errors triggered suppression. This appears intentional and matches the removed xfail entries, but it is a genuine widening; worth a one-line comment on thehasCompilerDiagnosticsassignment documenting the intended granularity. sameDiagnosticFilerelative suffix (load.go:664-672): A relative diagnostic path likeexample/load.gomatches any absolute compiled path ending in/example/load.go. Fine in the normal flow (the relative path comes from the samego listinvocation), but combined with the inline finding it slightly widens the accidental-match surface.diagnosticFileLine(load.go:608-624): Accepts a column-lessfile:Nas a valid positioned diagnostic and, for malformed middle segments (e.g.load.go:2a:3), silently reinterprets a column as a line. Not reachable from well-formedgo listoutput; informational only.
No performance or documentation issues found (nested loops are gated to the compile-failure path and bounded by small per-package inputs; doc-comment examples match actual behavior).
|
@cpunion request for review, please. |
Extends #2274's authoritative syntax diagnostic handling to go list build diagnostics that originate from a package's compiled Go files.
The implementation includes:
ListErrordiagnostic blocks whose source path matches the package'sCompiledGoFiles.This aligns LLGo's frontend errors with cmd/compile while limiting the behavior to verified Go source diagnostics.