diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..6e3491d3 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,44 @@ +name: CI + +on: + push: + pull_request: + +permissions: + contents: read + +jobs: + go: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-go@v7 + with: + go-version-file: go.mod + cache: true + - name: Check formatting + run: | + unformatted="$(gofmt -l $(git ls-files '*.go'))" + if [ -n "$unformatted" ]; then + printf '%s\n' "$unformatted" + exit 1 + fi + - name: Vet + run: go vet ./... + - name: Test + run: go test ./... + - name: Race test + run: go test -race ./... + + fixtures: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-go@v7 + with: + go-version-file: go.mod + cache: true + - name: Bundle compiler + run: go run ./scripts/bundle.go + - name: Run source fixtures + run: PEEPER_BIN="$PWD/build/bin/peeper" go test -count=1 ./x_test diff --git a/.gitignore b/.gitignore index 0e336be1..b17a56f0 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,7 @@ graphify-out clean.md .*/ !.gitignore +!.github/ +!.github/workflows/ +!.github/workflows/*.yml x_test/owned_pointer_carrier/main diff --git a/cmd/build.go b/cmd/build.go index 09139de5..3d5c1566 100644 --- a/cmd/build.go +++ b/cmd/build.go @@ -34,7 +34,7 @@ func compileEntry(path string, debugBuild bool, targetOS, targetArch string) (co )) return compilerContext, nil } - program = compiler.CompileFile(compilerContext, path, "") + program = compiler.CompileFile(compilerContext, path, nil) return compilerContext, program } diff --git a/cmd/command.go b/cmd/command.go index 08c48521..9f228cd5 100644 --- a/cmd/command.go +++ b/cmd/command.go @@ -12,7 +12,7 @@ import ( "strings" "compiler/internal/diagnostics" - driver "compiler/internal/driver" + "compiler/internal/driver" "compiler/internal/project" "compiler/internal/target" "compiler/pkg/colors" @@ -246,7 +246,7 @@ func runCommand(args []string) error { cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { if exitErr, ok := err.(*exec.ExitError); ok { - os.Exit(exitErr.ExitCode()) + return programExitStatus(exitErr.ExitCode()) } return fmt.Errorf("run program: %w", err) } @@ -372,7 +372,7 @@ func checkCommand(args []string) error { failed := false for _, key := range keys { owner := owners[key] - ctx := driver.NewCompilerContext(project.Config{ + ctx := compiler.NewCompilerContext(project.Config{ RootDir: owner.RootDir, ProjectName: owner.ProjectName, Extension: peeper.SourceExt, @@ -380,7 +380,7 @@ func checkCommand(args []string) error { TargetArch: opts.targetArch, }, diagnostics.NewDiagnosticBag()) for _, filePath := range groups[key] { - driver.CompileFile(ctx, filePath, "") + compiler.CompileFile(ctx, filePath, nil) } if err := emitAndCheckDiagnostics(ctx); err != nil { failed = true diff --git a/cmd/command_test.go b/cmd/command_test.go index bdb6485b..aa2dbc1c 100644 --- a/cmd/command_test.go +++ b/cmd/command_test.go @@ -1,6 +1,7 @@ package main import ( + "errors" "os" "path/filepath" "testing" @@ -22,6 +23,28 @@ func TestParseCommandArgsRunDebug(t *testing.T) { } } +func TestRunCommandReturnsProgramStatusAfterCleanup(t *testing.T) { + root := t.TempDir() + sourcePath := filepath.Join(root, "exit"+peeper.SourceExt) + if err := os.WriteFile(sourcePath, []byte("fn main() -> i32 { return 10; }\n"), 0o644); err != nil { + t.Fatalf("write source: %v", err) + } + tempDir := t.TempDir() + t.Setenv("TMPDIR", tempDir) + err := runCommand([]string{sourcePath}) + var status programExitStatus + if !errors.As(err, &status) || status != 10 { + t.Fatalf("runCommand error = %v, want program status 10", err) + } + entries, readErr := os.ReadDir(tempDir) + if readErr != nil { + t.Fatalf("read temp directory: %v", readErr) + } + if len(entries) != 0 { + t.Fatalf("runCommand leaked temporary files: %v", entries) + } +} + func TestParseCommandArgsRejectsConflictingM32TargetArch(t *testing.T) { _, err := parseCommandArgs("build", []string{"--m32", "--target-arch", "amd64"}, false) if err == nil { diff --git a/cmd/dispatch.go b/cmd/dispatch.go index 61b6b494..0869ec1c 100644 --- a/cmd/dispatch.go +++ b/cmd/dispatch.go @@ -1,14 +1,14 @@ package main import ( - "slices" "errors" "flag" "fmt" "os" + "slices" "compiler/cmd/cli" - compiler "compiler/internal/driver" + "compiler/internal/driver" "compiler/internal/lsp" "compiler/pkg/colors" "compiler/pkg/manifest" @@ -21,6 +21,12 @@ const ( exitCodeUsage = 2 ) +type programExitStatus int + +func (status programExitStatus) Error() string { + return fmt.Sprintf("program exited with status %d", status) +} + // exitOnCommandError prints err to stderr in red (unless it is // errAlreadyReported, which the caller has already reported) and exits. func exitOnCommandError(err error) { @@ -30,6 +36,9 @@ func exitOnCommandError(err error) { if errors.Is(err, errAlreadyReported) { os.Exit(exitCodeError) } + if status, ok := errors.AsType[programExitStatus](err); ok { + os.Exit(int(status)) + } colors.RED.Fprintln(os.Stderr, err) os.Exit(exitCodeError) } diff --git a/cmd/dispatch_test.go b/cmd/dispatch_test.go index 77934b48..c89c42b2 100644 --- a/cmd/dispatch_test.go +++ b/cmd/dispatch_test.go @@ -2,10 +2,25 @@ package main import ( "os" + "os/exec" "strings" "testing" ) +func TestExitOnCommandErrorPreservesProgramStatus(t *testing.T) { + if os.Getenv("PEEPER_TEST_PROGRAM_EXIT") == "1" { + exitOnCommandError(programExitStatus(10)) + return + } + cmd := exec.Command(os.Args[0], "-test.run=TestExitOnCommandErrorPreservesProgramStatus") + cmd.Env = append(os.Environ(), "PEEPER_TEST_PROGRAM_EXIT=1") + err := cmd.Run() + exitErr, ok := err.(*exec.ExitError) + if !ok || exitErr.ExitCode() != 10 { + t.Fatalf("subprocess error = %v, want exit status 10", err) + } +} + func TestCommandRegistryHasUniqueNamesAndRequiredAliases(t *testing.T) { seen := make(map[string]string) for _, command := range commandRegistry { diff --git a/cmd/main.go b/cmd/main.go index 8e63da12..af2944bb 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -10,12 +10,12 @@ func main() { exe, _ := os.Executable() if strings.Contains(exe, "go-build") { fmt.Println("run compiled program instead of 'go run'") - os.Exit(1) + os.Exit(exitCodeError) } if parseAndRunCommand(os.Args[1:]) { return } - printUsageAndExit(2) + printUsageAndExit(exitCodeUsage) } diff --git a/docs/diagrams/cli-flow-detailed.d2 b/docs/diagrams/cli-flow-detailed.d2 index 1590ec21..61fce1fb 100644 --- a/docs/diagrams/cli-flow-detailed.d2 +++ b/docs/diagrams/cli-flow-detailed.d2 @@ -13,8 +13,8 @@ entry: { compile: { compileEntry: "compileEntry" - newContext: "driver.NewContext" - parse: "driver.ParseFileWithOverlay" + newContext: "compiler.NewContext" + parse: "compiler.ParseFileWithOverlay" pipeline: "pipeline.Run" phases: "AST -> semantics -> HIR -> CFG -> ownership -> MIR -> LLVM [conceptual]" @@ -53,8 +53,8 @@ check: { args: "parseCommandArgs" discover: "project.DiscoverSourceFiles" owner: "manifest.ResolveSourceFileProject" - context: "driver.NewContext" - roots: "driver.ParseFileWithOverlay" + context: "compiler.NewContext" + roots: "compiler.ParseFileWithOverlay" diagnostics: "emitAndCheckDiagnostics" command -> args -> discover -> owner -> context -> roots -> compile.pipeline -> diagnostics diff --git a/docs/diagrams/cli-flow-high-level.d2 b/docs/diagrams/cli-flow-high-level.d2 index e96e9cf5..f92c7e52 100644 --- a/docs/diagrams/cli-flow-high-level.d2 +++ b/docs/diagrams/cli-flow-high-level.d2 @@ -17,7 +17,7 @@ handler -> lsp: "lspCommand" build: { resolve: "resolveBuildTarget" compile: "compileEntry" - driver: "driver.ParseFileWithOverlay" + driver: "compiler.ParseFileWithOverlay" pipeline: "pipeline.Run" artifacts: "saveIRs" link: "buildExecutable" @@ -31,8 +31,8 @@ build: { check: { discover: "project.DiscoverSourceFiles" owner: "manifest.ResolveSourceFileProject" - context: "driver.NewContext" - roots: "driver.ParseFileWithOverlay" + context: "compiler.NewContext" + roots: "compiler.ParseFileWithOverlay" discover -> owner -> context -> roots -> pipeline } diff --git a/docs/language-spec.md b/docs/language-spec.md index ab8555fd..9ca9e61a 100644 --- a/docs/language-spec.md +++ b/docs/language-spec.md @@ -112,6 +112,12 @@ arithmetic for signed integers and logical for unsigned integers and `byte`. Shift count must be non-negative and smaller than operand width. Invalid constant counts are compile errors; invalid runtime counts trap before shift. +Integer addition, subtraction, multiplication, division, and remainder use the +same finite-width representation. Signed division truncates toward zero. The +unrepresentable signed case `MIN / -1` wraps to `MIN`, and `MIN % -1` is zero. +Integer division or remainder by zero traps at runtime. Floating-point division +and remainder keep IEEE behavior. + Expression precedence, highest to lowest, is: 1. call, index, and selector diff --git a/internal/backend/llvm/dynamic_array_emit.go b/internal/backend/llvm/dynamic_array_emit.go new file mode 100644 index 00000000..c8c7741c --- /dev/null +++ b/internal/backend/llvm/dynamic_array_emit.go @@ -0,0 +1,284 @@ +package llvm + +import ( + "fmt" + "strconv" + + "compiler/internal/ir" + "compiler/internal/ir/mir" + "compiler/internal/semantics/symbols" +) + +func emitDynamicArrayAlloc(b *llvmBuilder, alloc *mir.DynamicArrayAlloc) llvmValue { + if b == nil || alloc == nil { + return llvmValue{} + } + if alloc.Length < 0 { + b.emitter.markInvalid("dynamic array allocation has negative length") + return b.zero(b.emitter.layout(alloc.Type)) + } + arrayType, ok := b.emitter.mod.Types.Type(alloc.Type) + if !ok || arrayType.Kind != ir.TypeArray || arrayType.Length != "" { + b.emitter.markInvalid("dynamic array allocation has invalid type") + return b.zero(b.emitter.layout(alloc.Type)) + } + allocator := allocatorHandleFromRef(b, alloc.Allocator) + indexLayout := b.emitter.layout(b.emitter.mod.Types.IndexType()) + length := b.value(strconv.Itoa(alloc.Length), indexLayout) + if alloc.Length == 0 { + dataLayout := llvmPointerLayout(b.emitter.layout(arrayType.Elem)) + return emitDynamicArrayHeader(b, alloc.Type, b.value("null", dataLayout), length, length, allocator) + } + data := emitDynamicArrayStorageAlloc(b, arrayType.Elem, length, allocator) + return emitDynamicArrayHeader(b, alloc.Type, data, length, length, allocator) +} + +func emitDynamicArrayStorageAlloc(b *llvmBuilder, elemType ir.TypeID, capacity, allocator llvmValue) llvmValue { + size := emitAllocatorStorageSize(b, elemType, capacity) + raw := emitAllocatorAllocate(b, allocator, size, b.value("8", llvmScalarLayout("i32"))) + missing := b.compare("icmp", "eq", raw, b.value("null", raw.Layout)) + id := b.nextID + b.nextID++ + failLabel := fmt.Sprintf("array_alloc_fail_%d", id) + readyLabel := fmt.Sprintf("array_alloc_ready_%d", id) + b.condBranch(missing, failLabel, readyLabel) + b.namedLabel(failLabel) + b.trap() + b.namedLabel(readyLabel) + return b.bitcast(raw, llvmPointerLayout(b.emitter.layout(elemType))) +} + +func emitDynamicArrayHeader(b *llvmBuilder, arrayTypeID ir.TypeID, data, length, capacity, allocator llvmValue) llvmValue { + header := b.zero(b.emitter.layout(arrayTypeID)) + header = b.insertField(header, data, llvmFieldData) + header = b.insertField(header, length, llvmFieldLength) + header = b.insertField(header, capacity, llvmFieldCapacity) + return b.insertField(header, allocator, llvmFieldAllocator) +} + +func emitAlloc(b *llvmBuilder, e *mir.Alloc) llvmValue { + pointerType, ok := b.emitter.mod.Types.Type(e.Type) + if !ok || pointerType.Kind != ir.TypeOwnedPtr { + b.emitter.markInvalid("alloc has invalid result type") + return b.zero(b.emitter.layout(e.Type)) + } + allocReg := allocatorHandleFromRef(b, e.Allocator) + targetLayout := b.emitter.layout(pointerType.Elem) + sizeLayout := b.emitter.layout(b.emitter.mod.Types.IndexType()) + payloadEnd := b.value(fmt.Sprintf("getelementptr (%s, %s* null, i32 1)", targetLayout.Text, targetLayout.Text), llvmPointerLayout(targetLayout)) + size := b.cast("ptrtoint", payloadEnd, sizeLayout) + zeroSize := b.compare("icmp", "eq", size, b.value("0", sizeLayout)) + normSize := b.selectValue(zeroSize, b.value("1", sizeLayout), size) + raw := emitAllocatorAllocate(b, allocReg, normSize, b.value("8", llvmScalarLayout("i32"))) + isNull := b.compare("icmp", "eq", raw, b.value("null", raw.Layout)) + id := b.nextID + b.nextID++ + failLabel := fmt.Sprintf("alloc_fail_%d", id) + doneLabel := fmt.Sprintf("alloc_done_%d", id) + b.condBranch(isNull, failLabel, doneLabel) + b.namedLabel(failLabel) + b.trap() + b.namedLabel(doneLabel) + + dataPtr := b.bitcast(raw, llvmPointerLayout(targetLayout)) + b.store(b.pointerPlace(dataPtr), emitRef(b, e.Value)) + carrier := b.insertField(b.zero(b.emitter.layout(e.Type)), dataPtr, llvmFieldData) + return b.insertField(carrier, allocReg, llvmFieldAllocator) +} + +func emitDynamicArrayReserve(b *llvmBuilder, array llvmValue, typeID ir.TypeID, minimum llvmValue) llvmValue { + elemTypeID, ok := dynamicArrayElementType(b.emitter.mod.Types, typeID) + if !ok { + b.emitter.markInvalid("dynamic array reserve has invalid type") + return b.zero(b.emitter.layout(typeID)) + } + oldData := b.extractField(array, llvmFieldData) + length := b.extractField(array, llvmFieldLength) + capacity := b.extractField(array, llvmFieldCapacity) + allocator := b.extractField(array, llvmFieldAllocator) + sufficient := b.compare("icmp", "uge", capacity, minimum) + id := b.nextID + b.nextID++ + reuseLabel := fmt.Sprintf("array_reserve_reuse_%d", id) + growLabel := fmt.Sprintf("array_reserve_grow_%d", id) + loopLabel := fmt.Sprintf("array_relocate_loop_%d", id) + bodyLabel := fmt.Sprintf("array_relocate_body_%d", id) + continueLabel := fmt.Sprintf("array_relocate_continue_%d", id) + doneLabel := fmt.Sprintf("array_relocate_done_%d", id) + mergeLabel := fmt.Sprintf("array_reserve_done_%d", id) + b.condBranch(sufficient, reuseLabel, growLabel) + b.namedLabel(reuseLabel) + b.branch(mergeLabel) + b.namedLabel(growLabel) + newData := emitDynamicArrayStorageAlloc(b, elemTypeID, minimum, allocator) + relocateEntry := b.currentLabel + b.branch(loopLabel) + b.namedLabel(loopLabel) + nextIndex := b.nextValue(length.Layout) + index := b.phi(length.Layout, llvmIncoming{Value: b.value("0", length.Layout), Label: relocateEntry}, llvmIncoming{Value: nextIndex, Label: continueLabel}) + more := b.compare("icmp", "ult", index, length) + b.condBranch(more, bodyLabel, doneLabel) + b.namedLabel(bodyLabel) + item := b.load(b.gep(b.pointerPlace(oldData), index, false)) + b.store(b.gep(b.pointerPlace(newData), index, false), item) + b.branch(continueLabel) + b.namedLabel(continueLabel) + b.defineArithmetic(nextIndex, "add", index, b.value("1", index.Layout)) + b.branch(loopLabel) + b.namedLabel(doneLabel) + oldIsNull := b.compare("icmp", "eq", oldData, b.value("null", oldData.Layout)) + releaseLabel := fmt.Sprintf("array_reserve_release_%d", id) + releaseDoneLabel := fmt.Sprintf("array_reserve_release_done_%d", id) + b.condBranch(oldIsNull, releaseDoneLabel, releaseLabel) + b.namedLabel(releaseLabel) + oldSize := emitAllocatorStorageSize(b, elemTypeID, capacity) + oldRaw := b.bitcast(oldData, llvmPointerLayout(llvmScalarLayout("i8"))) + emitAllocatorDeallocate(b, allocator, oldRaw, oldSize, b.value("8", llvmScalarLayout("i32"))) + b.branch(releaseDoneLabel) + b.namedLabel(releaseDoneLabel) + resized := emitDynamicArrayHeader(b, typeID, newData, length, minimum, allocator) + b.branch(mergeLabel) + b.namedLabel(mergeLabel) + return b.phi(array.Layout, llvmIncoming{Value: array, Label: reuseLabel}, llvmIncoming{Value: resized, Label: releaseDoneLabel}) +} + +func emitDynamicArrayOp(b *llvmBuilder, op *mir.DynamicArrayOp) { + if b == nil || op == nil || op.Array == nil { + return + } + elemTypeID, ok := dynamicArrayElementType(b.emitter.mod.Types, op.ArrayType) + if !ok { + b.emitter.markInvalid("dynamic array operation has invalid type") + return + } + arrayPlace := b.pointerPlace(emitRef(b, op.Array)) + array := b.load(arrayPlace) + var updated llvmValue + switch op.Op { + case symbols.CompilerOpReserve: + if op.Length == nil { + b.emitter.markInvalid("reserve requires a minimum capacity") + return + } + minimum := emitCast(b, &mir.Cast{Arg: op.Length, Type: b.emitter.mod.Types.IndexType()}) + updated = emitDynamicArrayReserve(b, array, op.ArrayType, minimum) + case symbols.CompilerOpAppend: + updated = emitDynamicArrayAppend(b, op, array) + case symbols.CompilerOpResize: + updated = emitDynamicArrayResize(b, op, array) + case symbols.CompilerOpShrink: + updated = emitDynamicArrayShrink(b, op, array, elemTypeID) + default: + b.emitter.markInvalid("unknown dynamic array operation " + string(op.Op)) + return + } + b.store(arrayPlace, updated) +} + +func emitDynamicArrayShrink(b *llvmBuilder, op *mir.DynamicArrayOp, array llvmValue, elemTypeID ir.TypeID) llvmValue { + if op.Length == nil { + b.emitter.markInvalid("shrink requires a length") + return array + } + data := b.extractField(array, llvmFieldData) + oldLength := b.extractField(array, llvmFieldLength) + capacity := b.extractField(array, llvmFieldCapacity) + allocator := b.extractField(array, llvmFieldAllocator) + newLength := emitCast(b, &mir.Cast{Arg: op.Length, Type: b.emitter.mod.Types.IndexType()}) + shorter := b.compare("icmp", "ult", newLength, oldLength) + id := b.nextID + b.nextID++ + keepLabel := fmt.Sprintf("array_shrink_keep_%d", id) + shrinkLabel := fmt.Sprintf("array_shrink_drop_%d", id) + doneLabel := fmt.Sprintf("array_shrink_done_%d", id) + b.condBranch(shorter, shrinkLabel, keepLabel) + b.namedLabel(keepLabel) + b.branch(doneLabel) + b.namedLabel(shrinkLabel) + emitDynamicArrayElementRangeDrop(b, data, elemTypeID, newLength, oldLength) + shrunk := emitDynamicArrayHeader(b, op.ArrayType, data, newLength, capacity, allocator) + shrinkDoneLabel := b.currentLabel + b.branch(doneLabel) + b.namedLabel(doneLabel) + return b.phi(array.Layout, llvmIncoming{Value: array, Label: keepLabel}, llvmIncoming{Value: shrunk, Label: shrinkDoneLabel}) +} + +func emitDynamicArrayAppend(b *llvmBuilder, op *mir.DynamicArrayOp, array llvmValue) llvmValue { + if op.Value == nil { + b.emitter.markInvalid("append requires a value") + return array + } + length := b.extractField(array, llvmFieldLength) + capacity := b.extractField(array, llvmFieldCapacity) + newLength := b.arithmetic("add", length, b.value("1", length.Layout)) + overflow := b.compare("icmp", "ult", newLength, length) + id := b.nextID + b.nextID++ + failLabel := fmt.Sprintf("array_append_fail_%d", id) + capacityLabel := fmt.Sprintf("array_append_capacity_%d", id) + keepLabel := fmt.Sprintf("array_append_keep_%d", id) + growLabel := fmt.Sprintf("array_append_grow_%d", id) + growReadyLabel := fmt.Sprintf("array_append_grow_ready_%d", id) + readyLabel := fmt.Sprintf("array_append_ready_%d", id) + b.condBranch(overflow, failLabel, capacityLabel) + b.namedLabel(failLabel) + b.trap() + b.namedLabel(capacityLabel) + hasSpace := b.compare("icmp", "ult", length, capacity) + b.condBranch(hasSpace, keepLabel, growLabel) + b.namedLabel(keepLabel) + b.branch(readyLabel) + b.namedLabel(growLabel) + overflowLayout := llvmAggregateLayout([]*llvmLayout{capacity.Layout, llvmScalarLayout("i1")}, nil) + overflowFn := b.value("@llvm.umul.with.overflow."+capacity.Layout.Text, llvmFunctionLayout(overflowLayout, []*llvmLayout{capacity.Layout, capacity.Layout})) + doubledAndOverflow := b.call(overflowFn, []llvmValue{capacity, b.value("2", capacity.Layout)}) + doubled := b.extractIndex(doubledAndOverflow, 0) + doubleOverflow := b.extractIndex(doubledAndOverflow, 1) + b.condBranch(doubleOverflow, failLabel, growReadyLabel) + b.namedLabel(growReadyLabel) + tooSmall := b.compare("icmp", "ult", doubled, newLength) + grownCapacity := b.selectValue(tooSmall, newLength, doubled) + b.branch(readyLabel) + b.namedLabel(readyLabel) + desiredCapacity := b.phi(capacity.Layout, llvmIncoming{Value: capacity, Label: keepLabel}, llvmIncoming{Value: grownCapacity, Label: growReadyLabel}) + reserved := emitDynamicArrayReserve(b, array, op.ArrayType, desiredCapacity) + data := b.extractField(reserved, llvmFieldData) + finalCapacity := b.extractField(reserved, llvmFieldCapacity) + b.store(b.gep(b.pointerPlace(data), length, false), emitRef(b, op.Value)) + allocator := b.extractField(reserved, llvmFieldAllocator) + return emitDynamicArrayHeader(b, op.ArrayType, data, newLength, finalCapacity, allocator) +} + +func emitDynamicArrayResize(b *llvmBuilder, op *mir.DynamicArrayOp, array llvmValue) llvmValue { + if op.Length == nil || op.Value == nil { + b.emitter.markInvalid("resize requires a length and fill value") + return array + } + oldLength := b.extractField(array, llvmFieldLength) + newLength := emitCast(b, &mir.Cast{Arg: op.Length, Type: b.emitter.mod.Types.IndexType()}) + resized := emitDynamicArrayReserve(b, array, op.ArrayType, newLength) + data := b.extractField(resized, llvmFieldData) + capacity := b.extractField(resized, llvmFieldCapacity) + allocator := b.extractField(resized, llvmFieldAllocator) + id := b.nextID + b.nextID++ + entryLabel := b.currentLabel + loopLabel := fmt.Sprintf("array_resize_loop_%d", id) + bodyLabel := fmt.Sprintf("array_resize_body_%d", id) + continueLabel := fmt.Sprintf("array_resize_continue_%d", id) + doneLabel := fmt.Sprintf("array_resize_done_%d", id) + b.branch(loopLabel) + b.namedLabel(loopLabel) + nextIndex := b.nextValue(oldLength.Layout) + index := b.phi(oldLength.Layout, llvmIncoming{Value: oldLength, Label: entryLabel}, llvmIncoming{Value: nextIndex, Label: continueLabel}) + more := b.compare("icmp", "ult", index, newLength) + b.condBranch(more, bodyLabel, doneLabel) + b.namedLabel(bodyLabel) + b.store(b.gep(b.pointerPlace(data), index, false), emitRef(b, op.Value)) + b.branch(continueLabel) + b.namedLabel(continueLabel) + b.defineArithmetic(nextIndex, "add", index, b.value("1", index.Layout)) + b.branch(loopLabel) + b.namedLabel(doneLabel) + return emitDynamicArrayHeader(b, op.ArrayType, data, newLength, capacity, allocator) +} diff --git a/internal/backend/llvm/module_emit.go b/internal/backend/llvm/emitter.go similarity index 100% rename from internal/backend/llvm/module_emit.go rename to internal/backend/llvm/emitter.go diff --git a/internal/backend/llvm/lower_llvm_test.go b/internal/backend/llvm/emitter_test.go similarity index 96% rename from internal/backend/llvm/lower_llvm_test.go rename to internal/backend/llvm/emitter_test.go index 0fff9faa..b2fa4c33 100644 --- a/internal/backend/llvm/lower_llvm_test.go +++ b/internal/backend/llvm/emitter_test.go @@ -370,6 +370,83 @@ func TestGenerateLLVMIRLowersIntegerBitwiseOperators(t *testing.T) { } } +func TestGenerateLLVMIRGuardsIntegerDivisionAndRemainder(t *testing.T) { + tests := []struct { + name string + op string + typeID ir.TypeID + operation string + overflow string + unexpected string + }{ + {name: "signed division", op: "/", typeID: llvmTypes.i8, operation: "sdiv", overflow: "-128"}, + {name: "signed remainder", op: "%", typeID: llvmTypes.i8, operation: "srem", overflow: "0"}, + {name: "unsigned division", op: "/", typeID: llvmTypes.u8, operation: "udiv", unexpected: "icmp eq i8 %right, -1"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := &mir.RefName{Name: "result", Type: tt.typeID} + mod := &mir.Module{ + Name: "test", Types: llvmTypes.table, + Funcs: []*mir.Function{{ + Name: "apply", + Params: []ir.Param{{Name: "left", Type: tt.typeID}, {Name: "right", Type: tt.typeID}}, + ReturnType: tt.typeID, + Blocks: []*mir.Block{{ + ID: 0, + Instrs: []mir.Instr{&mir.Assign{Name: "result", Value: &mir.Binary{ + Op: tt.op, Left: &mir.RefName{Name: "left", Type: tt.typeID}, Right: &mir.RefName{Name: "right", Type: tt.typeID}, Type: tt.typeID, + }}}, + Term: &mir.Ret{Value: result}, + }}, + }}, + } + out := GenerateLLVMIR(mod, diagnostics.NewDiagnosticBag(), testLinuxAMD64, false) + zeroGuard := strings.Index(out, "icmp eq i8 %right, 0") + trap := strings.Index(out, "call void @llvm.trap()") + operation := strings.Index(out, " = "+tt.operation+" i8 %left, %right") + if zeroGuard < 0 || trap < zeroGuard || operation < trap { + t.Fatalf("zero-divisor guard must dominate %s, got:\n%s", tt.operation, out) + } + if tt.overflow != "" { + leftGuard := strings.Index(out, "icmp eq i8 %left, -128") + rightGuard := strings.Index(out, "icmp eq i8 %right, -1") + merge := strings.Index(out, " = phi i8 [ "+tt.overflow+", %") + if leftGuard < trap || rightGuard < leftGuard || operation < rightGuard || merge < operation { + t.Fatalf("signed overflow guard must bypass %s and merge %s, got:\n%s", tt.operation, tt.overflow, out) + } + } + if tt.unexpected != "" && strings.Contains(out, tt.unexpected) { + t.Fatalf("unsigned division emitted signed overflow guard, got:\n%s", out) + } + }) + } +} + +func TestGenerateLLVMIRLeavesFloatDivisionUnguarded(t *testing.T) { + f32 := llvmTypes.table.Intern(ir.Type{Kind: ir.TypeFloat, Bits: 32}) + result := &mir.RefName{Name: "result", Type: f32} + mod := &mir.Module{ + Name: "test", Types: llvmTypes.table, + Funcs: []*mir.Function{{ + Name: "apply", + Params: []ir.Param{{Name: "left", Type: f32}, {Name: "right", Type: f32}}, + ReturnType: f32, + Blocks: []*mir.Block{{ + ID: 0, + Instrs: []mir.Instr{&mir.Assign{Name: "result", Value: &mir.Binary{ + Op: "/", Left: &mir.RefName{Name: "left", Type: f32}, Right: &mir.RefName{Name: "right", Type: f32}, Type: f32, + }}}, + Term: &mir.Ret{Value: result}, + }}, + }}, + } + out := GenerateLLVMIR(mod, diagnostics.NewDiagnosticBag(), testLinuxAMD64, false) + if !strings.Contains(out, " = fdiv float %left, %right") || strings.Contains(out, "call void @llvm.trap()") { + t.Fatalf("float division must retain direct IEEE lowering, got:\n%s", out) + } +} + func TestGenerateLLVMIRGuardsMixedShiftCountBeforeCast(t *testing.T) { u16 := llvmTypes.table.Intern(ir.Type{Kind: ir.TypeInteger, Bits: 16}) result := &mir.RefName{Name: "result", Type: llvmTypes.u8} diff --git a/internal/backend/llvm/instruction_emit.go b/internal/backend/llvm/instruction_emit.go new file mode 100644 index 00000000..f431fab0 --- /dev/null +++ b/internal/backend/llvm/instruction_emit.go @@ -0,0 +1,977 @@ +package llvm + +import ( + "fmt" + "math" + "math/big" + "strconv" + "strings" + + "compiler/internal/diagnostics" + "compiler/internal/ir" + "compiler/internal/ir/mir" + "compiler/internal/problems" + "compiler/internal/source" + "compiler/internal/target" +) + +type llvmEmitter struct { + mod *mir.Module + diag *diagnostics.DiagnosticBag + target target.Info + badTypes map[string]struct{} + layouts map[ir.TypeID]*llvmLayout + invalid bool + externalGlobals map[string]ir.TypeID + debug *llvmDebugEmitter +} + +func emitStore(b *llvmBuilder, store *mir.Store) { + if b == nil || store == nil || store.Place == nil || store.Value == nil { + return + } + ptr, ok := emitPlacePtr(b, store.Place) + if !ok { + return + } + b.store(ptr, emitRef(b, store.Value)) +} + +func emitPrint(b *llvmBuilder, printInstr *mir.Print) { + if b == nil || printInstr == nil || printInstr.Value == nil { + return + } + typeID := mirRefType(printInstr.Value) + typ, typeOK := b.emitter.mod.Types.Type(typeID) + if !typeOK { + b.emitter.markInvalid("print reached LLVM with invalid type") + return + } + value := emitRef(b, printInstr.Value) + formatName := "" + formatSize := 0 + arguments := make([]llvmValue, 0, 2) + i8 := llvmScalarLayout("i8") + i8Pointer := llvmPointerLayout(i8) + switch { + case typ.Kind == ir.TypeBool: + trueText := "getelementptr inbounds ([5 x i8], [5 x i8]* @.print.true, i32 0, i32 0)" + falseText := "getelementptr inbounds ([6 x i8], [6 x i8]* @.print.false, i32 0, i32 0)" + selected := b.selectValue(value, b.value(trueText, i8Pointer), b.value(falseText, i8Pointer)) + formatName, formatSize, arguments = "string", 3, []llvmValue{selected} + case typ.Kind == ir.TypeCStr: + formatName, formatSize, arguments = "string", 3, []llvmValue{value} + case typ.Kind == ir.TypeString: + data, length := emitStringDataAndLength(b, value) + precision := length + switch length.Layout.Text { + case "i32": + case "i64": + precision = b.cast("trunc", length, llvmScalarLayout("i32")) + default: + b.emitter.markInvalid("print reached LLVM with unsupported string length type " + length.Layout.Text) + return + } + formatName, formatSize, arguments = "str", 5, []llvmValue{precision, data} + case typ.Kind == ir.TypeRawPtr: + formatName, formatSize, arguments = "pointer", 3, []llvmValue{value} + case typ.Kind == ir.TypeFloat: + if typ.Bits == 32 { + f64 := b.emitter.mod.Types.Intern(ir.Type{Kind: ir.TypeFloat, Bits: 64}) + value = emitCast(b, &mir.Cast{Arg: printInstr.Value, Type: f64}) + } + formatName, formatSize, arguments = "float", 3, []llvmValue{value} + default: + signed, _, ok := integerInfoID(b.emitter.mod.Types, typeID) + if !ok { + b.emitter.markInvalid("print reached LLVM with unsupported type " + b.emitter.mod.Types.Text(typeID)) + return + } + promotedType := b.emitter.mod.Types.Intern(ir.Type{Kind: ir.TypeInteger, Bits: 64}) + formatName = "unsigned" + if signed { + promotedType = b.emitter.mod.Types.Intern(ir.Type{Kind: ir.TypeInteger, Signed: true, Bits: 64}) + formatName = "signed" + } + value = emitCast(b, &mir.Cast{Arg: printInstr.Value, Type: promotedType}) + formatSize, arguments = 5, []llvmValue{value} + } + formatText := fmt.Sprintf("getelementptr inbounds ([%d x i8], [%d x i8]* @.print.%s, i32 0, i32 0)", formatSize, formatSize, formatName) + printf := b.value("@printf", llvmFunctionLayout(llvmScalarLayout("i32"), []*llvmLayout{i8Pointer})) + b.variadicCall(printf, []llvmValue{b.value(formatText, i8Pointer)}, arguments) + if printInstr.Newline { + newline := b.value("getelementptr inbounds ([2 x i8], [2 x i8]* @.print.newline, i32 0, i32 0)", i8Pointer) + b.variadicCall(printf, []llvmValue{newline}, nil) + } +} + +// emitTargetIndexAsI64 widens a target-sized length before it reaches lowering +// paths whose arithmetic and comparisons are intentionally i64. +func emitIndexPtr(b *llvmBuilder, base llvmValue, baseType ir.TypeID, addressed bool, indexRef mir.ValueRef) (llvmPlace, bool) { + if b == nil || base.Layout == nil || baseType == ir.InvalidType || indexRef == nil { + return llvmPlace{}, false + } + targetID := baseType + pointed := false + referenced := false + if typ, ok := b.emitter.mod.Types.Type(targetID); ok { + switch typ.Kind { + case ir.TypeOwnedPtr: + targetID, pointed = typ.Elem, true + case ir.TypeReference: + targetID, referenced = typ.Elem, true + } + } + target, ok := b.emitter.mod.Types.Type(targetID) + if !ok || (target.Kind != ir.TypeArray && target.Kind != ir.TypeSlice) { + return llvmPlace{}, false + } + if target.Kind == ir.TypeSlice || target.Length == "" { + header := base + // Dynamic-owner references lower as pointers to their carrier header; + // slice references lower as the carrier aggregate itself. + if addressed || pointed || referenced && base.Layout.Kind == llvmLayoutPointer { + header = b.load(b.pointerPlace(base)) + } + data := b.extractField(header, llvmFieldData) + length := b.extractField(header, llvmFieldLength) + index, ok := emitBoundsCheckedIndex(b, indexRef, emitTargetIndexAsI64(b, length)) + if !ok { + return llvmPlace{}, false + } + return b.gep(b.pointerPlace(data), index, false), true + } + length, lengthErr := strconv.Atoi(target.Length) + var index llvmValue + if indexConst, constant := indexRef.(*mir.RefConst); constant { + parsedIndex, indexErr := strconv.Atoi(indexConst.Value) + if lengthErr != nil || indexErr != nil || parsedIndex < 0 || parsedIndex >= length { + b.emitter.invalid = true + if b.emitter.diag != nil { + b.emitter.diag.Add(problems.ArrayIndexOutOfBounds(indexConst.Value, target.Length, nil)) + } + return llvmPlace{}, false + } + index = emitRef(b, indexRef) + } else { + if lengthErr != nil { + return llvmPlace{}, false + } + index, ok = emitBoundsCheckedIndex(b, indexRef, b.value(target.Length, llvmScalarLayout("i64"))) + if !ok { + return llvmPlace{}, false + } + } + if !addressed && !pointed && !referenced { + b.emitter.markInvalid("fixed-array index place requires addressable storage") + return llvmPlace{}, false + } + arrayPlace := b.pointerPlace(base) + return b.arrayElement(arrayPlace, index, true), true +} + +// Directly addressed roots need entry-block storage so one pointer dominates every place use. +func placeNeedsRootAddr(types *ir.TypeTable, place *mir.Place) bool { + if place == nil || place.Root == nil || len(place.Projections) == 0 { + return place != nil && place.Root != nil + } + projection := place.Projections[0] + switch projection.Kind { + case mir.PlaceProjectionDeref: + return false + case mir.PlaceProjectionField: + return true + case mir.PlaceProjectionIndex: + rootType, ok := types.Type(mirRefType(place.Root)) + if !ok { + return false + } + if rootType.Kind == ir.TypeOwnedPtr || rootType.Kind == ir.TypeReference { + return false + } + return rootType.Kind == ir.TypeArray && rootType.Length != "" + default: + return false + } +} + +func emitPlaceRootAddr(b *llvmBuilder, root mir.ValueRef) (llvmPlace, bool) { + if b == nil || root == nil { + return llvmPlace{}, false + } + if ref, ok := root.(*mir.RefName); ok && ref != nil { + if ptr, found := ensureLocalAddr(b, ref); found { + return ptr, true + } + } + value := emitRef(b, root) + ptr := b.alloca(value.Layout) + b.store(ptr, value) + return ptr, true +} + +func emitPlacePtr(b *llvmBuilder, place *mir.Place) (llvmPlace, bool) { + if b == nil || place == nil || place.Root == nil { + return llvmPlace{}, false + } + previousLocation := b.debugLocationID + defer func() { b.debugLocationID = previousLocation }() + + addressed := placeNeedsRootAddr(b.emitter.mod.Types, place) + current := llvmPlace{} + hasCurrent := false + if addressed { + current, hasCurrent = emitPlaceRootAddr(b, place.Root) + } + currentType := mirRefType(place.Root) + for _, projection := range place.Projections { + b.setLocation(projection.Location) + switch projection.Kind { + case mir.PlaceProjectionDeref: + var value llvmValue + if hasCurrent { + value = b.load(current) + } else { + value = emitRef(b, place.Root) + } + if !isOwnedInterfaceType(b.emitter.mod.Types, currentType) { + if typ, ok := b.emitter.mod.Types.Type(currentType); ok && typ.Kind == ir.TypeOwnedPtr { + value = b.extractField(value, llvmFieldData) + } + } + current = b.pointerPlace(value) + hasCurrent = true + addressed = true + case mir.PlaceProjectionField: + if !hasCurrent { + b.emitter.markInvalid("field place requires addressable storage") + return llvmPlace{}, false + } + current = b.fieldPlace(current, projection.FieldIndex) + case mir.PlaceProjectionIndex: + base := emitRef(b, place.Root) + if hasCurrent { + base = b.pointerValue(current) + } + var ok bool + current, ok = emitIndexPtr(b, base, currentType, addressed, projection.Index) + if !ok { + return llvmPlace{}, false + } + hasCurrent = true + addressed = true + default: + b.emitter.markInvalid(fmt.Sprintf("unsupported MIR place projection %d", projection.Kind)) + return llvmPlace{}, false + } + currentType = projection.Type + } + if addressed && hasCurrent { + return current, true + } + b.setLocation(place.Location) + return emitPlaceRootAddr(b, place.Root) +} + +type llvmBuilder struct { + out *strings.Builder + nextID int + locals map[string]llvmValue + localPtrs map[string]llvmPlace + emitter *llvmEmitter + debug *llvmDebugEmitter + debugScopeID int + debugLocationID int + currentLabel string +} + +func emitCast(b *llvmBuilder, cast *mir.Cast) llvmValue { + if b == nil || cast == nil || cast.Arg == nil { + if b != nil { + b.invariant("cast requires MIR argument") + } + return llvmValue{} + } + + argRef := emitRef(b, cast.Arg) + fromType := mirRefType(cast.Arg) + toType := cast.Type + from, fromOK := b.emitter.mod.Types.Type(fromType) + to, toOK := b.emitter.mod.Types.Type(toType) + if !fromOK || !toOK { + return argRef + } + toLayout := b.emitter.layout(toType) + if toLayout == nil { + b.invariant("cast target has no LLVM layout") + } + + if fromType == toType { + return argRef + } + if to.Kind == ir.TypeBool { + if from.Kind == ir.TypeFloat { + return b.compare("fcmp", "one", argRef, b.value("0.0", argRef.Layout)) + } + if _, _, ok := integerInfoID(b.emitter.mod.Types, fromType); ok { + return b.compare("icmp", "ne", argRef, b.value("0", argRef.Layout)) + } + return argRef + } + + if toSigned, _, ok := integerInfoID(b.emitter.mod.Types, toType); from.Kind == ir.TypeFloat && ok { + if toSigned { + return b.cast("fptosi", argRef, toLayout) + } + return b.cast("fptoui", argRef, toLayout) + } else if fromSigned, _, ok := integerInfoID(b.emitter.mod.Types, fromType); ok && to.Kind == ir.TypeFloat { + if fromSigned { + return b.cast("sitofp", argRef, toLayout) + } + return b.cast("uitofp", argRef, toLayout) + } else if from.Kind == ir.TypeFloat && to.Kind == ir.TypeFloat { + if from.Bits == 64 && to.Bits == 32 { + return b.cast("fptrunc", argRef, toLayout) + } else if from.Bits == 32 && to.Bits == 64 { + return b.cast("fpext", argRef, toLayout) + } + return argRef + } else if fromSigned, fromBits, ok := integerInfoID(b.emitter.mod.Types, fromType); ok { + _, toBits, ok := integerInfoID(b.emitter.mod.Types, toType) + if !ok { + return argRef + } + if fromBits < toBits { + if fromSigned { + return b.cast("sext", argRef, toLayout) + } + return b.cast("zext", argRef, toLayout) + } else if fromBits > toBits { + return b.cast("trunc", argRef, toLayout) + } + return argRef + } + 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 { + debug = emitter.debug + } + return &llvmBuilder{ + out: out, + nextID: 1, + locals: make(map[string]llvmValue), + localPtrs: make(map[string]llvmPlace), + emitter: emitter, + debug: debug, + debugScopeID: debugScopeID, + debugLocationID: -1, + } +} + +func (b *llvmBuilder) nextReg() string { + name := fmt.Sprintf("%%t%d", b.nextID) + b.nextID++ + return name +} + +func (b *llvmBuilder) line(text string) { + b.out.WriteString(" ") + b.out.WriteString(text) + if b.debugLocationID >= 0 { + fmt.Fprintf(b.out, ", !dbg !%d", b.debugLocationID) + } + b.out.WriteString("\n") +} + +func (b *llvmBuilder) label(id int) { + b.namedLabel(fmt.Sprintf("b%d", id)) +} + +func (b *llvmBuilder) namedLabel(name string) { + fmt.Fprintf(b.out, "%s:\n", name) + b.currentLabel = name +} + +func (b *llvmBuilder) setLocation(loc *source.Location) { + if b == nil { + return + } + if b.debug == nil { + b.debugLocationID = -1 + return + } + b.debugLocationID = b.debug.locationID(loc, b.debugScopeID) +} + +func withLLVMLocation[T any](b *llvmBuilder, loc *source.Location, emit func() T) T { + if b == nil || emit == nil { + var zero T + return zero + } + prev := b.debugLocationID + b.setLocation(loc) + out := emit() + b.debugLocationID = prev + return out +} + +// emitIntegerDivRem prevents LLVM's undefined integer division cases from +// executing while preserving Peeper's finite-width arithmetic contract. +func emitIntegerDivRem(b *llvmBuilder, op string, typeID ir.TypeID, left, right llvmValue) llvmValue { + signed, bits, ok := integerInfoID(b.emitter.mod.Types, typeID) + if !ok { + b.emitter.markInvalid("integer division lowering requires integral operands") + return left + } + + id := b.nextID + b.nextID++ + failLabel := fmt.Sprintf("divrem_zero_fail_%d", id) + nonzeroLabel := fmt.Sprintf("divrem_nonzero_%d", id) + zero := b.value("0", right.Layout) + b.condBranch(b.compare("icmp", "eq", right, zero), failLabel, nonzeroLabel) + b.namedLabel(failLabel) + b.trap() + b.namedLabel(nonzeroLabel) + + opcode := "udiv" + if op == "%" { + opcode = "urem" + } + if !signed { + return b.arithmetic(opcode, left, right) + } + + opcode = "sdiv" + minValue := b.value(new(big.Int).Neg(new(big.Int).Lsh(big.NewInt(1), uint(bits-1))).String(), left.Layout) + overflowValue := minValue + if op == "%" { + opcode = "srem" + overflowValue = b.value("0", left.Layout) + } + + leftIsMin := b.compare("icmp", "eq", left, minValue) + rightIsNegativeOne := b.compare("icmp", "eq", right, b.value("-1", right.Layout)) + overflow := b.arithmetic("and", leftIsMin, rightIsNegativeOne) + overflowLabel := fmt.Sprintf("divrem_overflow_%d", id) + computeLabel := fmt.Sprintf("divrem_compute_%d", id) + readyLabel := fmt.Sprintf("divrem_ready_%d", id) + b.condBranch(overflow, overflowLabel, computeLabel) + b.namedLabel(overflowLabel) + b.branch(readyLabel) + b.namedLabel(computeLabel) + computed := b.arithmetic(opcode, left, right) + b.branch(readyLabel) + b.namedLabel(readyLabel) + return b.phi(left.Layout, + llvmIncoming{Value: overflowValue, Label: overflowLabel}, + llvmIncoming{Value: computed, Label: computeLabel}, + ) +} + +func emitValueExpr(b *llvmBuilder, expr mir.ValueExpr) llvmValue { + return withLLVMLocation(b, mir.ValueExprLocation(expr), func() llvmValue { + switch e := expr.(type) { + case *mir.Move: + return emitRef(b, e.Src) + case *mir.Len: + return emitLen(b, e.Value) + case *mir.StringLiteral: + layout := b.emitter.layout(e.Type) + rawPointer := layout.Elements[layout.Fields[llvmFieldData]] + dataType := fmt.Sprintf("[%d x i8]", e.Length+1) + data := b.value(fmt.Sprintf("getelementptr inbounds (%s, %s* %s, i64 0, i64 0)", dataType, dataType, e.Name), rawPointer) + value := b.insertField(b.zero(layout), data, llvmFieldData) + value = b.insertField(value, b.value(strconv.Itoa(e.Length), layout.Elements[layout.Fields[llvmFieldLength]]), llvmFieldLength) + return b.insertField(value, b.value("null", rawPointer), llvmFieldAllocator) + case *mir.Cast: + return emitCast(b, e) + case *mir.Unary: + arg := emitRef(b, e.Arg) + switch e.Op { + case "-": + if isFloatType(b.emitter.mod.Types, e.Type) { + return b.arithmetic("fsub", b.value("0.0", arg.Layout), arg) + } + return b.arithmetic("sub", b.value("0", arg.Layout), arg) + case "!": + return emitLogicalNot(b, arg, e.Arg) + case "~": + return b.arithmetic("xor", arg, b.value("-1", arg.Layout)) + default: + return arg + } + case *mir.Binary: + left := emitRef(b, e.Left) + right := emitRef(b, e.Right) + leftType := mirRefType(e.Left) + opcode := "" + switch e.Op { + case "+": + if isFloatType(b.emitter.mod.Types, leftType) { + opcode = "fadd" + } else { + opcode = "add" + } + case "-": + if isFloatType(b.emitter.mod.Types, leftType) { + opcode = "fsub" + } else { + opcode = "sub" + } + case "*": + if isFloatType(b.emitter.mod.Types, leftType) { + opcode = "fmul" + } else { + opcode = "mul" + } + case "/": + if isFloatType(b.emitter.mod.Types, leftType) { + opcode = "fdiv" + } else { + return emitIntegerDivRem(b, e.Op, leftType, left, right) + } + case "%": + if isFloatType(b.emitter.mod.Types, leftType) { + opcode = "frem" + } else { + return emitIntegerDivRem(b, e.Op, leftType, left, right) + } + case "&": + opcode = "and" + case "|": + opcode = "or" + case "^": + opcode = "xor" + case "<<", ">>": + _, bits, ok := integerInfoID(b.emitter.mod.Types, leftType) + if !ok { + b.emitter.markInvalid("shift lowering requires integral operands") + return left + } + invalid := b.compare("icmp", "uge", right, b.value(strconv.Itoa(bits), right.Layout)) + shiftID := b.nextID + b.nextID++ + failLabel := fmt.Sprintf("shift_fail_%d", shiftID) + readyLabel := fmt.Sprintf("shift_ready_%d", shiftID) + b.condBranch(invalid, failLabel, readyLabel) + b.namedLabel(failLabel) + b.trap() + b.namedLabel(readyLabel) + opcode := "shl" + if e.Op == ">>" { + opcode = "ashr" + if isUnsignedTypeID(b.emitter.mod.Types, leftType) { + opcode = "lshr" + } + } + shiftCount := right + if mirRefType(e.Right) != mirRefType(e.Left) { + shiftCount = emitCast(b, &mir.Cast{Arg: e.Right, Type: mirRefType(e.Left), Location: mir.ValueRefLocation(e.Right)}) + } + return b.arithmetic(opcode, left, shiftCount) + case "==", "!=", "<", "<=", ">", ">=": + if result, ok := emitOptionalNoneCompare(b, e.Op, e.Left, e.Right, left, right); ok { + return result + } + if isFloatType(b.emitter.mod.Types, leftType) { + pred := map[string]string{"==": "oeq", "!=": "one", "<": "olt", "<=": "ole", ">": "ogt", ">=": "oge"}[e.Op] + return b.compare("fcmp", pred, left, right) + } + return b.compare("icmp", integerComparePredID(b.emitter.mod.Types, e.Op, leftType), left, right) + case "&&", "||": + lc := emitCondRef(b, e.Left) + rc := emitCondRef(b, e.Right) + if e.Op == "&&" { + return b.arithmetic("and", lc, rc) + } + return b.arithmetic("or", lc, rc) + default: + return left + } + return b.arithmetic(opcode, left, right) + case *mir.Call: + args := make([]llvmValue, len(e.Args)) + for i, arg := range e.Args { + args[i] = emitRef(b, arg) + } + callee := emitRef(b, e.Callee) + if callee.Layout.Kind != llvmLayoutFunction { + b.emitter.markInvalid("call reached LLVM without function type") + return b.value("0", b.emitter.layout(e.Type)) + } + return b.call(callee, args) + case *mir.AddrOf: + place, ok := emitPlacePtr(b, e.Place) + if !ok { + return b.value("0", b.emitter.layout(e.Type)) + } + pointer := b.pointerValue(place) + resultType, ok := b.emitter.mod.Types.Type(e.Type) + if !ok || resultType.Kind != ir.TypeRawPtr { + return pointer + } + if place.Pointee.Text == "i8" { + return pointer + } + return b.bitcast(pointer, b.emitter.layout(e.Type)) + case *mir.SliceView: + return emitSliceView(b, e) + case *mir.StringChars: + return emitStringChars(b, e) + case *mir.Load: + ptr, ok := emitPlacePtr(b, e.Place) + if !ok { + return b.value("0", b.emitter.layout(e.Type)) + } + return b.load(ptr) + case *mir.Field: + return b.extractIndex(emitRef(b, e.Base), e.Index) + case *mir.StructLit: + current := b.zero(b.emitter.layout(e.Type)) + for i, field := range e.Fields { + current = b.insertIndex(current, emitRef(b, field), i) + } + return current + case *mir.ArrayLit: + current := b.zero(b.emitter.layout(e.Type)) + for i, item := range e.Values { + current = b.insertIndex(current, emitRef(b, item), i) + } + return current + case *mir.DynamicArrayAlloc: + return emitDynamicArrayAlloc(b, e) + case *mir.Alloc: + return emitAlloc(b, e) + case *mir.ZeroValue: + return b.zero(b.emitter.layout(e.Type)) + case *mir.OptionalSome: + optional, ok := b.emitter.mod.Types.Type(e.Type) + if !ok || optional.Kind != ir.TypeOptional { + return b.value("0", b.emitter.layout(e.Type)) + } + value := b.insertField(b.zero(b.emitter.layout(e.Type)), b.value("true", llvmScalarLayout("i1")), llvmFieldPresent) + return b.insertField(value, emitRef(b, e.Value), llvmFieldValue) + case *mir.InterfaceMake: + value := emitRef(b, e.Value) + dataPtr := value + var allocator llvmValue + if valueTypeInfo, isOwned := b.emitter.mod.Types.Type(mirRefType(e.Value)); isOwned && valueTypeInfo.Kind == ir.TypeOwnedPtr { + if !isOwnedInterfaceType(b.emitter.mod.Types, mirRefType(e.Value)) { + dataPtr = b.extractField(value, llvmFieldData) + allocator = b.extractField(value, llvmFieldAllocator) + } + } + rawPointer := llvmPointerLayout(llvmScalarLayout("i8")) + dataBytePtr := b.bitcast(dataPtr, rawPointer) + itabSym := interfaceSymbolName("itab", b.emitter.mod.Types, e.Type, e.DataType) + itabPtr := b.value(fmt.Sprintf("bitcast ([%d x i8*]* %s to i8*)", interfaceVtableLength(b.emitter.mod.Types, e.Type, len(e.Slots)), itabSym), rawPointer) + current := b.insertField(b.zero(b.emitter.layout(e.Type)), dataBytePtr, llvmFieldData) + current = b.insertField(current, itabPtr, llvmFieldDispatch) + if allocator.Layout == nil { + return current + } + return b.insertField(current, allocator, llvmFieldAllocator) + case *mir.InterfaceCall: + data, fn, ok := emitInterfaceCallTarget(b, e.Base, e.Slot) + if !ok { + return b.value("0", b.emitter.layout(e.Type)) + } + args := make([]llvmValue, 1, len(e.Args)+1) + args[0] = data + for _, arg := range e.Args { + args = append(args, emitRef(b, arg)) + } + result := b.call(fn, args) + if consumesOwnedInterfaceStorage(b.emitter.mod.Types, e) { + emitInterfaceStorageRelease(b, mirRefType(e.Base), emitRef(b, e.Base), data) + } + return result + default: + b.invariant("unsupported MIR value expression %T", expr) + return llvmValue{} + } + }) +} + +func emitOptionalNoneCompare(b *llvmBuilder, op string, leftRef, rightRef mir.ValueRef, leftValue, rightValue llvmValue) (llvmValue, bool) { + if op != "==" && op != "!=" { + return llvmValue{}, false + } + leftType, leftOK := b.emitter.mod.Types.Type(mirRefType(leftRef)) + rightType, rightOK := b.emitter.mod.Types.Type(mirRefType(rightRef)) + leftOptional := leftOK && leftType.Kind == ir.TypeOptional + rightOptional := rightOK && rightType.Kind == ir.TypeOptional + if !leftOptional && !rightOptional { + return llvmValue{}, false + } + leftNone := leftValue.Text == "zeroinitializer" + rightNone := rightValue.Text == "zeroinitializer" + if leftNone && rightNone { + if op == "==" { + return b.value("true", llvmScalarLayout("i1")), true + } + return b.value("false", llvmScalarLayout("i1")), true + } + var value llvmValue + if leftNone { + value = rightValue + } else if rightNone { + value = leftValue + } else { + if b != nil && b.emitter != nil { + b.emitter.markInvalid("optional equality currently requires `none` on one side") + } + return b.value("false", llvmScalarLayout("i1")), true + } + tag := b.extractField(value, llvmFieldPresent) + pred := "eq" + if op == "!=" { + pred = "ne" + } + return b.compare("icmp", pred, tag, b.value("false", tag.Layout)), true +} + +func emitRef(b *llvmBuilder, ref mir.ValueRef) llvmValue { + return withLLVMLocation(b, mir.ValueRefLocation(ref), func() llvmValue { + if ref == nil { + b.invariant("reference emission requires MIR value") + } + layout := b.emitter.layout(mirRefType(ref)) + if layout == nil { + b.invariant("reference has unsupported type %s", b.emitter.mod.Types.Text(mirRefType(ref))) + } + switch v := ref.(type) { + case *mir.RefConst: + typ, ok := b.emitter.mod.Types.Type(v.Type) + if !ok { + return b.value("0", layout) + } + if typ.Kind == ir.TypeBool && v.Value != "false" && v.Value != "true" { + if b.emitter != nil { + b.emitter.markInvalid("invalid boolean constant: " + v.Value) + } + return b.value("false", layout) + } + if typ.Kind == ir.TypeFloat { + return b.value(llvmFloatConst(v.Value, typ.Bits), layout) + } + if typ.Kind == ir.TypeCStr { + return b.value("null", layout) + } + return b.value(v.Value, layout) + case *mir.RefName: + typ, _ := b.emitter.mod.Types.Type(v.Type) + isFunc := typ.Kind == ir.TypeFunction + if ptr, ok := b.localPtrs[v.Name]; ok { + return b.load(ptr) + } + if reg, ok := b.locals[v.Name]; ok { + return reg + } + if isFunc { + return b.value("@"+ir.SanitizeSymbolName(ir.StripSymbolInstance(v.Name)), layout) + } + + isLocalStatic := false + var localEntry *mir.StaticEntry + if b.emitter != nil && b.emitter.mod != nil { + for _, entry := range b.emitter.mod.StaticData { + eName := strings.TrimPrefix(entry.Name, "@") + vName := strings.TrimPrefix(v.Name, "@") + if eName == vName { + isLocalStatic = true + localEntry = entry + break + } + } + } + + if isLocalStatic && localEntry != nil { + if localEntry.Bytes { + arrayType := fmt.Sprintf("[%d x i8]", len(localEntry.Value)+1) + return b.value(fmt.Sprintf("getelementptr inbounds (%s, %s* %s, i64 0, i64 0)", arrayType, arrayType, localEntry.Name), layout) + } + staticLayout := b.emitter.layout(localEntry.Type) + return b.alignedLoad(b.place(localEntry.Name, staticLayout), localEntry.Align) + } + + if idx := strings.IndexByte(v.Name, '$'); idx >= 0 { + name := "@" + v.Name + if b.emitter.externalGlobals == nil { + b.emitter.externalGlobals = make(map[string]ir.TypeID) + } + b.emitter.externalGlobals[name] = v.Type + + return b.alignedLoad(b.place(name, layout), 4) + } + + if strings.HasPrefix(v.Name, "@") { + return b.value(v.Name, layout) + } + return b.value("0", layout) + default: + b.invariant("unsupported MIR reference %T", ref) + return llvmValue{} + } + }) +} + +func ensureLocalAddr(b *llvmBuilder, ref *mir.RefName) (llvmPlace, bool) { + if b == nil || ref == nil { + return llvmPlace{}, false + } + if ptr, ok := b.localPtrs[ref.Name]; ok { + return ptr, true + } + reg, ok := b.locals[ref.Name] + if !ok { + return llvmPlace{}, false + } + ptr := b.alloca(reg.Layout) + b.store(ptr, reg) + b.localPtrs[ref.Name] = ptr + return ptr, true +} + +func llvmFloatConst(value string, bits int) string { + parsed, err := strconv.ParseFloat(value, bits) + if err != nil { + return value + } + if bits == 32 { + parsed = float64(float32(parsed)) + } + return fmt.Sprintf("0x%016X", math.Float64bits(parsed)) +} + +func emitCondRef(b *llvmBuilder, ref mir.ValueRef) llvmValue { + return withLLVMLocation(b, mir.ValueRefLocation(ref), func() llvmValue { + val := emitRef(b, ref) + refType := mirRefType(ref) + if typ, ok := b.emitter.mod.Types.Type(refType); ok && typ.Kind == ir.TypeBool { + return val + } + if b != nil && b.emitter != nil { + b.emitter.markInvalid("non-bool condition reached llvm lowering: " + b.emitter.mod.Types.Text(refType)) + } + return b.value("false", llvmScalarLayout("i1")) + }) +} + +func mirRefType(ref mir.ValueRef) ir.TypeID { + switch v := ref.(type) { + case *mir.RefConst: + return v.Type + case *mir.RefName: + return v.Type + default: + return ir.InvalidType + } +} + +func emitLogicalNot(b *llvmBuilder, arg llvmValue, ref mir.ValueRef) llvmValue { + if typ, ok := b.emitter.mod.Types.Type(mirRefType(ref)); ok && typ.Kind == ir.TypeBool { + return b.arithmetic("xor", arg, b.value("true", arg.Layout)) + } + cmp := emitCondRef(b, ref) + return b.arithmetic("xor", cmp, b.value("true", cmp.Layout)) +} + +func isFloatType(types *ir.TypeTable, id ir.TypeID) bool { + typ, ok := types.Type(id) + return ok && typ.Kind == ir.TypeFloat +} + +func llvmEscapeString(s string) string { + var sb strings.Builder + for i := range len(s) { + b := s[i] + if b == '\\' { + sb.WriteString(`\5C`) + } else if b == '"' { + sb.WriteString(`\22`) + } else if b >= 32 && b <= 126 { + sb.WriteByte(b) + } else { + fmt.Fprintf(&sb, "\\%02X", b) + } + } + sb.WriteString(`\00`) + return sb.String() +} + +type callDecl struct { + Name string + ReturnType ir.TypeID + Params []ir.TypeID +} + +func collectCallDecls(mod *mir.Module) []callDecl { + if mod == nil { + return nil + } + defined := make(map[string]struct{}) + for _, fn := range mod.Funcs { + if fn != nil && fn.Name != "" { + defined[fn.Name] = struct{}{} + } + } + decls := make(map[string]callDecl) + for _, fn := range mod.Funcs { + if fn == nil || fn.Blocks == nil { + continue + } + for _, block := range fn.Blocks { + if block == nil { + continue + } + for _, instr := range block.Instrs { + switch callInstr := instr.(type) { + case *mir.Assign: + call, ok := callInstr.Value.(*mir.Call) + if !ok || call == nil { + continue + } + recordCallDecl(decls, defined, call) + case *mir.Call: + recordCallDecl(decls, defined, callInstr) + } + } + } + } + out := make([]callDecl, 0, len(decls)) + for _, decl := range decls { + out = append(out, decl) + } + return out +} + +func recordCallDecl(decls map[string]callDecl, defined map[string]struct{}, call *mir.Call) { + if call == nil { + return + } + nameRef, ok := call.Callee.(*mir.RefName) + if !ok || nameRef == nil { + return + } + name := nameRef.Name + if idx := strings.IndexByte(name, '$'); idx >= 0 { + name = name[:idx] + } + if _, ok := defined[name]; ok { + return + } + params := make([]ir.TypeID, 0, len(call.Args)) + for _, arg := range call.Args { + params = append(params, mirRefType(arg)) + } + decls[name] = callDecl{Name: name, ReturnType: call.Type, Params: params} +} diff --git a/internal/backend/llvm/lower_llvm.go b/internal/backend/llvm/lower_llvm.go deleted file mode 100644 index 3bf5a02c..00000000 --- a/internal/backend/llvm/lower_llvm.go +++ /dev/null @@ -1,1815 +0,0 @@ -package llvm - -import ( - "fmt" - "math" - "strconv" - "strings" - - "compiler/internal/diagnostics" - "compiler/internal/ir" - "compiler/internal/ir/mir" - "compiler/internal/problems" - "compiler/internal/semantics/symbols" - "compiler/internal/source" - "compiler/internal/target" -) - -type llvmEmitter struct { - mod *mir.Module - diag *diagnostics.DiagnosticBag - target target.Info - badTypes map[string]struct{} - layouts map[ir.TypeID]*llvmLayout - invalid bool - externalGlobals map[string]ir.TypeID - debug *llvmDebugEmitter -} - -func emitStore(b *llvmBuilder, store *mir.Store) { - if b == nil || store == nil || store.Place == nil || store.Value == nil { - return - } - ptr, ok := emitPlacePtr(b, store.Place) - if !ok { - return - } - b.store(ptr, emitRef(b, store.Value)) -} - -func emitPrint(b *llvmBuilder, printInstr *mir.Print) { - if b == nil || printInstr == nil || printInstr.Value == nil { - return - } - typeID := mirRefType(printInstr.Value) - typ, typeOK := b.emitter.mod.Types.Type(typeID) - if !typeOK { - b.emitter.markInvalid("print reached LLVM with invalid type") - return - } - value := emitRef(b, printInstr.Value) - formatName := "" - formatSize := 0 - arguments := make([]llvmValue, 0, 2) - i8 := llvmScalarLayout("i8") - i8Pointer := llvmPointerLayout(i8) - switch { - case typ.Kind == ir.TypeBool: - trueText := "getelementptr inbounds ([5 x i8], [5 x i8]* @.print.true, i32 0, i32 0)" - falseText := "getelementptr inbounds ([6 x i8], [6 x i8]* @.print.false, i32 0, i32 0)" - selected := b.selectValue(value, b.value(trueText, i8Pointer), b.value(falseText, i8Pointer)) - formatName, formatSize, arguments = "string", 3, []llvmValue{selected} - case typ.Kind == ir.TypeCStr: - formatName, formatSize, arguments = "string", 3, []llvmValue{value} - case typ.Kind == ir.TypeString: - data, length := emitStringDataAndLength(b, value) - precision := length - switch length.Layout.Text { - case "i32": - case "i64": - precision = b.cast("trunc", length, llvmScalarLayout("i32")) - default: - b.emitter.markInvalid("print reached LLVM with unsupported string length type " + length.Layout.Text) - return - } - formatName, formatSize, arguments = "str", 5, []llvmValue{precision, data} - case typ.Kind == ir.TypeRawPtr: - formatName, formatSize, arguments = "pointer", 3, []llvmValue{value} - case typ.Kind == ir.TypeFloat: - if typ.Bits == 32 { - f64 := b.emitter.mod.Types.Intern(ir.Type{Kind: ir.TypeFloat, Bits: 64}) - value = emitCast(b, &mir.Cast{Arg: printInstr.Value, Type: f64}) - } - formatName, formatSize, arguments = "float", 3, []llvmValue{value} - default: - signed, _, ok := integerInfoID(b.emitter.mod.Types, typeID) - if !ok { - b.emitter.markInvalid("print reached LLVM with unsupported type " + b.emitter.mod.Types.Text(typeID)) - return - } - promotedType := b.emitter.mod.Types.Intern(ir.Type{Kind: ir.TypeInteger, Bits: 64}) - formatName = "unsigned" - if signed { - promotedType = b.emitter.mod.Types.Intern(ir.Type{Kind: ir.TypeInteger, Signed: true, Bits: 64}) - formatName = "signed" - } - value = emitCast(b, &mir.Cast{Arg: printInstr.Value, Type: promotedType}) - formatSize, arguments = 5, []llvmValue{value} - } - formatText := fmt.Sprintf("getelementptr inbounds ([%d x i8], [%d x i8]* @.print.%s, i32 0, i32 0)", formatSize, formatSize, formatName) - printf := b.value("@printf", llvmFunctionLayout(llvmScalarLayout("i32"), []*llvmLayout{i8Pointer})) - b.variadicCall(printf, []llvmValue{b.value(formatText, i8Pointer)}, arguments) - if printInstr.Newline { - newline := b.value("getelementptr inbounds ([2 x i8], [2 x i8]* @.print.newline, i32 0, i32 0)", i8Pointer) - b.variadicCall(printf, []llvmValue{newline}, nil) - } -} - -// emitTargetIndexAsI64 widens a target-sized length before it reaches lowering -// paths whose arithmetic and comparisons are intentionally i64. -func emitTargetIndexAsI64(b *llvmBuilder, value llvmValue) llvmValue { - if value.Layout.Text == "i64" { - return value - } - if value.Layout.Text != "i32" { - b.emitter.markInvalid("unsupported target index type " + value.Layout.Text) - return value - } - return b.cast("zext", value, llvmScalarLayout("i64")) -} - -func normalizeIndexForLength(b *llvmBuilder, indexRef mir.ValueRef, lengthI64 llvmValue) (compareIndex, compareLength, indexI64 llvmValue, ok bool) { - if b == nil || indexRef == nil { - return llvmValue{}, llvmValue{}, llvmValue{}, false - } - indexType := mirRefType(indexRef) - _, indexBits, ok := integerInfoID(b.emitter.mod.Types, indexType) - if !ok { - b.emitter.markInvalid("indexed access lowering requires integral index") - return llvmValue{}, llvmValue{}, llvmValue{}, false - } - compareIndex = emitRef(b, indexRef) - compareLength = lengthI64 - indexI64 = compareIndex - if indexBits < 64 { - u64 := b.emitter.mod.Types.Intern(ir.Type{Kind: ir.TypeInteger, Bits: 64}) - compareIndex = emitCast(b, &mir.Cast{Arg: indexRef, Type: u64}) - indexI64 = compareIndex - } else if indexBits > 64 { - compareLength = b.cast("zext", lengthI64, compareIndex.Layout) - indexI64 = b.cast("trunc", compareIndex, llvmScalarLayout("i64")) - } - return compareIndex, compareLength, indexI64, true -} - -func emitBoundsCheckedIndex(b *llvmBuilder, indexRef mir.ValueRef, length llvmValue) (llvmValue, bool) { - compareIndex, compareLength, index, ok := normalizeIndexForLength(b, indexRef, length) - if !ok { - return llvmValue{}, false - } - // Unsigned comparison also rejects negative signed indexes after sign extension. - outOfBounds := b.compare("icmp", "uge", compareIndex, compareLength) - boundsID := b.nextID - b.nextID++ - failLabel := fmt.Sprintf("bounds_fail_%d", boundsID) - okLabel := fmt.Sprintf("bounds_ok_%d", boundsID) - b.condBranch(outOfBounds, failLabel, okLabel) - b.namedLabel(failLabel) - b.trap() - b.namedLabel(okLabel) - return index, true -} - -func emitSliceBounds(b *llvmBuilder, view *mir.SliceView, lengthI64 llvmValue) (llvmValue, llvmValue, bool) { - i64 := llvmScalarLayout("i64") - startI64 := b.value("0", i64) - endI64 := lengthI64 - var invalid llvmValue - if view.Start != nil { - start, compareLength, normalized, ok := normalizeIndexForLength(b, view.Start, lengthI64) - if !ok { - return llvmValue{}, llvmValue{}, false - } - startI64 = normalized - invalid = b.compare("icmp", "ugt", start, compareLength) - } - if view.End != nil { - end, compareLength, normalized, ok := normalizeIndexForLength(b, view.End, lengthI64) - if !ok { - return llvmValue{}, llvmValue{}, false - } - endI64 = normalized - predicate := "ugt" - if !view.EndExclusive { - predicate = "uge" - } - endInvalid := b.compare("icmp", predicate, end, compareLength) - if invalid.Layout == nil { - invalid = endInvalid - } else { - invalid = b.arithmetic("or", invalid, endInvalid) - } - } - - boundsID := b.nextID - b.nextID++ - failLabel := fmt.Sprintf("slice_bounds_fail_%d", boundsID) - normalizedLabel := fmt.Sprintf("slice_bounds_normalized_%d", boundsID) - readyLabel := fmt.Sprintf("slice_bounds_ready_%d", boundsID) - failEmitted := false - if invalid.Layout != nil { - b.condBranch(invalid, failLabel, normalizedLabel) - b.namedLabel(failLabel) - b.trap() - b.namedLabel(normalizedLabel) - failEmitted = true - } - if view.End != nil && !view.EndExclusive { - endI64 = b.arithmetic("add", endI64, b.value("1", i64)) - } - reversed := b.compare("icmp", "ugt", startI64, endI64) - b.condBranch(reversed, failLabel, readyLabel) - if !failEmitted { - b.namedLabel(failLabel) - b.trap() - } - b.namedLabel(readyLabel) - return startI64, endI64, true -} - -func emitSliceView(b *llvmBuilder, view *mir.SliceView) llvmValue { - if b == nil || view == nil { - return llvmValue{} - } - resultLayout := b.emitter.layout(view.Type) - if view.Source == nil { - return b.zero(resultLayout) - } - sourceTypeID := view.Source.Type - targetTypeID := sourceTypeID - if sourceType, ok := b.emitter.mod.Types.Type(sourceTypeID); ok && sourceType.Kind == ir.TypeReference { - targetTypeID = sourceType.Elem - } - targetType, ok := b.emitter.mod.Types.Type(targetTypeID) - if ok && targetType.Kind == ir.TypeString { - return emitStringSliceView(b, view) - } - if !ok || (targetType.Kind != ir.TypeArray && targetType.Kind != ir.TypeSlice) { - b.emitter.markInvalid("slice view source shape is not lowerable in current compiler stage") - return b.zero(resultLayout) - } - var data, length llvmValue - var fixedArrayPlace llvmPlace - if targetType.Kind == ir.TypeSlice || targetType.Length == "" { - var source llvmValue - if sliceViewUsesPlacePtr(b.emitter.mod.Types, view.Source) { - ptr, ok := emitPlacePtr(b, view.Source) - if !ok { - return b.zero(resultLayout) - } - source = b.load(ptr) - } else { - source = emitRef(b, view.Source.Root) - // Dynamic-owner references are pointers to carrier headers. Slice - // references are already carrier aggregates and must stay unloaded. - if source.Layout.Kind == llvmLayoutPointer { - source = b.load(b.pointerPlace(source)) - } - } - data = b.extractField(source, llvmFieldData) - length = b.extractField(source, llvmFieldLength) - } else { - length = b.value(targetType.Length, llvmScalarLayout("i64")) - if sliceViewUsesPlacePtr(b.emitter.mod.Types, view.Source) { - ptr, found := emitPlacePtr(b, view.Source) - if found { - fixedArrayPlace = ptr - } - } else { - root := emitRef(b, view.Source.Root) - if root.Layout.Kind == llvmLayoutPointer { - fixedArrayPlace = b.pointerPlace(root) - } - } - if fixedArrayPlace.Pointee == nil { - b.emitter.markInvalid("fixed-array slicing requires addressable storage") - return b.zero(resultLayout) - } - } - - indexLayout := b.emitter.layout(b.emitter.mod.Types.IndexType()) - lengthI64 := length - if fixedArrayPlace.Pointee == nil { - lengthI64 = emitTargetIndexAsI64(b, length) - } - startI64, endI64, ok := emitSliceBounds(b, view, lengthI64) - if !ok { - return b.zero(resultLayout) - } - - if fixedArrayPlace.Pointee != nil { - data = b.pointerValue(b.arrayElement(fixedArrayPlace, b.value("0", llvmScalarLayout("i32")), false)) - } - adjustedData := b.pointerValue(b.gep(b.pointerPlace(data), startI64, false)) - viewLength := b.arithmetic("sub", endI64, startI64) - if indexLayout.Text != "i64" { - viewLength = b.cast("trunc", viewLength, indexLayout) - } - result := b.insertField(b.zero(resultLayout), adjustedData, llvmFieldData) - return b.insertField(result, viewLength, llvmFieldLength) -} - -func emitStringDataAndLength(b *llvmBuilder, value llvmValue) (llvmValue, llvmValue) { - return b.extractField(value, llvmFieldData), b.extractField(value, llvmFieldLength) -} - -func emitStringSliceView(b *llvmBuilder, view *mir.SliceView) llvmValue { - if b == nil || view == nil { - return llvmValue{} - } - resultLayout := b.emitter.layout(view.Type) - if view.Source == nil { - return b.zero(resultLayout) - } - _, ok := b.emitter.mod.Types.Type(view.Source.Type) - if !ok { - b.emitter.markInvalid("string slice view has invalid source type") - return b.zero(resultLayout) - } - var source llvmValue - if len(view.Source.Projections) > 0 { - ptr, ok := emitPlacePtr(b, view.Source) - if !ok { - return b.zero(resultLayout) - } - source = b.load(ptr) - } else { - source = emitRef(b, view.Source.Root) - } - data, length := emitStringDataAndLength(b, source) - - indexLayout := b.emitter.layout(b.emitter.mod.Types.IndexType()) - lengthI64 := emitTargetIndexAsI64(b, length) - startI64, endI64, ok := emitSliceBounds(b, view, lengthI64) - if !ok { - return b.zero(resultLayout) - } - - resultType, ok := b.emitter.mod.Types.Type(view.Type) - if !ok || resultType.Kind != ir.TypeReference { - b.emitter.markInvalid("string slice view has invalid result type") - return b.zero(resultLayout) - } - resultTarget, ok := b.emitter.mod.Types.Type(resultType.Elem) - if !ok { - b.emitter.markInvalid("string slice view has invalid result target") - return b.zero(resultLayout) - } - if resultTarget.Kind == ir.TypeString { - var boundaryValid llvmValue - for _, index := range []llvmValue{startI64, endI64} { - boundary := emitUTF8BoundaryCheck(b, data, index, lengthI64) - if boundaryValid.Layout == nil { - boundaryValid = boundary - } else { - boundaryValid = b.arithmetic("and", boundaryValid, boundary) - } - } - boundaryID := b.nextID - b.nextID++ - boundaryFail := fmt.Sprintf("string_boundary_fail_%d", boundaryID) - boundaryReady := fmt.Sprintf("string_boundary_ready_%d", boundaryID) - b.condBranch(boundaryValid, boundaryReady, boundaryFail) - b.namedLabel(boundaryFail) - b.trap() - b.namedLabel(boundaryReady) - } - - adjustedData := b.pointerValue(b.gep(b.pointerPlace(data), startI64, false)) - viewLength := b.arithmetic("sub", endI64, startI64) - if indexLayout.Text != "i64" { - viewLength = b.cast("trunc", viewLength, indexLayout) - } - result := b.insertField(b.zero(resultLayout), adjustedData, llvmFieldData) - return b.insertField(result, viewLength, llvmFieldLength) -} - -func emitUTF8BoundaryCheck(b *llvmBuilder, data, index, length llvmValue) llvmValue { - atEnd := b.compare("icmp", "eq", index, length) - id := b.nextID - b.nextID++ - loadLabel := fmt.Sprintf("utf8_boundary_load_%d", id) - endLabel := fmt.Sprintf("utf8_boundary_end_%d", id) - mergeLabel := fmt.Sprintf("utf8_boundary_merge_%d", id) - b.condBranch(atEnd, endLabel, loadLabel) - b.namedLabel(loadLabel) - value := b.load(b.gep(b.pointerPlace(data), index, false)) - masked := b.arithmetic("and", value, b.value("-64", value.Layout)) - continuation := b.compare("icmp", "eq", masked, b.value("-128", value.Layout)) - notContinuation := b.arithmetic("xor", continuation, b.value("true", continuation.Layout)) - b.branch(mergeLabel) - b.namedLabel(endLabel) - b.branch(mergeLabel) - b.namedLabel(mergeLabel) - return b.phi(llvmScalarLayout("i1"), - llvmIncoming{Value: b.value("true", llvmScalarLayout("i1")), Label: endLabel}, - llvmIncoming{Value: notContinuation, Label: loadLabel}, - ) -} - -func emitStringChars(b *llvmBuilder, chars *mir.StringChars) llvmValue { - if b == nil || chars == nil { - return llvmValue{} - } - resultLayout := b.emitter.layout(chars.Type) - if chars.Value == nil { - return b.zero(resultLayout) - } - refType, ok := b.emitter.mod.Types.Type(mirRefType(chars.Value)) - if !ok || refType.Kind != ir.TypeReference { - b.emitter.markInvalid("string character conversion requires a string reference") - return b.zero(resultLayout) - } - stringType, ok := b.emitter.mod.Types.Type(refType.Elem) - if !ok || stringType.Kind != ir.TypeString { - b.emitter.markInvalid("string character conversion requires a string reference") - return b.zero(resultLayout) - } - arrayType, ok := b.emitter.mod.Types.Type(chars.Type) - if !ok || arrayType.Kind != ir.TypeArray || arrayType.Length != "" || arrayType.Elem == ir.InvalidType { - b.emitter.markInvalid("string character conversion has invalid result type") - return b.zero(resultLayout) - } - if elemType, ok := b.emitter.mod.Types.Type(arrayType.Elem); !ok || elemType.Kind != ir.TypeChar { - b.emitter.markInvalid("string character conversion result must be a char array") - return b.zero(resultLayout) - } - - data, length := emitStringDataAndLength(b, emitRef(b, chars.Value)) - lengthI64 := emitTargetIndexAsI64(b, length) - indexLayout := b.emitter.layout(b.emitter.mod.Types.IndexType()) - count := emitUTF8CodepointCount(b, data, lengthI64) - id := b.nextID - b.nextID++ - countForHeader := count - if indexLayout.Text != "i64" { - tooLarge := b.compare("icmp", "ugt", count, b.value("4294967295", count.Layout)) - trapLabel := fmt.Sprintf("string_chars_length_fail_%d", id) - lengthReady := fmt.Sprintf("string_chars_length_ready_%d", id) - b.condBranch(tooLarge, trapLabel, lengthReady) - b.namedLabel(trapLabel) - b.trap() - b.namedLabel(lengthReady) - countForHeader = b.cast("trunc", count, indexLayout) - } - countValue := countForHeader - allocator := emitDefaultAllocatorHandle(b) - zero := b.compare("icmp", "eq", count, b.value("0", count.Layout)) - emptyLabel := fmt.Sprintf("string_chars_empty_%d", id) - allocateLabel := fmt.Sprintf("string_chars_allocate_%d", id) - readyLabel := fmt.Sprintf("string_chars_ready_%d", id) - b.condBranch(zero, emptyLabel, allocateLabel) - b.namedLabel(emptyLabel) - b.branch(readyLabel) - emptyBlock := b.currentLabel - b.namedLabel(allocateLabel) - allocated := emitDynamicArrayStorageAlloc(b, arrayType.Elem, countValue, allocator) - b.branch(readyLabel) - allocatedBlock := b.currentLabel - b.namedLabel(readyLabel) - charData := b.phi(allocated.Layout, llvmIncoming{Value: b.value("null", allocated.Layout), Label: emptyBlock}, llvmIncoming{Value: allocated, Label: allocatedBlock}) - return emitStringCharsFill(b, data, lengthI64, charData, countValue, chars.Type, allocator) -} - -func emitStringCharsFill(b *llvmBuilder, data, length, charData, count llvmValue, arrayType ir.TypeID, allocator llvmValue) llvmValue { - id := b.nextID - b.nextID++ - entryLabel := b.currentLabel - loopLabel := fmt.Sprintf("string_chars_fill_loop_%d", id) - bodyLabel := fmt.Sprintf("string_chars_fill_body_%d", id) - continueLabel := fmt.Sprintf("string_chars_fill_continue_%d", id) - doneLabel := fmt.Sprintf("string_chars_fill_done_%d", id) - b.branch(loopLabel) - b.namedLabel(loopLabel) - i64 := llvmScalarLayout("i64") - nextByteIndex := b.nextValue(i64) - nextCharIndex := b.nextValue(i64) - byteIndex := b.nextValue(i64) - charIndex := b.nextValue(i64) - b.definePhi(byteIndex, llvmIncoming{Value: b.value("0", i64), Label: entryLabel}, llvmIncoming{Value: nextByteIndex, Label: continueLabel}) - b.definePhi(charIndex, llvmIncoming{Value: b.value("0", i64), Label: entryLabel}, llvmIncoming{Value: nextCharIndex, Label: continueLabel}) - more := b.compare("icmp", "ult", byteIndex, length) - b.condBranch(more, bodyLabel, doneLabel) - b.namedLabel(bodyLabel) - next, codepoint := emitUTF8DecodeStep(b, data, byteIndex, length) - b.store(b.gep(b.pointerPlace(charData), charIndex, false), codepoint) - b.branch(continueLabel) - b.namedLabel(continueLabel) - b.defineArithmetic(nextByteIndex, "add", next, b.value("0", i64)) - b.defineArithmetic(nextCharIndex, "add", charIndex, b.value("1", i64)) - b.branch(loopLabel) - b.namedLabel(doneLabel) - return emitDynamicArrayHeader(b, arrayType, charData, count, count, allocator) -} - -func emitUTF8CodepointCount(b *llvmBuilder, data, length llvmValue) llvmValue { - id := b.nextID - b.nextID++ - entryLabel := b.currentLabel - loopLabel := fmt.Sprintf("utf8_count_loop_%d", id) - bodyLabel := fmt.Sprintf("utf8_count_body_%d", id) - continueLabel := fmt.Sprintf("utf8_count_continue_%d", id) - doneLabel := fmt.Sprintf("utf8_count_done_%d", id) - b.branch(loopLabel) - b.namedLabel(loopLabel) - i64 := llvmScalarLayout("i64") - nextIndex := b.nextValue(i64) - nextCount := b.nextValue(i64) - index := b.nextValue(i64) - count := b.nextValue(i64) - b.definePhi(index, llvmIncoming{Value: b.value("0", i64), Label: entryLabel}, llvmIncoming{Value: nextIndex, Label: continueLabel}) - b.definePhi(count, llvmIncoming{Value: b.value("0", i64), Label: entryLabel}, llvmIncoming{Value: nextCount, Label: continueLabel}) - more := b.compare("icmp", "ult", index, length) - b.condBranch(more, bodyLabel, doneLabel) - b.namedLabel(bodyLabel) - decodedNext, _ := emitUTF8DecodeStep(b, data, index, length) - b.defineArithmetic(nextCount, "add", count, b.value("1", i64)) - b.branch(continueLabel) - b.namedLabel(continueLabel) - b.defineArithmetic(nextIndex, "add", decodedNext, b.value("0", i64)) - b.branch(loopLabel) - b.namedLabel(doneLabel) - return count -} - -func emitUTF8DecodeStep(b *llvmBuilder, data, index, length llvmValue) (llvmValue, llvmValue) { - id := b.nextID - b.nextID++ - invalidLabel := fmt.Sprintf("utf8_decode_invalid_%d", id) - asciiLabel := fmt.Sprintf("utf8_decode_ascii_%d", id) - kindLabel := fmt.Sprintf("utf8_decode_kind_%d", id) - twoLabel := fmt.Sprintf("utf8_decode_two_%d", id) - threeOrFourLabel := fmt.Sprintf("utf8_decode_three_or_four_%d", id) - threeLabel := fmt.Sprintf("utf8_decode_three_%d", id) - fourLabel := fmt.Sprintf("utf8_decode_four_%d", id) - mergeLabel := fmt.Sprintf("utf8_decode_merge_%d", id) - - i64 := llvmScalarLayout("i64") - lead := b.load(b.gep(b.pointerPlace(data), index, false)) - isASCII := b.compare("icmp", "ule", lead, b.value("127", lead.Layout)) - twoLow := b.compare("icmp", "uge", lead, b.value("-62", lead.Layout)) - twoHigh := b.compare("icmp", "ule", lead, b.value("-33", lead.Layout)) - isTwo := b.arithmetic("and", twoLow, twoHigh) - threeLow := b.compare("icmp", "uge", lead, b.value("-32", lead.Layout)) - threeHigh := b.compare("icmp", "ule", lead, b.value("-17", lead.Layout)) - isThree := b.arithmetic("and", threeLow, threeHigh) - fourLow := b.compare("icmp", "uge", lead, b.value("-16", lead.Layout)) - fourHigh := b.compare("icmp", "ule", lead, b.value("-12", lead.Layout)) - isFour := b.arithmetic("and", fourLow, fourHigh) - validTwoThree := b.arithmetic("or", isTwo, isThree) - validLead := b.arithmetic("or", validTwoThree, isFour) - b.condBranch(isASCII, asciiLabel, kindLabel) - b.namedLabel(kindLabel) - b.condBranch(validLead, kindLabel+"_valid", invalidLabel) - b.namedLabel(kindLabel + "_valid") - b.condBranch(isTwo, twoLabel, threeOrFourLabel) - b.namedLabel(threeOrFourLabel) - b.condBranch(isThree, threeLabel, fourLabel) - b.namedLabel(invalidLabel) - b.trap() - - b.namedLabel(asciiLabel) - asciiNext := b.arithmetic("add", index, b.value("1", i64)) - asciiRune := emitUTF8ByteI32(b, lead, 127) - b.branch(mergeLabel) - - b.namedLabel(twoLabel) - emitUTF8WidthCheck(b, index, length, 2, invalidLabel) - secondIndex := b.arithmetic("add", index, b.value("1", i64)) - second := emitUTF8ContinuationByte(b, data, secondIndex, invalidLabel) - twoNext := b.arithmetic("add", index, b.value("2", i64)) - twoRuneLead := emitUTF8ByteI32(b, lead, 31) - twoRuneSecond := emitUTF8ByteI32(b, second, 63) - twoShifted := b.arithmetic("shl", twoRuneLead, b.value("6", twoRuneLead.Layout)) - twoRune := b.arithmetic("or", twoShifted, twoRuneSecond) - twoPred := b.currentLabel - b.branch(mergeLabel) - - b.namedLabel(threeLabel) - emitUTF8WidthCheck(b, index, length, 3, invalidLabel) - threeSecondIndex := b.arithmetic("add", index, b.value("1", i64)) - threeSecond := emitUTF8ContinuationByte(b, data, threeSecondIndex, invalidLabel) - e0 := b.compare("icmp", "eq", lead, b.value("-32", lead.Layout)) - ed := b.compare("icmp", "eq", lead, b.value("-19", lead.Layout)) - notE0 := b.arithmetic("xor", e0, b.value("true", e0.Layout)) - notED := b.arithmetic("xor", ed, b.value("true", ed.Layout)) - e0OK := b.compare("icmp", "uge", threeSecond, b.value("-96", threeSecond.Layout)) - edOK := b.compare("icmp", "ule", threeSecond, b.value("-97", threeSecond.Layout)) - lowOK := b.arithmetic("or", notE0, e0OK) - highOK := b.arithmetic("or", notED, edOK) - threeSecondOK := b.arithmetic("and", lowOK, highOK) - threeReady := fmt.Sprintf("utf8_decode_three_ready_%d", id) - b.condBranch(threeSecondOK, threeReady, invalidLabel) - b.namedLabel(threeReady) - threeThirdIndex := b.arithmetic("add", index, b.value("2", i64)) - threeThird := emitUTF8ContinuationByte(b, data, threeThirdIndex, invalidLabel) - threeNext := b.arithmetic("add", index, b.value("3", i64)) - threeLeadRune := emitUTF8ByteI32(b, lead, 15) - threeSecondRune := emitUTF8ByteI32(b, threeSecond, 63) - threeThirdRune := emitUTF8ByteI32(b, threeThird, 63) - threeLeadShift := b.arithmetic("shl", threeLeadRune, b.value("12", threeLeadRune.Layout)) - threeSecondShift := b.arithmetic("shl", threeSecondRune, b.value("6", threeSecondRune.Layout)) - threeFirstCombine := b.arithmetic("or", threeLeadShift, threeSecondShift) - threeRune := b.arithmetic("or", threeFirstCombine, threeThirdRune) - threePred := b.currentLabel - b.branch(mergeLabel) - - b.namedLabel(fourLabel) - emitUTF8WidthCheck(b, index, length, 4, invalidLabel) - fourSecondIndex := b.arithmetic("add", index, b.value("1", i64)) - fourSecond := emitUTF8ContinuationByte(b, data, fourSecondIndex, invalidLabel) - f0 := b.compare("icmp", "eq", lead, b.value("-16", lead.Layout)) - f4 := b.compare("icmp", "eq", lead, b.value("-12", lead.Layout)) - notF0 := b.arithmetic("xor", f0, b.value("true", f0.Layout)) - notF4 := b.arithmetic("xor", f4, b.value("true", f4.Layout)) - f0OK := b.compare("icmp", "uge", fourSecond, b.value("-112", fourSecond.Layout)) - f4OK := b.compare("icmp", "ule", fourSecond, b.value("-113", fourSecond.Layout)) - fourLowOK := b.arithmetic("or", notF0, f0OK) - fourHighOK := b.arithmetic("or", notF4, f4OK) - fourSecondOK := b.arithmetic("and", fourLowOK, fourHighOK) - fourReady := fmt.Sprintf("utf8_decode_four_ready_%d", id) - b.condBranch(fourSecondOK, fourReady, invalidLabel) - b.namedLabel(fourReady) - fourThirdIndex := b.arithmetic("add", index, b.value("2", i64)) - fourThird := emitUTF8ContinuationByte(b, data, fourThirdIndex, invalidLabel) - fourFourthIndex := b.arithmetic("add", index, b.value("3", i64)) - fourFourth := emitUTF8ContinuationByte(b, data, fourFourthIndex, invalidLabel) - fourNext := b.arithmetic("add", index, b.value("4", i64)) - fourLeadRune := emitUTF8ByteI32(b, lead, 7) - fourSecondRune := emitUTF8ByteI32(b, fourSecond, 63) - fourThirdRune := emitUTF8ByteI32(b, fourThird, 63) - fourFourthRune := emitUTF8ByteI32(b, fourFourth, 63) - fourLeadShift := b.arithmetic("shl", fourLeadRune, b.value("18", fourLeadRune.Layout)) - fourSecondShift := b.arithmetic("shl", fourSecondRune, b.value("12", fourSecondRune.Layout)) - fourThirdShift := b.arithmetic("shl", fourThirdRune, b.value("6", fourThirdRune.Layout)) - fourFirstCombine := b.arithmetic("or", fourLeadShift, fourSecondShift) - fourSecondCombine := b.arithmetic("or", fourFirstCombine, fourThirdShift) - fourRune := b.arithmetic("or", fourSecondCombine, fourFourthRune) - fourPred := b.currentLabel - b.branch(mergeLabel) - - b.namedLabel(mergeLabel) - next := b.phi(i64, llvmIncoming{Value: asciiNext, Label: asciiLabel}, llvmIncoming{Value: twoNext, Label: twoPred}, llvmIncoming{Value: threeNext, Label: threePred}, llvmIncoming{Value: fourNext, Label: fourPred}) - runeValue := b.phi(llvmScalarLayout("i32"), llvmIncoming{Value: asciiRune, Label: asciiLabel}, llvmIncoming{Value: twoRune, Label: twoPred}, llvmIncoming{Value: threeRune, Label: threePred}, llvmIncoming{Value: fourRune, Label: fourPred}) - return next, runeValue -} - -func emitUTF8WidthCheck(b *llvmBuilder, index, length llvmValue, width int, invalidLabel string) { - remaining := b.arithmetic("sub", length, index) - enough := b.compare("icmp", "uge", remaining, b.value(strconv.Itoa(width), remaining.Layout)) - id := b.nextID - b.nextID++ - readyLabel := fmt.Sprintf("utf8_width_ready_%d", id) - b.condBranch(enough, readyLabel, invalidLabel) - b.namedLabel(readyLabel) -} - -func emitUTF8ContinuationByte(b *llvmBuilder, data, index llvmValue, invalidLabel string) llvmValue { - value := b.load(b.gep(b.pointerPlace(data), index, false)) - low := b.compare("icmp", "uge", value, b.value("-128", value.Layout)) - high := b.compare("icmp", "ule", value, b.value("-65", value.Layout)) - valid := b.arithmetic("and", low, high) - id := b.nextID - b.nextID++ - readyLabel := fmt.Sprintf("utf8_continuation_ready_%d", id) - b.condBranch(valid, readyLabel, invalidLabel) - b.namedLabel(readyLabel) - return value -} - -func emitUTF8ByteI32(b *llvmBuilder, value llvmValue, mask int) llvmValue { - wide := b.cast("zext", value, llvmScalarLayout("i32")) - return b.arithmetic("and", wide, b.value(strconv.Itoa(mask), wide.Layout)) -} - -func emitLen(b *llvmBuilder, value mir.ValueRef) llvmValue { - if b == nil || value == nil { - return llvmValue{} - } - indexLayout := b.emitter.layout(b.emitter.mod.Types.IndexType()) - refType, ok := b.emitter.mod.Types.Type(mirRefType(value)) - if !ok || refType.Kind != ir.TypeReference { - b.emitter.markInvalid("len requires a reference value") - return b.value("0", indexLayout) - } - target, ok := b.emitter.mod.Types.Type(refType.Elem) - if !ok { - b.emitter.markInvalid("len has invalid reference target") - return b.value("0", indexLayout) - } - switch target.Kind { - case ir.TypeString: - return b.extractField(emitRef(b, value), llvmFieldLength) - case ir.TypeArray: - if target.Length != "" { - if _, err := strconv.ParseUint(target.Length, 10, 64); err != nil { - b.emitter.markInvalid("fixed array has invalid length") - return b.value("0", indexLayout) - } - return b.value(target.Length, indexLayout) - } - ownerRef := emitRef(b, value) - return b.extractField(b.load(b.pointerPlace(ownerRef)), llvmFieldLength) - case ir.TypeSlice: - return b.extractField(emitRef(b, value), llvmFieldLength) - default: - b.emitter.markInvalid("len requires a string or array reference") - return b.value("0", indexLayout) - } -} - -func sliceViewUsesPlacePtr(types *ir.TypeTable, source *mir.Place) bool { - if source == nil { - return false - } - if len(source.Projections) > 0 { - return true - } - if typ, ok := types.Type(source.Type); ok && typ.Kind == ir.TypeReference { - return false - } - typ, ok := types.Type(source.Type) - return ok && typ.Kind == ir.TypeArray && typ.Length != "" -} - -func emitDynamicArrayAlloc(b *llvmBuilder, alloc *mir.DynamicArrayAlloc) llvmValue { - if b == nil || alloc == nil { - return llvmValue{} - } - if alloc.Length < 0 { - b.emitter.markInvalid("dynamic array allocation has negative length") - return b.zero(b.emitter.layout(alloc.Type)) - } - arrayType, ok := b.emitter.mod.Types.Type(alloc.Type) - if !ok || arrayType.Kind != ir.TypeArray || arrayType.Length != "" { - b.emitter.markInvalid("dynamic array allocation has invalid type") - return b.zero(b.emitter.layout(alloc.Type)) - } - allocator := allocatorHandleFromRef(b, alloc.Allocator) - indexLayout := b.emitter.layout(b.emitter.mod.Types.IndexType()) - length := b.value(strconv.Itoa(alloc.Length), indexLayout) - if alloc.Length == 0 { - dataLayout := llvmPointerLayout(b.emitter.layout(arrayType.Elem)) - return emitDynamicArrayHeader(b, alloc.Type, b.value("null", dataLayout), length, length, allocator) - } - data := emitDynamicArrayStorageAlloc(b, arrayType.Elem, length, allocator) - return emitDynamicArrayHeader(b, alloc.Type, data, length, length, allocator) -} - -func emitDynamicArrayStorageAlloc(b *llvmBuilder, elemType ir.TypeID, capacity, allocator llvmValue) llvmValue { - size := emitAllocatorStorageSize(b, elemType, capacity) - raw := emitAllocatorAllocate(b, allocator, size, b.value("8", llvmScalarLayout("i32"))) - missing := b.compare("icmp", "eq", raw, b.value("null", raw.Layout)) - id := b.nextID - b.nextID++ - failLabel := fmt.Sprintf("array_alloc_fail_%d", id) - readyLabel := fmt.Sprintf("array_alloc_ready_%d", id) - b.condBranch(missing, failLabel, readyLabel) - b.namedLabel(failLabel) - b.trap() - b.namedLabel(readyLabel) - return b.bitcast(raw, llvmPointerLayout(b.emitter.layout(elemType))) -} - -func emitDynamicArrayHeader(b *llvmBuilder, arrayTypeID ir.TypeID, data, length, capacity, allocator llvmValue) llvmValue { - header := b.zero(b.emitter.layout(arrayTypeID)) - header = b.insertField(header, data, llvmFieldData) - header = b.insertField(header, length, llvmFieldLength) - header = b.insertField(header, capacity, llvmFieldCapacity) - return b.insertField(header, allocator, llvmFieldAllocator) -} - -func emitAlloc(b *llvmBuilder, e *mir.Alloc) llvmValue { - pointerType, ok := b.emitter.mod.Types.Type(e.Type) - if !ok || pointerType.Kind != ir.TypeOwnedPtr { - b.emitter.markInvalid("alloc has invalid result type") - return b.zero(b.emitter.layout(e.Type)) - } - allocReg := allocatorHandleFromRef(b, e.Allocator) - targetLayout := b.emitter.layout(pointerType.Elem) - sizeLayout := b.emitter.layout(b.emitter.mod.Types.IndexType()) - payloadEnd := b.value(fmt.Sprintf("getelementptr (%s, %s* null, i32 1)", targetLayout.Text, targetLayout.Text), llvmPointerLayout(targetLayout)) - size := b.cast("ptrtoint", payloadEnd, sizeLayout) - zeroSize := b.compare("icmp", "eq", size, b.value("0", sizeLayout)) - normSize := b.selectValue(zeroSize, b.value("1", sizeLayout), size) - raw := emitAllocatorAllocate(b, allocReg, normSize, b.value("8", llvmScalarLayout("i32"))) - isNull := b.compare("icmp", "eq", raw, b.value("null", raw.Layout)) - id := b.nextID - b.nextID++ - failLabel := fmt.Sprintf("alloc_fail_%d", id) - doneLabel := fmt.Sprintf("alloc_done_%d", id) - b.condBranch(isNull, failLabel, doneLabel) - b.namedLabel(failLabel) - b.trap() - b.namedLabel(doneLabel) - - dataPtr := b.bitcast(raw, llvmPointerLayout(targetLayout)) - b.store(b.pointerPlace(dataPtr), emitRef(b, e.Value)) - carrier := b.insertField(b.zero(b.emitter.layout(e.Type)), dataPtr, llvmFieldData) - return b.insertField(carrier, allocReg, llvmFieldAllocator) -} - -func emitDynamicArrayReserve(b *llvmBuilder, array llvmValue, typeID ir.TypeID, minimum llvmValue) llvmValue { - elemTypeID, ok := dynamicArrayElementType(b.emitter.mod.Types, typeID) - if !ok { - b.emitter.markInvalid("dynamic array reserve has invalid type") - return b.zero(b.emitter.layout(typeID)) - } - oldData := b.extractField(array, llvmFieldData) - length := b.extractField(array, llvmFieldLength) - capacity := b.extractField(array, llvmFieldCapacity) - allocator := b.extractField(array, llvmFieldAllocator) - sufficient := b.compare("icmp", "uge", capacity, minimum) - id := b.nextID - b.nextID++ - reuseLabel := fmt.Sprintf("array_reserve_reuse_%d", id) - growLabel := fmt.Sprintf("array_reserve_grow_%d", id) - loopLabel := fmt.Sprintf("array_relocate_loop_%d", id) - bodyLabel := fmt.Sprintf("array_relocate_body_%d", id) - continueLabel := fmt.Sprintf("array_relocate_continue_%d", id) - doneLabel := fmt.Sprintf("array_relocate_done_%d", id) - mergeLabel := fmt.Sprintf("array_reserve_done_%d", id) - b.condBranch(sufficient, reuseLabel, growLabel) - b.namedLabel(reuseLabel) - b.branch(mergeLabel) - b.namedLabel(growLabel) - newData := emitDynamicArrayStorageAlloc(b, elemTypeID, minimum, allocator) - relocateEntry := b.currentLabel - b.branch(loopLabel) - b.namedLabel(loopLabel) - nextIndex := b.nextValue(length.Layout) - index := b.phi(length.Layout, llvmIncoming{Value: b.value("0", length.Layout), Label: relocateEntry}, llvmIncoming{Value: nextIndex, Label: continueLabel}) - more := b.compare("icmp", "ult", index, length) - b.condBranch(more, bodyLabel, doneLabel) - b.namedLabel(bodyLabel) - item := b.load(b.gep(b.pointerPlace(oldData), index, false)) - b.store(b.gep(b.pointerPlace(newData), index, false), item) - b.branch(continueLabel) - b.namedLabel(continueLabel) - b.defineArithmetic(nextIndex, "add", index, b.value("1", index.Layout)) - b.branch(loopLabel) - b.namedLabel(doneLabel) - oldIsNull := b.compare("icmp", "eq", oldData, b.value("null", oldData.Layout)) - releaseLabel := fmt.Sprintf("array_reserve_release_%d", id) - releaseDoneLabel := fmt.Sprintf("array_reserve_release_done_%d", id) - b.condBranch(oldIsNull, releaseDoneLabel, releaseLabel) - b.namedLabel(releaseLabel) - oldSize := emitAllocatorStorageSize(b, elemTypeID, capacity) - oldRaw := b.bitcast(oldData, llvmPointerLayout(llvmScalarLayout("i8"))) - emitAllocatorDeallocate(b, allocator, oldRaw, oldSize, b.value("8", llvmScalarLayout("i32"))) - b.branch(releaseDoneLabel) - b.namedLabel(releaseDoneLabel) - resized := emitDynamicArrayHeader(b, typeID, newData, length, minimum, allocator) - b.branch(mergeLabel) - b.namedLabel(mergeLabel) - return b.phi(array.Layout, llvmIncoming{Value: array, Label: reuseLabel}, llvmIncoming{Value: resized, Label: releaseDoneLabel}) -} - -func emitDynamicArrayOp(b *llvmBuilder, op *mir.DynamicArrayOp) { - if b == nil || op == nil || op.Array == nil { - return - } - elemTypeID, ok := dynamicArrayElementType(b.emitter.mod.Types, op.ArrayType) - if !ok { - b.emitter.markInvalid("dynamic array operation has invalid type") - return - } - arrayPlace := b.pointerPlace(emitRef(b, op.Array)) - array := b.load(arrayPlace) - var updated llvmValue - switch op.Op { - case symbols.CompilerOpReserve: - if op.Length == nil { - b.emitter.markInvalid("reserve requires a minimum capacity") - return - } - minimum := emitCast(b, &mir.Cast{Arg: op.Length, Type: b.emitter.mod.Types.IndexType()}) - updated = emitDynamicArrayReserve(b, array, op.ArrayType, minimum) - case symbols.CompilerOpAppend: - updated = emitDynamicArrayAppend(b, op, array) - case symbols.CompilerOpResize: - updated = emitDynamicArrayResize(b, op, array) - case symbols.CompilerOpShrink: - updated = emitDynamicArrayShrink(b, op, array, elemTypeID) - default: - b.emitter.markInvalid("unknown dynamic array operation " + string(op.Op)) - return - } - b.store(arrayPlace, updated) -} - -func emitDynamicArrayShrink(b *llvmBuilder, op *mir.DynamicArrayOp, array llvmValue, elemTypeID ir.TypeID) llvmValue { - if op.Length == nil { - b.emitter.markInvalid("shrink requires a length") - return array - } - data := b.extractField(array, llvmFieldData) - oldLength := b.extractField(array, llvmFieldLength) - capacity := b.extractField(array, llvmFieldCapacity) - allocator := b.extractField(array, llvmFieldAllocator) - newLength := emitCast(b, &mir.Cast{Arg: op.Length, Type: b.emitter.mod.Types.IndexType()}) - shorter := b.compare("icmp", "ult", newLength, oldLength) - id := b.nextID - b.nextID++ - keepLabel := fmt.Sprintf("array_shrink_keep_%d", id) - shrinkLabel := fmt.Sprintf("array_shrink_drop_%d", id) - doneLabel := fmt.Sprintf("array_shrink_done_%d", id) - b.condBranch(shorter, shrinkLabel, keepLabel) - b.namedLabel(keepLabel) - b.branch(doneLabel) - b.namedLabel(shrinkLabel) - emitDynamicArrayElementRangeDrop(b, data, elemTypeID, newLength, oldLength) - shrunk := emitDynamicArrayHeader(b, op.ArrayType, data, newLength, capacity, allocator) - shrinkDoneLabel := b.currentLabel - b.branch(doneLabel) - b.namedLabel(doneLabel) - return b.phi(array.Layout, llvmIncoming{Value: array, Label: keepLabel}, llvmIncoming{Value: shrunk, Label: shrinkDoneLabel}) -} - -func emitDynamicArrayAppend(b *llvmBuilder, op *mir.DynamicArrayOp, array llvmValue) llvmValue { - if op.Value == nil { - b.emitter.markInvalid("append requires a value") - return array - } - length := b.extractField(array, llvmFieldLength) - capacity := b.extractField(array, llvmFieldCapacity) - newLength := b.arithmetic("add", length, b.value("1", length.Layout)) - overflow := b.compare("icmp", "ult", newLength, length) - id := b.nextID - b.nextID++ - failLabel := fmt.Sprintf("array_append_fail_%d", id) - capacityLabel := fmt.Sprintf("array_append_capacity_%d", id) - keepLabel := fmt.Sprintf("array_append_keep_%d", id) - growLabel := fmt.Sprintf("array_append_grow_%d", id) - growReadyLabel := fmt.Sprintf("array_append_grow_ready_%d", id) - readyLabel := fmt.Sprintf("array_append_ready_%d", id) - b.condBranch(overflow, failLabel, capacityLabel) - b.namedLabel(failLabel) - b.trap() - b.namedLabel(capacityLabel) - hasSpace := b.compare("icmp", "ult", length, capacity) - b.condBranch(hasSpace, keepLabel, growLabel) - b.namedLabel(keepLabel) - b.branch(readyLabel) - b.namedLabel(growLabel) - overflowLayout := llvmAggregateLayout([]*llvmLayout{capacity.Layout, llvmScalarLayout("i1")}, nil) - overflowFn := b.value("@llvm.umul.with.overflow."+capacity.Layout.Text, llvmFunctionLayout(overflowLayout, []*llvmLayout{capacity.Layout, capacity.Layout})) - doubledAndOverflow := b.call(overflowFn, []llvmValue{capacity, b.value("2", capacity.Layout)}) - doubled := b.extractIndex(doubledAndOverflow, 0) - doubleOverflow := b.extractIndex(doubledAndOverflow, 1) - b.condBranch(doubleOverflow, failLabel, growReadyLabel) - b.namedLabel(growReadyLabel) - tooSmall := b.compare("icmp", "ult", doubled, newLength) - grownCapacity := b.selectValue(tooSmall, newLength, doubled) - b.branch(readyLabel) - b.namedLabel(readyLabel) - desiredCapacity := b.phi(capacity.Layout, llvmIncoming{Value: capacity, Label: keepLabel}, llvmIncoming{Value: grownCapacity, Label: growReadyLabel}) - reserved := emitDynamicArrayReserve(b, array, op.ArrayType, desiredCapacity) - data := b.extractField(reserved, llvmFieldData) - finalCapacity := b.extractField(reserved, llvmFieldCapacity) - b.store(b.gep(b.pointerPlace(data), length, false), emitRef(b, op.Value)) - allocator := b.extractField(reserved, llvmFieldAllocator) - return emitDynamicArrayHeader(b, op.ArrayType, data, newLength, finalCapacity, allocator) -} - -func emitDynamicArrayResize(b *llvmBuilder, op *mir.DynamicArrayOp, array llvmValue) llvmValue { - if op.Length == nil || op.Value == nil { - b.emitter.markInvalid("resize requires a length and fill value") - return array - } - oldLength := b.extractField(array, llvmFieldLength) - newLength := emitCast(b, &mir.Cast{Arg: op.Length, Type: b.emitter.mod.Types.IndexType()}) - resized := emitDynamicArrayReserve(b, array, op.ArrayType, newLength) - data := b.extractField(resized, llvmFieldData) - capacity := b.extractField(resized, llvmFieldCapacity) - allocator := b.extractField(resized, llvmFieldAllocator) - id := b.nextID - b.nextID++ - entryLabel := b.currentLabel - loopLabel := fmt.Sprintf("array_resize_loop_%d", id) - bodyLabel := fmt.Sprintf("array_resize_body_%d", id) - continueLabel := fmt.Sprintf("array_resize_continue_%d", id) - doneLabel := fmt.Sprintf("array_resize_done_%d", id) - b.branch(loopLabel) - b.namedLabel(loopLabel) - nextIndex := b.nextValue(oldLength.Layout) - index := b.phi(oldLength.Layout, llvmIncoming{Value: oldLength, Label: entryLabel}, llvmIncoming{Value: nextIndex, Label: continueLabel}) - more := b.compare("icmp", "ult", index, newLength) - b.condBranch(more, bodyLabel, doneLabel) - b.namedLabel(bodyLabel) - b.store(b.gep(b.pointerPlace(data), index, false), emitRef(b, op.Value)) - b.branch(continueLabel) - b.namedLabel(continueLabel) - b.defineArithmetic(nextIndex, "add", index, b.value("1", index.Layout)) - b.branch(loopLabel) - b.namedLabel(doneLabel) - return emitDynamicArrayHeader(b, op.ArrayType, data, newLength, capacity, allocator) -} - -func emitIndexPtr(b *llvmBuilder, base llvmValue, baseType ir.TypeID, addressed bool, indexRef mir.ValueRef) (llvmPlace, bool) { - if b == nil || base.Layout == nil || baseType == ir.InvalidType || indexRef == nil { - return llvmPlace{}, false - } - targetID := baseType - pointed := false - referenced := false - if typ, ok := b.emitter.mod.Types.Type(targetID); ok { - switch typ.Kind { - case ir.TypeOwnedPtr: - targetID, pointed = typ.Elem, true - case ir.TypeReference: - targetID, referenced = typ.Elem, true - } - } - target, ok := b.emitter.mod.Types.Type(targetID) - if !ok || (target.Kind != ir.TypeArray && target.Kind != ir.TypeSlice) { - return llvmPlace{}, false - } - if target.Kind == ir.TypeSlice || target.Length == "" { - header := base - // Dynamic-owner references lower as pointers to their carrier header; - // slice references lower as the carrier aggregate itself. - if addressed || pointed || referenced && base.Layout.Kind == llvmLayoutPointer { - header = b.load(b.pointerPlace(base)) - } - data := b.extractField(header, llvmFieldData) - length := b.extractField(header, llvmFieldLength) - index, ok := emitBoundsCheckedIndex(b, indexRef, emitTargetIndexAsI64(b, length)) - if !ok { - return llvmPlace{}, false - } - return b.gep(b.pointerPlace(data), index, false), true - } - length, lengthErr := strconv.Atoi(target.Length) - var index llvmValue - if indexConst, constant := indexRef.(*mir.RefConst); constant { - parsedIndex, indexErr := strconv.Atoi(indexConst.Value) - if lengthErr != nil || indexErr != nil || parsedIndex < 0 || parsedIndex >= length { - b.emitter.invalid = true - if b.emitter.diag != nil { - b.emitter.diag.Add(problems.ArrayIndexOutOfBounds(indexConst.Value, target.Length, nil)) - } - return llvmPlace{}, false - } - index = emitRef(b, indexRef) - } else { - if lengthErr != nil { - return llvmPlace{}, false - } - index, ok = emitBoundsCheckedIndex(b, indexRef, b.value(target.Length, llvmScalarLayout("i64"))) - if !ok { - return llvmPlace{}, false - } - } - if !addressed && !pointed && !referenced { - b.emitter.markInvalid("fixed-array index place requires addressable storage") - return llvmPlace{}, false - } - arrayPlace := b.pointerPlace(base) - return b.arrayElement(arrayPlace, index, true), true -} - -// Directly addressed roots need entry-block storage so one pointer dominates every place use. -func placeNeedsRootAddr(types *ir.TypeTable, place *mir.Place) bool { - if place == nil || place.Root == nil || len(place.Projections) == 0 { - return place != nil && place.Root != nil - } - projection := place.Projections[0] - switch projection.Kind { - case mir.PlaceProjectionDeref: - return false - case mir.PlaceProjectionField: - return true - case mir.PlaceProjectionIndex: - rootType, ok := types.Type(mirRefType(place.Root)) - if !ok { - return false - } - if rootType.Kind == ir.TypeOwnedPtr || rootType.Kind == ir.TypeReference { - return false - } - return rootType.Kind == ir.TypeArray && rootType.Length != "" - default: - return false - } -} - -func emitPlaceRootAddr(b *llvmBuilder, root mir.ValueRef) (llvmPlace, bool) { - if b == nil || root == nil { - return llvmPlace{}, false - } - if ref, ok := root.(*mir.RefName); ok && ref != nil { - if ptr, found := ensureLocalAddr(b, ref); found { - return ptr, true - } - } - value := emitRef(b, root) - ptr := b.alloca(value.Layout) - b.store(ptr, value) - return ptr, true -} - -func emitPlacePtr(b *llvmBuilder, place *mir.Place) (llvmPlace, bool) { - if b == nil || place == nil || place.Root == nil { - return llvmPlace{}, false - } - previousLocation := b.debugLocationID - defer func() { b.debugLocationID = previousLocation }() - - addressed := placeNeedsRootAddr(b.emitter.mod.Types, place) - current := llvmPlace{} - hasCurrent := false - if addressed { - current, hasCurrent = emitPlaceRootAddr(b, place.Root) - } - currentType := mirRefType(place.Root) - for _, projection := range place.Projections { - b.setLocation(projection.Location) - switch projection.Kind { - case mir.PlaceProjectionDeref: - var value llvmValue - if hasCurrent { - value = b.load(current) - } else { - value = emitRef(b, place.Root) - } - if !isOwnedInterfaceType(b.emitter.mod.Types, currentType) { - if typ, ok := b.emitter.mod.Types.Type(currentType); ok && typ.Kind == ir.TypeOwnedPtr { - value = b.extractField(value, llvmFieldData) - } - } - current = b.pointerPlace(value) - hasCurrent = true - addressed = true - case mir.PlaceProjectionField: - if !hasCurrent { - b.emitter.markInvalid("field place requires addressable storage") - return llvmPlace{}, false - } - current = b.fieldPlace(current, projection.FieldIndex) - case mir.PlaceProjectionIndex: - base := emitRef(b, place.Root) - if hasCurrent { - base = b.pointerValue(current) - } - var ok bool - current, ok = emitIndexPtr(b, base, currentType, addressed, projection.Index) - if !ok { - return llvmPlace{}, false - } - hasCurrent = true - addressed = true - default: - b.emitter.markInvalid(fmt.Sprintf("unsupported MIR place projection %d", projection.Kind)) - return llvmPlace{}, false - } - currentType = projection.Type - } - if addressed && hasCurrent { - return current, true - } - b.setLocation(place.Location) - return emitPlaceRootAddr(b, place.Root) -} - -type llvmBuilder struct { - out *strings.Builder - nextID int - locals map[string]llvmValue - localPtrs map[string]llvmPlace - emitter *llvmEmitter - debug *llvmDebugEmitter - debugScopeID int - debugLocationID int - currentLabel string -} - -func emitCast(b *llvmBuilder, cast *mir.Cast) llvmValue { - if b == nil || cast == nil || cast.Arg == nil { - if b != nil { - b.invariant("cast requires MIR argument") - } - return llvmValue{} - } - - argRef := emitRef(b, cast.Arg) - fromType := mirRefType(cast.Arg) - toType := cast.Type - from, fromOK := b.emitter.mod.Types.Type(fromType) - to, toOK := b.emitter.mod.Types.Type(toType) - if !fromOK || !toOK { - return argRef - } - toLayout := b.emitter.layout(toType) - if toLayout == nil { - b.invariant("cast target has no LLVM layout") - } - - if fromType == toType { - return argRef - } - if to.Kind == ir.TypeBool { - if from.Kind == ir.TypeFloat { - return b.compare("fcmp", "one", argRef, b.value("0.0", argRef.Layout)) - } - if _, _, ok := integerInfoID(b.emitter.mod.Types, fromType); ok { - return b.compare("icmp", "ne", argRef, b.value("0", argRef.Layout)) - } - return argRef - } - - if toSigned, _, ok := integerInfoID(b.emitter.mod.Types, toType); from.Kind == ir.TypeFloat && ok { - if toSigned { - return b.cast("fptosi", argRef, toLayout) - } - return b.cast("fptoui", argRef, toLayout) - } else if fromSigned, _, ok := integerInfoID(b.emitter.mod.Types, fromType); ok && to.Kind == ir.TypeFloat { - if fromSigned { - return b.cast("sitofp", argRef, toLayout) - } - return b.cast("uitofp", argRef, toLayout) - } else if from.Kind == ir.TypeFloat && to.Kind == ir.TypeFloat { - if from.Bits == 64 && to.Bits == 32 { - return b.cast("fptrunc", argRef, toLayout) - } else if from.Bits == 32 && to.Bits == 64 { - return b.cast("fpext", argRef, toLayout) - } - return argRef - } else if fromSigned, fromBits, ok := integerInfoID(b.emitter.mod.Types, fromType); ok { - _, toBits, ok := integerInfoID(b.emitter.mod.Types, toType) - if !ok { - return argRef - } - if fromBits < toBits { - if fromSigned { - return b.cast("sext", argRef, toLayout) - } - return b.cast("zext", argRef, toLayout) - } else if fromBits > toBits { - return b.cast("trunc", argRef, toLayout) - } - return argRef - } - 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 { - debug = emitter.debug - } - return &llvmBuilder{ - out: out, - nextID: 1, - locals: make(map[string]llvmValue), - localPtrs: make(map[string]llvmPlace), - emitter: emitter, - debug: debug, - debugScopeID: debugScopeID, - debugLocationID: -1, - } -} - -func (b *llvmBuilder) nextReg() string { - name := fmt.Sprintf("%%t%d", b.nextID) - b.nextID++ - return name -} - -func (b *llvmBuilder) line(text string) { - b.out.WriteString(" ") - b.out.WriteString(text) - if b.debugLocationID >= 0 { - fmt.Fprintf(b.out, ", !dbg !%d", b.debugLocationID) - } - b.out.WriteString("\n") -} - -func (b *llvmBuilder) label(id int) { - b.namedLabel(fmt.Sprintf("b%d", id)) -} - -func (b *llvmBuilder) namedLabel(name string) { - fmt.Fprintf(b.out, "%s:\n", name) - b.currentLabel = name -} - -func (b *llvmBuilder) setLocation(loc *source.Location) { - if b == nil { - return - } - if b.debug == nil { - b.debugLocationID = -1 - return - } - b.debugLocationID = b.debug.locationID(loc, b.debugScopeID) -} - -func withLLVMLocation[T any](b *llvmBuilder, loc *source.Location, emit func() T) T { - if b == nil || emit == nil { - var zero T - return zero - } - prev := b.debugLocationID - b.setLocation(loc) - out := emit() - b.debugLocationID = prev - return out -} - -func emitValueExpr(b *llvmBuilder, expr mir.ValueExpr) llvmValue { - return withLLVMLocation(b, mir.ValueExprLocation(expr), func() llvmValue { - switch e := expr.(type) { - case *mir.Move: - return emitRef(b, e.Src) - case *mir.Len: - return emitLen(b, e.Value) - case *mir.StringLiteral: - layout := b.emitter.layout(e.Type) - rawPointer := layout.Elements[layout.Fields[llvmFieldData]] - dataType := fmt.Sprintf("[%d x i8]", e.Length+1) - data := b.value(fmt.Sprintf("getelementptr inbounds (%s, %s* %s, i64 0, i64 0)", dataType, dataType, e.Name), rawPointer) - value := b.insertField(b.zero(layout), data, llvmFieldData) - value = b.insertField(value, b.value(strconv.Itoa(e.Length), layout.Elements[layout.Fields[llvmFieldLength]]), llvmFieldLength) - return b.insertField(value, b.value("null", rawPointer), llvmFieldAllocator) - case *mir.Cast: - return emitCast(b, e) - case *mir.Unary: - arg := emitRef(b, e.Arg) - switch e.Op { - case "-": - if isFloatType(b.emitter.mod.Types, e.Type) { - return b.arithmetic("fsub", b.value("0.0", arg.Layout), arg) - } - return b.arithmetic("sub", b.value("0", arg.Layout), arg) - case "!": - return emitLogicalNot(b, arg, e.Arg) - case "~": - return b.arithmetic("xor", arg, b.value("-1", arg.Layout)) - default: - return arg - } - case *mir.Binary: - left := emitRef(b, e.Left) - right := emitRef(b, e.Right) - opcode := "" - switch e.Op { - case "+": - if isFloatType(b.emitter.mod.Types, mirRefType(e.Left)) { - opcode = "fadd" - } else { - opcode = "add" - } - case "-": - if isFloatType(b.emitter.mod.Types, mirRefType(e.Left)) { - opcode = "fsub" - } else { - opcode = "sub" - } - case "*": - if isFloatType(b.emitter.mod.Types, mirRefType(e.Left)) { - opcode = "fmul" - } else { - opcode = "mul" - } - case "/": - if isFloatType(b.emitter.mod.Types, mirRefType(e.Left)) { - opcode = "fdiv" - } else if isUnsignedTypeID(b.emitter.mod.Types, mirRefType(e.Left)) { - opcode = "udiv" - } else { - opcode = "sdiv" - } - case "%": - if isFloatType(b.emitter.mod.Types, mirRefType(e.Left)) { - opcode = "frem" - } else if isUnsignedTypeID(b.emitter.mod.Types, mirRefType(e.Left)) { - opcode = "urem" - } else { - opcode = "srem" - } - case "&": - opcode = "and" - case "|": - opcode = "or" - case "^": - opcode = "xor" - case "<<", ">>": - _, bits, ok := integerInfoID(b.emitter.mod.Types, mirRefType(e.Left)) - if !ok { - b.emitter.markInvalid("shift lowering requires integral operands") - return left - } - invalid := b.compare("icmp", "uge", right, b.value(strconv.Itoa(bits), right.Layout)) - shiftID := b.nextID - b.nextID++ - failLabel := fmt.Sprintf("shift_fail_%d", shiftID) - readyLabel := fmt.Sprintf("shift_ready_%d", shiftID) - b.condBranch(invalid, failLabel, readyLabel) - b.namedLabel(failLabel) - b.trap() - b.namedLabel(readyLabel) - opcode := "shl" - if e.Op == ">>" { - opcode = "ashr" - if isUnsignedTypeID(b.emitter.mod.Types, mirRefType(e.Left)) { - opcode = "lshr" - } - } - shiftCount := right - if mirRefType(e.Right) != mirRefType(e.Left) { - shiftCount = emitCast(b, &mir.Cast{Arg: e.Right, Type: mirRefType(e.Left), Location: mir.ValueRefLocation(e.Right)}) - } - return b.arithmetic(opcode, left, shiftCount) - case "==", "!=", "<", "<=", ">", ">=": - if result, ok := emitOptionalNoneCompare(b, e.Op, e.Left, e.Right, left, right); ok { - return result - } - if isFloatType(b.emitter.mod.Types, mirRefType(e.Left)) { - pred := map[string]string{"==": "oeq", "!=": "one", "<": "olt", "<=": "ole", ">": "ogt", ">=": "oge"}[e.Op] - return b.compare("fcmp", pred, left, right) - } - return b.compare("icmp", integerComparePredID(b.emitter.mod.Types, e.Op, mirRefType(e.Left)), left, right) - case "&&", "||": - lc := emitCondRef(b, e.Left) - rc := emitCondRef(b, e.Right) - if e.Op == "&&" { - return b.arithmetic("and", lc, rc) - } - return b.arithmetic("or", lc, rc) - default: - return left - } - return b.arithmetic(opcode, left, right) - case *mir.Call: - args := make([]llvmValue, len(e.Args)) - for i, arg := range e.Args { - args[i] = emitRef(b, arg) - } - callee := emitRef(b, e.Callee) - if callee.Layout.Kind != llvmLayoutFunction { - b.emitter.markInvalid("call reached LLVM without function type") - return b.value("0", b.emitter.layout(e.Type)) - } - return b.call(callee, args) - case *mir.AddrOf: - place, ok := emitPlacePtr(b, e.Place) - if !ok { - return b.value("0", b.emitter.layout(e.Type)) - } - pointer := b.pointerValue(place) - resultType, ok := b.emitter.mod.Types.Type(e.Type) - if !ok || resultType.Kind != ir.TypeRawPtr { - return pointer - } - if place.Pointee.Text == "i8" { - return pointer - } - return b.bitcast(pointer, b.emitter.layout(e.Type)) - case *mir.SliceView: - return emitSliceView(b, e) - case *mir.StringChars: - return emitStringChars(b, e) - case *mir.Load: - ptr, ok := emitPlacePtr(b, e.Place) - if !ok { - return b.value("0", b.emitter.layout(e.Type)) - } - return b.load(ptr) - case *mir.Field: - return b.extractIndex(emitRef(b, e.Base), e.Index) - case *mir.StructLit: - current := b.zero(b.emitter.layout(e.Type)) - for i, field := range e.Fields { - current = b.insertIndex(current, emitRef(b, field), i) - } - return current - case *mir.ArrayLit: - current := b.zero(b.emitter.layout(e.Type)) - for i, item := range e.Values { - current = b.insertIndex(current, emitRef(b, item), i) - } - return current - case *mir.DynamicArrayAlloc: - return emitDynamicArrayAlloc(b, e) - case *mir.Alloc: - return emitAlloc(b, e) - case *mir.ZeroValue: - return b.zero(b.emitter.layout(e.Type)) - case *mir.OptionalSome: - optional, ok := b.emitter.mod.Types.Type(e.Type) - if !ok || optional.Kind != ir.TypeOptional { - return b.value("0", b.emitter.layout(e.Type)) - } - value := b.insertField(b.zero(b.emitter.layout(e.Type)), b.value("true", llvmScalarLayout("i1")), llvmFieldPresent) - return b.insertField(value, emitRef(b, e.Value), llvmFieldValue) - case *mir.InterfaceMake: - value := emitRef(b, e.Value) - dataPtr := value - var allocator llvmValue - if valueTypeInfo, isOwned := b.emitter.mod.Types.Type(mirRefType(e.Value)); isOwned && valueTypeInfo.Kind == ir.TypeOwnedPtr { - if !isOwnedInterfaceType(b.emitter.mod.Types, mirRefType(e.Value)) { - dataPtr = b.extractField(value, llvmFieldData) - allocator = b.extractField(value, llvmFieldAllocator) - } - } - rawPointer := llvmPointerLayout(llvmScalarLayout("i8")) - dataBytePtr := b.bitcast(dataPtr, rawPointer) - itabSym := interfaceSymbolName("itab", b.emitter.mod.Types, e.Type, e.DataType) - itabPtr := b.value(fmt.Sprintf("bitcast ([%d x i8*]* %s to i8*)", interfaceVtableLength(b.emitter.mod.Types, e.Type, len(e.Slots)), itabSym), rawPointer) - current := b.insertField(b.zero(b.emitter.layout(e.Type)), dataBytePtr, llvmFieldData) - current = b.insertField(current, itabPtr, llvmFieldDispatch) - if allocator.Layout == nil { - return current - } - return b.insertField(current, allocator, llvmFieldAllocator) - case *mir.InterfaceCall: - data, fn, ok := emitInterfaceCallTarget(b, e.Base, e.Slot) - if !ok { - return b.value("0", b.emitter.layout(e.Type)) - } - args := make([]llvmValue, 1, len(e.Args)+1) - args[0] = data - for _, arg := range e.Args { - args = append(args, emitRef(b, arg)) - } - result := b.call(fn, args) - if consumesOwnedInterfaceStorage(b.emitter.mod.Types, e) { - emitInterfaceStorageRelease(b, mirRefType(e.Base), emitRef(b, e.Base), data) - } - return result - default: - b.invariant("unsupported MIR value expression %T", expr) - return llvmValue{} - } - }) -} - -func emitOptionalNoneCompare(b *llvmBuilder, op string, leftRef, rightRef mir.ValueRef, leftValue, rightValue llvmValue) (llvmValue, bool) { - if op != "==" && op != "!=" { - return llvmValue{}, false - } - leftType, leftOK := b.emitter.mod.Types.Type(mirRefType(leftRef)) - rightType, rightOK := b.emitter.mod.Types.Type(mirRefType(rightRef)) - leftOptional := leftOK && leftType.Kind == ir.TypeOptional - rightOptional := rightOK && rightType.Kind == ir.TypeOptional - if !leftOptional && !rightOptional { - return llvmValue{}, false - } - leftNone := leftValue.Text == "zeroinitializer" - rightNone := rightValue.Text == "zeroinitializer" - if leftNone && rightNone { - if op == "==" { - return b.value("true", llvmScalarLayout("i1")), true - } - return b.value("false", llvmScalarLayout("i1")), true - } - var value llvmValue - if leftNone { - value = rightValue - } else if rightNone { - value = leftValue - } else { - if b != nil && b.emitter != nil { - b.emitter.markInvalid("optional equality currently requires `none` on one side") - } - return b.value("false", llvmScalarLayout("i1")), true - } - tag := b.extractField(value, llvmFieldPresent) - pred := "eq" - if op == "!=" { - pred = "ne" - } - return b.compare("icmp", pred, tag, b.value("false", tag.Layout)), true -} - -func emitRef(b *llvmBuilder, ref mir.ValueRef) llvmValue { - return withLLVMLocation(b, mir.ValueRefLocation(ref), func() llvmValue { - if ref == nil { - b.invariant("reference emission requires MIR value") - } - layout := b.emitter.layout(mirRefType(ref)) - if layout == nil { - b.invariant("reference has unsupported type %s", b.emitter.mod.Types.Text(mirRefType(ref))) - } - switch v := ref.(type) { - case *mir.RefConst: - typ, ok := b.emitter.mod.Types.Type(v.Type) - if !ok { - return b.value("0", layout) - } - if typ.Kind == ir.TypeBool && v.Value != "false" && v.Value != "true" { - if b.emitter != nil { - b.emitter.markInvalid("invalid boolean constant: " + v.Value) - } - return b.value("false", layout) - } - if typ.Kind == ir.TypeFloat { - return b.value(llvmFloatConst(v.Value, typ.Bits), layout) - } - if typ.Kind == ir.TypeCStr { - return b.value("null", layout) - } - return b.value(v.Value, layout) - case *mir.RefName: - typ, _ := b.emitter.mod.Types.Type(v.Type) - isFunc := typ.Kind == ir.TypeFunction - if ptr, ok := b.localPtrs[v.Name]; ok { - return b.load(ptr) - } - if reg, ok := b.locals[v.Name]; ok { - return reg - } - if isFunc { - return b.value("@"+ir.SanitizeSymbolName(ir.StripSymbolInstance(v.Name)), layout) - } - - isLocalStatic := false - var localEntry *mir.StaticEntry - if b.emitter != nil && b.emitter.mod != nil { - for _, entry := range b.emitter.mod.StaticData { - eName := strings.TrimPrefix(entry.Name, "@") - vName := strings.TrimPrefix(v.Name, "@") - if eName == vName { - isLocalStatic = true - localEntry = entry - break - } - } - } - - if isLocalStatic && localEntry != nil { - if localEntry.Bytes { - arrayType := fmt.Sprintf("[%d x i8]", len(localEntry.Value)+1) - return b.value(fmt.Sprintf("getelementptr inbounds (%s, %s* %s, i64 0, i64 0)", arrayType, arrayType, localEntry.Name), layout) - } - staticLayout := b.emitter.layout(localEntry.Type) - return b.alignedLoad(b.place(localEntry.Name, staticLayout), localEntry.Align) - } - - if idx := strings.IndexByte(v.Name, '$'); idx >= 0 { - name := "@" + v.Name - if b.emitter.externalGlobals == nil { - b.emitter.externalGlobals = make(map[string]ir.TypeID) - } - b.emitter.externalGlobals[name] = v.Type - - return b.alignedLoad(b.place(name, layout), 4) - } - - if strings.HasPrefix(v.Name, "@") { - return b.value(v.Name, layout) - } - return b.value("0", layout) - default: - b.invariant("unsupported MIR reference %T", ref) - return llvmValue{} - } - }) -} - -func ensureLocalAddr(b *llvmBuilder, ref *mir.RefName) (llvmPlace, bool) { - if b == nil || ref == nil { - return llvmPlace{}, false - } - if ptr, ok := b.localPtrs[ref.Name]; ok { - return ptr, true - } - reg, ok := b.locals[ref.Name] - if !ok { - return llvmPlace{}, false - } - ptr := b.alloca(reg.Layout) - b.store(ptr, reg) - b.localPtrs[ref.Name] = ptr - return ptr, true -} - -func llvmFloatConst(value string, bits int) string { - parsed, err := strconv.ParseFloat(value, bits) - if err != nil { - return value - } - if bits == 32 { - parsed = float64(float32(parsed)) - } - return fmt.Sprintf("0x%016X", math.Float64bits(parsed)) -} - -func emitCondRef(b *llvmBuilder, ref mir.ValueRef) llvmValue { - return withLLVMLocation(b, mir.ValueRefLocation(ref), func() llvmValue { - val := emitRef(b, ref) - refType := mirRefType(ref) - if typ, ok := b.emitter.mod.Types.Type(refType); ok && typ.Kind == ir.TypeBool { - return val - } - if b != nil && b.emitter != nil { - b.emitter.markInvalid("non-bool condition reached llvm lowering: " + b.emitter.mod.Types.Text(refType)) - } - return b.value("false", llvmScalarLayout("i1")) - }) -} - -func mirRefType(ref mir.ValueRef) ir.TypeID { - switch v := ref.(type) { - case *mir.RefConst: - return v.Type - case *mir.RefName: - return v.Type - default: - return ir.InvalidType - } -} - -func emitLogicalNot(b *llvmBuilder, arg llvmValue, ref mir.ValueRef) llvmValue { - if typ, ok := b.emitter.mod.Types.Type(mirRefType(ref)); ok && typ.Kind == ir.TypeBool { - return b.arithmetic("xor", arg, b.value("true", arg.Layout)) - } - cmp := emitCondRef(b, ref) - return b.arithmetic("xor", cmp, b.value("true", cmp.Layout)) -} - -func isFloatType(types *ir.TypeTable, id ir.TypeID) bool { - typ, ok := types.Type(id) - return ok && typ.Kind == ir.TypeFloat -} - -func llvmEscapeString(s string) string { - var sb strings.Builder - for i := range len(s) { - b := s[i] - if b == '\\' { - sb.WriteString(`\5C`) - } else if b == '"' { - sb.WriteString(`\22`) - } else if b >= 32 && b <= 126 { - sb.WriteByte(b) - } else { - fmt.Fprintf(&sb, "\\%02X", b) - } - } - sb.WriteString(`\00`) - return sb.String() -} - -type callDecl struct { - Name string - ReturnType ir.TypeID - Params []ir.TypeID -} - -func collectCallDecls(mod *mir.Module) []callDecl { - if mod == nil { - return nil - } - defined := make(map[string]struct{}) - for _, fn := range mod.Funcs { - if fn != nil && fn.Name != "" { - defined[fn.Name] = struct{}{} - } - } - decls := make(map[string]callDecl) - for _, fn := range mod.Funcs { - if fn == nil || fn.Blocks == nil { - continue - } - for _, block := range fn.Blocks { - if block == nil { - continue - } - for _, instr := range block.Instrs { - switch callInstr := instr.(type) { - case *mir.Assign: - call, ok := callInstr.Value.(*mir.Call) - if !ok || call == nil { - continue - } - recordCallDecl(decls, defined, call) - case *mir.Call: - recordCallDecl(decls, defined, callInstr) - } - } - } - } - out := make([]callDecl, 0, len(decls)) - for _, decl := range decls { - out = append(out, decl) - } - return out -} - -func recordCallDecl(decls map[string]callDecl, defined map[string]struct{}, call *mir.Call) { - if call == nil { - return - } - nameRef, ok := call.Callee.(*mir.RefName) - if !ok || nameRef == nil { - return - } - name := nameRef.Name - if idx := strings.IndexByte(name, '$'); idx >= 0 { - name = name[:idx] - } - if _, ok := defined[name]; ok { - return - } - params := make([]ir.TypeID, 0, len(call.Args)) - for _, arg := range call.Args { - params = append(params, mirRefType(arg)) - } - decls[name] = callDecl{Name: name, ReturnType: call.Type, Params: params} -} diff --git a/internal/backend/llvm/string_slice_emit.go b/internal/backend/llvm/string_slice_emit.go new file mode 100644 index 00000000..57348224 --- /dev/null +++ b/internal/backend/llvm/string_slice_emit.go @@ -0,0 +1,624 @@ +package llvm + +import ( + "fmt" + "strconv" + + "compiler/internal/ir" + "compiler/internal/ir/mir" +) + +func emitTargetIndexAsI64(b *llvmBuilder, value llvmValue) llvmValue { + if value.Layout.Text == "i64" { + return value + } + if value.Layout.Text != "i32" { + b.emitter.markInvalid("unsupported target index type " + value.Layout.Text) + return value + } + return b.cast("zext", value, llvmScalarLayout("i64")) +} + +func normalizeIndexForLength(b *llvmBuilder, indexRef mir.ValueRef, lengthI64 llvmValue) (compareIndex, compareLength, indexI64 llvmValue, ok bool) { + if b == nil || indexRef == nil { + return llvmValue{}, llvmValue{}, llvmValue{}, false + } + indexType := mirRefType(indexRef) + _, indexBits, ok := integerInfoID(b.emitter.mod.Types, indexType) + if !ok { + b.emitter.markInvalid("indexed access lowering requires integral index") + return llvmValue{}, llvmValue{}, llvmValue{}, false + } + compareIndex = emitRef(b, indexRef) + compareLength = lengthI64 + indexI64 = compareIndex + if indexBits < 64 { + u64 := b.emitter.mod.Types.Intern(ir.Type{Kind: ir.TypeInteger, Bits: 64}) + compareIndex = emitCast(b, &mir.Cast{Arg: indexRef, Type: u64}) + indexI64 = compareIndex + } else if indexBits > 64 { + compareLength = b.cast("zext", lengthI64, compareIndex.Layout) + indexI64 = b.cast("trunc", compareIndex, llvmScalarLayout("i64")) + } + return compareIndex, compareLength, indexI64, true +} + +func emitBoundsCheckedIndex(b *llvmBuilder, indexRef mir.ValueRef, length llvmValue) (llvmValue, bool) { + compareIndex, compareLength, index, ok := normalizeIndexForLength(b, indexRef, length) + if !ok { + return llvmValue{}, false + } + // Unsigned comparison also rejects negative signed indexes after sign extension. + outOfBounds := b.compare("icmp", "uge", compareIndex, compareLength) + boundsID := b.nextID + b.nextID++ + failLabel := fmt.Sprintf("bounds_fail_%d", boundsID) + okLabel := fmt.Sprintf("bounds_ok_%d", boundsID) + b.condBranch(outOfBounds, failLabel, okLabel) + b.namedLabel(failLabel) + b.trap() + b.namedLabel(okLabel) + return index, true +} + +func emitSliceBounds(b *llvmBuilder, view *mir.SliceView, lengthI64 llvmValue) (llvmValue, llvmValue, bool) { + i64 := llvmScalarLayout("i64") + startI64 := b.value("0", i64) + endI64 := lengthI64 + var invalid llvmValue + if view.Start != nil { + start, compareLength, normalized, ok := normalizeIndexForLength(b, view.Start, lengthI64) + if !ok { + return llvmValue{}, llvmValue{}, false + } + startI64 = normalized + invalid = b.compare("icmp", "ugt", start, compareLength) + } + if view.End != nil { + end, compareLength, normalized, ok := normalizeIndexForLength(b, view.End, lengthI64) + if !ok { + return llvmValue{}, llvmValue{}, false + } + endI64 = normalized + predicate := "ugt" + if !view.EndExclusive { + predicate = "uge" + } + endInvalid := b.compare("icmp", predicate, end, compareLength) + if invalid.Layout == nil { + invalid = endInvalid + } else { + invalid = b.arithmetic("or", invalid, endInvalid) + } + } + + boundsID := b.nextID + b.nextID++ + failLabel := fmt.Sprintf("slice_bounds_fail_%d", boundsID) + normalizedLabel := fmt.Sprintf("slice_bounds_normalized_%d", boundsID) + readyLabel := fmt.Sprintf("slice_bounds_ready_%d", boundsID) + failEmitted := false + if invalid.Layout != nil { + b.condBranch(invalid, failLabel, normalizedLabel) + b.namedLabel(failLabel) + b.trap() + b.namedLabel(normalizedLabel) + failEmitted = true + } + if view.End != nil && !view.EndExclusive { + endI64 = b.arithmetic("add", endI64, b.value("1", i64)) + } + reversed := b.compare("icmp", "ugt", startI64, endI64) + b.condBranch(reversed, failLabel, readyLabel) + if !failEmitted { + b.namedLabel(failLabel) + b.trap() + } + b.namedLabel(readyLabel) + return startI64, endI64, true +} + +func emitSliceView(b *llvmBuilder, view *mir.SliceView) llvmValue { + if b == nil || view == nil { + return llvmValue{} + } + resultLayout := b.emitter.layout(view.Type) + if view.Source == nil { + return b.zero(resultLayout) + } + sourceTypeID := view.Source.Type + targetTypeID := sourceTypeID + if sourceType, ok := b.emitter.mod.Types.Type(sourceTypeID); ok && sourceType.Kind == ir.TypeReference { + targetTypeID = sourceType.Elem + } + targetType, ok := b.emitter.mod.Types.Type(targetTypeID) + if ok && targetType.Kind == ir.TypeString { + return emitStringSliceView(b, view) + } + if !ok || (targetType.Kind != ir.TypeArray && targetType.Kind != ir.TypeSlice) { + b.emitter.markInvalid("slice view source shape is not lowerable in current compiler stage") + return b.zero(resultLayout) + } + var data, length llvmValue + var fixedArrayPlace llvmPlace + if targetType.Kind == ir.TypeSlice || targetType.Length == "" { + var source llvmValue + if sliceViewUsesPlacePtr(b.emitter.mod.Types, view.Source) { + ptr, ok := emitPlacePtr(b, view.Source) + if !ok { + return b.zero(resultLayout) + } + source = b.load(ptr) + } else { + source = emitRef(b, view.Source.Root) + // Dynamic-owner references are pointers to carrier headers. Slice + // references are already carrier aggregates and must stay unloaded. + if source.Layout.Kind == llvmLayoutPointer { + source = b.load(b.pointerPlace(source)) + } + } + data = b.extractField(source, llvmFieldData) + length = b.extractField(source, llvmFieldLength) + } else { + length = b.value(targetType.Length, llvmScalarLayout("i64")) + if sliceViewUsesPlacePtr(b.emitter.mod.Types, view.Source) { + ptr, found := emitPlacePtr(b, view.Source) + if found { + fixedArrayPlace = ptr + } + } else { + root := emitRef(b, view.Source.Root) + if root.Layout.Kind == llvmLayoutPointer { + fixedArrayPlace = b.pointerPlace(root) + } + } + if fixedArrayPlace.Pointee == nil { + b.emitter.markInvalid("fixed-array slicing requires addressable storage") + return b.zero(resultLayout) + } + } + + indexLayout := b.emitter.layout(b.emitter.mod.Types.IndexType()) + lengthI64 := length + if fixedArrayPlace.Pointee == nil { + lengthI64 = emitTargetIndexAsI64(b, length) + } + startI64, endI64, ok := emitSliceBounds(b, view, lengthI64) + if !ok { + return b.zero(resultLayout) + } + + if fixedArrayPlace.Pointee != nil { + data = b.pointerValue(b.arrayElement(fixedArrayPlace, b.value("0", llvmScalarLayout("i32")), false)) + } + adjustedData := b.pointerValue(b.gep(b.pointerPlace(data), startI64, false)) + viewLength := b.arithmetic("sub", endI64, startI64) + if indexLayout.Text != "i64" { + viewLength = b.cast("trunc", viewLength, indexLayout) + } + result := b.insertField(b.zero(resultLayout), adjustedData, llvmFieldData) + return b.insertField(result, viewLength, llvmFieldLength) +} + +func emitStringDataAndLength(b *llvmBuilder, value llvmValue) (llvmValue, llvmValue) { + return b.extractField(value, llvmFieldData), b.extractField(value, llvmFieldLength) +} + +func emitStringSliceView(b *llvmBuilder, view *mir.SliceView) llvmValue { + if b == nil || view == nil { + return llvmValue{} + } + resultLayout := b.emitter.layout(view.Type) + if view.Source == nil { + return b.zero(resultLayout) + } + _, ok := b.emitter.mod.Types.Type(view.Source.Type) + if !ok { + b.emitter.markInvalid("string slice view has invalid source type") + return b.zero(resultLayout) + } + var source llvmValue + if len(view.Source.Projections) > 0 { + ptr, ok := emitPlacePtr(b, view.Source) + if !ok { + return b.zero(resultLayout) + } + source = b.load(ptr) + } else { + source = emitRef(b, view.Source.Root) + } + data, length := emitStringDataAndLength(b, source) + + indexLayout := b.emitter.layout(b.emitter.mod.Types.IndexType()) + lengthI64 := emitTargetIndexAsI64(b, length) + startI64, endI64, ok := emitSliceBounds(b, view, lengthI64) + if !ok { + return b.zero(resultLayout) + } + + resultType, ok := b.emitter.mod.Types.Type(view.Type) + if !ok || resultType.Kind != ir.TypeReference { + b.emitter.markInvalid("string slice view has invalid result type") + return b.zero(resultLayout) + } + resultTarget, ok := b.emitter.mod.Types.Type(resultType.Elem) + if !ok { + b.emitter.markInvalid("string slice view has invalid result target") + return b.zero(resultLayout) + } + if resultTarget.Kind == ir.TypeString { + var boundaryValid llvmValue + for _, index := range []llvmValue{startI64, endI64} { + boundary := emitUTF8BoundaryCheck(b, data, index, lengthI64) + if boundaryValid.Layout == nil { + boundaryValid = boundary + } else { + boundaryValid = b.arithmetic("and", boundaryValid, boundary) + } + } + boundaryID := b.nextID + b.nextID++ + boundaryFail := fmt.Sprintf("string_boundary_fail_%d", boundaryID) + boundaryReady := fmt.Sprintf("string_boundary_ready_%d", boundaryID) + b.condBranch(boundaryValid, boundaryReady, boundaryFail) + b.namedLabel(boundaryFail) + b.trap() + b.namedLabel(boundaryReady) + } + + adjustedData := b.pointerValue(b.gep(b.pointerPlace(data), startI64, false)) + viewLength := b.arithmetic("sub", endI64, startI64) + if indexLayout.Text != "i64" { + viewLength = b.cast("trunc", viewLength, indexLayout) + } + result := b.insertField(b.zero(resultLayout), adjustedData, llvmFieldData) + return b.insertField(result, viewLength, llvmFieldLength) +} + +func emitUTF8BoundaryCheck(b *llvmBuilder, data, index, length llvmValue) llvmValue { + atEnd := b.compare("icmp", "eq", index, length) + id := b.nextID + b.nextID++ + loadLabel := fmt.Sprintf("utf8_boundary_load_%d", id) + endLabel := fmt.Sprintf("utf8_boundary_end_%d", id) + mergeLabel := fmt.Sprintf("utf8_boundary_merge_%d", id) + b.condBranch(atEnd, endLabel, loadLabel) + b.namedLabel(loadLabel) + value := b.load(b.gep(b.pointerPlace(data), index, false)) + masked := b.arithmetic("and", value, b.value("-64", value.Layout)) + continuation := b.compare("icmp", "eq", masked, b.value("-128", value.Layout)) + notContinuation := b.arithmetic("xor", continuation, b.value("true", continuation.Layout)) + b.branch(mergeLabel) + b.namedLabel(endLabel) + b.branch(mergeLabel) + b.namedLabel(mergeLabel) + return b.phi(llvmScalarLayout("i1"), + llvmIncoming{Value: b.value("true", llvmScalarLayout("i1")), Label: endLabel}, + llvmIncoming{Value: notContinuation, Label: loadLabel}, + ) +} + +func emitStringChars(b *llvmBuilder, chars *mir.StringChars) llvmValue { + if b == nil || chars == nil { + return llvmValue{} + } + resultLayout := b.emitter.layout(chars.Type) + if chars.Value == nil { + return b.zero(resultLayout) + } + refType, ok := b.emitter.mod.Types.Type(mirRefType(chars.Value)) + if !ok || refType.Kind != ir.TypeReference { + b.emitter.markInvalid("string character conversion requires a string reference") + return b.zero(resultLayout) + } + stringType, ok := b.emitter.mod.Types.Type(refType.Elem) + if !ok || stringType.Kind != ir.TypeString { + b.emitter.markInvalid("string character conversion requires a string reference") + return b.zero(resultLayout) + } + arrayType, ok := b.emitter.mod.Types.Type(chars.Type) + if !ok || arrayType.Kind != ir.TypeArray || arrayType.Length != "" || arrayType.Elem == ir.InvalidType { + b.emitter.markInvalid("string character conversion has invalid result type") + return b.zero(resultLayout) + } + if elemType, ok := b.emitter.mod.Types.Type(arrayType.Elem); !ok || elemType.Kind != ir.TypeChar { + b.emitter.markInvalid("string character conversion result must be a char array") + return b.zero(resultLayout) + } + + data, length := emitStringDataAndLength(b, emitRef(b, chars.Value)) + lengthI64 := emitTargetIndexAsI64(b, length) + indexLayout := b.emitter.layout(b.emitter.mod.Types.IndexType()) + count := emitUTF8CodepointCount(b, data, lengthI64) + id := b.nextID + b.nextID++ + countForHeader := count + if indexLayout.Text != "i64" { + tooLarge := b.compare("icmp", "ugt", count, b.value("4294967295", count.Layout)) + trapLabel := fmt.Sprintf("string_chars_length_fail_%d", id) + lengthReady := fmt.Sprintf("string_chars_length_ready_%d", id) + b.condBranch(tooLarge, trapLabel, lengthReady) + b.namedLabel(trapLabel) + b.trap() + b.namedLabel(lengthReady) + countForHeader = b.cast("trunc", count, indexLayout) + } + countValue := countForHeader + allocator := emitDefaultAllocatorHandle(b) + zero := b.compare("icmp", "eq", count, b.value("0", count.Layout)) + emptyLabel := fmt.Sprintf("string_chars_empty_%d", id) + allocateLabel := fmt.Sprintf("string_chars_allocate_%d", id) + readyLabel := fmt.Sprintf("string_chars_ready_%d", id) + b.condBranch(zero, emptyLabel, allocateLabel) + b.namedLabel(emptyLabel) + b.branch(readyLabel) + emptyBlock := b.currentLabel + b.namedLabel(allocateLabel) + allocated := emitDynamicArrayStorageAlloc(b, arrayType.Elem, countValue, allocator) + b.branch(readyLabel) + allocatedBlock := b.currentLabel + b.namedLabel(readyLabel) + charData := b.phi(allocated.Layout, llvmIncoming{Value: b.value("null", allocated.Layout), Label: emptyBlock}, llvmIncoming{Value: allocated, Label: allocatedBlock}) + return emitStringCharsFill(b, data, lengthI64, charData, countValue, chars.Type, allocator) +} + +func emitStringCharsFill(b *llvmBuilder, data, length, charData, count llvmValue, arrayType ir.TypeID, allocator llvmValue) llvmValue { + id := b.nextID + b.nextID++ + entryLabel := b.currentLabel + loopLabel := fmt.Sprintf("string_chars_fill_loop_%d", id) + bodyLabel := fmt.Sprintf("string_chars_fill_body_%d", id) + continueLabel := fmt.Sprintf("string_chars_fill_continue_%d", id) + doneLabel := fmt.Sprintf("string_chars_fill_done_%d", id) + b.branch(loopLabel) + b.namedLabel(loopLabel) + i64 := llvmScalarLayout("i64") + nextByteIndex := b.nextValue(i64) + nextCharIndex := b.nextValue(i64) + byteIndex := b.nextValue(i64) + charIndex := b.nextValue(i64) + b.definePhi(byteIndex, llvmIncoming{Value: b.value("0", i64), Label: entryLabel}, llvmIncoming{Value: nextByteIndex, Label: continueLabel}) + b.definePhi(charIndex, llvmIncoming{Value: b.value("0", i64), Label: entryLabel}, llvmIncoming{Value: nextCharIndex, Label: continueLabel}) + more := b.compare("icmp", "ult", byteIndex, length) + b.condBranch(more, bodyLabel, doneLabel) + b.namedLabel(bodyLabel) + next, codepoint := emitUTF8DecodeStep(b, data, byteIndex, length) + b.store(b.gep(b.pointerPlace(charData), charIndex, false), codepoint) + b.branch(continueLabel) + b.namedLabel(continueLabel) + b.defineArithmetic(nextByteIndex, "add", next, b.value("0", i64)) + b.defineArithmetic(nextCharIndex, "add", charIndex, b.value("1", i64)) + b.branch(loopLabel) + b.namedLabel(doneLabel) + return emitDynamicArrayHeader(b, arrayType, charData, count, count, allocator) +} + +func emitUTF8CodepointCount(b *llvmBuilder, data, length llvmValue) llvmValue { + id := b.nextID + b.nextID++ + entryLabel := b.currentLabel + loopLabel := fmt.Sprintf("utf8_count_loop_%d", id) + bodyLabel := fmt.Sprintf("utf8_count_body_%d", id) + continueLabel := fmt.Sprintf("utf8_count_continue_%d", id) + doneLabel := fmt.Sprintf("utf8_count_done_%d", id) + b.branch(loopLabel) + b.namedLabel(loopLabel) + i64 := llvmScalarLayout("i64") + nextIndex := b.nextValue(i64) + nextCount := b.nextValue(i64) + index := b.nextValue(i64) + count := b.nextValue(i64) + b.definePhi(index, llvmIncoming{Value: b.value("0", i64), Label: entryLabel}, llvmIncoming{Value: nextIndex, Label: continueLabel}) + b.definePhi(count, llvmIncoming{Value: b.value("0", i64), Label: entryLabel}, llvmIncoming{Value: nextCount, Label: continueLabel}) + more := b.compare("icmp", "ult", index, length) + b.condBranch(more, bodyLabel, doneLabel) + b.namedLabel(bodyLabel) + decodedNext, _ := emitUTF8DecodeStep(b, data, index, length) + b.defineArithmetic(nextCount, "add", count, b.value("1", i64)) + b.branch(continueLabel) + b.namedLabel(continueLabel) + b.defineArithmetic(nextIndex, "add", decodedNext, b.value("0", i64)) + b.branch(loopLabel) + b.namedLabel(doneLabel) + return count +} + +func emitUTF8DecodeStep(b *llvmBuilder, data, index, length llvmValue) (llvmValue, llvmValue) { + id := b.nextID + b.nextID++ + invalidLabel := fmt.Sprintf("utf8_decode_invalid_%d", id) + asciiLabel := fmt.Sprintf("utf8_decode_ascii_%d", id) + kindLabel := fmt.Sprintf("utf8_decode_kind_%d", id) + twoLabel := fmt.Sprintf("utf8_decode_two_%d", id) + threeOrFourLabel := fmt.Sprintf("utf8_decode_three_or_four_%d", id) + threeLabel := fmt.Sprintf("utf8_decode_three_%d", id) + fourLabel := fmt.Sprintf("utf8_decode_four_%d", id) + mergeLabel := fmt.Sprintf("utf8_decode_merge_%d", id) + + i64 := llvmScalarLayout("i64") + lead := b.load(b.gep(b.pointerPlace(data), index, false)) + isASCII := b.compare("icmp", "ule", lead, b.value("127", lead.Layout)) + twoLow := b.compare("icmp", "uge", lead, b.value("-62", lead.Layout)) + twoHigh := b.compare("icmp", "ule", lead, b.value("-33", lead.Layout)) + isTwo := b.arithmetic("and", twoLow, twoHigh) + threeLow := b.compare("icmp", "uge", lead, b.value("-32", lead.Layout)) + threeHigh := b.compare("icmp", "ule", lead, b.value("-17", lead.Layout)) + isThree := b.arithmetic("and", threeLow, threeHigh) + fourLow := b.compare("icmp", "uge", lead, b.value("-16", lead.Layout)) + fourHigh := b.compare("icmp", "ule", lead, b.value("-12", lead.Layout)) + isFour := b.arithmetic("and", fourLow, fourHigh) + validTwoThree := b.arithmetic("or", isTwo, isThree) + validLead := b.arithmetic("or", validTwoThree, isFour) + b.condBranch(isASCII, asciiLabel, kindLabel) + b.namedLabel(kindLabel) + b.condBranch(validLead, kindLabel+"_valid", invalidLabel) + b.namedLabel(kindLabel + "_valid") + b.condBranch(isTwo, twoLabel, threeOrFourLabel) + b.namedLabel(threeOrFourLabel) + b.condBranch(isThree, threeLabel, fourLabel) + b.namedLabel(invalidLabel) + b.trap() + + b.namedLabel(asciiLabel) + asciiNext := b.arithmetic("add", index, b.value("1", i64)) + asciiRune := emitUTF8ByteI32(b, lead, 127) + b.branch(mergeLabel) + + b.namedLabel(twoLabel) + emitUTF8WidthCheck(b, index, length, 2, invalidLabel) + secondIndex := b.arithmetic("add", index, b.value("1", i64)) + second := emitUTF8ContinuationByte(b, data, secondIndex, invalidLabel) + twoNext := b.arithmetic("add", index, b.value("2", i64)) + twoRuneLead := emitUTF8ByteI32(b, lead, 31) + twoRuneSecond := emitUTF8ByteI32(b, second, 63) + twoShifted := b.arithmetic("shl", twoRuneLead, b.value("6", twoRuneLead.Layout)) + twoRune := b.arithmetic("or", twoShifted, twoRuneSecond) + twoPred := b.currentLabel + b.branch(mergeLabel) + + b.namedLabel(threeLabel) + emitUTF8WidthCheck(b, index, length, 3, invalidLabel) + threeSecondIndex := b.arithmetic("add", index, b.value("1", i64)) + threeSecond := emitUTF8ContinuationByte(b, data, threeSecondIndex, invalidLabel) + e0 := b.compare("icmp", "eq", lead, b.value("-32", lead.Layout)) + ed := b.compare("icmp", "eq", lead, b.value("-19", lead.Layout)) + notE0 := b.arithmetic("xor", e0, b.value("true", e0.Layout)) + notED := b.arithmetic("xor", ed, b.value("true", ed.Layout)) + e0OK := b.compare("icmp", "uge", threeSecond, b.value("-96", threeSecond.Layout)) + edOK := b.compare("icmp", "ule", threeSecond, b.value("-97", threeSecond.Layout)) + lowOK := b.arithmetic("or", notE0, e0OK) + highOK := b.arithmetic("or", notED, edOK) + threeSecondOK := b.arithmetic("and", lowOK, highOK) + threeReady := fmt.Sprintf("utf8_decode_three_ready_%d", id) + b.condBranch(threeSecondOK, threeReady, invalidLabel) + b.namedLabel(threeReady) + threeThirdIndex := b.arithmetic("add", index, b.value("2", i64)) + threeThird := emitUTF8ContinuationByte(b, data, threeThirdIndex, invalidLabel) + threeNext := b.arithmetic("add", index, b.value("3", i64)) + threeLeadRune := emitUTF8ByteI32(b, lead, 15) + threeSecondRune := emitUTF8ByteI32(b, threeSecond, 63) + threeThirdRune := emitUTF8ByteI32(b, threeThird, 63) + threeLeadShift := b.arithmetic("shl", threeLeadRune, b.value("12", threeLeadRune.Layout)) + threeSecondShift := b.arithmetic("shl", threeSecondRune, b.value("6", threeSecondRune.Layout)) + threeFirstCombine := b.arithmetic("or", threeLeadShift, threeSecondShift) + threeRune := b.arithmetic("or", threeFirstCombine, threeThirdRune) + threePred := b.currentLabel + b.branch(mergeLabel) + + b.namedLabel(fourLabel) + emitUTF8WidthCheck(b, index, length, 4, invalidLabel) + fourSecondIndex := b.arithmetic("add", index, b.value("1", i64)) + fourSecond := emitUTF8ContinuationByte(b, data, fourSecondIndex, invalidLabel) + f0 := b.compare("icmp", "eq", lead, b.value("-16", lead.Layout)) + f4 := b.compare("icmp", "eq", lead, b.value("-12", lead.Layout)) + notF0 := b.arithmetic("xor", f0, b.value("true", f0.Layout)) + notF4 := b.arithmetic("xor", f4, b.value("true", f4.Layout)) + f0OK := b.compare("icmp", "uge", fourSecond, b.value("-112", fourSecond.Layout)) + f4OK := b.compare("icmp", "ule", fourSecond, b.value("-113", fourSecond.Layout)) + fourLowOK := b.arithmetic("or", notF0, f0OK) + fourHighOK := b.arithmetic("or", notF4, f4OK) + fourSecondOK := b.arithmetic("and", fourLowOK, fourHighOK) + fourReady := fmt.Sprintf("utf8_decode_four_ready_%d", id) + b.condBranch(fourSecondOK, fourReady, invalidLabel) + b.namedLabel(fourReady) + fourThirdIndex := b.arithmetic("add", index, b.value("2", i64)) + fourThird := emitUTF8ContinuationByte(b, data, fourThirdIndex, invalidLabel) + fourFourthIndex := b.arithmetic("add", index, b.value("3", i64)) + fourFourth := emitUTF8ContinuationByte(b, data, fourFourthIndex, invalidLabel) + fourNext := b.arithmetic("add", index, b.value("4", i64)) + fourLeadRune := emitUTF8ByteI32(b, lead, 7) + fourSecondRune := emitUTF8ByteI32(b, fourSecond, 63) + fourThirdRune := emitUTF8ByteI32(b, fourThird, 63) + fourFourthRune := emitUTF8ByteI32(b, fourFourth, 63) + fourLeadShift := b.arithmetic("shl", fourLeadRune, b.value("18", fourLeadRune.Layout)) + fourSecondShift := b.arithmetic("shl", fourSecondRune, b.value("12", fourSecondRune.Layout)) + fourThirdShift := b.arithmetic("shl", fourThirdRune, b.value("6", fourThirdRune.Layout)) + fourFirstCombine := b.arithmetic("or", fourLeadShift, fourSecondShift) + fourSecondCombine := b.arithmetic("or", fourFirstCombine, fourThirdShift) + fourRune := b.arithmetic("or", fourSecondCombine, fourFourthRune) + fourPred := b.currentLabel + b.branch(mergeLabel) + + b.namedLabel(mergeLabel) + next := b.phi(i64, llvmIncoming{Value: asciiNext, Label: asciiLabel}, llvmIncoming{Value: twoNext, Label: twoPred}, llvmIncoming{Value: threeNext, Label: threePred}, llvmIncoming{Value: fourNext, Label: fourPred}) + runeValue := b.phi(llvmScalarLayout("i32"), llvmIncoming{Value: asciiRune, Label: asciiLabel}, llvmIncoming{Value: twoRune, Label: twoPred}, llvmIncoming{Value: threeRune, Label: threePred}, llvmIncoming{Value: fourRune, Label: fourPred}) + return next, runeValue +} + +func emitUTF8WidthCheck(b *llvmBuilder, index, length llvmValue, width int, invalidLabel string) { + remaining := b.arithmetic("sub", length, index) + enough := b.compare("icmp", "uge", remaining, b.value(strconv.Itoa(width), remaining.Layout)) + id := b.nextID + b.nextID++ + readyLabel := fmt.Sprintf("utf8_width_ready_%d", id) + b.condBranch(enough, readyLabel, invalidLabel) + b.namedLabel(readyLabel) +} + +func emitUTF8ContinuationByte(b *llvmBuilder, data, index llvmValue, invalidLabel string) llvmValue { + value := b.load(b.gep(b.pointerPlace(data), index, false)) + low := b.compare("icmp", "uge", value, b.value("-128", value.Layout)) + high := b.compare("icmp", "ule", value, b.value("-65", value.Layout)) + valid := b.arithmetic("and", low, high) + id := b.nextID + b.nextID++ + readyLabel := fmt.Sprintf("utf8_continuation_ready_%d", id) + b.condBranch(valid, readyLabel, invalidLabel) + b.namedLabel(readyLabel) + return value +} + +func emitUTF8ByteI32(b *llvmBuilder, value llvmValue, mask int) llvmValue { + wide := b.cast("zext", value, llvmScalarLayout("i32")) + return b.arithmetic("and", wide, b.value(strconv.Itoa(mask), wide.Layout)) +} + +func emitLen(b *llvmBuilder, value mir.ValueRef) llvmValue { + if b == nil || value == nil { + return llvmValue{} + } + indexLayout := b.emitter.layout(b.emitter.mod.Types.IndexType()) + refType, ok := b.emitter.mod.Types.Type(mirRefType(value)) + if !ok || refType.Kind != ir.TypeReference { + b.emitter.markInvalid("len requires a reference value") + return b.value("0", indexLayout) + } + target, ok := b.emitter.mod.Types.Type(refType.Elem) + if !ok { + b.emitter.markInvalid("len has invalid reference target") + return b.value("0", indexLayout) + } + switch target.Kind { + case ir.TypeString: + return b.extractField(emitRef(b, value), llvmFieldLength) + case ir.TypeArray: + if target.Length != "" { + if _, err := strconv.ParseUint(target.Length, 10, 64); err != nil { + b.emitter.markInvalid("fixed array has invalid length") + return b.value("0", indexLayout) + } + return b.value(target.Length, indexLayout) + } + ownerRef := emitRef(b, value) + return b.extractField(b.load(b.pointerPlace(ownerRef)), llvmFieldLength) + case ir.TypeSlice: + return b.extractField(emitRef(b, value), llvmFieldLength) + default: + b.emitter.markInvalid("len requires a string or array reference") + return b.value("0", indexLayout) + } +} + +func sliceViewUsesPlacePtr(types *ir.TypeTable, source *mir.Place) bool { + if source == nil { + return false + } + if len(source.Projections) > 0 { + return true + } + if typ, ok := types.Type(source.Type); ok && typ.Kind == ir.TypeReference { + return false + } + typ, ok := types.Type(source.Type) + return ok && typ.Kind == ir.TypeArray && typ.Length != "" +} diff --git a/internal/backend/llvm/type_lowering.go b/internal/backend/llvm/type_layout.go similarity index 100% rename from internal/backend/llvm/type_lowering.go rename to internal/backend/llvm/type_layout.go diff --git a/internal/constvalue/value_test.go b/internal/constvalue/value_test.go index 14e3fdb3..fc59e8e0 100644 --- a/internal/constvalue/value_test.go +++ b/internal/constvalue/value_test.go @@ -168,6 +168,42 @@ func TestFoldIntegerDivisionUsesTruncTowardZero(t *testing.T) { } } +func TestFoldSignedIntegerDivisionOverflowUsesFiniteWidth(t *testing.T) { + tests := []struct { + typeID string + min string + }{ + {typeID: "i8", min: "-128"}, + {typeID: "i16", min: "-32768"}, + {typeID: "i32", min: "-2147483648"}, + {typeID: "i64", min: "-9223372036854775808"}, + } + for _, tt := range tests { + t.Run(tt.typeID, func(t *testing.T) { + for _, op := range []string{"/", "%"} { + got, ok := FoldBinary(op, mustIntConst(t, tt.min, tt.typeID), mustIntConst(t, "-1", tt.typeID)) + value, valueOK := got.(*IntConst) + want := tt.min + if op == "%" { + want = "0" + } + if !ok || !valueOK || value.Text() != want || value.TypeText() != tt.typeID { + t.Fatalf("FoldBinary(%s %s -1) = %#v, want %s %s", tt.min, op, got, want, tt.typeID) + } + } + }) + } +} + +func TestFoldIntegerDivisionByZeroIsNotConstant(t *testing.T) { + for _, op := range []string{"/", "%"} { + got, ok := FoldBinary(op, mustIntConst(t, "1", "i32"), mustIntConst(t, "0", "i32")) + if ok || got != nil { + t.Fatalf("FoldBinary(1 %s 0) = %#v, want no constant", op, got) + } + } +} + func TestFoldFloatBinaryRoundsF32(t *testing.T) { got, ok := FoldBinary("+", mustFloatConst(t, "16777216", "f32"), mustFloatConst(t, "1", "f32")) value, valueOK := got.(*FloatConst) diff --git a/internal/driver/compiler.go b/internal/driver/compiler.go index 19714c7c..36e2f903 100644 --- a/internal/driver/compiler.go +++ b/internal/driver/compiler.go @@ -21,8 +21,9 @@ func NewCompilerContext(cfg project.Config, diag *diagnostics.DiagnosticBag) *pr return ctx } -// CompileFile compiles the entry file using in-memory content instead of reading from disk if content is provided. -func CompileFile(ctx *project.CompilerContext, path string, content string) *project.Module { +// CompileFile compiles the entry file from overlay when non-nil, otherwise it +// reads the source from disk. +func CompileFile(ctx *project.CompilerContext, path string, overlay *string) *project.Module { if ctx == nil { return nil } @@ -36,13 +37,16 @@ func CompileFile(ctx *project.CompilerContext, path string, content string) *pro diag.Add(diagnostics.NewError("resolve input path: " + err.Error())) return nil } - if content == "" { + content := "" + if overlay == nil { data, err := os.ReadFile(absPath) if err != nil { diag.Add(diagnostics.NewError("read input file: " + err.Error())) return nil } content = string(data) + } else { + content = *overlay } if module, ok := prelude.ModuleForFile(ctx, absPath, content); ok { module.IsEntry = true diff --git a/internal/driver/compiler_test.go b/internal/driver/compiler_test.go new file mode 100644 index 00000000..6c27f88f --- /dev/null +++ b/internal/driver/compiler_test.go @@ -0,0 +1,44 @@ +package compiler + +import ( + "os" + "path/filepath" + "testing" + + "compiler/internal/diagnostics" + "compiler/internal/frontend/ast" + "compiler/internal/project" + "compiler/pkg/peeper" +) + +func TestCompileFileSourceSelection(t *testing.T) { + root := t.TempDir() + path := filepath.Join(root, "main"+peeper.SourceExt) + disk := "fn disk() -> i32 { return 1; }\n" + if err := os.WriteFile(path, []byte(disk), 0o644); err != nil { + t.Fatalf("write source: %v", err) + } + empty := "" + nonempty := "fn overlay() -> i32 { return 2; }\n" + tests := []struct { + name string + overlay *string + want string + }{ + {name: "disk", want: disk}, + {name: "empty overlay", overlay: &empty, want: ""}, + {name: "nonempty overlay", overlay: &nonempty, want: nonempty}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := project.NewWithConfig(project.Config{RootDir: root, Extension: peeper.SourceExt}, diagnostics.NewDiagnosticBag()) + mod := CompileFile(ctx, path, tt.overlay) + if mod == nil { + t.Fatalf("CompileFile returned nil") + } + if mod.ContentHash != ast.HashText(tt.want) { + t.Fatalf("content hash = %q, want hash for %q", mod.ContentHash, tt.want) + } + }) + } +} diff --git a/internal/frontend/lexer/lexer.go b/internal/frontend/lexer/lexer.go index 58ad01c2..96963d47 100644 --- a/internal/frontend/lexer/lexer.go +++ b/internal/frontend/lexer/lexer.go @@ -420,4 +420,4 @@ func unescapeQuoted(s string, quote byte) (string, error) { } return string(out), nil -} \ No newline at end of file +} diff --git a/internal/ir/hir_fold/fold.go b/internal/ir/hir/fold/fold.go similarity index 92% rename from internal/ir/hir_fold/fold.go rename to internal/ir/hir/fold/fold.go index 71311ef2..d7c24cfb 100644 --- a/internal/ir/hir_fold/fold.go +++ b/internal/ir/hir/fold/fold.go @@ -1,10 +1,11 @@ -package hir_fold +package fold import ( "compiler/internal/constvalue" "compiler/internal/diagnostics" "compiler/internal/ir" "compiler/internal/ir/hir" + "compiler/internal/problems" "compiler/internal/source" "maps" ) @@ -38,7 +39,9 @@ func foldBlock(types *ir.TypeTable, block *hir.Block, diag *diagnostics.Diagnost continue } if terminated { - addUnreachableWarning(diag, hir.LocOf(stmt)) + if diag != nil { + diag.Add(problems.UnreachableCode(hir.LocOf(stmt))) + } continue } folded := foldStmt(types, stmt, diag, env) @@ -162,15 +165,3 @@ func addConstantConditionWarning(diag *diagnostics.DiagnosticBag, loc *source.Lo WithPrimaryLabel(loc, msg), ) } - -func addUnreachableWarning(diag *diagnostics.DiagnosticBag, loc *source.Location) { - if diag == nil { - return - } - diag.Add( - diagnostics.NewWarning("unreachable code"). - WithCode(diagnostics.WarnUnreachableCode). - WithPrimaryLabel(loc, "this code is unreachable"). - WithHelp("remove this code or restructure control flow"), - ) -} diff --git a/internal/ir/hir_fold/fold_test.go b/internal/ir/hir/fold/fold_test.go similarity index 99% rename from internal/ir/hir_fold/fold_test.go rename to internal/ir/hir/fold/fold_test.go index 622577f2..a986868c 100644 --- a/internal/ir/hir_fold/fold_test.go +++ b/internal/ir/hir/fold/fold_test.go @@ -1,4 +1,4 @@ -package hir_fold +package fold import ( "testing" diff --git a/internal/ir/hir/lower/lower_interface.go b/internal/ir/hir/lower/lower_interface.go new file mode 100644 index 00000000..bf3a178c --- /dev/null +++ b/internal/ir/hir/lower/lower_interface.go @@ -0,0 +1,119 @@ +package lower + +import ( + "compiler/internal/frontend/ast" + "compiler/internal/ir" + "compiler/internal/project" + "compiler/internal/semantics/table" + "compiler/internal/semantics/typeinfo" +) + +func maybeLowerInterfaceExpr(ctx *project.CompilerContext, module *project.Module, scope *table.Scope, expr ast.Expr, expectedType typeinfo.Type) ir.Expr { + if expectedType == nil { + return nil + } + expectedRuntime := loweredRuntimeType(module, expectedType, nil) + iface, ok := typeinfo.InterfaceTypeOf(expectedRuntime) + if !ok { + return nil + } + resolved := exprResolvedType(module, expr) + if resolved == nil { + return nil + } + resolvedRuntime := loweredRuntimeType(module, resolved, nil) + if _, ok := typeinfo.InterfaceTypeOf(resolvedRuntime); ok { + return nil + } + dataType := resolvedRuntime + if target, _, ok := typeinfo.ReferenceTarget(typeinfo.Underlying(resolvedRuntime)); ok { + dataType = target + } else if target, ok := typeinfo.PointerTarget(typeinfo.Underlying(resolvedRuntime)); ok { + dataType = target + } + slots := make([]ir.InterfaceSlot, 0, len(iface.Methods)) + implementations := module.Semantics.InterfaceImplementations[expr.ID()] + if len(implementations) != len(iface.Methods) { + return &ir.InvalidExpr{Message: "missing interface implementation evidence", Type: ir.InvalidType, Location: ast.LocOf(expr)} + } + for index, method := range iface.Methods { + implementation := implementations[index] + if implementation.MethodName != method.Name || implementation.CallableType == nil || implementation.Symbol == nil || implementation.OwnerKey == "" { + return &ir.InvalidExpr{Message: "missing interface method implementation", Type: ir.InvalidType, Location: ast.LocOf(expr)} + } + slotType, ok := interfaceSlotTypeID(ctx, module, method) + if !ok { + return &ir.InvalidExpr{Message: "unsupported interface method shape", Type: ir.InvalidType, Location: ast.LocOf(expr)} + } + slots = append(slots, ir.InterfaceSlot{ + InterfaceType: loweredTypeID(ctx, module, expectedType), + MethodName: method.Name, + SlotType: slotType, + FuncName: methodSymbolRefName(implementation.OwnerKey, implementation.Symbol), + FuncType: loweredTypeID(ctx, module, implementation.CallableType), + DataType: loweredTypeID(ctx, module, dataType), + }) + } + return &ir.InterfaceMake{ + Value: lowerASTExpr(ctx, module, scope, expr, nil), + Slots: slots, + Type: loweredTypeID(ctx, module, expectedType), + Location: ast.LocOf(expr), + } +} + +func lookupInterfaceMethod(module *project.Module, baseType typeinfo.Type, name string) (*typeinfo.Method, int, bool) { + iface, ok := typeinfo.InterfaceTypeOf(loweredRuntimeType(module, baseType, nil)) + if !ok { + return nil, -1, false + } + for i := range iface.Methods { + if iface.Methods[i].Name == name { + return &iface.Methods[i], i, true + } + } + return nil, -1, false +} + +func interfaceSlotTypeID(ctx *project.CompilerContext, module *project.Module, method typeinfo.Method) (ir.TypeID, bool) { + params := []ir.TypeID{ctx.Types.Intern(ir.Type{Kind: ir.TypeRawPtr})} + for i, param := range method.Params { + if i == 0 { + continue + } + typ, ok := lowerInterfaceSlotValueType(ctx, module, param.Type) + if !ok { + return ir.InvalidType, false + } + params = append(params, typ) + } + returnType, ok := lowerInterfaceSlotValueType(ctx, module, method.Return) + if !ok { + return ir.InvalidType, false + } + if returnType == ir.InvalidType { + returnType = ctx.Types.Intern(ir.Type{Kind: ir.TypeVoid}) + } + return ctx.Types.Intern(ir.Type{Kind: ir.TypeFunction, Params: params, Return: returnType}), true +} + +func lowerInterfaceSlotValueType(ctx *project.CompilerContext, module *project.Module, t typeinfo.Type) (ir.TypeID, bool) { + if t == nil { + return ctx.Types.Intern(ir.Type{Kind: ir.TypeVoid}), true + } + runtimeType := loweredRuntimeType(module, t, nil) + if _, ok := typeinfo.InterfaceTypeOf(runtimeType); ok { + return loweredTypeID(ctx, module, runtimeType), true + } + if typeinfo.ContainsAbstractSelf(runtimeType) { + return ir.InvalidType, false + } + typ := loweredTypeID(ctx, module, runtimeType) + if typ == ir.InvalidType { + return ir.InvalidType, false + } + return typ, true +} + +// exprResolvedType reads typechecker output from semantic cache. +// Lowering consumes that result; it should not re-infer expression types. diff --git a/internal/ir/hir/lower/lower_types.go b/internal/ir/hir/lower/lower_types.go new file mode 100644 index 00000000..c56a46d8 --- /dev/null +++ b/internal/ir/hir/lower/lower_types.go @@ -0,0 +1,249 @@ +package lower + +import ( + "compiler/internal/ir" + "compiler/internal/project" + "compiler/internal/semantics/symbols" + "compiler/internal/semantics/table" + "compiler/internal/semantics/typeinfo" +) + +func loweredTypeID(ctx *project.CompilerContext, module *project.Module, t typeinfo.Type) ir.TypeID { + if ctx == nil || ctx.Types == nil || t == nil { + return ir.InvalidType + } + return internRuntimeType(ctx.Types, loweredRuntimeType(module, t, nil)) +} + +func loweredReturnTypeID(ctx *project.CompilerContext, module *project.Module, t typeinfo.Type) ir.TypeID { + if t == nil { + return ctx.Types.Intern(ir.Type{Kind: ir.TypeVoid}) + } + return loweredTypeID(ctx, module, t) +} + +// internRuntimeType is the semantic-to-IR type boundary. It receives only +// runtime-normalized semantic types, so IR never reparses source type text. +func internRuntimeType(types *ir.TypeTable, t typeinfo.Type) ir.TypeID { + if types == nil || t == nil { + return ir.InvalidType + } + switch typ := typeinfo.Underlying(t).(type) { + case *typeinfo.InvalidType, *typeinfo.UnknownType: + return ir.InvalidType + case *typeinfo.IntegerType: + if typ == nil { + return ir.InvalidType + } + return types.Intern(ir.Type{Kind: ir.TypeInteger, Signed: typ.Signed, Bits: typ.Bits}) + case *typeinfo.ByteType: + return types.Intern(ir.Type{Kind: ir.TypeByte}) + case *typeinfo.CharType: + return types.Intern(ir.Type{Kind: ir.TypeChar}) + case *typeinfo.FloatType: + if typ == nil { + return ir.InvalidType + } + return types.Intern(ir.Type{Kind: ir.TypeFloat, Bits: typ.Bits}) + case *typeinfo.BoolType: + return types.Intern(ir.Type{Kind: ir.TypeBool}) + case *typeinfo.CStrType: + return types.Intern(ir.Type{Kind: ir.TypeCStr}) + case *typeinfo.StringType: + return types.Intern(ir.Type{Kind: ir.TypeString}) + case *typeinfo.NoneType: + return types.Intern(ir.Type{Kind: ir.TypeVoid}) + case *typeinfo.AllocatorType: + return types.Intern(ir.Type{Kind: ir.TypeAllocator}) + case *typeinfo.NamedType: + if typ == nil || typ.Name == "" { + return ir.InvalidType + } + return types.Intern(ir.Type{Kind: ir.TypeNamed, Name: typ.Name}) + case *typeinfo.OwnedPtrType: + if typ == nil { + return ir.InvalidType + } + return types.Intern(ir.Type{Kind: ir.TypeOwnedPtr, Elem: internRuntimeType(types, typ.Target)}) + case *typeinfo.RawPtrType: + return types.Intern(ir.Type{Kind: ir.TypeRawPtr}) + case *typeinfo.RefType: + if typ == nil { + return ir.InvalidType + } + return types.Intern(ir.Type{Kind: ir.TypeReference, Mutable: typ.Mutable, Elem: internRuntimeType(types, typ.Target)}) + case *typeinfo.OptionalType: + if typ == nil { + return ir.InvalidType + } + return types.Intern(ir.Type{Kind: ir.TypeOptional, Elem: internRuntimeType(types, typ.Inner)}) + case *typeinfo.ArrayType: + if typ == nil { + return ir.InvalidType + } + if typ.Shape == typeinfo.ArraySlice { + return types.Intern(ir.Type{Kind: ir.TypeSlice, Elem: internRuntimeType(types, typ.Elem)}) + } + return types.Intern(ir.Type{Kind: ir.TypeArray, Length: typ.Len, Elem: internRuntimeType(types, typ.Elem)}) + case *typeinfo.StructType: + if typ == nil { + return ir.InvalidType + } + fields := make([]ir.TypeField, 0, len(typ.Fields)) + for _, field := range typ.Fields { + fields = append(fields, ir.TypeField{Name: field.Name, Type: internRuntimeType(types, field.Type)}) + } + return types.Intern(ir.Type{Kind: ir.TypeStruct, Fields: fields}) + case *typeinfo.InterfaceType: + if typ == nil { + return ir.InvalidType + } + methods := make([]ir.TypeMethod, 0, len(typ.Methods)) + for _, method := range typ.Methods { + params := make([]ir.TypeField, 0, len(method.Params)) + for _, param := range method.Params { + params = append(params, ir.TypeField{Name: param.Name, Type: internRuntimeType(types, param.Type)}) + } + returnType := internRuntimeType(types, method.Return) + if returnType == ir.InvalidType { + returnType = types.Intern(ir.Type{Kind: ir.TypeVoid}) + } + methods = append(methods, ir.TypeMethod{Name: method.Name, Params: params, Return: returnType}) + } + return types.Intern(ir.Type{Kind: ir.TypeInterface, Methods: methods}) + case *typeinfo.FuncType: + if typ == nil { + return ir.InvalidType + } + params := make([]ir.TypeID, 0, len(typ.Params)) + for _, param := range typ.Params { + params = append(params, internRuntimeType(types, param)) + } + returnType := internRuntimeType(types, typ.Return) + if returnType == ir.InvalidType { + returnType = types.Intern(ir.Type{Kind: ir.TypeVoid}) + } + return types.Intern(ir.Type{Kind: ir.TypeFunction, Params: params, Return: returnType}) + case *typeinfo.EnumType: + if typ == nil { + return ir.InvalidType + } + return types.Intern(ir.Type{Kind: ir.TypeNamed, Name: typ.Text()}) + default: + return ir.InvalidType + } +} + +// resolveNamedType performs a single-hop scope lookup for a NamedType so the +// lowerer can collapse source-level aliases before runtime layout work. +// Called only from loweredRuntimeType; lives here to avoid importing table +// from the leaf typeinfo package. +func resolveNamedType(scope *table.Scope, t typeinfo.Type) typeinfo.Type { + if scope == nil || t == nil { + return t + } + named, ok := t.(*typeinfo.NamedType) + if !ok || named == nil { + return t + } + sym, found := scope.Lookup(named.Name) + if found && sym != nil && sym.Kind == symbols.SymbolType { + if resolved, ok := symbols.GetSymbolType(sym); ok && resolved != nil { + return resolved + } + } + return t +} + +// loweredRuntimeType strips semantic-only named layers and preserves recursive +// shells so MIR sees runtime layout, not source-level aliases. +func loweredRuntimeType(module *project.Module, t typeinfo.Type, seen map[*typeinfo.DefinedType]struct{}) typeinfo.Type { + if seen == nil { + seen = make(map[*typeinfo.DefinedType]struct{}) + } + if t == nil { + return nil + } + if module != nil { + t = resolveNamedType(module.ModuleScope, t) + } + switch typ := t.(type) { + case *typeinfo.DefinedType: + if typ == nil { + return nil + } + if _, ok := seen[typ]; ok { + // Stop self-recursive expansion once shell already seen. + return &typeinfo.NamedType{Name: typ.Name} + } + seen[typ] = struct{}{} + defer delete(seen, typ) + return loweredRuntimeType(module, typ.Underlying, seen) + case *typeinfo.OwnedPtrType: + if typ == nil { + return nil + } + return &typeinfo.OwnedPtrType{Target: loweredRuntimeType(module, typ.Target, seen)} + case *typeinfo.RawPtrType: + if typ == nil { + return nil + } + return &typeinfo.RawPtrType{} + case *typeinfo.RefType: + if typ == nil { + return nil + } + return &typeinfo.RefType{Mutable: typ.Mutable, Target: loweredRuntimeType(module, typ.Target, seen)} + case *typeinfo.OptionalType: + if typ == nil { + return nil + } + return &typeinfo.OptionalType{Inner: loweredRuntimeType(module, typ.Inner, seen)} + case *typeinfo.ArrayType: + if typ == nil { + return nil + } + return &typeinfo.ArrayType{Len: typ.Len, Shape: typ.Shape, Elem: loweredRuntimeType(module, typ.Elem, seen)} + case *typeinfo.StructType: + if typ == nil { + return nil + } + fields := make([]typeinfo.Field, 0, len(typ.Fields)) + for _, field := range typ.Fields { + fields = append(fields, typeinfo.Field{Name: field.Name, Type: loweredRuntimeType(module, field.Type, seen)}) + } + return &typeinfo.StructType{Fields: fields} + case *typeinfo.InterfaceType: + if typ == nil { + return nil + } + methods := make([]typeinfo.Method, 0, len(typ.Methods)) + for _, method := range typ.Methods { + params := make([]typeinfo.Field, 0, len(method.Params)) + for _, param := range method.Params { + params = append(params, typeinfo.Field{ + Name: param.Name, + Type: loweredRuntimeType(module, param.Type, seen), + }) + } + methods = append(methods, typeinfo.Method{ + Name: method.Name, + Params: params, + Return: loweredRuntimeType(module, method.Return, seen), + }) + } + return &typeinfo.InterfaceType{Methods: methods} + case *typeinfo.FuncType: + if typ == nil { + return nil + } + params := make([]typeinfo.Type, 0, len(typ.Params)) + for _, param := range typ.Params { + params = append(params, loweredRuntimeType(module, param, seen)) + } + // defensive slice copy to prevent sharing original backing array + return &typeinfo.FuncType{Params: params, Return: loweredRuntimeType(module, typ.Return, seen)} + default: + return typeinfo.Underlying(t) + } +} diff --git a/internal/ir/hir_lower/lower.go b/internal/ir/hir/lower/module_lower.go similarity index 77% rename from internal/ir/hir_lower/lower.go rename to internal/ir/hir/lower/module_lower.go index 1aab322b..890550fe 100644 --- a/internal/ir/hir_lower/lower.go +++ b/internal/ir/hir/lower/module_lower.go @@ -1,4 +1,4 @@ -package hir_lower +package lower import ( "fmt" @@ -967,115 +967,6 @@ func lowerAllocCall(ctx *project.CompilerContext, module *project.Module, scope } } -func maybeLowerInterfaceExpr(ctx *project.CompilerContext, module *project.Module, scope *table.Scope, expr ast.Expr, expectedType typeinfo.Type) ir.Expr { - if expectedType == nil { - return nil - } - expectedRuntime := loweredRuntimeType(module, expectedType, nil) - iface, ok := typeinfo.InterfaceTypeOf(expectedRuntime) - if !ok { - return nil - } - resolved := exprResolvedType(module, expr) - if resolved == nil { - return nil - } - resolvedRuntime := loweredRuntimeType(module, resolved, nil) - if _, ok := typeinfo.InterfaceTypeOf(resolvedRuntime); ok { - return nil - } - dataType := resolvedRuntime - if target, _, ok := typeinfo.ReferenceTarget(typeinfo.Underlying(resolvedRuntime)); ok { - dataType = target - } else if target, ok := typeinfo.PointerTarget(typeinfo.Underlying(resolvedRuntime)); ok { - dataType = target - } - slots := make([]ir.InterfaceSlot, 0, len(iface.Methods)) - implementations := module.Semantics.InterfaceImplementations[expr.ID()] - if len(implementations) != len(iface.Methods) { - return &ir.InvalidExpr{Message: "missing interface implementation evidence", Type: ir.InvalidType, Location: ast.LocOf(expr)} - } - for index, method := range iface.Methods { - implementation := implementations[index] - if implementation.MethodName != method.Name || implementation.CallableType == nil || implementation.Symbol == nil || implementation.OwnerKey == "" { - return &ir.InvalidExpr{Message: "missing interface method implementation", Type: ir.InvalidType, Location: ast.LocOf(expr)} - } - slotType, ok := interfaceSlotTypeID(ctx, module, method) - if !ok { - return &ir.InvalidExpr{Message: "unsupported interface method shape", Type: ir.InvalidType, Location: ast.LocOf(expr)} - } - slots = append(slots, ir.InterfaceSlot{ - InterfaceType: loweredTypeID(ctx, module, expectedType), - MethodName: method.Name, - SlotType: slotType, - FuncName: methodSymbolRefName(implementation.OwnerKey, implementation.Symbol), - FuncType: loweredTypeID(ctx, module, implementation.CallableType), - DataType: loweredTypeID(ctx, module, dataType), - }) - } - return &ir.InterfaceMake{ - Value: lowerASTExpr(ctx, module, scope, expr, nil), - Slots: slots, - Type: loweredTypeID(ctx, module, expectedType), - Location: ast.LocOf(expr), - } -} - -func lookupInterfaceMethod(module *project.Module, baseType typeinfo.Type, name string) (*typeinfo.Method, int, bool) { - iface, ok := typeinfo.InterfaceTypeOf(loweredRuntimeType(module, baseType, nil)) - if !ok { - return nil, -1, false - } - for i := range iface.Methods { - if iface.Methods[i].Name == name { - return &iface.Methods[i], i, true - } - } - return nil, -1, false -} - -func interfaceSlotTypeID(ctx *project.CompilerContext, module *project.Module, method typeinfo.Method) (ir.TypeID, bool) { - params := []ir.TypeID{ctx.Types.Intern(ir.Type{Kind: ir.TypeRawPtr})} - for i, param := range method.Params { - if i == 0 { - continue - } - typ, ok := lowerInterfaceSlotValueType(ctx, module, param.Type) - if !ok { - return ir.InvalidType, false - } - params = append(params, typ) - } - returnType, ok := lowerInterfaceSlotValueType(ctx, module, method.Return) - if !ok { - return ir.InvalidType, false - } - if returnType == ir.InvalidType { - returnType = ctx.Types.Intern(ir.Type{Kind: ir.TypeVoid}) - } - return ctx.Types.Intern(ir.Type{Kind: ir.TypeFunction, Params: params, Return: returnType}), true -} - -func lowerInterfaceSlotValueType(ctx *project.CompilerContext, module *project.Module, t typeinfo.Type) (ir.TypeID, bool) { - if t == nil { - return ctx.Types.Intern(ir.Type{Kind: ir.TypeVoid}), true - } - runtimeType := loweredRuntimeType(module, t, nil) - if _, ok := typeinfo.InterfaceTypeOf(runtimeType); ok { - return loweredTypeID(ctx, module, runtimeType), true - } - if typeinfo.ContainsAbstractSelf(runtimeType) { - return ir.InvalidType, false - } - typ := loweredTypeID(ctx, module, runtimeType) - if typ == ir.InvalidType { - return ir.InvalidType, false - } - return typ, true -} - -// exprResolvedType reads typechecker output from semantic cache. -// Lowering consumes that result; it should not re-infer expression types. func exprResolvedType(module *project.Module, expr ast.Expr) typeinfo.Type { if module == nil || module.Semantics == nil || expr == nil { return nil @@ -1178,243 +1069,3 @@ func shouldDiscardBindingValue(sym *symbols.Symbol) bool { return false } } - -func loweredTypeID(ctx *project.CompilerContext, module *project.Module, t typeinfo.Type) ir.TypeID { - if ctx == nil || ctx.Types == nil || t == nil { - return ir.InvalidType - } - return internRuntimeType(ctx.Types, loweredRuntimeType(module, t, nil)) -} - -func loweredReturnTypeID(ctx *project.CompilerContext, module *project.Module, t typeinfo.Type) ir.TypeID { - if t == nil { - return ctx.Types.Intern(ir.Type{Kind: ir.TypeVoid}) - } - return loweredTypeID(ctx, module, t) -} - -// internRuntimeType is the semantic-to-IR type boundary. It receives only -// runtime-normalized semantic types, so IR never reparses source type text. -func internRuntimeType(types *ir.TypeTable, t typeinfo.Type) ir.TypeID { - if types == nil || t == nil { - return ir.InvalidType - } - switch typ := typeinfo.Underlying(t).(type) { - case *typeinfo.InvalidType, *typeinfo.UnknownType: - return ir.InvalidType - case *typeinfo.IntegerType: - if typ == nil { - return ir.InvalidType - } - return types.Intern(ir.Type{Kind: ir.TypeInteger, Signed: typ.Signed, Bits: typ.Bits}) - case *typeinfo.ByteType: - return types.Intern(ir.Type{Kind: ir.TypeByte}) - case *typeinfo.CharType: - return types.Intern(ir.Type{Kind: ir.TypeChar}) - case *typeinfo.FloatType: - if typ == nil { - return ir.InvalidType - } - return types.Intern(ir.Type{Kind: ir.TypeFloat, Bits: typ.Bits}) - case *typeinfo.BoolType: - return types.Intern(ir.Type{Kind: ir.TypeBool}) - case *typeinfo.CStrType: - return types.Intern(ir.Type{Kind: ir.TypeCStr}) - case *typeinfo.StringType: - return types.Intern(ir.Type{Kind: ir.TypeString}) - case *typeinfo.NoneType: - return types.Intern(ir.Type{Kind: ir.TypeVoid}) - case *typeinfo.AllocatorType: - return types.Intern(ir.Type{Kind: ir.TypeAllocator}) - case *typeinfo.NamedType: - if typ == nil || typ.Name == "" { - return ir.InvalidType - } - return types.Intern(ir.Type{Kind: ir.TypeNamed, Name: typ.Name}) - case *typeinfo.OwnedPtrType: - if typ == nil { - return ir.InvalidType - } - return types.Intern(ir.Type{Kind: ir.TypeOwnedPtr, Elem: internRuntimeType(types, typ.Target)}) - case *typeinfo.RawPtrType: - return types.Intern(ir.Type{Kind: ir.TypeRawPtr}) - case *typeinfo.RefType: - if typ == nil { - return ir.InvalidType - } - return types.Intern(ir.Type{Kind: ir.TypeReference, Mutable: typ.Mutable, Elem: internRuntimeType(types, typ.Target)}) - case *typeinfo.OptionalType: - if typ == nil { - return ir.InvalidType - } - return types.Intern(ir.Type{Kind: ir.TypeOptional, Elem: internRuntimeType(types, typ.Inner)}) - case *typeinfo.ArrayType: - if typ == nil { - return ir.InvalidType - } - if typ.Shape == typeinfo.ArraySlice { - return types.Intern(ir.Type{Kind: ir.TypeSlice, Elem: internRuntimeType(types, typ.Elem)}) - } - return types.Intern(ir.Type{Kind: ir.TypeArray, Length: typ.Len, Elem: internRuntimeType(types, typ.Elem)}) - case *typeinfo.StructType: - if typ == nil { - return ir.InvalidType - } - fields := make([]ir.TypeField, 0, len(typ.Fields)) - for _, field := range typ.Fields { - fields = append(fields, ir.TypeField{Name: field.Name, Type: internRuntimeType(types, field.Type)}) - } - return types.Intern(ir.Type{Kind: ir.TypeStruct, Fields: fields}) - case *typeinfo.InterfaceType: - if typ == nil { - return ir.InvalidType - } - methods := make([]ir.TypeMethod, 0, len(typ.Methods)) - for _, method := range typ.Methods { - params := make([]ir.TypeField, 0, len(method.Params)) - for _, param := range method.Params { - params = append(params, ir.TypeField{Name: param.Name, Type: internRuntimeType(types, param.Type)}) - } - returnType := internRuntimeType(types, method.Return) - if returnType == ir.InvalidType { - returnType = types.Intern(ir.Type{Kind: ir.TypeVoid}) - } - methods = append(methods, ir.TypeMethod{Name: method.Name, Params: params, Return: returnType}) - } - return types.Intern(ir.Type{Kind: ir.TypeInterface, Methods: methods}) - case *typeinfo.FuncType: - if typ == nil { - return ir.InvalidType - } - params := make([]ir.TypeID, 0, len(typ.Params)) - for _, param := range typ.Params { - params = append(params, internRuntimeType(types, param)) - } - returnType := internRuntimeType(types, typ.Return) - if returnType == ir.InvalidType { - returnType = types.Intern(ir.Type{Kind: ir.TypeVoid}) - } - return types.Intern(ir.Type{Kind: ir.TypeFunction, Params: params, Return: returnType}) - case *typeinfo.EnumType: - if typ == nil { - return ir.InvalidType - } - return types.Intern(ir.Type{Kind: ir.TypeNamed, Name: typ.Text()}) - default: - return ir.InvalidType - } -} - -// resolveNamedType performs a single-hop scope lookup for a NamedType so the -// lowerer can collapse source-level aliases before runtime layout work. -// Called only from loweredRuntimeType; lives here to avoid importing table -// from the leaf typeinfo package. -func resolveNamedType(scope *table.Scope, t typeinfo.Type) typeinfo.Type { - if scope == nil || t == nil { - return t - } - named, ok := t.(*typeinfo.NamedType) - if !ok || named == nil { - return t - } - sym, found := scope.Lookup(named.Name) - if found && sym != nil && sym.Kind == symbols.SymbolType { - if resolved, ok := symbols.GetSymbolType(sym); ok && resolved != nil { - return resolved - } - } - return t -} - -// loweredRuntimeType strips semantic-only named layers and preserves recursive -// shells so MIR sees runtime layout, not source-level aliases. -func loweredRuntimeType(module *project.Module, t typeinfo.Type, seen map[*typeinfo.DefinedType]struct{}) typeinfo.Type { - if seen == nil { - seen = make(map[*typeinfo.DefinedType]struct{}) - } - if t == nil { - return nil - } - if module != nil { - t = resolveNamedType(module.ModuleScope, t) - } - switch typ := t.(type) { - case *typeinfo.DefinedType: - if typ == nil { - return nil - } - if _, ok := seen[typ]; ok { - // Stop self-recursive expansion once shell already seen. - return &typeinfo.NamedType{Name: typ.Name} - } - seen[typ] = struct{}{} - defer delete(seen, typ) - return loweredRuntimeType(module, typ.Underlying, seen) - case *typeinfo.OwnedPtrType: - if typ == nil { - return nil - } - return &typeinfo.OwnedPtrType{Target: loweredRuntimeType(module, typ.Target, seen)} - case *typeinfo.RawPtrType: - if typ == nil { - return nil - } - return &typeinfo.RawPtrType{} - case *typeinfo.RefType: - if typ == nil { - return nil - } - return &typeinfo.RefType{Mutable: typ.Mutable, Target: loweredRuntimeType(module, typ.Target, seen)} - case *typeinfo.OptionalType: - if typ == nil { - return nil - } - return &typeinfo.OptionalType{Inner: loweredRuntimeType(module, typ.Inner, seen)} - case *typeinfo.ArrayType: - if typ == nil { - return nil - } - return &typeinfo.ArrayType{Len: typ.Len, Shape: typ.Shape, Elem: loweredRuntimeType(module, typ.Elem, seen)} - case *typeinfo.StructType: - if typ == nil { - return nil - } - fields := make([]typeinfo.Field, 0, len(typ.Fields)) - for _, field := range typ.Fields { - fields = append(fields, typeinfo.Field{Name: field.Name, Type: loweredRuntimeType(module, field.Type, seen)}) - } - return &typeinfo.StructType{Fields: fields} - case *typeinfo.InterfaceType: - if typ == nil { - return nil - } - methods := make([]typeinfo.Method, 0, len(typ.Methods)) - for _, method := range typ.Methods { - params := make([]typeinfo.Field, 0, len(method.Params)) - for _, param := range method.Params { - params = append(params, typeinfo.Field{ - Name: param.Name, - Type: loweredRuntimeType(module, param.Type, seen), - }) - } - methods = append(methods, typeinfo.Method{ - Name: method.Name, - Params: params, - Return: loweredRuntimeType(module, method.Return, seen), - }) - } - return &typeinfo.InterfaceType{Methods: methods} - case *typeinfo.FuncType: - if typ == nil { - return nil - } - params := make([]typeinfo.Type, 0, len(typ.Params)) - for _, param := range typ.Params { - params = append(params, loweredRuntimeType(module, param, seen)) - } - // defensive slice copy to prevent sharing original backing array - return &typeinfo.FuncType{Params: params, Return: loweredRuntimeType(module, typ.Return, seen)} - default: - return typeinfo.Underlying(t) - } -} diff --git a/internal/ir/hir_lower/lower_test.go b/internal/ir/hir/lower/module_lower_test.go similarity index 99% rename from internal/ir/hir_lower/lower_test.go rename to internal/ir/hir/lower/module_lower_test.go index dd10ff76..0d1926af 100644 --- a/internal/ir/hir_lower/lower_test.go +++ b/internal/ir/hir/lower/module_lower_test.go @@ -1,4 +1,4 @@ -package hir_lower +package lower import ( "testing" diff --git a/internal/ir/mir/lower.go b/internal/ir/mir/module_lower.go similarity index 100% rename from internal/ir/mir/lower.go rename to internal/ir/mir/module_lower.go diff --git a/internal/ir/mir/lower_test.go b/internal/ir/mir/module_lower_test.go similarity index 100% rename from internal/ir/mir/lower_test.go rename to internal/ir/mir/module_lower_test.go diff --git a/internal/ir/shared.go b/internal/ir/nodes.go similarity index 99% rename from internal/ir/shared.go rename to internal/ir/nodes.go index 965696d4..a18acfc0 100644 --- a/internal/ir/shared.go +++ b/internal/ir/nodes.go @@ -958,7 +958,7 @@ func SignatureText(types *TypeTable, params []Param, returnType TypeID) string { return b.String() } -// ir/shared.go +// ir/nodes.go func SanitizeSymbolName(text string) string { if text == "" { return "unknown" diff --git a/internal/ir/shared_test.go b/internal/ir/nodes_test.go similarity index 100% rename from internal/ir/shared_test.go rename to internal/ir/nodes_test.go diff --git a/internal/lsp/completion.go b/internal/lsp/completion.go index 28118521..9bd80b79 100644 --- a/internal/lsp/completion.go +++ b/internal/lsp/completion.go @@ -5,11 +5,9 @@ import ( "slices" "sort" "strings" - "unicode/utf16" - "unicode/utf8" "compiler/internal/diagnostics" - driver "compiler/internal/driver" + "compiler/internal/driver" "compiler/internal/frontend/ast" "compiler/internal/project" "compiler/internal/semantics/intrinsics" @@ -109,7 +107,11 @@ func (s *ServerState) HandleCompletion(params CompletionParams) ([]CompletionIte } rewrite := Range{Start: positionAtOffset(sourceText, parsed.rewriteStart), End: replacement.End} sentinelPosition := positionAtOffset(parsed.sentinel, parsed.sentinelAt) - return operationCompletionItems(sentinelCtx, sentinelModule, sentinelPosition, parsed.prefix, replacement, rewrite, parsed.pipe, parsed.callSuffix == completionCallArguments), nil + semanticPosition, ok := sourcePositionAt(parsed.sentinel, sentinelPosition) + if !ok { + return []CompletionItem{}, nil + } + return operationCompletionItems(sentinelCtx, sentinelModule, semanticPosition, parsed.prefix, replacement, rewrite, parsed.pipe, parsed.callSuffix == completionCallArguments), nil case completionNames: semanticCursor := source.NewPosition() semanticCursor.Advance(sourceText[:parsed.cursor]) @@ -138,11 +140,11 @@ func (s *ServerState) completionOverlays(currentFile string) map[string]string { } func compileCompletionSource(cfg project.Config, overlays map[string]string, filePath, content string) (*project.CompilerContext, *project.Module) { - ctx := driver.NewCompilerContext(cfg, diagnostics.NewDiagnosticBag()) + ctx := compiler.NewCompilerContext(cfg, diagnostics.NewDiagnosticBag()) for overlayPath, overlayContent := range overlays { - driver.AddSource(ctx, overlayPath, overlayContent) + compiler.AddSource(ctx, overlayPath, overlayContent) } - return ctx, driver.CompileFile(ctx, filePath, content) + return ctx, compiler.CompileFile(ctx, filePath, &content) } func parseCompletionContext(text string, position Position) parsedCompletionContext { @@ -417,10 +419,10 @@ func qualifiedCompletionItems(ctx *project.CompilerContext, module *project.Modu return sortCompletionItems(items) } -func operationCompletionItems(ctx *project.CompilerContext, module *project.Module, sentinelPosition Position, prefix string, replacement, rewrite Range, pipe, preserveArguments bool) []CompletionItem { +func operationCompletionItems(ctx *project.CompilerContext, module *project.Module, cursorPosition source.Position, prefix string, replacement, rewrite Range, pipe, preserveArguments bool) []CompletionItem { var selector *ast.SelectorExpr var piped *ast.CallExpr - cursor := buildCursorContext(ctx, module, sentinelPosition.Line+1, sentinelPosition.Character+1) + cursor := buildCursorContext(ctx, module, cursorPosition) if cursor == nil { return []CompletionItem{} } @@ -659,55 +661,3 @@ func sortCompletionItems(items []CompletionItem) []CompletionItem { }) return items } - -func offsetAtPosition(text string, position Position) (int, bool) { - if position.Line < 0 || position.Character < 0 { - return 0, false - } - lineStart := 0 - for range position.Line { - newline := strings.IndexByte(text[lineStart:], '\n') - if newline < 0 { - return 0, false - } - lineStart += newline + 1 - } - lineEnd := len(text) - if newline := strings.IndexByte(text[lineStart:], '\n'); newline >= 0 { - lineEnd = lineStart + newline - } - units := 0 - for offset := lineStart; offset < lineEnd; { - if units == position.Character { - return offset, true - } - r, size := utf8.DecodeRuneInString(text[offset:lineEnd]) - runeUnits := 1 - if r > 0xffff { - runeUnits = 2 - } - if units+runeUnits > position.Character { - return 0, false - } - units += runeUnits - offset += size - } - if units == position.Character { - return lineEnd, true - } - return 0, false -} - -func positionAtOffset(text string, offset int) Position { - if offset < 0 { - offset = 0 - } - if offset > len(text) { - offset = len(text) - } - lineStart := strings.LastIndexByte(text[:offset], '\n') + 1 - return Position{ - Line: strings.Count(text[:lineStart], "\n"), - Character: len(utf16.Encode([]rune(text[lineStart:offset]))), - } -} diff --git a/internal/lsp/cursor.go b/internal/lsp/cursor.go index b3dd9324..6c5b646b 100644 --- a/internal/lsp/cursor.go +++ b/internal/lsp/cursor.go @@ -28,7 +28,7 @@ func locContains(loc *source.Location, line, col int) bool { if line < loc.Start.Line || (line == loc.Start.Line && col < loc.Start.Column) { return false } - if line > loc.End.Line || (line == loc.End.Line && col > loc.End.Column) { + if line > loc.End.Line || (line == loc.End.Line && col >= loc.End.Column) { return false } return true @@ -64,22 +64,22 @@ func walkModuleAST(module *project.Module, visit func(ast.Node, ast.Node) bool) } } -func buildCursorContext(ctx *project.CompilerContext, module *project.Module, line, col int) *cursorContext { +func buildCursorContext(ctx *project.CompilerContext, module *project.Module, position source.Position) *cursorContext { if ctx == nil || module == nil || module.AST == nil { return nil } cc := &cursorContext{ ctx: ctx, module: module, - line: line, - col: col, + line: position.Line, + col: position.Column, parents: make(map[ast.NodeID]ast.Node), } walkModuleAST(module, func(n ast.Node, parent ast.Node) bool { if parent != nil { cc.parents[n.ID()] = parent } - if locContains(ast.LocOf(n), line, col) { + if locContains(ast.LocOf(n), position.Line, position.Column) { cc.node = n return true } diff --git a/internal/lsp/hover.go b/internal/lsp/hover.go index 4903ef49..e3483079 100644 --- a/internal/lsp/hover.go +++ b/internal/lsp/hover.go @@ -27,7 +27,7 @@ const ( type hoverSubject struct { Kind hoverSubjectKind Node ast.Node - Range Range + Location *source.Location Symbol *symbols.Symbol ExprType typeinfo.Type ResolvedType typeinfo.Type @@ -37,24 +37,9 @@ type hoverSubject struct { MethodSymbols []*symbols.Symbol } -func hoverRange(node ast.Node) Range { - loc := ast.LocOf(node) - return locationRange(loc) -} - -func locationRange(loc *source.Location) Range { - if loc == nil || loc.Start == nil || loc.End == nil { - return Range{} - } - return Range{ - Start: Position{Line: loc.Start.Line - 1, Character: loc.Start.Column - 1}, - End: Position{Line: loc.End.Line - 1, Character: loc.End.Column - 1}, - } -} - -func (s *ServerState) resolveHoverSubject(filePath string, line, col int) *hoverSubject { +func (s *ServerState) resolveHoverSubject(filePath string, position source.Position) *hoverSubject { ctx, mod := s.currentCompiledModule(filePath) - cc := buildCursorContext(ctx, mod, line, col) + cc := buildCursorContext(ctx, mod, position) if cc == nil { return nil } @@ -100,7 +85,7 @@ func attributeHoverSubject(node ast.Node, cc *cursorContext) *hoverSubject { return &hoverSubject{ Kind: hoverSubjectAttribute, Node: node, - Range: locationRange(attr.Location), + Location: attr.Location, Attribute: &hoverAttr, } } @@ -122,7 +107,7 @@ func resolveImportHoverSubject(cc *cursorContext) *hoverSubject { return &hoverSubject{ Kind: hoverSubjectImport, Node: ident, - Range: hoverRange(ident), + Location: ast.LocOf(ident), ResolvedImport: &hoverImp, } } @@ -142,7 +127,7 @@ func resolveTypeHoverSubject(cc *cursorContext) *hoverSubject { return &hoverSubject{ Kind: hoverSubjectType, Node: cc.node, - Range: hoverRange(cc.node), + Location: ast.LocOf(cc.node), ResolvedType: resolved, MethodSymbols: lookupMethodSet(cc.ctx, resolved, hoverMethodKeysForTypeNode(typeNode, cc.parents, resolved)), } @@ -301,10 +286,10 @@ func resolveDeclHoverSubject(cc *cursorContext) *hoverSubject { func declHoverSubject(cc *cursorContext, decl ast.Node, name *ast.Ident) *hoverSubject { subject := &hoverSubject{ - Kind: hoverSubjectDecl, - Node: decl, - Decl: decl, - Range: hoverRange(decl), + Kind: hoverSubjectDecl, + Node: decl, + Decl: decl, + Location: ast.LocOf(decl), } if name != nil { subject.Symbol = resolveIdentSymbol(name, cc.parents, cc.module, cc.ctx) @@ -329,11 +314,11 @@ func resolveSelectorHoverSubject(cc *cursorContext) *hoverSubject { } if sym := resolveSelectorMemberSymbol(sel, ident, cc.parents, cc.module, cc.ctx); sym != nil { return &hoverSubject{ - Kind: hoverSubjectSymbol, - Node: ident, - Decl: documentedDeclAncestor(ident, cc.parents), - Range: hoverRange(ident), - Symbol: sym, + Kind: hoverSubjectSymbol, + Node: ident, + Decl: documentedDeclAncestor(ident, cc.parents), + Location: ast.LocOf(ident), + Symbol: sym, } } if subject := resolveInterfaceSelectorMethodHoverSubject(cc, sel, ident); subject != nil { @@ -343,7 +328,7 @@ func resolveSelectorHoverSubject(cc *cursorContext) *hoverSubject { return &hoverSubject{ Kind: hoverSubjectExpr, Node: ident, - Range: hoverRange(ident), + Location: ast.LocOf(ident), ExprType: exprType, } } @@ -365,10 +350,10 @@ func resolveInterfaceSelectorMethodHoverSubject(cc *cursorContext, sel *ast.Sele continue } return &hoverSubject{ - Kind: hoverSubjectSymbol, - Node: ident, - Range: hoverRange(ident), - Symbol: interfaceMethodSymbol(ident, method), + Kind: hoverSubjectSymbol, + Node: ident, + Location: ast.LocOf(ident), + Symbol: interfaceMethodSymbol(ident, method), } } return nil @@ -390,11 +375,11 @@ func resolveSymbolHoverSubject(cc *cursorContext) *hoverSubject { return nil } subject := &hoverSubject{ - Kind: hoverSubjectSymbol, - Node: ident, - Decl: documentedDeclAncestor(ident, cc.parents), - Range: hoverRange(ident), - Symbol: sym, + Kind: hoverSubjectSymbol, + Node: ident, + Decl: documentedDeclAncestor(ident, cc.parents), + Location: ast.LocOf(ident), + Symbol: sym, } if sym.Kind == symbols.SymbolType { if typ, ok := symbols.GetSymbolType(sym); ok { @@ -514,7 +499,7 @@ func resolveExprHoverSubject(cc *cursorContext) *hoverSubject { return &hoverSubject{ Kind: hoverSubjectExpr, Node: cc.node, - Range: hoverRange(cc.node), + Location: ast.LocOf(cc.node), ExprType: exprType, } } @@ -721,7 +706,15 @@ func hoverDocComment(subject *hoverSubject) string { func (s *ServerState) HandleHover(params HoverParams) (*Hover, error) { path := uriToPath(string(params.TextDocument.URI)) - subject := s.resolveHoverSubject(path, params.Position.Line+1, params.Position.Character+1) + text, err := s.completionSource(path) + if err != nil { + return nil, nil + } + position, ok := sourcePositionAt(text, params.Position) + if !ok { + return nil, nil + } + subject := s.resolveHoverSubject(path, position) if subject == nil { return nil, nil } @@ -730,11 +723,15 @@ func (s *ServerState) HandleHover(params HoverParams) (*Hover, error) { return nil, nil } + hoverRange, ok := rangeAtLocation(text, subject.Location) + if !ok { + return nil, nil + } return &Hover{ Contents: MarkupContent{ Kind: "markdown", Value: value, }, - Range: &subject.Range, + Range: &hoverRange, }, nil } diff --git a/internal/lsp/navigation.go b/internal/lsp/navigation.go index 190613f4..7da7e70c 100644 --- a/internal/lsp/navigation.go +++ b/internal/lsp/navigation.go @@ -26,7 +26,12 @@ func symLocationsMatch(l1, l2 *source.Location) bool { func (s *ServerState) HandleDefinition(params DefinitionParams) ([]Location, error) { path := uriToPath(string(params.TextDocument.URI)) ctx, mod := s.currentCompiledModule(path) - cc := buildCursorContext(ctx, mod, params.Position.Line+1, params.Position.Character+1) + text, ok := sourceTextForFile(ctx, path) + position, positionOK := sourcePositionAt(text, params.Position) + if !ok || !positionOK { + return nil, nil + } + cc := buildCursorContext(ctx, mod, position) if cc == nil { return nil, nil } @@ -39,13 +44,15 @@ func (s *ServerState) HandleDefinition(params DefinitionParams) ([]Location, err return nil, nil } + targetText, ok := sourceTextForFile(ctx, *sym.Location.Filename) + targetRange, rangeOK := rangeAtLocation(targetText, sym.Location) + if !ok || !rangeOK { + return nil, nil + } return []Location{ { - URI: DocumentURI(pathToURI(*sym.Location.Filename)), - Range: Range{ - Start: Position{Line: sym.Location.Start.Line - 1, Character: sym.Location.Start.Column - 1}, - End: Position{Line: sym.Location.End.Line - 1, Character: sym.Location.End.Column - 1}, - }, + URI: DocumentURI(pathToURI(*sym.Location.Filename)), + Range: targetRange, }, }, nil } @@ -53,7 +60,12 @@ func (s *ServerState) HandleDefinition(params DefinitionParams) ([]Location, err func (s *ServerState) HandleRename(params RenameParams) (*WorkspaceEdit, error) { path := uriToPath(string(params.TextDocument.URI)) ctx, mod := s.currentCompiledModule(path) - cc := buildCursorContext(ctx, mod, params.Position.Line+1, params.Position.Character+1) + text, ok := sourceTextForFile(ctx, path) + position, positionOK := sourcePositionAt(text, params.Position) + if !ok || !positionOK { + return nil, nil + } + cc := buildCursorContext(ctx, mod, position) if cc == nil { return nil, nil } @@ -83,6 +95,7 @@ func (s *ServerState) HandleRename(params RenameParams) (*WorkspaceEdit, error) if mod != cc.module { parents = make(map[ast.NodeID]ast.Node) } + moduleText, hasModuleText := sourceTextForFile(s.LastCtx, mod.FilePath) walkModuleAST(mod, func(n ast.Node, parent ast.Node) bool { if parent != nil { parents[n.ID()] = parent @@ -104,12 +117,9 @@ func (s *ServerState) HandleRename(params RenameParams) (*WorkspaceEdit, error) return true } uri := DocumentURI(pathToURI(mod.FilePath)) - if loc != nil && loc.Start != nil && loc.End != nil { + if editRange, ok := rangeAtLocation(moduleText, loc); hasModuleText && ok { changes[uri] = append(changes[uri], TextEdit{ - Range: Range{ - Start: Position{Line: loc.Start.Line - 1, Character: loc.Start.Column - 1}, - End: Position{Line: loc.End.Line - 1, Character: loc.End.Column - 1}, - }, + Range: editRange, NewText: params.NewName, }) } diff --git a/internal/lsp/position.go b/internal/lsp/position.go new file mode 100644 index 00000000..b60a556c --- /dev/null +++ b/internal/lsp/position.go @@ -0,0 +1,124 @@ +package lsp + +import ( + "strings" + "unicode/utf16" + "unicode/utf8" + + "compiler/internal/project" + "compiler/internal/source" +) + +func offsetAtPosition(text string, position Position) (int, bool) { + if position.Line < 0 || position.Character < 0 { + return 0, false + } + lineStart := 0 + for range position.Line { + newline := strings.IndexByte(text[lineStart:], '\n') + if newline < 0 { + return 0, false + } + lineStart += newline + 1 + } + lineEnd := len(text) + if newline := strings.IndexByte(text[lineStart:], '\n'); newline >= 0 { + lineEnd = lineStart + newline + } + units := 0 + for offset := lineStart; offset < lineEnd; { + if units == position.Character { + return offset, true + } + r, size := utf8.DecodeRuneInString(text[offset:lineEnd]) + runeUnits := 1 + if r > 0xffff { + runeUnits = 2 + } + if units+runeUnits > position.Character { + return 0, false + } + units += runeUnits + offset += size + } + if units == position.Character { + return lineEnd, true + } + return 0, false +} + +func positionAtOffset(text string, offset int) Position { + if offset < 0 { + offset = 0 + } + if offset > len(text) { + offset = len(text) + } + lineStart := strings.LastIndexByte(text[:offset], '\n') + 1 + return Position{ + Line: strings.Count(text[:lineStart], "\n"), + Character: len(utf16.Encode([]rune(text[lineStart:offset]))), + } +} + +func sourcePositionAt(text string, position Position) (source.Position, bool) { + offset, ok := offsetAtPosition(text, position) + if !ok { + return source.Position{}, false + } + out := source.NewPosition() + out.Advance(text[:offset]) + return out, true +} + +func offsetAtSourcePosition(text string, position *source.Position) (int, bool) { + if position == nil || position.Line < 1 || position.Column < 1 { + return 0, false + } + lineStart := 0 + for line := 1; line < position.Line; line++ { + newline := strings.IndexByte(text[lineStart:], '\n') + if newline < 0 { + return 0, false + } + lineStart += newline + 1 + } + lineEnd := len(text) + if newline := strings.IndexByte(text[lineStart:], '\n'); newline >= 0 { + lineEnd = lineStart + newline + } + column := 1 + for offset := range text[lineStart:lineEnd] { + if column == position.Column { + return lineStart + offset, true + } + column++ + } + if column == position.Column { + return lineEnd, true + } + return 0, false +} + +func rangeAtLocation(text string, location *source.Location) (Range, bool) { + if location == nil || location.Start == nil || location.End == nil { + return Range{}, false + } + start, startOK := offsetAtSourcePosition(text, location.Start) + end, endOK := offsetAtSourcePosition(text, location.End) + if !startOK || !endOK || end < start { + return Range{}, false + } + return Range{Start: positionAtOffset(text, start), End: positionAtOffset(text, end)}, true +} + +func sourceTextForFile(ctx *project.CompilerContext, filePath string) (string, bool) { + if ctx == nil || ctx.Diagnostics == nil || filePath == "" { + return "", false + } + lines, ok := ctx.Diagnostics.GetSourceCache().GetLinesRange(filePath, 1, int(^uint(0)>>1)) + if !ok { + return "", false + } + return strings.Join(lines, "\n"), true +} diff --git a/internal/lsp/position_test.go b/internal/lsp/position_test.go new file mode 100644 index 00000000..6c4505f6 --- /dev/null +++ b/internal/lsp/position_test.go @@ -0,0 +1,54 @@ +package lsp + +import ( + "testing" + + "compiler/internal/source" +) + +func TestSourcePositionAndRangeUseUTF16Boundary(t *testing.T) { + text := "🙂x\n\t𝄞y" + position, ok := sourcePositionAt(text, Position{Line: 0, Character: 2}) + if !ok || position.Line != 1 || position.Column != 2 || position.Index != len("🙂") { + t.Fatalf("source position = %#v, %v", position, ok) + } + if _, ok := sourcePositionAt(text, Position{Line: 0, Character: 1}); ok { + t.Fatalf("accepted position inside surrogate pair") + } + + location := source.NewLocation("test.peep", source.Position{Line: 1, Column: 2}, source.Position{Line: 1, Column: 3}) + rangeValue, ok := rangeAtLocation(text, location) + if !ok || rangeValue != (Range{Start: Position{Line: 0, Character: 2}, End: Position{Line: 0, Character: 3}}) { + t.Fatalf("range = %#v, %v", rangeValue, ok) + } + start, startOK := offsetAtPosition(text, rangeValue.Start) + end, endOK := offsetAtPosition(text, rangeValue.End) + if !startOK || !endOK || text[start:end] != "x" { + t.Fatalf("range maps to %q, want x", text[start:end]) + } +} + +func TestLocationContainmentIsHalfOpen(t *testing.T) { + location := source.NewLocation("test.peep", source.Position{Line: 1, Column: 3}, source.Position{Line: 1, Column: 5}) + for _, test := range []struct { + name string + line, col int + wantInside bool + }{ + {name: "start", line: 1, col: 3, wantInside: true}, + {name: "interior", line: 1, col: 4, wantInside: true}, + {name: "end", line: 1, col: 5}, + {name: "before", line: 1, col: 2}, + } { + t.Run(test.name, func(t *testing.T) { + if got := locContains(location, test.line, test.col); got != test.wantInside { + t.Fatalf("locContains(%d, %d) = %v, want %v", test.line, test.col, got, test.wantInside) + } + }) + } + + multiline := source.NewLocation("test.peep", source.Position{Line: 1, Column: 3}, source.Position{Line: 2, Column: 2}) + if !locContains(multiline, 2, 1) || locContains(multiline, 2, 2) { + t.Fatalf("multiline end is not half-open") + } +} diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 0951a403..6f7568b3 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -247,12 +247,10 @@ func diagnosticNotifications(snapshot *diagnosticSnapshot) []Notification { var r Range hasRange := false + text, hasText := sourceTextForFile(snapshot.ctx, filePath) for _, label := range diag.Labels { - if label.Location != nil && label.Location.Start != nil && label.Location.End != nil { - r = Range{ - Start: Position{Line: label.Location.Start.Line - 1, Character: label.Location.Start.Column - 1}, - End: Position{Line: label.Location.End.Line - 1, Character: label.Location.End.Column - 1}, - } + if labelRange, ok := rangeAtLocation(text, label.Location); hasText && ok { + r = labelRange hasRange = true break } diff --git a/internal/lsp/server_test.go b/internal/lsp/server_test.go index 36cd0e34..ae01b441 100644 --- a/internal/lsp/server_test.go +++ b/internal/lsp/server_test.go @@ -3,7 +3,6 @@ package lsp import ( "bufio" "bytes" - driver "compiler/internal/driver" "encoding/json" "io" "path/filepath" @@ -13,6 +12,7 @@ import ( "testing" "time" + "compiler/internal/driver" "compiler/internal/project" "compiler/internal/semantics/symbols" "compiler/internal/semantics/typeinfo" @@ -332,13 +332,14 @@ func TestParseBundledPreludeFileKeepsStdlibIdentity(t *testing.T) { writeWorkspaceProjectConfig(t, root, "app") writeWorkspaceFile(t, globalPath, "const stdout: i32 = 1;\n") - ctx := driver.NewCompilerContext(project.Config{ + ctx := compiler.NewCompilerContext(project.Config{ RootDir: root, ProjectName: "app", Extension: peeper.SourceExt, LibraryBaseDir: libraryBase, }, nil) - mod := driver.CompileFile(ctx, globalPath, "const stdout: i32 = 1;\n") + content := "const stdout: i32 = 1;\n" + mod := compiler.CompileFile(ctx, globalPath, &content) if mod == nil { t.Fatalf("expected compiled bundled library module") } @@ -540,7 +541,7 @@ func TestHoverReusesFreshCompiledSnapshot(t *testing.T) { hover, err := state.HandleHover(HoverParams{ TextDocumentPositionParams: TextDocumentPositionParams{ TextDocument: TextDocumentIdentifier{URI: DocumentURI(pathToURI(filePath))}, - Position: Position{Line: 2, Character: 9}, + Position: Position{Line: 2, Character: 8}, }, }) if err != nil { diff --git a/internal/lsp/state.go b/internal/lsp/state.go index 635581d2..74b44b34 100644 --- a/internal/lsp/state.go +++ b/internal/lsp/state.go @@ -1,13 +1,12 @@ package lsp import ( - "path/filepath" "strings" "sync" "time" "compiler/internal/diagnostics" - driver "compiler/internal/driver" + "compiler/internal/driver" "compiler/internal/frontend/ast" "compiler/internal/project" "compiler/pkg/manifest" @@ -137,6 +136,7 @@ func (s *ServerState) recompile(entryFile string) (*project.CompilerContext, *pr } func (s *ServerState) recompileLocked(entryFile string) (*project.CompilerContext, *project.Module) { + canonicalEntry := project.CanonicalPath(entryFile) diagBag := diagnostics.NewDiagnosticBag() sourceProject, err := manifest.ResolveSourceFileProject(entryFile) rootDir := sourceProject.RootDir @@ -145,7 +145,7 @@ func (s *ServerState) recompileLocked(entryFile string) (*project.CompilerContex RootDir: rootDir, ProjectName: projectName, } - ctx := driver.NewCompilerContext(cfg, diagBag) + ctx := compiler.NewCompilerContext(cfg, diagBag) ctx.Metrics = &project.CompileMetrics{} if err != nil { ctx.Diagnostics.Add(diagnostics.NewError( @@ -166,10 +166,10 @@ func (s *ServerState) recompileLocked(entryFile string) (*project.CompilerContex ctx.Metrics.AddDirtyFiles(len(dirtyFiles)) s.seedReusableModules(ctx, dirtyFiles) for cachedPath, cachedContent := range s.Cache { - driver.AddSource(ctx, cachedPath, cachedContent) + compiler.AddSource(ctx, cachedPath, cachedContent) } if virtualPath, content, ok := s.workspace.syntheticEntry(entryFile); ok { - if driver.CompileFile(ctx, virtualPath, content) != nil { + if compiler.CompileFile(ctx, virtualPath, &content) != nil { s.LastCtx = ctx s.LastMetrics = ctx.Metrics.Snapshot() s.captureModules(ctx) @@ -181,17 +181,18 @@ func (s *ServerState) recompileLocked(entryFile string) (*project.CompilerContex } } - absEntry, err := filepath.Abs(entryFile) for cachedPath, cachedContent := range s.Cache { - absCached, err2 := filepath.Abs(cachedPath) - if err2 != nil || (err == nil && absCached == absEntry) { + if project.CanonicalPath(cachedPath) == canonicalEntry { continue } - driver.AddSource(ctx, cachedPath, cachedContent) + compiler.AddSource(ctx, cachedPath, cachedContent) } - content := s.Cache[entryFile] - mod := driver.CompileFile(ctx, entryFile, content) + var overlay *string + if content, ok := s.Cache[canonicalEntry]; ok { + overlay = &content + } + mod := compiler.CompileFile(ctx, entryFile, overlay) s.LastCtx = ctx s.LastMetrics = ctx.Metrics.Snapshot() s.captureModules(ctx) diff --git a/internal/lsp/workspace_test.go b/internal/lsp/workspace_test.go index 044c9c39..e0792c04 100644 --- a/internal/lsp/workspace_test.go +++ b/internal/lsp/workspace_test.go @@ -499,6 +499,80 @@ func TestServerStateKeepsWorkspaceIndexAcrossRecompile(t *testing.T) { } } +func TestRecompileUsesEmptyDocumentOverlay(t *testing.T) { + root := t.TempDir() + writeWorkspaceProjectConfig(t, root, "app") + entry := filepath.Join(root, peeper.SourceDirName, "main"+peeper.SourceExt) + disk := "fn DiskOnly() -> i32 { return 7; }\n" + writeWorkspaceFile(t, entry, disk) + + state := NewServerState() + state.RootDir = root + empty := "" + state.applyDocumentSnapshot(entry, &empty, nil) + _, mod := state.recompile(entry) + if mod == nil { + t.Fatalf("empty overlay compile returned nil module") + } + if mod.ContentHash != ast.HashText("") { + t.Fatalf("empty overlay hash = %q, want empty source hash", mod.ContentHash) + } + + state.applyDocumentSnapshot(entry, nil, nil) + _, mod = state.recompile(entry) + if mod == nil { + t.Fatalf("disk compile returned nil module") + } + if mod.ContentHash != ast.HashText(disk) { + t.Fatalf("closed overlay hash = %q, want disk source hash", mod.ContentHash) + } +} + +func TestNavigationRangesUseUTF16AfterNonBMPText(t *testing.T) { + root := t.TempDir() + writeWorkspaceProjectConfig(t, root, "app") + entry := filepath.Join(root, peeper.SourceDirName, "main"+peeper.SourceExt) + marked := "fn main() -> i32 { let text: cstr = \"🙂\"; let x = 1; return " + hoverMarker + "x; }\n" + content, position := markerPosition(t, marked) + writeWorkspaceFile(t, entry, content) + + state := NewServerState() + state.RootDir = root + state.Cache[entry] = content + if _, mod := state.recompile(entry); mod == nil { + t.Fatalf("expected compiled module") + } + + definition, err := state.HandleDefinition(DefinitionParams{TextDocumentPositionParams: TextDocumentPositionParams{ + TextDocument: TextDocumentIdentifier{URI: DocumentURI(pathToURI(entry))}, + Position: position, + }}) + if err != nil || len(definition) != 1 { + t.Fatalf("definition = %#v, err = %v", definition, err) + } + start, startOK := offsetAtPosition(content, definition[0].Range.Start) + end, endOK := offsetAtPosition(content, definition[0].Range.End) + if !startOK || !endOK || content[start:end] != "x" { + t.Fatalf("definition range maps to %q, want x", content[start:end]) + } + + edit, err := state.HandleRename(RenameParams{ + TextDocument: TextDocumentIdentifier{URI: DocumentURI(pathToURI(entry))}, + Position: position, + NewName: "renamed", + }) + if err != nil || edit == nil { + t.Fatalf("rename = %#v, err = %v", edit, err) + } + for _, textEdit := range edit.Changes[DocumentURI(pathToURI(entry))] { + start, startOK := offsetAtPosition(content, textEdit.Range.Start) + end, endOK := offsetAtPosition(content, textEdit.Range.End) + if !startOK || !endOK || content[start:end] != "x" { + t.Fatalf("rename range maps to %q, want x", content[start:end]) + } + } +} + func writeWorkspaceFile(t *testing.T, path, content string) { t.Helper() if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { diff --git a/internal/pipeline/loader.go b/internal/pipeline/loader.go index d47fe772..4112cbaf 100644 --- a/internal/pipeline/loader.go +++ b/internal/pipeline/loader.go @@ -91,13 +91,14 @@ func (l *moduleLoader) loadModule(module *project.Module) { } return } - if module.Content == "" && module.FilePath != "" { + if !module.ContentProvided && module.Content == "" && module.FilePath != "" { content, err := os.ReadFile(module.FilePath) if err != nil { l.addImportError(nil, diagnostics.ErrModuleNotFound, "read module: "+err.Error()) return } module.Content = string(content) + module.ContentProvided = true } if l.ctx != nil && l.ctx.Diagnostics != nil && module.FilePath != "" { l.ctx.Diagnostics.AddSourceContent(module.FilePath, module.Content) diff --git a/internal/pipeline/pipeline.go b/internal/pipeline/pipeline.go index 4eecab05..630c7d34 100644 --- a/internal/pipeline/pipeline.go +++ b/internal/pipeline/pipeline.go @@ -9,8 +9,8 @@ import ( "compiler/internal/backend/llvm" "compiler/internal/diagnostics" "compiler/internal/graph" - "compiler/internal/ir/hir_fold" - "compiler/internal/ir/hir_lower" + "compiler/internal/ir/hir/fold" + "compiler/internal/ir/hir/lower" "compiler/internal/ir/mir" "compiler/internal/project" "compiler/internal/semantics/binder" @@ -324,11 +324,11 @@ func (p *Pipeline) advanceModulePhase(module *project.Module, diag *diagnostics. return true } if module.Phase < project.PhaseHIR { - modhir := hir_lower.GenerateHIR(p.ctx, module) + modhir := lower.GenerateHIR(p.ctx, module) if modhir == nil { return false } - modhir = hir_fold.ApplyConstantFolding(modhir, diag) + modhir = fold.ApplyConstantFolding(modhir, diag) module.HIR = modhir module.Phase = project.PhaseHIR p.ctx.Metrics.AddPhaseAdvance() diff --git a/internal/prelude/prelude.go b/internal/prelude/prelude.go index f0bfffb8..5fa62100 100644 --- a/internal/prelude/prelude.go +++ b/internal/prelude/prelude.go @@ -34,12 +34,13 @@ func ModuleForFile(ctx *project.CompilerContext, filePath, content string) (*pro return nil, false } return &project.Module{ - Key: "core:prelude/global", - ImportPath: "prelude/global", - FilePath: preludePath, - Namespace: "core", - Origin: project.ModuleOriginStdlib, - Content: content, + Key: "core:prelude/global", + ImportPath: "prelude/global", + FilePath: preludePath, + Namespace: "core", + Origin: project.ModuleOriginStdlib, + Content: content, + ContentProvided: true, }, true } diff --git a/internal/problems/problems.go b/internal/problems/problems.go index 442a4172..971ec24e 100644 --- a/internal/problems/problems.go +++ b/internal/problems/problems.go @@ -4,7 +4,6 @@ import ( "fmt" "compiler/internal/diagnostics" - "compiler/internal/project" "compiler/internal/semantics/table" "compiler/internal/source" ) @@ -18,16 +17,33 @@ func ArrayIndexOutOfBounds(index, length string, loc *source.Location) *diagnost return d } -func ReportRedeclaration(ctx *project.CompilerContext, scope *table.Scope, err string, name string, loc *source.Location) { - if ctx == nil || ctx.Diagnostics == nil { - return +func UnreachableCode(loc *source.Location) *diagnostics.Diagnostic { + return diagnostics.NewWarning("unreachable code"). + WithCode(diagnostics.WarnUnreachableCode). + WithPrimaryLabel(loc, "this code is unreachable"). + WithHelp("remove this code or restructure control flow") +} + +func Redeclaration(message string, current, previous *source.Location) *diagnostics.Diagnostic { + d := diagnostics.NewError(message). + WithCode(diagnostics.ErrRedeclaredSymbol). + WithPrimaryLabel(current, "redeclared here") + if previous != nil { + d.WithSecondaryLabel(previous, "first declared here") } - d := ctx.Diagnostics.AddError(diagnostics.ErrRedeclaredSymbol, err, loc, "redeclared here") - if scope == nil { + return d +} + +func ReportRedeclaration(diag *diagnostics.DiagnosticBag, scope *table.Scope, err string, name string, loc *source.Location) { + if diag == nil { return } - oldSym, _ := scope.LookupLocal(name) - if oldSym != nil && oldSym.Location != nil { - d.WithSecondaryLabel(oldSym.Location, "first declared here") + var previous *source.Location + if scope != nil { + oldSym, _ := scope.LookupLocal(name) + if oldSym != nil { + previous = oldSym.Location + } } + diag.Add(Redeclaration(err, loc, previous)) } diff --git a/internal/problems/problems_test.go b/internal/problems/problems_test.go new file mode 100644 index 00000000..1d887274 --- /dev/null +++ b/internal/problems/problems_test.go @@ -0,0 +1,64 @@ +package problems + +import ( + "testing" + + "compiler/internal/diagnostics" + "compiler/internal/source" +) + +func TestUnreachableCode(t *testing.T) { + loc := source.NewLocation("main.peep", source.Position{Line: 1, Column: 1}, source.Position{Line: 1, Column: 2}) + d := UnreachableCode(loc) + + if d.Severity != diagnostics.Warning { + t.Fatalf("severity = %s, want warning", d.Severity) + } + if d.Message != "unreachable code" { + t.Fatalf("message = %q, want %q", d.Message, "unreachable code") + } + if d.Code != diagnostics.WarnUnreachableCode { + t.Fatalf("code = %q, want %q", d.Code, diagnostics.WarnUnreachableCode) + } + if len(d.Labels) != 1 { + t.Fatalf("label count = %d, want 1", len(d.Labels)) + } + label := d.Labels[0] + if label.Location != loc || label.Message != "this code is unreachable" || label.Style != diagnostics.Primary { + t.Fatalf("primary label = %#v", label) + } + if len(d.Extras) != 1 { + t.Fatalf("extra count = %d, want 1", len(d.Extras)) + } + extra := d.Extras[0] + if extra.Kind != diagnostics.ExtraText || extra.Text.Kind != "help" || extra.Text.Message != "remove this code or restructure control flow" { + t.Fatalf("help text = %#v", extra) + } +} + +func TestRedeclaration(t *testing.T) { + current := source.NewLocation("main.peep", source.Position{Line: 2, Column: 1}, source.Position{Line: 2, Column: 2}) + previous := source.NewLocation("main.peep", source.Position{Line: 1, Column: 1}, source.Position{Line: 1, Column: 2}) + d := Redeclaration("symbol already declared", current, previous) + + if d.Severity != diagnostics.Error { + t.Fatalf("severity = %s, want error", d.Severity) + } + if d.Message != "symbol already declared" { + t.Fatalf("message = %q, want %q", d.Message, "symbol already declared") + } + if d.Code != diagnostics.ErrRedeclaredSymbol { + t.Fatalf("code = %q, want %q", d.Code, diagnostics.ErrRedeclaredSymbol) + } + if len(d.Labels) != 2 { + t.Fatalf("label count = %d, want 2", len(d.Labels)) + } + primary := d.Labels[0] + if primary.Location != current || primary.Message != "redeclared here" || primary.Style != diagnostics.Primary { + t.Fatalf("primary label = %#v", primary) + } + secondary := d.Labels[1] + if secondary.Location != previous || secondary.Message != "first declared here" || secondary.Style != diagnostics.Secondary { + t.Fatalf("secondary label = %#v", secondary) + } +} diff --git a/internal/project/modules.go b/internal/project/modules.go index 16f04a22..ad093176 100644 --- a/internal/project/modules.go +++ b/internal/project/modules.go @@ -103,6 +103,9 @@ type Module struct { Dependency string // Loaded source text. Content string + // ContentProvided distinguishes an explicit empty source from a module that + // still needs to load its source from FilePath. + ContentProvided bool // Reserved for incremental builds. ContentHash string // Stable syntax-derived import surface for invalidation. @@ -251,11 +254,12 @@ func (ctx *CompilerContext) NewModuleForFile(filePath, content string) *Module { } origin, namespace := ctx.ModuleOriginForFile(filePath) module := &Module{ - Key: ModuleKeyFor(origin, filePath), - FilePath: filePath, - Namespace: namespace, - Origin: origin, - Content: content, + Key: ModuleKeyFor(origin, filePath), + FilePath: filePath, + Namespace: namespace, + Origin: origin, + Content: content, + ContentProvided: true, } if importPath, err := ctx.ImportPathForFile(origin, namespace, filePath); err == nil { module.ImportPath = importPath diff --git a/internal/semantics/cfg/analyze.go b/internal/semantics/cfg/analyze.go index c0a8d76a..9c06f35f 100644 --- a/internal/semantics/cfg/analyze.go +++ b/internal/semantics/cfg/analyze.go @@ -4,6 +4,7 @@ import ( "compiler/internal/diagnostics" "compiler/internal/ir" "compiler/internal/ir/hir" + "compiler/internal/problems" "compiler/internal/source" ) @@ -41,12 +42,7 @@ func analyzeFunction(fn *Graph, diag *diagnostics.DiagnosticBag) { continue } loc := unreachableBlockLoc(block) - diag.Add( - diagnostics.NewWarning("unreachable code"). - WithCode(diagnostics.WarnUnreachableCode). - WithPrimaryLabel(loc, "this code is unreachable"). - WithHelp("remove this code or restructure control flow"), - ) + diag.Add(problems.UnreachableCode(loc)) } returnType, hasReturnType := fn.Types.Type(fn.ReturnType) diff --git a/internal/semantics/collector/collector.go b/internal/semantics/collector/collector.go index c91339a8..a3c62c24 100644 --- a/internal/semantics/collector/collector.go +++ b/internal/semantics/collector/collector.go @@ -83,13 +83,8 @@ func (c *collector) collectFnDecl(fn *ast.FnDecl) { } } if previous != nil { - d := diagnostics.NewError("method `"+fn.Name.Name+"` already declared for `"+targetKey+"`"). - WithCode(diagnostics.ErrRedeclaredSymbol). - WithPrimaryLabel(fn.Name.Location, "redeclared here") - if previous.Location != nil { - d.WithSecondaryLabel(previous.Location, "first declared here") - } - c.ctx.Diagnostics.Add(d) + message := "method `" + fn.Name.Name + "` already declared for `" + targetKey + "`" + c.ctx.Diagnostics.Add(problems.Redeclaration(message, fn.Name.Location, previous.Location)) return } sym := symbols.New(fn.Name.Name, symbols.SymbolMethod, fn, ast.LocOf(fn.Name)) @@ -101,7 +96,7 @@ func (c *collector) collectFnDecl(fn *ast.FnDecl) { sym := symbols.New(fn.Name.Name, symbols.SymbolFunc, fn, ast.LocOf(fn.Name)) sym.Scope = table.New(c.module.ModuleScope) if err := c.module.ModuleScope.Declare(sym); err != nil { - problems.ReportRedeclaration(c.ctx, c.module.ModuleScope, err.Error(), fn.Name.Name, fn.Name.Location) + problems.ReportRedeclaration(c.ctx.Diagnostics, c.module.ModuleScope, err.Error(), fn.Name.Name, fn.Name.Location) return } } @@ -120,7 +115,7 @@ func (c *collector) collectConcreteTypeDecl(name *ast.Ident, typ ast.TypeExpr, n // Underlying is filled by binder. } if err := c.module.ModuleScope.Declare(sym); err != nil { - problems.ReportRedeclaration(c.ctx, c.module.ModuleScope, err.Error(), name.Name, name.Location) + problems.ReportRedeclaration(c.ctx.Diagnostics, c.module.ModuleScope, err.Error(), name.Name, name.Location) return } } @@ -132,7 +127,7 @@ func (c *collector) collectModuleBinding(name *ast.Ident, kind symbols.Kind, typ sym := symbols.New(name.Name, kind, node, ast.LocOf(name)) sym.Type = &typeinfo.UnknownType{} // binder fills real type if err := c.module.ModuleScope.Declare(sym); err != nil { - problems.ReportRedeclaration(c.ctx, c.module.ModuleScope, err.Error(), name.Name, name.Location) + problems.ReportRedeclaration(c.ctx.Diagnostics, c.module.ModuleScope, err.Error(), name.Name, name.Location) } } diff --git a/internal/semantics/ownership/ownership_test.go b/internal/semantics/ownership/ownership_test.go index 2a694659..e62c6541 100644 --- a/internal/semantics/ownership/ownership_test.go +++ b/internal/semantics/ownership/ownership_test.go @@ -10,7 +10,7 @@ import ( "compiler/internal/frontend/lexer" "compiler/internal/frontend/parser" "compiler/internal/ir" - "compiler/internal/ir/hir_lower" + "compiler/internal/ir/hir/lower" "compiler/internal/project" "compiler/internal/semantics/binder" "compiler/internal/semantics/cfg" @@ -49,7 +49,7 @@ func checkOwnershipSource(t *testing.T, src string) *ownershipResult { binder.Bind(ctx, module) resolver.Resolve(ctx, module) typechecker.Check(ctx, module) - module.HIR = hir_lower.GenerateHIR(ctx, module) + module.HIR = lower.GenerateHIR(ctx, module) module.CFG = cfg.BuildModule(module.HIR) Check(ctx, module) return &ownershipResult{DiagnosticBag: diag, ctx: ctx, module: module} diff --git a/internal/semantics/resolver/resolver.go b/internal/semantics/resolver/resolver.go index 1c6f1a51..c6ae8476 100644 --- a/internal/semantics/resolver/resolver.go +++ b/internal/semantics/resolver/resolver.go @@ -105,7 +105,7 @@ func (r *resolver) resolveFunction(fn *ast.FnDecl) { paramSym.IsReceiver = fn.Receiver != nil && i == 0 paramSym.Initialized = true if err := funcScope.Declare(paramSym); err != nil { - problems.ReportRedeclaration(r.ctx, funcScope, err.Error(), param.Name.Name, param.Name.Location) + problems.ReportRedeclaration(r.ctx.Diagnostics, funcScope, err.Error(), param.Name.Name, param.Name.Location) return } } @@ -206,7 +206,7 @@ func (r *resolver) resolveLocalBinding(scope *table.Scope, name *ast.Ident, kind sym := symbols.New(name.Name, kind, node, ast.LocOf(name)) sym.Initializing = true if err := scope.Declare(sym); err != nil { - problems.ReportRedeclaration(r.ctx, scope, err.Error(), name.Name, loc) + problems.ReportRedeclaration(r.ctx.Diagnostics, scope, err.Error(), name.Name, loc) return } if value != nil { diff --git a/internal/semantics/typeinfo/info.go b/internal/semantics/typeinfo/types.go similarity index 100% rename from internal/semantics/typeinfo/info.go rename to internal/semantics/typeinfo/types.go diff --git a/internal/semantics/typeinfo/info_test.go b/internal/semantics/typeinfo/types_test.go similarity index 100% rename from internal/semantics/typeinfo/info_test.go rename to internal/semantics/typeinfo/types_test.go diff --git a/internal/source/location.go b/internal/source/location.go index 5aeb53f7..fbd85d64 100644 --- a/internal/source/location.go +++ b/internal/source/location.go @@ -47,24 +47,25 @@ func (l *Location) GetText(cache SourceCache) string { return "" } if l.Start.Line == l.End.Line { - line := lines[0] + line := []rune(lines[0]) if l.Start.Column < 1 || l.End.Column < l.Start.Column || l.End.Column > len(line)+1 { return "" } - return line[l.Start.Column-1 : l.End.Column-1] + return string(line[l.Start.Column-1 : l.End.Column-1]) } var result strings.Builder for i, line := range lines { + runes := []rune(line) lineNum := l.Start.Line + i switch lineNum { case l.Start.Line: - if l.Start.Column >= 1 && l.Start.Column <= len(line)+1 { - result.WriteString(line[l.Start.Column-1:]) + if l.Start.Column >= 1 && l.Start.Column <= len(runes)+1 { + result.WriteString(string(runes[l.Start.Column-1:])) } case l.End.Line: - if l.End.Column >= 1 && l.End.Column <= len(line)+1 { + if l.End.Column >= 1 && l.End.Column <= len(runes)+1 { result.WriteString("\n") - result.WriteString(line[:l.End.Column-1]) + result.WriteString(string(runes[:l.End.Column-1])) } default: result.WriteString("\n") diff --git a/internal/source/source_test.go b/internal/source/source_test.go index f37bbc1e..394c48d4 100644 --- a/internal/source/source_test.go +++ b/internal/source/source_test.go @@ -14,10 +14,13 @@ type fakeCache struct { } func (f fakeCache) GetLinesRange(_ string, startLine, endLine int) ([]string, bool) { - if !f.ok || startLine < 1 || endLine < startLine { + if !f.ok || startLine < 1 || endLine < startLine || startLine > len(f.lines) { return nil, false } - return f.lines, true + if endLine > len(f.lines) { + endLine = len(f.lines) + } + return f.lines[startLine-1 : endLine], true } func TestPositionAdvance(t *testing.T) { @@ -53,6 +56,27 @@ func TestLocationGetTextAndRange(t *testing.T) { } } +func TestLocationGetTextUsesRuneColumns(t *testing.T) { + cache := fakeCache{ok: true, lines: []string{"🙂abc", "𝄞def"}} + tests := []struct { + name string + start, end Position + want string + }{ + {name: "after emoji", start: Position{Line: 1, Column: 2}, end: Position{Line: 1, Column: 5}, want: "abc"}, + {name: "supplementary rune", start: Position{Line: 2, Column: 1}, end: Position{Line: 2, Column: 2}, want: "𝄞"}, + {name: "multiline", start: Position{Line: 1, Column: 2}, end: Position{Line: 2, Column: 3}, want: "abc\n𝄞d"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + location := NewLocation("ignored", tt.start, tt.end) + if got := location.GetText(cache); got != tt.want { + t.Fatalf("GetText = %q, want %q", got, tt.want) + } + }) + } +} + func TestGetSourceLinesRangeUsesCache(t *testing.T) { lines, err := GetSourceLinesRange("ignored", 1, 1, fakeCache{ok: true, lines: []string{"cached"}}) if err != nil { diff --git a/internal/target/llvm_triple.go b/internal/target/llvm_triple.go index 2790ba53..002a1f0c 100644 --- a/internal/target/llvm_triple.go +++ b/internal/target/llvm_triple.go @@ -6,17 +6,65 @@ import ( "strings" ) +type targetKey struct { + OS string + Arch string +} + +var llvmTriples = map[targetKey]string{ + {OS: "aix", Arch: "ppc64"}: "powerpc64-ibm-aix", + {OS: "android", Arch: "386"}: "i386-linux-android", + {OS: "android", Arch: "amd64"}: "x86_64-linux-android", + {OS: "android", Arch: "arm"}: "arm-linux-android", + {OS: "android", Arch: "arm64"}: "aarch64-linux-android", + {OS: "darwin", Arch: "amd64"}: "x86_64-apple-darwin", + {OS: "darwin", Arch: "arm64"}: "aarch64-apple-darwin", + {OS: "dragonfly", Arch: "amd64"}: "x86_64-unknown-dragonfly", + {OS: "freebsd", Arch: "386"}: "i386-unknown-freebsd", + {OS: "freebsd", Arch: "amd64"}: "x86_64-unknown-freebsd", + {OS: "freebsd", Arch: "arm"}: "arm-unknown-freebsd", + {OS: "freebsd", Arch: "arm64"}: "aarch64-unknown-freebsd", + {OS: "illumos", Arch: "amd64"}: "x86_64-unknown-illumos", + {OS: "ios", Arch: "amd64"}: "x86_64-apple-ios", + {OS: "ios", Arch: "arm64"}: "aarch64-apple-ios", + {OS: "linux", Arch: "386"}: "i386-unknown-linux-gnu", + {OS: "linux", Arch: "amd64"}: "x86_64-unknown-linux-gnu", + {OS: "linux", Arch: "arm"}: "arm-unknown-linux-gnu", + {OS: "linux", Arch: "arm64"}: "aarch64-unknown-linux-gnu", + {OS: "linux", Arch: "loong64"}: "loongarch64-unknown-linux-gnu", + {OS: "linux", Arch: "mips"}: "mips-unknown-linux-gnu", + {OS: "linux", Arch: "mips64"}: "mips64-unknown-linux-gnu", + {OS: "linux", Arch: "mips64le"}: "mips64el-unknown-linux-gnu", + {OS: "linux", Arch: "mipsle"}: "mipsel-unknown-linux-gnu", + {OS: "linux", Arch: "ppc64"}: "powerpc64-unknown-linux-gnu", + {OS: "linux", Arch: "ppc64le"}: "powerpc64le-unknown-linux-gnu", + {OS: "linux", Arch: "riscv64"}: "riscv64-unknown-linux-gnu", + {OS: "linux", Arch: "s390x"}: "s390x-unknown-linux-gnu", + {OS: "netbsd", Arch: "386"}: "i386-unknown-netbsd", + {OS: "netbsd", Arch: "amd64"}: "x86_64-unknown-netbsd", + {OS: "netbsd", Arch: "arm"}: "arm-unknown-netbsd", + {OS: "netbsd", Arch: "arm64"}: "aarch64-unknown-netbsd", + {OS: "openbsd", Arch: "386"}: "i386-unknown-openbsd", + {OS: "openbsd", Arch: "amd64"}: "x86_64-unknown-openbsd", + {OS: "openbsd", Arch: "arm"}: "arm-unknown-openbsd", + {OS: "openbsd", Arch: "arm64"}: "aarch64-unknown-openbsd", + {OS: "openbsd", Arch: "ppc64"}: "powerpc64-unknown-openbsd", + {OS: "openbsd", Arch: "riscv64"}: "riscv64-unknown-openbsd", + {OS: "solaris", Arch: "amd64"}: "x86_64-sun-solaris", + {OS: "wasip1", Arch: "wasm"}: "wasm32-unknown-wasi", + {OS: "windows", Arch: "386"}: "i386-pc-windows-msvc", + {OS: "windows", Arch: "amd64"}: "x86_64-pc-windows-msvc", + {OS: "windows", Arch: "arm64"}: "aarch64-pc-windows-msvc", +} + // LLVMTriple returns the canonical LLVM target triple for a normalized GOOS/GOARCH pair. func LLVMTriple(targetOS, targetArch string) (string, error) { - arch, err := llvmArch(targetArch) - if err != nil { - return "", err - } - platform, err := llvmPlatform(targetOS) - if err != nil { - return "", err + targetOS = NormalizeOS(targetOS) + targetArch = NormalizeArch(targetArch) + if triple, ok := llvmTriples[targetKey{OS: targetOS, Arch: targetArch}]; ok { + return triple, nil } - return arch + "-" + platform, nil + return "", fmt.Errorf("unsupported target combination %q", targetOS+"/"+targetArch) } // NormalizeOS trims and lowercases a target OS, defaulting to host when empty. @@ -49,71 +97,3 @@ func ExecutableExt(targetOS string) string { func IsHostTarget(targetOS, targetArch string) bool { return NormalizeOS(targetOS) == runtime.GOOS && NormalizeArch(targetArch) == runtime.GOARCH } - -func llvmArch(targetArch string) (string, error) { - switch arch := NormalizeArch(targetArch); arch { - case "386": - return "i386", nil - case "amd64": - return "x86_64", nil - case "arm": - return "arm", nil - case "arm64": - return "aarch64", nil - case "loong64": - return "loongarch64", nil - case "mips": - return "mips", nil - case "mips64": - return "mips64", nil - case "mips64le": - return "mips64el", nil - case "mipsle": - return "mipsel", nil - case "ppc64": - return "powerpc64", nil - case "ppc64le": - return "powerpc64le", nil - case "riscv64": - return "riscv64", nil - case "s390x": - return "s390x", nil - case "wasm": - return "wasm32", nil - default: - return "", fmt.Errorf("unsupported target architecture %q", targetArch) - } -} - -func llvmPlatform(targetOS string) (string, error) { - switch os := NormalizeOS(targetOS); os { - case "aix": - return "ibm-aix", nil - case "android": - return "linux-android", nil - case "darwin": - return "apple-darwin", nil - case "dragonfly": - return "unknown-dragonfly", nil - case "freebsd": - return "unknown-freebsd", nil - case "illumos": - return "unknown-illumos", nil - case "ios": - return "apple-ios", nil - case "linux": - return "unknown-linux-gnu", nil - case "netbsd": - return "unknown-netbsd", nil - case "openbsd": - return "unknown-openbsd", nil - case "solaris": - return "sun-solaris", nil - case "wasip1": - return "unknown-wasi", nil - case "windows": - return "pc-windows-msvc", nil - default: - return "", fmt.Errorf("unsupported target operating system %q", targetOS) - } -} diff --git a/internal/target/llvm_triple_test.go b/internal/target/llvm_triple_test.go index 094dae40..bfc671a2 100644 --- a/internal/target/llvm_triple_test.go +++ b/internal/target/llvm_triple_test.go @@ -35,20 +35,34 @@ func TestLLVMTriple(t *testing.T) { } func TestLLVMTripleRejectsUnknownTarget(t *testing.T) { - _, err := LLVMTriple("linux", "mystery") - if err == nil { - t.Fatal("LLVMTriple returned nil error for unknown arch") - } - if !strings.Contains(err.Error(), "unsupported target architecture") { - t.Fatalf("unexpected error: %v", err) + for _, pair := range [][2]string{{"linux", "mystery"}, {"mystery", "amd64"}, {"linux", "wasm"}, {"wasip1", "amd64"}, {"darwin", "386"}, {"aix", "amd64"}, {"windows", "mips"}} { + _, err := LLVMTriple(pair[0], pair[1]) + want := "unsupported target combination \"" + pair[0] + "/" + pair[1] + "\"" + if err == nil || err.Error() != want { + t.Fatalf("LLVMTriple(%q, %q) error = %v, want %q", pair[0], pair[1], err, want) + } } +} - _, err = LLVMTriple("mystery", "amd64") - if err == nil { - t.Fatal("LLVMTriple returned nil error for unknown os") +func TestLLVMTripleAcceptsGoTargetIntersection(t *testing.T) { + allowed := map[string][]string{ + "aix": {"ppc64"}, "android": {"386", "amd64", "arm", "arm64"}, "darwin": {"amd64", "arm64"}, + "dragonfly": {"amd64"}, "freebsd": {"386", "amd64", "arm", "arm64"}, "illumos": {"amd64"}, + "ios": {"amd64", "arm64"}, "linux": {"386", "amd64", "arm", "arm64", "loong64", "mips", "mips64", "mips64le", "mipsle", "ppc64", "ppc64le", "riscv64", "s390x"}, + "netbsd": {"386", "amd64", "arm", "arm64"}, "openbsd": {"386", "amd64", "arm", "arm64", "ppc64", "riscv64"}, + "solaris": {"amd64"}, "wasip1": {"wasm"}, "windows": {"386", "amd64", "arm64"}, + } + count := 0 + for targetOS, architectures := range allowed { + for _, targetArch := range architectures { + count++ + if triple, err := LLVMTriple(targetOS, targetArch); err != nil || triple == "" { + t.Fatalf("LLVMTriple(%q, %q) = %q, %v", targetOS, targetArch, triple, err) + } + } } - if !strings.Contains(err.Error(), "unsupported target operating system") { - t.Fatalf("unexpected error: %v", err) + if count != len(llvmTriples) { + t.Fatalf("accepted pair count = %d, implementation count = %d", count, len(llvmTriples)) } } diff --git a/internal/target/wordsize_test.go b/internal/target/wordsize_test.go index 6f8b6697..27ff866e 100644 --- a/internal/target/wordsize_test.go +++ b/internal/target/wordsize_test.go @@ -72,8 +72,8 @@ func TestInfoUsesArchitectureWidthAndTriple(t *testing.T) { func TestInfoRejectsUnsupportedTarget(t *testing.T) { _, err := New("linux", "mystery") - if err == nil || !strings.Contains(err.Error(), "unsupported target architecture") { - t.Fatalf("New returned %v, want unsupported architecture", err) + if err == nil || !strings.Contains(err.Error(), "unsupported target combination") { + t.Fatalf("New returned %v, want unsupported target combination", err) } } diff --git a/new_node_extension_flow.md b/new_node_extension_flow.md index 6f3023fc..35f73232 100644 --- a/new_node_extension_flow.md +++ b/new_node_extension_flow.md @@ -166,7 +166,7 @@ Always if type has real semantics: If runtime representation matters: -- `internal/ir/hir_lower/lower.go` +- `internal/ir/hir/lower/module_lower.go` - maybe `internal/ir/mir/model.go` - maybe backend lowering @@ -224,7 +224,7 @@ If name binding/desugaring changes: ### HIR lowering -- `internal/ir/hir_lower/lower.go` +- `internal/ir/hir/lower/module_lower.go` ### MIR / backend diff --git a/pkg/ascii/ascii.go b/pkg/ascii/ascii.go index 830290c3..e5cb7883 100644 --- a/pkg/ascii/ascii.go +++ b/pkg/ascii/ascii.go @@ -11,4 +11,4 @@ func IsDigit(r rune) bool { func IsAlnum(r rune) bool { return IsLetter(r) || IsDigit(r) -} \ No newline at end of file +} diff --git a/pkg/remotes/remotes.go b/pkg/remotes/remotes.go index e43d7251..826aa1d0 100644 --- a/pkg/remotes/remotes.go +++ b/pkg/remotes/remotes.go @@ -75,4 +75,4 @@ func StripProviderPrefix(path string) string { return repoPath } return path -} \ No newline at end of file +} diff --git a/x_test/copy_move_semantics/peeper.toml b/x_test/copy_move_semantics/peeper.toml index 8f095ddd..839a9ddb 100644 --- a/x_test/copy_move_semantics/peeper.toml +++ b/x_test/copy_move_semantics/peeper.toml @@ -1,2 +1,6 @@ name = "copy_move_semantics" build = "program" + +[test] +mode = "check" +outcome = "success" diff --git a/x_test/default_interface_evidence/peeper.toml b/x_test/default_interface_evidence/peeper.toml index 07728851..e59745d0 100644 --- a/x_test/default_interface_evidence/peeper.toml +++ b/x_test/default_interface_evidence/peeper.toml @@ -1,2 +1,6 @@ name = "default_interface_evidence" build = "program" + +[test] +mode = "check" +outcome = "success" diff --git a/x_test/default_parameters/peeper.toml b/x_test/default_parameters/peeper.toml index 32346985..166c9385 100644 --- a/x_test/default_parameters/peeper.toml +++ b/x_test/default_parameters/peeper.toml @@ -1,2 +1,6 @@ name = "default_parameters" build = "program" + +[test] +mode = "check" +outcome = "success" diff --git a/x_test/fixtures_test.go b/x_test/fixtures_test.go new file mode 100644 index 00000000..12bb739e --- /dev/null +++ b/x_test/fixtures_test.go @@ -0,0 +1,184 @@ +package xtest_test + +import ( + "bytes" + "context" + "errors" + "os" + "os/exec" + "path/filepath" + "slices" + "strings" + "testing" + "time" + + "compiler/pkg/toml" +) + +type fixtureExpectation struct { + Name string + Dir string + Mode string + Outcome string + ExitCode int + CompilerArgs []string + ProgramArgs []string + StdoutContains []string + StderrContains []string +} + +func TestFixtureContracts(t *testing.T) { + manifests, err := filepath.Glob(filepath.Join("*", "peeper.toml")) + if err != nil { + t.Fatalf("discover fixtures: %v", err) + } + slices.Sort(manifests) + if len(manifests) == 0 { + t.Fatal("no fixture manifests found") + } + + expectations := make([]fixtureExpectation, 0, len(manifests)) + for _, manifestPath := range manifests { + expectations = append(expectations, readFixtureExpectation(t, manifestPath)) + } + + binary := os.Getenv("PEEPER_BIN") + if binary == "" { + t.Skip("PEEPER_BIN not set; fixture manifests validated without execution") + } + binary, err = filepath.Abs(binary) + if err != nil { + t.Fatalf("resolve PEEPER_BIN: %v", err) + } + for _, expectation := range expectations { + t.Run(expectation.Name, func(t *testing.T) { + runFixture(t, binary, expectation) + }) + } +} + +func readFixtureExpectation(t *testing.T, manifestPath string) fixtureExpectation { + t.Helper() + data, err := toml.ParseFile(manifestPath) + if err != nil { + t.Fatalf("parse %s: %v", manifestPath, err) + } + section, ok := data.Section("test") + if !ok { + t.Fatalf("%s has no [test] section", manifestPath) + } + name := filepath.Base(filepath.Dir(manifestPath)) + expectation := fixtureExpectation{Name: name, Dir: filepath.Dir(manifestPath)} + expectation.Mode = requiredFixtureValue[string](t, section, manifestPath, "mode") + expectation.Outcome = requiredFixtureValue[string](t, section, manifestPath, "outcome") + expectation.ExitCode = optionalFixtureValue[int](t, section, manifestPath, "exit_code") + expectation.CompilerArgs = optionalFixtureValue[[]string](t, section, manifestPath, "compiler_args") + expectation.ProgramArgs = optionalFixtureValue[[]string](t, section, manifestPath, "program_args") + expectation.StdoutContains = optionalFixtureValue[[]string](t, section, manifestPath, "stdout_contains") + expectation.StderrContains = optionalFixtureValue[[]string](t, section, manifestPath, "stderr_contains") + if !slices.Contains([]string{"check", "build", "run"}, expectation.Mode) { + t.Fatalf("%s has invalid mode %q", manifestPath, expectation.Mode) + } + if !slices.Contains([]string{"success", "failure", "exit_code"}, expectation.Outcome) { + t.Fatalf("%s has invalid outcome %q", manifestPath, expectation.Outcome) + } + if expectation.Outcome == "exit_code" && expectation.Mode != "run" { + t.Fatalf("%s uses exit_code outside run mode", manifestPath) + } + if expectation.Outcome != "exit_code" && expectation.ExitCode != 0 { + t.Fatalf("%s sets exit_code without exit_code outcome", manifestPath) + } + return expectation +} + +func requiredFixtureValue[T any](t *testing.T, section toml.Table, path, key string) T { + t.Helper() + value, found, err := toml.LookupKey[T](section, key) + if err != nil || !found { + t.Fatalf("%s invalid or missing %s: %v", path, key, err) + } + return value +} + +func optionalFixtureValue[T any](t *testing.T, section toml.Table, path, key string) T { + t.Helper() + value, _, err := toml.LookupKey[T](section, key) + if err != nil { + t.Fatalf("%s invalid %s: %v", path, key, err) + } + return value +} + +func runFixture(t *testing.T, binary string, expectation fixtureExpectation) { + t.Helper() + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + compilerArgs := append([]string{"-logformat", "normal"}, expectation.CompilerArgs...) + if expectation.Mode == "check" { + checkArgs := append([]string{"check"}, compilerArgs...) + checkArgs = append(checkArgs, expectation.Dir) + stdout, stderr, exitCode := executeFixtureProcess(t, ctx, binary, checkArgs...) + checkFixtureOutcome(t, expectation, stdout, stderr, exitCode) + return + } + + executable := filepath.Join(t.TempDir(), "fixture") + buildArgs := append([]string{"build"}, compilerArgs...) + buildArgs = append(buildArgs, "-o", executable, expectation.Dir) + stdout, stderr, exitCode := executeFixtureProcess(t, ctx, binary, buildArgs...) + if expectation.Mode == "build" { + checkFixtureOutcome(t, expectation, stdout, stderr, exitCode) + return + } + if exitCode != 0 { + t.Fatalf("fixture build failed with exit %d\nstdout:\n%s\nstderr:\n%s", exitCode, stdout, stderr) + } + stdout, stderr, exitCode = executeFixtureProcess(t, ctx, executable, expectation.ProgramArgs...) + checkFixtureOutcome(t, expectation, stdout, stderr, exitCode) +} + +func executeFixtureProcess(t *testing.T, ctx context.Context, command string, args ...string) (string, string, int) { + t.Helper() + cmd := exec.CommandContext(ctx, command, args...) + var stdout bytes.Buffer + var stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err := cmd.Run() + if err == nil { + return stdout.String(), stderr.String(), 0 + } + if exitErr, ok := errors.AsType[*exec.ExitError](err); ok { + return stdout.String(), stderr.String(), exitErr.ExitCode() + } + t.Fatalf("execute %s: %v", command, err) + return "", "", -1 +} + +func checkFixtureOutcome(t *testing.T, expectation fixtureExpectation, stdout, stderr string, exitCode int) { + t.Helper() + switch expectation.Outcome { + case "success": + if exitCode != 0 { + t.Fatalf("exit = %d, want 0\nstdout:\n%s\nstderr:\n%s", exitCode, stdout, stderr) + } + case "failure": + if exitCode == 0 { + t.Fatalf("exit = 0, want failure\nstdout:\n%s\nstderr:\n%s", stdout, stderr) + } + case "exit_code": + if exitCode != expectation.ExitCode { + t.Fatalf("exit = %d, want %d\nstdout:\n%s\nstderr:\n%s", exitCode, expectation.ExitCode, stdout, stderr) + } + } + for _, text := range expectation.StdoutContains { + if !strings.Contains(stdout, text) { + t.Fatalf("stdout missing %q:\n%s", text, stdout) + } + } + for _, text := range expectation.StderrContains { + if !strings.Contains(stderr, text) { + t.Fatalf("stderr missing %q:\n%s", text, stderr) + } + } +} diff --git a/x_test/import_basic/peeper.toml b/x_test/import_basic/peeper.toml index 4522d23b..2b1cb98a 100644 --- a/x_test/import_basic/peeper.toml +++ b/x_test/import_basic/peeper.toml @@ -1,2 +1,6 @@ name = "import_basic" build = "program" + +[test] +mode = "check" +outcome = "success" diff --git a/x_test/import_default_parameters/peeper.toml b/x_test/import_default_parameters/peeper.toml index 7a53747a..fd6770d9 100644 --- a/x_test/import_default_parameters/peeper.toml +++ b/x_test/import_default_parameters/peeper.toml @@ -1,2 +1,6 @@ name = "import_default_parameters" build = "program" + +[test] +mode = "check" +outcome = "success" diff --git a/x_test/import_void_call/peeper.toml b/x_test/import_void_call/peeper.toml index 4afe5c8e..0c3998b9 100644 --- a/x_test/import_void_call/peeper.toml +++ b/x_test/import_void_call/peeper.toml @@ -1,2 +1,6 @@ name = "import_void_call" build = "program" + +[test] +mode = "check" +outcome = "success" diff --git a/x_test/lsp_completion/peeper.toml b/x_test/lsp_completion/peeper.toml index b59bf8d4..637bc9ac 100644 --- a/x_test/lsp_completion/peeper.toml +++ b/x_test/lsp_completion/peeper.toml @@ -1,2 +1,6 @@ name = "lsp_completion" build = "program" + +[test] +mode = "check" +outcome = "success" diff --git a/x_test/negative_alloc_missing_value/peeper.toml b/x_test/negative_alloc_missing_value/peeper.toml index 8013ab5e..4672247d 100644 --- a/x_test/negative_alloc_missing_value/peeper.toml +++ b/x_test/negative_alloc_missing_value/peeper.toml @@ -1,2 +1,7 @@ name = "negative_alloc_missing_value" build = "lib" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_alloc_reference_storage/peeper.toml b/x_test/negative_alloc_reference_storage/peeper.toml index 5242c44f..e13016f5 100644 --- a/x_test/negative_alloc_reference_storage/peeper.toml +++ b/x_test/negative_alloc_reference_storage/peeper.toml @@ -1,3 +1,8 @@ name = "negative_alloc_reference_storage" build = "program" entry = "src/main.peep" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_alloc_wrong_allocator/peeper.toml b/x_test/negative_alloc_wrong_allocator/peeper.toml index c2c7fc76..686fd2c7 100644 --- a/x_test/negative_alloc_wrong_allocator/peeper.toml +++ b/x_test/negative_alloc_wrong_allocator/peeper.toml @@ -1,3 +1,8 @@ name = "negative_alloc_wrong_allocator" build = "program" entry = "src/main.peep" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_assignment_base_move/peeper.toml b/x_test/negative_assignment_base_move/peeper.toml index d5c5b37e..8272ced2 100644 --- a/x_test/negative_assignment_base_move/peeper.toml +++ b/x_test/negative_assignment_base_move/peeper.toml @@ -1,2 +1,7 @@ name = "negative_assignment_base_move" build = "lib" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_bad_import/peeper.toml b/x_test/negative_bad_import/peeper.toml index 3e5ee982..df96dd06 100644 --- a/x_test/negative_bad_import/peeper.toml +++ b/x_test/negative_bad_import/peeper.toml @@ -1,2 +1,7 @@ name = "negative_bad_import" build = "lib" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["M0003"] diff --git a/x_test/negative_bad_import/src/bad_import.peep b/x_test/negative_bad_import/src/bad_import.peep index 558d5fd7..203a1d83 100644 --- a/x_test/negative_bad_import/src/bad_import.peep +++ b/x_test/negative_bad_import/src/bad_import.peep @@ -1 +1 @@ -//import as bar; +import "../outside"; diff --git a/x_test/negative_bare_interface_value/peeper.toml b/x_test/negative_bare_interface_value/peeper.toml index c514199d..036df618 100644 --- a/x_test/negative_bare_interface_value/peeper.toml +++ b/x_test/negative_bare_interface_value/peeper.toml @@ -1,3 +1,8 @@ name = "negative_bare_interface_value" build = "program" entry = "src/main.peep" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_bitwise_shift_count/peeper.toml b/x_test/negative_bitwise_shift_count/peeper.toml index 522e7e3b..d33e822c 100644 --- a/x_test/negative_bitwise_shift_count/peeper.toml +++ b/x_test/negative_bitwise_shift_count/peeper.toml @@ -1,2 +1,7 @@ name = "negative_bitwise_shift_count" build = "program" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_bitwise_types/peeper.toml b/x_test/negative_bitwise_types/peeper.toml index b9828fc8..f3db2b75 100644 --- a/x_test/negative_bitwise_types/peeper.toml +++ b/x_test/negative_bitwise_types/peeper.toml @@ -1,2 +1,7 @@ name = "negative_bitwise_types" build = "program" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_borrow_conflict/peeper.toml b/x_test/negative_borrow_conflict/peeper.toml index c84bd990..1e3e8403 100644 --- a/x_test/negative_borrow_conflict/peeper.toml +++ b/x_test/negative_borrow_conflict/peeper.toml @@ -1,2 +1,7 @@ name = "negative_borrow_conflict" build = "lib" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_borrowed_interface_consume/peeper.toml b/x_test/negative_borrowed_interface_consume/peeper.toml index ced2bb77..3a5d0b1d 100644 --- a/x_test/negative_borrowed_interface_consume/peeper.toml +++ b/x_test/negative_borrowed_interface_consume/peeper.toml @@ -1,3 +1,8 @@ name = "negative_borrowed_interface_consume" build = "program" entry = "src/main.peep" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_default_parameter_arity/peeper.toml b/x_test/negative_default_parameter_arity/peeper.toml index 1bdec64a..95355756 100644 --- a/x_test/negative_default_parameter_arity/peeper.toml +++ b/x_test/negative_default_parameter_arity/peeper.toml @@ -1,2 +1,7 @@ name = "negative_default_parameter_arity" build = "program" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_default_parameter_effect/peeper.toml b/x_test/negative_default_parameter_effect/peeper.toml index ef7f4ce6..b88d5d20 100644 --- a/x_test/negative_default_parameter_effect/peeper.toml +++ b/x_test/negative_default_parameter_effect/peeper.toml @@ -1,2 +1,7 @@ name = "negative_default_parameter_effect" build = "program" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_default_parameter_move/peeper.toml b/x_test/negative_default_parameter_move/peeper.toml index 58464f31..5f912324 100644 --- a/x_test/negative_default_parameter_move/peeper.toml +++ b/x_test/negative_default_parameter_move/peeper.toml @@ -1,2 +1,7 @@ name = "negative_default_parameter_move" build = "program" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_direct_call_implicit_borrow/peeper.toml b/x_test/negative_direct_call_implicit_borrow/peeper.toml index 8614d14f..8d9333ed 100644 --- a/x_test/negative_direct_call_implicit_borrow/peeper.toml +++ b/x_test/negative_direct_call_implicit_borrow/peeper.toml @@ -1,2 +1,7 @@ name = "negative_direct_call_implicit_borrow" build = "lib" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_direct_string_index/peeper.toml b/x_test/negative_direct_string_index/peeper.toml index c5aa10d6..34975838 100644 --- a/x_test/negative_direct_string_index/peeper.toml +++ b/x_test/negative_direct_string_index/peeper.toml @@ -1,2 +1,7 @@ name = "negative_direct_string_index" build = "lib" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_discarded_owner_projection/peeper.toml b/x_test/negative_discarded_owner_projection/peeper.toml index 16d3b00c..b15d5b2e 100644 --- a/x_test/negative_discarded_owner_projection/peeper.toml +++ b/x_test/negative_discarded_owner_projection/peeper.toml @@ -1,2 +1,7 @@ name = "negative_discarded_owner_projection" build = "lib" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_dynamic_array_append_element_move/peeper.toml b/x_test/negative_dynamic_array_append_element_move/peeper.toml index d116a58b..320959b9 100644 --- a/x_test/negative_dynamic_array_append_element_move/peeper.toml +++ b/x_test/negative_dynamic_array_append_element_move/peeper.toml @@ -2,3 +2,7 @@ name = "negative_dynamic_array_append_element_move" build = "program" entry = "src/main.peep" +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_dynamic_array_element_move/peeper.toml b/x_test/negative_dynamic_array_element_move/peeper.toml index 4a1b9240..e745f42c 100644 --- a/x_test/negative_dynamic_array_element_move/peeper.toml +++ b/x_test/negative_dynamic_array_element_move/peeper.toml @@ -1,3 +1,8 @@ name = "negative_dynamic_array_element_move" build = "program" entry = "src/main.peep" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_dynamic_array_immutable_mutation/peeper.toml b/x_test/negative_dynamic_array_immutable_mutation/peeper.toml index 067804b4..81babbaf 100644 --- a/x_test/negative_dynamic_array_immutable_mutation/peeper.toml +++ b/x_test/negative_dynamic_array_immutable_mutation/peeper.toml @@ -1,2 +1,7 @@ name = "negative_dynamic_array_immutable_mutation" build = "lib" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_dynamic_array_invalid_element/peeper.toml b/x_test/negative_dynamic_array_invalid_element/peeper.toml index f2e33b33..b60d1d0d 100644 --- a/x_test/negative_dynamic_array_invalid_element/peeper.toml +++ b/x_test/negative_dynamic_array_invalid_element/peeper.toml @@ -1,3 +1,8 @@ name = "negative_dynamic_array_invalid_element" build = "program" entry = "src/main.peep" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_dynamic_array_operation_value/peeper.toml b/x_test/negative_dynamic_array_operation_value/peeper.toml index c3d7a928..080c7c4f 100644 --- a/x_test/negative_dynamic_array_operation_value/peeper.toml +++ b/x_test/negative_dynamic_array_operation_value/peeper.toml @@ -1,2 +1,7 @@ name = "negative_dynamic_array_operation_value" build = "lib" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_dynamic_array_reference_element/peeper.toml b/x_test/negative_dynamic_array_reference_element/peeper.toml index 5f76d56f..f7c801cc 100644 --- a/x_test/negative_dynamic_array_reference_element/peeper.toml +++ b/x_test/negative_dynamic_array_reference_element/peeper.toml @@ -1,3 +1,8 @@ name = "negative_dynamic_array_reference_element" build = "program" entry = "src/main.peep" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_dynamic_array_resize_move_element/peeper.toml b/x_test/negative_dynamic_array_resize_move_element/peeper.toml index de8f3746..553cbefa 100644 --- a/x_test/negative_dynamic_array_resize_move_element/peeper.toml +++ b/x_test/negative_dynamic_array_resize_move_element/peeper.toml @@ -2,3 +2,7 @@ name = "negative_dynamic_array_resize_move_element" build = "program" entry = "src/main.peep" +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_dynamic_array_shrink_view/peeper.toml b/x_test/negative_dynamic_array_shrink_view/peeper.toml index 4738278b..a8edf28a 100644 --- a/x_test/negative_dynamic_array_shrink_view/peeper.toml +++ b/x_test/negative_dynamic_array_shrink_view/peeper.toml @@ -1,3 +1,8 @@ name = "negative_dynamic_array_shrink_view" build = "program" entry = "src/main.peep" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_dynamic_array_view_growth/peeper.toml b/x_test/negative_dynamic_array_view_growth/peeper.toml index 29c5bcda..af9e0662 100644 --- a/x_test/negative_dynamic_array_view_growth/peeper.toml +++ b/x_test/negative_dynamic_array_view_growth/peeper.toml @@ -2,3 +2,7 @@ name = "negative_dynamic_array_view_growth" build = "program" entry = "src/main.peep" +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_extern_nested_owner/peeper.toml b/x_test/negative_extern_nested_owner/peeper.toml index 4c99d710..ad303baf 100644 --- a/x_test/negative_extern_nested_owner/peeper.toml +++ b/x_test/negative_extern_nested_owner/peeper.toml @@ -1,3 +1,8 @@ name = "negative_extern_nested_owner" build = "program" entry = "src/main.peep" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_extern_owned_carrier/peeper.toml b/x_test/negative_extern_owned_carrier/peeper.toml index 43d42b45..5715c774 100644 --- a/x_test/negative_extern_owned_carrier/peeper.toml +++ b/x_test/negative_extern_owned_carrier/peeper.toml @@ -1,3 +1,8 @@ name = "negative_extern_owned_carrier" build = "program" entry = "src/main.peep" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_extern_owner_parameter/peeper.toml b/x_test/negative_extern_owner_parameter/peeper.toml index a2d00ea6..091877d7 100644 --- a/x_test/negative_extern_owner_parameter/peeper.toml +++ b/x_test/negative_extern_owner_parameter/peeper.toml @@ -1,3 +1,8 @@ name = "negative_extern_owner_parameter" build = "program" entry = "src/main.peep" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_fixed_array_constant_oob/peeper.toml b/x_test/negative_fixed_array_constant_oob/peeper.toml index 7e0ecbfe..77c61df3 100644 --- a/x_test/negative_fixed_array_constant_oob/peeper.toml +++ b/x_test/negative_fixed_array_constant_oob/peeper.toml @@ -1,3 +1,8 @@ name = "negative_fixed_array_constant_oob" build = "program" entry = "src/main.peep" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_immutable_param_mutation/peeper.toml b/x_test/negative_immutable_param_mutation/peeper.toml index a33176ab..43937650 100644 --- a/x_test/negative_immutable_param_mutation/peeper.toml +++ b/x_test/negative_immutable_param_mutation/peeper.toml @@ -1,2 +1,7 @@ name = "negative_immutable_param_mutation" build = "lib" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_impl_declaration/peeper.toml b/x_test/negative_impl_declaration/peeper.toml index 561ba9ee..0be72488 100644 --- a/x_test/negative_impl_declaration/peeper.toml +++ b/x_test/negative_impl_declaration/peeper.toml @@ -1,3 +1,8 @@ name = "negative_impl_declaration" build = "program" entry = "src/main.peep" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_implicit_move_use/peeper.toml b/x_test/negative_implicit_move_use/peeper.toml index 79492efa..5f3c70b4 100644 --- a/x_test/negative_implicit_move_use/peeper.toml +++ b/x_test/negative_implicit_move_use/peeper.toml @@ -1,2 +1,7 @@ name = "negative_implicit_move_use" build = "lib" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_imported_printf_symbol/peeper.toml b/x_test/negative_imported_printf_symbol/peeper.toml index eae63b22..213225cb 100644 --- a/x_test/negative_imported_printf_symbol/peeper.toml +++ b/x_test/negative_imported_printf_symbol/peeper.toml @@ -1,2 +1,7 @@ name = "negative_imported_printf_symbol" build = "program" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_index_element_move/peeper.toml b/x_test/negative_index_element_move/peeper.toml index 6f69d0be..2340b8e0 100644 --- a/x_test/negative_index_element_move/peeper.toml +++ b/x_test/negative_index_element_move/peeper.toml @@ -1,2 +1,7 @@ name = "negative_index_element_move" build = "lib" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_interface_keyword/peeper.toml b/x_test/negative_interface_keyword/peeper.toml index 19c52732..65310a22 100644 --- a/x_test/negative_interface_keyword/peeper.toml +++ b/x_test/negative_interface_keyword/peeper.toml @@ -1,3 +1,8 @@ name = "negative_interface_keyword" build = "program" entry = "src/main.peep" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_interface_method_default/peeper.toml b/x_test/negative_interface_method_default/peeper.toml index 9dea9a2b..6b348f6b 100644 --- a/x_test/negative_interface_method_default/peeper.toml +++ b/x_test/negative_interface_method_default/peeper.toml @@ -1,2 +1,7 @@ name = "negative_interface_method_default" build = "program" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_intrinsic_interface_conformance/peeper.toml b/x_test/negative_intrinsic_interface_conformance/peeper.toml index 2fcf6358..663b7fca 100644 --- a/x_test/negative_intrinsic_interface_conformance/peeper.toml +++ b/x_test/negative_intrinsic_interface_conformance/peeper.toml @@ -1,3 +1,8 @@ name = "negative_intrinsic_interface_conformance" build = "program" entry = "src/main.peep" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_invalid_free/peeper.toml b/x_test/negative_invalid_free/peeper.toml index 7d61d920..002849df 100644 --- a/x_test/negative_invalid_free/peeper.toml +++ b/x_test/negative_invalid_free/peeper.toml @@ -1,3 +1,8 @@ name = "negative_invalid_free" build = "program" entry = "src/main.peep" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_invalid_free_abi/peeper.toml b/x_test/negative_invalid_free_abi/peeper.toml index 48762779..8ab45771 100644 --- a/x_test/negative_invalid_free_abi/peeper.toml +++ b/x_test/negative_invalid_free_abi/peeper.toml @@ -1,3 +1,8 @@ name = "negative_invalid_free_abi" build = "program" entry = "src/main.peep" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_len_requires_borrow/peeper.toml b/x_test/negative_len_requires_borrow/peeper.toml index f71812df..918e014c 100644 --- a/x_test/negative_len_requires_borrow/peeper.toml +++ b/x_test/negative_len_requires_borrow/peeper.toml @@ -1,2 +1,7 @@ name = "negative_len_requires_borrow" build = "lib" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_literal_kinds/peeper.toml b/x_test/negative_literal_kinds/peeper.toml index 6c53ba4e..ae4e7da1 100644 --- a/x_test/negative_literal_kinds/peeper.toml +++ b/x_test/negative_literal_kinds/peeper.toml @@ -1,2 +1,7 @@ name = "negative_literal_kinds" build = "program" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_module_move_binding/peeper.toml b/x_test/negative_module_move_binding/peeper.toml index 16f6c18c..63d8db53 100644 --- a/x_test/negative_module_move_binding/peeper.toml +++ b/x_test/negative_module_move_binding/peeper.toml @@ -1,2 +1,7 @@ name = "negative_module_move_binding" build = "lib" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_mutable_borrow/peeper.toml b/x_test/negative_mutable_borrow/peeper.toml index b10afce1..d37d459b 100644 --- a/x_test/negative_mutable_borrow/peeper.toml +++ b/x_test/negative_mutable_borrow/peeper.toml @@ -1,2 +1,7 @@ name = "negative_mutable_borrow" build = "lib" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_mutable_reference_copy/peeper.toml b/x_test/negative_mutable_reference_copy/peeper.toml deleted file mode 100644 index 014ae941..00000000 --- a/x_test/negative_mutable_reference_copy/peeper.toml +++ /dev/null @@ -1,2 +0,0 @@ -name = "negative_mutable_reference_copy" -build = "lib" diff --git a/x_test/negative_nested_unsized_array/peeper.toml b/x_test/negative_nested_unsized_array/peeper.toml index a1b1c60e..e6196d6e 100644 --- a/x_test/negative_nested_unsized_array/peeper.toml +++ b/x_test/negative_nested_unsized_array/peeper.toml @@ -1,3 +1,8 @@ name = "negative_nested_unsized_array" build = "program" entry = "src/main.peep" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_nonaddressable_slice/peeper.toml b/x_test/negative_nonaddressable_slice/peeper.toml index 213d6bb5..1fe8172c 100644 --- a/x_test/negative_nonaddressable_slice/peeper.toml +++ b/x_test/negative_nonaddressable_slice/peeper.toml @@ -1,3 +1,8 @@ name = "negative_nonaddressable_slice" build = "program" entry = "src/main.peep" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_numeric_conversion/peeper.toml b/x_test/negative_numeric_conversion/peeper.toml index ee57c566..0b81cfe9 100644 --- a/x_test/negative_numeric_conversion/peeper.toml +++ b/x_test/negative_numeric_conversion/peeper.toml @@ -1,2 +1,7 @@ name = "negative_numeric_conversion" build = "program" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_numeric_postfix/peeper.toml b/x_test/negative_numeric_postfix/peeper.toml index fc51fd62..e6255918 100644 --- a/x_test/negative_numeric_postfix/peeper.toml +++ b/x_test/negative_numeric_postfix/peeper.toml @@ -1,2 +1,7 @@ name = "negative_numeric_postfix" build = "program" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_ownership_join/peeper.toml b/x_test/negative_ownership_join/peeper.toml index 866a1eb0..00a8dd8b 100644 --- a/x_test/negative_ownership_join/peeper.toml +++ b/x_test/negative_ownership_join/peeper.toml @@ -1,3 +1,8 @@ name = "negative_ownership_join" build = "program" entry = "src/main.peep" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_pipe_alloc_arity/peeper.toml b/x_test/negative_pipe_alloc_arity/peeper.toml index 210dff4c..2fc6d02e 100644 --- a/x_test/negative_pipe_alloc_arity/peeper.toml +++ b/x_test/negative_pipe_alloc_arity/peeper.toml @@ -1,2 +1,7 @@ name = "negative_pipe_alloc_arity" build = "lib" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_print_printf_symbol/peeper.toml b/x_test/negative_print_printf_symbol/peeper.toml index 43783586..6272e443 100644 --- a/x_test/negative_print_printf_symbol/peeper.toml +++ b/x_test/negative_print_printf_symbol/peeper.toml @@ -1,3 +1,8 @@ name = "negative_print_printf_symbol" build = "program" entry = "src/main.peep" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_print_receiver_printf_symbol/peeper.toml b/x_test/negative_print_receiver_printf_symbol/peeper.toml index a4ddd415..8190e599 100644 --- a/x_test/negative_print_receiver_printf_symbol/peeper.toml +++ b/x_test/negative_print_receiver_printf_symbol/peeper.toml @@ -1,3 +1,8 @@ name = "negative_print_receiver_printf_symbol" build = "program" entry = "src/main.peep" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_print_wide_integer/peeper.toml b/x_test/negative_print_wide_integer/peeper.toml index e59f9063..55377d00 100644 --- a/x_test/negative_print_wide_integer/peeper.toml +++ b/x_test/negative_print_wide_integer/peeper.toml @@ -1,3 +1,8 @@ name = "negative_print_wide_integer" build = "program" entry = "src/main.peep" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_range_address/peeper.toml b/x_test/negative_range_address/peeper.toml index 5c66853c..0329881b 100644 --- a/x_test/negative_range_address/peeper.toml +++ b/x_test/negative_range_address/peeper.toml @@ -1,2 +1,7 @@ name = "negative_range_address" build = "lib" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_receiver_target/peeper.toml b/x_test/negative_receiver_target/peeper.toml index 8badc4f7..9c7135d9 100644 --- a/x_test/negative_receiver_target/peeper.toml +++ b/x_test/negative_receiver_target/peeper.toml @@ -1,3 +1,8 @@ name = "negative_receiver_target" build = "program" entry = "src/main.peep" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_reference_return/peeper.toml b/x_test/negative_reference_return/peeper.toml index f84af7bc..c2a14fae 100644 --- a/x_test/negative_reference_return/peeper.toml +++ b/x_test/negative_reference_return/peeper.toml @@ -1,2 +1,7 @@ name = "negative_reference_return" build = "lib" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_reference_storage/peeper.toml b/x_test/negative_reference_storage/peeper.toml index baf7222d..c2d95860 100644 --- a/x_test/negative_reference_storage/peeper.toml +++ b/x_test/negative_reference_storage/peeper.toml @@ -1,2 +1,7 @@ name = "negative_reference_storage" build = "lib" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_scalar_shrink_bad_free/peeper.toml b/x_test/negative_scalar_shrink_bad_free/peeper.toml index dbe51de6..502f9bb0 100644 --- a/x_test/negative_scalar_shrink_bad_free/peeper.toml +++ b/x_test/negative_scalar_shrink_bad_free/peeper.toml @@ -1,3 +1,8 @@ name = "negative_scalar_shrink_bad_free" build = "program" entry = "src/main.peep" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_shared_index_borrow/peeper.toml b/x_test/negative_shared_index_borrow/peeper.toml index 75e4bc5e..39748747 100644 --- a/x_test/negative_shared_index_borrow/peeper.toml +++ b/x_test/negative_shared_index_borrow/peeper.toml @@ -1,2 +1,7 @@ name = "negative_shared_index_borrow" build = "lib" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_shared_reference_mutation/peeper.toml b/x_test/negative_shared_reference_mutation/peeper.toml index ac3c2939..b5ead524 100644 --- a/x_test/negative_shared_reference_mutation/peeper.toml +++ b/x_test/negative_shared_reference_mutation/peeper.toml @@ -1,2 +1,7 @@ name = "negative_shared_reference_mutation" build = "lib" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_shared_slice_view_mutation/peeper.toml b/x_test/negative_shared_slice_view_mutation/peeper.toml index 2eb49929..ad73856e 100644 --- a/x_test/negative_shared_slice_view_mutation/peeper.toml +++ b/x_test/negative_shared_slice_view_mutation/peeper.toml @@ -1,2 +1,7 @@ name = "negative_shared_slice_view_mutation" build = "lib" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_slice_view_comparison/peeper.toml b/x_test/negative_slice_view_comparison/peeper.toml index 27691713..80199137 100644 --- a/x_test/negative_slice_view_comparison/peeper.toml +++ b/x_test/negative_slice_view_comparison/peeper.toml @@ -1,2 +1,7 @@ name = "negative_slice_view_comparison" build = "lib" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_slice_view_move/peeper.toml b/x_test/negative_slice_view_move/peeper.toml index 0862a959..e0e5ff48 100644 --- a/x_test/negative_slice_view_move/peeper.toml +++ b/x_test/negative_slice_view_move/peeper.toml @@ -1,2 +1,7 @@ name = "negative_slice_view_move" build = "lib" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_standalone_slice_type/peeper.toml b/x_test/negative_standalone_slice_type/peeper.toml index 305b7afc..375c4727 100644 --- a/x_test/negative_standalone_slice_type/peeper.toml +++ b/x_test/negative_standalone_slice_type/peeper.toml @@ -1,2 +1,7 @@ name = "negative_standalone_slice_type" build = "lib" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_string_intrinsic_args/peeper.toml b/x_test/negative_string_intrinsic_args/peeper.toml index da6d6f1d..cd4ca0e9 100644 --- a/x_test/negative_string_intrinsic_args/peeper.toml +++ b/x_test/negative_string_intrinsic_args/peeper.toml @@ -1,2 +1,7 @@ name = "negative_string_intrinsic_args" build = "lib" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_string_mutation/peeper.toml b/x_test/negative_string_mutation/peeper.toml index 5b3fe76d..15ef9244 100644 --- a/x_test/negative_string_mutation/peeper.toml +++ b/x_test/negative_string_mutation/peeper.toml @@ -1,2 +1,7 @@ name = "negative_string_mutation" build = "lib" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_target_array_length/peeper.toml b/x_test/negative_target_array_length/peeper.toml index 38dbfbe0..6f4bc19a 100644 --- a/x_test/negative_target_array_length/peeper.toml +++ b/x_test/negative_target_array_length/peeper.toml @@ -1,3 +1,9 @@ name = "negative_target_array_length" build = "program" entry = "src/main.peep" + +[test] +mode = "check" +outcome = "failure" +compiler_args = ["-target-os", "linux", "-target-arch", "386"] +stderr_contains = ["Compilation failed"] diff --git a/x_test/negative_temporary_borrow_escape/peeper.toml b/x_test/negative_temporary_borrow_escape/peeper.toml index 0114392e..066c3d0e 100644 --- a/x_test/negative_temporary_borrow_escape/peeper.toml +++ b/x_test/negative_temporary_borrow_escape/peeper.toml @@ -1,2 +1,7 @@ name = "negative_temporary_borrow_escape" build = "lib" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["Compilation failed"] diff --git a/x_test/optional_none_basic/peeper.toml b/x_test/optional_none_basic/peeper.toml index a341731b..caaf34f6 100644 --- a/x_test/optional_none_basic/peeper.toml +++ b/x_test/optional_none_basic/peeper.toml @@ -1,2 +1,6 @@ name = "optional_none_basic" build = "program" + +[test] +mode = "check" +outcome = "success" diff --git a/x_test/owned_pointer_carrier/peeper.toml b/x_test/owned_pointer_carrier/peeper.toml index 1774ba06..0ee848fd 100644 --- a/x_test/owned_pointer_carrier/peeper.toml +++ b/x_test/owned_pointer_carrier/peeper.toml @@ -1,2 +1,6 @@ [project] name = "owned_pointer_carrier" + +[test] +mode = "check" +outcome = "success" diff --git a/x_test/ownership_flow_basic/peeper.toml b/x_test/ownership_flow_basic/peeper.toml index 177cdb10..aa377f8f 100644 --- a/x_test/ownership_flow_basic/peeper.toml +++ b/x_test/ownership_flow_basic/peeper.toml @@ -1,2 +1,6 @@ name = "ownership_flow_basic" build = "program" + +[test] +mode = "check" +outcome = "success" diff --git a/x_test/review_consteval_gaps/peeper.toml b/x_test/review_consteval_gaps/peeper.toml index 47937132..5a3011ac 100644 --- a/x_test/review_consteval_gaps/peeper.toml +++ b/x_test/review_consteval_gaps/peeper.toml @@ -1,2 +1,7 @@ name = "review_consteval_gaps" build = "bin" + +[test] +mode = "check" +outcome = "failure" +stderr_contains = ["T0009"] diff --git a/x_test/runtime_alloc_nested/peeper.toml b/x_test/runtime_alloc_nested/peeper.toml index 3b84329a..edf9337f 100644 --- a/x_test/runtime_alloc_nested/peeper.toml +++ b/x_test/runtime_alloc_nested/peeper.toml @@ -1,3 +1,7 @@ name = "runtime_alloc_nested" build = "program" entry = "src/main.peep" + +[test] +mode = "run" +outcome = "success" diff --git a/x_test/runtime_bitwise_operators/peeper.toml b/x_test/runtime_bitwise_operators/peeper.toml index 816f846f..d00ec744 100644 --- a/x_test/runtime_bitwise_operators/peeper.toml +++ b/x_test/runtime_bitwise_operators/peeper.toml @@ -1,2 +1,6 @@ name = "runtime_bitwise_operators" build = "program" + +[test] +mode = "run" +outcome = "success" diff --git a/x_test/runtime_borrow_conflicts/peeper.toml b/x_test/runtime_borrow_conflicts/peeper.toml index 74368e28..f32f4b03 100644 --- a/x_test/runtime_borrow_conflicts/peeper.toml +++ b/x_test/runtime_borrow_conflicts/peeper.toml @@ -1,2 +1,6 @@ name = "runtime_borrow_conflicts" build = "program" + +[test] +mode = "run" +outcome = "success" diff --git a/x_test/runtime_collection_len/peeper.toml b/x_test/runtime_collection_len/peeper.toml index 2699272c..80125d87 100644 --- a/x_test/runtime_collection_len/peeper.toml +++ b/x_test/runtime_collection_len/peeper.toml @@ -1,2 +1,6 @@ name = "runtime_collection_len" build = "program" + +[test] +mode = "run" +outcome = "success" diff --git a/x_test/runtime_const_address_identity/peeper.toml b/x_test/runtime_const_address_identity/peeper.toml index 5651503e..9ba1addf 100644 --- a/x_test/runtime_const_address_identity/peeper.toml +++ b/x_test/runtime_const_address_identity/peeper.toml @@ -1,2 +1,6 @@ name = "runtime_const_address_identity" build = "program" + +[test] +mode = "run" +outcome = "success" diff --git a/x_test/runtime_constvalue_folding/peeper.toml b/x_test/runtime_constvalue_folding/peeper.toml index ba8d176b..fbfce3b3 100644 --- a/x_test/runtime_constvalue_folding/peeper.toml +++ b/x_test/runtime_constvalue_folding/peeper.toml @@ -1,2 +1,6 @@ name = "runtime_constvalue_folding" build = "program" + +[test] +mode = "run" +outcome = "success" diff --git a/x_test/runtime_cstr/peeper.toml b/x_test/runtime_cstr/peeper.toml index 00a4500e..b1d4fa29 100644 --- a/x_test/runtime_cstr/peeper.toml +++ b/x_test/runtime_cstr/peeper.toml @@ -1,2 +1,6 @@ name = "runtime_cstr" build = "program" + +[test] +mode = "run" +outcome = "success" diff --git a/x_test/runtime_default_range_clone/peeper.toml b/x_test/runtime_default_range_clone/peeper.toml index 9a284d08..56932fb0 100644 --- a/x_test/runtime_default_range_clone/peeper.toml +++ b/x_test/runtime_default_range_clone/peeper.toml @@ -1,2 +1,6 @@ name = "runtime_default_range_clone" build = "program" + +[test] +mode = "run" +outcome = "success" diff --git a/x_test/runtime_default_range_clone/src/main.peep b/x_test/runtime_default_range_clone/src/main.peep index 7f24c89f..fee4c567 100644 --- a/x_test/runtime_default_range_clone/src/main.peep +++ b/x_test/runtime_default_range_clone/src/main.peep @@ -1,4 +1,4 @@ -fn First(values: &[]i32, view: &[]i32 = values[..]) -> i32 { +fn First(values: &[]i32, view: &[..]i32 = values[..]) -> i32 { return view[0]; } diff --git a/x_test/runtime_dynamic_array_growth/peeper.toml b/x_test/runtime_dynamic_array_growth/peeper.toml index aaa5d42a..4630db76 100644 --- a/x_test/runtime_dynamic_array_growth/peeper.toml +++ b/x_test/runtime_dynamic_array_growth/peeper.toml @@ -1,3 +1,6 @@ name = "runtime_dynamic_array_growth" build = "program" +[test] +mode = "run" +outcome = "success" diff --git a/x_test/runtime_dynamic_array_literal/peeper.toml b/x_test/runtime_dynamic_array_literal/peeper.toml index 4681f33a..e9551e60 100644 --- a/x_test/runtime_dynamic_array_literal/peeper.toml +++ b/x_test/runtime_dynamic_array_literal/peeper.toml @@ -1,2 +1,6 @@ name = "runtime_dynamic_array_literal" build = "program" + +[test] +mode = "run" +outcome = "success" diff --git a/x_test/runtime_dynamic_array_reference_slice/peeper.toml b/x_test/runtime_dynamic_array_reference_slice/peeper.toml index f420fc44..634c5fac 100644 --- a/x_test/runtime_dynamic_array_reference_slice/peeper.toml +++ b/x_test/runtime_dynamic_array_reference_slice/peeper.toml @@ -1,2 +1,6 @@ name = "runtime_dynamic_array_reference_slice" build = "program" + +[test] +mode = "run" +outcome = "success" diff --git a/x_test/runtime_dynamic_array_shrink/peeper.toml b/x_test/runtime_dynamic_array_shrink/peeper.toml index be1068bb..abbbdc0b 100644 --- a/x_test/runtime_dynamic_array_shrink/peeper.toml +++ b/x_test/runtime_dynamic_array_shrink/peeper.toml @@ -1,2 +1,6 @@ name = "runtime_dynamic_array_shrink" build = "program" + +[test] +mode = "run" +outcome = "success" diff --git a/x_test/runtime_fixed_array_index/peeper.toml b/x_test/runtime_fixed_array_index/peeper.toml index 9842faa5..5685f5af 100644 --- a/x_test/runtime_fixed_array_index/peeper.toml +++ b/x_test/runtime_fixed_array_index/peeper.toml @@ -1,2 +1,6 @@ name = "runtime_fixed_array_index" build = "program" + +[test] +mode = "run" +outcome = "success" diff --git a/x_test/runtime_fixed_array_index_oob/peeper.toml b/x_test/runtime_fixed_array_index_oob/peeper.toml index 7bdb8d5e..418f04ec 100644 --- a/x_test/runtime_fixed_array_index_oob/peeper.toml +++ b/x_test/runtime_fixed_array_index_oob/peeper.toml @@ -1,2 +1,6 @@ name = "runtime_fixed_array_index_oob" build = "program" + +[test] +mode = "run" +outcome = "failure" diff --git a/x_test/runtime_import_owned_abi/peeper.toml b/x_test/runtime_import_owned_abi/peeper.toml index c257fc1c..d95d70c7 100644 --- a/x_test/runtime_import_owned_abi/peeper.toml +++ b/x_test/runtime_import_owned_abi/peeper.toml @@ -1,3 +1,7 @@ name = "runtime_import_owned_abi" build = "program" entry = "src/main.peep" + +[test] +mode = "run" +outcome = "success" diff --git a/x_test/runtime_index_element_access/peeper.toml b/x_test/runtime_index_element_access/peeper.toml index 1787f1bf..2dc0ec00 100644 --- a/x_test/runtime_index_element_access/peeper.toml +++ b/x_test/runtime_index_element_access/peeper.toml @@ -1,2 +1,6 @@ name = "runtime_index_element_access" build = "program" + +[test] +mode = "run" +outcome = "success" diff --git a/x_test/runtime_integer_division/peeper.toml b/x_test/runtime_integer_division/peeper.toml new file mode 100644 index 00000000..961eda7c --- /dev/null +++ b/x_test/runtime_integer_division/peeper.toml @@ -0,0 +1,6 @@ +name = "runtime_integer_division" +build = "program" + +[test] +mode = "run" +outcome = "success" diff --git a/x_test/runtime_integer_division/src/main.peep b/x_test/runtime_integer_division/src/main.peep new file mode 100644 index 00000000..fc73b9ab --- /dev/null +++ b/x_test/runtime_integer_division/src/main.peep @@ -0,0 +1,62 @@ +fn div_i8(left: i8, right: i8) -> i8 { + return left / right; +} + +fn rem_i8(left: i8, right: i8) -> i8 { + return left % right; +} + +fn div_i16(left: i16, right: i16) -> i16 { + return left / right; +} + +fn rem_i16(left: i16, right: i16) -> i16 { + return left % right; +} + +fn div_i32(left: i32, right: i32) -> i32 { + return left / right; +} + +fn rem_i32(left: i32, right: i32) -> i32 { + return left % right; +} + +fn div_i64(left: i64, right: i64) -> i64 { + return left / right; +} + +fn rem_i64(left: i64, right: i64) -> i64 { + return left % right; +} + +fn main() -> i32 { + if (-128i8 / -1i8) != div_i8(-128i8, -1i8) || div_i8(-128i8, -1i8) != -128i8 { + return 1; + } + if (-128i8 % -1i8) != rem_i8(-128i8, -1i8) || rem_i8(-128i8, -1i8) != 0i8 { + return 2; + } + if (-32768i16 / -1i16) != div_i16(-32768i16, -1i16) || div_i16(-32768i16, -1i16) != -32768i16 { + return 3; + } + if (-32768i16 % -1i16) != rem_i16(-32768i16, -1i16) || rem_i16(-32768i16, -1i16) != 0i16 { + return 4; + } + if (-2147483648i32 / -1i32) != div_i32(-2147483648i32, -1i32) || div_i32(-2147483648i32, -1i32) != -2147483648i32 { + return 5; + } + if (-2147483648i32 % -1i32) != rem_i32(-2147483648i32, -1i32) || rem_i32(-2147483648i32, -1i32) != 0i32 { + return 6; + } + if (-9223372036854775808i64 / -1i64) != div_i64(-9223372036854775808i64, -1i64) || div_i64(-9223372036854775808i64, -1i64) != -9223372036854775808i64 { + return 7; + } + if (-9223372036854775808i64 % -1i64) != rem_i64(-9223372036854775808i64, -1i64) || rem_i64(-9223372036854775808i64, -1i64) != 0i64 { + return 8; + } + if div_i32(-7i32, 3i32) != -2i32 || rem_i32(-7i32, 3i32) != -1i32 { + return 9; + } + return 0; +} diff --git a/x_test/runtime_integer_division_zero/peeper.toml b/x_test/runtime_integer_division_zero/peeper.toml new file mode 100644 index 00000000..3a7625ba --- /dev/null +++ b/x_test/runtime_integer_division_zero/peeper.toml @@ -0,0 +1,6 @@ +name = "runtime_integer_division_zero" +build = "program" + +[test] +mode = "run" +outcome = "failure" diff --git a/x_test/runtime_integer_division_zero/src/main.peep b/x_test/runtime_integer_division_zero/src/main.peep new file mode 100644 index 00000000..3842a30c --- /dev/null +++ b/x_test/runtime_integer_division_zero/src/main.peep @@ -0,0 +1,7 @@ +fn divide(left: i32, right: i32) -> i32 { + return left / right; +} + +fn main() -> i32 { + return divide(1i32, 0i32); +} diff --git a/x_test/runtime_interface/peeper.toml b/x_test/runtime_interface/peeper.toml index 85f67da8..9020026d 100644 --- a/x_test/runtime_interface/peeper.toml +++ b/x_test/runtime_interface/peeper.toml @@ -1,2 +1,6 @@ name = "runtime_interface" build = "program" + +[test] +mode = "run" +outcome = "success" diff --git a/x_test/runtime_interface_nested/peeper.toml b/x_test/runtime_interface_nested/peeper.toml index 6cc5d6e7..5e256519 100644 --- a/x_test/runtime_interface_nested/peeper.toml +++ b/x_test/runtime_interface_nested/peeper.toml @@ -1,2 +1,6 @@ name = "runtime_interface_nested" build = "program" + +[test] +mode = "run" +outcome = "success" diff --git a/x_test/runtime_invalid_string_boundary/peeper.toml b/x_test/runtime_invalid_string_boundary/peeper.toml index b759b6a5..52afb4a0 100644 --- a/x_test/runtime_invalid_string_boundary/peeper.toml +++ b/x_test/runtime_invalid_string_boundary/peeper.toml @@ -1,2 +1,6 @@ name = "runtime_invalid_string_boundary" build = "program" + +[test] +mode = "run" +outcome = "failure" diff --git a/x_test/runtime_invalid_string_bounds/peeper.toml b/x_test/runtime_invalid_string_bounds/peeper.toml index 2b3143ca..77fdaedb 100644 --- a/x_test/runtime_invalid_string_bounds/peeper.toml +++ b/x_test/runtime_invalid_string_bounds/peeper.toml @@ -1,2 +1,6 @@ name = "runtime_invalid_string_bounds" build = "program" + +[test] +mode = "run" +outcome = "failure" diff --git a/x_test/runtime_invalid_string_order/peeper.toml b/x_test/runtime_invalid_string_order/peeper.toml index 700aca70..1dd544bc 100644 --- a/x_test/runtime_invalid_string_order/peeper.toml +++ b/x_test/runtime_invalid_string_order/peeper.toml @@ -1,2 +1,6 @@ name = "runtime_invalid_string_order" build = "program" + +[test] +mode = "run" +outcome = "failure" diff --git a/x_test/runtime_literal_kinds/peeper.toml b/x_test/runtime_literal_kinds/peeper.toml index 096ca905..865dc3e5 100644 --- a/x_test/runtime_literal_kinds/peeper.toml +++ b/x_test/runtime_literal_kinds/peeper.toml @@ -1,2 +1,6 @@ name = "runtime_literal_kinds" build = "program" + +[test] +mode = "run" +outcome = "success" diff --git a/x_test/runtime_numbers/peeper.toml b/x_test/runtime_numbers/peeper.toml index c28d7ef8..e23310e7 100644 --- a/x_test/runtime_numbers/peeper.toml +++ b/x_test/runtime_numbers/peeper.toml @@ -1,2 +1,7 @@ name = "runtime_numbers" build = "program" + +[test] +mode = "run" +outcome = "exit_code" +exit_code = 2 diff --git a/x_test/runtime_numeric_bases/peeper.toml b/x_test/runtime_numeric_bases/peeper.toml index 16fd905a..c046bac1 100644 --- a/x_test/runtime_numeric_bases/peeper.toml +++ b/x_test/runtime_numeric_bases/peeper.toml @@ -1,2 +1,6 @@ name = "runtime_numeric_bases" build = "program" + +[test] +mode = "run" +outcome = "success" diff --git a/x_test/runtime_numeric_postfix/peeper.toml b/x_test/runtime_numeric_postfix/peeper.toml index 0f146d47..75df2ea9 100644 --- a/x_test/runtime_numeric_postfix/peeper.toml +++ b/x_test/runtime_numeric_postfix/peeper.toml @@ -1,2 +1,6 @@ name = "runtime_numeric_postfix" build = "program" + +[test] +mode = "run" +outcome = "success" diff --git a/x_test/runtime_pipe_calls/peeper.toml b/x_test/runtime_pipe_calls/peeper.toml index f1c676cf..09a90708 100644 --- a/x_test/runtime_pipe_calls/peeper.toml +++ b/x_test/runtime_pipe_calls/peeper.toml @@ -1,2 +1,6 @@ name = "runtime_pipe_calls" build = "program" + +[test] +mode = "run" +outcome = "success" diff --git a/x_test/runtime_print_scalars/peeper.toml b/x_test/runtime_print_scalars/peeper.toml index 988570ca..8482b190 100644 --- a/x_test/runtime_print_scalars/peeper.toml +++ b/x_test/runtime_print_scalars/peeper.toml @@ -1,2 +1,6 @@ name = "runtime_print_scalars" build = "program" + +[test] +mode = "run" +outcome = "success" diff --git a/x_test/runtime_println/peeper.toml b/x_test/runtime_println/peeper.toml index 271abbc2..a209765f 100644 --- a/x_test/runtime_println/peeper.toml +++ b/x_test/runtime_println/peeper.toml @@ -1,2 +1,6 @@ name = "runtime_println" build = "program" + +[test] +mode = "run" +outcome = "success" diff --git a/x_test/runtime_projection_places/peeper.toml b/x_test/runtime_projection_places/peeper.toml index 5c92c2ee..4355e0fd 100644 --- a/x_test/runtime_projection_places/peeper.toml +++ b/x_test/runtime_projection_places/peeper.toml @@ -1,2 +1,6 @@ name = "runtime_projection_places" build = "program" + +[test] +mode = "run" +outcome = "success" diff --git a/x_test/runtime_raw_ffi/peeper.toml b/x_test/runtime_raw_ffi/peeper.toml index b8512735..7c63c4b1 100644 --- a/x_test/runtime_raw_ffi/peeper.toml +++ b/x_test/runtime_raw_ffi/peeper.toml @@ -1,3 +1,7 @@ name = "runtime_raw_ffi" build = "program" entry = "src/main.peep" + +[test] +mode = "run" +outcome = "success" diff --git a/x_test/runtime_raw_ffi/src/main.peep b/x_test/runtime_raw_ffi/src/main.peep index 5af2b9d2..dda9eb81 100644 --- a/x_test/runtime_raw_ffi/src/main.peep +++ b/x_test/runtime_raw_ffi/src/main.peep @@ -10,6 +10,6 @@ fn PutLine(value: cstr) -> i32; fn main() -> i32 { let ptr: rawptr = MallocRaw(4); FreeRaw(ptr); - let _ = PutLine("raw ffi ok"); + let _ = PutLine(c"raw ffi ok"); return 0; } diff --git a/x_test/runtime_reference_interface/peeper.toml b/x_test/runtime_reference_interface/peeper.toml index ab870dfd..59ef2bf1 100644 --- a/x_test/runtime_reference_interface/peeper.toml +++ b/x_test/runtime_reference_interface/peeper.toml @@ -1,2 +1,6 @@ name = "runtime_reference_interface" build = "program" + +[test] +mode = "run" +outcome = "success" diff --git a/x_test/runtime_reference_receivers/peeper.toml b/x_test/runtime_reference_receivers/peeper.toml index 9101fbbb..fcb7a744 100644 --- a/x_test/runtime_reference_receivers/peeper.toml +++ b/x_test/runtime_reference_receivers/peeper.toml @@ -1,2 +1,6 @@ name = "runtime_reference_receivers" build = "program" + +[test] +mode = "run" +outcome = "success" diff --git a/x_test/runtime_reference_return_import/peeper.toml b/x_test/runtime_reference_return_import/peeper.toml index 80fe3fc5..c06bfd4f 100644 --- a/x_test/runtime_reference_return_import/peeper.toml +++ b/x_test/runtime_reference_return_import/peeper.toml @@ -1,2 +1,6 @@ name = "runtime_reference_return_import" build = "program" + +[test] +mode = "run" +outcome = "success" diff --git a/x_test/runtime_reference_returns/peeper.toml b/x_test/runtime_reference_returns/peeper.toml index 5e454bc7..7c849388 100644 --- a/x_test/runtime_reference_returns/peeper.toml +++ b/x_test/runtime_reference_returns/peeper.toml @@ -1,2 +1,6 @@ name = "runtime_reference_returns" build = "program" + +[test] +mode = "run" +outcome = "success" diff --git a/x_test/runtime_returned_string_range/peeper.toml b/x_test/runtime_returned_string_range/peeper.toml index 2bce82c2..f5b3c671 100644 --- a/x_test/runtime_returned_string_range/peeper.toml +++ b/x_test/runtime_returned_string_range/peeper.toml @@ -1,2 +1,6 @@ name = "runtime_returned_string_range" build = "program" + +[test] +mode = "run" +outcome = "success" diff --git a/x_test/runtime_string_views/peeper.toml b/x_test/runtime_string_views/peeper.toml index de3464f3..1ed24c52 100644 --- a/x_test/runtime_string_views/peeper.toml +++ b/x_test/runtime_string_views/peeper.toml @@ -1,2 +1,6 @@ name = "runtime_string_views" build = "program" + +[test] +mode = "run" +outcome = "success" diff --git a/x_test/runtime_struct/peeper.toml b/x_test/runtime_struct/peeper.toml index f5dbd056..679de04b 100644 --- a/x_test/runtime_struct/peeper.toml +++ b/x_test/runtime_struct/peeper.toml @@ -1,2 +1,8 @@ name = "runtime_struct" build = "program" + +[test] +mode = "run" +outcome = "exit_code" +exit_code = 10 +stdout_contains = ["struct ok"] diff --git a/x_test/runtime_temporary_borrows/peeper.toml b/x_test/runtime_temporary_borrows/peeper.toml index 88a0f68f..144d44f7 100644 --- a/x_test/runtime_temporary_borrows/peeper.toml +++ b/x_test/runtime_temporary_borrows/peeper.toml @@ -1,2 +1,6 @@ name = "runtime_temporary_borrows" build = "program" + +[test] +mode = "run" +outcome = "success" diff --git a/x_test/runtime_temporary_dynamic_array_projection/peeper.toml b/x_test/runtime_temporary_dynamic_array_projection/peeper.toml index 1d76f871..944c6bad 100644 --- a/x_test/runtime_temporary_dynamic_array_projection/peeper.toml +++ b/x_test/runtime_temporary_dynamic_array_projection/peeper.toml @@ -1,2 +1,6 @@ name = "runtime_temporary_dynamic_array_projection" build = "program" + +[test] +mode = "run" +outcome = "success" diff --git a/x_test/smoke_basic/peeper.toml b/x_test/smoke_basic/peeper.toml index 6a11fc31..e5660609 100644 --- a/x_test/smoke_basic/peeper.toml +++ b/x_test/smoke_basic/peeper.toml @@ -1,2 +1,6 @@ name = "smoke_basic" build = "program" + +[test] +mode = "check" +outcome = "success" diff --git a/x_test/type_allocator_capabilities/peeper.toml b/x_test/type_allocator_capabilities/peeper.toml index 0a046be1..24e08132 100644 --- a/x_test/type_allocator_capabilities/peeper.toml +++ b/x_test/type_allocator_capabilities/peeper.toml @@ -1,3 +1,7 @@ name = "type_allocator_capabilities" build = "program" entry = "src/main.peep" + +[test] +mode = "check" +outcome = "success" diff --git a/x_test/type_arrays/peeper.toml b/x_test/type_arrays/peeper.toml index 85588840..c5f48ecc 100644 --- a/x_test/type_arrays/peeper.toml +++ b/x_test/type_arrays/peeper.toml @@ -1,2 +1,6 @@ name = "type_arrays" build = "lib" + +[test] +mode = "check" +outcome = "success" diff --git a/x_test/type_automatic_cleanup_plan/peeper.toml b/x_test/type_automatic_cleanup_plan/peeper.toml index b716fc46..247bfd2a 100644 --- a/x_test/type_automatic_cleanup_plan/peeper.toml +++ b/x_test/type_automatic_cleanup_plan/peeper.toml @@ -1,3 +1,7 @@ name = "type_automatic_cleanup_plan" build = "program" entry = "src/main.peep" + +[test] +mode = "check" +outcome = "success" diff --git a/x_test/type_empty_dynamic_array_no_malloc/peeper.toml b/x_test/type_empty_dynamic_array_no_malloc/peeper.toml index dc8227a3..126ea9ed 100644 --- a/x_test/type_empty_dynamic_array_no_malloc/peeper.toml +++ b/x_test/type_empty_dynamic_array_no_malloc/peeper.toml @@ -1,3 +1,7 @@ name = "type_empty_dynamic_array_no_malloc" build = "program" entry = "src/main.peep" + +[test] +mode = "check" +outcome = "success" diff --git a/x_test/type_generics_angle/peeper.toml b/x_test/type_generics_angle/peeper.toml index c3d74f22..c91ff1ec 100644 --- a/x_test/type_generics_angle/peeper.toml +++ b/x_test/type_generics_angle/peeper.toml @@ -1,2 +1,6 @@ name = "type_generics_angle" build = "lib" + +[test] +mode = "check" +outcome = "success" diff --git a/x_test/type_interface_carriers/peeper.toml b/x_test/type_interface_carriers/peeper.toml index 9a4533d5..2a928fdf 100644 --- a/x_test/type_interface_carriers/peeper.toml +++ b/x_test/type_interface_carriers/peeper.toml @@ -1,3 +1,7 @@ name = "type_interface_carriers" build = "program" entry = "src/main.peep" + +[test] +mode = "check" +outcome = "success" diff --git a/x_test/type_local_dynamic_array_operation_shadow/peeper.toml b/x_test/type_local_dynamic_array_operation_shadow/peeper.toml index aa64074e..8ccd7c9b 100644 --- a/x_test/type_local_dynamic_array_operation_shadow/peeper.toml +++ b/x_test/type_local_dynamic_array_operation_shadow/peeper.toml @@ -2,3 +2,6 @@ name = "type_local_dynamic_array_operation_shadow" build = "program" entry = "src/main.peep" +[test] +mode = "check" +outcome = "success" diff --git a/x_test/type_mutable_reference_transfer/peeper.toml b/x_test/type_mutable_reference_transfer/peeper.toml new file mode 100644 index 00000000..ec1aac29 --- /dev/null +++ b/x_test/type_mutable_reference_transfer/peeper.toml @@ -0,0 +1,6 @@ +name = "type_mutable_reference_transfer" +build = "lib" + +[test] +mode = "check" +outcome = "success" diff --git a/x_test/negative_mutable_reference_copy/src/main.peep b/x_test/type_mutable_reference_transfer/src/main.peep similarity index 100% rename from x_test/negative_mutable_reference_copy/src/main.peep rename to x_test/type_mutable_reference_transfer/src/main.peep diff --git a/x_test/type_reference_callbacks/peeper.toml b/x_test/type_reference_callbacks/peeper.toml index c68b590a..be54f868 100644 --- a/x_test/type_reference_callbacks/peeper.toml +++ b/x_test/type_reference_callbacks/peeper.toml @@ -1,2 +1,6 @@ name = "type_reference_callbacks" build = "lib" + +[test] +mode = "check" +outcome = "success" diff --git a/x_test/type_reference_return_contracts/peeper.toml b/x_test/type_reference_return_contracts/peeper.toml index 880847fc..205ded23 100644 --- a/x_test/type_reference_return_contracts/peeper.toml +++ b/x_test/type_reference_return_contracts/peeper.toml @@ -1,2 +1,6 @@ name = "type_reference_return_contracts" build = "lib" + +[test] +mode = "check" +outcome = "success" diff --git a/x_test/type_scalar_dynamic_array_shrink_no_free/peeper.toml b/x_test/type_scalar_dynamic_array_shrink_no_free/peeper.toml index 0518d4a5..793ace91 100644 --- a/x_test/type_scalar_dynamic_array_shrink_no_free/peeper.toml +++ b/x_test/type_scalar_dynamic_array_shrink_no_free/peeper.toml @@ -1,3 +1,7 @@ name = "type_scalar_dynamic_array_shrink_no_free" build = "program" entry = "src/main.peep" + +[test] +mode = "check" +outcome = "success" diff --git a/x_test/type_slice_views/peeper.toml b/x_test/type_slice_views/peeper.toml index ae8d4a25..6e9cc0cd 100644 --- a/x_test/type_slice_views/peeper.toml +++ b/x_test/type_slice_views/peeper.toml @@ -1,2 +1,6 @@ name = "type_slice_views" build = "program" + +[test] +mode = "check" +outcome = "success" diff --git a/x_test/type_surface_basic/peeper.toml b/x_test/type_surface_basic/peeper.toml index aa7bf627..d0f8a6d4 100644 --- a/x_test/type_surface_basic/peeper.toml +++ b/x_test/type_surface_basic/peeper.toml @@ -1,2 +1,6 @@ name = "type_surface_basic" build = "lib" + +[test] +mode = "check" +outcome = "success" diff --git a/x_test/type_surface_basic/src/main.peep b/x_test/type_surface_basic/src/main.peep index cea9ab25..caa99e6b 100644 --- a/x_test/type_surface_basic/src/main.peep +++ b/x_test/type_surface_basic/src/main.peep @@ -13,4 +13,4 @@ struct View { type MaybeInt = ?i32; type Name = string; -const Fallback: ?i32 = none; +const Fallback: i32 = 0; diff --git a/x_test/type_target_array_length/peeper.toml b/x_test/type_target_array_length/peeper.toml index fa53fa9d..a4ac7ec5 100644 --- a/x_test/type_target_array_length/peeper.toml +++ b/x_test/type_target_array_length/peeper.toml @@ -1,3 +1,7 @@ name = "type_target_array_length" build = "program" entry = "src/main.peep" + +[test] +mode = "check" +outcome = "success" diff --git a/x_test/usage_warnings/peeper.toml b/x_test/usage_warnings/peeper.toml index ec3ed770..da3a4268 100644 --- a/x_test/usage_warnings/peeper.toml +++ b/x_test/usage_warnings/peeper.toml @@ -1,2 +1,6 @@ name = "usage_warnings" build = "program" + +[test] +mode = "check" +outcome = "success"