diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d8664f6d2..2c96d85cd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,9 +17,6 @@ jobs: name: Lint runs-on: ubuntu-latest timeout-minutes: 15 - env: - GOCACHE: ${{ runner.temp }}/go-build - GOLANGCI_LINT_CACHE: ${{ runner.temp }}/golangci-lint steps: - name: Check out code uses: actions/checkout@v6 @@ -30,8 +27,18 @@ jobs: go-version: "1.23.x" cache: true + - name: Verify generated parser + env: + GOCACHE: ${{ runner.temp }}/go-build + run: | + go generate ./compiler/parse + git diff --exit-code -- compiler/parse/parser.go + - name: Run golangci-lint uses: golangci/golangci-lint-action@v9.2.0 + env: + GOCACHE: ${{ runner.temp }}/go-build + GOLANGCI_LINT_CACHE: ${{ runner.temp }}/golangci-lint with: version: v2.8.0 only-new-issues: false @@ -45,8 +52,6 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest] - env: - GOCACHE: ${{ runner.temp }}/go-build steps: - name: Check out code uses: actions/checkout@v6 @@ -58,14 +63,14 @@ jobs: cache: true - name: Run tests + env: + GOCACHE: ${{ runner.temp }}/go-build run: go test -timeout 120s ./... race: name: Race Tests runs-on: ubuntu-latest timeout-minutes: 40 - env: - GOCACHE: ${{ runner.temp }}/go-build steps: - name: Check out code uses: actions/checkout@v6 @@ -78,6 +83,7 @@ jobs: - name: Run race tests env: + GOCACHE: ${{ runner.temp }}/go-build GORACE: "halt_on_error=1" run: go test -race -timeout 120s ./... @@ -85,8 +91,6 @@ jobs: name: Fuzz runs-on: ubuntu-latest timeout-minutes: 20 - env: - GOCACHE: ${{ runner.temp }}/go-build steps: - name: Check out code uses: actions/checkout@v6 @@ -98,20 +102,24 @@ jobs: cache: true - name: Fuzz type decode + env: + GOCACHE: ${{ runner.temp }}/go-build run: go test -fuzz=FuzzTypeDecodeToValidation -fuzztime=60s -timeout=120s - name: Fuzz Lua source types + env: + GOCACHE: ${{ runner.temp }}/go-build run: go test -fuzz=FuzzLuaTypeValidation -fuzztime=60s -timeout=120s - name: Fuzz Lua with manifest + env: + GOCACHE: ${{ runner.temp }}/go-build run: go test -fuzz=FuzzLuaWithManifestTypes -fuzztime=60s -timeout=120s bench: name: Benchmarks runs-on: ubuntu-latest timeout-minutes: 15 - env: - GOCACHE: ${{ runner.temp }}/go-build steps: - name: Check out code uses: actions/checkout@v6 @@ -123,6 +131,8 @@ jobs: cache: true - name: Run benchmarks + env: + GOCACHE: ${{ runner.temp }}/go-build run: go test -bench="Benchmark(Validate|Is)" -benchmem -count=1 -timeout=60s | tee bench.txt - name: Upload benchmark results diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 000000000..2f2e3abaf --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,16 @@ +version: "2" + +linters: + exclusions: + warn-unused: true + rules: + # These test assertion helpers return diagnostic.Diagnostic so callers + # can inspect the selected value. Diagnostic also implements error for + # production consumers, which makes ignored assertion results look like + # unchecked errors to errcheck even though the helpers fail through T. + - linters: + - errcheck + source: '^\s*requireDiagnostic(Code)?\(' + - linters: + - errcheck + source: '^\s*requireDiagnosticShape\(' diff --git a/analysis/architecture/SCHEMA_VERSIONS.md b/analysis/architecture/SCHEMA_VERSIONS.md index 48db18fdc..bb088d00b 100644 --- a/analysis/architecture/SCHEMA_VERSIONS.md +++ b/analysis/architecture/SCHEMA_VERSIONS.md @@ -6,6 +6,7 @@ closed or negotiate before emitting a surface newer than the consumer supports. | Surface | Constant | Current | Covers | Bump when | | --- | --- | --- | --- | --- | +| Canonical manifest wire | `manifest.WireFormatVersion` | v1 | Top-level JSON manifest envelope and its type, signature, effect, typestate, callback-phase, and ambient-global fields. | A wire field is removed, renamed, reinterpreted, or made newly required; additive optional fields still require a compatibility review. | | Checker embedding | `embedding.EmbeddingSchemaVersion` | v1 | Stable document/source/resolution identity DTOs: `DocumentID`, digest-bound source locations and snapshots, unit plans/imports, resolution snapshots, `SolveSeq`, and `BodyInputDigest`. | An exported embedding DTO field, initial document scheme, or its identity/versioning semantics changes. | | Judgment IR | `judgment.JIRSchemaVersion` | v11 | Judgment code registry and exported judgment record shape. | A judgment code, code metadata, or exported judgment/evidence/subject field shape changes. | | Signature escape vocabulary | `signature.EscapeVocabVersion` | v1 | Signature `EscapeKind` labels and audited ownership capability labels synced with arena CallArgEscape/Ownership. | An escape/ownership label is added, removed, renamed, or changes boundary meaning. Requires joint cross-repo signoff per fence #1425. | @@ -32,6 +33,14 @@ constant + journal a D-entry`. ## Journal +- D21: Registered canonical manifest JSON wire v1. The decoder continues to + accept the short-lived unversioned canonical JSON shape as v1, rejects future + versions closed, and identifies the pre-abstract-interpreter binary `INAM` + format with a typed migration error. `INAM` embeds removed type/effect + domains and cannot be converted losslessly; cache owners must treat that + error as a cache miss and rebuild the manifest from source. Standalone legacy + binary type payloads use the equivalent `types/io.ErrLegacyTypeWire` signal. + - D20: Post-union repin: Judgment IR v11 is the single consumer contract for the declared typestate-requirement codes, `BodyInputDigest` and digest-bound `SourceLocation` spans, and exported repair descriptors. Artifact debug maps diff --git a/analysis/architecture/promptmap_meta_audit_test.go b/analysis/architecture/promptmap_meta_audit_test.go index c8bfe3734..75736c316 100644 --- a/analysis/architecture/promptmap_meta_audit_test.go +++ b/analysis/architecture/promptmap_meta_audit_test.go @@ -13,7 +13,7 @@ func TestPromptmapMetaAuditMatrixIsPresentAndWellFormed(t *testing.T) { if err != nil { t.Fatalf("open promptmap meta audit matrix: %v", err) } - defer f.Close() + t.Cleanup(func() { _ = f.Close() }) reader := csv.NewReader(f) rows, err := reader.ReadAll() diff --git a/analysis/check/body/result_version.go b/analysis/check/body/result_version.go index 6cec29dc1..7dad6f43c 100644 --- a/analysis/check/body/result_version.go +++ b/analysis/check/body/result_version.go @@ -144,10 +144,6 @@ func (w *bodyDigestWriter) writeType(label string, t typ.Type) { w.writeString(label+":display", t.String()) } -func (w *bodyDigestWriter) writeProduct(label string, value product.Value) { - w.writeUint64(label, w.stableProductHash(value)) -} - func (w *bodyDigestWriter) stableProductHash(value product.Value) uint64 { h := internalhash.NewWriter() _, _ = h.WriteString("product:") diff --git a/analysis/check/diagnostics/advice.go b/analysis/check/diagnostics/advice.go index f21f88e57..7e05414b4 100644 --- a/analysis/check/diagnostics/advice.go +++ b/analysis/check/diagnostics/advice.go @@ -20,7 +20,8 @@ func renderAdviceJudgmentWithPolicy(ctx judgmentRenderContext, item judgment.Jud return diagnostic.Diagnostic{}, false } return diagnostic.New(diagnostic.DiagnosticSpec{ - File: item.Spans[0].File, + Location: item.Spans[0].Location, + File: item.Spans[0].DisplayFile(), Span: span, Code: code, Severity: severity, diff --git a/analysis/check/diagnostics/call_argument_proof_context.go b/analysis/check/diagnostics/call_argument_proof_context.go index afc0a49d7..86932fed9 100644 --- a/analysis/check/diagnostics/call_argument_proof_context.go +++ b/analysis/check/diagnostics/call_argument_proof_context.go @@ -46,7 +46,8 @@ func (ProofContext) DirectCallArgument(item judgment.Judgment, primary diagnosti Help: help, Evidence: directCallArgumentJudgmentEvidence(display, item, proof, wording, primary), Labels: []diagnostic.Label{{ - File: item.Spans[0].File, + Location: item.Spans[0].Location, + File: item.Spans[0].DisplayFile(), Span: primary, Message: labelArgumentValue, Placement: diagnostic.LabelPlacementBelow, diff --git a/analysis/check/diagnostics/discriminated_union_exhaustiveness_chain_render.go b/analysis/check/diagnostics/discriminated_union_exhaustiveness_chain_render.go index f0c589c07..90195c3fc 100644 --- a/analysis/check/diagnostics/discriminated_union_exhaustiveness_chain_render.go +++ b/analysis/check/diagnostics/discriminated_union_exhaustiveness_chain_render.go @@ -20,7 +20,8 @@ func renderDiscriminatedUnionJudgmentWithPolicy(ctx judgmentRenderContext, item return diagnostic.Diagnostic{}, false } return diagnostic.New(diagnostic.DiagnosticSpec{ - File: item.Spans[0].File, + Location: item.Spans[0].Location, + File: item.Spans[0].DisplayFile(), Span: span, Code: code, Severity: severity, diff --git a/analysis/check/diagnostics/discriminated_union_exhaustiveness_optional_render.go b/analysis/check/diagnostics/discriminated_union_exhaustiveness_optional_render.go index c20acd670..25bd49e6c 100644 --- a/analysis/check/diagnostics/discriminated_union_exhaustiveness_optional_render.go +++ b/analysis/check/diagnostics/discriminated_union_exhaustiveness_optional_render.go @@ -20,7 +20,8 @@ func renderOptionalJudgmentWithPolicy(ctx judgmentRenderContext, item judgment.J return diagnostic.Diagnostic{}, false } return diagnostic.New(diagnostic.DiagnosticSpec{ - File: item.Spans[0].File, + Location: item.Spans[0].Location, + File: item.Spans[0].DisplayFile(), Span: span, Code: code, Severity: severity, diff --git a/analysis/check/diagnostics/discriminated_union_exhaustiveness_registration_render.go b/analysis/check/diagnostics/discriminated_union_exhaustiveness_registration_render.go index 33569e007..682bec0a8 100644 --- a/analysis/check/diagnostics/discriminated_union_exhaustiveness_registration_render.go +++ b/analysis/check/diagnostics/discriminated_union_exhaustiveness_registration_render.go @@ -20,7 +20,8 @@ func renderRegistrationJudgmentWithPolicy(ctx judgmentRenderContext, item judgme return diagnostic.Diagnostic{}, false } return diagnostic.New(diagnostic.DiagnosticSpec{ - File: item.Spans[0].File, + Location: item.Spans[0].Location, + File: item.Spans[0].DisplayFile(), Span: primary, Code: code, Severity: severity, diff --git a/analysis/check/diagnostics/discriminated_union_exhaustiveness_result_shape_render.go b/analysis/check/diagnostics/discriminated_union_exhaustiveness_result_shape_render.go index 094c6296f..a83976908 100644 --- a/analysis/check/diagnostics/discriminated_union_exhaustiveness_result_shape_render.go +++ b/analysis/check/diagnostics/discriminated_union_exhaustiveness_result_shape_render.go @@ -20,7 +20,8 @@ func renderResultShapeJudgmentWithPolicy(ctx judgmentRenderContext, item judgmen return diagnostic.Diagnostic{}, false } return diagnostic.New(diagnostic.DiagnosticSpec{ - File: item.Spans[0].File, + Location: item.Spans[0].Location, + File: item.Spans[0].DisplayFile(), Span: span, Code: code, Severity: severity, diff --git a/analysis/check/diagnostics/discriminated_union_exhaustiveness_table_dispatch_render.go b/analysis/check/diagnostics/discriminated_union_exhaustiveness_table_dispatch_render.go index 88a0e32dc..5a4fe196a 100644 --- a/analysis/check/diagnostics/discriminated_union_exhaustiveness_table_dispatch_render.go +++ b/analysis/check/diagnostics/discriminated_union_exhaustiveness_table_dispatch_render.go @@ -20,7 +20,8 @@ func renderTableDispatchJudgmentWithPolicy(ctx judgmentRenderContext, item judgm return diagnostic.Diagnostic{}, false } return diagnostic.New(diagnostic.DiagnosticSpec{ - File: item.Spans[0].File, + Location: item.Spans[0].Location, + File: item.Spans[0].DisplayFile(), Span: lookupSpan, Code: code, Severity: severity, diff --git a/analysis/check/diagnostics/judgment_adapter.go b/analysis/check/diagnostics/judgment_adapter.go index 333d39e67..ee0f27cb4 100644 --- a/analysis/check/diagnostics/judgment_adapter.go +++ b/analysis/check/diagnostics/judgment_adapter.go @@ -78,8 +78,13 @@ func RenderJudgments(items []judgment.Judgment, config Config) []diagnostic.Diag if !ok { continue } - if d.Position.File == "" && len(item.Spans) != 0 { - d.Position.File = item.Spans[0].File + if len(item.Spans) != 0 { + if !d.Location.Document.Valid() { + d.Location = item.Spans[0].Location + } + if d.Position.File == "" { + d.Position.File = item.Spans[0].DisplayFile() + } } out = append(out, d) } diff --git a/analysis/check/diagnostics/judgment_channel_lifecycle.go b/analysis/check/diagnostics/judgment_channel_lifecycle.go index bf66380c9..beabb742d 100644 --- a/analysis/check/diagnostics/judgment_channel_lifecycle.go +++ b/analysis/check/diagnostics/judgment_channel_lifecycle.go @@ -21,7 +21,8 @@ func renderChannelLifecycleJudgmentWithPolicy(ctx judgmentRenderContext, item ju return diagnostic.Diagnostic{}, false } return diagnostic.New(diagnostic.DiagnosticSpec{ - File: item.Spans[0].File, + Location: item.Spans[0].Location, + File: item.Spans[0].DisplayFile(), Span: span, Code: code, Severity: severity, diff --git a/analysis/check/diagnostics/judgment_channel_select.go b/analysis/check/diagnostics/judgment_channel_select.go index 04ce61d8b..f06c3a9b4 100644 --- a/analysis/check/diagnostics/judgment_channel_select.go +++ b/analysis/check/diagnostics/judgment_channel_select.go @@ -20,7 +20,8 @@ func renderChannelSelectJudgmentWithPolicy(ctx judgmentRenderContext, item judgm return diagnostic.Diagnostic{}, false } return diagnostic.New(diagnostic.DiagnosticSpec{ - File: item.Spans[0].File, + Location: item.Spans[0].Location, + File: item.Spans[0].DisplayFile(), Span: span, Code: code, Severity: severity, diff --git a/analysis/check/diagnostics/judgment_dead_assignment.go b/analysis/check/diagnostics/judgment_dead_assignment.go index bb2c693a8..00715ff5a 100644 --- a/analysis/check/diagnostics/judgment_dead_assignment.go +++ b/analysis/check/diagnostics/judgment_dead_assignment.go @@ -21,7 +21,8 @@ func renderDeadAssignmentJudgmentWithPolicy(ctx judgmentRenderContext, item judg span := diagnosticSpanFromJudgment(item.Spans[0]) presentation := ctx.proof.DeadAssignment(item, span, name) return diagnostic.New(diagnostic.DiagnosticSpec{ - File: item.Spans[0].File, + Location: item.Spans[0].Location, + File: item.Spans[0].DisplayFile(), Span: span, Code: code, Severity: severity, diff --git a/analysis/check/diagnostics/judgment_direct_call.go b/analysis/check/diagnostics/judgment_direct_call.go index 7cb9ba1d9..06ad482bf 100644 --- a/analysis/check/diagnostics/judgment_direct_call.go +++ b/analysis/check/diagnostics/judgment_direct_call.go @@ -42,7 +42,8 @@ func renderDirectCallArgumentJudgmentWithPolicy(ctx judgmentRenderContext, item return diagnostic.Diagnostic{}, false } return diagnostic.New(diagnostic.DiagnosticSpec{ - File: item.Spans[0].File, + Location: item.Spans[0].Location, + File: item.Spans[0].DisplayFile(), Span: span, Code: code, Severity: severity, diff --git a/analysis/check/diagnostics/judgment_direct_call_arity.go b/analysis/check/diagnostics/judgment_direct_call_arity.go index 0f4ae8186..a090effe4 100644 --- a/analysis/check/diagnostics/judgment_direct_call_arity.go +++ b/analysis/check/diagnostics/judgment_direct_call_arity.go @@ -22,7 +22,8 @@ func renderCallArityJudgmentWithPolicy(ctx judgmentRenderContext, item judgment. return diagnostic.Diagnostic{}, false } return diagnostic.New(diagnostic.DiagnosticSpec{ - File: item.Spans[0].File, + Location: item.Spans[0].Location, + File: item.Spans[0].DisplayFile(), Span: span, Code: presentation.Code, Severity: severity, diff --git a/analysis/check/diagnostics/judgment_direct_call_callee.go b/analysis/check/diagnostics/judgment_direct_call_callee.go index aa194548f..6e0d0a3af 100644 --- a/analysis/check/diagnostics/judgment_direct_call_callee.go +++ b/analysis/check/diagnostics/judgment_direct_call_callee.go @@ -22,7 +22,8 @@ func renderCallCalleeJudgmentWithPolicy(ctx judgmentRenderContext, item judgment return diagnostic.Diagnostic{}, false } return diagnostic.New(diagnostic.DiagnosticSpec{ - File: item.Spans[0].File, + Location: item.Spans[0].Location, + File: item.Spans[0].DisplayFile(), Span: span, Code: presentation.Code, Severity: severity, diff --git a/analysis/check/diagnostics/judgment_frozen_table.go b/analysis/check/diagnostics/judgment_frozen_table.go index cad58bfae..dcf43f846 100644 --- a/analysis/check/diagnostics/judgment_frozen_table.go +++ b/analysis/check/diagnostics/judgment_frozen_table.go @@ -17,7 +17,8 @@ func renderFrozenTableJudgmentWithPolicy(ctx judgmentRenderContext, item judgmen span := diagnosticSpanFromJudgment(item.Spans[0]) presentation := ctx.proof.FrozenTable(item, span) return diagnostic.New(diagnostic.DiagnosticSpec{ - File: item.Spans[0].File, + Location: item.Spans[0].Location, + File: item.Spans[0].DisplayFile(), Span: span, Code: code, Message: presentation.Message, diff --git a/analysis/check/diagnostics/judgment_member_read.go b/analysis/check/diagnostics/judgment_member_read.go index 06b2edb06..b5d5ac53c 100644 --- a/analysis/check/diagnostics/judgment_member_read.go +++ b/analysis/check/diagnostics/judgment_member_read.go @@ -20,7 +20,8 @@ func renderMemberReadJudgmentWithPolicy(ctx judgmentRenderContext, item judgment return diagnostic.Diagnostic{}, false } return diagnostic.New(diagnostic.DiagnosticSpec{ - File: item.Spans[0].File, + Location: item.Spans[0].Location, + File: item.Spans[0].DisplayFile(), Span: span, Code: code, Severity: severity, diff --git a/analysis/check/diagnostics/judgment_nonnil_assertion.go b/analysis/check/diagnostics/judgment_nonnil_assertion.go index 0bafaeb93..fd11c4f19 100644 --- a/analysis/check/diagnostics/judgment_nonnil_assertion.go +++ b/analysis/check/diagnostics/judgment_nonnil_assertion.go @@ -17,7 +17,8 @@ func renderNonNilAssertionJudgmentWithPolicy(ctx judgmentRenderContext, item jud span := diagnosticSpanFromJudgment(item.Spans[0]) presentation := ctx.proof.NonNilAssertion(item, span) return diagnostic.New(diagnostic.DiagnosticSpec{ - File: item.Spans[0].File, + Location: item.Spans[0].Location, + File: item.Spans[0].DisplayFile(), Span: span, Code: code, Severity: severity, diff --git a/analysis/check/diagnostics/judgment_numeric_for.go b/analysis/check/diagnostics/judgment_numeric_for.go index 1229633ed..3c2da105e 100644 --- a/analysis/check/diagnostics/judgment_numeric_for.go +++ b/analysis/check/diagnostics/judgment_numeric_for.go @@ -20,7 +20,8 @@ func renderNumericForJudgmentWithPolicy(ctx judgmentRenderContext, item judgment return diagnostic.Diagnostic{}, false } return diagnostic.New(diagnostic.DiagnosticSpec{ - File: item.Spans[0].File, + Location: item.Spans[0].Location, + File: item.Spans[0].DisplayFile(), Span: span, Code: code, Severity: severity, diff --git a/analysis/check/diagnostics/judgment_return.go b/analysis/check/diagnostics/judgment_return.go index 30ee9943d..399831633 100644 --- a/analysis/check/diagnostics/judgment_return.go +++ b/analysis/check/diagnostics/judgment_return.go @@ -27,7 +27,8 @@ func renderReturnJudgmentWithPolicy(ctx judgmentRenderContext, item judgment.Jud sourceName := item.Actual.Label presentation := ctx.proof.Return(item, label, sourceName, got, want, span) return diagnostic.New(diagnostic.DiagnosticSpec{ - File: item.Spans[0].File, + Location: item.Spans[0].Location, + File: item.Spans[0].DisplayFile(), Span: span, Code: code, Severity: severity, diff --git a/analysis/check/diagnostics/judgment_send_safety.go b/analysis/check/diagnostics/judgment_send_safety.go index 3dab53275..e47658382 100644 --- a/analysis/check/diagnostics/judgment_send_safety.go +++ b/analysis/check/diagnostics/judgment_send_safety.go @@ -20,7 +20,8 @@ func renderSendIsolationJudgmentWithPolicy(_ judgmentRenderContext, item judgmen labels = append(labels, sourceLabel(proofSpan, labelSendSafetyProof)) } return diagnostic.New(diagnostic.DiagnosticSpec{ - File: item.Spans[0].File, + Location: item.Spans[0].Location, + File: item.Spans[0].DisplayFile(), Span: span, Code: code, Message: sendSafetyMessage(item), diff --git a/analysis/check/diagnostics/judgment_typestate_requirement.go b/analysis/check/diagnostics/judgment_typestate_requirement.go index b8954cb08..6b6218a11 100644 --- a/analysis/check/diagnostics/judgment_typestate_requirement.go +++ b/analysis/check/diagnostics/judgment_typestate_requirement.go @@ -26,12 +26,12 @@ func renderTypestateRequirementJudgmentWithPolicy(ctx judgmentRenderContext, ite } if item.Code == judgment.CodeTypestateInvalidRequirement { message := fmt.Sprintf("invalid typestate requirement for resource %s in protocol %s: expected %s, found %s", codeName(resource), protocol, codeName(item.Expected.Label), codeName(item.Actual.Label)) - return diagnostic.New(diagnostic.DiagnosticSpec{File: item.Spans[0].File, Span: span, Code: diagnosticCodeForJudgment(item), Severity: severity, + return diagnostic.New(diagnostic.DiagnosticSpec{Location: item.Spans[0].Location, File: item.Spans[0].DisplayFile(), Span: span, Code: diagnosticCodeForJudgment(item), Severity: severity, Message: message, Explanation: diagnostic.NewExplanation(diagnostic.Evidence{Kind: diagnostic.EvidenceAbstractFact, Trust: diagnosticTrustFromJudgmentEvidence(item, judgment.EvidenceAbstractFact, diagnostic.TrustProven), Span: diagnosticEvidenceSpanOr(item, judgment.EvidenceAbstractFact, span), Message: fmt.Sprintf("this call requires %s to be in %s, but solved state is %s", codeName(resource), codeName(item.Expected.Label), codeName(item.Actual.Label))}), Labels: []diagnostic.Label{sourceLabel(span, "invalid typestate requirement")}, Help: fmt.Sprintf("Call this operation only when %s is in %s state.", codeName(resource), codeName(item.Expected.Label))}), true } message := fmt.Sprintf("cannot prove typestate requirement for resource %s: expected %s", codeName(resource), codeName(item.Expected.Label)) - return diagnostic.New(diagnostic.DiagnosticSpec{File: item.Spans[0].File, Span: span, Code: diagnosticCodeForJudgment(item), Severity: severity, + return diagnostic.New(diagnostic.DiagnosticSpec{Location: item.Spans[0].Location, File: item.Spans[0].DisplayFile(), Span: span, Code: diagnosticCodeForJudgment(item), Severity: severity, Message: message, Explanation: diagnostic.NewExplanation(diagnostic.Evidence{Kind: diagnostic.EvidenceMissingProof, Trust: diagnosticTrustFromJudgmentEvidence(item, judgment.EvidenceMissingProof, diagnostic.TrustRefuted), Span: diagnosticEvidenceSpanOr(item, judgment.EvidenceMissingProof, span), Message: fmt.Sprintf("no proof establishes %s in %s state at this call", codeName(resource), codeName(item.Expected.Label))}), Labels: []diagnostic.Label{sourceLabel(span, "unproven typestate requirement")}, Help: fmt.Sprintf("Establish that %s is in %s state before this call.", codeName(resource), codeName(item.Expected.Label))}), true } diff --git a/analysis/check/diagnostics/judgment_typestate_transition.go b/analysis/check/diagnostics/judgment_typestate_transition.go index 6f479a427..0676593c8 100644 --- a/analysis/check/diagnostics/judgment_typestate_transition.go +++ b/analysis/check/diagnostics/judgment_typestate_transition.go @@ -27,7 +27,8 @@ func renderTypestateInvalidTransitionJudgmentWithPolicy(ctx judgmentRenderContex message := fmt.Sprintf("invalid transition for resource %s in protocol %s: expected %s, found %s", codeName(resource), evidence.Detail.Protocol, codeName(evidence.Detail.FromState), codeName(evidence.Detail.CurrentState)) explanation := fmt.Sprintf("this transition requires %s to be in %s, but solved state is %s", codeName(resource), codeName(evidence.Detail.FromState), codeName(evidence.Detail.CurrentState)) return diagnostic.New(diagnostic.DiagnosticSpec{ - File: item.Spans[0].File, + Location: item.Spans[0].Location, + File: item.Spans[0].DisplayFile(), Span: span, Code: diagnosticCodeForJudgment(item), Severity: severity, diff --git a/analysis/check/diagnostics/judgment_unresolved_type.go b/analysis/check/diagnostics/judgment_unresolved_type.go index 46b4b39ee..372cf0a2a 100644 --- a/analysis/check/diagnostics/judgment_unresolved_type.go +++ b/analysis/check/diagnostics/judgment_unresolved_type.go @@ -21,7 +21,8 @@ func renderUnresolvedTypeJudgmentWithPolicy(ctx judgmentRenderContext, item judg span := diagnosticSpanFromJudgment(item.Spans[0]) presentation := ctx.proof.UnresolvedType(item, name, span) return diagnostic.New(diagnostic.DiagnosticSpec{ - File: item.Spans[0].File, + Location: item.Spans[0].Location, + File: item.Spans[0].DisplayFile(), Span: span, Code: code, Severity: severity, diff --git a/analysis/check/diagnostics/judgment_unresolved_value.go b/analysis/check/diagnostics/judgment_unresolved_value.go index ac7e5a5cf..7bbc2b14d 100644 --- a/analysis/check/diagnostics/judgment_unresolved_value.go +++ b/analysis/check/diagnostics/judgment_unresolved_value.go @@ -21,7 +21,8 @@ func renderUnresolvedValueJudgmentWithPolicy(ctx judgmentRenderContext, item jud span := diagnosticSpanFromJudgment(item.Spans[0]) presentation := ctx.proof.UnresolvedValue(item, name, span) return diagnostic.New(diagnostic.DiagnosticSpec{ - File: item.Spans[0].File, + Location: item.Spans[0].Location, + File: item.Spans[0].DisplayFile(), Span: span, Code: code, Severity: severity, diff --git a/analysis/check/diagnostics/judgment_unused_local.go b/analysis/check/diagnostics/judgment_unused_local.go index 9574728f7..21091fefe 100644 --- a/analysis/check/diagnostics/judgment_unused_local.go +++ b/analysis/check/diagnostics/judgment_unused_local.go @@ -21,7 +21,8 @@ func renderUnusedLocalJudgmentWithPolicy(ctx judgmentRenderContext, item judgmen span := diagnosticSpanFromJudgment(item.Spans[0]) presentation := ctx.proof.UnusedLocal(item, name, span) return diagnostic.New(diagnostic.DiagnosticSpec{ - File: item.Spans[0].File, + Location: item.Spans[0].Location, + File: item.Spans[0].DisplayFile(), Span: span, Code: code, Severity: severity, diff --git a/analysis/check/diagnostics/lifecycle_proof_context.go b/analysis/check/diagnostics/lifecycle_proof_context.go index a5fc93d2f..444fac780 100644 --- a/analysis/check/diagnostics/lifecycle_proof_context.go +++ b/analysis/check/diagnostics/lifecycle_proof_context.go @@ -113,8 +113,8 @@ func lifecycleFinalStateLabel(detail judgment.EvidenceDetail) string { func lifecycleFile(item judgment.Judgment) string { for _, evidence := range item.Evidence { - if evidence.Span.File != "" { - return evidence.Span.File + if file := evidence.Span.DisplayFile(); file != "" { + return file } } return "" diff --git a/analysis/check/diagnostics/precedence.go b/analysis/check/diagnostics/precedence.go index 9a8ae5815..8275369bf 100644 --- a/analysis/check/diagnostics/precedence.go +++ b/analysis/check/diagnostics/precedence.go @@ -173,7 +173,7 @@ func diagnosticCauseCoversSpan(cause, dependent diagnostic.Diagnostic) bool { } } for _, label := range cause.Labels { - file := label.File + file := label.DisplayFile() if file == "" { file = cause.Position.File } diff --git a/analysis/check/fixpoint/program/context_index.go b/analysis/check/fixpoint/program/context_index.go index add7de348..0679a65fa 100644 --- a/analysis/check/fixpoint/program/context_index.go +++ b/analysis/check/fixpoint/program/context_index.go @@ -164,11 +164,11 @@ func (idx *contextIndex) nextContextKey(baseKey summary.SummaryKey, identity con func stableContextKeyDigest(baseKey summary.SummaryKey, identity contextKeyIdentity, collision uint64) summary.Digest { h := fnv.New64a() - fmt.Fprint(h, "call-context-key-v1:") + _, _ = fmt.Fprint(h, "call-context-key-v1:") writeSummaryKeyDigest(h, baseKey) - fmt.Fprint(h, "kind:", identity.kind, ";owner:") + _, _ = fmt.Fprint(h, "kind:", identity.kind, ";owner:") writeSummaryKeyDigest(h, identity.owner) - fmt.Fprint(h, "expr:", uint32(identity.expr), ";collision:", collision, ";") + _, _ = fmt.Fprint(h, "expr:", uint32(identity.expr), ";collision:", collision, ";") return summary.Digest(h.Sum64()) } diff --git a/analysis/check/fixpoint/program/materialization_cache.go b/analysis/check/fixpoint/program/materialization_cache.go index 66531673e..a94027d00 100644 --- a/analysis/check/fixpoint/program/materialization_cache.go +++ b/analysis/check/fixpoint/program/materialization_cache.go @@ -214,7 +214,7 @@ func trackedSummaryReadDigests(reg *axis.Registry, deps map[summary.SummaryKey]t if dep.present { _, _ = h.Write([]byte("present:")) payload := uint64(summary.NormalizedPayloadDigest(reg, dep.sum)) - fmt.Fprintf(h, "%d;", payload) + _, _ = fmt.Fprintf(h, "%d;", payload) } else { _, _ = h.Write([]byte("missing")) } diff --git a/analysis/check/fixpoint/program/program_keys.go b/analysis/check/fixpoint/program/program_keys.go index 3b67476d5..5247ab1f9 100644 --- a/analysis/check/fixpoint/program/program_keys.go +++ b/analysis/check/fixpoint/program/program_keys.go @@ -111,7 +111,7 @@ func materializedProgramShapeDigest(keys programKeys) uint64 { return 0 }) for _, key := range contextKeys { - fmt.Fprint(h, "ctx:") + _, _ = fmt.Fprint(h, "ctx:") writeSummaryKeyDigest(h, key) } return h.Sum64() @@ -127,7 +127,7 @@ func writeSymbolSummaryKeySetDigest(h interface{ Write([]byte) (int, error) }, v } slices.Sort(keys) for _, key := range keys { - fmt.Fprintf(h, "map:%d=", key) + _, _ = fmt.Fprintf(h, "map:%d=", key) writeSummaryKeyDigest(h, values[key]) } } @@ -162,7 +162,7 @@ func writeIdentitySummaryKeySetDigest(h interface{ Write([]byte) (int, error) }, return 0 }) for _, key := range keys { - fmt.Fprintf(h, "id:%s/%s/%d=", key.Kind, key.Site, key.Index) + _, _ = fmt.Fprintf(h, "id:%s/%s/%d=", key.Kind, key.Site, key.Index) writeSummaryKeyDigest(h, values[key]) } } @@ -177,7 +177,7 @@ func writeCalleePathKeySetDigest(h interface{ Write([]byte) (int, error) }, valu } slices.Sort(keys) for _, key := range keys { - fmt.Fprintf(h, "path:%s=", key) + _, _ = fmt.Fprintf(h, "path:%s=", key) writeSummaryKeyDigest(h, values[key]) } } @@ -192,7 +192,7 @@ func writeCalleePathMultiKeySetDigest(h interface{ Write([]byte) (int, error) }, } slices.Sort(keys) for _, key := range keys { - fmt.Fprintf(h, "multi:%s=", key) + _, _ = fmt.Fprintf(h, "multi:%s=", key) summaryKeys := append([]summary.SummaryKey(nil), values[key]...) slices.SortFunc(summaryKeys, func(a, b summary.SummaryKey) int { if a.Less(b) { @@ -210,7 +210,7 @@ func writeCalleePathMultiKeySetDigest(h interface{ Write([]byte) (int, error) }, } func writeSummaryKeyDigest(h interface{ Write([]byte) (int, error) }, key summary.SummaryKey) { - fmt.Fprintf( + _, _ = fmt.Fprintf( h, "%d/%d/%d/%d/%d;", key.Ref.Kind, diff --git a/analysis/check/fixpoint/program/program_test.go b/analysis/check/fixpoint/program/program_test.go index 937d062ca..106d86533 100644 --- a/analysis/check/fixpoint/program/program_test.go +++ b/analysis/check/fixpoint/program/program_test.go @@ -2073,7 +2073,6 @@ return mapped t.Fatalf("contextual map_result param 1 = %v, want concrete Result", contextualFn.Params[0].Type) } var sawContext bool - var sawResultRoot bool for _, child := range result.RootResult().FunctionResults() { if child == nil || !child.IsCallContextResult() || child.Graph() == nil { continue @@ -2096,7 +2095,6 @@ return mapped if !rootOK { continue } - sawResultRoot = true got, ok := typevalue.TypeOf(reg, value) if !ok || !subtype.IsSubtype(got, typ.Number) { structural, structuralOK := typevalue.StructuralTypeOf(reg, typevalue.NewCache(), value, typevalue.StructuralTypeOptions{}) @@ -2122,9 +2120,6 @@ return mapped if !sawContext { t.Fatalf("specialized map_result call context missing") } - if !sawResultRoot { - return - } t.Fatalf("callback call argument result.value not found") } diff --git a/analysis/check/fixpoint/summary/path_facts.go b/analysis/check/fixpoint/summary/path_facts.go index 968e9ab08..80d34ebc7 100644 --- a/analysis/check/fixpoint/summary/path_facts.go +++ b/analysis/check/fixpoint/summary/path_facts.go @@ -68,29 +68,6 @@ func pathStaticMemberMap(reg *axis.Registry) factmap.Map[pathValueFactKey, callb } } -// pathStaticMemberDeltaMap is the canonical may map for structural param -// additions. Unlike PathStaticMembers, it keeps branch-local writes so consumers -// can materialize them as optional members. -func pathStaticMemberDeltaMap(reg *axis.Registry) factmap.Map[pathValueFactKey, callboundary.PathStaticMemberDeltaFact, product.Value] { - return factmap.Map[pathValueFactKey, callboundary.PathStaticMemberDeltaFact, product.Value]{ - Key: func(f callboundary.PathStaticMemberDeltaFact) pathValueFactKey { return pathValueFactKey(f.Path.Key()) }, - Value: func(f callboundary.PathStaticMemberDeltaFact) product.Value { return f.Value }, - WithValue: func(f callboundary.PathStaticMemberDeltaFact, v product.Value) callboundary.PathStaticMemberDeltaFact { - f.Value = v - return f - }, - Less: func(a, b callboundary.PathStaticMemberDeltaFact) bool { return a.Path.Less(b.Path) }, - Valid: func(f callboundary.PathStaticMemberDeltaFact) bool { - return f.Path.IsPlaceholder() || boundaryReturnSlotPath(f.Path) || f.Path.Symbol != 0 - }, - CloneFact: func(f callboundary.PathStaticMemberDeltaFact) callboundary.PathStaticMemberDeltaFact { - f.Path = f.Path.Clone() - return f - }, - Domain: product.Domain(reg), - } -} - func clonePathValueFacts(in []callboundary.PathValueFact) []callboundary.PathValueFact { if len(in) == 0 { return nil diff --git a/analysis/check/fixpoint/summary/return_flow.go b/analysis/check/fixpoint/summary/return_flow.go index 22ac5fbeb..2887750c6 100644 --- a/analysis/check/fixpoint/summary/return_flow.go +++ b/analysis/check/fixpoint/summary/return_flow.go @@ -33,19 +33,6 @@ type returnFlowKey struct { path pathdom.PathKey } -func returnFlowParam(returnIndex, param int) ReturnFlow { - return ReturnFlow{ReturnIndex: returnIndex, Kind: ReturnFlowParam, Param: param} -} - -func returnFlowParamMember(returnIndex, param int, path []segment.Segment) ReturnFlow { - return ReturnFlow{ - ReturnIndex: returnIndex, - Kind: ReturnFlowParamMember, - Param: param, - Path: append([]segment.Segment(nil), path...), - } -} - var returnFlowLane returnFlowFactLane type returnFlowFactLane struct{} diff --git a/analysis/check/internal/readmodel/registration_discriminant.go b/analysis/check/internal/readmodel/registration_discriminant.go index c3a32c207..62d03187d 100644 --- a/analysis/check/internal/readmodel/registration_discriminant.go +++ b/analysis/check/internal/readmodel/registration_discriminant.go @@ -155,18 +155,25 @@ func identifierName(s string) bool { if s == "" { return false } - if !((s[0] >= 'A' && s[0] <= 'Z') || (s[0] >= 'a' && s[0] <= 'z') || s[0] == '_') { + if !identifierStartByte(s[0]) { return false } for i := 1; i < len(s); i++ { - ch := s[i] - if !((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || ch == '_') { + if !identifierContinueByte(s[i]) { return false } } return true } +func identifierStartByte(ch byte) bool { + return ch >= 'A' && ch <= 'Z' || ch >= 'a' && ch <= 'z' || ch == '_' +} + +func identifierContinueByte(ch byte) bool { + return identifierStartByte(ch) || ch >= '0' && ch <= '9' +} + func appendSegment(prefix []segment.Segment, seg segment.Segment) []segment.Segment { next := make([]segment.Segment, 0, len(prefix)+1) next = append(next, prefix...) diff --git a/analysis/check/judgment/judgment.go b/analysis/check/judgment/judgment.go index 0de12aa1a..f7c3c955e 100644 --- a/analysis/check/judgment/judgment.go +++ b/analysis/check/judgment/judgment.go @@ -971,6 +971,14 @@ func (s SpanRef) DisplayFile() string { return embedding.DefaultDocumentLabel(s.Location.Document) } +// SetDisplayFileIfEmpty fills the legacy renderer projection without changing +// the digest-bound source identity. +func (s *SpanRef) SetDisplayFileIfEmpty(file string) { + if s != nil && s.File == "" { + s.File = file + } +} + // Judgment is the semantic obligation record emitted after solve and consumed // by rendering/dedup/policy layers. type Judgment struct { diff --git a/analysis/check/obligation/pass/assignment_test.go b/analysis/check/obligation/pass/assignment_test.go index 29fc75240..beac4d9cc 100644 --- a/analysis/check/obligation/pass/assignment_test.go +++ b/analysis/check/obligation/pass/assignment_test.go @@ -50,7 +50,7 @@ func TestAssignmentsRefutesConcreteMismatch(t *testing.T) { !strings.Contains(stable, "ord:0") { t.Fatalf("stable key = %q", item.Subject.StableKey()) } - if len(item.Spans) != 1 || item.Spans[0].File != "test.lua" || item.Spans[0].StartLine != 1 { + if len(item.Spans) != 1 || item.Spans[0].DisplayFile() != "test.lua" || item.Spans[0].StartLine != 1 { t.Fatalf("spans = %#v, want source span on test.lua line 1", item.Spans) } } @@ -92,7 +92,7 @@ end`), "test.lua") if item.Actual.ProjectedType == nil || item.Expected.Type == nil { t.Fatalf("actual/expected = %#v/%#v, want concrete types", item.Actual, item.Expected) } - if len(item.Spans) != 1 || item.Spans[0].File != "test.lua" || item.Spans[0].StartLine != 2 { + if len(item.Spans) != 1 || item.Spans[0].DisplayFile() != "test.lua" || item.Spans[0].StartLine != 2 { t.Fatalf("spans = %#v, want return expression span on test.lua line 2", item.Spans) } } diff --git a/analysis/check/obligation/pass/call_argument_test.go b/analysis/check/obligation/pass/call_argument_test.go index a3bf2527a..3a9e74c5a 100644 --- a/analysis/check/obligation/pass/call_argument_test.go +++ b/analysis/check/obligation/pass/call_argument_test.go @@ -113,7 +113,7 @@ add(1, "wrong")`, "test.lua").RootResult() if len(got) != 1 { t.Fatalf("judgments = %d, want 1: %#v", len(got), got) } - if got[0].Spans[0].File != "test.lua" || got[0].Spans[0].StartLine != 4 { + if got[0].Spans[0].DisplayFile() != "test.lua" || got[0].Spans[0].StartLine != 4 { t.Fatalf("span = %#v, want test.lua line 4", got[0].Spans[0]) } } diff --git a/analysis/check/obligation/pass/call_callee.go b/analysis/check/obligation/pass/call_callee.go index 5b569b261..3810c0d5f 100644 --- a/analysis/check/obligation/pass/call_callee.go +++ b/analysis/check/obligation/pass/call_callee.go @@ -39,14 +39,15 @@ func callCalleeJudgment(ctx Context, functionKey string, call readmodel.CallSite } verdict := judgment.VerdictRefuted missingTrust := judgment.EvidenceTrustRefuted - if report.Kind == readmodel.CallCalleeReportMayBeNil { + switch report.Kind { + case readmodel.CallCalleeReportMayBeNil: detail = judgment.CalleeMayBeNilEvidenceDetail(report.Callable) if report.MemberAccess { detail = judgment.MemberCalleeMayBeNilEvidenceDetail(report.Callable) } verdict = judgment.VerdictUnknown missingTrust = judgment.EvidenceTrustUnknown - } else if report.Kind == readmodel.CallCalleeReportMissingMember { + case readmodel.CallCalleeReportMissingMember: detail = judgment.MemberMissingEvidenceDetail(report.MemberName) } span := spanFromReadModel(ctx.SourceFile, report.Span) diff --git a/analysis/check/obligation/pass/redundant_condition_test.go b/analysis/check/obligation/pass/redundant_condition_test.go index f6ad34ef1..11118ba42 100644 --- a/analysis/check/obligation/pass/redundant_condition_test.go +++ b/analysis/check/obligation/pass/redundant_condition_test.go @@ -36,7 +36,7 @@ end !judgmentHasEvidenceDetail(item, judgment.EvidenceDetailRedundantConditionStability) { t.Fatalf("evidence = %#v, want check, proof, and stability details", item.Evidence) } - if len(item.Spans) != 2 || item.Spans[0].File != "test.lua" || item.Spans[1].File != "test.lua" { + if len(item.Spans) != 2 || item.Spans[0].DisplayFile() != "test.lua" || item.Spans[1].DisplayFile() != "test.lua" { t.Fatalf("spans = %#v, want current and proof spans in test.lua", item.Spans) } } diff --git a/analysis/check/obligation/pass/subject_anchor.go b/analysis/check/obligation/pass/subject_anchor.go index 9a19224f2..9106c970d 100644 --- a/analysis/check/obligation/pass/subject_anchor.go +++ b/analysis/check/obligation/pass/subject_anchor.go @@ -198,8 +198,8 @@ func subjectAnchorModule(ctx Context, item judgment.Judgment) string { return ctx.SourceFile } for _, span := range item.Spans { - if span.File != "" { - return span.File + if file := span.DisplayFile(); file != "" { + return file } } return item.Subject.FunctionKey diff --git a/analysis/check/obligation/pass/unresolved_type_test.go b/analysis/check/obligation/pass/unresolved_type_test.go index 575e3cb62..67d6bca6e 100644 --- a/analysis/check/obligation/pass/unresolved_type_test.go +++ b/analysis/check/obligation/pass/unresolved_type_test.go @@ -45,7 +45,7 @@ local p: LocalPoint = {x = 1} !strings.Contains(stable, "ord:0") { t.Fatalf("stable key = %q", stable) } - if len(item.Spans) != 1 || item.Spans[0].File != "test.lua" || item.Spans[0].StartLine != 5 || item.Spans[0].StartCol != 10 { + if len(item.Spans) != 1 || item.Spans[0].DisplayFile() != "test.lua" || item.Spans[0].StartLine != 5 || item.Spans[0].StartCol != 10 { t.Fatalf("spans = %#v, want LocalPoint source span", item.Spans) } if len(item.Evidence) != 1 || item.Evidence[0].Kind != judgment.EvidenceAbstractFact || item.Evidence[0].Trust != judgment.EvidenceTrustProven { diff --git a/analysis/check/obligation/pass/unresolved_value_test.go b/analysis/check/obligation/pass/unresolved_value_test.go index 01ca75638..1d7d39d29 100644 --- a/analysis/check/obligation/pass/unresolved_value_test.go +++ b/analysis/check/obligation/pass/unresolved_value_test.go @@ -38,7 +38,7 @@ func TestUnresolvedValuesRefutesImplicitGlobalRead(t *testing.T) { !strings.Contains(stable, "ord:0") { t.Fatalf("stable key = %q", stable) } - if len(item.Spans) != 1 || item.Spans[0].File != "test.lua" || item.Spans[0].StartLine != 1 || item.Spans[0].StartCol != 11 { + if len(item.Spans) != 1 || item.Spans[0].DisplayFile() != "test.lua" || item.Spans[0].StartLine != 1 || item.Spans[0].StartCol != 11 { t.Fatalf("spans = %#v, want missing identifier source span", item.Spans) } if len(item.Evidence) != 1 || item.Evidence[0].Kind != judgment.EvidenceAbstractFact || item.Evidence[0].Trust != judgment.EvidenceTrustProven { diff --git a/analysis/check/service/locations.go b/analysis/check/service/locations.go index 32d8a58cd..43666d65c 100644 --- a/analysis/check/service/locations.go +++ b/analysis/check/service/locations.go @@ -34,7 +34,7 @@ func bindJudgmentLocations(items []judgment.Judgment, input UnitInput) { span := &items[itemIndex].Spans[spanIndex] document, ok := span.Location.Document, span.Location.Document.Valid() if !ok { - document, ok = documentForLabel(input, span.File) + document, ok = documentForLabel(input, span.DisplayFile()) } if !ok { continue @@ -44,9 +44,7 @@ func bindJudgmentLocations(items []judgment.Judgment, input UnitInput) { continue } span.Location = locationForSpan(document, snapshot, span.StartLine, span.StartCol, span.EndLine, span.EndCol) - if span.File == "" { - span.File = documentLabel(input, document) - } + span.SetDisplayFileIfEmpty(documentLabel(input, document)) } } } @@ -70,16 +68,14 @@ func bindDiagnosticLocations(items []diagnostic.Diagnostic, input UnitInput) { label := &item.Labels[labelIndex] labelDocument, labelOK := label.Location.Document, label.Location.Document.Valid() if !labelOK { - labelDocument, labelOK = documentForLabel(input, label.File) + labelDocument, labelOK = documentForLabel(input, label.DisplayFile()) } if !labelOK { continue } if snapshot, exists := input.Sources[labelDocument]; exists { label.Location = locationForSpan(labelDocument, snapshot, label.Span.StartLine, label.Span.StartCol, label.Span.EndLine, label.Span.EndCol) - if label.File == "" { - label.File = documentLabel(input, labelDocument) - } + label.SetDisplayFileIfEmpty(documentLabel(input, labelDocument)) } } } diff --git a/analysis/check/service/semantic_projection.go b/analysis/check/service/semantic_projection.go index d60ae76e9..9436fae6e 100644 --- a/analysis/check/service/semantic_projection.go +++ b/analysis/check/service/semantic_projection.go @@ -1090,8 +1090,3 @@ func lineColumnAt(data []byte, target int) (int, int) { } return line, column } -func identifierByte(value byte) bool { - return value == '_' || value >= 'a' && value <= 'z' || value >= 'A' && value <= 'Z' || value >= '0' && value <= '9' -} -func bytesIndex(data, needle []byte) int { return strings.Index(string(data), string(needle)) } -func bytesIndexByte(data []byte, value byte) int { return strings.IndexByte(string(data), value) } diff --git a/analysis/check/service/semantic_query.go b/analysis/check/service/semantic_query.go index 41d785535..c2f504dc0 100644 --- a/analysis/check/service/semantic_query.go +++ b/analysis/check/service/semantic_query.go @@ -40,6 +40,15 @@ func (l SourceLocation) Valid() bool { return l.Document.Valid() && !l.ContentDigest.IsZero() && l.ByteSpan.Valid() } +// BelongsToDocument compares semantic identity first and retains the legacy +// file projection only for callers that have not populated Document yet. +func (l SourceLocation) BelongsToDocument(document embedding.DocumentID) bool { + if l.Document.Valid() { + return l.Document == document + } + return l.File != "" && l.File == document.OpaqueKey +} + // BinderKind is the closed value-binder vocabulary exposed to embedding // clients. It mirrors WIR SymbolInfo rather than leaking bind internals. type BinderKind string diff --git a/analysis/check/service/semantic_query_api.go b/analysis/check/service/semantic_query_api.go index f29157fa8..0c6ca96d1 100644 --- a/analysis/check/service/semantic_query_api.go +++ b/analysis/check/service/semantic_query_api.go @@ -280,16 +280,6 @@ func cloneBodyCallRelations(item BodyCallRelations) BodyCallRelations { return item } -func repairActionsForCodes(defaultFile string, items []judgment.Judgment, allowed map[judgment.Code]struct{}) []RepairAction { - filtered := make([]judgment.Judgment, 0, len(items)) - for _, item := range items { - if _, ok := allowed[item.Code]; ok { - filtered = append(filtered, item) - } - } - return repairActionsFromJudgments(defaultFile, filtered) -} - func cloneRepairAction(item RepairAction) RepairAction { item.Payload.Edits = append([]RepairEdit(nil), item.Payload.Edits...) return item diff --git a/analysis/check/service/service_test.go b/analysis/check/service/service_test.go index 106a7e127..faa6b9590 100644 --- a/analysis/check/service/service_test.go +++ b/analysis/check/service/service_test.go @@ -226,8 +226,8 @@ func TestDocumentKeyedUnitInputBindsResultLocationsAndPreservesDisplay(t *testin if span.Location.Document != document || span.Location.ContentDigest != digest || !span.Location.Valid() { t.Fatalf("judgment span location = %#v, want digest-bound registry location", span.Location) } - if span.File != "orders.lua" { - t.Fatalf("judgment span display = %q, want label projection", span.File) + if span.DisplayFile() != "orders.lua" { + t.Fatalf("judgment span display = %q, want label projection", span.DisplayFile()) } } } diff --git a/analysis/diagnostic/diffreport/io.go b/analysis/diagnostic/diffreport/io.go index 132c00ecc..b4ff436fd 100644 --- a/analysis/diagnostic/diffreport/io.go +++ b/analysis/diagnostic/diffreport/io.go @@ -40,8 +40,15 @@ func ReadJSONLFile(path string) ([]Record, error) { if err != nil { return nil, err } - defer file.Close() - return ReadJSONL(file) + records, readErr := ReadJSONL(file) + closeErr := file.Close() + if readErr != nil { + return nil, readErr + } + if closeErr != nil { + return nil, closeErr + } + return records, nil } // WriteReport writes report in either "human", "text", "json", or "jsonl" diff --git a/analysis/diagnostic/types.go b/analysis/diagnostic/types.go index 86d45c17a..f1be5b6b2 100644 --- a/analysis/diagnostic/types.go +++ b/analysis/diagnostic/types.go @@ -121,6 +121,23 @@ type Label struct { Placement LabelPlacement } +// DisplayFile returns the compatibility renderer label. Semantic consumers +// must use Location.Document instead. +func (l Label) DisplayFile() string { + if l.File != "" { + return l.File + } + return embedding.DefaultDocumentLabel(l.Location.Document) +} + +// SetDisplayFileIfEmpty fills the compatibility renderer projection without +// changing the digest-bound source identity. +func (l *Label) SetDisplayFileIfEmpty(file string) { + if l != nil && l.File == "" { + l.File = file + } +} + // LabelPlacement controls where rich source-frame labels render relative to // the source line. Auto keeps the structural fallback: primary annotations below // the line, secondary annotations above it. diff --git a/analysis/domain/lattice/factset/factset.go b/analysis/domain/lattice/factset/factset.go index c4c23ec2f..2c3f61c28 100644 --- a/analysis/domain/lattice/factset/factset.go +++ b/analysis/domain/lattice/factset/factset.go @@ -95,7 +95,7 @@ func (s Set[K, F]) normalize(in []F, clone bool) []F { } fact = s.maybeCloneOne(fact, clone) key := s.Key(fact) - if kept, ok := merged[key]; ok && !(s.Prefer != nil && s.Prefer(kept, fact)) { + if kept, ok := merged[key]; ok && (s.Prefer == nil || !s.Prefer(kept, fact)) { continue } merged[key] = fact diff --git a/analysis/domain/value/refinement/refinement.go b/analysis/domain/value/refinement/refinement.go index 04f896a80..d8275c87b 100644 --- a/analysis/domain/value/refinement/refinement.go +++ b/analysis/domain/value/refinement/refinement.go @@ -226,7 +226,7 @@ func recoverCompatibleWitnessMeet(reg *axis.Registry, value, constraint product. if !ok { return product.Value{}, false } - narrower := constraintWitness + var narrower typewitness.Value switch { case subtype.IsSubtype(constraintType, valueType): narrower = constraintWitness diff --git a/analysis/domain/value/variant/narrow_truthy.go b/analysis/domain/value/variant/narrow_truthy.go index e25cf0eec..dfe907237 100644 --- a/analysis/domain/value/variant/narrow_truthy.go +++ b/analysis/domain/value/variant/narrow_truthy.go @@ -140,7 +140,7 @@ func typeCanBeTruthy(t typ.Type, depth int) bool { } return false case *typ.Literal: - return !(v.Base == kind.Boolean && v.Value == false) + return v.Base != kind.Boolean || v.Value != false default: if v == nil { return false diff --git a/analysis/engine/factapply/branches_refinement_test.go b/analysis/engine/factapply/branches_refinement_test.go index 521acafe9..fb0c21b5e 100644 --- a/analysis/engine/factapply/branches_refinement_test.go +++ b/analysis/engine/factapply/branches_refinement_test.go @@ -794,9 +794,10 @@ func TestFactsEdgeTransferAppliesBranchDiffConstraintRelationGraphKeys(t *testin t.Fatalf("true-edge relational constraints = %#v, want one relation", constraints) } constraint := constraints[0] + isSymmetricPair := (constraint.A == state.RelValueOperand(iKey) && constraint.B == state.RelValueOperand(jKey)) || + (constraint.A == state.RelValueOperand(jKey) && constraint.B == state.RelValueOperand(iKey)) if constraint.CoA != 1 || constraint.CoB != 1 || constraint.K != 0 || - !((constraint.A == state.RelValueOperand(iKey) && constraint.B == state.RelValueOperand(jKey)) || - (constraint.A == state.RelValueOperand(jKey) && constraint.B == state.RelValueOperand(iKey))) || + !isSymmetricPair || constraint.C != state.RelLengthOperand(xsKey) { t.Fatalf("true-edge relation = %#v, want i+j-len(xs)<=0 under relation graph keys", constraint) } diff --git a/analysis/engine/factapply/call_outcome_test.go b/analysis/engine/factapply/call_outcome_test.go index 03995cc96..c84336fda 100644 --- a/analysis/engine/factapply/call_outcome_test.go +++ b/analysis/engine/factapply/call_outcome_test.go @@ -793,9 +793,10 @@ func TestFactsNodeTransferCallOutcomeAppliesReturnSlotPathLanes(t *testing.T) { t.Fatalf("return-slot relational constraints = %#v, want one", constraints) } constraint := constraints[0] + isSymmetricPair := (constraint.A == state.RelValueOperand(iKey) && constraint.B == state.RelValueOperand(jKey)) || + (constraint.A == state.RelValueOperand(jKey) && constraint.B == state.RelValueOperand(iKey)) if constraint.CoA != 1 || constraint.CoB != 1 || constraint.K != 0 || - !((constraint.A == state.RelValueOperand(iKey) && constraint.B == state.RelValueOperand(jKey)) || - (constraint.A == state.RelValueOperand(jKey) && constraint.B == state.RelValueOperand(iKey))) || + !isSymmetricPair || constraint.C != state.RelLengthOperand(itemsKey) { t.Fatalf("return-slot relational constraint = %#v, want i+j-len(items)<=0", constraint) } @@ -1484,9 +1485,10 @@ func TestFactsNodeTransferCallOutcomeAppliesNormalReturnRelConstraints(t *testin t.Fatalf("relational constraints = %#v, want one rebased relation", constraints) } constraint := constraints[0] + isSymmetricPair := (constraint.A == state.RelValueOperand(iKey) && constraint.B == state.RelValueOperand(jKey)) || + (constraint.A == state.RelValueOperand(jKey) && constraint.B == state.RelValueOperand(iKey)) if constraint.CoA != 1 || constraint.CoB != 1 || constraint.K != 0 || - !((constraint.A == state.RelValueOperand(iKey) && constraint.B == state.RelValueOperand(jKey)) || - (constraint.A == state.RelValueOperand(jKey) && constraint.B == state.RelValueOperand(iKey))) || + !isSymmetricPair || constraint.C != state.RelLengthOperand(xsKey) { t.Fatalf("relational constraint = %#v, want i+j-len(xs)<=0 after rebasing", constraint) } diff --git a/analysis/engine/factapply/path_presence_implication.go b/analysis/engine/factapply/path_presence_implication.go index 6f9fe9b4d..04635a102 100644 --- a/analysis/engine/factapply/path_presence_implication.go +++ b/analysis/engine/factapply/path_presence_implication.go @@ -148,20 +148,6 @@ func publishCallReturnPresenceImplication( return activatePathPresenceImplications(ctx.Registry, resolver, ctx.Point, out) } -func applyPathValuePresenceImplication( - ctx transfer.NodeContext, - resolver *visibility.Resolver, - out state.State, - fact factflow.PathValuePresenceImplication, -) state.State { - implication, ok := pathValuePresenceImplicationAt(ctx, resolver, fact) - if !ok { - return out - } - out = out.AddPathPresenceImplication(implication) - return activatePathPresenceImplications(ctx.Registry, resolver, ctx.Point, out) -} - func pathValuePresenceImplicationAt( ctx transfer.NodeContext, resolver *visibility.Resolver, diff --git a/analysis/engine/solve/solver.go b/analysis/engine/solve/solver.go index d760d3e79..b2843b8aa 100644 --- a/analysis/engine/solve/solver.go +++ b/analysis/engine/solve/solver.go @@ -539,7 +539,7 @@ func (s *solveState[Cell, State]) runWithCancellation(cancel *cancellationGuard) } func (s *solveState[Cell, State]) runNarrowingWithoutCancellation() { - s.runNarrowing(nil) + _ = s.runNarrowing(nil) } func (s *solveState[Cell, State]) runNarrowing(cancel *cancellationGuard) error { diff --git a/analysis/engine/state/diff_relation_lane.go b/analysis/engine/state/diff_relation_lane.go index f40212592..7315478b0 100644 --- a/analysis/engine/state/diff_relation_lane.go +++ b/analysis/engine/state/diff_relation_lane.go @@ -97,7 +97,7 @@ func (l diffRelationLane) add(c RelConstraint) (diffRelationLane, bool) { c.B = RelOperand{} c.CoB = 0 } - lane, changed := l.mustSetLane.insert(c) + lane, changed := l.insert(c) return diffRelationLane{lane}, changed } diff --git a/analysis/engine/state/domain_test.go b/analysis/engine/state/domain_test.go index 977af7b8a..57b0ca6e4 100644 --- a/analysis/engine/state/domain_test.go +++ b/analysis/engine/state/domain_test.go @@ -490,11 +490,6 @@ func TestLaneSetValidatesAndCopiesSelection(t *testing.T) { if emptyCopy == nil || len(emptyCopy) != 0 { t.Fatalf("CloneLanes(empty) = %#v, want non-nil empty slice", emptyCopy) } - emptySource = append(emptySource, LaneFrozenTables) - if len(emptyCopy) != 0 { - t.Fatalf("CloneLanes(empty) kept caller storage: %#v", emptyCopy) - } - withoutFrozen := DefaultLaneSet().Without(LaneFrozenTables) if withoutFrozen.Has(LaneFrozenTables) { t.Fatal("Without kept disabled frozen-table lane") diff --git a/analysis/engine/state/frozen_table.go b/analysis/engine/state/frozen_table.go index 36222801d..d9a44d77c 100644 --- a/analysis/engine/state/frozen_table.go +++ b/analysis/engine/state/frozen_table.go @@ -33,7 +33,7 @@ func (l frozenTableLane) freeze(id identity.ID) (frozenTableLane, bool) { if id == (identity.ID{}) { return l, false } - lane, changed := l.mustSetLane.insert(id) + lane, changed := l.insert(id) return frozenTableLane{lane}, changed } diff --git a/analysis/engine/state/store_relation.go b/analysis/engine/state/store_relation.go index 1e0b8a9e3..ccab109e3 100644 --- a/analysis/engine/state/store_relation.go +++ b/analysis/engine/state/store_relation.go @@ -41,7 +41,7 @@ func (l storeRelationLane) add(relation StoreRelation) (storeRelationLane, bool) if relation.Source == "" || relation.Into == "" { return l, false } - lane, changed := l.mustSetLane.insert(relation) + lane, changed := l.insert(relation) return storeRelationLane{lane}, changed } diff --git a/analysis/internal/hash/hash.go b/analysis/internal/hash/hash.go index 75890c8ec..2170a9a5f 100644 --- a/analysis/internal/hash/hash.go +++ b/analysis/internal/hash/hash.go @@ -49,7 +49,7 @@ func (w *Writer) Sum64() uint64 { // Write implements io.Writer. func (w *Writer) Write(p []byte) (int, error) { for _, b := range p { - w.WriteByte(b) + w.mixByte(b) } return len(p), nil } @@ -57,16 +57,20 @@ func (w *Writer) Write(p []byte) (int, error) { // WriteString writes s directly into the hash. func (w *Writer) WriteString(s string) (int, error) { for i := range len(s) { - w.WriteByte(s[i]) + w.mixByte(s[i]) } return len(s), nil } // WriteByte writes one byte into the hash. func (w *Writer) WriteByte(b byte) error { + w.mixByte(b) + return nil +} + +func (w *Writer) mixByte(b byte) { w.h ^= uint64(b) w.h *= fnvPrime64 - return nil } // WriteUintDecimal writes v in base-10 ASCII form. diff --git a/analysis/internal/hash/hash_test.go b/analysis/internal/hash/hash_test.go index 8c604429c..9dc2dc092 100644 --- a/analysis/internal/hash/hash_test.go +++ b/analysis/internal/hash/hash_test.go @@ -114,7 +114,7 @@ func TestMixHashMatchesOneFNV1aUint64Step(t *testing.T) { func TestWriterMatchesStandardLibraryFNV1a64(t *testing.T) { t.Parallel() - var writer Writer = NewWriter() + writer := NewWriter() _, _ = writer.WriteString("prefix:") writer.WriteIntDecimal(-42) _ = writer.WriteByte(':') @@ -127,7 +127,7 @@ func TestWriterMatchesStandardLibraryFNV1a64(t *testing.T) { writer.WriteBool(false) std := fnv.New64a() - fmt.Fprintf(std, "prefix:%d:%d:%x:%t:%t", -42, uint64(123456789), uint64(0xabcdef), true, false) + _, _ = fmt.Fprintf(std, "prefix:%d:%d:%x:%t:%t", -42, uint64(123456789), uint64(0xabcdef), true, false) if got, want := writer.Sum64(), std.Sum64(); got != want { t.Fatalf("Writer digest = %d, want standard FNV-1a %d", got, want) } diff --git a/analysis/ir/wir/wir_test.go b/analysis/ir/wir/wir_test.go index 08ad2444f..345331e8c 100644 --- a/analysis/ir/wir/wir_test.go +++ b/analysis/ir/wir/wir_test.go @@ -22,10 +22,12 @@ func TestInternPoolsAreOneBasedAndDeduped(t *testing.T) { } c := Const{Kind: ConstNumber, Number: "42"} - if b.InternConst(c) != b.InternConst(c) { + constRef1, constRef2 := b.InternConst(c), b.InternConst(c) + if constRef1 != constRef2 { t.Fatalf("const interning not deduped") } - if b.InternType(typ.String) != b.InternType(typ.String) { + stringRef1, stringRef2 := b.InternType(typ.String), b.InternType(typ.String) + if stringRef1 != stringRef2 { t.Fatalf("type interning not deduped") } if b.InternType(typ.String) == b.InternType(typ.Number) { @@ -35,7 +37,8 @@ func TestInternPoolsAreOneBasedAndDeduped(t *testing.T) { t.Fatalf("nil (unresolved) type must intern to none") } // Distinct checks are never deduped: each branch owns one. - if b.InternCheck(b.Check(0)) == b.InternCheck(b.Check(0)) { + checkRef1, checkRef2 := b.InternCheck(b.Check(0)), b.InternCheck(b.Check(0)) + if checkRef1 == checkRef2 { t.Fatalf("checks must not dedupe") } } diff --git a/analysis/lsp/server.go b/analysis/lsp/server.go index b547165b1..e0e78ef3c 100644 --- a/analysis/lsp/server.go +++ b/analysis/lsp/server.go @@ -861,7 +861,7 @@ func (d semanticDocument) protocolLocation(location service.SourceLocation) (Loc } func (d semanticDocument) protocolRange(location service.SourceLocation) (Range, bool) { - if location.File != d.document.OpaqueKey { + if !location.BelongsToDocument(d.document) { return Range{}, false } item, err := d.buffer.rangeForCompilerSpan( diff --git a/analysis/lsp/server_test.go b/analysis/lsp/server_test.go index bbc33332a..51c1be741 100644 --- a/analysis/lsp/server_test.go +++ b/analysis/lsp/server_test.go @@ -30,8 +30,10 @@ func TestInProcessTransportLifecycleIncrementalCancellationAndVersionedDiagnosti } server := NewServer(session, Options{Debounce: time.Millisecond}) client, peer := net.Pipe() - defer client.Close() - defer peer.Close() + t.Cleanup(func() { + _ = client.Close() + _ = peer.Close() + }) serveDone := make(chan error, 1) go func() { serveDone <- ServeStream(context.Background(), peer, peer, server) }() framer := jsonrpc2.NewFramer(client, client) @@ -835,23 +837,6 @@ type blockBinderOccurrencesSession struct { release chan struct{} } -type blockRepairActionsSession struct { - service.WorkspaceSession - started chan struct{} - release chan struct{} - once sync.Once -} - -func (s *blockRepairActionsSession) RepairActions(ctx context.Context, request service.RepairActionsRequest) (service.RepairActionsResponse, error) { - s.once.Do(func() { close(s.started) }) - select { - case <-s.release: - case <-ctx.Done(): - return service.RepairActionsResponse{}, ctx.Err() - } - return s.WorkspaceSession.RepairActions(ctx, request) -} - type blockPositionLookupSession struct { service.WorkspaceSession started chan struct{} diff --git a/analysis/lua/wirlower/wirlower.go b/analysis/lua/wirlower/wirlower.go index d8bfcb1bb..1692c5307 100644 --- a/analysis/lua/wirlower/wirlower.go +++ b/analysis/lua/wirlower/wirlower.go @@ -1004,7 +1004,7 @@ type callMetadata struct { func (b *builder) callMetadata(context wir.CallContextKind, exprs []ast.Expr, exprIndex int, call *ast.FuncCallExpr, openTailFinal bool) callMetadata { meta := callMetadata{context: context, expr: exprIndex} - final := true + var final bool allowExpansion := false openTail := false switch context { diff --git a/analysis/module/manifest/manifest.go b/analysis/module/manifest/manifest.go index 5d973355d..f624c654d 100644 --- a/analysis/module/manifest/manifest.go +++ b/analysis/module/manifest/manifest.go @@ -16,6 +16,21 @@ import ( "github.com/wippyai/go-lua/analysis/type/typ" ) +// WireFormatVersion identifies the canonical JSON manifest envelope. The +// module's Manifest.Version remains consumer metadata and is intentionally +// independent from this codec version. +const WireFormatVersion = 1 + +var ( + // ErrLegacyWireFormat identifies the pre-abstract-interpreter binary INAM + // format. Those manifests contain type/effect domains that no longer have a + // lossless representation and must be rebuilt by the owning cache. + ErrLegacyWireFormat = errors.New("manifest: legacy binary wire format") + // ErrUnsupportedWireFormat identifies a canonical JSON envelope produced by + // a newer, unsupported codec version. + ErrUnsupportedWireFormat = errors.New("manifest: unsupported wire format") +) + // Manifest is the stable module-boundary type metadata exchanged between // compiled modules. It intentionally does not own checker stores, query caches, // or interprocedural state. @@ -232,6 +247,7 @@ func Encode(m *Manifest) ([]byte, error) { } wm := manifestWire{ + FormatVersion: WireFormatVersion, Path: m.Path, Version: m.Version, Globals: append([]string(nil), m.Globals...), @@ -333,6 +349,9 @@ func Decode(data []byte) (decoded *Manifest, err error) { err = fmt.Errorf("manifest: invalid wire: %v", recovered) } }() + if bytes.HasPrefix(data, []byte("INAM")) { + return nil, ErrLegacyWireFormat + } if len(bytes.TrimSpace(data)) == 0 { return nil, errors.New("manifest: decode empty data") } @@ -341,6 +360,11 @@ func Decode(data []byte) (decoded *Manifest, err error) { if err := json.Unmarshal(data, &wm); err != nil { return nil, err } + // FormatVersion 0 is the short-lived unversioned JSON format emitted by the + // abstract branch before the envelope was pinned. Its shape is canonical v1. + if wm.FormatVersion != 0 && wm.FormatVersion != WireFormatVersion { + return nil, fmt.Errorf("%w: got v%d, support v%d", ErrUnsupportedWireFormat, wm.FormatVersion, WireFormatVersion) + } m := New(wm.Path) m.Version = wm.Version @@ -401,6 +425,7 @@ func Decode(data []byte) (decoded *Manifest, err error) { } type manifestWire struct { + FormatVersion int `json:"formatVersion"` Path string `json:"path"` Version string `json:"version,omitempty"` Export *typeWire `json:"export,omitempty"` @@ -447,11 +472,7 @@ func encodeCallbackPhaseRegistrations(in []CallbackPhaseRegistration) []callback if registration.Function == "" || registration.CallbackParam < 0 || registration.Phase == "" { continue } - out = append(out, callbackPhaseRegistrationWire{ - Function: registration.Function, - CallbackParam: registration.CallbackParam, - Phase: registration.Phase, - }) + out = append(out, callbackPhaseRegistrationWire(registration)) } sort.Slice(out, func(i, j int) bool { if out[i].Function != out[j].Function { diff --git a/analysis/module/manifest/manifest_test.go b/analysis/module/manifest/manifest_test.go index 71ca3ccee..25ad553a9 100644 --- a/analysis/module/manifest/manifest_test.go +++ b/analysis/module/manifest/manifest_test.go @@ -2,6 +2,8 @@ package manifest import ( "bytes" + "encoding/json" + "errors" "strings" "testing" @@ -29,6 +31,52 @@ import ( "github.com/wippyai/go-lua/analysis/type/typeexpr" ) +func TestManifestWireFormatVersion(t *testing.T) { + m := New("example/versioned") + m.SetExport(typ.String) + + data, err := Encode(m) + if err != nil { + t.Fatalf("Encode: %v", err) + } + var wire map[string]any + if err := json.Unmarshal(data, &wire); err != nil { + t.Fatalf("json.Unmarshal: %v", err) + } + version, ok := wire["formatVersion"].(float64) + if !ok { + t.Fatalf("formatVersion = %#v, want JSON number", wire["formatVersion"]) + } + if got := int(version); got != WireFormatVersion { + t.Fatalf("formatVersion = %d, want %d", got, WireFormatVersion) + } + + delete(wire, "formatVersion") + unversioned, err := json.Marshal(wire) + if err != nil { + t.Fatalf("json.Marshal: %v", err) + } + if _, err := Decode(unversioned); err != nil { + t.Fatalf("Decode unversioned canonical JSON: %v", err) + } + + wire["formatVersion"] = float64(WireFormatVersion + 1) + future, err := json.Marshal(wire) + if err != nil { + t.Fatalf("json.Marshal future: %v", err) + } + if _, err := Decode(future); !errors.Is(err, ErrUnsupportedWireFormat) { + t.Fatalf("Decode future error = %v, want ErrUnsupportedWireFormat", err) + } +} + +func TestManifestRejectsLegacyBinaryWithMigrationSignal(t *testing.T) { + legacy := []byte{'I', 'N', 'A', 'M', 8, 0, 0, 0} + if _, err := Decode(legacy); !errors.Is(err, ErrLegacyWireFormat) { + t.Fatalf("Decode legacy error = %v, want ErrLegacyWireFormat", err) + } +} + func TestManifestDefineTypeAndSetExport(t *testing.T) { m := New("example/module") user := typetable.NewRecord(). @@ -2211,7 +2259,7 @@ func TestManifestEncodeOrdersNamedTypesDeterministically(t *testing.T) { alpha := strings.Index(text, `"name": "Alpha"`) middle := strings.Index(text, `"name": "Middle"`) zed := strings.Index(text, `"name": "Zed"`) - if alpha < 0 || middle < 0 || zed < 0 || !(alpha < middle && middle < zed) { + if alpha < 0 || middle < 0 || zed < 0 || alpha >= middle || middle >= zed { t.Fatalf("named types are not sorted:\n%s", text) } } diff --git a/analysis/module/manifest/schema_version_test.go b/analysis/module/manifest/schema_version_test.go new file mode 100644 index 000000000..6f691161a --- /dev/null +++ b/analysis/module/manifest/schema_version_test.go @@ -0,0 +1,62 @@ +package manifest + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "reflect" + "sort" + "strings" + "testing" +) + +const expectedManifestWireVersion1Hash = "ce85d4c63edb72848c7f8b1d270d8b2d719b22dc31017250366e61b01717e759" + +func TestManifestWireVersionPinsCurrentSurface(t *testing.T) { + surface := manifestWireSchemaSurface() + got := hashManifestWireSurface(surface) + want := map[int]string{ + 1: expectedManifestWireVersion1Hash, + }[WireFormatVersion] + if want == "" { + t.Fatalf("no expected manifest wire hash for version %d: bump WireFormatVersion + journal a D-entry\nhash: %s\nsurface:\n%s", WireFormatVersion, got, strings.Join(surface, "\n")) + } + if got != want { + t.Fatalf("manifest wire surface changed: bump WireFormatVersion + journal a D-entry\nversion: %d\nwant hash: %s\ngot hash: %s\nsurface:\n%s", WireFormatVersion, want, got, strings.Join(surface, "\n")) + } +} + +func manifestWireSchemaSurface() []string { + seen := make(map[reflect.Type]struct{}) + var surface []string + var walk func(reflect.Type) + walk = func(current reflect.Type) { + for current.Kind() == reflect.Pointer || current.Kind() == reflect.Slice || current.Kind() == reflect.Array || current.Kind() == reflect.Map { + current = current.Elem() + } + if current.Kind() != reflect.Struct || current.PkgPath() != reflect.TypeOf(manifestWire{}).PkgPath() { + return + } + if _, ok := seen[current]; ok { + return + } + seen[current] = struct{}{} + for i := 0; i < current.NumField(); i++ { + field := current.Field(i) + surface = append(surface, fmt.Sprintf("record:%s|field:%s|type:%s|json:%s", current.Name(), field.Name, field.Type.String(), field.Tag.Get("json"))) + walk(field.Type) + } + } + walk(reflect.TypeOf(manifestWire{})) + sort.Strings(surface) + return surface +} + +func hashManifestWireSurface(surface []string) string { + hash := sha256.New() + for _, line := range surface { + _, _ = hash.Write([]byte(line)) + _, _ = hash.Write([]byte{'\n'}) + } + return hex.EncodeToString(hash.Sum(nil)) +} diff --git a/analysis/type/subtype/subtype_test.go b/analysis/type/subtype/subtype_test.go index 3c4ec4fad..fcd49c3aa 100644 --- a/analysis/type/subtype/subtype_test.go +++ b/analysis/type/subtype/subtype_test.go @@ -37,8 +37,8 @@ func TestSubtypeDepthExhaustionFailsClosed(t *testing.T) { t.Fatal("subtype comparison succeeded after recursion-depth exhaustion") } - var sub typ.Type = typ.Number - var super typ.Type = typ.String + sub := typ.Type(typ.Number) + super := typ.Type(typ.String) for i := 0; i < typ.DefaultRecursionDepth+2; i++ { sub = typetable.NewRecord().Field("next", sub).Build() super = typetable.NewRecord().Field("next", super).Build() diff --git a/analysis/type/transform/rewrite_test.go b/analysis/type/transform/rewrite_test.go index bbc6f486b..73b478bea 100644 --- a/analysis/type/transform/rewrite_test.go +++ b/analysis/type/transform/rewrite_test.go @@ -1000,8 +1000,11 @@ func TestRewrite_RecursiveBody(t *testing.T) { t.Fatalf("expected rewritten value field, got %v", value) } next := body.GetField("next") + if next == nil { + t.Fatal("expected self-reference field, got nil") + } opt, ok := next.Type.(*typ.Optional) - if next == nil || !ok || opt.Inner != got { + if !ok || opt.Inner != got { t.Fatalf("expected self-reference to point at rewritten recursive node, got %v", next) } } diff --git a/analysis/type/typ/dedup.go b/analysis/type/typ/dedup.go index 70f41ca26..6c9a19ec7 100644 --- a/analysis/type/typ/dedup.go +++ b/analysis/type/typ/dedup.go @@ -89,10 +89,7 @@ func sameRecursiveIdentityGraph(a, b Type) bool { return false } return left.all(func(id uint64) bool { - if !right.has(id) { - return false - } - return true + return right.has(id) }) } diff --git a/analysis/type/typ/equals_test.go b/analysis/type/typ/equals_test.go index c7dc85d10..587884f32 100644 --- a/analysis/type/typ/equals_test.go +++ b/analysis/type/typ/equals_test.go @@ -213,8 +213,8 @@ func TestTypeEqualsDepthLimit(t *testing.T) { } func TestTypeEqualsDepthExhaustionFailsClosed(t *testing.T) { - var left Type = Number - var right Type = String + left := Type(Number) + right := Type(String) for i := 0; i < DefaultRecursionDepth+2; i++ { left = &Alias{Name: "Left", Target: left} right = &Alias{Name: "Right", Target: right} diff --git a/analysis/type/typ/generic.go b/analysis/type/typ/generic.go index 8a0d912f0..3f3056343 100644 --- a/analysis/type/typ/generic.go +++ b/analysis/type/typ/generic.go @@ -81,7 +81,7 @@ func (g *Generic) SetBody(body Type) { h = hash.MixHash(h, body.Hash()) g.hash = h - g.typeProperties.include(body) + g.include(body) g.strCache = stringCache{} } diff --git a/analysis/type/typ/recursive_graph_closed.go b/analysis/type/typ/recursive_graph_closed.go index b4485d946..15cde723a 100644 --- a/analysis/type/typ/recursive_graph_closed.go +++ b/analysis/type/typ/recursive_graph_closed.go @@ -86,7 +86,7 @@ func recursiveContainsGraphClosedMemo(t Type, seen map[*Recursive]bool, memo map } } - result := true + var result bool switch n := t.(type) { case nil: result = true diff --git a/analysis/type/typ/recursive_hash_deps.go b/analysis/type/typ/recursive_hash_deps.go index acf7befe2..95f3ca1c7 100644 --- a/analysis/type/typ/recursive_hash_deps.go +++ b/analysis/type/typ/recursive_hash_deps.go @@ -57,7 +57,7 @@ func collectRecursiveHashDepsInTypeMemo(t Type, seen map[*Recursive]bool, memo m } } - result := true + var result bool switch n := t.(type) { case nil: result = true diff --git a/analysis/type/typ/recursive_test.go b/analysis/type/typ/recursive_test.go index 1e8af0551..6bfe5a061 100644 --- a/analysis/type/typ/recursive_test.go +++ b/analysis/type/typ/recursive_test.go @@ -895,7 +895,7 @@ func TestRecursiveHashIntersection(t *testing.T) { } func TestRecursiveContainsGraphClosedHandlesDeepAcyclicProducts(t *testing.T) { - var body Type = String + body := Type(String) for i := 0; i < 80; i++ { body = NewArray(body) } @@ -992,7 +992,7 @@ func TestExportedContainsPredicatesSeeOpenRecursiveBodyMutation(t *testing.T) { func TestRecursiveHashDepsHandlesDeepAcyclicProducts(t *testing.T) { rec := NewRecursive("Deep", func(self Type) Type { - var body Type = self + body := self for i := 0; i < 80; i++ { body = NewArray(body) } @@ -1072,7 +1072,8 @@ func TestRecursiveHashReadonlyMapTraversesKeyAndValue(t *testing.T) { return tc.wrap(NewArray(self)) }) - if direct.Hash() != direct.Hash() { + firstHash, secondHash := direct.Hash(), direct.Hash() + if firstHash != secondHash { t.Fatal("recursive ReadonlyMap hash should be stable") } if direct.Hash() == nested.Hash() { @@ -1106,7 +1107,8 @@ func TestRecursiveHashStaticMemberTraversesType(t *testing.T) { Build() }) - if direct.Hash() != direct.Hash() { + firstHash, secondHash := direct.Hash(), direct.Hash() + if firstHash != secondHash { t.Fatal("recursive static-member hash should be stable") } if direct.Hash() == nested.Hash() { diff --git a/baselib.go b/baselib.go index ff8a23c7c..ab529e7b6 100644 --- a/baselib.go +++ b/baselib.go @@ -171,20 +171,16 @@ func basePCall(L *LState) int { }() if err != nil { - L.stack.SetSp(sp) - L.currentFrame = L.stack.Last() + L.unwindCallFrames(sp) L.reg.SetTop(base) L.currentFrame.Protected = false - // Clear continuation info after error - if ext := L.getFrameExt(L.currentFrame); ext != nil { - ext.Continuation = nil - ext.ContinuationCtx = nil - } + L.clearFrameExt(L.currentFrame) L.Push(LFalse) L.Push(err.(*ApiError).Object) return 2 } L.currentFrame.Protected = false + L.clearFrameExt(L.currentFrame) } else { // In coroutine - use continuation for yield-transparency // Still need defer/recover for error handling @@ -211,20 +207,16 @@ func basePCall(L *LState) int { } if err != nil { - L.stack.SetSp(sp) - L.currentFrame = L.stack.Last() + L.unwindCallFrames(sp) L.reg.SetTop(base) L.currentFrame.Protected = false - // Clear continuation info after error - if ext := L.getFrameExt(L.currentFrame); ext != nil { - ext.Continuation = nil - ext.ContinuationCtx = nil - } + L.clearFrameExt(L.currentFrame) L.Push(LFalse) L.Push(err.(*ApiError).Object) return 2 } L.currentFrame.Protected = false + L.clearFrameExt(L.currentFrame) } L.Insert(LTrue, 1) @@ -404,28 +396,89 @@ func xpcallContinuation(L *LState, ctx interface{}, _ ResumeState) int { func baseXPCall(L *LState) int { fn := L.CheckFunction(1) errfunc := L.CheckFunction(2) + protectedFrame := L.currentFrame - // Mark the xpcall frame as protected for error handling - L.currentFrame.Protected = true - L.setFrameExt(L.currentFrame).ErrFunc = errfunc + // Mark the xpcall frame as protected. handleProtectedError (in threadRun) + // honors this for errors that surface after a yield inside a coroutine. + // The inline recover below is the boundary for synchronous errors and is + // the only recover layer present under a direct DoString/PCall call, which + // is why xpcall previously leaked its error in non-coroutine contexts. + protectedFrame.Protected = true + L.setFrameExt(protectedFrame).ErrFunc = errfunc top := L.GetTop() + sp := L.stack.Sp() L.Push(fn) + base := L.reg.Top() - 1 // nargs == 0 for xpcall's protected call - // Use CallK with continuation for yield-transparent xpcall - L.CallK(0, MultRet, xpcallContinuation, top) + var errValue LValue + func() { + defer func() { + if rcv := recover(); rcv != nil { + errValue = errorObjectFromRecover(rcv) + } + }() + L.CallK(0, MultRet, xpcallContinuation, top) + }() - // Check for yield before any cleanup + // Check for yield before any cleanup. if L.yieldState != yieldNone { return -1 } - L.currentFrame.Protected = false - if ext := L.getFrameExt(L.currentFrame); ext != nil { - ext.ErrFunc = nil + if errValue != nil { + // Invoke the handler BEFORE resetting the stack, while the errored + // frames are still on L.stack. This matches standard Lua semantics so + // the handler can inspect the throw site (debug.getlocal, + // debug.traceback, etc.). PCall's own errfunc path does the same. + handled := invokeErrorHandler(L, errfunc, errValue) + + L.unwindCallFrames(sp) + L.reg.SetTop(base) + protectedFrame.Protected = false + L.clearFrameExt(protectedFrame) + L.Push(LFalse) + L.Push(handled) + return 2 } + + protectedFrame.Protected = false + L.clearFrameExt(protectedFrame) L.Insert(LTrue, top+1) return L.GetTop() - top } +// errorObjectFromRecover extracts the Lua value carried by a recovered panic. +// Lua errors panic with *ApiError; anything else is stringified. +func errorObjectFromRecover(rcv any) LValue { + if aerr, ok := rcv.(*ApiError); ok { + return aerr.Object + } + + return LString(fmt.Sprint(rcv)) +} + +// invokeErrorHandler calls the xpcall error handler with the caught error +// value while the throw-site frames are still on the stack. If the handler +// itself raises, its error value is surfaced instead of the original. Stack +// cleanup belongs to baseXPCall so both handler outcomes use the same unwind. +func invokeErrorHandler(L *LState, errfunc *LFunction, errValue LValue) LValue { + L.Push(errfunc) + L.Push(errValue) + + handled := errValue + func() { + defer func() { + if rcv := recover(); rcv != nil { + handled = errorObjectFromRecover(rcv) + } + }() + + L.Call(1, 1) + handled = L.Get(-1) + }() + + return handled +} + /* }}} */ diff --git a/compiler/parse/generate.go b/compiler/parse/generate.go new file mode 100644 index 000000000..26b3b93a7 --- /dev/null +++ b/compiler/parse/generate.go @@ -0,0 +1,6 @@ +package parse + +// Keep the parser generator pinned to a release that supports the module's +// Go 1.23 toolchain. CI regenerates parser.go and rejects a dirty result. +// +//go:generate sh generate.sh diff --git a/compiler/parse/generate.sh b/compiler/parse/generate.sh new file mode 100644 index 000000000..7cc5b8ddc --- /dev/null +++ b/compiler/parse/generate.sh @@ -0,0 +1,5 @@ +#!/bin/sh +set -eu + +trap 'rm -f y.output' EXIT +go run golang.org/x/tools/cmd/goyacc@v0.34.0 -o parser.go parser.go.y diff --git a/compiler/parse/parser.go b/compiler/parse/parser.go index 8a1d665af..36d8db1ec 100644 --- a/compiler/parse/parser.go +++ b/compiler/parse/parser.go @@ -3,15 +3,15 @@ //line parser.go.y:2 package parse -import ( - __yyfmt__ "fmt" +import __yyfmt__ "fmt" + +//line parser.go.y:2 +import ( "github.com/wippyai/go-lua/compiler/ast" "github.com/wippyai/go-lua/compiler/parse/numparse" ) -//line parser.go.y:2 - func setLastPosFromExprs(node ast.PositionHolder, exprs []ast.Expr, fallback ast.PositionHolder) { if node == nil { return @@ -25,7 +25,7 @@ func setLastPosFromExprs(node ast.PositionHolder, exprs []ast.Expr, fallback ast } } -//line parser.go.y:87 +//line parser.go.y:71 type yySymType struct { yys int token ast.Token @@ -203,7 +203,7 @@ const yyEofCode = 1 const yyErrCode = 2 const yyInitialStackSize = 16 -//line parser.go.y:1303 +//line parser.go.y:1297 // nameWithPos holds a name with its token position type nameWithPos struct { @@ -1077,7 +1077,7 @@ yydefault: case 1: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:166 +//line parser.go.y:153 { yyVAL.stmts = yyDollar[1].stmts if l, ok := yylex.(*Lexer); ok { @@ -1086,7 +1086,7 @@ yydefault: } case 2: yyDollar = yyS[yypt-2 : yypt+1] -//line parser.go.y:172 +//line parser.go.y:159 { yyVAL.stmts = append(yyDollar[1].stmts, yyDollar[2].stmt) if l, ok := yylex.(*Lexer); ok { @@ -1095,7 +1095,7 @@ yydefault: } case 3: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:178 +//line parser.go.y:165 { yyVAL.stmts = append(yyDollar[1].stmts, yyDollar[2].stmt) if l, ok := yylex.(*Lexer); ok { @@ -1104,38 +1104,38 @@ yydefault: } case 4: yyDollar = yyS[yypt-0 : yypt+1] -//line parser.go.y:186 +//line parser.go.y:173 { yyVAL.stmts = []ast.Stmt{} } case 5: yyDollar = yyS[yypt-2 : yypt+1] -//line parser.go.y:189 +//line parser.go.y:176 { yyVAL.stmts = append(yyDollar[1].stmts, yyDollar[2].stmt) } case 6: yyDollar = yyS[yypt-2 : yypt+1] -//line parser.go.y:192 +//line parser.go.y:179 { yyVAL.stmts = yyDollar[1].stmts } case 7: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:197 +//line parser.go.y:184 { yyVAL.stmts = yyDollar[1].stmts } case 8: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:202 +//line parser.go.y:189 { yyVAL.stmt = &ast.AssignStmt{Lhs: yyDollar[1].exprlist, Rhs: yyDollar[3].exprlist} yyVAL.stmt.CopyPos(yyDollar[1].exprlist[0]) } case 9: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:207 +//line parser.go.y:194 { if _, ok := yyDollar[1].expr.(*ast.FuncCallExpr); !ok { yylex.(*Lexer).Error("parse error") @@ -1146,7 +1146,7 @@ yydefault: } case 10: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:215 +//line parser.go.y:202 { yyVAL.stmt = &ast.DoBlockStmt{Stmts: yyDollar[2].stmts} yyVAL.stmt.SetPosFromToken(yyDollar[1].token.Pos) @@ -1154,7 +1154,7 @@ yydefault: } case 11: yyDollar = yyS[yypt-5 : yypt+1] -//line parser.go.y:220 +//line parser.go.y:207 { yyVAL.stmt = &ast.WhileStmt{Condition: yyDollar[2].expr, Stmts: yyDollar[4].stmts} yyVAL.stmt.SetPosFromToken(yyDollar[1].token.Pos) @@ -1162,7 +1162,7 @@ yydefault: } case 12: yyDollar = yyS[yypt-4 : yypt+1] -//line parser.go.y:225 +//line parser.go.y:212 { yyVAL.stmt = &ast.RepeatStmt{Condition: yyDollar[4].expr, Stmts: yyDollar[2].stmts} yyVAL.stmt.SetPosFromToken(yyDollar[1].token.Pos) @@ -1170,7 +1170,7 @@ yydefault: } case 13: yyDollar = yyS[yypt-6 : yypt+1] -//line parser.go.y:230 +//line parser.go.y:217 { yyVAL.stmt = &ast.IfStmt{Condition: yyDollar[2].expr, Then: yyDollar[4].stmts} cur := yyVAL.stmt @@ -1183,7 +1183,7 @@ yydefault: } case 14: yyDollar = yyS[yypt-8 : yypt+1] -//line parser.go.y:240 +//line parser.go.y:227 { yyVAL.stmt = &ast.IfStmt{Condition: yyDollar[2].expr, Then: yyDollar[4].stmts} cur := yyVAL.stmt @@ -1197,7 +1197,7 @@ yydefault: } case 15: yyDollar = yyS[yypt-9 : yypt+1] -//line parser.go.y:251 +//line parser.go.y:238 { yyVAL.stmt = &ast.NumberForStmt{Name: yyDollar[2].token.Str, NamePosition: yyDollar[2].token.Pos, Init: yyDollar[4].expr, Limit: yyDollar[6].expr, Stmts: yyDollar[8].stmts} yyVAL.stmt.SetPosFromToken(yyDollar[1].token.Pos) @@ -1205,7 +1205,7 @@ yydefault: } case 16: yyDollar = yyS[yypt-11 : yypt+1] -//line parser.go.y:256 +//line parser.go.y:243 { yyVAL.stmt = &ast.NumberForStmt{Name: yyDollar[2].token.Str, NamePosition: yyDollar[2].token.Pos, Init: yyDollar[4].expr, Limit: yyDollar[6].expr, Step: yyDollar[8].expr, Stmts: yyDollar[10].stmts} yyVAL.stmt.SetPosFromToken(yyDollar[1].token.Pos) @@ -1213,7 +1213,7 @@ yydefault: } case 17: yyDollar = yyS[yypt-7 : yypt+1] -//line parser.go.y:261 +//line parser.go.y:248 { names, positions := splitNameList(yyDollar[2].namelist) yyVAL.stmt = &ast.GenericForStmt{Names: names, NamePositions: positions, Exprs: yyDollar[4].exprlist, Stmts: yyDollar[6].stmts} @@ -1222,7 +1222,7 @@ yydefault: } case 18: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:267 +//line parser.go.y:254 { yyVAL.stmt = &ast.FuncDefStmt{Name: yyDollar[2].funcname, Func: yyDollar[3].funcexpr} yyVAL.stmt.SetPosFromToken(yyDollar[1].token.Pos) @@ -1230,7 +1230,7 @@ yydefault: } case 19: yyDollar = yyS[yypt-4 : yypt+1] -//line parser.go.y:272 +//line parser.go.y:259 { yyVAL.stmt = &ast.LocalAssignStmt{Names: []string{yyDollar[3].token.Str}, NamePositions: []ast.Position{yyDollar[3].token.Pos}, Exprs: []ast.Expr{yyDollar[4].funcexpr}} yyVAL.stmt.SetPosFromToken(yyDollar[1].token.Pos) @@ -1238,7 +1238,7 @@ yydefault: } case 20: yyDollar = yyS[yypt-4 : yypt+1] -//line parser.go.y:277 +//line parser.go.y:264 { names, positions, types := splitTypedNames(yyDollar[2].typednames) yyVAL.stmt = &ast.LocalAssignStmt{Names: names, NamePositions: positions, Types: types, Exprs: yyDollar[4].exprlist} @@ -1246,7 +1246,7 @@ yydefault: } case 21: yyDollar = yyS[yypt-2 : yypt+1] -//line parser.go.y:282 +//line parser.go.y:269 { names, positions, types := splitTypedNames(yyDollar[2].typednames) yyVAL.stmt = &ast.LocalAssignStmt{Names: names, NamePositions: positions, Types: types, Exprs: []ast.Expr{}} @@ -1254,21 +1254,21 @@ yydefault: } case 22: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:287 +//line parser.go.y:274 { yyVAL.stmt = &ast.LabelStmt{Name: yyDollar[1].token.Str} yyVAL.stmt.SetPosFromToken(yyDollar[1].token.Pos) } case 23: yyDollar = yyS[yypt-2 : yypt+1] -//line parser.go.y:291 +//line parser.go.y:278 { yyVAL.stmt = &ast.GotoStmt{Label: yyDollar[2].token.Str} yyVAL.stmt.SetPosFromToken(yyDollar[1].token.Pos) } case 24: yyDollar = yyS[yypt-4 : yypt+1] -//line parser.go.y:295 +//line parser.go.y:282 { if yyDollar[1].token.Str != "type" { yylex.(*Lexer).Error("unexpected identifier") @@ -1278,7 +1278,7 @@ yydefault: } case 25: yyDollar = yyS[yypt-5 : yypt+1] -//line parser.go.y:302 +//line parser.go.y:289 { if yyDollar[1].token.Str != "type" { yylex.(*Lexer).Error("unexpected identifier") @@ -1288,7 +1288,7 @@ yydefault: } case 26: yyDollar = yyS[yypt-5 : yypt+1] -//line parser.go.y:309 +//line parser.go.y:296 { yyVAL.stmt = &ast.InterfaceDefStmt{Name: yyDollar[2].token.Str, Extends: yyDollar[3].typereflist, Methods: yyDollar[4].ifacemethods} yyVAL.stmt.SetPosFromToken(yyDollar[1].token.Pos) @@ -1296,96 +1296,96 @@ yydefault: } case 27: yyDollar = yyS[yypt-0 : yypt+1] -//line parser.go.y:316 +//line parser.go.y:303 { yyVAL.stmts = []ast.Stmt{} } case 28: yyDollar = yyS[yypt-5 : yypt+1] -//line parser.go.y:319 +//line parser.go.y:306 { yyVAL.stmts = append(yyDollar[1].stmts, &ast.IfStmt{Condition: yyDollar[3].expr, Then: yyDollar[5].stmts}) yyVAL.stmts[len(yyVAL.stmts)-1].SetPosFromToken(yyDollar[2].token.Pos) } case 29: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:325 +//line parser.go.y:312 { yyVAL.stmt = &ast.ReturnStmt{Exprs: nil} yyVAL.stmt.SetPosFromToken(yyDollar[1].token.Pos) } case 30: yyDollar = yyS[yypt-2 : yypt+1] -//line parser.go.y:329 +//line parser.go.y:316 { yyVAL.stmt = &ast.ReturnStmt{Exprs: yyDollar[2].exprlist} yyVAL.stmt.SetPosFromToken(yyDollar[1].token.Pos) } case 31: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:333 +//line parser.go.y:320 { yyVAL.stmt = &ast.BreakStmt{} yyVAL.stmt.SetPosFromToken(yyDollar[1].token.Pos) } case 32: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:339 +//line parser.go.y:326 { yyVAL.funcname = yyDollar[1].funcname } case 33: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:342 +//line parser.go.y:329 { yyVAL.funcname = &ast.FuncName{Func: nil, Receiver: yyDollar[1].funcname.Func, Method: yyDollar[3].token.Str} } case 34: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:345 +//line parser.go.y:332 { yyVAL.funcname = &ast.FuncName{Func: nil, Receiver: yyDollar[1].funcname.Func, Method: "type"} } case 35: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:348 +//line parser.go.y:335 { yyVAL.funcname = &ast.FuncName{Func: nil, Receiver: yyDollar[1].funcname.Func, Method: "interface"} } case 36: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:351 +//line parser.go.y:338 { yyVAL.funcname = &ast.FuncName{Func: nil, Receiver: yyDollar[1].funcname.Func, Method: "readonly"} } case 37: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:354 +//line parser.go.y:341 { yyVAL.funcname = &ast.FuncName{Func: nil, Receiver: yyDollar[1].funcname.Func, Method: "as"} } case 38: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:357 +//line parser.go.y:344 { yyVAL.funcname = &ast.FuncName{Func: nil, Receiver: yyDollar[1].funcname.Func, Method: "asserts"} } case 39: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:360 +//line parser.go.y:347 { yyVAL.funcname = &ast.FuncName{Func: nil, Receiver: yyDollar[1].funcname.Func, Method: "is"} } case 40: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:365 +//line parser.go.y:352 { yyVAL.funcname = &ast.FuncName{Func: &ast.IdentExpr{Value: yyDollar[1].token.Str}} yyVAL.funcname.Func.SetPosFromToken(yyDollar[1].token.Pos) } case 41: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:369 +//line parser.go.y:356 { key := &ast.StringExpr{Value: yyDollar[3].token.Str} key.SetPosFromToken(yyDollar[3].token.Pos) @@ -1397,26 +1397,26 @@ yydefault: } case 42: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:380 +//line parser.go.y:367 { yyVAL.exprlist = []ast.Expr{yyDollar[1].expr} } case 43: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:383 +//line parser.go.y:370 { yyVAL.exprlist = append(yyDollar[1].exprlist, yyDollar[3].expr) } case 44: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:388 +//line parser.go.y:375 { yyVAL.expr = &ast.IdentExpr{Value: yyDollar[1].token.Str} yyVAL.expr.SetPosFromToken(yyDollar[1].token.Pos) } case 45: yyDollar = yyS[yypt-4 : yypt+1] -//line parser.go.y:392 +//line parser.go.y:379 { yyVAL.expr = &ast.AttrGetExpr{Object: yyDollar[1].expr, Key: yyDollar[3].expr, KeySyntax: ast.AttrKeyIndex} yyVAL.expr.CopyPos(yyDollar[1].expr) @@ -1425,7 +1425,7 @@ yydefault: } case 46: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:396 +//line parser.go.y:385 { key := &ast.StringExpr{Value: yyDollar[3].token.Str} key.SetPosFromToken(yyDollar[3].token.Pos) @@ -1436,7 +1436,7 @@ yydefault: } case 47: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:404 +//line parser.go.y:393 { key := &ast.StringExpr{Value: "type"} key.SetPosFromToken(yyDollar[3].token.Pos) @@ -1447,7 +1447,7 @@ yydefault: } case 48: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:412 +//line parser.go.y:401 { key := &ast.StringExpr{Value: "interface"} key.SetPosFromToken(yyDollar[3].token.Pos) @@ -1458,7 +1458,7 @@ yydefault: } case 49: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:420 +//line parser.go.y:409 { key := &ast.StringExpr{Value: "readonly"} key.SetPosFromToken(yyDollar[3].token.Pos) @@ -1469,7 +1469,7 @@ yydefault: } case 50: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:428 +//line parser.go.y:417 { key := &ast.StringExpr{Value: "as"} key.SetPosFromToken(yyDollar[3].token.Pos) @@ -1480,7 +1480,7 @@ yydefault: } case 51: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:436 +//line parser.go.y:425 { key := &ast.StringExpr{Value: "asserts"} key.SetPosFromToken(yyDollar[3].token.Pos) @@ -1491,7 +1491,7 @@ yydefault: } case 52: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:444 +//line parser.go.y:433 { key := &ast.StringExpr{Value: "is"} key.SetPosFromToken(yyDollar[3].token.Pos) @@ -1502,90 +1502,90 @@ yydefault: } case 53: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:454 +//line parser.go.y:443 { yyVAL.namelist = []nameWithPos{{Name: yyDollar[1].token.Str, Pos: yyDollar[1].token.Pos}} } case 54: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:457 +//line parser.go.y:446 { yyVAL.namelist = append(yyDollar[1].namelist, nameWithPos{Name: yyDollar[3].token.Str, Pos: yyDollar[3].token.Pos}) } case 55: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:462 +//line parser.go.y:451 { yyVAL.exprlist = []ast.Expr{yyDollar[1].expr} } case 56: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:465 +//line parser.go.y:454 { yyVAL.exprlist = append(yyDollar[1].exprlist, yyDollar[3].expr) } case 57: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:470 +//line parser.go.y:459 { yyVAL.expr = &ast.NilExpr{} yyVAL.expr.SetPosFromToken(yyDollar[1].token.Pos) } case 58: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:474 +//line parser.go.y:463 { yyVAL.expr = &ast.FalseExpr{} yyVAL.expr.SetPosFromToken(yyDollar[1].token.Pos) } case 59: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:478 +//line parser.go.y:467 { yyVAL.expr = &ast.TrueExpr{} yyVAL.expr.SetPosFromToken(yyDollar[1].token.Pos) } case 60: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:482 +//line parser.go.y:471 { yyVAL.expr = &ast.NumberExpr{Value: yyDollar[1].token.Str} yyVAL.expr.SetPosFromToken(yyDollar[1].token.Pos) } case 61: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:486 +//line parser.go.y:475 { yyVAL.expr = &ast.Comma3Expr{} yyVAL.expr.SetPosFromToken(yyDollar[1].token.Pos) } case 62: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:490 +//line parser.go.y:479 { yyVAL.expr = yyDollar[1].expr } case 63: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:493 +//line parser.go.y:482 { yyVAL.expr = yyDollar[1].expr } case 64: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:496 +//line parser.go.y:485 { yyVAL.expr = yyDollar[1].expr } case 65: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:499 +//line parser.go.y:488 { yyVAL.expr = yyDollar[1].expr } case 66: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:502 +//line parser.go.y:491 { yyVAL.expr = &ast.LogicalOpExpr{Lhs: yyDollar[1].expr, Operator: "or", Rhs: yyDollar[3].expr} yyVAL.expr.CopyPos(yyDollar[1].expr) @@ -1593,7 +1593,7 @@ yydefault: } case 67: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:507 +//line parser.go.y:496 { yyVAL.expr = &ast.LogicalOpExpr{Lhs: yyDollar[1].expr, Operator: "and", Rhs: yyDollar[3].expr} yyVAL.expr.CopyPos(yyDollar[1].expr) @@ -1601,7 +1601,7 @@ yydefault: } case 68: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:512 +//line parser.go.y:501 { yyVAL.expr = &ast.RelationalOpExpr{Lhs: yyDollar[1].expr, Operator: ">", Rhs: yyDollar[3].expr} yyVAL.expr.CopyPos(yyDollar[1].expr) @@ -1609,7 +1609,7 @@ yydefault: } case 69: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:517 +//line parser.go.y:506 { yyVAL.expr = &ast.RelationalOpExpr{Lhs: yyDollar[1].expr, Operator: "<", Rhs: yyDollar[3].expr} yyVAL.expr.CopyPos(yyDollar[1].expr) @@ -1617,7 +1617,7 @@ yydefault: } case 70: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:522 +//line parser.go.y:511 { yyVAL.expr = &ast.RelationalOpExpr{Lhs: yyDollar[1].expr, Operator: ">=", Rhs: yyDollar[3].expr} yyVAL.expr.CopyPos(yyDollar[1].expr) @@ -1625,7 +1625,7 @@ yydefault: } case 71: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:527 +//line parser.go.y:516 { yyVAL.expr = &ast.RelationalOpExpr{Lhs: yyDollar[1].expr, Operator: "<=", Rhs: yyDollar[3].expr} yyVAL.expr.CopyPos(yyDollar[1].expr) @@ -1633,7 +1633,7 @@ yydefault: } case 72: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:532 +//line parser.go.y:521 { yyVAL.expr = &ast.RelationalOpExpr{Lhs: yyDollar[1].expr, Operator: "==", Rhs: yyDollar[3].expr} yyVAL.expr.CopyPos(yyDollar[1].expr) @@ -1641,7 +1641,7 @@ yydefault: } case 73: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:537 +//line parser.go.y:526 { yyVAL.expr = &ast.RelationalOpExpr{Lhs: yyDollar[1].expr, Operator: "~=", Rhs: yyDollar[3].expr} yyVAL.expr.CopyPos(yyDollar[1].expr) @@ -1649,7 +1649,7 @@ yydefault: } case 74: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:542 +//line parser.go.y:531 { yyVAL.expr = &ast.StringConcatOpExpr{Lhs: yyDollar[1].expr, Rhs: yyDollar[3].expr} yyVAL.expr.CopyPos(yyDollar[1].expr) @@ -1657,7 +1657,7 @@ yydefault: } case 75: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:547 +//line parser.go.y:536 { yyVAL.expr = &ast.ArithmeticOpExpr{Lhs: yyDollar[1].expr, Operator: "+", Rhs: yyDollar[3].expr} yyVAL.expr.CopyPos(yyDollar[1].expr) @@ -1665,7 +1665,7 @@ yydefault: } case 76: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:552 +//line parser.go.y:541 { yyVAL.expr = &ast.ArithmeticOpExpr{Lhs: yyDollar[1].expr, Operator: "-", Rhs: yyDollar[3].expr} yyVAL.expr.CopyPos(yyDollar[1].expr) @@ -1673,7 +1673,7 @@ yydefault: } case 77: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:557 +//line parser.go.y:546 { yyVAL.expr = &ast.ArithmeticOpExpr{Lhs: yyDollar[1].expr, Operator: "*", Rhs: yyDollar[3].expr} yyVAL.expr.CopyPos(yyDollar[1].expr) @@ -1681,7 +1681,7 @@ yydefault: } case 78: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:562 +//line parser.go.y:551 { yyVAL.expr = &ast.ArithmeticOpExpr{Lhs: yyDollar[1].expr, Operator: "/", Rhs: yyDollar[3].expr} yyVAL.expr.CopyPos(yyDollar[1].expr) @@ -1689,7 +1689,7 @@ yydefault: } case 79: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:567 +//line parser.go.y:556 { yyVAL.expr = &ast.ArithmeticOpExpr{Lhs: yyDollar[1].expr, Operator: "%", Rhs: yyDollar[3].expr} yyVAL.expr.CopyPos(yyDollar[1].expr) @@ -1697,7 +1697,7 @@ yydefault: } case 80: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:572 +//line parser.go.y:561 { yyVAL.expr = &ast.ArithmeticOpExpr{Lhs: yyDollar[1].expr, Operator: "^", Rhs: yyDollar[3].expr} yyVAL.expr.CopyPos(yyDollar[1].expr) @@ -1705,7 +1705,7 @@ yydefault: } case 81: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:577 +//line parser.go.y:566 { yyVAL.expr = &ast.ArithmeticOpExpr{Lhs: yyDollar[1].expr, Operator: "&", Rhs: yyDollar[3].expr} yyVAL.expr.CopyPos(yyDollar[1].expr) @@ -1713,7 +1713,7 @@ yydefault: } case 82: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:582 +//line parser.go.y:571 { yyVAL.expr = &ast.ArithmeticOpExpr{Lhs: yyDollar[1].expr, Operator: "|", Rhs: yyDollar[3].expr} yyVAL.expr.CopyPos(yyDollar[1].expr) @@ -1721,7 +1721,7 @@ yydefault: } case 83: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:587 +//line parser.go.y:576 { yyVAL.expr = &ast.ArithmeticOpExpr{Lhs: yyDollar[1].expr, Operator: "~", Rhs: yyDollar[3].expr} yyVAL.expr.CopyPos(yyDollar[1].expr) @@ -1729,7 +1729,7 @@ yydefault: } case 84: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:592 +//line parser.go.y:581 { yyVAL.expr = &ast.ArithmeticOpExpr{Lhs: yyDollar[1].expr, Operator: "<<", Rhs: yyDollar[3].expr} yyVAL.expr.CopyPos(yyDollar[1].expr) @@ -1737,7 +1737,7 @@ yydefault: } case 85: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:597 +//line parser.go.y:586 { yyVAL.expr = &ast.ArithmeticOpExpr{Lhs: yyDollar[1].expr, Operator: ">>", Rhs: yyDollar[3].expr} yyVAL.expr.CopyPos(yyDollar[1].expr) @@ -1745,7 +1745,7 @@ yydefault: } case 86: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:602 +//line parser.go.y:591 { yyVAL.expr = &ast.ArithmeticOpExpr{Lhs: yyDollar[1].expr, Operator: "//", Rhs: yyDollar[3].expr} yyVAL.expr.CopyPos(yyDollar[1].expr) @@ -1753,7 +1753,7 @@ yydefault: } case 87: yyDollar = yyS[yypt-2 : yypt+1] -//line parser.go.y:607 +//line parser.go.y:596 { yyVAL.expr = &ast.UnaryMinusOpExpr{Expr: yyDollar[2].expr} yyVAL.expr.CopyPos(yyDollar[2].expr) @@ -1761,7 +1761,7 @@ yydefault: } case 88: yyDollar = yyS[yypt-2 : yypt+1] -//line parser.go.y:612 +//line parser.go.y:601 { yyVAL.expr = &ast.UnaryNotOpExpr{Expr: yyDollar[2].expr} yyVAL.expr.SetPosFromToken(yyDollar[1].token.Pos) @@ -1769,7 +1769,7 @@ yydefault: } case 89: yyDollar = yyS[yypt-2 : yypt+1] -//line parser.go.y:617 +//line parser.go.y:606 { yyVAL.expr = &ast.UnaryLenOpExpr{Expr: yyDollar[2].expr} yyVAL.expr.CopyPos(yyDollar[2].expr) @@ -1777,7 +1777,7 @@ yydefault: } case 90: yyDollar = yyS[yypt-2 : yypt+1] -//line parser.go.y:622 +//line parser.go.y:611 { yyVAL.expr = &ast.UnaryBNotOpExpr{Expr: yyDollar[2].expr} yyVAL.expr.CopyPos(yyDollar[2].expr) @@ -1785,7 +1785,7 @@ yydefault: } case 91: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:627 +//line parser.go.y:616 { yyVAL.expr = &ast.CastExpr{Expr: yyDollar[1].expr, Type: yyDollar[3].typeexpr, Syntax: ast.CastSyntaxAs} yyVAL.expr.CopyPos(yyDollar[1].expr) @@ -1793,7 +1793,7 @@ yydefault: } case 92: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:632 +//line parser.go.y:621 { yyVAL.expr = &ast.CastExpr{Expr: yyDollar[1].expr, Type: yyDollar[3].typeexpr, Syntax: ast.CastSyntaxColonColon} yyVAL.expr.CopyPos(yyDollar[1].expr) @@ -1801,7 +1801,7 @@ yydefault: } case 93: yyDollar = yyS[yypt-2 : yypt+1] -//line parser.go.y:637 +//line parser.go.y:626 { yyVAL.expr = &ast.NonNilAssertExpr{Expr: yyDollar[1].expr} yyVAL.expr.CopyPos(yyDollar[1].expr) @@ -1809,32 +1809,32 @@ yydefault: } case 94: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:644 +//line parser.go.y:633 { yyVAL.expr = &ast.StringExpr{Value: yyDollar[1].token.Str} yyVAL.expr.SetPosFromToken(yyDollar[1].token.Pos) } case 95: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:650 +//line parser.go.y:639 { yyVAL.expr = yyDollar[1].expr } case 96: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:653 +//line parser.go.y:642 { yyVAL.expr = yyDollar[1].expr } case 97: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:656 +//line parser.go.y:645 { yyVAL.expr = yyDollar[1].expr } case 98: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:659 +//line parser.go.y:648 { if ex, ok := yyDollar[2].expr.(*ast.Comma3Expr); ok { ex.AdjustRet = true @@ -1845,14 +1845,14 @@ yydefault: } case 99: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:669 +//line parser.go.y:658 { yyDollar[2].expr.(*ast.FuncCallExpr).AdjustRet = true yyVAL.expr = yyDollar[2].expr } case 100: yyDollar = yyS[yypt-2 : yypt+1] -//line parser.go.y:675 +//line parser.go.y:664 { yyVAL.expr = &ast.FuncCallExpr{Func: yyDollar[1].expr, Args: yyDollar[2].exprlist} yyVAL.expr.CopyPos(yyDollar[1].expr) @@ -1860,7 +1860,7 @@ yydefault: } case 101: yyDollar = yyS[yypt-4 : yypt+1] -//line parser.go.y:680 +//line parser.go.y:669 { yyVAL.expr = &ast.FuncCallExpr{Method: yyDollar[3].token.Str, Receiver: yyDollar[1].expr, Args: yyDollar[4].exprlist} yyVAL.expr.CopyPos(yyDollar[1].expr) @@ -1868,7 +1868,7 @@ yydefault: } case 102: yyDollar = yyS[yypt-4 : yypt+1] -//line parser.go.y:685 +//line parser.go.y:674 { yyVAL.expr = &ast.FuncCallExpr{Method: "type", Receiver: yyDollar[1].expr, Args: yyDollar[4].exprlist} yyVAL.expr.CopyPos(yyDollar[1].expr) @@ -1876,7 +1876,7 @@ yydefault: } case 103: yyDollar = yyS[yypt-4 : yypt+1] -//line parser.go.y:690 +//line parser.go.y:679 { yyVAL.expr = &ast.FuncCallExpr{Method: "interface", Receiver: yyDollar[1].expr, Args: yyDollar[4].exprlist} yyVAL.expr.CopyPos(yyDollar[1].expr) @@ -1884,7 +1884,7 @@ yydefault: } case 104: yyDollar = yyS[yypt-4 : yypt+1] -//line parser.go.y:695 +//line parser.go.y:684 { yyVAL.expr = &ast.FuncCallExpr{Method: "readonly", Receiver: yyDollar[1].expr, Args: yyDollar[4].exprlist} yyVAL.expr.CopyPos(yyDollar[1].expr) @@ -1892,7 +1892,7 @@ yydefault: } case 105: yyDollar = yyS[yypt-4 : yypt+1] -//line parser.go.y:700 +//line parser.go.y:689 { yyVAL.expr = &ast.FuncCallExpr{Method: "as", Receiver: yyDollar[1].expr, Args: yyDollar[4].exprlist} yyVAL.expr.CopyPos(yyDollar[1].expr) @@ -1900,7 +1900,7 @@ yydefault: } case 106: yyDollar = yyS[yypt-4 : yypt+1] -//line parser.go.y:705 +//line parser.go.y:694 { yyVAL.expr = &ast.FuncCallExpr{Method: "asserts", Receiver: yyDollar[1].expr, Args: yyDollar[4].exprlist} yyVAL.expr.CopyPos(yyDollar[1].expr) @@ -1908,7 +1908,7 @@ yydefault: } case 107: yyDollar = yyS[yypt-4 : yypt+1] -//line parser.go.y:710 +//line parser.go.y:699 { yyVAL.expr = &ast.FuncCallExpr{Method: "is", Receiver: yyDollar[1].expr, Args: yyDollar[4].exprlist} yyVAL.expr.CopyPos(yyDollar[1].expr) @@ -1916,7 +1916,7 @@ yydefault: } case 108: yyDollar = yyS[yypt-2 : yypt+1] -//line parser.go.y:717 +//line parser.go.y:706 { if yylex.(*Lexer).PNewLine { yylex.(*Lexer).TokenError(yyDollar[1].token, "ambiguous syntax (function call x new statement)") @@ -1925,7 +1925,7 @@ yydefault: } case 109: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:723 +//line parser.go.y:712 { if yylex.(*Lexer).PNewLine { yylex.(*Lexer).TokenError(yyDollar[1].token, "ambiguous syntax (function call x new statement)") @@ -1934,19 +1934,19 @@ yydefault: } case 110: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:729 +//line parser.go.y:718 { yyVAL.exprlist = []ast.Expr{yyDollar[1].expr} } case 111: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:732 +//line parser.go.y:721 { yyVAL.exprlist = []ast.Expr{yyDollar[1].expr} } case 112: yyDollar = yyS[yypt-2 : yypt+1] -//line parser.go.y:737 +//line parser.go.y:726 { yyVAL.expr = &ast.FunctionExpr{TypeParams: yyDollar[2].funcexpr.TypeParams, ParList: yyDollar[2].funcexpr.ParList, ReturnTypes: yyDollar[2].funcexpr.ReturnTypes, Stmts: yyDollar[2].funcexpr.Stmts} yyVAL.expr.SetPosFromToken(yyDollar[1].token.Pos) @@ -1954,7 +1954,7 @@ yydefault: } case 113: yyDollar = yyS[yypt-6 : yypt+1] -//line parser.go.y:744 +//line parser.go.y:733 { yyVAL.funcexpr = &ast.FunctionExpr{ParList: yyDollar[2].parlist, ReturnTypes: yyDollar[4].typeexprlist, Stmts: yyDollar[5].stmts} yyVAL.funcexpr.SetPosFromToken(yyDollar[1].token.Pos) @@ -1962,7 +1962,7 @@ yydefault: } case 114: yyDollar = yyS[yypt-5 : yypt+1] -//line parser.go.y:749 +//line parser.go.y:738 { yyVAL.funcexpr = &ast.FunctionExpr{ParList: &ast.ParList{HasVargs: false, Names: []string{}}, ReturnTypes: yyDollar[3].typeexprlist, Stmts: yyDollar[4].stmts} yyVAL.funcexpr.SetPosFromToken(yyDollar[1].token.Pos) @@ -1970,7 +1970,7 @@ yydefault: } case 115: yyDollar = yyS[yypt-7 : yypt+1] -//line parser.go.y:754 +//line parser.go.y:743 { yyVAL.funcexpr = &ast.FunctionExpr{TypeParams: yyDollar[1].typeparams, ParList: yyDollar[3].parlist, ReturnTypes: yyDollar[5].typeexprlist, Stmts: yyDollar[6].stmts} yyVAL.funcexpr.SetPosFromToken(yyDollar[2].token.Pos) @@ -1978,7 +1978,7 @@ yydefault: } case 116: yyDollar = yyS[yypt-6 : yypt+1] -//line parser.go.y:759 +//line parser.go.y:748 { yyVAL.funcexpr = &ast.FunctionExpr{TypeParams: yyDollar[1].typeparams, ParList: &ast.ParList{HasVargs: false, Names: []string{}}, ReturnTypes: yyDollar[4].typeexprlist, Stmts: yyDollar[5].stmts} yyVAL.funcexpr.SetPosFromToken(yyDollar[2].token.Pos) @@ -1986,40 +1986,40 @@ yydefault: } case 117: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:766 +//line parser.go.y:755 { yyVAL.parlist = &ast.ParList{HasVargs: true, VarargPosition: yyDollar[1].token.Pos, Names: []string{}} } case 118: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:769 +//line parser.go.y:758 { yyVAL.parlist = &ast.ParList{HasVargs: true, VarargType: yyDollar[3].typeexpr, VarargPosition: yyDollar[1].token.Pos, Names: []string{}} } case 119: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:772 +//line parser.go.y:761 { names, positions, types := splitTypedNames(yyDollar[1].typednames) yyVAL.parlist = &ast.ParList{HasVargs: false, Names: names, NamePositions: positions, Types: types} } case 120: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:776 +//line parser.go.y:765 { names, positions, types := splitTypedNames(yyDollar[1].typednames) yyVAL.parlist = &ast.ParList{HasVargs: true, VarargPosition: yyDollar[3].token.Pos, Names: names, NamePositions: positions, Types: types} } case 121: yyDollar = yyS[yypt-5 : yypt+1] -//line parser.go.y:780 +//line parser.go.y:769 { names, positions, types := splitTypedNames(yyDollar[1].typednames) yyVAL.parlist = &ast.ParList{HasVargs: true, VarargType: yyDollar[5].typeexpr, VarargPosition: yyDollar[3].token.Pos, Names: names, NamePositions: positions, Types: types} } case 122: yyDollar = yyS[yypt-2 : yypt+1] -//line parser.go.y:787 +//line parser.go.y:776 { yyVAL.expr = &ast.TableExpr{Fields: []*ast.Field{}} yyVAL.expr.SetPosFromToken(yyDollar[1].token.Pos) @@ -2027,7 +2027,7 @@ yydefault: } case 123: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:792 +//line parser.go.y:781 { yyVAL.expr = &ast.TableExpr{Fields: yyDollar[2].fieldlist} yyVAL.expr.SetPosFromToken(yyDollar[1].token.Pos) @@ -2035,193 +2035,193 @@ yydefault: } case 124: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:800 +//line parser.go.y:789 { yyVAL.fieldlist = []*ast.Field{yyDollar[1].field} } case 125: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:803 +//line parser.go.y:792 { yyVAL.fieldlist = append(yyDollar[1].fieldlist, yyDollar[3].field) } case 126: yyDollar = yyS[yypt-2 : yypt+1] -//line parser.go.y:806 +//line parser.go.y:795 { yyVAL.fieldlist = yyDollar[1].fieldlist } case 127: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:811 +//line parser.go.y:800 { yyVAL.field = &ast.Field{Key: &ast.StringExpr{Value: yyDollar[1].fieldname}, KeySyntax: ast.AttrKeyDot, Value: yyDollar[3].expr} } case 128: yyDollar = yyS[yypt-5 : yypt+1] -//line parser.go.y:814 +//line parser.go.y:803 { yyVAL.field = &ast.Field{Key: yyDollar[2].expr, KeySyntax: ast.AttrKeyIndex, Value: yyDollar[5].expr} } case 129: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:817 +//line parser.go.y:806 { yyVAL.field = &ast.Field{Value: yyDollar[1].expr} } case 130: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:823 +//line parser.go.y:812 { yyVAL.fieldname = yyDollar[1].token.Str } case 131: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:826 +//line parser.go.y:815 { yyVAL.fieldname = "type" } case 132: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:829 +//line parser.go.y:818 { yyVAL.fieldname = "interface" } case 133: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:832 +//line parser.go.y:821 { yyVAL.fieldname = "readonly" } case 134: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:835 +//line parser.go.y:824 { yyVAL.fieldname = "as" } case 135: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:838 +//line parser.go.y:827 { yyVAL.fieldname = "asserts" } case 136: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:841 +//line parser.go.y:830 { yyVAL.fieldname = "is" } case 137: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:844 +//line parser.go.y:833 { yyVAL.fieldname = "keyof" } case 138: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:847 +//line parser.go.y:836 { yyVAL.fieldname = "extends" } case 139: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:852 +//line parser.go.y:841 { yyVAL.fieldname = "type" } case 140: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:855 +//line parser.go.y:844 { yyVAL.fieldname = "interface" } case 141: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:858 +//line parser.go.y:847 { yyVAL.fieldname = "readonly" } case 142: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:861 +//line parser.go.y:850 { yyVAL.fieldname = "as" } case 143: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:864 +//line parser.go.y:853 { yyVAL.fieldname = "asserts" } case 144: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:867 +//line parser.go.y:856 { yyVAL.fieldname = "is" } case 145: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:870 +//line parser.go.y:859 { yyVAL.fieldname = "keyof" } case 146: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:873 +//line parser.go.y:862 { yyVAL.fieldname = "extends" } case 147: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:876 +//line parser.go.y:865 { yyVAL.fieldname = "typeof" } case 148: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:881 +//line parser.go.y:870 { yyVAL.fieldsep = "," } case 149: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:884 +//line parser.go.y:873 { yyVAL.fieldsep = ";" } case 150: yyDollar = yyS[yypt-0 : yypt+1] -//line parser.go.y:891 +//line parser.go.y:880 { yyVAL.typeexprlist = nil } case 151: yyDollar = yyS[yypt-2 : yypt+1] -//line parser.go.y:894 +//line parser.go.y:883 { yyVAL.typeexprlist = yyDollar[2].typeexprlist } case 152: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:897 +//line parser.go.y:886 { yyVAL.typeexprlist = nil } case 153: yyDollar = yyS[yypt-6 : yypt+1] -//line parser.go.y:900 +//line parser.go.y:889 { yyVAL.typeexprlist = append([]ast.TypeExpr{yyDollar[3].typeexpr}, yyDollar[5].typeexprlist...) } case 154: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:905 +//line parser.go.y:894 { yyVAL.typeexpr = yyDollar[1].typeexpr } case 155: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:908 +//line parser.go.y:897 { if union, ok := yyDollar[1].typeexpr.(*ast.UnionTypeExpr); ok { union.Types = append(union.Types, yyDollar[3].typeexpr) @@ -2232,7 +2232,7 @@ yydefault: } case 156: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:916 +//line parser.go.y:905 { if inter, ok := yyDollar[1].typeexpr.(*ast.IntersectionTypeExpr); ok { inter.Types = append(inter.Types, yyDollar[3].typeexpr) @@ -2243,20 +2243,20 @@ yydefault: } case 157: yyDollar = yyS[yypt-7 : yypt+1] -//line parser.go.y:924 +//line parser.go.y:913 { yyVAL.typeexpr = &ast.ConditionalTypeExpr{Check: yyDollar[1].typeexpr, Extends: yyDollar[3].typeexpr, Then: yyDollar[5].typeexpr, Else: yyDollar[7].typeexpr} yyVAL.typeexpr.CopyPos(yyDollar[1].typeexpr) } case 158: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:930 +//line parser.go.y:919 { yyVAL.typeexpr = yyDollar[1].typeexpr } case 159: yyDollar = yyS[yypt-2 : yypt+1] -//line parser.go.y:933 +//line parser.go.y:922 { if prim, ok := yyDollar[1].typeexpr.(*ast.PrimitiveTypeExpr); ok { prim.Annotations = yyDollar[2].annotations @@ -2270,41 +2270,41 @@ yydefault: } case 160: yyDollar = yyS[yypt-2 : yypt+1] -//line parser.go.y:944 +//line parser.go.y:933 { yyVAL.typeexpr = &ast.OptionalTypeExpr{Inner: yyDollar[1].typeexpr} } case 161: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:949 +//line parser.go.y:938 { yyVAL.typeexpr = &ast.PrimitiveTypeExpr{Name: "nil"} yyVAL.typeexpr.SetPosFromToken(yyDollar[1].token.Pos) } case 162: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:953 +//line parser.go.y:942 { yyVAL.typeexpr = &ast.LiteralTypeExpr{Value: true} yyVAL.typeexpr.SetPosFromToken(yyDollar[1].token.Pos) } case 163: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:957 +//line parser.go.y:946 { yyVAL.typeexpr = &ast.LiteralTypeExpr{Value: false} yyVAL.typeexpr.SetPosFromToken(yyDollar[1].token.Pos) } case 164: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:961 +//line parser.go.y:950 { yyVAL.typeexpr = &ast.LiteralTypeExpr{Value: yyDollar[1].token.Str} yyVAL.typeexpr.SetPosFromToken(yyDollar[1].token.Pos) } case 165: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:965 +//line parser.go.y:954 { if i, ok := numparse.ParseIntegerLiteral(yyDollar[1].token.Str); ok { yyVAL.typeexpr = &ast.LiteralTypeExpr{Value: i} @@ -2317,21 +2317,21 @@ yydefault: } case 166: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:970 +//line parser.go.y:964 { yyVAL.typeexpr = &ast.PrimitiveTypeExpr{Name: yyDollar[1].token.Str} yyVAL.typeexpr.SetPosFromToken(yyDollar[1].token.Pos) } case 167: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:974 +//line parser.go.y:968 { yyVAL.typeexpr = &ast.TypeRefExpr{Path: []string{yyDollar[1].token.Str, yyDollar[3].token.Str}} yyVAL.typeexpr.SetPosFromToken(yyDollar[1].token.Pos) } case 168: yyDollar = yyS[yypt-4 : yypt+1] -//line parser.go.y:978 +//line parser.go.y:972 { yyVAL.typeexpr = &ast.GenericTypeExpr{ Base: &ast.TypeRefExpr{Path: []string{yyDollar[1].token.Str}}, @@ -2341,189 +2341,189 @@ yydefault: } case 169: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:985 +//line parser.go.y:979 { yyVAL.typeexpr = &ast.ArrayTypeExpr{Element: yyDollar[2].typeexpr} yyVAL.typeexpr.SetPosFromToken(yyDollar[1].token.Pos) } case 170: yyDollar = yyS[yypt-7 : yypt+1] -//line parser.go.y:989 +//line parser.go.y:983 { yyVAL.typeexpr = &ast.MapTypeExpr{Key: yyDollar[3].typeexpr, Value: yyDollar[6].typeexpr} yyVAL.typeexpr.SetPosFromToken(yyDollar[1].token.Pos) } case 171: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:993 +//line parser.go.y:987 { yyVAL.typeexpr = &ast.RecordTypeExpr{Fields: yyDollar[2].recordfields} yyVAL.typeexpr.SetPosFromToken(yyDollar[1].token.Pos) } case 172: yyDollar = yyS[yypt-7 : yypt+1] -//line parser.go.y:997 +//line parser.go.y:991 { yyVAL.typeexpr = &ast.FunctionTypeExpr{Params: yyDollar[2].funcparams, Returns: yyDollar[6].typeexprlist} yyVAL.typeexpr.SetPosFromToken(yyDollar[1].token.Pos) } case 173: yyDollar = yyS[yypt-6 : yypt+1] -//line parser.go.y:1001 +//line parser.go.y:995 { yyVAL.typeexpr = &ast.FunctionTypeExpr{Params: yyDollar[2].funcparams, Returns: []ast.TypeExpr{}} yyVAL.typeexpr.SetPosFromToken(yyDollar[1].token.Pos) } case 174: yyDollar = yyS[yypt-5 : yypt+1] -//line parser.go.y:1005 +//line parser.go.y:999 { yyVAL.typeexpr = &ast.FunctionTypeExpr{Params: yyDollar[2].funcparams, Returns: []ast.TypeExpr{yyDollar[5].typeexpr}} yyVAL.typeexpr.SetPosFromToken(yyDollar[1].token.Pos) } case 175: yyDollar = yyS[yypt-6 : yypt+1] -//line parser.go.y:1009 +//line parser.go.y:1003 { yyVAL.typeexpr = &ast.FunctionTypeExpr{Params: []ast.FunctionParamExpr{}, Returns: yyDollar[5].typeexprlist} yyVAL.typeexpr.SetPosFromToken(yyDollar[1].token.Pos) } case 176: yyDollar = yyS[yypt-4 : yypt+1] -//line parser.go.y:1013 +//line parser.go.y:1007 { yyVAL.typeexpr = &ast.FunctionTypeExpr{Params: []ast.FunctionParamExpr{}, Returns: []ast.TypeExpr{yyDollar[4].typeexpr}} yyVAL.typeexpr.SetPosFromToken(yyDollar[1].token.Pos) } case 177: yyDollar = yyS[yypt-5 : yypt+1] -//line parser.go.y:1017 +//line parser.go.y:1011 { yyVAL.typeexpr = &ast.FunctionTypeExpr{Params: []ast.FunctionParamExpr{}, Returns: []ast.TypeExpr{}} yyVAL.typeexpr.SetPosFromToken(yyDollar[1].token.Pos) } case 178: yyDollar = yyS[yypt-7 : yypt+1] -//line parser.go.y:1021 +//line parser.go.y:1015 { yyVAL.typeexpr = &ast.FunctionTypeExpr{Params: yyDollar[3].funcparams, Returns: []ast.TypeExpr{yyDollar[6].typeexpr}} yyVAL.typeexpr.SetPosFromToken(yyDollar[1].token.Pos) } case 179: yyDollar = yyS[yypt-6 : yypt+1] -//line parser.go.y:1025 +//line parser.go.y:1019 { yyVAL.typeexpr = &ast.FunctionTypeExpr{Params: []ast.FunctionParamExpr{}, Returns: []ast.TypeExpr{yyDollar[5].typeexpr}} yyVAL.typeexpr.SetPosFromToken(yyDollar[1].token.Pos) } case 180: yyDollar = yyS[yypt-9 : yypt+1] -//line parser.go.y:1029 +//line parser.go.y:1023 { yyVAL.typeexpr = &ast.FunctionTypeExpr{Params: yyDollar[3].funcparams, Returns: yyDollar[7].typeexprlist} yyVAL.typeexpr.SetPosFromToken(yyDollar[1].token.Pos) } case 181: yyDollar = yyS[yypt-8 : yypt+1] -//line parser.go.y:1033 +//line parser.go.y:1027 { yyVAL.typeexpr = &ast.FunctionTypeExpr{Params: yyDollar[3].funcparams, Returns: []ast.TypeExpr{}} yyVAL.typeexpr.SetPosFromToken(yyDollar[1].token.Pos) } case 182: yyDollar = yyS[yypt-8 : yypt+1] -//line parser.go.y:1037 +//line parser.go.y:1031 { yyVAL.typeexpr = &ast.FunctionTypeExpr{Params: []ast.FunctionParamExpr{}, Returns: yyDollar[6].typeexprlist} yyVAL.typeexpr.SetPosFromToken(yyDollar[1].token.Pos) } case 183: yyDollar = yyS[yypt-7 : yypt+1] -//line parser.go.y:1041 +//line parser.go.y:1035 { yyVAL.typeexpr = &ast.FunctionTypeExpr{Params: []ast.FunctionParamExpr{}, Returns: []ast.TypeExpr{}} yyVAL.typeexpr.SetPosFromToken(yyDollar[1].token.Pos) } case 184: yyDollar = yyS[yypt-6 : yypt+1] -//line parser.go.y:1045 +//line parser.go.y:1039 { yyVAL.typeexpr = &ast.FunctionTypeExpr{Params: yyDollar[3].funcparams, Returns: []ast.TypeExpr{yyDollar[6].typeexpr}} yyVAL.typeexpr.SetPosFromToken(yyDollar[1].token.Pos) } case 185: yyDollar = yyS[yypt-8 : yypt+1] -//line parser.go.y:1049 +//line parser.go.y:1043 { yyVAL.typeexpr = &ast.FunctionTypeExpr{Params: yyDollar[3].funcparams, Returns: yyDollar[7].typeexprlist} yyVAL.typeexpr.SetPosFromToken(yyDollar[1].token.Pos) } case 186: yyDollar = yyS[yypt-5 : yypt+1] -//line parser.go.y:1053 +//line parser.go.y:1047 { yyVAL.typeexpr = &ast.FunctionTypeExpr{Params: []ast.FunctionParamExpr{}, Returns: []ast.TypeExpr{yyDollar[5].typeexpr}} yyVAL.typeexpr.SetPosFromToken(yyDollar[1].token.Pos) } case 187: yyDollar = yyS[yypt-7 : yypt+1] -//line parser.go.y:1057 +//line parser.go.y:1051 { yyVAL.typeexpr = &ast.FunctionTypeExpr{Params: []ast.FunctionParamExpr{}, Returns: yyDollar[6].typeexprlist} yyVAL.typeexpr.SetPosFromToken(yyDollar[1].token.Pos) } case 188: yyDollar = yyS[yypt-7 : yypt+1] -//line parser.go.y:1061 +//line parser.go.y:1055 { yyVAL.typeexpr = &ast.FunctionTypeExpr{Params: yyDollar[3].funcparams, Returns: []ast.TypeExpr{}} yyVAL.typeexpr.SetPosFromToken(yyDollar[1].token.Pos) } case 189: yyDollar = yyS[yypt-6 : yypt+1] -//line parser.go.y:1065 +//line parser.go.y:1059 { yyVAL.typeexpr = &ast.FunctionTypeExpr{Params: []ast.FunctionParamExpr{}, Returns: []ast.TypeExpr{}} yyVAL.typeexpr.SetPosFromToken(yyDollar[1].token.Pos) } case 190: yyDollar = yyS[yypt-4 : yypt+1] -//line parser.go.y:1069 +//line parser.go.y:1063 { yyVAL.typeexpr = &ast.FunctionTypeExpr{Params: yyDollar[3].funcparams, Returns: nil} yyVAL.typeexpr.SetPosFromToken(yyDollar[1].token.Pos) } case 191: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:1073 +//line parser.go.y:1067 { yyVAL.typeexpr = &ast.FunctionTypeExpr{Params: []ast.FunctionParamExpr{}, Returns: nil} yyVAL.typeexpr.SetPosFromToken(yyDollar[1].token.Pos) } case 192: yyDollar = yyS[yypt-4 : yypt+1] -//line parser.go.y:1077 +//line parser.go.y:1071 { yyVAL.typeexpr = &ast.RecordTypeExpr{Fields: yyDollar[3].recordfields} yyVAL.typeexpr.SetPosFromToken(yyDollar[1].token.Pos) } case 193: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:1081 +//line parser.go.y:1075 { yyVAL.typeexpr = &ast.RecordTypeExpr{Fields: nil} yyVAL.typeexpr.SetPosFromToken(yyDollar[1].token.Pos) } case 194: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:1085 +//line parser.go.y:1079 { yyVAL.typeexpr = &ast.ArrayTypeExpr{Element: yyDollar[1].typeexpr} yyVAL.typeexpr.CopyPos(yyDollar[1].typeexpr) } case 195: yyDollar = yyS[yypt-4 : yypt+1] -//line parser.go.y:1089 +//line parser.go.y:1083 { arr := &ast.ArrayTypeExpr{Element: yyDollar[3].typeexpr, Readonly: true} arr.SetPosFromToken(yyDollar[1].token.Pos) @@ -2531,7 +2531,7 @@ yydefault: } case 196: yyDollar = yyS[yypt-8 : yypt+1] -//line parser.go.y:1094 +//line parser.go.y:1088 { m := &ast.MapTypeExpr{Key: yyDollar[4].typeexpr, Value: yyDollar[7].typeexpr, Readonly: true} m.SetPosFromToken(yyDollar[1].token.Pos) @@ -2539,85 +2539,85 @@ yydefault: } case 197: yyDollar = yyS[yypt-4 : yypt+1] -//line parser.go.y:1099 +//line parser.go.y:1093 { yyVAL.typeexpr = &ast.RecordTypeExpr{Fields: yyDollar[3].recordfields, Readonly: true} yyVAL.typeexpr.SetPosFromToken(yyDollar[1].token.Pos) } case 198: yyDollar = yyS[yypt-2 : yypt+1] -//line parser.go.y:1103 +//line parser.go.y:1097 { yyVAL.typeexpr = &ast.RecordTypeExpr{Fields: nil} yyVAL.typeexpr.SetPosFromToken(yyDollar[1].token.Pos) } case 199: yyDollar = yyS[yypt-2 : yypt+1] -//line parser.go.y:1107 +//line parser.go.y:1101 { yyVAL.typeexpr = &ast.AssertsTypeExpr{ParamName: yyDollar[2].token.Str, NarrowTo: nil} yyVAL.typeexpr.SetPosFromToken(yyDollar[1].token.Pos) } case 200: yyDollar = yyS[yypt-4 : yypt+1] -//line parser.go.y:1111 +//line parser.go.y:1105 { yyVAL.typeexpr = &ast.AssertsTypeExpr{ParamName: yyDollar[2].token.Str, NarrowTo: yyDollar[4].typeexpr} yyVAL.typeexpr.SetPosFromToken(yyDollar[1].token.Pos) } case 201: yyDollar = yyS[yypt-4 : yypt+1] -//line parser.go.y:1115 +//line parser.go.y:1109 { yyVAL.typeexpr = &ast.TypeOfExpr{Expr: yyDollar[3].expr} yyVAL.typeexpr.SetPosFromToken(yyDollar[1].token.Pos) } case 202: yyDollar = yyS[yypt-4 : yypt+1] -//line parser.go.y:1119 +//line parser.go.y:1113 { yyVAL.typeexpr = &ast.KeyOfExpr{Inner: yyDollar[3].typeexpr} yyVAL.typeexpr.SetPosFromToken(yyDollar[1].token.Pos) } case 203: yyDollar = yyS[yypt-4 : yypt+1] -//line parser.go.y:1123 +//line parser.go.y:1117 { yyVAL.typeexpr = &ast.IndexAccessExpr{Object: yyDollar[1].typeexpr, Index: yyDollar[3].typeexpr} yyVAL.typeexpr.CopyPos(yyDollar[1].typeexpr) } case 204: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:1129 +//line parser.go.y:1123 { yyVAL.typeexprlist = []ast.TypeExpr{yyDollar[1].typeexpr} } case 205: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:1132 +//line parser.go.y:1126 { yyVAL.typeexprlist = append(yyDollar[1].typeexprlist, yyDollar[3].typeexpr) } case 206: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:1137 +//line parser.go.y:1131 { yyVAL.typeexprlist = []ast.TypeExpr{yyDollar[1].typeexpr, yyDollar[3].typeexpr} } case 207: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:1140 +//line parser.go.y:1134 { yyVAL.typeexprlist = append(yyDollar[1].typeexprlist, yyDollar[3].typeexpr) } case 208: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:1145 +//line parser.go.y:1139 { } case 209: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:1147 +//line parser.go.y:1141 { yylex.(*Lexer).PendingGT = &ast.Token{ Type: '>', @@ -2627,223 +2627,223 @@ yydefault: } case 210: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:1156 +//line parser.go.y:1150 { yyVAL.funcparam = ast.FunctionParamExpr{Name: yyDollar[1].token.Str, Type: yyDollar[3].typeexpr} } case 211: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:1159 +//line parser.go.y:1153 { yyVAL.funcparam = ast.FunctionParamExpr{Name: "", Type: yyDollar[1].typeexpr} } case 212: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:1164 +//line parser.go.y:1158 { yyVAL.funcparams = []ast.FunctionParamExpr{yyDollar[1].funcparam} } case 213: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:1167 +//line parser.go.y:1161 { yyVAL.funcparams = append(yyDollar[1].funcparams, yyDollar[3].funcparam) } case 214: yyDollar = yyS[yypt-2 : yypt+1] -//line parser.go.y:1170 +//line parser.go.y:1164 { yyVAL.funcparams = []ast.FunctionParamExpr{{Name: "...", Type: yyDollar[2].typeexpr}} } case 215: yyDollar = yyS[yypt-4 : yypt+1] -//line parser.go.y:1173 +//line parser.go.y:1167 { yyVAL.funcparams = append(yyDollar[1].funcparams, ast.FunctionParamExpr{Name: "...", Type: yyDollar[4].typeexpr}) } case 216: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:1178 +//line parser.go.y:1172 { yyVAL.recordfields = []ast.RecordFieldExpr{yyDollar[1].recordfield} } case 217: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:1181 +//line parser.go.y:1175 { yyVAL.recordfields = append(yyDollar[1].recordfields, yyDollar[3].recordfield) } case 218: yyDollar = yyS[yypt-2 : yypt+1] -//line parser.go.y:1184 +//line parser.go.y:1178 { yyVAL.recordfields = yyDollar[1].recordfields } case 219: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:1189 +//line parser.go.y:1183 { yyVAL.recordfield = ast.RecordFieldExpr{Name: yyDollar[1].token.Str, Type: yyDollar[3].typeexpr, Optional: false} } case 220: yyDollar = yyS[yypt-4 : yypt+1] -//line parser.go.y:1192 +//line parser.go.y:1186 { yyVAL.recordfield = ast.RecordFieldExpr{Name: yyDollar[1].token.Str, Type: yyDollar[3].typeexpr, Optional: false, Annotations: yyDollar[4].annotations} } case 221: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:1195 +//line parser.go.y:1189 { yyVAL.recordfield = ast.RecordFieldExpr{Name: yyDollar[1].token.Str, Type: yyDollar[3].typeexpr, Optional: true} } case 222: yyDollar = yyS[yypt-4 : yypt+1] -//line parser.go.y:1198 +//line parser.go.y:1192 { yyVAL.recordfield = ast.RecordFieldExpr{Name: yyDollar[1].token.Str, Type: yyDollar[3].typeexpr, Optional: true, Annotations: yyDollar[4].annotations} } case 223: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:1201 +//line parser.go.y:1195 { yyVAL.recordfield = ast.RecordFieldExpr{Name: yyDollar[1].fieldname, Type: yyDollar[3].typeexpr, Optional: false} } case 224: yyDollar = yyS[yypt-4 : yypt+1] -//line parser.go.y:1204 +//line parser.go.y:1198 { yyVAL.recordfield = ast.RecordFieldExpr{Name: yyDollar[1].fieldname, Type: yyDollar[3].typeexpr, Optional: false, Annotations: yyDollar[4].annotations} } case 225: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:1207 +//line parser.go.y:1201 { yyVAL.recordfield = ast.RecordFieldExpr{Name: yyDollar[1].fieldname, Type: yyDollar[3].typeexpr, Optional: true} } case 226: yyDollar = yyS[yypt-4 : yypt+1] -//line parser.go.y:1210 +//line parser.go.y:1204 { yyVAL.recordfield = ast.RecordFieldExpr{Name: yyDollar[1].fieldname, Type: yyDollar[3].typeexpr, Optional: true, Annotations: yyDollar[4].annotations} } case 227: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:1215 +//line parser.go.y:1209 { yyVAL.annotations = []ast.AnnotationExpr{yyDollar[1].annotation} } case 228: yyDollar = yyS[yypt-2 : yypt+1] -//line parser.go.y:1218 +//line parser.go.y:1212 { yyVAL.annotations = append(yyDollar[1].annotations, yyDollar[2].annotation) } case 229: yyDollar = yyS[yypt-2 : yypt+1] -//line parser.go.y:1223 +//line parser.go.y:1217 { yyVAL.annotation = ast.AnnotationExpr{Name: yyDollar[2].token.Str, Args: nil} } case 230: yyDollar = yyS[yypt-4 : yypt+1] -//line parser.go.y:1226 +//line parser.go.y:1220 { yyVAL.annotation = ast.AnnotationExpr{Name: yyDollar[2].token.Str, Args: nil} } case 231: yyDollar = yyS[yypt-5 : yypt+1] -//line parser.go.y:1229 +//line parser.go.y:1223 { yyVAL.annotation = ast.AnnotationExpr{Name: yyDollar[2].token.Str, Args: yyDollar[4].exprlist} } case 232: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:1234 +//line parser.go.y:1228 { yyVAL.typeparams = yyDollar[2].typeparams } case 233: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:1239 +//line parser.go.y:1233 { yyVAL.typeparams = []ast.TypeParamExpr{yyDollar[1].typeparam} } case 234: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:1242 +//line parser.go.y:1236 { yyVAL.typeparams = append(yyDollar[1].typeparams, yyDollar[3].typeparam) } case 235: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:1247 +//line parser.go.y:1241 { yyVAL.typeparam = ast.TypeParamExpr{Name: yyDollar[1].token.Str, Constraint: nil} } case 236: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:1250 +//line parser.go.y:1244 { yyVAL.typeparam = ast.TypeParamExpr{Name: yyDollar[1].token.Str, Constraint: yyDollar[3].typeexpr} } case 237: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:1255 +//line parser.go.y:1249 { yyVAL.typednames = []typedNameEntry{yyDollar[1].typedname} } case 238: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:1258 +//line parser.go.y:1252 { yyVAL.typednames = append(yyDollar[1].typednames, yyDollar[3].typedname) } case 239: yyDollar = yyS[yypt-1 : yypt+1] -//line parser.go.y:1263 +//line parser.go.y:1257 { yyVAL.typedname = typedNameEntry{Name: yyDollar[1].token.Str, Pos: yyDollar[1].token.Pos, Type: nil} } case 240: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:1266 +//line parser.go.y:1260 { yyVAL.typedname = typedNameEntry{Name: yyDollar[1].token.Str, Pos: yyDollar[1].token.Pos, Type: yyDollar[3].typeexpr} } case 241: yyDollar = yyS[yypt-0 : yypt+1] -//line parser.go.y:1271 +//line parser.go.y:1265 { yyVAL.typereflist = nil } case 242: yyDollar = yyS[yypt-2 : yypt+1] -//line parser.go.y:1274 +//line parser.go.y:1268 { yyVAL.typereflist = []*ast.TypeRefExpr{{Path: []string{yyDollar[2].token.Str}}} } case 243: yyDollar = yyS[yypt-3 : yypt+1] -//line parser.go.y:1277 +//line parser.go.y:1271 { yyVAL.typereflist = append(yyDollar[1].typereflist, &ast.TypeRefExpr{Path: []string{yyDollar[3].token.Str}}) } case 244: yyDollar = yyS[yypt-0 : yypt+1] -//line parser.go.y:1282 +//line parser.go.y:1276 { yyVAL.ifacemethods = nil } case 245: yyDollar = yyS[yypt-2 : yypt+1] -//line parser.go.y:1285 +//line parser.go.y:1279 { yyVAL.ifacemethods = append(yyDollar[1].ifacemethods, yyDollar[2].ifacemethod) } case 246: yyDollar = yyS[yypt-5 : yypt+1] -//line parser.go.y:1290 +//line parser.go.y:1284 { yyVAL.ifacemethod = ast.InterfaceMethodExpr{ Name: yyDollar[2].token.Str, @@ -2852,7 +2852,7 @@ yydefault: } case 247: yyDollar = yyS[yypt-6 : yypt+1] -//line parser.go.y:1296 +//line parser.go.y:1290 { yyVAL.ifacemethod = ast.InterfaceMethodExpr{ Name: yyDollar[2].token.Str, diff --git a/compiler/parse/parser.go.y b/compiler/parse/parser.go.y index b6e93923b..58224586e 100644 --- a/compiler/parse/parser.go.y +++ b/compiler/parse/parser.go.y @@ -136,12 +136,15 @@ func setLastPosFromExprs(node ast.PositionHolder, exprs []ast.Expr, fallback ast %nonassoc T2Colon /* :: cast - nonassoc to prefer reduce over shift for labels */ %left TAs TBang /* type cast (as) and non-nil assertion */ -/* Known shift/reduce conflicts (14 total, all resolved correctly by shift): - - 3 Lua inherent: prefixexp '(' ambiguity (call vs grouping) - cannot be eliminated - - 5 type optional: simpletypeexpr TQuestion binds tighter than union/intersection - - 2 function return: (params) -> typeexpr followed by |/& binds to return type - - 2 type decl: type Name pattern - shift to continue parsing type name - - 2 generic: TIdent '<' in type context - shift for generic args +/* Known shift/reduce conflicts (39 total, all resolved by shift): + - Lua prefix/call and typed parameter ambiguities + - optional, union, and intersection type binding + - function return types and generic argument lookahead + - record/interface field and annotation lookahead + + The generated parser is pinned by go:generate and CI. Any grammar change + that changes these resolutions must regenerate parser.go and pass the full + parser/fixture suite. */ %% @@ -376,8 +379,8 @@ var: prefixexp '[' expr ']' { $$ = &ast.AttrGetExpr{Object: $1, Key: $3, KeySyntax: ast.AttrKeyIndex} $$.CopyPos($1) - $$.SetLastLine($4.Pos.Line) - $$.SetLastColumn($4.Pos.Column + 1) + $$.SetLastLine($4.Pos.Line) + $$.SetLastColumn($4.Pos.Column + 1) } | prefixexp '.' TIdent { key := &ast.StringExpr{Value:$3.Str} diff --git a/diagnostic_diff_report_test.go b/diagnostic_diff_report_test.go index ecf92ba08..3526cc5db 100644 --- a/diagnostic_diff_report_test.go +++ b/diagnostic_diff_report_test.go @@ -40,7 +40,7 @@ func TestWriteDiagnosticDiffReport(t *testing.T) { t.Fatalf("creating report %s: %v", outPath, err) } if err := diffreport.WriteReport(file, report, os.Getenv("DIFFREPORT_FORMAT")); err != nil { - file.Close() + _ = file.Close() t.Fatalf("writing report %s: %v", outPath, err) } if err := file.Close(); err != nil { diff --git a/fixture_baseline_test.go b/fixture_baseline_test.go index 4ee49eb4d..8a56d8309 100644 --- a/fixture_baseline_test.go +++ b/fixture_baseline_test.go @@ -65,9 +65,13 @@ func TestWriteFixtureDiagnosticBaseline(t *testing.T) { if err != nil { t.Fatalf("creating baseline %s: %v", outPath, err) } - defer file.Close() + closed := false + t.Cleanup(func() { + if !closed { + _ = file.Close() + } + }) w := bufio.NewWriter(file) - defer w.Flush() for _, s := range suites { skip, _ := shouldSkipOracleSuite(s) @@ -115,6 +119,13 @@ func TestWriteFixtureDiagnosticBaseline(t *testing.T) { writeFixtureBaselineRecord(t, w, fixtureDiagnosticBaselineRecord(s.Name, entry, d)) } } + if err := w.Flush(); err != nil { + t.Fatalf("flush baseline %s: %v", outPath, err) + } + if err := file.Close(); err != nil { + t.Fatalf("close baseline %s: %v", outPath, err) + } + closed = true } func writeFixtureBaselineRecord(t *testing.T, w *bufio.Writer, record fixtureBaselineRecord) { @@ -172,7 +183,7 @@ func fixtureBaselineLabels(labels []diag.Label) []fixtureBaselineLabel { out := make([]fixtureBaselineLabel, 0, len(labels)) for _, label := range labels { out = append(out, fixtureBaselineLabel{ - File: label.File, + File: label.DisplayFile(), Span: fixtureBaselineSpanFromDiagnostic(label.Span), Message: label.Message, Placement: fixtureBaselineLabelPlacement(label.Placement), diff --git a/fixture_harness_test.go b/fixture_harness_test.go index b5a9130b6..2cd39e947 100644 --- a/fixture_harness_test.go +++ b/fixture_harness_test.go @@ -1020,7 +1020,7 @@ func matchesDiagnosticLabelFile(expFile string, d diag.Diagnostic, entryFile str if expFile == "" { return true } - actual := label.File + actual := label.DisplayFile() if actual == "" { actual = d.Position.File } @@ -1641,7 +1641,10 @@ func verifyGoldenOutput(t *testing.T, s namedSuite, buf *bytes.Buffer) { } got := buf.String() - want := string(golden) + // Git may check text fixtures out with CRLF on Windows, while capturePrint + // deliberately emits Lua's canonical newline. Compare the content rather + // than the checkout's platform-specific line endings. + want := string(bytes.ReplaceAll(golden, []byte("\r\n"), []byte("\n"))) if got != want { t.Errorf("output mismatch:\n--- want ---\n%s--- got ---\n%s", want, got) } diff --git a/inspect/stack.go b/inspect/stack.go index 98c235db4..af0c57f4f 100644 --- a/inspect/stack.go +++ b/inspect/stack.go @@ -105,9 +105,12 @@ func GetStackFrame(l *lua.LState, level int) (StackFrame, bool) { return StackFrame{}, false } - // Spawn debug info with function info - funcTable := l.NewTable() - if _, err := l.GetInfo("nSluf", ar, funcTable); err != nil { + // GetInfo with the "f" flag returns the frame's *LFunction as its first + // return value (it does not store it in a table). Capture it here so the + // Go-function name resolution and the upvalue walk below use the real + // function rather than reading a never-populated table key. + fnVal, err := l.GetInfo("nSluf", ar, nil) + if err != nil { return StackFrame{}, false } @@ -121,18 +124,16 @@ func GetStackFrame(l *lua.LState, level int) (StackFrame, bool) { // If no name is provided and this is a Go function, try to determine the name if frame.Name == "" && (frame.FuncType == "Go" || frame.FuncType == "C") { - // Spawn the actual function from the debug info table - fn := funcTable.RawGet(lua.LString("f")) - if luaFn, ok := fn.(*lua.LFunction); ok { + if luaFn, ok := fnVal.(*lua.LFunction); ok { // Iterate over globals and see if any key maps to this function globals := l.Get(lua.GlobalsIndex).(*lua.LTable) globals.ForEach(func(key, value lua.LValue) { if globalFn, ok := value.(*lua.LFunction); ok && globalFn == luaFn { - // Use the global key as the function name. frame.Name = key.String() } }) } + // Fallback if no match was found. if frame.Name == "" { frame.Name = fmt.Sprintf("", frame.Source, frame.CurrentLine) @@ -145,19 +146,19 @@ func GetStackFrame(l *lua.LState, level int) (StackFrame, bool) { if name == "" { break } + frame.Locals = append(frame.Locals, Local{name, value}) } // Only get upvalues for Lua functions - if fn := funcTable.RawGet(lua.LString("f")); fn != lua.LNil { - if luaFn, ok := fn.(*lua.LFunction); ok { - for i := 1; ; i++ { - name, value := l.GetUpvalue(luaFn, i) - if name == "" { - break - } - frame.Upvalues = append(frame.Upvalues, Upvalue{name, value}) + if luaFn, ok := fnVal.(*lua.LFunction); ok && !luaFn.IsG { + for i := 1; ; i++ { + name, value := l.GetUpvalue(luaFn, i) + if name == "" { + break } + + frame.Upvalues = append(frame.Upvalues, Upvalue{name, value}) } } diff --git a/inspect/stack_test.go b/inspect/stack_test.go index dc73e5960..309b76ced 100644 --- a/inspect/stack_test.go +++ b/inspect/stack_test.go @@ -250,3 +250,43 @@ func makeInspectFunc(key string) lua.LGFunction { return 0 } } + +// TestGetStackFrameCapturesUpvalues guards against a regression where +// GetStackFrame discarded the function returned by GetInfo("...f...") and read +// a never-populated table key instead, so upvalues were never reported. +func TestGetStackFrameCapturesUpvalues(t *testing.T) { + L := lua.NewState() + defer L.Close() + + L.SetGlobal("inspect_up", L.NewFunction(func(L *lua.LState) int { + trace := GetStackTrace(L) + L.SetField(L.Get(lua.RegistryIndex).(*lua.LTable), "up_trace", lua.LString(trace.String())) + return 0 + })) + + if err := L.DoString(` + local function make_outer() + local captured = "upvalue-payload" + local function inner() + captured = captured .. "." + inspect_up() + return captured + end + inner() + end + make_outer() + `); err != nil { + t.Fatalf("Failed to run script: %v", err) + } + + registry := L.Get(lua.RegistryIndex).(*lua.LTable) + traceStr := registry.RawGetString("up_trace").String() + + if !strings.Contains(traceStr, "Upvalues:") { + t.Errorf("trace must include an Upvalues section;\ngot:\n%s", traceStr) + } + + if !strings.Contains(traceStr, "captured = upvalue-payload") { + t.Errorf("trace must list the captured upvalue;\ngot:\n%s", traceStr) + } +} diff --git a/state.go b/state.go index fc259270e..cd6eeb169 100644 --- a/state.go +++ b/state.go @@ -196,6 +196,18 @@ func (ls *LState) clearFrameExt(cf *callFrame) { } } +// unwindCallFrames discards frames down to sp and removes their extension +// metadata first. Extensions are keyed by frame index, so a plain SetSp would +// let a later, unrelated Go call at a reused index inherit a stale continuation +// or xpcall error handler. +func (ls *LState) unwindCallFrames(sp int) { + for i := ls.stack.Sp() - 1; i >= sp; i-- { + ls.clearFrameExt(ls.stack.At(i)) + } + ls.stack.SetSp(sp) + ls.currentFrame = ls.stack.Last() +} + type callFrame struct { Fn *LFunction GoFunc LGoFunc // stateless Go function (used when Fn is nil) @@ -1925,8 +1937,7 @@ func (ls *LState) PCall(nargs, nret int, errfunc *LFunction) (err error) { err = rcv.(*ApiError) err.(*ApiError).StackTrace = ls.stackTrace(0) } - ls.stack.SetSp(sp) - ls.currentFrame = ls.stack.Last() + ls.unwindCallFrames(sp) ls.reg.SetTop(base) } }() @@ -1935,15 +1946,14 @@ func (ls *LState) PCall(nargs, nret int, errfunc *LFunction) (err error) { } else if len(err.(*ApiError).StackTrace) == 0 { err.(*ApiError).StackTrace = ls.stackTrace(0) } - ls.stack.SetSp(sp) - ls.currentFrame = ls.stack.Last() + ls.unwindCallFrames(sp) ls.reg.SetTop(base) } // Skip stack reset if yield happened if ls.yieldState != yieldNone { return } - ls.stack.SetSp(sp) + ls.unwindCallFrames(sp) if sp == 0 { ls.currentFrame = nil } diff --git a/types/diag/render.go b/types/diag/render.go index eb5308558..b6cc88c64 100644 --- a/types/diag/render.go +++ b/types/diag/render.go @@ -140,7 +140,7 @@ func (d Diagnostic) render(source SourceLines, c colorScheme) string { b.WriteString("\n") b.WriteString(c.gutter) - b.WriteString(fmt.Sprintf("%*d | ", gutterWidth-1, d.Position.Line)) + _, _ = fmt.Fprintf(&b, "%*d | ", gutterWidth-1, d.Position.Line) b.WriteString(c.reset) b.WriteString(line) b.WriteString("\n") @@ -186,7 +186,7 @@ func (d Diagnostic) render(source SourceLines, c colorScheme) string { labelLine := expandTabs(labelLineRaw) if labelLine != "" { b.WriteString(c.gutter) - b.WriteString(fmt.Sprintf("%*d | ", gutterWidth-1, label.Span.StartLine)) + _, _ = fmt.Fprintf(&b, "%*d | ", gutterWidth-1, label.Span.StartLine) b.WriteString(c.reset) b.WriteString(labelLine) b.WriteString("\n") diff --git a/types/io/manifest.go b/types/io/manifest.go index 1ff7bc0ec..203927584 100644 --- a/types/io/manifest.go +++ b/types/io/manifest.go @@ -5,6 +5,9 @@ package io import ( + "bytes" + "encoding/json" + "errors" "fmt" "strconv" "sync" @@ -18,6 +21,22 @@ import ( legacytyp "github.com/wippyai/go-lua/types/typ" ) +var ( + // ErrLegacyManifestWire is returned for the pre-abstract-interpreter INAM + // manifest format. Callers that own a cache should treat it as a cache miss + // and rebuild from source rather than silently dropping legacy semantics. + ErrLegacyManifestWire = typemanifest.ErrLegacyWireFormat + // ErrLegacyTypeWire is the equivalent migration signal for the old + // standalone binary type codec. + ErrLegacyTypeWire = errors.New("types/io: legacy binary type wire format") +) + +// legacyTypeKindMax is the last kind tag emitted by the v1 binary type codec +// (Recursive). That format had no magic prefix, so a recognized leading tag is +// the only protocol discriminator available after canonical JSON rejects the +// payload. +const legacyTypeKindMax byte = 31 + // Manifest captures module-boundary type metadata for legacy callers. type Manifest struct { Path string @@ -27,7 +46,8 @@ type Manifest struct { Types map[string]typ.Type Globals map[string]typ.Type - Summaries map[string]*FunctionSummary + Summaries map[string]*FunctionSummary + FunctionSignatures map[string]typesignature.Function cacheMu sync.RWMutex cachedLookupValues map[string]lookupValueResult @@ -56,10 +76,11 @@ type ManifestQuerier interface { func NewManifest(path string) *Manifest { return &Manifest{ - Path: path, - Types: make(map[string]typ.Type), - Globals: make(map[string]typ.Type), - Summaries: make(map[string]*FunctionSummary), + Path: path, + Types: make(map[string]typ.Type), + Globals: make(map[string]typ.Type), + Summaries: make(map[string]*FunctionSummary), + FunctionSignatures: make(map[string]typesignature.Function), } } @@ -140,6 +161,33 @@ func (m *Manifest) DefineSummary(name string, summary *FunctionSummary) { m.invalidateCaches() } +// DefineFunctionSignature records the full canonical signature for a module +// member. It is the lossless compatibility path for callers that have migrated +// beyond legacy FunctionSummary's reduced escape/return vocabulary. +func (m *Manifest) DefineFunctionSignature(name string, sig typesignature.Function) { + if m == nil || name == "" || sig.Type == nil { + return + } + if m.FunctionSignatures == nil { + m.FunctionSignatures = make(map[string]typesignature.Function) + } + m.FunctionSignatures[name] = sig.Clone() + m.invalidateCaches() +} + +// AllFunctionSignatures returns cloned canonical signatures keyed by their +// module-local or qualified lookup names. +func (m *Manifest) AllFunctionSignatures() map[string]typesignature.Function { + if m == nil || len(m.FunctionSignatures) == 0 { + return nil + } + out := make(map[string]typesignature.Function, len(m.FunctionSignatures)) + for name, sig := range m.FunctionSignatures { + out[name] = sig.Clone() + } + return out +} + func (m *Manifest) SetExport(t typ.Type) { if m == nil { return @@ -277,11 +325,25 @@ func Encode(t typ.Type) ([]byte, error) { func Decode(data []byte) (typ.Type, error) { m, err := typemanifest.Decode(data) if err != nil { + var syntaxError *json.SyntaxError + if errors.As(err, &syntaxError) && looksLikeLegacyTypeWire(data) { + return nil, fmt.Errorf("%w: %v", ErrLegacyTypeWire, err) + } return nil, err } return m.Export, nil } +func looksLikeLegacyTypeWire(data []byte) bool { + if len(data) == 0 || data[0] > legacyTypeKindMax { + return false + } + // A canonical object may have leading JSON whitespace whose byte value also + // overlaps a legacy kind tag. Preserve JSON classification when its first + // non-space byte is the object delimiter. + return !bytes.HasPrefix(bytes.TrimSpace(data), []byte("{")) +} + func FromCanonical(canonical *typemanifest.Manifest) *Manifest { if canonical == nil { return nil @@ -300,6 +362,7 @@ func FromCanonical(canonical *typemanifest.Manifest) *Manifest { m.AddGlobal(name, t) } for name, sig := range canonical.FunctionSignatures { + m.DefineFunctionSignature(name, sig) if summary := summaryFromCanonicalSignature(sig); summary != nil { m.DefineSummary(name, summary) } @@ -328,10 +391,16 @@ func (m *Manifest) toCanonical() *typemanifest.Manifest { canonical.DefineGlobalType(name, t) } for name, summary := range m.Summaries { + if _, canonical := m.FunctionSignatures[name]; canonical { + continue + } if sig, ok := canonicalFunctionSignature(summary); ok { canonical.DefineFunctionSignature(name, sig) } } + for name, sig := range m.FunctionSignatures { + canonical.DefineFunctionSignature(name, sig.Clone()) + } return canonical } diff --git a/types/io/manifest_test.go b/types/io/manifest_test.go index 6d7e4ee70..152ea6b56 100644 --- a/types/io/manifest_test.go +++ b/types/io/manifest_test.go @@ -1,6 +1,8 @@ package io import ( + "encoding/base64" + "errors" "testing" "github.com/wippyai/go-lua/analysis/domain/effect" @@ -13,6 +15,56 @@ import ( "github.com/wippyai/go-lua/types/typ" ) +func TestDecodeLegacyManifestReturnsMigrationSignal(t *testing.T) { + // Produced by go-lua v1.5.16's binary manifest codec from a representative + // record/type/global manifest. Keep the fixture immutable so detection stays + // compatible with real persisted cache entries. + const encoded = "SU5BTQgAAAAAAAAAAA0AAABjb21wYXQvbW9kdWxlAQ8CAAAABQAAAGNvdW50AwEABAAAAG5hbWUEAAAAAAABAAAABwAAAFBheWxvYWQOBAUAAAAAAQAAAA0AAABjb21wYXRfZ2xvYmFsAQ==" + data, err := base64.StdEncoding.DecodeString(encoded) + if err != nil { + t.Fatalf("DecodeString: %v", err) + } + if _, err := DecodeManifest(data); !errors.Is(err, ErrLegacyManifestWire) { + t.Fatalf("DecodeManifest error = %v, want ErrLegacyManifestWire", err) + } +} + +func TestDecodeLegacyStandaloneTypeReturnsMigrationSignal(t *testing.T) { + // v1 binary type encoding for typ.String is its one-byte kind tag. + if _, err := Decode([]byte{4}); !errors.Is(err, ErrLegacyTypeWire) { + t.Fatalf("Decode error = %v, want ErrLegacyTypeWire", err) + } +} + +func TestDecodeMalformedNonLegacyTypeDoesNotReturnMigrationSignal(t *testing.T) { + if _, err := Decode([]byte{0xff}); err == nil || errors.Is(err, ErrLegacyTypeWire) { + t.Fatalf("Decode error = %v, want non-legacy syntax error", err) + } +} + +func TestCompatibilityManifestPreservesCanonicalFunctionSignatures(t *testing.T) { + fn := typ.Func().Param("value", typ.String).Returns(typ.Boolean).Build() + want := signature.Function{ + Type: fn, + Effect: effect.Empty.With(returns.ErrorReturn{ValueIndex: 0, ErrorIndex: 1}), + } + m := NewManifest("compat/signature") + m.DefineFunctionSignature("send", want) + + encoded, err := m.Encode() + if err != nil { + t.Fatalf("Encode: %v", err) + } + decoded, err := DecodeManifest(encoded) + if err != nil { + t.Fatalf("DecodeManifest: %v", err) + } + got, ok := decoded.AllFunctionSignatures()["send"] + if !ok || !got.Equals(want) { + t.Fatalf("signature = %#v/%v, want %#v", got, ok, want) + } +} + func TestEncodeDecodeTypeUsesCanonicalManifestCodec(t *testing.T) { want := typ.NewRecord(). Field("name", typ.String). diff --git a/xpcall_direct_test.go b/xpcall_direct_test.go new file mode 100644 index 000000000..1a69afe59 --- /dev/null +++ b/xpcall_direct_test.go @@ -0,0 +1,296 @@ +package lua + +import ( + "strings" + "testing" +) + +// TestXPCallDirectCallCatchesError verifies that xpcall catches errors when +// invoked in a direct (non-coroutine) context, i.e. under DoString/PCall. +// +// basePCall wraps CallK in its own defer/recover (baselib.go), so pcall works +// whether the outermost boundary is PCall or threadRun. baseXPCall historically +// relied on threadRun's recover via handleProtectedError, which is ONLY the +// boundary inside coroutines. Under DoString the panic hit PCall's recover +// first, errFunc never fired, and the error leaked past xpcall. +func TestXPCallDirectCallCatchesError(t *testing.T) { + L := NewState() + defer L.Close() + + err := L.DoString(` + local ok, err = xpcall(function() error("boom") end, function(e) + return "handled: " .. tostring(e) + end) + assert(ok == false, "xpcall must return false on error") + assert(type(err) == "string", "xpcall must return the handler's value") + assert(string.find(err, "handled"), "xpcall must invoke the handler; got: " .. tostring(err)) + `) + if err != nil { + t.Fatalf("xpcall leaked the error past the call (bug): %v", err) + } +} + +// TestXPCallDirectCallPreservesSurroundingChunk verifies that a leaked xpcall +// error must not abort the rest of the chunk. Before the fix, the panic +// propagated through DoString and killed every statement after the xpcall. +func TestXPCallDirectCallPreservesSurroundingChunk(t *testing.T) { + L := NewState() + defer L.Close() + + err := L.DoString(` + local xpcall_ok, xpcall_err = xpcall(function() error("boom") end, function(e) + return "handled" + end) + after_marker = "reached" + `) + if err != nil { + t.Fatalf("xpcall error leaked and aborted the chunk: %v", err) + } + + if got := L.GetGlobal("after_marker").String(); got != "reached" { + t.Fatalf("statement after xpcall did not execute; after_marker=%q", got) + } +} + +// TestXPCallDirectErrorDoesNotSkipNextGoCall guards the protected-frame +// cleanup invariant. CallK stores xpcall's continuation on its call frame. If +// an error unwinds without clearing that metadata, the next Go-backed Lua call +// at the same stack depth resumes the stale xpcall continuation instead of +// invoking its own function. +func TestXPCallDirectErrorDoesNotSkipNextGoCall(t *testing.T) { + L := NewState() + defer L.Close() + + err := L.DoString(` + local ok, handled = xpcall(function() + error("first") + end, function(e) + return "handled: " .. tostring(e) + end) + assert(ok == false) + assert(string.find(handled, "handled")) + + local body_ran = false + local next_ok, next_value = pcall(function() + body_ran = true + return "next-result" + end) + assert(next_ok == true, "next pcall must execute normally") + assert(next_value == "next-result", "next pcall returned the wrong result") + assert(body_ran == true, "next pcall body was skipped by a stale continuation") + `) + if err != nil { + t.Fatalf("call after failed xpcall was corrupted: %v", err) + } +} + +// TestXPCallHandlerErrorDoesNotSkipNextGoCall covers the same cleanup path +// when the message handler itself raises. The handler's error must become the +// xpcall result, and no handler/continuation state may survive the return. +func TestXPCallHandlerErrorDoesNotSkipNextGoCall(t *testing.T) { + L := NewState() + defer L.Close() + + err := L.DoString(` + local ok, handled = xpcall(function() + error("original") + end, function() + error("handler-failed") + end) + assert(ok == false) + assert(string.find(tostring(handled), "handler%-failed")) + + local body_ran = false + local next_ok = pcall(function() + body_ran = true + end) + assert(next_ok == true) + assert(body_ran == true, "handler failure left stale frame metadata") + `) + if err != nil { + t.Fatalf("handler failure corrupted the next call: %v", err) + } +} + +func TestXPCallDirectErrorClearsFrameExtensions(t *testing.T) { + L := NewState() + defer L.Close() + + if err := L.DoString(` + xpcall(function() error("boom") end, function(e) return e end) + `); err != nil { + t.Fatal(err) + } + + for idx, ext := range L.frameExt { + if ext != nil && (ext.ErrFunc != nil || ext.Continuation != nil || ext.ContinuationCtx != nil) { + t.Fatalf("stale protected-call metadata remains at frame index %d: %+v", idx, ext) + } + } +} + +// TestPCallErrorClearsNestedFrameExtensions covers the general unwind path, +// not only xpcall's own frame. A Go function using CallK leaves continuation +// metadata on its frame when the called Lua function panics. The surrounding +// pcall must discard that metadata before the frame index is reused. +func TestPCallErrorClearsNestedFrameExtensions(t *testing.T) { + L := NewState() + defer L.Close() + + L.SetGlobal("call_with_continuation", L.NewFunction(func(L *LState) int { + fn := L.CheckFunction(1) + L.Push(fn) + L.CallK(0, 0, func(*LState, any, ResumeState) int { return 0 }, nil) + return 0 + })) + + markerCalled := false + L.SetGlobal("mark_called", L.NewFunction(func(*LState) int { + markerCalled = true + return 0 + })) + + err := L.DoString(` + local ok = pcall(function() + call_with_continuation(function() + error("nested failure") + end) + end) + assert(ok == false) + + local next_ok = pcall(function() + mark_called() + end) + assert(next_ok == true) + `) + if err != nil { + t.Fatalf("nested protected-call unwind failed: %v", err) + } + if !markerCalled { + t.Fatal("next Go call was skipped by a discarded frame's continuation") + } +} + +func TestAPIPCallErrorClearsNestedFrameExtensions(t *testing.T) { + L := NewState() + defer L.Close() + + L.SetGlobal("call_with_continuation", L.NewFunction(func(L *LState) int { + fn := L.CheckFunction(1) + L.Push(fn) + L.CallK(0, 0, func(*LState, any, ResumeState) int { return 0 }, nil) + return 0 + })) + + markerCalled := false + L.SetGlobal("mark_called", L.NewFunction(func(*LState) int { + markerCalled = true + return 0 + })) + + if err := L.DoString(` + function api_failure() + call_with_continuation(function() + error("api failure") + end) + end + function api_next_call() + mark_called() + end + `); err != nil { + t.Fatal(err) + } + + L.Push(L.GetGlobal("api_failure")) + if err := L.PCall(0, 0, nil); err == nil { + t.Fatal("expected API PCall to catch the nested error") + } + + L.Push(L.GetGlobal("api_next_call")) + if err := L.PCall(0, 0, nil); err != nil { + t.Fatalf("next API PCall failed: %v", err) + } + if !markerCalled { + t.Fatal("next API PCall was skipped by a discarded frame's continuation") + } +} + +// TestXPCallDirectCallReturnsTrueOnSuccess verifies the success path under +// direct call. +func TestXPCallDirectCallReturnsTrueOnSuccess(t *testing.T) { + L := NewState() + defer L.Close() + + err := L.DoString(` + local ok, val = xpcall(function() return "ok-value" end, function(e) + return "should-not-run" + end) + assert(ok == true, "xpcall must return true on success") + assert(val == "ok-value", "xpcall must return the function's results; got: " .. tostring(val)) + `) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +// TestXPCallNestedUnderPCall verifies xpcall also works when nested inside a +// pcall in a direct context (a common recovery pattern). +func TestXPCallNestedUnderPCall(t *testing.T) { + L := NewState() + defer L.Close() + + err := L.DoString(` + local outer_ok, outer_err = pcall(function() + local ok, err = xpcall(function() error("inner") end, function(e) + return "handled: " .. tostring(e) + end) + assert(ok == false, "xpcall should have caught the error") + assert(string.find(tostring(err), "handled"), "handler should have run") + return "completed" + end) + assert(outer_ok == true, "pcall must succeed because xpcall handled the error; got err: " .. tostring(outer_err)) + assert(outer_err == "completed", "unexpected outer return: " .. tostring(outer_err)) + `) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +// TestXPCallCoroutineStillWorks guards against the fix breaking the existing +// coroutine path (where xpcall was already handled via threadRun). +func TestXPCallCoroutineStillWorks(t *testing.T) { + L := NewState() + defer L.Close() + + if err := L.DoString(` + function with_handler() + local ok, err = xpcall(function() error("co-boom") end, function(e) + return "co-handled: " .. tostring(e) + end) + return ok, err + end + `); err != nil { + t.Fatal(err) + } + + co, cancel := L.NewThread() + defer cancel() + fn := L.GetGlobal("with_handler").(*LFunction) + + state, results, err := L.Resume(co, fn) + if err != nil { + t.Fatalf("resume failed: %v", err) + } + if state != ResumeOK { + t.Fatalf("expected ResumeOK, got %v", state) + } + if len(results) < 2 { + t.Fatalf("expected 2 results, got %d", len(results)) + } + if results[0] != LFalse { + t.Errorf("expected xpcall false, got %v", results[0]) + } + if !strings.Contains(results[1].String(), "co-handled") { + t.Errorf("expected handler output, got %v", results[1]) + } +}