From 6bbb77710c8c3e9cc7447b86df8ae9bc6d5ebc5d Mon Sep 17 00:00:00 2001 From: itsfuad Date: Wed, 7 Oct 2026 22:40:09 +0600 Subject: [PATCH] Remove leftover code and split long compiler functions Clean-code pass with no change to compiler behaviour, diagnostic text or generated output. Output for all 288 x_test projects was compared before and after: 249 LLVM files and every message are identical. Remove code that lost its last caller in earlier redesigns: - parser context stack and owner constants - AST-based place resolution (Resolve, LocalRoot and helpers) with the tests that only covered it, and the Binding wrapper it needed - typechecker copies of two flow diagnostics, Match.Arm - unused backend, IR, emitter and CLI helpers and struct fields - manifest.RemoveDependency (peeper remove does not use it) Simplify: - drop parameters and results no caller uses - merge CallableTypeFor with its private twin - share one quote scanner in the syntax highlighter, one string lexer handler, one temp-store helper in MIR lowering, one arm lookup on thir.Match, and one request helper in the LSP loop - replace deprecated zip and tar calls Split GenerateLLVMIR, checkForInStmt, workspace rebuild and the LSP Run loop into named steps by moving code unchanged. Keep printUnderlineIndent and the unused TOML helpers at maintainer request, with comments saying so. Move ServerState.recompile into a test file since only tests call it. Update architecture docs for removed names. --- cmd/cli/get.go | 8 - cmd/cli/get_test.go | 16 +- cmd/cli/ui.go | 4 - cmd/cli/ui_test.go | 3 +- docs/architecture/cli-and-packages.md | 4 +- docs/architecture/frontend.md | 6 +- docs/architecture/semantics-bindings.md | 24 +- internal/backend/llvm/emitter.go | 395 ++++++++++-------- internal/backend/llvm/emitter_test.go | 21 - internal/backend/llvm/instruction_emit.go | 6 +- internal/backend/llvm/type_layout.go | 12 - internal/backend/llvm/typed_builder.go | 16 +- internal/diagnostics/emitter.go | 19 +- internal/diagnostics/syntax_highlighter.go | 53 +-- internal/frontend/lexer/lexer.go | 56 +-- internal/frontend/parser/parser.go | 24 -- internal/ir/mir/expr_lower.go | 97 ++--- internal/ir/nodes.go | 19 +- internal/ir/thir/model.go | 10 + internal/lsp/server.go | 200 ++++----- internal/lsp/state.go | 9 - internal/lsp/state_test.go | 17 + internal/lsp/workspace.go | 152 ++++--- internal/semantics/analysis/flow_thir.go | 8 +- internal/semantics/analysis/ownership.go | 8 +- .../semantics/analysis/ownership_effects.go | 12 +- internal/semantics/analysis/ownership_expr.go | 2 +- internal/semantics/binder/binder.go | 23 +- internal/semantics/place/addressable.go | 57 +-- internal/semantics/place/origin.go | 148 +------ internal/semantics/place/origin_test.go | 291 +------------ internal/semantics/resolver/resolver.go | 4 +- internal/semantics/resolver/suggest.go | 23 +- internal/semantics/typechecker/check_expr.go | 8 +- internal/semantics/typechecker/check_fn.go | 14 +- internal/semantics/typechecker/check_stmt.go | 182 ++++---- internal/semantics/typechecker/errors.go | 15 - internal/semantics/typechecker/evidence.go | 22 +- internal/semantics/typeinfo/types.go | 6 +- pkg/distribution/pack.go | 14 +- pkg/manifest/manifest.go | 13 +- pkg/registry/download.go | 2 +- pkg/toml/data.go | 5 + pkg/toml/parser.go | 18 +- scripts/toolchain_sources_test.go | 9 - 45 files changed, 710 insertions(+), 1345 deletions(-) create mode 100644 internal/lsp/state_test.go diff --git a/cmd/cli/get.go b/cmd/cli/get.go index e7f5e34a..ad23465a 100644 --- a/cmd/cli/get.go +++ b/cmd/cli/get.go @@ -94,14 +94,6 @@ func prepareInstallContext() (*installContext, error) { }, nil } -func installAllDependencies() error { - ctx, err := prepareInstallContext() - if err != nil { - return err - } - return installManifestDependencies(ctx) -} - func installManifestDependencies(ctx *installContext) error { if len(ctx.file.Dependencies) == 0 { printInfo("No dependencies to install") diff --git a/cmd/cli/get_test.go b/cmd/cli/get_test.go index 1d9a4e86..f73e1957 100644 --- a/cmd/cli/get_test.go +++ b/cmd/cli/get_test.go @@ -62,8 +62,8 @@ build = "lib" } }() - if err := installAllDependencies(); err != nil { - t.Fatalf("installAllDependencies: %v", err) + if err := GetCommand(nil); err != nil { + t.Fatalf("GetCommand: %v", err) } if _, err := os.Stat(filepath.Join(versionedModule, manifest.FileName)); err != nil { t.Fatalf("expected restored versioned cache: %v", err) @@ -105,7 +105,7 @@ mock_path = "./mock" mustWriteGetTest(t, filepath.Join(cachePackage, "src", "pkg.peep"), "unlocked-cache") chdirForTest(t, root) - if err := installAllDependencies(); err != nil { + if err := GetCommand(nil); err != nil { t.Fatal(err) } lock, err := manifest.LoadLockfile(root) @@ -127,7 +127,7 @@ mock_path = "./mock" if err := os.Rename(mockPackage, offlineMock); err != nil { t.Fatal(err) } - if err := installAllDependencies(); err != nil { + if err := GetCommand(nil); err != nil { t.Fatalf("valid cache triggered refetch: %v", err) } if err := os.Rename(offlineMock, mockPackage); err != nil { @@ -138,7 +138,7 @@ mock_path = "./mock" if err := os.WriteFile(cacheSource, []byte("tampered"), 0o644); err != nil { t.Fatal(err) } - if err := installAllDependencies(); err != nil { + if err := GetCommand(nil); err != nil { t.Fatalf("tampered cache was not repaired: %v", err) } if data, err := os.ReadFile(cacheSource); err != nil || string(data) != "original" { @@ -155,7 +155,7 @@ mock_path = "./mock" if err != nil { t.Fatal(err) } - if err := installAllDependencies(); err == nil || !strings.Contains(err.Error(), "checksum") { + if err := GetCommand(nil); err == nil || !strings.Contains(err.Error(), "checksum") { t.Fatalf("moved tag error = %v", err) } lockAfter, err := os.ReadFile(filepath.Join(root, manifest.LockfileName)) @@ -191,7 +191,7 @@ child = "github.com/acme/child" mustWriteGetTest(t, filepath.Join(root, "mock", "acme", "child-v1.0.0", manifest.FileName), "name = \"child\"\nbuild = \"lib\"\n") chdirForTest(t, root) - if err := installAllDependencies(); err != nil { + if err := GetCommand(nil); err != nil { t.Fatal(err) } lock, err := manifest.LoadLockfile(root) @@ -307,7 +307,7 @@ mock_path = "./mock" } chdirForTest(t, root) - err = installAllDependencies() + err = GetCommand(nil) if test.wantError { if err == nil { t.Fatal("legacy migration succeeded") diff --git a/cmd/cli/ui.go b/cmd/cli/ui.go index 7ff82148..5706ccc9 100644 --- a/cmd/cli/ui.go +++ b/cmd/cli/ui.go @@ -16,10 +16,6 @@ func printInfo(text string) { fmt.Printf("ℹ %s\n", text) } -func printWarning(text string) { - fmt.Printf("⚠ %s\n", text) -} - func printError(text string) { fmt.Printf("✗ %s\n", text) } diff --git a/cmd/cli/ui_test.go b/cmd/cli/ui_test.go index a28c670a..9eceb240 100644 --- a/cmd/cli/ui_test.go +++ b/cmd/cli/ui_test.go @@ -33,7 +33,6 @@ func TestUIPrinters(t *testing.T) { printHeader("Header") printSuccess("ok") printInfo("info") - printWarning("warn") printError("err") printUpdate("upd") printPackage("pkg", "1.0.0") @@ -42,7 +41,7 @@ func TestUIPrinters(t *testing.T) { printCached() printTransitive("dep", "2.0.0") }) - for _, part := range []string{"Header", "ok", "info", "warn", "err", "upd", "pkg @1.0.0", "dim", "dl", "cached", "dep@2.0.0"} { + for _, part := range []string{"Header", "ok", "info", "err", "upd", "pkg @1.0.0", "dim", "dl", "cached", "dep@2.0.0"} { if !strings.Contains(out, part) { t.Fatalf("missing %q in output: %q", part, out) } diff --git a/docs/architecture/cli-and-packages.md b/docs/architecture/cli-and-packages.md index e4cff63e..5cf349f9 100644 --- a/docs/architecture/cli-and-packages.md +++ b/docs/architecture/cli-and-packages.md @@ -82,7 +82,7 @@ registry tests check unique names, aliases, arity contracts, and help output. `cmd/cli` owns user-facing dependency operations. It uses `manifest` for project state, `registry` for package content and versions, `remotes` for host validation, and `semver` for constraints. `ui.go` is a thin stdout vocabulary: -`printHeader`, `printSuccess`, `printInfo`, `printWarning`, `printError`, +`printHeader`, `printSuccess`, `printInfo`, `printError`, `printUpdate`, `printPackage`, `printDim`, `printDownload`, `printCached`, and `printTransitive`. @@ -205,7 +205,7 @@ validator; `StripProviderPrefix` returns the provider-local path when valid. rule to CLI and LSP. `Load` parses package metadata, compiler constraint, build type, neighbor/remote dependencies, and optional mock-remote settings. `ValidatePackageName` and `ParseDependency` own package/dependency validation. - `Save` and `RemoveDependency` serialize manifest state. + `Save` serializes manifest state. - `lockfile.go`: `LockfileEntry` stores version, resolved URL, checksum, direct bit, description, dependency edges, reverse `UsedBy`, and timestamp. `Lockfile` stores schema, alias-to-package `DirectDeps`, package entries, and diff --git a/docs/architecture/frontend.md b/docs/architecture/frontend.md index 211847fc..a8db7ef6 100644 --- a/docs/architecture/frontend.md +++ b/docs/architecture/frontend.md @@ -117,8 +117,8 @@ precede `.`. Whitespace and ordinary comments are skipped. - `numberHandler` emits `NUMBER` without numeric interpretation. - `docHandler` emits `DOC_COMMENT`; it removes the `///` prefix and trims the remaining text. Normal `//` and `/* ... */` comments are discarded. -- `stringHandler` and `cstringHandler` remove delimiters, decode escapes, and - emit `STRING` or `CSTRING`. +- `stringHandler` removes delimiters, decodes escapes, and emits `STRING` or + `CSTRING`. - `charHandler` decodes a single-quoted value and requires exactly one valid UTF-8 rune. - `byteCharHandler` decodes a `b'...'` value and requires exactly one byte. @@ -156,7 +156,7 @@ ranges must retain the same one-based convention. ### Shared state: `parser/parser.go` `Parser` stores the file path, token stream, diagnostic bag, current token index, -next AST node ID, and a context stack. `New` receives +and next AST node ID. `New` receives the already-tokenized stream; the lexer is not called by the parser. `current`, `next`, and `prev` inspect the stream without advancing. `advance` diff --git a/docs/architecture/semantics-bindings.md b/docs/architecture/semantics-bindings.md index dbe71679..f5b40ccc 100644 --- a/docs/architecture/semantics-bindings.md +++ b/docs/architecture/semantics-bindings.md @@ -81,7 +81,7 @@ This map records binding, type, place, intrinsic, and constant implementation ob wins and shadowing is lexical. - `Symbols` preserves declaration order; declaration identity comes from the binding result rather than scanning symbol AST pointers. - `IsMutableBinding` combines lookup, kind, and `Symbol.IsMutable`. -- `Parent` supports bounded analyses such as `place.LocalRoot`. +- `Parent` supports bounded scope walks. ## Symbol index @@ -167,8 +167,7 @@ be discarded. Backing maps remain private. - Functions with parameters are added to `OperationFunctions`. - `bindModuleBinding` binds an explicit source type through `TypeFromSyntax`. - Missing top-level types retain `UnknownType` for later inference/recovery. -- `moduleScopeSymbol`, `bindModuleScopeType`, and - `bindModuleScopeTypeIfUnset` centralize module-local symbol access and writes. +- `moduleScopeSymbol` centralizes module-local symbol access. ### Named type binding @@ -393,29 +392,16 @@ are `types.go`, `syntax.go`, `relations.go`, `compatibility.go`, `lookup.go`, - `Project` recognizes one selector or non-slicing index projection. - `Decompose` returns root expression and projections in source order. - Slices are not independently addressable places; range indexes become wildcard - origin projections in `Resolve`. + origin projections. - `Addressable` answers whether an identifier or projection names addressable storage, including pointer/reference bases. - `MutableAddressable` preserves binding permission through value and owning-pointer projections. Mutable references grant referent access; shared-reference restrictions survive subsequent safe projections. It also reports shared reference targets and the mutable binding required by the access, for diagnostics and usage tracking. -- `LocalRoot` finds a caller-local root below module scope and stops at pointer - indirection. -- `Binding` carries a symbol and a transient `Local` flag. - `BindingResolver` supplies symbols for identifiers injected into cloned defaults. -- Expanded defaults use `Local=false`; declaration-module storage must not become a - caller pointer-escape source. -- `Resolve` is the canonical place walk and returns storage origins, value origins, - dependencies, and stability. -- Identifiers start with a symbol root. -- Selectors append field projections. -- Constant indexes append stable index projections. -- Integral identifier indexes append binding-index projections and dependencies. -- Unknown indexes and range indexes append wildcard projections. -- Owned pointers append pointee projections for the projected base. -- Proven variant cases append variant-payload projections. -- Reference and raw-pointer value origins can be normalized through callbacks. +- `Resolution` carries storage origins, value origins, dependencies, and + stability; flow analysis over THIR builds it. - `OriginsOverlap` is conservative; concrete different fields/fixed indexes prove disjointness, while prefixes, wildcards, and symbolic indexes may overlap. diff --git a/internal/backend/llvm/emitter.go b/internal/backend/llvm/emitter.go index 0dd3fcdf..89f62eee 100644 --- a/internal/backend/llvm/emitter.go +++ b/internal/backend/llvm/emitter.go @@ -62,39 +62,106 @@ func GenerateLLVMIR(mod *mir.Module, diag *diagnostics.DiagnosticBag, targetInfo } printUsed, _, allocUsed, allocatorRuntimeUsed, freeRuntimeUsed := moduleRuntimeOperations(mod) if printUsed { - b.WriteString("@.print.signed = private unnamed_addr constant [5 x i8] c\"%lld\\00\", align 1\n") - b.WriteString("@.print.unsigned = private unnamed_addr constant [5 x i8] c\"%llu\\00\", align 1\n") - b.WriteString("@.print.float = private unnamed_addr constant [3 x i8] c\"%g\\00\", align 1\n") - b.WriteString("@.print.string = private unnamed_addr constant [3 x i8] c\"%s\\00\", align 1\n") - b.WriteString("@.print.str = private unnamed_addr constant [5 x i8] c\"%.*s\\00\", align 1\n") - b.WriteString("@.print.pointer = private unnamed_addr constant [3 x i8] c\"%p\\00\", align 1\n") - b.WriteString("@.print.true = private unnamed_addr constant [5 x i8] c\"true\\00\", align 1\n") - b.WriteString("@.print.false = private unnamed_addr constant [6 x i8] c\"false\\00\", align 1\n\n") - b.WriteString("@.print.newline = private unnamed_addr constant [2 x i8] c\"\\0A\\00\", align 1\n\n") - } - - for _, entry := range mod.StaticData { + emitPrintFormats(&b) + } + emitter.emitStaticData(&b) + interfaceMakes := emitter.emitInterfaceTables(&b) + runtimeFreeDeclared, runtimeAllocDeclared := emitter.emitFunctionDeclarations(&b) + if printUsed { + fmt.Fprintf(&b, "declare i32 @%s(i8*, ...)\n\n", runtimePrintfSymbol) + } + if (allocatorRuntimeUsed || freeRuntimeUsed) && !runtimeFreeDeclared { + fmt.Fprintf(&b, "declare void @%s(i8*)\n\n", runtimeFreeSymbol) + } + if allocUsed || allocatorRuntimeUsed { + sizeType := emitter.layout(mod.Types.IndexType()).Text + if !runtimeAllocDeclared { + fmt.Fprintf(&b, "declare i8* @%s(%s)\n", runtimeAllocSymbol, sizeType) + } + if allocUsed { + fmt.Fprintf(&b, "declare { %s, i1 } @llvm.umul.with.overflow.%s(%s, %s)\n\n", sizeType, sizeType, sizeType, sizeType) + } else { + b.WriteString("\n") + } + } + + if allocatorRuntimeUsed { + emitDefaultDescriptorThunks(&b, emitter) + } + + emitter.emitCallDeclarations(&b, allocUsed || allocatorRuntimeUsed) + + hasDefine := false + for _, fn := range mod.Funcs { + if fn != nil && fn.Blocks != nil { + hasDefine = true + break + } + } + if !hasDefine { + return finalLLVMText(&b, emitter) + } + emitter.emitNamedDropHelpers(&b) + for _, thunk := range mod.InterfaceThunks { + emitInterfaceThunk(&b, emitter, thunk) + } + for _, makeVal := range interfaceMakes { + emitInterfacePayloadDropThunk(&b, emitter, makeVal) + if isOwnedInterfaceType(mod.Types, makeVal.Type) { + emitInterfacePayloadReleaseThunk(&b, emitter, makeVal) + } + } + if len(mod.InterfaceThunks) > 0 || len(interfaceMakes) > 0 { + b.WriteString("\n") + } + for _, fn := range mod.Funcs { + if fn == nil || fn.Blocks == nil { + continue + } + emitter.emitFunctionDefinition(&b, fn) + } + return finalLLVMText(&b, emitter) +} + +func emitPrintFormats(b *strings.Builder) { + b.WriteString("@.print.signed = private unnamed_addr constant [5 x i8] c\"%lld\\00\", align 1\n") + b.WriteString("@.print.unsigned = private unnamed_addr constant [5 x i8] c\"%llu\\00\", align 1\n") + b.WriteString("@.print.float = private unnamed_addr constant [3 x i8] c\"%g\\00\", align 1\n") + b.WriteString("@.print.string = private unnamed_addr constant [3 x i8] c\"%s\\00\", align 1\n") + b.WriteString("@.print.str = private unnamed_addr constant [5 x i8] c\"%.*s\\00\", align 1\n") + b.WriteString("@.print.pointer = private unnamed_addr constant [3 x i8] c\"%p\\00\", align 1\n") + b.WriteString("@.print.true = private unnamed_addr constant [5 x i8] c\"true\\00\", align 1\n") + b.WriteString("@.print.false = private unnamed_addr constant [6 x i8] c\"false\\00\", align 1\n\n") + b.WriteString("@.print.newline = private unnamed_addr constant [2 x i8] c\"\\0A\\00\", align 1\n\n") +} + +func (e *llvmEmitter) emitStaticData(b *strings.Builder) { + for _, entry := range e.mod.StaticData { if entry.Constant == nil { escaped := llvmEscapeString(entry.Bytes) - fmt.Fprintf(&b, "%s = private unnamed_addr constant [%d x i8] c\"%s\", align %d\n", entry.Name, len(entry.Bytes)+1, escaped, entry.Align) + fmt.Fprintf(b, "%s = private unnamed_addr constant [%d x i8] c\"%s\", align %d\n", entry.Name, len(entry.Bytes)+1, escaped, entry.Align) continue } - value, ok := emitter.staticConstant(entry.Constant, entry.Type) + value, ok := e.staticConstant(entry.Constant, entry.Type) if !ok { - emitter.markInvalid("cannot lower typed static constant " + entry.Name) + e.markInvalid("cannot lower typed static constant " + entry.Name) continue } - llvmType := emitter.layout(entry.Type).Text - fmt.Fprintf(&b, "%s = constant %s %s\n", entry.Name, llvmType, value) + llvmType := e.layout(entry.Type).Text + fmt.Fprintf(b, "%s = constant %s %s\n", entry.Name, llvmType, value) } - if len(mod.StaticData) > 0 { + if len(e.mod.StaticData) > 0 { b.WriteString("\n") } +} +// emitInterfaceTables writes one itab per distinct interface/data pair and +// returns the makes that own them, in first-use order. +func (e *llvmEmitter) emitInterfaceTables(b *strings.Builder) []*mir.InterfaceMake { emittedItabs := make(map[string]bool) interfaceMakes := make([]*mir.InterfaceMake, 0) hasItab := false - for _, fn := range mod.Funcs { + for _, fn := range e.mod.Funcs { if fn == nil || fn.Blocks == nil { continue } @@ -111,7 +178,7 @@ func GenerateLLVMIR(mod *mir.Module, diag *diagnostics.DiagnosticBag, targetInfo if !ok || makeVal == nil { continue } - itabSym := interfaceSymbolName("itab", mod.Types, makeVal.Type, makeVal.DataType) + itabSym := interfaceSymbolName("itab", e.mod.Types, makeVal.Type, makeVal.DataType) if emittedItabs[itabSym] { continue } @@ -119,10 +186,10 @@ func GenerateLLVMIR(mod *mir.Module, diag *diagnostics.DiagnosticBag, targetInfo interfaceMakes = append(interfaceMakes, makeVal) hasItab = true b.WriteString(itabSym) - fmt.Fprintf(&b, " = private constant [%d x i8*] [", interfaceVtableLength(mod.Types, makeVal.Type, len(makeVal.Slots))) - fmt.Fprintf(&b, "i8* bitcast (void (i8*)* %s to i8*)", interfaceSymbolName("iface_drop", mod.Types, makeVal.Type, makeVal.DataType)) - if isOwnedInterfaceType(mod.Types, makeVal.Type) { - fmt.Fprintf(&b, ", i8* bitcast (void (i8*, i8*)* %s to i8*)", interfaceSymbolName("iface_release", mod.Types, makeVal.Type, makeVal.DataType)) + fmt.Fprintf(b, " = private constant [%d x i8*] [", interfaceVtableLength(e.mod.Types, makeVal.Type, len(makeVal.Slots))) + fmt.Fprintf(b, "i8* bitcast (void (i8*)* %s to i8*)", interfaceSymbolName("iface_drop", e.mod.Types, makeVal.Type, makeVal.DataType)) + if isOwnedInterfaceType(e.mod.Types, makeVal.Type) { + fmt.Fprintf(b, ", i8* bitcast (void (i8*, i8*)* %s to i8*)", interfaceSymbolName("iface_release", e.mod.Types, makeVal.Type, makeVal.DataType)) } for _, slot := range makeVal.Slots { b.WriteString(", ") @@ -133,15 +200,15 @@ func GenerateLLVMIR(mod *mir.Module, diag *diagnostics.DiagnosticBag, targetInfo } else { slotName = "null" } - slotLayout := emitter.layout(slot.TypeID()) + slotLayout := e.layout(slot.TypeID()) if slotLayout == nil || slotLayout.Kind != llvmLayoutFunction { - emitter.markInvalid("interface slot reached LLVM without published function type") + e.markInvalid("interface slot reached LLVM without published function type") slotLayout = llvmPointerLayout(llvmScalarLayout("i8")) } if slotName == "null" { b.WriteString("i8* null") } else { - fmt.Fprintf(&b, "i8* bitcast (%s %s to i8*)", slotLayout.Text, slotName) + fmt.Fprintf(b, "i8* bitcast (%s %s to i8*)", slotLayout.Text, slotName) } } b.WriteString("], align 8\n") @@ -151,27 +218,30 @@ func GenerateLLVMIR(mod *mir.Module, diag *diagnostics.DiagnosticBag, targetInfo if hasItab { b.WriteString("\n") } + return interfaceMakes +} +// emitFunctionDeclarations declares bodiless functions and reports whether the +// module already declares the runtime free and alloc symbols itself. +func (e *llvmEmitter) emitFunctionDeclarations(b *strings.Builder) (runtimeFreeDeclared, runtimeAllocDeclared bool) { hasDecl := false - runtimeFreeDeclared := false - runtimeAllocDeclared := false - for _, fn := range mod.Funcs { + for _, fn := range e.mod.Funcs { if fn == nil { continue } name := ir.SanitizeSymbolName(fn.Name) if name == runtimeFreeSymbol { - runtimeFreeDeclared = runtimeFreeDeclared || runtimeFreeDeclaration(mod.Types, fn) + runtimeFreeDeclared = runtimeFreeDeclared || runtimeFreeDeclaration(e.mod.Types, fn) } if name == runtimeAllocSymbol { - runtimeAllocDeclared = runtimeAllocDeclared || runtimeAllocDeclaration(mod.Types, fn) + runtimeAllocDeclared = runtimeAllocDeclared || runtimeAllocDeclaration(e.mod.Types, fn) } if fn.Blocks != nil { continue } hasDecl = true b.WriteString("declare ") - b.WriteString(emitter.layout(llvmFunctionReturnType(mod.Types, fn)).Text) + b.WriteString(e.layout(llvmFunctionReturnType(e.mod.Types, fn)).Text) b.WriteString(" @") b.WriteString(name) b.WriteString("(") @@ -179,42 +249,26 @@ func GenerateLLVMIR(mod *mir.Module, diag *diagnostics.DiagnosticBag, targetInfo if i > 0 { b.WriteString(", ") } - b.WriteString(emitter.layout(param.Type).Text) + b.WriteString(e.layout(param.Type).Text) } b.WriteString(")\n") } if hasDecl { b.WriteString("\n") } - if printUsed { - fmt.Fprintf(&b, "declare i32 @%s(i8*, ...)\n\n", runtimePrintfSymbol) - } - if (allocatorRuntimeUsed || freeRuntimeUsed) && !runtimeFreeDeclared { - fmt.Fprintf(&b, "declare void @%s(i8*)\n\n", runtimeFreeSymbol) - } - if allocUsed || allocatorRuntimeUsed { - sizeType := emitter.layout(mod.Types.IndexType()).Text - if !runtimeAllocDeclared { - fmt.Fprintf(&b, "declare i8* @%s(%s)\n", runtimeAllocSymbol, sizeType) - } - if allocUsed { - fmt.Fprintf(&b, "declare { %s, i1 } @llvm.umul.with.overflow.%s(%s, %s)\n\n", sizeType, sizeType, sizeType, sizeType) - } else { - b.WriteString("\n") - } - } - - if allocatorRuntimeUsed { - emitDefaultDescriptorThunks(&b, emitter) - } + return runtimeFreeDeclared, runtimeAllocDeclared +} - decls := collectCallDecls(mod) +// emitCallDeclarations declares called externals. skipRuntimeAlloc leaves out +// the runtime alloc symbol when the runtime declarations already cover it. +func (e *llvmEmitter) emitCallDeclarations(b *strings.Builder, skipRuntimeAlloc bool) { + decls := collectCallDecls(e.mod) for _, decl := range decls { - if (allocUsed || allocatorRuntimeUsed) && ir.SanitizeSymbolName(decl.Name) == runtimeAllocSymbol { + if skipRuntimeAlloc && ir.SanitizeSymbolName(decl.Name) == runtimeAllocSymbol { continue } b.WriteString("declare ") - b.WriteString(emitter.layout(decl.ReturnType).Text) + b.WriteString(e.layout(decl.ReturnType).Text) b.WriteString(" @") b.WriteString(ir.SanitizeSymbolName(decl.Name)) b.WriteString("(") @@ -222,147 +276,124 @@ func GenerateLLVMIR(mod *mir.Module, diag *diagnostics.DiagnosticBag, targetInfo if i > 0 { b.WriteString(", ") } - b.WriteString(emitter.layout(param).Text) + b.WriteString(e.layout(param).Text) } b.WriteString(")\n") } if len(decls) > 0 { b.WriteString("\n") } +} - hasDefine := false - for _, fn := range mod.Funcs { - if fn != nil && fn.Blocks != nil { - hasDefine = true - break - } - } - if !hasDefine { - return finalLLVMText(&b, emitter) - } - emitter.emitNamedDropHelpers(&b) - for _, thunk := range mod.InterfaceThunks { - emitInterfaceThunk(&b, emitter, thunk) +func (e *llvmEmitter) emitFunctionDefinition(b *strings.Builder, fn *mir.Function) { + debugScopeID := -1 + if e.debug != nil { + debugScopeID = e.debug.functionID(fn) + } + b.WriteString("define ") + b.WriteString(e.layout(llvmFunctionReturnType(e.mod.Types, fn)).Text) + b.WriteString(" @") + b.WriteString(ir.SanitizeSymbolName(fn.Name)) + b.WriteString("(") + for i, param := range fn.Params { + if i > 0 { + b.WriteString(", ") + } + b.WriteString(e.layout(param.Type).Text) + b.WriteString(" %") + b.WriteString(param.Name) + } + b.WriteString(")") + if debugScopeID >= 0 { + fmt.Fprintf(b, " !dbg !%d", debugScopeID) + } + b.WriteString(" {\n") + lb := newLLVMBuilder(b, e, debugScopeID) + stackSlots := stackLocalSlots(e.mod.Types, fn) + for _, param := range fn.Params { + lb.locals[param.Name] = lb.value("%"+param.Name, e.layout(param.Type)) } - for _, makeVal := range interfaceMakes { - emitInterfacePayloadDropThunk(&b, emitter, makeVal) - if isOwnedInterfaceType(mod.Types, makeVal.Type) { - emitInterfacePayloadReleaseThunk(&b, emitter, makeVal) + for _, block := range fn.Blocks { + if block == nil { + continue } + e.emitBlock(lb, fn, block, stackSlots) } - if len(mod.InterfaceThunks) > 0 || len(interfaceMakes) > 0 { - b.WriteString("\n") + b.WriteString("}\n") +} + +func (e *llvmEmitter) emitBlock(lb *llvmBuilder, fn *mir.Function, block *mir.Block, stackSlots []stackLocalSlot) { + lb.label(block.ID) + if block.ID == fn.EntryID { + emitStackLocalSlots(lb, stackSlots) } - for _, fn := range mod.Funcs { - if fn == nil || fn.Blocks == nil { + for _, instr := range block.Instrs { + if instr == nil { continue } - debugScopeID := -1 - if emitter.debug != nil { - debugScopeID = emitter.debug.functionID(fn) - } - b.WriteString("define ") - b.WriteString(emitter.layout(llvmFunctionReturnType(mod.Types, fn)).Text) - b.WriteString(" @") - b.WriteString(ir.SanitizeSymbolName(fn.Name)) - b.WriteString("(") - for i, param := range fn.Params { - if i > 0 { - b.WriteString(", ") + lb.setLocation(instr.SourceLocation()) + // Emitting nothing for an unrecognized instruction silently drops + // program behavior, so every MIR instruction must be classified + // here. mir.Instr is sealed, so the only way to reach the + // default is to add an instruction inside the mir package and + // not classify it here: a compiler bug, not invalid source. + switch typed := instr.(type) { + case *mir.Assign: + val := emitValueExpr(lb, typed.Value) + if ptr, ok := lb.localPtrs[typed.Name]; ok { + lb.store(ptr, val) + } else { + lb.locals[typed.Name] = val } - b.WriteString(emitter.layout(param.Type).Text) - b.WriteString(" %") - b.WriteString(param.Name) - } - b.WriteString(")") - if debugScopeID >= 0 { - fmt.Fprintf(&b, " !dbg !%d", debugScopeID) - } - b.WriteString(" {\n") - lb := newLLVMBuilder(&b, emitter, debugScopeID) - stackSlots := stackLocalSlots(mod.Types, fn) - for _, param := range fn.Params { - lb.locals[param.Name] = lb.value("%"+param.Name, emitter.layout(param.Type)) - } - for _, block := range fn.Blocks { - if block == nil { - continue + case *mir.Store: + emitStore(lb, typed) + case *mir.Print: + emitPrint(lb, typed) + case *mir.Drop: + emitDrop(lb, typed) + case *mir.DynamicArrayOp: + emitDynamicArrayOp(lb, typed) + case *mir.Call: + emitDiscardedCall(lb, typed) + case *mir.InterfaceCall: + emitDiscardedInterfaceCall(lb, typed) + default: + panic(fmt.Sprintf("LLVM emission: unhandled MIR instruction %T", instr)) + } + } + // Both terminator invariants are compiler bugs, not invalid source: + // every block carries a terminator, and every terminator kind emits + // one. Skipping either silently produces unterminated LLVM IR. + // mir.Terminator is sealed, so the unhandled-kind default below can + // only be reached from inside the mir package. + if block.Term == nil { + panic(fmt.Sprintf("LLVM emission: block b%d has no terminator", block.ID)) + } + returnLayout := e.layout(llvmFunctionReturnType(e.mod.Types, fn)) + lb.setLocation(block.Term.SourceLocation()) + switch term := block.Term.(type) { + case *mir.Jump: + lb.branch(fmt.Sprintf("b%d", term.TargetID)) + case *mir.Branch: + cond := emitCondRef(lb, term.Cond) + lb.condBranch(cond, fmt.Sprintf("b%d", term.ThenID), fmt.Sprintf("b%d", term.ElseID)) + case *mir.SwitchVariant: + emitVariantSwitch(lb, term) + case *mir.Ret: + if term.Value == nil || isVoidType(e.mod.Types, fn.ReturnType) { + if returnLayout.Kind != llvmLayoutVoid { + lb.ret(lb.value("0", returnLayout), returnLayout) + } else { + lb.retVoid(returnLayout) } - lb.label(block.ID) - if block.ID == fn.EntryID { - emitStackLocalSlots(lb, stackSlots) - } - for _, instr := range block.Instrs { - if instr == nil { - continue - } - lb.setLocation(instr.SourceLocation()) - // Emitting nothing for an unrecognized instruction silently drops - // program behavior, so every MIR instruction must be classified - // here. mir.Instr is sealed, so the only way to reach the - // default is to add an instruction inside the mir package and - // not classify it here: a compiler bug, not invalid source. - switch typed := instr.(type) { - case *mir.Assign: - val := emitValueExpr(lb, typed.Value) - if ptr, ok := lb.localPtrs[typed.Name]; ok { - lb.store(ptr, val) - } else { - lb.locals[typed.Name] = val - } - case *mir.Store: - emitStore(lb, typed) - case *mir.Print: - emitPrint(lb, typed) - case *mir.Drop: - emitDrop(lb, typed) - case *mir.DynamicArrayOp: - emitDynamicArrayOp(lb, typed) - case *mir.Call: - emitDiscardedCall(lb, typed) - case *mir.InterfaceCall: - emitDiscardedInterfaceCall(lb, typed) - default: - panic(fmt.Sprintf("LLVM emission: unhandled MIR instruction %T", instr)) - } - } - // Both terminator invariants are compiler bugs, not invalid source: - // every block carries a terminator, and every terminator kind emits - // one. Skipping either silently produces unterminated LLVM IR. - // mir.Terminator is sealed, so the unhandled-kind default below can - // only be reached from inside the mir package. - if block.Term == nil { - panic(fmt.Sprintf("LLVM emission: block b%d has no terminator", block.ID)) - } - returnLayout := emitter.layout(llvmFunctionReturnType(mod.Types, fn)) - lb.setLocation(block.Term.SourceLocation()) - switch term := block.Term.(type) { - case *mir.Jump: - lb.branch(fmt.Sprintf("b%d", term.TargetID)) - case *mir.Branch: - cond := emitCondRef(lb, term.Cond) - lb.condBranch(cond, fmt.Sprintf("b%d", term.ThenID), fmt.Sprintf("b%d", term.ElseID)) - case *mir.SwitchVariant: - emitVariantSwitch(lb, term) - case *mir.Ret: - if term.Value == nil || isVoidType(mod.Types, fn.ReturnType) { - if returnLayout.Kind != llvmLayoutVoid { - lb.ret(lb.value("0", returnLayout), returnLayout) - } else { - lb.retVoid(returnLayout) - } - continue - } - val := emitRef(lb, term.Value) - lb.ret(val, returnLayout) - default: - panic(fmt.Sprintf("LLVM emission: unhandled MIR terminator %T", block.Term)) - } - lb.setLocation(nil) + return } - b.WriteString("}\n") + val := emitRef(lb, term.Value) + lb.ret(val, returnLayout) + default: + panic(fmt.Sprintf("LLVM emission: unhandled MIR terminator %T", block.Term)) } - return finalLLVMText(&b, emitter) + lb.setLocation(nil) } func (e *llvmEmitter) staticConstant(value constvalue.Value, typeID ir.TypeID) (string, bool) { diff --git a/internal/backend/llvm/emitter_test.go b/internal/backend/llvm/emitter_test.go index c9c3968b..d33e35c7 100644 --- a/internal/backend/llvm/emitter_test.go +++ b/internal/backend/llvm/emitter_test.go @@ -2195,7 +2195,6 @@ func TestGenerateLLVMIRReslicesSharedViewWithoutCapacity(t *testing.T) { } func TestGenerateLLVMIRLowersZeroValueOptionals(t *testing.T) { - const targetTriple = "x86_64-unknown-linux-gnu" mod := &mir.Module{ Name: "test", Types: llvmTypes.table, @@ -2244,7 +2243,6 @@ func TestGenerateLLVMIRLowersZeroValueOptionals(t *testing.T) { } func TestGenerateLLVMIRLowersVariantMake(t *testing.T) { - const targetTriple = "x86_64-unknown-linux-gnu" mod := &mir.Module{ Name: "test", Types: llvmTypes.table, @@ -2294,7 +2292,6 @@ func TestGenerateLLVMIRLowersVariantMake(t *testing.T) { } func TestGenerateLLVMIRReadsTaggedOptionalPresence(t *testing.T) { - const targetTriple = "x86_64-unknown-linux-gnu" mod := &mir.Module{ Name: "test", Types: llvmTypes.table, @@ -2361,7 +2358,6 @@ func TestGenerateLLVMIRLoadsTaggedOptionalPayload(t *testing.T) { } func TestGenerateLLVMIRLoopMutationUsesStackSlot(t *testing.T) { - const targetTriple = "x86_64-unknown-linux-gnu" mod := &mir.Module{ Name: "test", Types: llvmTypes.table, @@ -2504,7 +2500,6 @@ func TestGenerateLLVMIRDeclaresDiscardedDirectCall(t *testing.T) { } func TestGenerateLLVMIRDebugMetadata(t *testing.T) { - const targetTriple = "x86_64-unknown-linux-gnu" mod := &mir.Module{ Name: "test", Types: llvmTypes.table, @@ -2551,7 +2546,6 @@ func TestGenerateLLVMIRDebugMetadata(t *testing.T) { } func TestGenerateLLVMIRDebugMetadataPreservesNestedExpressionLines(t *testing.T) { - const targetTriple = "x86_64-unknown-linux-gnu" mod := &mir.Module{ Name: "test", Types: llvmTypes.table, @@ -2609,7 +2603,6 @@ func TestGenerateLLVMIRDebugMetadataPreservesNestedExpressionLines(t *testing.T) } func TestGenerateLLVMIRExplicitBoolCastUsesCompare(t *testing.T) { - const targetTriple = "x86_64-unknown-linux-gnu" mod := &mir.Module{ Name: "test", Types: llvmTypes.table, @@ -2664,7 +2657,6 @@ func TestGenerateLLVMIRExplicitBoolCastUsesCompare(t *testing.T) { } func TestGenerateLLVMIRLowersIndirectFieldPlaceWithoutTempAlloca(t *testing.T) { - const targetTriple = "x86_64-unknown-linux-gnu" owned := llvmTypes.table.Intern(ir.Type{Kind: ir.TypeOwnedPtr, Elem: llvmTypes.valueStruct}) shared := llvmTypes.refValueStruct mutable := llvmTypes.table.Intern(ir.Type{Kind: ir.TypeReference, IsMutable: true, Elem: llvmTypes.valueStruct}) @@ -2730,7 +2722,6 @@ func TestGenerateLLVMIRLowersIndirectFieldPlaceWithoutTempAlloca(t *testing.T) { } func TestGenerateLLVMIRLowersProjectedFieldRawAddressDirectly(t *testing.T) { - const targetTriple = "x86_64-unknown-linux-gnu" mutableStruct := llvmTypes.table.Intern(ir.Type{Kind: ir.TypeReference, IsMutable: true, Elem: llvmTypes.valueStruct}) mod := &mir.Module{ Name: "test", @@ -2901,7 +2892,6 @@ func indexStoreMIRModule(baseType ir.TypeID, index mir.ValueRef) *mir.Module { } func TestGenerateLLVMIRLowersIndexPlaceForArrayRead(t *testing.T) { - const targetTriple = "x86_64-unknown-linux-gnu" mod := indexReadMIRModule(llvmTypes.fixed4I32, &mir.RefConst{Value: "0", Type: llvmTypes.i32}) irText := GenerateLLVMIR(mod, diagnostics.NewDiagnosticBag(), testLinuxAMD64, false) if !strings.Contains(irText, "getelementptr inbounds [4 x i32], [4 x i32]*") { @@ -2964,7 +2954,6 @@ func TestGenerateLLVMIRNormalizesConstantArrayIndexes(t *testing.T) { } func TestGenerateLLVMIRBoundsChecksRuntimeFixedArrayIndex(t *testing.T) { - const targetTriple = "x86_64-unknown-linux-gnu" mod := indexReadMIRModule(llvmTypes.fixed4I32, &mir.RefName{Name: "index", Type: llvmTypes.i32}) irText := GenerateLLVMIR(mod, diagnostics.NewDiagnosticBag(), testLinuxAMD64, false) for _, expected := range []string{"sext i32 %index to i64", "icmp uge i64", "call void @llvm.trap()", "getelementptr inbounds [4 x i32]"} { @@ -2975,7 +2964,6 @@ func TestGenerateLLVMIRBoundsChecksRuntimeFixedArrayIndex(t *testing.T) { } func TestGenerateLLVMIRBoundsChecksWideRuntimeFixedArrayIndexBeforeTruncation(t *testing.T) { - const targetTriple = "x86_64-unknown-linux-gnu" mod := indexReadMIRModule(llvmTypes.fixed4I32, &mir.RefName{Name: "index", Type: llvmTypes.u128}) irText := GenerateLLVMIR(mod, diagnostics.NewDiagnosticBag(), testLinuxAMD64, false) for _, expected := range []string{"zext i64 4 to i128", "icmp uge i128 %index", "trunc i128 %index to i64", "getelementptr inbounds [4 x i32]"} { @@ -2986,7 +2974,6 @@ func TestGenerateLLVMIRBoundsChecksWideRuntimeFixedArrayIndexBeforeTruncation(t } func TestGenerateLLVMIRLowersIndexPlaceStoreForArrayWrite(t *testing.T) { - const targetTriple = "x86_64-unknown-linux-gnu" mod := indexStoreMIRModule(llvmTypes.fixed4I32, &mir.RefConst{Value: "0", Type: llvmTypes.i32}) irText := GenerateLLVMIR(mod, diagnostics.NewDiagnosticBag(), testLinuxAMD64, false) if !strings.Contains(irText, "getelementptr inbounds [4 x i32], [4 x i32]*") { @@ -2998,7 +2985,6 @@ func TestGenerateLLVMIRLowersIndexPlaceStoreForArrayWrite(t *testing.T) { } func TestGenerateLLVMIRLowersArrayLiteral(t *testing.T) { - const targetTriple = "x86_64-unknown-linux-gnu" mod := &mir.Module{ Name: "test", Types: llvmTypes.table, @@ -3040,7 +3026,6 @@ func TestGenerateLLVMIRLowersArrayLiteral(t *testing.T) { } func TestGenerateLLVMIRRejectsConstantArrayIndexOutOfBounds(t *testing.T) { - const targetTriple = "x86_64-unknown-linux-gnu" diag := diagnostics.NewDiagnosticBag() irText := GenerateLLVMIR(indexReadMIRModule(llvmTypes.fixed4I32, &mir.RefConst{Value: "4", Type: llvmTypes.i32}), diag, testLinuxAMD64, false) if irText != "" { @@ -3052,7 +3037,6 @@ func TestGenerateLLVMIRRejectsConstantArrayIndexOutOfBounds(t *testing.T) { } func TestGenerateLLVMIRRejectsInvalidConstantArrayIndexes(t *testing.T) { - const targetTriple = "x86_64-unknown-linux-gnu" for _, index := range []string{"-1", "bad"} { diag := diagnostics.NewDiagnosticBag() irText := GenerateLLVMIR(indexReadMIRModule(llvmTypes.fixed4I32, &mir.RefConst{Value: index, Type: llvmTypes.i32}), diag, testLinuxAMD64, false) @@ -3163,7 +3147,6 @@ func TestBoundsCheckedIndexHandlesUnsignedLengths(t *testing.T) { } func TestGenerateLLVMIRLowersDynamicArrayIndexRead(t *testing.T) { - const targetTriple = "x86_64-unknown-linux-gnu" irText := GenerateLLVMIR(indexReadMIRModule(llvmTypes.dynamicI32, &mir.RefName{Name: "i", Type: llvmTypes.i32}), diagnostics.NewDiagnosticBag(), testLinuxAMD64, false) if !strings.Contains(irText, "extractvalue { i32*, i64, i64, i8* } %xs, 0") { t.Fatalf("expected dynamic array index to extract data pointer, got:\n%s", irText) @@ -3193,7 +3176,6 @@ func TestGenerateLLVMIRLowersBorrowedDynamicArrayIndexRead(t *testing.T) { } func TestGenerateLLVMIRUsesWidenedUnsignedDynamicArrayIndexForGEP(t *testing.T) { - const targetTriple = "x86_64-unknown-linux-gnu" irText := GenerateLLVMIR(indexReadMIRModule(llvmTypes.dynamicI32, &mir.RefName{Name: "i", Type: llvmTypes.u8}), diagnostics.NewDiagnosticBag(), testLinuxAMD64, false) if !strings.Contains(irText, "zext i8 %i to i64") { t.Fatalf("expected narrow unsigned index widening, got:\n%s", irText) @@ -3211,7 +3193,6 @@ func TestGenerateLLVMIRUsesWidenedUnsignedDynamicArrayIndexForGEP(t *testing.T) } func TestGenerateLLVMIRLowersDynamicArrayIndexStore(t *testing.T) { - const targetTriple = "x86_64-unknown-linux-gnu" irText := GenerateLLVMIR(indexStoreMIRModule(llvmTypes.dynamicI32, &mir.RefConst{Value: "0", Type: llvmTypes.i32}), diagnostics.NewDiagnosticBag(), testLinuxAMD64, false) if !strings.Contains(irText, "extractvalue { i32*, i64, i64, i8* } %xs, 0") { t.Fatalf("expected dynamic array index to extract data pointer, got:\n%s", irText) @@ -3228,7 +3209,6 @@ func TestGenerateLLVMIRLowersDynamicArrayIndexStore(t *testing.T) { } func TestGenerateLLVMIRLowersSharedSliceViewIndexRead(t *testing.T) { - const targetTriple = "x86_64-unknown-linux-gnu" irText := GenerateLLVMIR(indexReadMIRModule(llvmTypes.refSliceI32, &mir.RefName{Name: "i", Type: llvmTypes.i32}), diagnostics.NewDiagnosticBag(), testLinuxAMD64, false) if !strings.Contains(irText, "extractvalue { i32*, i64 } %xs, 0") || !strings.Contains(irText, "extractvalue { i32*, i64 } %xs, 1") { @@ -3255,7 +3235,6 @@ func TestGenerateLLVMIRLowersSharedSliceViewIndexRead(t *testing.T) { } func TestGenerateLLVMIRLowersMutableSliceViewIndexStore(t *testing.T) { - const targetTriple = "x86_64-unknown-linux-gnu" irText := GenerateLLVMIR(indexStoreMIRModule(llvmTypes.mutRefSliceI32, &mir.RefConst{Value: "0", Type: llvmTypes.u8}), diagnostics.NewDiagnosticBag(), testLinuxAMD64, false) if !strings.Contains(irText, "extractvalue { i32*, i64 } %xs, 0") || !strings.Contains(irText, "extractvalue { i32*, i64 } %xs, 1") { diff --git a/internal/backend/llvm/instruction_emit.go b/internal/backend/llvm/instruction_emit.go index 1d0978d7..7e95670b 100644 --- a/internal/backend/llvm/instruction_emit.go +++ b/internal/backend/llvm/instruction_emit.go @@ -52,7 +52,7 @@ func emitPrint(b *llvmBuilder, printInstr *mir.Print) { value := emitRef(b, printInstr.Value) formatName := "" formatSize := 0 - arguments := make([]llvmValue, 0, 2) + var arguments []llvmValue i8 := llvmScalarLayout("i8") i8Pointer := llvmPointerLayout(i8) switch { @@ -376,10 +376,6 @@ func emitCast(b *llvmBuilder, cast *mir.Cast) llvmValue { return argRef } -func isMIRFloatType(typ string) bool { - return typ == "f32" || typ == "f64" -} - func newLLVMBuilder(out *strings.Builder, emitter *llvmEmitter, debugScopeID int) *llvmBuilder { debug := (*llvmDebugEmitter)(nil) if emitter != nil { diff --git a/internal/backend/llvm/type_layout.go b/internal/backend/llvm/type_layout.go index 8c205f15..82eed671 100644 --- a/internal/backend/llvm/type_layout.go +++ b/internal/backend/llvm/type_layout.go @@ -379,18 +379,6 @@ func isOwnedInterfaceType(types *ir.TypeTable, id ir.TypeID) bool { return ok && typ.Kind == ir.TypeOwnedPtr && isInterfaceType(types, typ.Elem) } -func interfaceTypeID(types *ir.TypeTable, id ir.TypeID) (ir.TypeID, bool) { - typ, ok := types.Type(id) - if !ok { - return ir.InvalidType, false - } - if typ.Kind == ir.TypeOwnedPtr || typ.Kind == ir.TypeReference { - id = typ.Elem - typ, ok = types.Type(id) - } - return id, ok && typ.Kind == ir.TypeInterface -} - func interfaceMethodVtableSlotID(types *ir.TypeTable, id ir.TypeID, methodSlot int) int { offset := interfaceReleaseVtableSlot if isOwnedInterfaceType(types, id) { diff --git a/internal/backend/llvm/typed_builder.go b/internal/backend/llvm/typed_builder.go index d04e1e69..6c176cf5 100644 --- a/internal/backend/llvm/typed_builder.go +++ b/internal/backend/llvm/typed_builder.go @@ -284,14 +284,6 @@ func (b *llvmBuilder) fieldPlace(base llvmPlace, index int) llvmPlace { return result } -func (b *llvmBuilder) namedFieldPlace(base llvmPlace, field llvmFieldName) llvmPlace { - index, ok := base.Pointee.Fields[field] - if !ok { - b.invariant("layout %s has no field %q", base.Pointee.Text, field) - } - return b.fieldPlace(base, index) -} - func (b *llvmBuilder) phi(layout *llvmLayout, incoming ...llvmIncoming) llvmValue { result := b.nextValue(layout) b.definePhi(result, incoming...) @@ -337,7 +329,7 @@ func (b *llvmBuilder) call(callee llvmValue, args []llvmValue) llvmValue { return result } -func (b *llvmBuilder) variadicCall(callee llvmValue, fixed, variadic []llvmValue) llvmValue { +func (b *llvmBuilder) variadicCall(callee llvmValue, fixed, variadic []llvmValue) { if callee.Layout == nil || callee.Layout.Kind != llvmLayoutFunction || callee.Layout.Return == nil || len(fixed) != len(callee.Layout.Parameters) { b.invariant("variadic call requires function and exact fixed arguments") } @@ -363,11 +355,9 @@ func (b *llvmBuilder) variadicCall(callee llvmValue, fixed, variadic []llvmValue callText := fmt.Sprintf("call %s (%s) %s(%s)", callee.Layout.Return.Text, signature, callee.Text, strings.Join(args, ", ")) if callee.Layout.Return.Kind == llvmLayoutVoid { b.line(callText) - return b.value("void", callee.Layout.Return) + return } - result := b.nextValue(callee.Layout.Return) - b.line(result.Text + " = " + callText) - return result + b.line(b.nextReg() + " = " + callText) } func (b *llvmBuilder) trap() { diff --git a/internal/diagnostics/emitter.go b/internal/diagnostics/emitter.go index b25989d8..1d11ed40 100644 --- a/internal/diagnostics/emitter.go +++ b/internal/diagnostics/emitter.go @@ -163,16 +163,12 @@ type Emitter struct { } type labelContext struct { - filepath string - line int - startLine int - endLine int - startCol int - endCol int - label Label - codeHint *CodeHint - lineNumWidth int - severity Severity + filepath string + line int + startCol int + endCol int + codeHint *CodeHint + severity Severity } func NewEmitter(w io.Writer) *Emitter { @@ -238,6 +234,9 @@ func (e *Emitter) printPipeOnly() { fmt.Fprintln(e.writer) } +// printUnderlineIndent pads to the column where source text starts, so an +// underline can be drawn beneath it. Retained at maintainer request for planned +// diagnostics work; do not remove it as dead code without asking. func (e *Emitter) printUnderlineIndent(extraPadding int) { indent := e.currentLineNumWidth + 6 // " N | " = width + 6 spaces/chars fmt.Fprint(e.writer, strings.Repeat(" ", indent+extraPadding)) diff --git a/internal/diagnostics/syntax_highlighter.go b/internal/diagnostics/syntax_highlighter.go index 8ad2b979..ae28b51d 100644 --- a/internal/diagnostics/syntax_highlighter.go +++ b/internal/diagnostics/syntax_highlighter.go @@ -70,6 +70,23 @@ type Token struct { Color colors.COLOR } +// scanQuoted returns the index just past the closing quote of a literal whose +// body starts at i, skipping escaped characters. An unterminated literal ends +// at the end of the line. +func scanQuoted(line string, i int, quote byte) int { + for i < len(line) && line[i] != quote { + if line[i] == '\\' && i+1 < len(line) { + i += 2 + } else { + i++ + } + } + if i < len(line) { + i++ + } + return i +} + // Highlight applies syntax highlighting to a line of code // Returns a slice of tokens with their associated colors func (sh *SyntaxHighlighter) Highlight(line string) []Token { @@ -94,17 +111,7 @@ func (sh *SyntaxHighlighter) Highlight(line string) []Token { // String literals (double quotes) if line[i] == '"' { start := i - i++ - for i < len(line) && line[i] != '"' { - if line[i] == '\\' && i+1 < len(line) { - i += 2 // Skip escaped character - } else { - i++ - } - } - if i < len(line) { - i++ // Include closing quote - } + i = scanQuoted(line, i+1, '"') tokensSlice = append(tokensSlice, Token{Text: line[start:i], Color: colorMap[STRING]}) continue } @@ -119,17 +126,7 @@ func (sh *SyntaxHighlighter) Highlight(line string) []Token { // Byte literals (b'X') if line[i] == 'b' && i+1 < len(line) && line[i+1] == '\'' { start := i - i += 2 - for i < len(line) && line[i] != '\'' { - if line[i] == '\\' && i+1 < len(line) { - i += 2 - } else { - i++ - } - } - if i < len(line) { - i++ - } + i = scanQuoted(line, i+2, '\'') tokensSlice = append(tokensSlice, Token{Text: line[start:i], Color: colorMap[CHAR]}) continue } @@ -137,17 +134,7 @@ func (sh *SyntaxHighlighter) Highlight(line string) []Token { // Character literals (single quotes) if line[i] == '\'' { start := i - i++ - for i < len(line) && line[i] != '\'' { - if line[i] == '\\' && i+1 < len(line) { - i += 2 // Skip escaped character - } else { - i++ - } - } - if i < len(line) { - i++ // Include closing quote - } + i = scanQuoted(line, i+1, '\'') tokensSlice = append(tokensSlice, Token{Text: line[start:i], Color: colorMap[CHAR]}) continue } diff --git a/internal/frontend/lexer/lexer.go b/internal/frontend/lexer/lexer.go index 125fcfae..48b28cd3 100644 --- a/internal/frontend/lexer/lexer.go +++ b/internal/frontend/lexer/lexer.go @@ -26,8 +26,8 @@ var regexPatterns = [...]regexPattern{ {regexp.MustCompile(`///[^\n\r]*`), docHandler}, {regexp.MustCompile(`//[^\n\r]*`), skipHandler}, {regexp.MustCompile(`(?s)/\*.*?\*/`), skipHandler}, - {regexp.MustCompile(`c"(?:\\.|[^"\\])*"`), cstringHandler}, - {regexp.MustCompile(`"(?:\\.|[^"\\])*"`), stringHandler}, + {regexp.MustCompile(`c"(?:\\.|[^"\\])*"`), stringHandler(1, token.CSTRING)}, + {regexp.MustCompile(`"(?:\\.|[^"\\])*"`), stringHandler(0, token.STRING)}, {regexp.MustCompile(`b'(?:\\.|[^'\\])*'`), byteCharHandler}, {regexp.MustCompile(`'(?:\\.|[^'\\])*'`), charHandler}, {regexp.MustCompile(numeric.NumberTokenPattern), numberHandler}, @@ -160,44 +160,28 @@ func numberHandler(l *Lexer, match string) { }) } -func stringHandler(l *Lexer, match string) { - start := l.pos - - l.advanceBy(match) - - inner := match[1 : len(match)-1] - value, err := unescapeQuoted(inner, '"') - if err != nil { - l.reportEscapeError(start, err) - return - } - - l.push(token.Token{ - Kind: token.STRING, - Literal: value, - Start: start, - End: l.pos, - }) -} +// stringHandler lexes a double-quoted literal whose opening quote follows +// prefixLen prefix bytes (0 for "...", 1 for c"..."). +func stringHandler(prefixLen int, kind token.Kind) func(*Lexer, string) { + return func(l *Lexer, match string) { + start := l.pos -func cstringHandler(l *Lexer, match string) { - start := l.pos + l.advanceBy(match) - l.advanceBy(match) + inner := match[prefixLen+1 : len(match)-1] + value, err := unescapeQuoted(inner, '"') + if err != nil { + l.reportEscapeError(start, err) + return + } - inner := match[2 : len(match)-1] - value, err := unescapeQuoted(inner, '"') - if err != nil { - l.reportEscapeError(start, err) - return + l.push(token.Token{ + Kind: kind, + Literal: value, + Start: start, + End: l.pos, + }) } - - l.push(token.Token{ - Kind: token.CSTRING, - Literal: value, - Start: start, - End: l.pos, - }) } func charHandler(l *Lexer, match string) { diff --git a/internal/frontend/parser/parser.go b/internal/frontend/parser/parser.go index 8588e14c..4841fd2b 100644 --- a/internal/frontend/parser/parser.go +++ b/internal/frontend/parser/parser.go @@ -26,15 +26,8 @@ type Parser struct { diag *diagnostics.DiagnosticBag pos int nodeID uint64 - context []string // parsing context stack for error messages } -const ( - ownerFunction = "function" - ownerIf = "if" - ownerElse = "else" -) - func New(filePath string, stream []token.Token, diag *diagnostics.DiagnosticBag) *Parser { return &Parser{ filePath: filePath, @@ -169,10 +162,6 @@ func (p *Parser) parseFnDecl() ast.Decl { receiver = p.parseReceiver() } name, typeParams, params, returnType, returnOrigins, ok := p.parseFnSignature() - if name != nil { - p.pushContext("function '" + name.Name + "'") - defer p.popContext() - } if !ok { // Return partial FnDecl with whatever was parsed decl := reg(p, &ast.FnDecl{ @@ -778,19 +767,6 @@ func reg[T ast.Node](p *Parser, n T) T { return n } -func (p *Parser) pushContext(ctx string) { p.context = append(p.context, ctx) } -func (p *Parser) popContext() { - if len(p.context) > 0 { - p.context = p.context[:len(p.context)-1] - } -} -func (p *Parser) currentContext() string { - if len(p.context) == 0 { - return "" - } - return p.context[len(p.context)-1] -} - func (p *Parser) lastNonNilToken(fallback token.Token) token.Token { if p.pos > 0 && p.pos-1 < len(p.stream) { return p.stream[p.pos-1] diff --git a/internal/ir/mir/expr_lower.go b/internal/ir/mir/expr_lower.go index bedc8181..f743c066 100644 --- a/internal/ir/mir/expr_lower.go +++ b/internal/ir/mir/expr_lower.go @@ -149,40 +149,28 @@ func (l *lowerer) lowerExpr(expr ir.Expr, out *[]Instr) ValueRef { } return &RefName{Name: name, Type: e.TypeID(), Location: e.Origin().Location} case *ir.ZeroValue: - name := l.nextTemp() - l.appendInstr(out, &Assign{Name: name, Value: &ZeroValue{Type: e.TypeID(), Location: e.Origin().Location}}) - return &RefName{Name: name, Type: e.TypeID(), Location: e.Origin().Location} + return l.storeInTemp(out, e, &ZeroValue{Type: e.TypeID(), Location: e.Origin().Location}) case *ir.VariantMake: payload := l.lowerExpr(e.Payload, out) - name := l.nextTemp() - l.appendInstr(out, &Assign{Name: name, Value: &VariantMake{Case: e.Case, Payload: payload, Type: e.TypeID(), Location: e.Origin().Location}}) - return &RefName{Name: name, Type: e.TypeID(), Location: e.Origin().Location} + return l.storeInTemp(out, e, &VariantMake{Case: e.Case, Payload: payload, Type: e.TypeID(), Location: e.Origin().Location}) case *ir.VariantIs: value := l.lowerExpr(e.Value, out) - name := l.nextTemp() - l.appendInstr(out, &Assign{Name: name, Value: &VariantIs{Value: value, Case: e.Case, Type: e.TypeID(), Location: e.Origin().Location}}) - return &RefName{Name: name, Type: e.TypeID(), Location: e.Origin().Location} + return l.storeInTemp(out, e, &VariantIs{Value: value, Case: e.Case, Type: e.TypeID(), Location: e.Origin().Location}) case *ir.Ident: ref := identifierRef(e) // Direct symbols retain linkage names; callable storage is captured too. if typ, ok := l.module.Types.Type(e.TypeID()); ok && typ.Kind == ir.TypeFunction && l.symbolValues[e.SymbolID] == nil { return ref } - name := l.nextTemp() // MIR Move captures a value; semantic consumption remains analysis-owned. - l.appendInstr(out, &Assign{Name: name, Value: asValueExpr(ref)}) - return &RefName{Name: name, Type: e.TypeID(), Location: e.Origin().Location} + return l.storeInTemp(out, e, asValueExpr(ref)) case *ir.Unary: arg := l.lowerExpr(e.Arg, out) - name := l.nextTemp() - l.appendInstr(out, &Assign{Name: name, Value: &Unary{Op: e.Op, Arg: arg, Type: e.TypeID(), Location: e.Origin().Location}}) - return &RefName{Name: name, Type: e.TypeID(), Location: e.Origin().Location} + return l.storeInTemp(out, e, &Unary{Op: e.Op, Arg: arg, Type: e.TypeID(), Location: e.Origin().Location}) case *ir.Binary: left := l.lowerExpr(e.Left, out) right := l.lowerExpr(e.Right, out) - name := l.nextTemp() - l.appendInstr(out, &Assign{Name: name, Value: &Binary{Op: e.Op, Left: left, Right: right, Type: e.TypeID(), Location: e.Origin().Location}}) - return &RefName{Name: name, Type: e.TypeID(), Location: e.Origin().Location} + return l.storeInTemp(out, e, &Binary{Op: e.Op, Left: left, Right: right, Type: e.TypeID(), Location: e.Origin().Location}) case *ir.StringConcat: left := l.lowerExpr(e.Left, out) right := l.lowerExpr(e.Right, out) @@ -198,9 +186,7 @@ func (l *lowerer) lowerExpr(expr ir.Expr, out *[]Instr) ValueRef { l.appendInstr(out, call) return nil } - name := l.nextTemp() - l.appendInstr(out, &Assign{Name: name, Value: call}) - return &RefName{Name: name, Type: e.TypeID(), Location: e.Origin().Location} + return l.storeInTemp(out, e, call) case *ir.Print: value := l.lowerExpr(e.Value, out) l.appendInstr(out, &Print{Value: value, AppendsNewline: e.AppendsNewline, Location: e.Origin().Location}) @@ -222,34 +208,26 @@ func (l *lowerer) lowerExpr(expr ir.Expr, out *[]Instr) ValueRef { return value case *ir.Len: value := l.lowerExpr(e.Value, out) - name := l.nextTemp() - l.appendInstr(out, &Assign{Name: name, Value: &Len{ + return l.storeInTemp(out, e, &Len{ Value: value, Type: e.TypeID(), Location: e.Origin().Location, - }}) - return &RefName{Name: name, Type: e.TypeID(), Location: e.Origin().Location} + }) case *ir.StringChars: value := l.lowerExpr(e.Value, out) - name := l.nextTemp() - l.appendInstr(out, &Assign{Name: name, Value: &StringChars{ + return l.storeInTemp(out, e, &StringChars{ Value: value, Type: e.TypeID(), Location: e.Origin().Location, - }}) - return &RefName{Name: name, Type: e.TypeID(), Location: e.Origin().Location} + }) case *ir.StringFromBytes: bytes := l.lowerExpr(e.Bytes, out) var allocator ValueRef if e.Allocator != nil { allocator = l.lowerExpr(e.Allocator, out) } - name := l.nextTemp() - l.appendInstr(out, &Assign{Name: name, Value: &StringFromBytes{ + return l.storeInTemp(out, e, &StringFromBytes{ Bytes: bytes, Allocator: allocator, Type: e.TypeID(), Location: e.Origin().Location, - }}) - return &RefName{Name: name, Type: e.TypeID(), Location: e.Origin().Location} + }) case *ir.AddrOf: place := l.lowerPlace(e.Place, out) - name := l.nextTemp() - l.appendInstr(out, &Assign{Name: name, Value: &AddrOf{Place: place, Type: e.TypeID(), Location: e.Origin().Location}}) - return &RefName{Name: name, Type: e.TypeID(), Location: e.Origin().Location} + return l.storeInTemp(out, e, &AddrOf{Place: place, Type: e.TypeID(), Location: e.Origin().Location}) case *ir.TempBorrow: value := l.lowerExpr(e.Value, out) if value == nil { @@ -279,16 +257,14 @@ func (l *lowerer) lowerExpr(expr ir.Expr, out *[]Instr) ValueRef { if e.End != nil { end = l.lowerExpr(e.End, out) } - name := l.nextTemp() - l.appendInstr(out, &Assign{Name: name, Value: &SliceView{ + return l.storeInTemp(out, e, &SliceView{ Source: source, Start: start, End: end, IsEndExclusive: e.IsEndExclusive, Type: e.TypeID(), Location: e.Origin().Location, - }}) - return &RefName{Name: name, Type: e.TypeID(), Location: e.Origin().Location} + }) case *ir.Field: base := l.lowerExpr(e.Base, out) name := l.nextTemp() @@ -306,9 +282,7 @@ func (l *lowerer) lowerExpr(expr ir.Expr, out *[]Instr) ValueRef { for _, field := range e.Fields { fields = append(fields, l.lowerExpr(field, out)) } - name := l.nextTemp() - l.appendInstr(out, &Assign{Name: name, Value: &StructLit{Fields: fields, Type: e.TypeID(), Location: e.Origin().Location}}) - return &RefName{Name: name, Type: e.TypeID(), Location: e.Origin().Location} + return l.storeInTemp(out, e, &StructLit{Fields: fields, Type: e.TypeID(), Location: e.Origin().Location}) case *ir.ArrayLit: if e.IsDynamic { name := l.nextTemp() @@ -340,9 +314,7 @@ func (l *lowerer) lowerExpr(expr ir.Expr, out *[]Instr) ValueRef { for _, value := range e.Values { values = append(values, l.lowerExpr(value, out)) } - name := l.nextTemp() - l.appendInstr(out, &Assign{Name: name, Value: &ArrayLit{Values: values, Type: e.TypeID(), Location: e.Origin().Location}}) - return &RefName{Name: name, Type: e.TypeID(), Location: e.Origin().Location} + return l.storeInTemp(out, e, &ArrayLit{Values: values, Type: e.TypeID(), Location: e.Origin().Location}) case *ir.DynamicArrayOp: array := l.lowerExpr(e.Array, out) var length, value ValueRef @@ -367,11 +339,9 @@ func (l *lowerer) lowerExpr(expr ir.Expr, out *[]Instr) ValueRef { if e.Allocator != nil { allocRef = l.lowerExpr(e.Allocator, out) } - name := l.nextTemp() - l.appendInstr(out, &Assign{Name: name, Value: &Alloc{ + return l.storeInTemp(out, e, &Alloc{ Value: value, Allocator: allocRef, Type: e.TypeID(), Location: e.Origin().Location, - }}) - return &RefName{Name: name, Type: e.TypeID(), Location: e.Origin().Location} + }) case *ir.InterfaceMake: value := l.lowerExpr(e.Value, out) dataType := interfaceDataType(l.module.Types, e.Value.TypeID()) @@ -384,34 +354,26 @@ func (l *lowerer) lowerExpr(expr ir.Expr, out *[]Instr) ValueRef { l.registerInterfaceThunk(slot, index) slots = append(slots, &RefName{Name: wrapperName, Type: slot.SlotType}) } - name := l.nextTemp() - l.appendInstr(out, &Assign{Name: name, Value: &InterfaceMake{ + return l.storeInTemp(out, e, &InterfaceMake{ Value: value, DataType: dataType, Slots: slots, Type: e.TypeID(), Location: e.Origin().Location, - }}) - return &RefName{Name: name, Type: e.TypeID(), Location: e.Origin().Location} + }) case *ir.InterfaceCall: call := l.lowerInterfaceCall(e, out) if l.isVoid(call.Type) { l.appendInstr(out, call) return nil } - name := l.nextTemp() - l.appendInstr(out, &Assign{Name: name, Value: call}) - return &RefName{Name: name, Type: e.TypeID(), Location: e.Origin().Location} + return l.storeInTemp(out, e, call) case *ir.Cast: arg := l.lowerExpr(e.Expr, out) if fields, ok := l.structCastFields(arg, e.TypeID(), e.Origin().Location, out); ok { - name := l.nextTemp() - l.appendInstr(out, &Assign{Name: name, Value: &StructLit{Fields: fields, Type: e.TypeID(), Location: e.Origin().Location}}) - return &RefName{Name: name, Type: e.TypeID(), Location: e.Origin().Location} + return l.storeInTemp(out, e, &StructLit{Fields: fields, Type: e.TypeID(), Location: e.Origin().Location}) } - name := l.nextTemp() - l.appendInstr(out, &Assign{Name: name, Value: &Cast{Arg: arg, Type: e.TypeID(), Location: e.Origin().Location}}) - return &RefName{Name: name, Type: e.TypeID(), Location: e.Origin().Location} + return l.storeInTemp(out, e, &Cast{Arg: arg, Type: e.TypeID(), Location: e.Origin().Location}) default: panic(fmt.Sprintf("MIR lowering: unhandled IR expression %T", expr)) } @@ -465,6 +427,15 @@ func (l *lowerer) registerInterfaceThunk(slot ir.InterfaceSlot, index int) { l.module.InterfaceThunks = append(l.module.InterfaceThunks, thunk) } +// storeInTemp binds value to a fresh temporary and returns a reference typed and +// located like e. Callers lower operands first so temporaries stay in +// evaluation order. +func (l *lowerer) storeInTemp(out *[]Instr, e ir.Expr, value ValueExpr) ValueRef { + name := l.nextTemp() + l.appendInstr(out, &Assign{Name: name, Value: value}) + return &RefName{Name: name, Type: e.TypeID(), Location: e.Origin().Location} +} + func (l *lowerer) nextTemp() string { l.tmp++ return fmt.Sprintf("t%d", l.tmp) diff --git a/internal/ir/nodes.go b/internal/ir/nodes.go index f5b65310..ab446875 100644 --- a/internal/ir/nodes.go +++ b/internal/ir/nodes.go @@ -499,12 +499,7 @@ func (e *BoolLit) TypeID() TypeID { } return e.Type } -func (e *ZeroValue) String() string { - if e == nil || e.Type == InvalidType { - return "zero" - } - return "zero" -} +func (*ZeroValue) String() string { return "zero" } func (e *ZeroValue) TypeID() TypeID { if e == nil { return InvalidType @@ -919,18 +914,6 @@ func (e *Cast) TypeID() TypeID { return e.Type } -func ArrayTypeParts(typeText string) (string, string, bool) { - typeText = strings.TrimSpace(typeText) - if !strings.HasPrefix(typeText, "[") { - return "", "", false - } - close := strings.IndexByte(typeText, ']') - if close <= 1 || close == len(typeText)-1 { - return "", "", false - } - return strings.TrimSpace(typeText[1:close]), strings.TrimSpace(typeText[close+1:]), true -} - func SignatureText(types *TypeTable, params []Param, returnType TypeID) string { if types == nil { panic("formatting IR signature without a type table") diff --git a/internal/ir/thir/model.go b/internal/ir/thir/model.go index e5e2fa57..826bb245 100644 --- a/internal/ir/thir/model.go +++ b/internal/ir/thir/model.go @@ -376,6 +376,16 @@ type Match struct { Arms []MatchArm } +// Arm returns the arm that handles caseIndex, or nil when no arm does. +func (s *Match) Arm(caseIndex int) *MatchArm { + for index := range s.Arms { + if s.Arms[index].Case == caseIndex { + return &s.Arms[index] + } + } + return nil +} + type MatchArm struct { Source ir.SourceInfo Case int diff --git a/internal/lsp/server.go b/internal/lsp/server.go index cf311d33..40affd7b 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -63,38 +63,7 @@ func Run(in io.ReadCloser, out io.Writer) error { switch req.Method { case "initialize": - var params InitializeParams - if err := json.Unmarshal(req.Params, ¶ms); err != nil { - respErr = invalidParams("Invalid params") - break - } - rootDir := state.RootDir - if params.RootURI != nil { - rootDir, err = uriToPath(string(*params.RootURI)) - if err != nil { - respErr = invalidParams(err.Error()) - break - } - } else if params.RootPath != nil { - rootDir = *params.RootPath - } - state.RootDir = rootDir - state.workspace = newWorkspaceIndex(state.RootDir) - result = InitializeResult{ - Capabilities: ServerCapabilities{ - TextDocumentSync: 1, // Full Sync - SupportsHover: true, - SupportsDefinition: true, - SupportsRename: true, - CompletionProvider: &CompletionOptions{ - TriggerCharacters: []string{".", "|", ">", ":", "/", "\""}, - }, - }, - ServerInfo: &ServerInfo{ - Name: "Peeper Language Server", - Version: LSP_VERSION, - }, - } + result, respErr = state.handleInitialize(req.Params) case "initialized": if err := publishWorkspaceDiagnostics(writer, state); err != nil { @@ -103,91 +72,32 @@ func Run(in io.ReadCloser, out io.Writer) error { continue case "textDocument/didOpen": - var params DidOpenTextDocumentParams - if err := json.Unmarshal(req.Params, ¶ms); err == nil { - filePath, uriErr := uriToPath(string(params.TextDocument.URI)) - if uriErr != nil { - continue - } - state.applyDocumentSnapshot(filePath, ¶ms.TextDocument.Text, ¶ms.TextDocument.Version) - if err := publishComponentDiagnostics(writer, state, filePath, nil); err != nil { - return err - } + if err := handleDidOpen(writer, state, req.Params); err != nil { + return err } continue case "textDocument/didChange": - var params DidChangeTextDocumentParams - if err := json.Unmarshal(req.Params, ¶ms); err == nil && len(params.ContentChanges) > 0 { - filePath, uriErr := uriToPath(string(params.TextDocument.URI)) - if uriErr != nil { - continue - } - // Under Full Sync, the first change has the entire file text - state.applyDocumentSnapshot(filePath, ¶ms.ContentChanges[0].Text, ¶ms.TextDocument.Version) - state.scheduleDiagnosticRefresh(filePath, diagnosticsDebounceDelay, func() error { - return publishComponentDiagnostics(writer, state, filePath, nil) - }) - } + handleDidChange(writer, state, req.Params) continue case "textDocument/didClose": - var params DidCloseTextDocumentParams - if err := json.Unmarshal(req.Params, ¶ms); err == nil { - filePath, uriErr := uriToPath(string(params.TextDocument.URI)) - if uriErr != nil { - continue - } - state.applyDocumentSnapshot(filePath, nil, nil) - if err := publishComponentDiagnostics(writer, state, filePath, nil); err != nil { - return err - } + if err := handleDidClose(writer, state, req.Params); err != nil { + return err } continue case "textDocument/hover": - var params HoverParams - if err := json.Unmarshal(req.Params, ¶ms); err == nil { - result, err = state.HandleHover(params) - if err != nil { - respErr = responseErrorFrom(err) - } - } else { - respErr = invalidParams("Invalid params") - } + result, respErr = handleRequest(req.Params, state.HandleHover) case "textDocument/definition": - var params DefinitionParams - if err := json.Unmarshal(req.Params, ¶ms); err == nil { - result, err = state.HandleDefinition(params) - if err != nil { - respErr = responseErrorFrom(err) - } - } else { - respErr = invalidParams("Invalid params") - } + result, respErr = handleRequest(req.Params, state.HandleDefinition) case "textDocument/completion": - var params CompletionParams - if err := json.Unmarshal(req.Params, ¶ms); err == nil { - result, err = state.HandleCompletion(params) - if err != nil { - respErr = responseErrorFrom(err) - } - } else { - respErr = invalidParams("Invalid params") - } + result, respErr = handleRequest(req.Params, state.HandleCompletion) case "textDocument/rename": - var params RenameParams - if err := json.Unmarshal(req.Params, ¶ms); err == nil { - result, err = state.HandleRename(params) - if err != nil { - respErr = responseErrorFrom(err) - } - } else { - respErr = invalidParams("Invalid params") - } + result, respErr = handleRequest(req.Params, state.HandleRename) case "shutdown": result = nil @@ -220,6 +130,96 @@ func Run(in io.ReadCloser, out io.Writer) error { } } +func (s *ServerState) handleInitialize(rawParams json.RawMessage) (any, *ResponseError) { + var params InitializeParams + if err := json.Unmarshal(rawParams, ¶ms); err != nil { + return nil, invalidParams("Invalid params") + } + rootDir := s.RootDir + if params.RootURI != nil { + var err error + rootDir, err = uriToPath(string(*params.RootURI)) + if err != nil { + return nil, invalidParams(err.Error()) + } + } else if params.RootPath != nil { + rootDir = *params.RootPath + } + s.RootDir = rootDir + s.workspace = newWorkspaceIndex(s.RootDir) + return InitializeResult{ + Capabilities: ServerCapabilities{ + TextDocumentSync: 1, // Full Sync + SupportsHover: true, + SupportsDefinition: true, + SupportsRename: true, + CompletionProvider: &CompletionOptions{ + TriggerCharacters: []string{".", "|", ">", ":", "/", "\""}, + }, + }, + ServerInfo: &ServerInfo{ + Name: "Peeper Language Server", + Version: LSP_VERSION, + }, + }, nil +} + +// handleRequest decodes a request's params, runs its handler, and maps the +// outcome to a JSON-RPC result or error. +func handleRequest[P, R any](rawParams json.RawMessage, handle func(P) (R, error)) (any, *ResponseError) { + var params P + if err := json.Unmarshal(rawParams, ¶ms); err != nil { + return nil, invalidParams("Invalid params") + } + result, err := handle(params) + if err != nil { + return result, responseErrorFrom(err) + } + return result, nil +} + +func handleDidOpen(writer *protocolWriter, state *ServerState, rawParams json.RawMessage) error { + var params DidOpenTextDocumentParams + if err := json.Unmarshal(rawParams, ¶ms); err != nil { + return nil + } + filePath, err := uriToPath(string(params.TextDocument.URI)) + if err != nil { + return nil + } + state.applyDocumentSnapshot(filePath, ¶ms.TextDocument.Text, ¶ms.TextDocument.Version) + return publishComponentDiagnostics(writer, state, filePath, nil) +} + +func handleDidChange(writer *protocolWriter, state *ServerState, rawParams json.RawMessage) { + var params DidChangeTextDocumentParams + if err := json.Unmarshal(rawParams, ¶ms); err != nil || len(params.ContentChanges) == 0 { + return + } + filePath, err := uriToPath(string(params.TextDocument.URI)) + if err != nil { + return + } + // Under Full Sync, the first change has the entire file text + state.applyDocumentSnapshot(filePath, ¶ms.ContentChanges[0].Text, ¶ms.TextDocument.Version) + state.scheduleDiagnosticRefresh(filePath, diagnosticsDebounceDelay, func() error { + return publishComponentDiagnostics(writer, state, filePath, nil) + }) +} + +func handleDidClose(writer *protocolWriter, state *ServerState, rawParams json.RawMessage) error { + var params DidCloseTextDocumentParams + if err := json.Unmarshal(rawParams, ¶ms); err != nil { + return nil + } + filePath, err := uriToPath(string(params.TextDocument.URI)) + if err != nil { + return nil + } + state.applyDocumentSnapshot(filePath, nil, nil) + return publishComponentDiagnostics(writer, state, filePath, nil) +} + func publishWorkspaceDiagnostics(writer *protocolWriter, state *ServerState) error { for _, snapshot := range state.workspaceDiagnosticSnapshots() { if err := publishDiagnosticSnapshot(writer, state, snapshot); err != nil { diff --git a/internal/lsp/state.go b/internal/lsp/state.go index 4885c53e..c50a3855 100644 --- a/internal/lsp/state.go +++ b/internal/lsp/state.go @@ -132,15 +132,6 @@ func (s *ServerState) workspaceDiagnosticSnapshots() []*diagnosticSnapshot { return snapshots } -func (s *ServerState) recompile(entryFile string) (*project.CompilerContext, *module.Module) { - if s == nil { - return nil, nil - } - s.mu.Lock() - defer s.mu.Unlock() - return s.recompileLocked(entryFile, nil) -} - func (s *ServerState) recompileLocked(entryFile string, parsedModules map[string]workspaceParse) (*project.CompilerContext, *module.Module) { canonicalEntry := project.CanonicalPath(entryFile) diagBag := diagnostics.NewDiagnosticBag() diff --git a/internal/lsp/state_test.go b/internal/lsp/state_test.go new file mode 100644 index 00000000..53d28dfe --- /dev/null +++ b/internal/lsp/state_test.go @@ -0,0 +1,17 @@ +package lsp + +import ( + "compiler/internal/module" + "compiler/internal/project" +) + +// recompile is the locked single-entry compile that tests and benchmarks drive. +// Production code calls recompileLocked while already holding the lock. +func (s *ServerState) recompile(entryFile string) (*project.CompilerContext, *module.Module) { + if s == nil { + return nil, nil + } + s.mu.Lock() + defer s.mu.Unlock() + return s.recompileLocked(entryFile, nil) +} diff --git a/internal/lsp/workspace.go b/internal/lsp/workspace.go index 7ff15d0f..fb3e64d9 100644 --- a/internal/lsp/workspace.go +++ b/internal/lsp/workspace.go @@ -75,12 +75,6 @@ func (w *workspaceIndex) rebuild(sourceOverrides map[string]string) (map[string] return nil, err } - type workspaceFileContext struct { - rootDir string - projectName string - importPath string - } - contexts := make(map[string]workspaceFileContext, len(files)) projectContexts := make(map[[2]string]*project.CompilerContext) contextForProject := func(rootDir, projectName string) *project.CompilerContext { key := [2]string{rootDir, projectName} @@ -95,51 +89,9 @@ func (w *workspaceIndex) rebuild(sourceOverrides map[string]string) (map[string] projectContexts[key] = ctx return ctx } - projectsByDir := make(map[string]*manifest.Project) - projectsByManifest := make(map[string]*manifest.Project) w.parsedFiles = 0 parsedModules := make(map[string]workspaceParse) - for _, filePath := range files { - fileDir := filepath.Dir(filePath) - rootDir := fileDir - projectName := "" - loadedProject, checked := projectsByDir[fileDir] - if !checked { - if manifestPath, err := manifest.FindManifestPath(filePath); err == nil { - manifestKey := project.CanonicalPath(manifestPath) - loadedProject, checked = projectsByManifest[manifestKey] - if !checked { - if configuredProject, loadErr := manifest.LoadProjectFromManifest(manifestPath); loadErr == nil { - loadedProject = configuredProject - } - projectsByManifest[manifestKey] = loadedProject - } - } - projectsByDir[fileDir] = loadedProject - } - if loadedProject != nil { - if !manifest.IsPathWithinSourceDir(loadedProject.RootDir, filePath) { - continue - } - rootDir = loadedProject.RootDir - projectName = loadedProject.File.Package.Name - } - importPath := "" - previous := w.modules[filePath] - if previous != nil && previous.rootDir == rootDir && previous.projectName == projectName { - importPath = previous.importPath - } else { - ctx := contextForProject(rootDir, projectName) - if resolved, resolveErr := ctx.ImportPathForFile(project.ModuleOriginLocal, "", filePath); resolveErr == nil { - importPath = resolved - } - } - contexts[filePath] = workspaceFileContext{ - rootDir: rootDir, - projectName: projectName, - importPath: importPath, - } - } + contexts := w.fileContexts(files, contextForProject) fileMembershipChanged := len(w.modules) != len(contexts) if !fileMembershipChanged { for filePath := range w.modules { @@ -225,22 +177,7 @@ func (w *workspaceIndex) rebuild(sourceOverrides map[string]string) (map[string] continue } ctx := contextForProject(fileCtx.rootDir, fileCtx.projectName) - seen := make(map[string]struct{}) - for _, rawPath := range module.sourceImportPaths { - resolved, resolveErr := ctx.ResolveImportPath(rawPath) - if resolveErr != nil || resolved == nil || resolved.ID.Origin != string(project.ModuleOriginLocal) { - continue - } - target := resolved.FilePath - if _, ok := contexts[target]; !ok { - continue - } - if _, dup := seen[target]; dup { - continue - } - seen[target] = struct{}{} - module.resolvedLocalImportFiles = append(module.resolvedLocalImportFiles, target) - } + resolveLocalImportFiles(ctx, module, contexts) } for filePath := range w.modules { @@ -254,6 +191,90 @@ func (w *workspaceIndex) rebuild(sourceOverrides map[string]string) (map[string] return parsedModules, nil } + w.rebuildImportGraph() + return parsedModules, nil +} + +// workspaceFileContext is the project a workspace file belongs to and the +// import path other files use to reach it. +type workspaceFileContext struct { + rootDir string + projectName string + importPath string +} + +// fileContexts decides, for every source file, which project owns it. Files +// outside their project's source directory are left out. +func (w *workspaceIndex) fileContexts(files []string, contextForProject func(rootDir, projectName string) *project.CompilerContext) map[string]workspaceFileContext { + contexts := make(map[string]workspaceFileContext, len(files)) + projectsByDir := make(map[string]*manifest.Project) + projectsByManifest := make(map[string]*manifest.Project) + for _, filePath := range files { + fileDir := filepath.Dir(filePath) + rootDir := fileDir + projectName := "" + loadedProject, checked := projectsByDir[fileDir] + if !checked { + if manifestPath, err := manifest.FindManifestPath(filePath); err == nil { + manifestKey := project.CanonicalPath(manifestPath) + loadedProject, checked = projectsByManifest[manifestKey] + if !checked { + if configuredProject, loadErr := manifest.LoadProjectFromManifest(manifestPath); loadErr == nil { + loadedProject = configuredProject + } + projectsByManifest[manifestKey] = loadedProject + } + } + projectsByDir[fileDir] = loadedProject + } + if loadedProject != nil { + if !manifest.IsPathWithinSourceDir(loadedProject.RootDir, filePath) { + continue + } + rootDir = loadedProject.RootDir + projectName = loadedProject.File.Package.Name + } + importPath := "" + previous := w.modules[filePath] + if previous != nil && previous.rootDir == rootDir && previous.projectName == projectName { + importPath = previous.importPath + } else { + ctx := contextForProject(rootDir, projectName) + if resolved, resolveErr := ctx.ImportPathForFile(project.ModuleOriginLocal, "", filePath); resolveErr == nil { + importPath = resolved + } + } + contexts[filePath] = workspaceFileContext{ + rootDir: rootDir, + projectName: projectName, + importPath: importPath, + } + } + return contexts +} + +// resolveLocalImportFiles records the workspace files a module imports, once +// each, in source order. +func resolveLocalImportFiles(ctx *project.CompilerContext, module *workspaceModule, contexts map[string]workspaceFileContext) { + seen := make(map[string]struct{}) + for _, rawPath := range module.sourceImportPaths { + resolved, resolveErr := ctx.ResolveImportPath(rawPath) + if resolveErr != nil || resolved == nil || resolved.ID.Origin != string(project.ModuleOriginLocal) { + continue + } + target := resolved.FilePath + if _, ok := contexts[target]; !ok { + continue + } + if _, dup := seen[target]; dup { + continue + } + seen[target] = struct{}{} + module.resolvedLocalImportFiles = append(module.resolvedLocalImportFiles, target) + } +} + +func (w *workspaceIndex) rebuildImportGraph() { g := graph.NewDependencyGraph(project.GraphEdgeImport) for _, module := range w.modules { for _, target := range module.resolvedLocalImportFiles { @@ -266,7 +287,6 @@ func (w *workspaceIndex) rebuild(sourceOverrides map[string]string) (map[string] w.components = buildWorkspaceComponents(w.modules, g) w.imports = g - return parsedModules, nil } func (w *workspaceIndex) syntheticEntry(filePath string) (string, string, bool) { diff --git a/internal/semantics/analysis/flow_thir.go b/internal/semantics/analysis/flow_thir.go index 90079867..a66dcad3 100644 --- a/internal/semantics/analysis/flow_thir.go +++ b/internal/semantics/analysis/flow_thir.go @@ -890,13 +890,7 @@ func (a *flowAnalyzer) applyVariantCaseEdge(site *cfg.Site, edge cfg.Edge, state dependencies: append([]*symbols.Symbol(nil), resolution.Dependencies...), }) } - var selected *thir.MatchArm - for index := range match.Arms { - if match.Arms[index].Case == edge.Case { - selected = &match.Arms[index] - break - } - } + selected := match.Arm(edge.Case) if selected == nil || selected.Payload == nil { return } diff --git a/internal/semantics/analysis/ownership.go b/internal/semantics/analysis/ownership.go index 80a6e63e..e01d49ea 100644 --- a/internal/semantics/analysis/ownership.go +++ b/internal/semantics/analysis/ownership.go @@ -536,13 +536,7 @@ func (a *analyzer) applyMatchEdge(node *site, edge cfg.Edge, st ownershipState) if match == nil { return } - var arm *thir.MatchArm - for index := range match.Arms { - if match.Arms[index].Case == edge.Case { - arm = &match.Arms[index] - break - } - } + arm := match.Arm(edge.Case) if arm == nil || arm.Payload == nil { return } diff --git a/internal/semantics/analysis/ownership_effects.go b/internal/semantics/analysis/ownership_effects.go index 33a64ab5..61eb8f74 100644 --- a/internal/semantics/analysis/ownership_effects.go +++ b/internal/semantics/analysis/ownership_effects.go @@ -64,7 +64,7 @@ func (v *ownershipEffectVisitor) visitWrite(op effectWrite) { } func (v *ownershipEffectVisitor) visitUse(op effectUse) { - v.a.applyUse(v.node, op, v.st, v.loans) + v.a.applyUse(op, v.st, v.loans) } func (v *ownershipEffectVisitor) visitBorrow(op effectBorrow) { @@ -249,7 +249,7 @@ func (a *analyzer) applyIterateEffect(op effectIterate, st ownershipState, loans }} } -func (a *analyzer) applyUse(node *site, op effectUse, st ownershipState, loans *loanContext) { +func (a *analyzer) applyUse(op effectUse, st ownershipState, loans *loanContext) { syntax := op.Source if op.Place.Root == nil { // A value with no owner. Only a projection out of one has an effect @@ -262,15 +262,15 @@ func (a *analyzer) applyUse(node *site, op effectUse, st ownershipState, loans * return } if len(op.Place.Projections) == 0 { - a.applyWholeUse(node, op, st, loans, syntax) + a.applyWholeUse(op, st, loans, syntax) return } - a.applyProjectedUse(op, st, loans, syntax) + a.applyProjectedUse(op, loans, syntax) } // applyWholeUse is the effect of using a binding entire: its move ownershipState changes, // and the storage it names is accessed. -func (a *analyzer) applyWholeUse(node *site, op effectUse, st ownershipState, loans *loanContext, syntax thir.Expr) { +func (a *analyzer) applyWholeUse(op effectUse, st ownershipState, loans *loanContext, syntax thir.Expr) { sym := op.Place.Root a.applyUseKind(sym, op, st, syntax) if _, isReference := referenceMutability(sym); isReference { @@ -288,7 +288,7 @@ func (a *analyzer) applyWholeUse(node *site, op effectUse, st ownershipState, lo // applyProjectedUse is the effect of using part of a binding. Consuming a part // is what a partial move is, and the language does not allow it out of a // move-only place. -func (a *analyzer) applyProjectedUse(op effectUse, st ownershipState, loans *loanContext, syntax thir.Expr) { +func (a *analyzer) applyProjectedUse(op effectUse, loans *loanContext, syntax thir.Expr) { if syntax == nil { return } diff --git a/internal/semantics/analysis/ownership_expr.go b/internal/semantics/analysis/ownership_expr.go index aea75608..2bd15ed9 100644 --- a/internal/semantics/analysis/ownership_expr.go +++ b/internal/semantics/analysis/ownership_expr.go @@ -133,7 +133,7 @@ func (a *analyzer) pointerOrigin(scope *symbols.Scope, expr thir.Expr, st owners } // localPointerRoot retains declaration-module locality for expanded defaults -// and stops at pointer projections, as place.LocalRoot does for source syntax. +// and stops at pointer projections. func (a *analyzer) localPointerRoot(scope *symbols.Scope, expr thir.Expr) *symbols.Symbol { if a == nil || a.input.Source == nil || scope == nil || expr == nil { return nil diff --git a/internal/semantics/binder/binder.go b/internal/semantics/binder/binder.go index 8e1ce884..a707baeb 100644 --- a/internal/semantics/binder/binder.go +++ b/internal/semantics/binder/binder.go @@ -95,14 +95,15 @@ func (b *binder) bindModuleBinding(name *ast.Ident, typ ast.TypeExpr) { return } if typ == nil { - if b.moduleScopeSymbol(name.Name) == nil { - return + if sym := b.moduleScopeSymbol(name.Name); sym != nil && sym.Type == nil { + sym.BindType(&typeinfo.UnknownType{}) } - b.bindModuleScopeTypeIfUnset(name.Name, &typeinfo.UnknownType{}) return } - b.bindModuleScopeType(name.Name, - b.ctx.TypeResolver.Resolve(b.ctx.Diagnostics, b.module, typ, typeresolution.Context{})) + resolved := b.ctx.TypeResolver.Resolve(b.ctx.Diagnostics, b.module, typ, typeresolution.Context{}) + if sym := b.moduleScopeSymbol(name.Name); sym != nil && resolved != nil { + sym.BindType(resolved) + } } // Bind named type declarations using one stable shell per symbol. @@ -153,15 +154,3 @@ func (b *binder) moduleScopeSymbol(name string) *symbols.Symbol { } return sym } - -func (b *binder) bindModuleScopeType(name string, typ typeinfo.Type) { - if sym := b.moduleScopeSymbol(name); sym != nil && typ != nil { - sym.BindType(typ) - } -} - -func (b *binder) bindModuleScopeTypeIfUnset(name string, typ typeinfo.Type) { - if sym := b.moduleScopeSymbol(name); sym != nil && typ != nil && sym.Type == nil { - sym.BindType(typ) - } -} diff --git a/internal/semantics/place/addressable.go b/internal/semantics/place/addressable.go index 7fcc461a..57fb6b45 100644 --- a/internal/semantics/place/addressable.go +++ b/internal/semantics/place/addressable.go @@ -8,23 +8,10 @@ import ( type ExprTypeFunc func(ast.Expr) typeinfo.Type -// Binding carries a symbol lookup result with transient context. -// Symbol is the underlying cross-module symbol pointer; Local is -// true only when the symbol was resolved in the current module's -// scope tree. A shared *symbols.Symbol cannot carry "local-to-this- -// module" because the same pointer appears in both the declaration -// and caller module's ExpandedDefaultBindings. -type Binding struct { - Symbol *symbols.Symbol - IsLocal bool -} - // BindingResolver supplies symbols for expressions that were resolved outside // the current scope tree (e.g. cloned defaults or qualified imports). When the -// resolver reports a match, scope lookup must be skipped entirely. Expanded -// defaults and qualified imports use Local=false to prevent LocalRoot from -// misclassifying non-local storage as a caller pointer-escape source. -type BindingResolver func(ast.Expr) (Binding, bool) +// resolver reports a match, scope lookup must be skipped entirely. +type BindingResolver func(ast.Expr) (*symbols.Symbol, bool) // Projection describes one syntactic projection from a base expression. // @@ -167,46 +154,6 @@ func MutableAddressable(scope *symbols.Scope, expr ast.Expr, exprType ExprTypeFu return isMutable, sharedReference, mutableBinding } -func LocalRoot(scope, moduleScope *symbols.Scope, expr ast.Expr, exprType ExprTypeFunc, resolve BindingResolver) (*symbols.Symbol, bool) { - if scope == nil || moduleScope == nil || expr == nil { - return nil, false - } - if e, ok := expr.(*ast.Ident); ok { - if e == nil { - return nil, false - } - if resolve != nil { - if binding, found := resolve(e); found { - // Expanded defaults have Local=false: the symbol - // lives in the declaration module, not the caller, - // so it is not a pointer-escape source. - if binding.IsLocal && addressableSymbol(binding.Symbol) { - return binding.Symbol, true - } - return nil, false - } - } - for current := scope; current != nil && current != moduleScope; current = current.Parent() { - sym, found := current.LookupLocal(e.Name) - if found { - return sym, addressableSymbol(sym) - } - } - return nil, false - } - projection, ok := Project(expr) - if !ok { - return nil, false - } - base := projection.Base - if exprType != nil { - if _, ok := typeinfo.PointerTarget(typeinfo.Underlying(exprType(base))); ok { - return nil, false - } - } - return LocalRoot(scope, moduleScope, base, exprType, resolve) -} - func addressableSymbol(sym *symbols.Symbol) bool { if sym == nil { return false diff --git a/internal/semantics/place/origin.go b/internal/semantics/place/origin.go index 4891d7e7..c208fcd6 100644 --- a/internal/semantics/place/origin.go +++ b/internal/semantics/place/origin.go @@ -3,7 +3,6 @@ package place import ( "compiler/internal/frontend/ast" "compiler/internal/semantics/symbols" - "compiler/internal/semantics/typeinfo" ) type OriginProjectionKind uint8 @@ -30,16 +29,6 @@ type Origin struct { Projections []OriginProjection } -type ResolveOptions struct { - ExprType ExprTypeFunc - ResolveBinding BindingResolver - ReferenceOrigins func([]Origin) []Origin - RawPointerOrigins func([]Origin) []Origin - CallOrigins func(*ast.CallExpr) []Origin - ConstantIndex func(ast.Expr) (string, bool) - PayloadCases func(ast.Expr) []int -} - // Resolution keeps carrier storage distinct from referenced value storage. // Stable is false when any projection cannot retain identity across CFG sites. type Resolution struct { @@ -49,119 +38,13 @@ type Resolution struct { IsStable bool } -// Resolve is the canonical place walk. Value origins preserve the previous -// eager safe-reference normalization; storage origins retain carrier identity. -func Resolve(scope *symbols.Scope, expr ast.Expr, opts ResolveOptions) Resolution { - if scope == nil || expr == nil { - return Resolution{} - } - switch node := expr.(type) { - case *ast.AddressExpr: - resolved := Resolve(scope, node.Expr, opts) - if opts.PayloadCases != nil { - if cases := opts.PayloadCases(node.Expr); len(cases) > 0 { - resolved.ValueOrigins = VariantPayloadOrigins(resolved.StorageOrigins, cases) - } - } - return resolved - case *ast.Ident: - sym, found := resolveSymbol(scope, node, opts.ResolveBinding) - if !found || sym == nil { - return Resolution{} - } - storage := []Origin{{Root: sym}} - if typ, ok := symbols.GetSymbolType(sym); ok { - return resolveStoredValueOrigins(typ, Resolution{ - StorageOrigins: storage, ValueOrigins: CloneOrigins(storage), IsStable: true, - }, opts) - } - return Resolution{StorageOrigins: storage, ValueOrigins: CloneOrigins(storage), IsStable: true} - case *ast.SelectorExpr: - if node.Name == nil { - return Resolution{} - } - base := Resolve(scope, node.Expr, opts) - origins := appendVariantPayloadProjections(base.ValueOrigins, node.Expr, opts.ExprType, opts.PayloadCases) - origins = appendIndirectProjection(origins, node.Expr, opts.ExprType) - origins = appendOriginProjection(origins, OriginProjection{Kind: OriginField, Field: node.Name.Name}) - resolved := Resolution{ - StorageOrigins: origins, - ValueOrigins: CloneOrigins(origins), - Dependencies: append([]*symbols.Symbol(nil), base.Dependencies...), - IsStable: base.IsStable && len(origins) > 0, - } - return resolveStoredExpressionOrigins(node, resolved, opts) - case *ast.IndexExpr: - base := Resolve(scope, node.Expr, opts) - origins := appendVariantPayloadProjections(base.ValueOrigins, node.Expr, opts.ExprType, opts.PayloadCases) - origins = appendIndirectProjection(origins, node.Expr, opts.ExprType) - dependencies := append([]*symbols.Symbol(nil), base.Dependencies...) - if _, rangeIndex := node.Index.(*ast.RangeExpr); rangeIndex { - origins = appendOriginProjection(origins, OriginProjection{Kind: OriginWildcard}) - return Resolution{StorageOrigins: origins, ValueOrigins: CloneOrigins(origins)} - } - if opts.ConstantIndex != nil { - if value, ok := opts.ConstantIndex(node.Index); ok { - origins = appendOriginProjection(origins, OriginProjection{Kind: OriginIndex, Index: value}) - resolved := Resolution{ - StorageOrigins: origins, - ValueOrigins: CloneOrigins(origins), - Dependencies: dependencies, - IsStable: base.IsStable && len(origins) > 0, - } - return resolveStoredExpressionOrigins(node, resolved, opts) - } - } - if index, ok := node.Index.(*ast.Ident); ok { - if sym, found := resolveSymbol(scope, index, opts.ResolveBinding); found && sym != nil { - if typ, typed := symbols.GetSymbolType(sym); typed && typeinfo.IsIntegral(typ) { - origins = appendOriginProjection(origins, OriginProjection{Kind: OriginBindingIndex, Binding: sym}) - dependencies = append(dependencies, sym) - resolved := Resolution{ - StorageOrigins: origins, - ValueOrigins: CloneOrigins(origins), - Dependencies: dependencies, - IsStable: base.IsStable && len(origins) > 0, - } - return resolveStoredExpressionOrigins(node, resolved, opts) - } - } - } - origins = appendOriginProjection(origins, OriginProjection{Kind: OriginWildcard}) - return Resolution{StorageOrigins: origins, ValueOrigins: CloneOrigins(origins), Dependencies: dependencies} - case *ast.CallExpr: - if opts.CallOrigins != nil { - return Resolution{ValueOrigins: CloneOrigins(opts.CallOrigins(node))} - } - return Resolution{} - default: - return Resolution{} - } -} - -func resolveStoredExpressionOrigins(expr ast.Expr, resolved Resolution, opts ResolveOptions) Resolution { - if opts.ExprType == nil { - return resolved - } - return resolveStoredValueOrigins(opts.ExprType(expr), resolved, opts) -} - -func resolveStoredValueOrigins(typ typeinfo.Type, resolved Resolution, opts ResolveOptions) Resolution { - if _, _, isReference := typeinfo.ReferenceValueTarget(typ); isReference && opts.ReferenceOrigins != nil { - resolved.ValueOrigins = CloneOrigins(opts.ReferenceOrigins(resolved.StorageOrigins)) - } else if _, isRaw := typeinfo.Underlying(typ).(*typeinfo.RawPtrType); isRaw && opts.RawPointerOrigins != nil { - resolved.ValueOrigins = CloneOrigins(opts.RawPointerOrigins(resolved.StorageOrigins)) - } - return resolved -} - func resolveSymbol(scope *symbols.Scope, expr ast.Expr, resolve BindingResolver) (*symbols.Symbol, bool) { if expr == nil { return nil, false } if resolve != nil { - if binding, found := resolve(expr); found { - return binding.Symbol, binding.Symbol != nil + if sym, found := resolve(expr); found { + return sym, sym != nil } } if ident, ok := expr.(*ast.Ident); ok && ident != nil { @@ -231,33 +114,6 @@ func OriginsOverlap(left, right []Origin) bool { return false } -func appendIndirectProjection(origins []Origin, base ast.Expr, exprType ExprTypeFunc) []Origin { - if exprType == nil { - return origins - } - if _, isOwned := typeinfo.PointerTarget(typeinfo.Underlying(exprType(base))); !isOwned { - return origins - } - return appendOriginProjection(origins, OriginProjection{Kind: OriginPointee}) -} - -func appendVariantPayloadProjections( - origins []Origin, - base ast.Expr, - exprType ExprTypeFunc, - payloadCases func(ast.Expr) []int, -) []Origin { - if payloadCases == nil { - return origins - } - if exprType != nil { - if _, _, isReference := typeinfo.ReferenceTarget(typeinfo.Underlying(exprType(base))); isReference { - return origins - } - } - return VariantPayloadOrigins(origins, payloadCases(base)) -} - // VariantPayloadOrigins projects carrier storage through exact proven cases. func VariantPayloadOrigins(origins []Origin, cases []int) []Origin { out := CloneOrigins(origins) diff --git a/internal/semantics/place/origin_test.go b/internal/semantics/place/origin_test.go index 740c3e87..56153802 100644 --- a/internal/semantics/place/origin_test.go +++ b/internal/semantics/place/origin_test.go @@ -4,7 +4,6 @@ import ( "testing" "compiler/internal/frontend/ast" - "compiler/internal/ir" "compiler/internal/semantics/symbols" "compiler/internal/semantics/typeinfo" ) @@ -56,15 +55,15 @@ func TestPlaceAddressabilityUsesResolvedBindingBeforeScope(t *testing.T) { } resolvedValue := symbols.New(symbols.ProjectedSymbolID(symbols.SymbolConst, "value"), "value", symbols.SymbolConst, nil, nil) projection := &ast.SelectorExpr{Expr: &ast.Ident{Name: "value"}, Name: &ast.Ident{Name: "field"}} - resolve := func(ast.Expr) (Binding, bool) { - return Binding{Symbol: resolvedValue}, true + resolve := func(ast.Expr) (*symbols.Symbol, bool) { + return resolvedValue, true } if !IsAddressable(scope, projection, nil, resolve) { t.Fatal("Addressable() rejected resolved binding") } - resolve = func(ast.Expr) (Binding, bool) { - return Binding{Symbol: symbols.New(symbols.ProjectedSymbolID(symbols.SymbolFunc, "value"), "value", symbols.SymbolFunc, nil, nil)}, true + resolve = func(ast.Expr) (*symbols.Symbol, bool) { + return symbols.New(symbols.ProjectedSymbolID(symbols.SymbolFunc, "value"), "value", symbols.SymbolFunc, nil, nil), true } if IsAddressable(scope, projection, nil, resolve) { t.Fatal("Addressable() fell back to shadowed scope binding") @@ -124,288 +123,6 @@ func TestPlaceAddressabilityPointerAndReferenceBoundaries(t *testing.T) { } } -func TestPlaceLocalRootPreservesBindingLocalAndPointerCutoff(t *testing.T) { - moduleScope := symbols.NewScope(nil) - scope := symbols.NewScope(moduleScope) - local := symbols.New(symbols.ProjectedSymbolID(symbols.SymbolVar, "value"), "value", symbols.SymbolVar, &ast.LetDecl{IsMutable: true}, nil) - if err := scope.Declare(local); err != nil { - t.Fatal(err) - } - base := &ast.Ident{Name: "value"} - projection := &ast.IndexExpr{Expr: base, Index: &ast.NumberLit{Value: "0"}} - root, ok := LocalRoot(scope, moduleScope, projection, nil, nil) - if !ok || root != local { - t.Fatalf("LocalRoot() = (%v, %v), want local root", root, ok) - } - - pointerType := &typeinfo.OwnedPtrType{Target: typeinfo.DefaultIntegerType()} - exprType := func(expr ast.Expr) typeinfo.Type { - if expr == base { - return pointerType - } - return nil - } - if root, ok := LocalRoot(scope, moduleScope, projection, exprType, nil); ok || root != nil { - t.Fatalf("LocalRoot() crossed owned pointer cutoff: (%v, %v)", root, ok) - } - - resolved := symbols.New(symbols.ProjectedSymbolID(symbols.SymbolConst, "value"), "value", symbols.SymbolConst, nil, nil) - for _, localBinding := range []bool{false, true} { - root, ok := LocalRoot(scope, moduleScope, base, nil, func(ast.Expr) (Binding, bool) { - return Binding{Symbol: resolved, IsLocal: localBinding}, true - }) - if localBinding { - if !ok || root != resolved { - t.Fatalf("LocalRoot() ignored local resolved binding: (%v, %v)", root, ok) - } - } else if ok || root != nil { - t.Fatalf("LocalRoot() treated non-local resolved binding as root: (%v, %v)", root, ok) - } - } -} - -func TestResolvePreferResolvedBindingOverShadowingScope(t *testing.T) { - scope := symbols.NewScope(nil) - callerValue := symbols.New(symbols.ProjectedSymbolID(symbols.SymbolVar, "value"), "value", symbols.SymbolVar, nil, nil) - declarationValue := symbols.New(symbols.ProjectedSymbolID(symbols.SymbolConst, "value"), "value", symbols.SymbolConst, nil, nil) - if err := scope.Declare(callerValue); err != nil { - t.Fatal(err) - } - - ident := &ast.Ident{Name: "value"} - resolved := Resolve(scope, ident, ResolveOptions{ - ResolveBinding: func(ast.Expr) (Binding, bool) { - return Binding{Symbol: declarationValue}, true - }, - }) - want := []Origin{{Root: declarationValue}} - if !AreSameOrigins(resolved.StorageOrigins, want) || !AreSameOrigins(resolved.ValueOrigins, want) || !resolved.IsStable { - t.Fatalf("resolution = %#v, want stable declaration binding", resolved) - } -} - -func TestResolveSeparatesReferenceStorageAndValueProjections(t *testing.T) { - scope := symbols.NewScope(nil) - value := symbols.New(symbols.ProjectedSymbolID(symbols.SymbolVar, "value"), "value", symbols.SymbolVar, nil, nil) - value.BindType(&typeinfo.StructType{Fields: []typeinfo.Field{{Name: "items", Type: &typeinfo.ArrayType{Len: "2", Elem: typeinfo.DefaultIntegerType()}}}}) - reference := symbols.New(symbols.ProjectedSymbolID(symbols.SymbolVar, "reference"), "reference", symbols.SymbolVar, nil, nil) - reference.BindType(&typeinfo.RefType{Target: value.Type}) - if err := scope.Declare(value); err != nil { - t.Fatal(err) - } - if err := scope.Declare(reference); err != nil { - t.Fatal(err) - } - - base := &ast.Ident{Name: "reference"} - field := &ast.SelectorExpr{Expr: base, Name: &ast.Ident{Name: "items"}} - index := &ast.IndexExpr{Expr: field, Index: &ast.NumberLit{Value: "1"}} - types := map[ast.Expr]typeinfo.Type{ - base: reference.Type, - field: value.Type.(*typeinfo.StructType).Fields[0].Type, - } - resolved := Resolve(scope, index, ResolveOptions{ - ExprType: func(expr ast.Expr) typeinfo.Type { return types[expr] }, - ReferenceOrigins: func(storage []Origin) []Origin { - if AreSameOrigins(storage, []Origin{{Root: reference}}) { - return []Origin{{Root: value}} - } - return nil - }, - ConstantIndex: func(ast.Expr) (string, bool) { return "1", true }, - }) - want := []Origin{{Root: value, Projections: []OriginProjection{ - {Kind: OriginField, Field: "items"}, - {Kind: OriginIndex, Index: "1"}, - }}} - if !AreSameOrigins(resolved.ValueOrigins, want) || !resolved.IsStable { - t.Fatalf("resolution = %#v, want stable value origins %#v", resolved, want) - } - if !AreSameOrigins(Resolve(scope, base, ResolveOptions{ - ReferenceOrigins: func([]Origin) []Origin { return []Origin{{Root: value}} }, - }).StorageOrigins, []Origin{{Root: reference}}) { - t.Fatal("reference carrier storage did not retain binding identity") - } -} - -func TestResolveReferenceOriginsByProjectedStoragePlace(t *testing.T) { - scope := symbols.NewScope(nil) - value := symbols.New(symbols.ProjectedSymbolID(symbols.SymbolVar, "value"), "value", symbols.SymbolVar, nil, nil) - holder := symbols.New(symbols.ProjectedSymbolID(symbols.SymbolVar, "holder"), "holder", symbols.SymbolVar, nil, nil) - holder.BindType(&typeinfo.StructType{Fields: []typeinfo.Field{{ - Name: "ref", Type: &typeinfo.RefType{Target: typeinfo.DefaultIntegerType()}, - }}}) - if err := scope.Declare(holder); err != nil { - t.Fatal(err) - } - base := &ast.Ident{Name: "holder"} - field := &ast.SelectorExpr{Expr: base, Name: &ast.Ident{Name: "ref"}} - types := map[ast.Expr]typeinfo.Type{ - base: holder.Type, - field: &typeinfo.RefType{Target: typeinfo.DefaultIntegerType()}, - } - wantStorage := []Origin{{Root: holder, Projections: []OriginProjection{{Kind: OriginField, Field: "ref"}}}} - resolved := Resolve(scope, field, ResolveOptions{ - ExprType: func(expr ast.Expr) typeinfo.Type { return types[expr] }, - ReferenceOrigins: func(storage []Origin) []Origin { - if AreSameOrigins(storage, wantStorage) { - return []Origin{{Root: value}} - } - return nil - }, - }) - if !AreSameOrigins(resolved.StorageOrigins, wantStorage) || - !AreSameOrigins(resolved.ValueOrigins, []Origin{{Root: value}}) || !resolved.IsStable { - t.Fatalf("projected reference resolution = %#v", resolved) - } -} - -func TestResolveAddressProjectsProvenOptionalPayloadValue(t *testing.T) { - scope := symbols.NewScope(nil) - carrier := symbols.New(symbols.ProjectedSymbolID(symbols.SymbolVar, "value"), "value", symbols.SymbolVar, nil, nil) - carrier.BindType(&typeinfo.OptionalType{Inner: typeinfo.DefaultIntegerType()}) - if err := scope.Declare(carrier); err != nil { - t.Fatal(err) - } - - ident := &ast.Ident{Name: "value"} - resolved := Resolve(scope, &ast.AddressExpr{Expr: ident}, ResolveOptions{ - PayloadCases: func(expr ast.Expr) []int { - if expr == ident { - return []int{ir.OptionalPresentCase} - } - return nil - }, - }) - storage := []Origin{{Root: carrier}} - payload := VariantPayloadOrigins(storage, []int{ir.OptionalPresentCase}) - if !AreSameOrigins(resolved.StorageOrigins, storage) || !AreSameOrigins(resolved.ValueOrigins, payload) { - t.Fatalf("address resolution = %#v, want carrier storage and payload value %#v", resolved, payload) - } -} - -func TestResolveOrdersOptionalPayloadBeforePointeeAndSkipsNormalizedReferences(t *testing.T) { - scope := symbols.NewScope(nil) - valueType := &typeinfo.StructType{Fields: []typeinfo.Field{{Name: "value", Type: typeinfo.DefaultIntegerType()}}} - owner := symbols.New(symbols.ProjectedSymbolID(symbols.SymbolVar, "owner"), "owner", symbols.SymbolVar, nil, nil) - owner.BindType(&typeinfo.OptionalType{Inner: &typeinfo.OwnedPtrType{Target: valueType}}) - referent := symbols.New(symbols.ProjectedSymbolID(symbols.SymbolVar, "referent"), "referent", symbols.SymbolVar, nil, nil) - referent.BindType(valueType) - reference := symbols.New(symbols.ProjectedSymbolID(symbols.SymbolVar, "reference"), "reference", symbols.SymbolVar, nil, nil) - reference.BindType(&typeinfo.OptionalType{Inner: &typeinfo.RefType{IsMutable: true, Target: valueType}}) - for _, sym := range []*symbols.Symbol{owner, referent, reference} { - if err := scope.Declare(sym); err != nil { - t.Fatal(err) - } - } - - ownerBase := &ast.Ident{Name: "owner"} - ownerField := &ast.SelectorExpr{Expr: ownerBase, Name: &ast.Ident{Name: "value"}} - ownerResolution := Resolve(scope, ownerField, ResolveOptions{ - ExprType: func(ast.Expr) typeinfo.Type { return &typeinfo.OwnedPtrType{Target: valueType} }, - PayloadCases: func(expr ast.Expr) []int { - if expr == ownerBase { - return []int{ir.OptionalPresentCase} - } - return nil - }, - }) - wantOwner := []Origin{{Root: owner, Projections: []OriginProjection{ - {Kind: OriginVariantPayload, Case: ir.OptionalPresentCase}, - {Kind: OriginPointee}, - {Kind: OriginField, Field: "value"}, - }}} - if !AreSameOrigins(ownerResolution.ValueOrigins, wantOwner) { - t.Fatalf("owned optional resolution = %#v, want %#v", ownerResolution, wantOwner) - } - - referenceBase := &ast.Ident{Name: "reference"} - referenceField := &ast.SelectorExpr{Expr: referenceBase, Name: &ast.Ident{Name: "value"}} - referenceResolution := Resolve(scope, referenceField, ResolveOptions{ - ExprType: func(expr ast.Expr) typeinfo.Type { - if expr == referenceBase { - return &typeinfo.RefType{IsMutable: true, Target: valueType} - } - return typeinfo.DefaultIntegerType() - }, - ReferenceOrigins: func(storage []Origin) []Origin { - if !AreSameOrigins(storage, []Origin{{Root: reference}}) { - return nil - } - return []Origin{{Root: referent}} - }, - PayloadCases: func(expr ast.Expr) []int { - if expr == referenceBase { - return []int{ir.OptionalPresentCase} - } - return nil - }, - }) - wantReference := []Origin{{Root: referent, Projections: []OriginProjection{{Kind: OriginField, Field: "value"}}}} - if !AreSameOrigins(referenceResolution.ValueOrigins, wantReference) { - t.Fatalf("optional reference resolution = %#v, want %#v", referenceResolution, wantReference) - } -} - -func TestResolvePreserveOwningPointeeAndCollapseUnknownDescendants(t *testing.T) { - scope := symbols.NewScope(nil) - owner := symbols.New(symbols.ProjectedSymbolID(symbols.SymbolVar, "owner"), "owner", symbols.SymbolVar, nil, nil) - inner := &typeinfo.ArrayType{Len: "2", Elem: typeinfo.DefaultIntegerType()} - owner.BindType(&typeinfo.OwnedPtrType{Target: &typeinfo.ArrayType{Len: "2", Elem: inner}}) - if err := scope.Declare(owner); err != nil { - t.Fatal(err) - } - - base := &ast.Ident{Name: "owner"} - unknown := &ast.Ident{Name: "index"} - first := &ast.IndexExpr{Expr: base, Index: unknown} - second := &ast.IndexExpr{Expr: first, Index: &ast.NumberLit{Value: "0"}} - types := map[ast.Expr]typeinfo.Type{ - base: owner.Type, - first: inner, - } - resolved := Resolve(scope, second, ResolveOptions{ - ExprType: func(expr ast.Expr) typeinfo.Type { return types[expr] }, - ConstantIndex: func(expr ast.Expr) (string, bool) { - literal, ok := expr.(*ast.NumberLit) - if !ok { - return "", false - } - return literal.Value, true - }, - }) - want := []Origin{{Root: owner, Projections: []OriginProjection{ - {Kind: OriginPointee}, - {Kind: OriginWildcard}, - }}} - if !AreSameOrigins(resolved.ValueOrigins, want) || resolved.IsStable { - t.Fatalf("resolution = %#v, want unstable origins %#v", resolved, want) - } -} - -func TestResolveUsesBindingIndexIdentityAfterConstantEvaluation(t *testing.T) { - scope := symbols.NewScope(nil) - values := symbols.New(symbols.ProjectedSymbolID(symbols.SymbolParam, "values"), "values", symbols.SymbolParam, nil, nil) - values.BindType(&typeinfo.ArrayType{Len: "2", Elem: typeinfo.DefaultIntegerType()}) - index := symbols.New(symbols.ProjectedSymbolID(symbols.SymbolParam, "index"), "index", symbols.SymbolParam, nil, nil) - index.BindType(typeinfo.DefaultIntegerType()) - if err := scope.Declare(values); err != nil { - t.Fatal(err) - } - if err := scope.Declare(index); err != nil { - t.Fatal(err) - } - expr := &ast.IndexExpr{Expr: &ast.Ident{Name: "values"}, Index: &ast.Ident{Name: "index"}} - resolved := Resolve(scope, expr, ResolveOptions{ - ConstantIndex: func(ast.Expr) (string, bool) { return "", false }, - }) - want := []Origin{{Root: values, Projections: []OriginProjection{{Kind: OriginBindingIndex, Binding: index}}}} - if !resolved.IsStable || !AreSameOrigins(resolved.StorageOrigins, want) || - len(resolved.Dependencies) != 1 || resolved.Dependencies[0] != index { - t.Fatalf("resolution = %#v, want binding-dependent stable index", resolved) - } -} - func TestMergeOriginsUnionsWithoutAliasingInputPaths(t *testing.T) { leftRoot := symbols.New(symbols.ProjectedSymbolID(symbols.SymbolVar, "left"), "left", symbols.SymbolVar, nil, nil) rightRoot := symbols.New(symbols.ProjectedSymbolID(symbols.SymbolVar, "right"), "right", symbols.SymbolVar, nil, nil) diff --git a/internal/semantics/resolver/resolver.go b/internal/semantics/resolver/resolver.go index c5ad937a..e2d206a0 100644 --- a/internal/semantics/resolver/resolver.go +++ b/internal/semantics/resolver/resolver.go @@ -268,7 +268,7 @@ func (r *resolver) resolveExpr(scope *symbols.Scope, expr ast.Expr) { } return } - reportUnresolved(r.module, scope, node, r.ctx.Diagnostics) + reportUnresolved(scope, node, r.ctx.Diagnostics) case *ast.ScopeResolution: if r.resolveVariantPath(scope, node) { return @@ -350,7 +350,7 @@ func (r *resolver) resolveAssignTarget(scope *symbols.Scope, expr ast.Expr) { r.module.SymbolIndex.MarkUsed(sym) return } - reportUnresolved(r.module, scope, node, r.ctx.Diagnostics) + reportUnresolved(scope, node, r.ctx.Diagnostics) case *ast.SelectorExpr: r.resolveExpr(scope, node.Expr) default: diff --git a/internal/semantics/resolver/suggest.go b/internal/semantics/resolver/suggest.go index 00d73cad..15d5e9fb 100644 --- a/internal/semantics/resolver/suggest.go +++ b/internal/semantics/resolver/suggest.go @@ -3,27 +3,22 @@ package resolver import ( "compiler/internal/diagnostics" "compiler/internal/frontend/ast" - "compiler/internal/module" "compiler/internal/semantics/symbols" "compiler/pkg/colors" ) -func reportUnresolved(module *module.Module, scope *symbols.Scope, node *ast.Ident, diag *diagnostics.DiagnosticBag) bool { - if module == nil || node == nil || diag == nil { - return false +func reportUnresolved(scope *symbols.Scope, node *ast.Ident, diag *diagnostics.DiagnosticBag) { + if node == nil || diag == nil { + return } + msg := "unknown identifier `" + node.Name + "`" + d := diagnostics.NewError(msg). + WithCode(diagnostics.ErrUndefinedSymbol). + WithPrimaryLabel(ast.LocOf(node), msg) if match, ok := nearestSymbolName(node.Name, scope); ok { - msg := "unknown identifier `" + node.Name + "`" - d := diagnostics.NewError(msg). - WithCode(diagnostics.ErrUndefinedSymbol). - WithPrimaryLabel(ast.LocOf(node), msg). - WithText("help", "did you mean `"+match+"`?", colors.GREEN) - diag.Add(d) - return false + d.WithText("help", "did you mean `"+match+"`?", colors.GREEN) } - msg := "unknown identifier `" + node.Name + "`" - diag.Add(diagnostics.NewError(msg).WithCode(diagnostics.ErrUndefinedSymbol).WithPrimaryLabel(ast.LocOf(node), msg)) - return false + diag.Add(d) } func nearestSymbolName(name string, scope *symbols.Scope) (string, bool) { diff --git a/internal/semantics/typechecker/check_expr.go b/internal/semantics/typechecker/check_expr.go index e567092a..bdd44dc6 100644 --- a/internal/semantics/typechecker/check_expr.go +++ b/internal/semantics/typechecker/check_expr.go @@ -39,11 +39,11 @@ func (c *checker) typePayloadExpr(scope *symbols.Scope, expr ast.Expr, expected return c.typeExpr(scope, expr, expected) } -func (c *checker) typeWholeCarrierExpr(scope *symbols.Scope, expr ast.Expr, expected typeinfo.Type) typeinfo.Type { +func (c *checker) typeWholeCarrierExpr(scope *symbols.Scope, expr ast.Expr) typeinfo.Type { previous := c.wholeCarrierExpr c.wholeCarrierExpr = expr defer func() { c.wholeCarrierExpr = previous }() - return c.typeExpr(scope, expr, expected) + return c.typeExpr(scope, expr, nil) } func (c *checker) effectiveExpressionType(expr ast.Expr, base, expected typeinfo.Type) typeinfo.Type { @@ -232,7 +232,7 @@ func (c *checker) typeAddressExpr(scope *symbols.Scope, node *ast.AddressExpr, e } var valueType typeinfo.Type if node.Mode == ast.AddressRaw { - valueType = c.typeWholeCarrierExpr(scope, node.Expr, nil) + valueType = c.typeWholeCarrierExpr(scope, node.Expr) } else { var valueExpected typeinfo.Type if target, _, isReference := typeinfo.ReferenceValueTarget(typeinfo.Underlying(expected)); isReference { @@ -478,7 +478,7 @@ func (c *checker) typeIsExpr(scope *symbols.Scope, node *ast.IsExpr) typeinfo.Ty if node == nil || node.Value == nil || node.Case == nil { return &typeinfo.InvalidType{} } - valueType := c.requireValueType(node.Value, c.typeWholeCarrierExpr(scope, node.Value, nil), "case-test subject") + valueType := c.requireValueType(node.Value, c.typeWholeCarrierExpr(scope, node.Value), "case-test subject") if typeinfo.IsInvalidOrUnknown(valueType) { return &typeinfo.InvalidType{} } diff --git a/internal/semantics/typechecker/check_fn.go b/internal/semantics/typechecker/check_fn.go index 0cbc168a..a80b80c9 100644 --- a/internal/semantics/typechecker/check_fn.go +++ b/internal/semantics/typechecker/check_fn.go @@ -276,7 +276,7 @@ func (c *checker) checkFunctionTypeContracts() { context := typeresolution.Context{} allowTypeParameters := false if typeDecl, ok := decl.(ast.TypeDecl); ok && len(typeDecl.DeclarationTypeParams()) > 0 { - context = c.typeContextForDecl(typeDecl, false) + context = c.typeContextForDecl(typeDecl) allowTypeParameters = true } ast.Inspect(decl, func(node ast.Node) bool { @@ -296,7 +296,7 @@ func (c *checker) checkTypeDeclReferenceStorage(decl ast.TypeDecl) { if decl == nil { return } - context := c.typeContextForDecl(decl, false) + context := c.typeContextForDecl(decl) switch node := decl.(type) { case *ast.StructDecl: strct, ok := node.Type.(*ast.StructType) @@ -325,7 +325,7 @@ func (c *checker) checkInterfaceDecl(decl *ast.InterfaceDecl) { c.ctx.Diagnostics.AddError(diagnostics.ErrInvalidTypeInParser, "interface declaration missing interface payload", ast.LocOf(decl), "") return } - resolvedIface, _ := c.ctx.TypeResolver.Resolve(c.ctx.Diagnostics, c.module, iface, c.typeContextForDecl(decl, false)).(*typeinfo.InterfaceType) + resolvedIface, _ := c.ctx.TypeResolver.Resolve(c.ctx.Diagnostics, c.module, iface, c.typeContextForDecl(decl)).(*typeinfo.InterfaceType) allowTypeParameters := len(decl.DeclarationTypeParams()) > 0 for methodIndex, method := range iface.Methods { if method.Name == nil || method.Name.Name == "" { @@ -342,7 +342,7 @@ func (c *checker) checkInterfaceDecl(decl *ast.InterfaceDecl) { c.ctx.Diagnostics.Add(invalidTypeError(method.Receiver.Type, "iface method receiver must be Self, &Self, or &mut Self")) } - context := c.typeContextForDecl(decl, false) + context := c.typeContextForDecl(decl) for _, param := range method.Params { paramType := c.ctx.TypeResolver.Resolve(c.ctx.Diagnostics, c.module, param.Type, context) if c.rejectUnsizedType(paramType, param.Type, "interface method parameter") { @@ -381,7 +381,7 @@ func (c *checker) checkEnumDecl(decl *ast.EnumDecl) { c.ctx.Diagnostics.Add(invalidTypeError(decl, "enum requires at least one variant")) return } - context := c.typeContextForDecl(decl, false) + context := c.typeContextForDecl(decl) allowTypeParameters := len(decl.DeclarationTypeParams()) > 0 dataFields := make(map[string]*source.Location) for _, variant := range enumType.Variants { @@ -466,8 +466,8 @@ func (c *checker) checkEnumPayloadType(syntax ast.TypeExpr, typeContext typereso } } -func (c *checker) typeContextForDecl(decl ast.TypeDecl, allowAbstractSelf bool) typeresolution.Context { - context := typeresolution.Context{AllowAbstractSelf: allowAbstractSelf} +func (c *checker) typeContextForDecl(decl ast.TypeDecl) typeresolution.Context { + var context typeresolution.Context if iface, ok := decl.(*ast.InterfaceDecl); ok { context.NamedInterfaceRoot = iface.UnderlyingType() } diff --git a/internal/semantics/typechecker/check_stmt.go b/internal/semantics/typechecker/check_stmt.go index 629514ed..92413493 100644 --- a/internal/semantics/typechecker/check_stmt.go +++ b/internal/semantics/typechecker/check_stmt.go @@ -124,7 +124,7 @@ func (c *checker) checkMatchStmt(scope *symbols.Scope, node *ast.MatchStmt, retu if c == nil || scope == nil || node == nil || node.Subject == nil { return } - subjectType := c.requireValueType(node.Subject, c.typeWholeCarrierExpr(scope, node.Subject, nil), "match subject") + subjectType := c.requireValueType(node.Subject, c.typeWholeCarrierExpr(scope, node.Subject), "match subject") if typeinfo.IsInvalidOrUnknown(subjectType) { return } @@ -267,7 +267,7 @@ func (c *checker) checkAssign(scope *symbols.Scope, node *ast.AssignStmt) { if c == nil || scope == nil || node == nil || node.Target == nil || node.Value == nil { return } - targetType := c.typeWholeCarrierExpr(scope, node.Target, nil) + targetType := c.typeWholeCarrierExpr(scope, node.Target) if targetType == nil || typeinfo.IsInvalid(targetType) { return } @@ -524,62 +524,7 @@ func (c *checker) checkForInStmt(scope *symbols.Scope, node *ast.ForStmt, return var carrierType typeinfo.Type rangeExpr, isRange := node.Iterable.(*ast.RangeExpr) if isRange { - if !rangeExpr.IsEndExclusive { - valid = false - c.ctx.Diagnostics.Add(invalidExpressionError(rangeExpr, "for range requires an exclusive end; use `..` instead of `..=`")) - } - _, badStart := rangeExpr.Start.(*ast.BadExpr) - if rangeExpr.Start == nil || badStart { - valid = false - c.ctx.Diagnostics.Add(invalidExpressionError(rangeExpr, "range iteration requires a start bound")) - } - _, badEnd := rangeExpr.End.(*ast.BadExpr) - if rangeExpr.End == nil || badEnd { - valid = false - c.ctx.Diagnostics.Add(invalidExpressionError(rangeExpr, "range iteration requires an end bound")) - } - - defaultType := typeinfo.DefaultIntegerType() - startNumber, startLiteral := rangeExpr.Start.(*ast.NumberLit) - endNumber, endLiteral := rangeExpr.End.(*ast.NumberLit) - startUntyped := startLiteral && startNumber.ExplicitType == "" - endUntyped := endLiteral && endNumber.ExplicitType == "" - var startType, endType typeinfo.Type - if startUntyped && !endUntyped { - endType = c.checkRangeBound(scope, rangeExpr.End, defaultType) - startType = c.checkRangeBound(scope, rangeExpr.Start, endType) - } else { - startType = c.checkRangeBound(scope, rangeExpr.Start, defaultType) - endExpected := defaultType - if endUntyped && !typeinfo.IsInvalidOrUnknown(startType) { - endExpected = startType - } - endType = c.checkRangeBound(scope, rangeExpr.End, endExpected) - } - if typeinfo.IsInvalidOrUnknown(startType) || typeinfo.IsInvalidOrUnknown(endType) || - !typeinfo.IsIntegral(startType) || !typeinfo.IsIntegral(endType) { - valid = false - } else { - elemType = typeinfo.CommonNumericType(startType, endType) - if elemType == nil || !typeinfo.IsIntegral(elemType) { - valid = false - c.ctx.Diagnostics.Add(typeMismatchError(rangeExpr, - "range bounds of type "+typeinfo.TypeText(startType)+" and "+typeinfo.TypeText(endType)+" have no common integer type")) - } else { - if !c.isAssignable(elemType, startType, rangeExpr.Start) || !c.isAssignable(elemType, endType, rangeExpr.End) { - valid = false - } - } - } - if valid { - startValue, startFound := c.evaluateConstant(c.ctx, rangeExpr.Start, elemType) - endValue, endFound := c.evaluateConstant(c.ctx, rangeExpr.End, elemType) - start, startIntegral := startValue.(*constvalue.IntConst) - end, endIntegral := endValue.(*constvalue.IntConst) - if startFound && endFound && startIntegral && endIntegral && start.Int().Cmp(end.Int()) < 0 { - evidence.HasGuaranteedEntry = true - } - } + elemType, valid, evidence.HasGuaranteedEntry = c.checkRangeIteration(scope, rangeExpr, valid) evidence.ElementType = elemType } else { var ok bool @@ -620,31 +565,8 @@ func (c *checker) checkForInStmt(scope *symbols.Scope, node *ast.ForStmt, return } evidence.ElementType = elem } else { - if valid { - call, isCallExpression := node.Iterable.(*ast.CallExpr) - optional, isOptionalResultType := typeinfo.Underlying(iterableType).(*typeinfo.OptionalType) - switch { - case !isCallExpression: - c.ctx.Diagnostics.Add(invalidExpressionError(node.Iterable, "cannot iterate over "+typeinfo.TypeText(iterableType)). - WithHelp("use a range, array, slice, or an explicit call returning an optional item, such as `for item in producer()`"). - WithNote("iterator calls, including arguments, are evaluated on every attempt; a bare optional value is not a producer")) - case !isOptionalResultType || optional.Inner == nil: - c.ctx.Diagnostics.Add(invalidExpressionError(node.Iterable, "iterator call must return an optional item"). - WithHelp("return an optional type, such as `?i32`: return an item to continue, or `none` to end the loop")) - case node.Index != nil: - c.ctx.Diagnostics.Add(invalidExpressionError(node.Index, "iterator loops provide an item, not an index"). - WithHelp("use `for item in producer()`; if you need an index, maintain a separate counter")) - default: - c.expandCallIteration(scope, node) - } - if checked := c.evidence.CheckedIteration(node.ID()); checked != nil { - c.bindLoopVariable(node.Value, optional.Inner) - previous := c.reusedCall - c.reusedCall = call - c.checkStmt(scope, checked, returnType) - c.reusedCall = previous - return - } + if valid && c.checkCallIteration(scope, node, iterableType, returnType) { + return } valid = false } @@ -683,6 +605,100 @@ func (c *checker) checkForInStmt(scope *symbols.Scope, node *ast.ForStmt, return c.loopDepth-- } +// checkRangeIteration types the bounds of `for x in a..b`. It returns the +// element type, whether the loop is still valid, and whether constant bounds +// prove the body runs at least once. +func (c *checker) checkRangeIteration(scope *symbols.Scope, rangeExpr *ast.RangeExpr, valid bool) (elemType typeinfo.Type, stillValid, hasGuaranteedEntry bool) { + if !rangeExpr.IsEndExclusive { + valid = false + c.ctx.Diagnostics.Add(invalidExpressionError(rangeExpr, "for range requires an exclusive end; use `..` instead of `..=`")) + } + _, badStart := rangeExpr.Start.(*ast.BadExpr) + if rangeExpr.Start == nil || badStart { + valid = false + c.ctx.Diagnostics.Add(invalidExpressionError(rangeExpr, "range iteration requires a start bound")) + } + _, badEnd := rangeExpr.End.(*ast.BadExpr) + if rangeExpr.End == nil || badEnd { + valid = false + c.ctx.Diagnostics.Add(invalidExpressionError(rangeExpr, "range iteration requires an end bound")) + } + + defaultType := typeinfo.DefaultIntegerType() + startNumber, startLiteral := rangeExpr.Start.(*ast.NumberLit) + endNumber, endLiteral := rangeExpr.End.(*ast.NumberLit) + startUntyped := startLiteral && startNumber.ExplicitType == "" + endUntyped := endLiteral && endNumber.ExplicitType == "" + var startType, endType typeinfo.Type + if startUntyped && !endUntyped { + endType = c.checkRangeBound(scope, rangeExpr.End, defaultType) + startType = c.checkRangeBound(scope, rangeExpr.Start, endType) + } else { + startType = c.checkRangeBound(scope, rangeExpr.Start, defaultType) + endExpected := defaultType + if endUntyped && !typeinfo.IsInvalidOrUnknown(startType) { + endExpected = startType + } + endType = c.checkRangeBound(scope, rangeExpr.End, endExpected) + } + if typeinfo.IsInvalidOrUnknown(startType) || typeinfo.IsInvalidOrUnknown(endType) || + !typeinfo.IsIntegral(startType) || !typeinfo.IsIntegral(endType) { + valid = false + } else { + elemType = typeinfo.CommonNumericType(startType, endType) + if elemType == nil || !typeinfo.IsIntegral(elemType) { + valid = false + c.ctx.Diagnostics.Add(typeMismatchError(rangeExpr, + "range bounds of type "+typeinfo.TypeText(startType)+" and "+typeinfo.TypeText(endType)+" have no common integer type")) + } else { + if !c.isAssignable(elemType, startType, rangeExpr.Start) || !c.isAssignable(elemType, endType, rangeExpr.End) { + valid = false + } + } + } + if valid { + startValue, startFound := c.evaluateConstant(c.ctx, rangeExpr.Start, elemType) + endValue, endFound := c.evaluateConstant(c.ctx, rangeExpr.End, elemType) + start, startIntegral := startValue.(*constvalue.IntConst) + end, endIntegral := endValue.(*constvalue.IntConst) + if startFound && endFound && startIntegral && endIntegral && start.Int().Cmp(end.Int()) < 0 { + hasGuaranteedEntry = true + } + } + return elemType, valid, hasGuaranteedEntry +} + +// checkCallIteration handles `for item in producer()`. It reports true when it +// expanded the loop and checked the expansion, so the caller has nothing left +// to do; false means the iterable is not a usable producer call. +func (c *checker) checkCallIteration(scope *symbols.Scope, node *ast.ForStmt, iterableType, returnType typeinfo.Type) bool { + call, isCallExpression := node.Iterable.(*ast.CallExpr) + optional, isOptionalResultType := typeinfo.Underlying(iterableType).(*typeinfo.OptionalType) + switch { + case !isCallExpression: + c.ctx.Diagnostics.Add(invalidExpressionError(node.Iterable, "cannot iterate over "+typeinfo.TypeText(iterableType)). + WithHelp("use a range, array, slice, or an explicit call returning an optional item, such as `for item in producer()`"). + WithNote("iterator calls, including arguments, are evaluated on every attempt; a bare optional value is not a producer")) + case !isOptionalResultType || optional.Inner == nil: + c.ctx.Diagnostics.Add(invalidExpressionError(node.Iterable, "iterator call must return an optional item"). + WithHelp("return an optional type, such as `?i32`: return an item to continue, or `none` to end the loop")) + case node.Index != nil: + c.ctx.Diagnostics.Add(invalidExpressionError(node.Index, "iterator loops provide an item, not an index"). + WithHelp("use `for item in producer()`; if you need an index, maintain a separate counter")) + default: + c.expandCallIteration(scope, node) + } + if checked := c.evidence.CheckedIteration(node.ID()); checked != nil { + c.bindLoopVariable(node.Value, optional.Inner) + previous := c.reusedCall + c.reusedCall = call + c.checkStmt(scope, checked, returnType) + c.reusedCall = previous + return true + } + return false +} + // expandCallIteration publishes ordinary checked statements before flow and // ownership. Keeping the entire source call inside the loop repeats its receiver // and arguments on every attempt, including the terminating attempt. diff --git a/internal/semantics/typechecker/errors.go b/internal/semantics/typechecker/errors.go index 70528f1c..cf0fa6fb 100644 --- a/internal/semantics/typechecker/errors.go +++ b/internal/semantics/typechecker/errors.go @@ -5,7 +5,6 @@ import ( "compiler/internal/diagnostics" "compiler/internal/frontend/ast" - "compiler/internal/source" ) func invalidExpressionError(node ast.Node, message string) *diagnostics.Diagnostic { @@ -47,20 +46,6 @@ func typeMismatchError(node ast.Node, message string) *diagnostics.Diagnostic { WithCode(diagnostics.ErrTypeMismatch) } -func optionalPayloadProofError(location *source.Location) *diagnostics.Diagnostic { - return diagnostics.NewError("optional payload use requires a presence proof"). - WithPrimaryLabel(location, "payload is not proven present here"). - WithCode(diagnostics.ErrOptionalPayloadProof). - WithHelp("guard this stable place with `value != none` or return after `value == none`") -} - -func unstableOptionalNarrowingError(location *source.Location) *diagnostics.Diagnostic { - return diagnostics.NewError("optional narrowing subject is not a stable place"). - WithPrimaryLabel(location, "this expression can change between the test and use"). - WithCode(diagnostics.ErrUnstableNarrowing). - WithHelp("bind the expression or index to a direct local before testing it") -} - func notCallableError(node ast.Node, message string) *diagnostics.Diagnostic { return diagnostics.NewError(message). WithPrimaryLabel(ast.LocOf(node), ""). diff --git a/internal/semantics/typechecker/evidence.go b/internal/semantics/typechecker/evidence.go index f3ea4a35..be0f9e3f 100644 --- a/internal/semantics/typechecker/evidence.go +++ b/internal/semantics/typechecker/evidence.go @@ -3,7 +3,6 @@ package typechecker import ( "compiler/internal/frontend/ast" "compiler/internal/semantics/intrinsics" - "compiler/internal/semantics/place" "compiler/internal/semantics/symbols" "compiler/internal/semantics/typeinfo" "compiler/internal/source" @@ -60,15 +59,6 @@ type MatchBinding struct { IsDiscard bool } -func (m Match) Arm(caseIndex int) (MatchArm, bool) { - for _, arm := range m.Arms { - if arm.Case == caseIndex { - return arm, true - } - } - return MatchArm{}, false -} - // ForIteration records typechecker-owned loop lowering and CFG evidence. // Generated symbols carry hidden loop state; source bindings remain body-scoped. // @@ -517,19 +507,19 @@ func (r *evidence) CloneReusableExpressionEvidenceFrom(dstID source.NodeID, src } } -func (c *checker) expandedDefaultBinding(expr ast.Expr) (place.Binding, bool) { +func (c *checker) expandedDefaultBinding(expr ast.Expr) (*symbols.Symbol, bool) { if c == nil || c.module == nil || c.module.SymbolIndex == nil || expr == nil { - return place.Binding{}, false + return nil, false } if path, ok := expr.(*ast.ScopeResolution); ok { if sym := c.module.SymbolIndex.Symbol(path); sym != nil { - return place.Binding{Symbol: sym}, true + return sym, true } - return place.Binding{}, false + return nil, false } ident, ok := expr.(*ast.Ident) if !ok || c.evidence == nil || !c.evidence.ExpandedDefaultBinding(ident.ID()) { - return place.Binding{}, false + return nil, false } - return place.Binding{Symbol: c.module.SymbolIndex.Symbol(ident)}, true + return c.module.SymbolIndex.Symbol(ident), true } diff --git a/internal/semantics/typeinfo/types.go b/internal/semantics/typeinfo/types.go index e6153cec..317ff32d 100644 --- a/internal/semantics/typeinfo/types.go +++ b/internal/semantics/typeinfo/types.go @@ -165,16 +165,12 @@ type Method struct { // CallableType returns the abstract interface signature used for source-facing // display and declaration checks. Receiver remains `Self` in slot zero. func (m Method) CallableType() *FuncType { - return m.callableType(&NamedType{Name: "Self"}) + return m.CallableTypeFor(&NamedType{Name: "Self"}) } // CallableTypeFor materializes the method signature for one concrete receiver // owner while preserving receiver slot zero and return-origin indexes. func (m Method) CallableTypeFor(owner Type) *FuncType { - return m.callableType(owner) -} - -func (m Method) callableType(owner Type) *FuncType { params := make([]Type, len(m.Params)+1) paramNames := make([]string, len(m.Params)+1) params[0] = m.Receiver.typeFor(owner) diff --git a/pkg/distribution/pack.go b/pkg/distribution/pack.go index c438b72f..f4421d58 100644 --- a/pkg/distribution/pack.go +++ b/pkg/distribution/pack.go @@ -304,12 +304,13 @@ func writeZipPack(output io.Writer, entries []packEntry, manifestJSON []byte) er zipWriter := zip.NewWriter(output) for _, entry := range entries { header := &zip.FileHeader{Name: entry.record.Path, Method: zip.Deflate} - header.SetModTime(time.Date(1980, 1, 1, 0, 0, 0, 0, time.UTC)) + header.Modified = time.Date(1980, 1, 1, 0, 0, 0, 0, time.UTC) mode := os.FileMode(entry.record.Mode) - if entry.record.Type == FileTypeDirectory { + switch entry.record.Type { + case FileTypeDirectory: header.Name += "/" mode |= os.ModeDir - } else if entry.record.Type == FileTypeSymlink { + case FileTypeSymlink: mode |= os.ModeSymlink } header.SetMode(mode) @@ -317,18 +318,19 @@ func writeZipPack(output io.Writer, entries []packEntry, manifestJSON []byte) er if err != nil { return fmt.Errorf("write zip header %q: %w", entry.record.Path, err) } - if entry.record.Type == FileTypeRegular { + switch entry.record.Type { + case FileTypeRegular: if err := copyPackFile(writer, entry); err != nil { return err } - } else if entry.record.Type == FileTypeSymlink { + case FileTypeSymlink: if _, err := io.WriteString(writer, entry.record.LinkTarget); err != nil { return fmt.Errorf("write zip symlink %q: %w", entry.record.Path, err) } } } manifestHeader := &zip.FileHeader{Name: ManifestName, Method: zip.Deflate} - manifestHeader.SetModTime(time.Date(1980, 1, 1, 0, 0, 0, 0, time.UTC)) + manifestHeader.Modified = time.Date(1980, 1, 1, 0, 0, 0, 0, time.UTC) manifestHeader.SetMode(0o644) manifestWriter, err := zipWriter.CreateHeader(manifestHeader) if err != nil { diff --git a/pkg/manifest/manifest.go b/pkg/manifest/manifest.go index 6c3a3bda..6e0ef8c8 100644 --- a/pkg/manifest/manifest.go +++ b/pkg/manifest/manifest.go @@ -196,7 +196,7 @@ func Load(path string) (*File, error) { if !ok { return nil, fmt.Errorf("missing top-level package config") } - name, ok, err := toml.LookupKey[string](pkg, "name") + name, _, err := toml.LookupKey[string](pkg, "name") if err != nil { return nil, err } @@ -226,7 +226,7 @@ func Load(path string) (*File, error) { } manifest.Package.CompilerVersion = compilerVersion } - build, ok, err := toml.LookupKey[string](pkg, "build") + build, _, err := toml.LookupKey[string](pkg, "build") if err != nil { return nil, fmt.Errorf("build: %w", err) } @@ -423,15 +423,6 @@ func renderDevSection(builder *strings.Builder, dev *DevConfig) { } } -func RemoveDependency(path, alias string) error { - file, err := Load(path) - if err != nil { - return err - } - delete(file.Dependencies, alias) - return Save(path, file) -} - func renderDependency(dep Dependency) string { switch dep.Type { case DependencyNeighbor: diff --git a/pkg/registry/download.go b/pkg/registry/download.go index 07429baa..6f7a8a3d 100644 --- a/pkg/registry/download.go +++ b/pkg/registry/download.go @@ -494,7 +494,7 @@ func extractTarGz(archivePath, destPath string) error { if err := os.MkdirAll(target, 0o755); err != nil { return err } - case tar.TypeReg, tar.TypeRegA: + case tar.TypeReg: if header.Size < 0 || header.Size > maxPackageExtractedBytes-extracted { return fmt.Errorf("package archive exceeds %d extracted byte limit", maxPackageExtractedBytes) } diff --git a/pkg/toml/data.go b/pkg/toml/data.go index 340213b7..dba97517 100644 --- a/pkg/toml/data.go +++ b/pkg/toml/data.go @@ -20,6 +20,8 @@ func NewData() Data { } } +// HasSection reports whether a section exists. Retained at maintainer request +// as part of the TOML API; do not remove it as dead code without asking. func (d Data) HasSection(name string) bool { _, ok := d.Sections[name] return ok @@ -30,6 +32,9 @@ func (d Data) Section(name string) (Table, bool) { return section, ok } +// EnsureSection returns a section, creating it when missing. Retained at +// maintainer request as part of the TOML API; do not remove it as dead code +// without asking. func (d *Data) EnsureSection(name string) (Table, error) { if section, ok := d.Sections[name]; ok { return section, nil diff --git a/pkg/toml/parser.go b/pkg/toml/parser.go index 28b6fef7..c7c8e32d 100644 --- a/pkg/toml/parser.go +++ b/pkg/toml/parser.go @@ -36,6 +36,8 @@ func ParseString(src string) (Data, error) { return Parse(strings.NewReader(src)) } +// ParseBytes parses TOML held in memory. Retained at maintainer request as part +// of the TOML API; do not remove it as dead code without asking. func ParseBytes(src []byte) (Data, error) { return Parse(bytes.NewReader(src)) } @@ -82,15 +84,15 @@ func ensureSection(data *Data, section string) error { } func parseKeyValue(data *Data, currentSection string, lineNo int, line string) error { - parts := strings.SplitN(line, "=", 2) - if len(parts) != 2 { + key, valueText, ok := strings.Cut(line, "=") + if !ok { return &ParseError{Line: lineNo, Message: fmt.Sprintf("invalid key/value pair: %s", line)} } - key := strings.TrimSpace(parts[0]) + key = strings.TrimSpace(key) if key == "" { return &ParseError{Line: lineNo, Message: "empty key"} } - valueText := stripInlineComment(strings.TrimSpace(parts[1])) + valueText = stripInlineComment(strings.TrimSpace(valueText)) section := currentSection if section == "" { section = "default" @@ -178,18 +180,18 @@ func parseInlineTable(text string) (Table, error) { return nil, err } for _, part := range parts { - kv := strings.SplitN(part, "=", 2) - if len(kv) != 2 { + key, valueText, ok := strings.Cut(part, "=") + if !ok { return nil, fmt.Errorf("invalid inline table entry %q", part) } - key := strings.TrimSpace(kv[0]) + key = strings.TrimSpace(key) if key == "" { return nil, fmt.Errorf("empty inline table key") } if _, exists := table[key]; exists { return nil, fmt.Errorf("duplicate inline table key %q", key) } - value, err := parseValue(strings.TrimSpace(kv[1])) + value, err := parseValue(strings.TrimSpace(valueText)) if err != nil { return nil, err } diff --git a/scripts/toolchain_sources_test.go b/scripts/toolchain_sources_test.go index 05f00cfb..0ecd31b8 100644 --- a/scripts/toolchain_sources_test.go +++ b/scripts/toolchain_sources_test.go @@ -184,12 +184,3 @@ func writeTestFile(t *testing.T, path, contents string) { t.Fatal(err) } } - -func runGit(t *testing.T, directory string, arguments ...string) { - t.Helper() - command := exec.Command("git", arguments...) - command.Dir = directory - if output, err := command.CombinedOutput(); err != nil { - t.Fatalf("git %s: %v\n%s", strings.Join(arguments, " "), err, output) - } -}