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) - } -}