# gasm-sdk: Go's Plan 9 assembler and the tooling around it.
#
# A binary: `just install` copies gasm into bindir. Everything below the variable block
# is the standard recipe set: the names and their meanings are fixed, and `gates` is the
# definition of done. link-parity, install-man, uninstall-man and gen are the project
# extensions.
binary := "gasm"
package := "./cmd/gasm"

# What the test and bench recipes sweep. Scope this to the logic packages when a thin
# cmd/ drags the coverage floor down, for example "./internal/... ./pkg/...". Never name
# a directory the project does not have: a pattern that matches nothing is a setup
# failure, not an empty run.
packages := "./arch/... ./asm/... ./ast/... ./disasm/... ./format/... ./lexer/... ./lint/... ./lsp/... ./parser/... ./token/... ./verify/..."

# The memory fence for the test recipes: a cgroup ceiling with swap off, so a
# runaway run dies at the ceiling as a failed run and never eats the machine. 4G is the
# default; raise it only with a reason recorded here.
memlimit := "4G"

# The race recipe's own fence: with the logic packages run in parallel the race
# footprint peaks over 8G (8G died in the parser package, 16G passed, measured
# 2026-10-02), and the wide machine at the keyboard can carry it.
racememlimit := "16G"

bindir := env_var_or_default("BINDIR", env_var("HOME") / ".local" / "bin")

# Where `install-man` puts the gzip-compressed pages (man1 below it). Exported because
# the Perl recipes read it from the environment.
export MANDIR := env_var_or_default("MANDIR", env_var("HOME") / ".local" / "share" / "man")

default:
    @just --list

# Compile. Zero errors, zero warnings; -trimpath and -buildvcs=true make the binary place-independent and version-stamped.
build:
    CGO_ENABLED=0 go build -trimpath -buildvcs=true -ldflags "-s -w" -o bin/{{binary}} {{package}}

# The test gate: the full suite, no cache, the coverage floor, under the memory fence.
test:
    #!/usr/bin/env perl
    my @fence = (q{systemd-run}, q{--user}, q{--scope},
                 q{-p}, q{MemoryMax={{memlimit}}}, q{-p}, q{MemorySwapMax=0});
    system(@fence, q{go}, q{test}, q{-count=1}, q{-timeout}, q{0},
      q{-coverprofile}, q{coverage.out}, qw({{packages}})) == 0
      or die qq{the test suite failed\n};
    # The packages outside the coverage set carry tests of their own: the CLI's
    # exit codes and manual-page guard, and the debugger's architecture-neutral
    # units.  They run without a profile, because a thin main and a ptrace-bound
    # package would drag the floor down rather than measure the product.
    system(@fence, q{go}, q{test}, q{-count=1}, q{-timeout}, q{0},
      q{./cmd/...}, q{./debug/...}) == 0
      or die qq{the tests outside the coverage set failed\n};
    open(my $c, q{-|}, q{go}, q{tool}, q{cover}, q{-func=coverage.out}) or die qq{cover: $!};
    my $total;
    while (my $l = <$c>) { $total = $1 if $l =~ m{^total:\s+\S+\s+([0-9.]+)%} }
    close($c);
    die qq{no total line in coverage.out\n} unless defined $total;
    printf qq{Total coverage: %s%%\n}, $total;
    exit($total < 80 ? 1 : 0);

# The same suite under the race detector. The expensive one, still fenced.
race:
    systemd-run --user --scope -p MemoryMax={{racememlimit}} -p MemorySwapMax=0 go test -race -count=1 -timeout 0 {{packages}}

# Fast scoped run for iterating. This is the one that runs after every edit.
unit pkgs=packages run=".*":
    systemd-run --user --scope -p MemoryMax={{memlimit}} -p MemorySwapMax=0 go test -timeout 0 {{pkgs}} -run '{{run}}'

# Time-boxed fuzz of one target in one package. The package is required; never a gate.
fuzz target pkg fuzztime="60s":
    systemd-run --user --scope -p MemoryMax={{memlimit}} -p MemorySwapMax=0 go test -timeout 0 -run '^$' -fuzz '{{target}}' -fuzztime={{fuzztime}} {{pkg}}

# The cmd/link GOOBJ parity gate, opt-in: it costs minutes no pipeline can afford.
link-parity:
    systemd-run --user --scope -p MemoryMax={{memlimit}} -p MemorySwapMax=0 env GASM_LINK_PARITY=1 go test -count=1 -timeout 0 -run 'TestGOOBJLinkRegression' ./verify/

# Benchmarks. On an idle machine only, and deliberately unfenced.
bench pkgs=packages:
    go test -timeout 0 -run '^$' -bench=. -benchmem -count=5 {{pkgs}}

# Format in place.
fmt:
    gofmt -w .

# Zero diff. Prints nothing when everything is formatted.
fmt-check:
    #!/usr/bin/env perl
    open(my $g, q{-|}, q{gofmt}, q{-l}, q{.}) or die qq{gofmt: $!};
    my @bad = <$g>;
    close($g);
    print @bad;
    exit(@bad ? 1 : 0);

# Both static gates: go vet and go fix -diff.
vet:
    go vet ./...
    go fix -diff ./...

# The definition of done, in one command. Once per task, never per edit.
gates: build fmt-check vet test race

# Build artefacts only, not the installed binary.
clean:
    rm -rf bin/ coverage.out

# Build, then copy the binary into bindir.
install: build
    install -d "{{bindir}}"
    install -m 755 bin/{{binary}} "{{bindir}}/{{binary}}"

# Remove the installed binary.
uninstall:
    rm -f "{{bindir}}/{{binary}}"

# Install the man pages under docs/man into mandir/man1, gzip-compressed; not a gate.
install-man:
    #!/usr/bin/env perl
    my $out = $ENV{MANDIR} . q{/man1};
    system(q{mkdir}, q{--parents}, $out) == 0 or die qq{mkdir $out: $!\n};
    for my $p (glob q{docs/man/*.1}) {
        open(my $g, q{-|}, q{gzip}, q{--stdout}, $p) or die qq{gzip $p: $!\n};
        my $content = do { local $/; <$g> };
        close($g);
        my $base = $p;
        $base =~ s{docs/man/}{};
        open(my $o, q{>}, qq{$out/$base.gz}) or die qq{write $out/$base.gz: $!\n};
        print {$o} $content;
        close($o);
        print qq{$out/$base.gz\n};
    }

# Remove the installed man pages.
uninstall-man:
    #!/usr/bin/env perl
    for my $p (glob q{docs/man/*.1}) {
        my $base = $p;
        $base =~ s{docs/man/}{};
        my $f = $ENV{MANDIR} . q{/man1/} . $base . q{.gz};
        if (-f $f) {
            unlink($f) or die qq{unlink $f: $!\n};
            print qq{removed $f\n};
        }
    }

# Run the program. The flag is there because `go run` does not stamp the build otherwise.
run:
    go run -buildvcs=true {{package}}

# Run with watch or hot reload, where the project has one.
dev:
    go run -buildvcs=true {{package}}

# Regenerate the architecture instruction tables from the Go toolchain source. Not a gate.
gen:
    go run _gen/gen.go
    gofmt -w arch/
