Skip to content
Closed
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
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ InferNode uses Plan 9's `mk` (not GNU make). Important points:
- Build from your host OS terminal, not inside the Inferno emulator
- `mkfile` in each directory defines build rules
- `mk install` compiles and copies output to `dis/`
- `mk nuke` cleans build artifacts
- `mk emunuke` cleans emulator build artifacts. Root `mk nuke` would delete tracked `dis/`; see `docs/WORKFLOW-TRAPS.md`.
- Don't use `&&` to chain commands in mkfiles — use `;` or separate rules

### Code Style
Expand Down
9 changes: 9 additions & 0 deletions build-macos-headless.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ echo "Building for: SYSHOST=$SYSHOST OBJTYPE=$OBJTYPE"
echo "GUI Backend: headless (no display)"
echo ""

# mkhost-MacOSX sets NDATE=ndate and resolves it through PATH. BSD date has
# no -n, so without utils/ndate built the emulator compiles KERNDATE from an
# empty string and fails on `ulong kerndate = ;`. The EMUDIRS walk builds it
# before emu; this script goes straight to the emulator, so build it here.
if [[ ! -x "$ROOT/MacOSX/arm64/bin/ndate" ]]; then
echo "Building utils/ndate (needed for KERNDATE)..."
(cd "$ROOT/utils/ndate" && mk install)
fi

# Build emulator
cd "$ROOT/emu/MacOSX"

Expand Down
13 changes: 8 additions & 5 deletions build-macos-sdl3.sh
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,16 @@ echo "Building for: SYSHOST=$SYSHOST OBJTYPE=$OBJTYPE"
echo "GUI Backend: SDL3"
echo ""

# Stamp build version (matches CI workflow)
# Stamp build version (matches CI workflow). Restore on any exit so a
# failed mk cannot leave the tracked file dirty. Match the
# unstamped form only so a leftover stamp cannot accumulate.
BUILD_DATE=$(date +%Y%m%d)
SHORT_SHA=$(git -C "$ROOT" rev-parse --short=8 HEAD 2>/dev/null || echo "local")
sed -i '' "s|InferNode 0.1|InferNode 0.1 build ${BUILD_DATE}-${SHORT_SHA}|" "$ROOT/include/version.h"
restore_version_h() {
git -C "$ROOT" checkout -- "$ROOT/include/version.h" 2>/dev/null || true
}
trap restore_version_h EXIT
sed -i '' "s|InferNode 0.1 (|InferNode 0.1 build ${BUILD_DATE}-${SHORT_SHA} (|" "$ROOT/include/version.h"
echo "Version: $(grep VERSION "$ROOT/include/version.h")"
echo ""

Expand All @@ -63,9 +69,6 @@ mk clean 2>/dev/null || true
echo "Building SDL3 GUI emulator..."
mk GUIBACK=sdl3

# Restore version.h so repeated builds don't accumulate stamps
git -C "$ROOT" checkout -- "$ROOT/include/version.h" 2>/dev/null || true

