Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,7 @@ graphify-out
clean.md
.*/
!.gitignore
!.github/
!.github/workflows/
!.github/workflows/*.yml
x_test/owned_pointer_carrier/main
2 changes: 1 addition & 1 deletion cmd/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
8 changes: 4 additions & 4 deletions cmd/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -372,15 +372,15 @@ 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,
TargetOS: opts.targetOS,
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
Expand Down
23 changes: 23 additions & 0 deletions cmd/command_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"errors"
"os"
"path/filepath"
"testing"
Expand All @@ -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 {
Expand Down
13 changes: 11 additions & 2 deletions cmd/dispatch.go
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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) {
Expand All @@ -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)
}
Expand Down
15 changes: 15 additions & 0 deletions cmd/dispatch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
8 changes: 4 additions & 4 deletions docs/diagrams/cli-flow-detailed.d2
Original file line number Diff line number Diff line change
Expand Up @@ -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]"

Expand Down Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions docs/diagrams/cli-flow-high-level.d2
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
}
Expand Down
6 changes: 6 additions & 0 deletions docs/language-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading