From f0126ab2b5e8c4c7b703f4ed622aaff5004eda4a Mon Sep 17 00:00:00 2001 From: Fuad Hasan Date: Fri, 21 Aug 2026 17:08:49 +0600 Subject: [PATCH] Remediate compiler audit findings Align integer division, source overlays, LSP positions, CLI exit propagation, target validation, and shared diagnostic construction. Move HIR packages under their owner, split lowering files by invariant domain, make source fixture expectations executable, and add CI gates. New helpers centralize repeated diagnostic, coordinate, fixture-process, and LLVM safety logic; no compatibility wrappers or stale aliases remain. Validated with gofmt, git diff --check, go vet ./..., go test ./..., go test -race ./..., and all 135 executable source fixture contracts. --- .github/workflows/ci.yml | 44 + .gitignore | 3 + cmd/build.go | 2 +- cmd/command.go | 8 +- cmd/command_test.go | 23 + cmd/dispatch.go | 13 +- cmd/dispatch_test.go | 15 + cmd/main.go | 4 +- docs/diagrams/cli-flow-detailed.d2 | 8 +- docs/diagrams/cli-flow-high-level.d2 | 6 +- docs/language-spec.md | 6 + internal/backend/llvm/dynamic_array_emit.go | 284 +++ .../llvm/{module_emit.go => emitter.go} | 0 .../{lower_llvm_test.go => emitter_test.go} | 77 + internal/backend/llvm/instruction_emit.go | 977 +++++++++ internal/backend/llvm/lower_llvm.go | 1815 ----------------- internal/backend/llvm/string_slice_emit.go | 624 ++++++ .../llvm/{type_lowering.go => type_layout.go} | 0 internal/constvalue/value_test.go | 36 + internal/driver/compiler.go | 10 +- internal/driver/compiler_test.go | 44 + internal/frontend/lexer/lexer.go | 2 +- internal/ir/{hir_fold => hir/fold}/fold.go | 19 +- .../ir/{hir_fold => hir/fold}/fold_test.go | 2 +- internal/ir/hir/lower/lower_interface.go | 119 ++ internal/ir/hir/lower/lower_types.go | 249 +++ .../lower.go => hir/lower/module_lower.go} | 351 +--- .../lower/module_lower_test.go} | 2 +- internal/ir/mir/{lower.go => module_lower.go} | 0 .../{lower_test.go => module_lower_test.go} | 0 internal/ir/{shared.go => nodes.go} | 2 +- internal/ir/{shared_test.go => nodes_test.go} | 0 internal/lsp/completion.go | 72 +- internal/lsp/cursor.go | 10 +- internal/lsp/hover.go | 83 +- internal/lsp/navigation.go | 34 +- internal/lsp/position.go | 124 ++ internal/lsp/position_test.go | 54 + internal/lsp/server.go | 8 +- internal/lsp/server_test.go | 9 +- internal/lsp/state.go | 23 +- internal/lsp/workspace_test.go | 74 + internal/pipeline/loader.go | 3 +- internal/pipeline/pipeline.go | 8 +- internal/prelude/prelude.go | 13 +- internal/problems/problems.go | 34 +- internal/problems/problems_test.go | 64 + internal/project/modules.go | 14 +- internal/semantics/cfg/analyze.go | 8 +- internal/semantics/collector/collector.go | 15 +- .../semantics/ownership/ownership_test.go | 4 +- internal/semantics/resolver/resolver.go | 4 +- .../semantics/typeinfo/{info.go => types.go} | 0 .../typeinfo/{info_test.go => types_test.go} | 0 internal/source/location.go | 13 +- internal/source/source_test.go | 28 +- internal/target/llvm_triple.go | 132 +- internal/target/llvm_triple_test.go | 36 +- internal/target/wordsize_test.go | 4 +- new_node_extension_flow.md | 4 +- pkg/ascii/ascii.go | 2 +- pkg/remotes/remotes.go | 2 +- x_test/copy_move_semantics/peeper.toml | 4 + x_test/default_interface_evidence/peeper.toml | 4 + x_test/default_parameters/peeper.toml | 4 + x_test/fixtures_test.go | 184 ++ x_test/import_basic/peeper.toml | 4 + x_test/import_default_parameters/peeper.toml | 4 + x_test/import_void_call/peeper.toml | 4 + x_test/lsp_completion/peeper.toml | 4 + .../negative_alloc_missing_value/peeper.toml | 5 + .../peeper.toml | 5 + .../peeper.toml | 5 + .../negative_assignment_base_move/peeper.toml | 5 + x_test/negative_bad_import/peeper.toml | 5 + .../negative_bad_import/src/bad_import.peep | 2 +- .../negative_bare_interface_value/peeper.toml | 5 + .../negative_bitwise_shift_count/peeper.toml | 5 + x_test/negative_bitwise_types/peeper.toml | 5 + x_test/negative_borrow_conflict/peeper.toml | 5 + .../peeper.toml | 5 + .../peeper.toml | 5 + .../peeper.toml | 5 + .../peeper.toml | 5 + .../peeper.toml | 5 + .../negative_direct_string_index/peeper.toml | 5 + .../peeper.toml | 5 + .../peeper.toml | 4 + .../peeper.toml | 5 + .../peeper.toml | 5 + .../peeper.toml | 5 + .../peeper.toml | 5 + .../peeper.toml | 5 + .../peeper.toml | 4 + .../peeper.toml | 5 + .../peeper.toml | 4 + .../negative_extern_nested_owner/peeper.toml | 5 + .../negative_extern_owned_carrier/peeper.toml | 5 + .../peeper.toml | 5 + .../peeper.toml | 5 + .../peeper.toml | 5 + x_test/negative_impl_declaration/peeper.toml | 5 + x_test/negative_implicit_move_use/peeper.toml | 5 + .../peeper.toml | 5 + .../negative_index_element_move/peeper.toml | 5 + x_test/negative_interface_keyword/peeper.toml | 5 + .../peeper.toml | 5 + .../peeper.toml | 5 + x_test/negative_invalid_free/peeper.toml | 5 + x_test/negative_invalid_free_abi/peeper.toml | 5 + .../negative_len_requires_borrow/peeper.toml | 5 + x_test/negative_literal_kinds/peeper.toml | 5 + .../negative_module_move_binding/peeper.toml | 5 + x_test/negative_mutable_borrow/peeper.toml | 5 + .../peeper.toml | 2 - .../negative_nested_unsized_array/peeper.toml | 5 + .../negative_nonaddressable_slice/peeper.toml | 5 + .../negative_numeric_conversion/peeper.toml | 5 + x_test/negative_numeric_postfix/peeper.toml | 5 + x_test/negative_ownership_join/peeper.toml | 5 + x_test/negative_pipe_alloc_arity/peeper.toml | 5 + .../negative_print_printf_symbol/peeper.toml | 5 + .../peeper.toml | 5 + .../negative_print_wide_integer/peeper.toml | 5 + x_test/negative_range_address/peeper.toml | 5 + x_test/negative_receiver_target/peeper.toml | 5 + x_test/negative_reference_return/peeper.toml | 5 + x_test/negative_reference_storage/peeper.toml | 5 + .../peeper.toml | 5 + .../negative_shared_index_borrow/peeper.toml | 5 + .../peeper.toml | 5 + .../peeper.toml | 5 + .../peeper.toml | 5 + x_test/negative_slice_view_move/peeper.toml | 5 + .../peeper.toml | 5 + .../peeper.toml | 5 + x_test/negative_string_mutation/peeper.toml | 5 + .../negative_target_array_length/peeper.toml | 6 + .../peeper.toml | 5 + x_test/optional_none_basic/peeper.toml | 4 + x_test/owned_pointer_carrier/peeper.toml | 4 + x_test/ownership_flow_basic/peeper.toml | 4 + x_test/review_consteval_gaps/peeper.toml | 5 + x_test/runtime_alloc_nested/peeper.toml | 4 + x_test/runtime_bitwise_operators/peeper.toml | 4 + x_test/runtime_borrow_conflicts/peeper.toml | 4 + x_test/runtime_collection_len/peeper.toml | 4 + .../peeper.toml | 4 + x_test/runtime_constvalue_folding/peeper.toml | 4 + x_test/runtime_cstr/peeper.toml | 4 + .../runtime_default_range_clone/peeper.toml | 4 + .../runtime_default_range_clone/src/main.peep | 2 +- .../runtime_dynamic_array_growth/peeper.toml | 3 + .../runtime_dynamic_array_literal/peeper.toml | 4 + .../peeper.toml | 4 + .../runtime_dynamic_array_shrink/peeper.toml | 4 + x_test/runtime_fixed_array_index/peeper.toml | 4 + .../runtime_fixed_array_index_oob/peeper.toml | 4 + x_test/runtime_import_owned_abi/peeper.toml | 4 + .../runtime_index_element_access/peeper.toml | 4 + x_test/runtime_integer_division/peeper.toml | 6 + x_test/runtime_integer_division/src/main.peep | 62 + .../runtime_integer_division_zero/peeper.toml | 6 + .../src/main.peep | 7 + x_test/runtime_interface/peeper.toml | 4 + x_test/runtime_interface_nested/peeper.toml | 4 + .../peeper.toml | 4 + .../runtime_invalid_string_bounds/peeper.toml | 4 + .../runtime_invalid_string_order/peeper.toml | 4 + x_test/runtime_literal_kinds/peeper.toml | 4 + x_test/runtime_numbers/peeper.toml | 5 + x_test/runtime_numeric_bases/peeper.toml | 4 + x_test/runtime_numeric_postfix/peeper.toml | 4 + x_test/runtime_pipe_calls/peeper.toml | 4 + x_test/runtime_print_scalars/peeper.toml | 4 + x_test/runtime_println/peeper.toml | 4 + x_test/runtime_projection_places/peeper.toml | 4 + x_test/runtime_raw_ffi/peeper.toml | 4 + x_test/runtime_raw_ffi/src/main.peep | 2 +- .../runtime_reference_interface/peeper.toml | 4 + .../runtime_reference_receivers/peeper.toml | 4 + .../peeper.toml | 4 + x_test/runtime_reference_returns/peeper.toml | 4 + .../runtime_returned_string_range/peeper.toml | 4 + x_test/runtime_string_views/peeper.toml | 4 + x_test/runtime_struct/peeper.toml | 6 + x_test/runtime_temporary_borrows/peeper.toml | 4 + .../peeper.toml | 4 + x_test/smoke_basic/peeper.toml | 4 + .../type_allocator_capabilities/peeper.toml | 4 + x_test/type_arrays/peeper.toml | 4 + .../type_automatic_cleanup_plan/peeper.toml | 4 + .../peeper.toml | 4 + x_test/type_generics_angle/peeper.toml | 4 + x_test/type_interface_carriers/peeper.toml | 4 + .../peeper.toml | 3 + .../peeper.toml | 6 + .../src/main.peep | 0 x_test/type_reference_callbacks/peeper.toml | 4 + .../peeper.toml | 4 + .../peeper.toml | 4 + x_test/type_slice_views/peeper.toml | 4 + x_test/type_surface_basic/peeper.toml | 4 + x_test/type_surface_basic/src/main.peep | 2 +- x_test/type_target_array_length/peeper.toml | 4 + x_test/usage_warnings/peeper.toml | 4 + 206 files changed, 4005 insertions(+), 2495 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 internal/backend/llvm/dynamic_array_emit.go rename internal/backend/llvm/{module_emit.go => emitter.go} (100%) rename internal/backend/llvm/{lower_llvm_test.go => emitter_test.go} (96%) create mode 100644 internal/backend/llvm/instruction_emit.go delete mode 100644 internal/backend/llvm/lower_llvm.go create mode 100644 internal/backend/llvm/string_slice_emit.go rename internal/backend/llvm/{type_lowering.go => type_layout.go} (100%) create mode 100644 internal/driver/compiler_test.go rename internal/ir/{hir_fold => hir/fold}/fold.go (92%) rename internal/ir/{hir_fold => hir/fold}/fold_test.go (99%) create mode 100644 internal/ir/hir/lower/lower_interface.go create mode 100644 internal/ir/hir/lower/lower_types.go rename internal/ir/{hir_lower/lower.go => hir/lower/module_lower.go} (77%) rename internal/ir/{hir_lower/lower_test.go => hir/lower/module_lower_test.go} (99%) rename internal/ir/mir/{lower.go => module_lower.go} (100%) rename internal/ir/mir/{lower_test.go => module_lower_test.go} (100%) rename internal/ir/{shared.go => nodes.go} (99%) rename internal/ir/{shared_test.go => nodes_test.go} (100%) create mode 100644 internal/lsp/position.go create mode 100644 internal/lsp/position_test.go create mode 100644 internal/problems/problems_test.go rename internal/semantics/typeinfo/{info.go => types.go} (100%) rename internal/semantics/typeinfo/{info_test.go => types_test.go} (100%) create mode 100644 x_test/fixtures_test.go delete mode 100644 x_test/negative_mutable_reference_copy/peeper.toml create mode 100644 x_test/runtime_integer_division/peeper.toml create mode 100644 x_test/runtime_integer_division/src/main.peep create mode 100644 x_test/runtime_integer_division_zero/peeper.toml create mode 100644 x_test/runtime_integer_division_zero/src/main.peep create mode 100644 x_test/type_mutable_reference_transfer/peeper.toml rename x_test/{negative_mutable_reference_copy => type_mutable_reference_transfer}/src/main.peep (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6e3491d --- /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 0e336be..b17a56f 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 09139de..3d5c156 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 08c4852..9f228cd 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 bdb6485..aa2dbc1 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 61b6b49..0869ec1 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 77934b4..c89c42b 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 8e63da1..af2944b 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 1590ec2..61fce1f 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 e96e9cf..f92c7e5 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 ab8555f..9ca9e61 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 0000000..c8c7741 --- /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 0fff9fa..b2fa4c3 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 0000000..f431fab --- /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 3bf5a02..0000000 --- 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 0000000..5734822 --- /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 14e3fdb..fc59e8e 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 19714c7..36e2f90 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 0000000..6c27f88 --- /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 58ad01c..96963d4 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 71311ef..d7c24cf 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 622577f..a986868 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 0000000..bf3a178 --- /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 0000000..c56a46d --- /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 1aab322..890550f 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 dd10ff7..0d1926a 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 965696d..a18acfc 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 2811852..9bd80b7 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 b3dd932..6c5b646 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 4903ef4..e348307 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 190613f..7da7e70 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 0000000..b60a556 --- /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 0000000..6c4505f --- /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 0951a40..6f7568b 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 36cd0e3..ae01b44 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 635581d..74b44b3 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 044c9c3..e0792c0 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 d47fe77..4112cba 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 4eecab0..630c7d3 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 f0bfffb..5fa6210 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 442a417..971ec24 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 0000000..1d88727 --- /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 16f04a2..ad09317 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 c0a8d76..9c06f35 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 c91339a..a3c62c2 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 2a69465..e62c654 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 1c6f1a5..c6ae847 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 5aeb53f..fbd85d6 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 f37bbc1..394c48d 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 2790ba5..002a1f0 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 094dae4..bfc671a 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 6f8b669..27ff866 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 6f3023f..35f7323 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 830290c..e5cb788 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 e43d725..826aa1d 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 8f095dd..839a9dd 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 0772885..e59745d 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 3234698..166c938 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 0000000..12bb739 --- /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 4522d23..2b1cb98 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 7a53747..fd6770d 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 4afe5c8..0c3998b 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 b59bf8d..637bc9a 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 8013ab5..4672247 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 5242c44..e13016f 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 c2c7fc7..686fd2c 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 d5c5b37..8272ced 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 3e5ee98..df96dd0 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 558d5fd..203a1d8 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 c514199..036df61 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 522e7e3..d33e822 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 b9828fc..f3db2b7 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 c84bd99..1e3e840 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 ced2bb7..3a5d0b1 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 1bdec64..9535575 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 ef7f4ce..b88d5d2 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 58464f3..5f91232 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 8614d14..8d9333e 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 c5aa10d..3497583 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 16d3b00..b15d5b2 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 d116a58..320959b 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 4a1b924..e745f42 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 067804b..81babba 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 f2e33b3..b60d1d0 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 c3d7a92..080c7c4 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 5f76d56..f7c801c 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 de8f374..553cbef 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 4738278..a8edf28 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 29c5bcd..af9e066 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 4c99d71..ad303ba 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 43d42b4..5715c77 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 a2d00ea..091877d 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 7e0ecbf..77c61df 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 a33176a..4393765 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 561ba9e..0be7248 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 79492ef..5f3c70b 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 eae63b2..213225c 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 6f69d0b..2340b8e 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 19c5273..65310a2 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 9dea9a2..6b348f6 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 2fcf635..663b7fc 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 7d61d92..002849d 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 4876277..8ab4577 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 f71812d..918e014 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 6c53ba4..ae4e7da 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 16f6c18..63d8db5 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 b10afce..d37d459 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 014ae94..0000000 --- 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 a1b1c60..e6196d6 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 213d6bb..1fe8172 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 ee57c56..0b81cfe 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 fc51fd6..e625591 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 866a1eb..00a8dd8 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 210dff4..2fc6d02 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 4378358..6272e44 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 a4ddd41..8190e59 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 e59f906..55377d0 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 5c66853..0329881 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 8badc4f..9c7135d 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 f84af7b..c2a14fa 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 baf7222..c2d9586 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 dbe51de..502f9bb 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 75e4bc5..3974874 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 ac3c293..b5ead52 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 2eb4992..ad73856 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 2769171..8019913 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 0862a95..e0e5ff4 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 305b7af..375c472 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 da6d6f1..cd4ca0e 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 5b3fe76..15ef924 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 38dbfbe..6f4bc19 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 0114392..066c3d0 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 a341731..caaf34f 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 1774ba0..0ee848f 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 177cdb1..aa377f8 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 4793713..5a3011a 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 3b84329..edf9337 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 816f846..d00ec74 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 74368e2..f32f4b0 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 2699272..80125d8 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 5651503..9ba1add 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 ba8d176..fbfce3b 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 00a4500..b1d4fa2 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 9a284d0..56932fb 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 7f24c89..fee4c56 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 aaa5d42..4630db7 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 4681f33..e9551e6 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 f420fc4..634c5fa 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 be1068b..abbbdc0 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 9842faa..5685f5a 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 7bdb8d5..418f04e 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 c257fc1..d95d70c 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 1787f1b..2dc0ec0 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 0000000..961eda7 --- /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 0000000..fc73b9a --- /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 0000000..3a7625b --- /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 0000000..3842a30 --- /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 85f67da..9020026 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 6cc5d6e..5e25651 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 b759b6a..52afb4a 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 2b3143c..77fdaed 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 700aca7..1dd544b 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 096ca90..865dc3e 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 c28d7ef..e23310e 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 16fd905..c046bac 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 0f146d4..75df2ea 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 f1c676c..09a9070 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 988570c..8482b19 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 271abbc..a209765 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 5c92c2e..4355e0f 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 b851273..7c63c4b 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 5af2b9d..dda9eb8 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 ab870df..59ef2bf 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 9101fbb..fcb7a74 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 80fe3fc..c06bfd4 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 5e454bc..7c84938 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 2bce82c..f5b3c67 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 de3464f..1ed24c5 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 f5dbd05..679de04 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 88a0f68..144d44f 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 1d76f87..944c6ba 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 6a11fc3..e566060 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 0a046be..24e0813 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 8558884..c5f48ec 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 b716fc4..247bfd2 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 dc8227a..126ea9e 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 c3d74f2..c91ff1e 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 9a4533d..2a928fd 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 aa64074..8ccd7c9 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 0000000..ec1aac2 --- /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 c68b590..be54f86 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 880847f..205ded2 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 0518d4a..793ace9 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 ae8d4a2..6e9cc0c 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 aa7bf62..d0f8a6d 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 cea9ab2..caa99e6 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 fa53fa9..a4ac7ec 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 ec3ed77..da3a426 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"