if [[ -f o.emu ]]; then
echo ""
echo "=== Build Successful ==="
Expand Down
7 changes: 7 additions & 0 deletions docs/DOCUMENTATION-INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,10 @@ Pool quanta must be 127 for 64-bit (not 31 as for 32-bit). This single change in
- [inferno64](https://github.com/caerwynj/inferno64) - Reference 64-bit port
- [Inferno Shell paper](https://www.vitanuova.com/inferno/papers/sh.html)
- [EMU manual](https://vitanuova.com/inferno/man/1/emu.html)

## Testing and Workflow

| Document | Description |
|----------|-------------|
| [WORKFLOW-TRAPS.md](WORKFLOW-TRAPS.md) | Non-obvious ways this repository wastes an afternoon: root `mk nuke`, veltro build collateral, unlisted tests, and stale test bytecode |

43 changes: 43 additions & 0 deletions docs/WORKFLOW-TRAPS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Workflow traps

Non-obvious ways this repository wastes an afternoon. Each of these has
cost one at least once. `CONTRIBUTING.md` covers the rules; this covers
the surprises.

## Building

**`mk nuke` at the repo root deletes the tracked `dis/` runtime tree.**
Root nuke walks `$DIRS` (`$EMUDIRS` + `appl`). appl's nuke deletes
`$DISBIN`, and for appl that is committed `dis/` — about 900 `.dis`
files a fresh clone needs in order to boot. Afterward the emulator dies
with `panic: loading "/dis/emuinit.dis": ... does not exist`, and
`git status` shows a wall of deletions that look like a bad merge.
Nothing is permanently lost: `git checkout -- .` restores the tree.
`mk emunuke` walks `$EMUDIRS` only and is the safe emulator-only clean.
On Posix, root `mk nuke` now refuses unless `NUKE_DIS=1` is set.
`cd appl && mk nuke` is unguarded and still deletes `dis/`.

**A failed macOS build can leave `include/version.h` rewritten.**
`build-macos-sdl3.sh` stamps the version before compiling; if the build
then fails, the stamped file stays behind and shows up as an unrelated
modification in the next `git status`. The script now restores it on
failure.

## Building Limbo

**`mk install` in `appl/veltro` rebuilds every veltro module.** The
tracked `dis/veltro/*.dis` were built by a different limbo build, so
unrelated `.dis` files show up modified. `appl/cmd` and `tests` support
per-target `mk <name>.install`; `appl/veltro` does not, so restore the
unrelated ones with `git checkout` afterwards.

## Testing

**Stale test bytecode fails as though the code were broken.** The
`post-merge` hook does not reliably rebuild `dis/tests/*.dis`, and there
is no `link typecheck` error to warn you when no `.m` interface changed:
a test's expectations are compiled into its bytecode, so a binary built
before a rename still asserts the old name while the source asserts the
new one. The failure names the assertion, not the staleness, which sends
you into the wrong file. `run-tests.sh` now refuses to run when any
`dis/tests/*.dis` is older than its source.
6 changes: 3 additions & 3 deletions lib/guide
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ ls /mnt/llm
cat /mnt/llm/model

# Speech interface
ls /n/speech
ls /mnt/speech

# Xenith filesystem
ls /mnt/xenith
Expand Down Expand Up @@ -372,8 +372,8 @@ cd appl/xenith && mk install
# Build a Veltro tool
cd appl/veltro/tools && limbo -I$ROOT/module -I$ROOT/appl/veltro -gw -o $ROOT/dis/veltro/tools/toolname.dis toolname.b

# Clean rebuild
mk nuke && mk install
# Clean rebuild (emulator only; root mk nuke deletes tracked dis/)
mk emunuke && mk emuinstall

# IMPORTANT: use native tools (MacOSX/arm64/bin/), not hosted limbo

Expand Down
14 changes: 14 additions & 0 deletions mkfile
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,20 @@ nukedist:V: nuke
rm -f $ROOT/$OBJDIR/lib/lib*.a

&-Posix:QV:
# appl nuke deletes the tracked dis/ runtime tree.
if [ "$stem" = nuke ]
then
if git -C $ROOT ls-files --error-unmatch dis/emuinit.dis >/dev/null 2>&1
then
if [ -z "$NUKE_DIS" ]
then
echo 'refusing mk nuke: would delete tracked dis/' >&2
echo 'use: mk emunuke # emulator-only clean' >&2
echo ' NUKE_DIS=1 mk nuke # really delete dis/' >&2
exit 1
fi
fi
fi
for j in $DIRS utils
do
echo "(cd $j; mk $MKFLAGS $stem)"
Expand Down
22 changes: 22 additions & 0 deletions run-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,28 @@ run_internal_tests() {
return 1
fi

# Refuse to run test bytecode that is older than its source. A stale
# .dis carries the assertions it was compiled with, so it fails against
# code that is correct, and no link typecheck error explains why unless
# a .m interface also changed. That reads as "the change is broken" and
# invites editing correct code to match a stale test. A merge is the
# usual way a tree ends up like this.
_stale=""
for _src in "$ROOT"/tests/*_test.b; do
[ -f "$_src" ] || continue
_dis="$ROOT/dis/tests/$(basename "$_src" .b).dis"
[ -f "$_dis" ] || continue
if [ "$_src" -nt "$_dis" ]; then
_stale="$_stale $(basename "$_dis")"
fi
done
if [ -n "$_stale" ]; then
echo "stale test bytecode, older than its source:"
for _s in $_stale; do echo " dis/tests/$_s"; done
echo "Run 'mk install' in tests/ before trusting a result."
return 1
fi

# Build emu args
EMU_ARGS="-r$ROOT"
if [ "$VERBOSE" = "1" ]; then
Expand Down
107 changes: 81 additions & 26 deletions tools/verify-dis-paths.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,24 @@
# correct path silently kept running while every "rebuilt" file landed
# in a parallel directory emu never read from.
#
# This script verifies that for every Limbo source file in
# appl/cmd/ that contains a `PATH: con "/dis/...` declaration, the
# This script verifies that for every Limbo source file that contains a
# `PATH: con "/dis/...` declaration for the module it IMPLEMENTS, the
# corresponding compiled .dis exists at that path AND is at least as
# new as the source. Runs in under a second. Pre-commit hook
# candidate; also wired into `make` (see mkfile).
# new as the source.
#
# Coverage: every directory that installs .dis files, i.e. every .b
# source under appl/ and tests/. The set is derived from the source
# tree itself (find), NOT from a hardcoded directory list: a hardcoded
# list is this same bug one directory later. Parsing the mkfiles
# instead would re-implement mk's recursive DIRS/include logic badly;
# the whole-tree scan is a strict superset of the mkfile-derived set,
# and sources that legitimately carry no inline PATH constant (their
# load path lives in module/*.m; mk's TARG governs their target) are
# counted and skipped explicitly, not failed.
#
# Runs in well under a second (single awk pass over the tree; stat is
# only spawned for the few sources that declare a PATH). Pre-commit
# hook candidate; also wired into CI (see .github/workflows/).
#
# Exit codes:
# 0 — all sources have a fresh, correctly-placed .dis
Expand All @@ -29,6 +42,7 @@ cd "$ROOT"

fail=0
checked=0
skipped=0

# stat(1) is the classic cross-platform divergence:
# macOS / BSD: stat -f %m FILE → mtime as epoch seconds
Expand All @@ -42,42 +56,81 @@ else
mtime() { stat -f %m "$1"; }
fi

scan_module_path() {
# Return the PATH constant of the module that this .b file
# IMPLEMENTS (top-level `implement Foo;`), not the first PATH it
# happens to mention. Files commonly cite the PATHs of modules
# they LOAD (cowfs, etc.) before declaring their own interface;
# matching the first PATH alone gives wrong answers.
#
# Strategy: find `implement Foo;`, then scan forward for
# `Foo: module {` ... `PATH: con "/dis/..."` ... `}`.
# scan_module_paths — one awk process over ALL sources.
# Same matching rules as tools/compile-limbo.sh (kept in sync): find
# `implement Foo;`, then scan forward for `Foo: module {` ...
# `PATH: con "/dis/..."` ... `}`. The PATH of the module the file
# IMPLEMENTS, not the first PATH it mentions — files commonly cite the
# PATHs of modules they LOAD (cowfs, etc.) before their own interface.
#
# Input: the .b sources as file arguments.
# Output: "<source>\t<PATH-constant>" per source that declares one,
# plus a final "#skipped <n>" line for sources that do not.
scan_module_paths() {
awk '
/^implement[[:space:]]+[A-Za-z][A-Za-z0-9_]*[[:space:]]*;/ {
function reset() {
impl = ""; in_mod = 0; done = 0
}
FILENAME != prev {
if (prev != "" && !done)
noskipped++
prev = FILENAME
reset()
}
!done && /^implement[[:space:]]+[A-Za-z][A-Za-z0-9_]*[[:space:]]*;/ {
match($0, /[A-Za-z][A-Za-z0-9_]*[[:space:]]*;/)
impl = substr($0, RSTART, RLENGTH)
sub(/[[:space:]]*;.*$/, "", impl)
}
impl != "" && $0 ~ ("^" impl "[[:space:]]*:[[:space:]]*module") {
!done && impl != "" && $0 ~ ("^" impl "[[:space:]]*:[[:space:]]*module") {
in_mod = 1
next
}
in_mod && /PATH[[:space:]]*:[[:space:]]*con[[:space:]]*"\/dis\// {
in_mod && !done && /PATH[[:space:]]*:[[:space:]]*con[[:space:]]*"\/dis\// {
match($0, /"\/dis\/[^"]+"/)
if(RSTART > 0) {
if (RSTART > 0) {
p = substr($0, RSTART+1, RLENGTH-2)
print p
exit
print FILENAME "\t" p
done = 1
}
}
in_mod && /^[[:space:]]*}/ {
in_mod = 0
}
' "$1"
END {
if (prev != "" && !done)
noskipped++
print "#skipped " noskipped + 0
}
' "$@"
}

for src in appl/cmd/*.b; do
disrel=$(scan_module_path "$src" || true)
[[ -z "$disrel" ]] && continue
# Every tree that installs .dis files: appl/ (via appl/*/mkfiles) and
# tests/ (via tests/mkfile). find-derived so a new directory is
# covered the moment it exists. bash 3.2 (macOS /bin/bash) has no
# mapfile, so build arrays with read loops.
SOURCES=()
while IFS= read -r -d '' src; do
SOURCES+=("$src")
done < <(find appl tests -name '*.b' -type f -print0 | sort -z)

if (( ${#SOURCES[@]} == 0 )); then
echo "verify-dis-paths: no .b sources found — run from the repo root" >&2
exit 2
fi

ROWS=()
while IFS= read -r row; do
ROWS+=("$row")
done < <(scan_module_paths "${SOURCES[@]}")

for row in "${ROWS[@]}"; do
if [[ "$row" == \#skipped* ]]; then
skipped="${row#\#skipped }"
continue
fi
src="${row%%$'\t'*}"
disrel="${row#*$'\t'}"

# Strip leading slash and prefix with the source-tree root.
dispath="${disrel#/}"
Expand All @@ -93,16 +146,18 @@ for src in appl/cmd/*.b; do
if (( src_t > dis_t )); then
echo "FAIL: $src is newer than $dispath (recompile needed)" >&2
fail=1
continue
fi
checked=$((checked + 1))
done

if (( fail )); then
echo "" >&2
echo "$checked sources checked; FAILURES above." >&2
echo "Recompile with: limbo -I module -o <PATH-from-source> <source.b>" >&2
echo "$checked sources checked, $skipped skipped (no inline PATH); FAILURES above." >&2
echo "Recompile with: tools/compile-limbo.sh <source.b> (do NOT pick an -o path by hand)" >&2
echo "(do NOT default to dis/cmd/X.dis — read the module's PATH constant)" >&2
exit 1
fi

echo "OK: $checked sources have fresh .dis at their declared PATH"
echo "OK: $checked sources have fresh .dis at their declared PATH" \
"($skipped skipped: no inline PATH constant, mk TARG governs those)"
Loading