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
7 changes: 7 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ All notable changes to the HAS (High Assembler) project will be documented in th

### Changed

- **Smaller, faster 68000-compatible instruction selection.** Immediate additions now use
`addq.l` for the complete encodable range `1..8`; positive address-register additions from
`9..32767` use `lea d16(An),An`; and the peephole optimizer replaces sized `cmp #0,Dn` tests
with matching `tst Dn` instructions. The same conservative rules apply to both CPU targets.
Devpac-mode build scripts also pass vasm's `-opt-allbra` after `-devpac`, allowing the assembler
to select short encodings for unsized branches while retaining final-displacement ownership.

- **Leaner code generation for calls to `__reg(...)`-parameterized procs/funcs.** Calling a
function with register-passed parameters no longer unconditionally pushes each computed
argument to the stack and immediately pops it back before `jsr`. The stash is now only emitted
Expand Down
9 changes: 9 additions & 0 deletions docs/COMPILER_DEVELOPERS_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,15 @@ target-neutral; legality is decided before optimization by indexed-address
lowering. Generated output must be assembled with matching `vasmm68k_mot -m68000`
or `-m68020` flags.

Conservative target-neutral instruction selection currently includes three rules:

- Immediate additions use `addq.l` for values `1..8`. Positive additions from `9..32767`
to address registers use `lea d16(An),An`; other values retain `add.l`.
- The peephole optimizer rewrites only sized `cmp.b/.w/.l #0,Dn` instructions to matching
`tst` instructions. Address registers, memory operands, and unsized comparisons are excluded.
- Build scripts using vasm's Devpac mode place `-opt-allbra` after `-devpac`, re-enabling safe
shortening of unsized branches. Branch displacement and final encoding remain assembler-owned.

### CodeGen Architecture (codegen.py)

The `CodeGen` class is the heart of the compiler (2800+ lines). Understanding its organization is crucial.
Expand Down
16 changes: 11 additions & 5 deletions hasc/codegen_utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Utility functions for code generation."""

import re

from . import ast


Expand Down Expand Up @@ -105,12 +107,16 @@ def expr_to_comment(expr):


def emit_add_immediate(indent, reg, value):
"""Emit ADD instruction with immediate value.
Uses ADDQ for values 0-7 (one instruction), ADD.L for larger values."""
if 0 <= value <= 7:
"""Emit a conservative long-word immediate addition."""
is_data_reg = re.fullmatch(r"d[0-7]", reg) is not None
is_address_reg = re.fullmatch(r"a[0-7]", reg) is not None

if (is_data_reg or is_address_reg) and 1 <= value <= 8:
return f"{indent}addq.l #{value},{reg}"
else:
return f"{indent}add.l #{value},{reg}"
if is_address_reg and 9 <= value <= 32767:
# LEA is one 4-byte instruction; ADDQ/LEA on An leave CCR unchanged.
return f"{indent}lea {value}({reg}),{reg}"
return f"{indent}add.l #{value},{reg}"


def frame_offset(offset, frame_reg="a6"):
Expand Down
34 changes: 34 additions & 0 deletions hasc/peepholeopt.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ def peephole_optimize(lines, target: TargetSpec = DEFAULT_TARGET):
optimized = _fold_neg_one(optimized)
optimized = _eliminate_tst_after_andi_neg(optimized)
optimized = _eliminate_redundant_flag_test(optimized)
optimized = _optimize_cmp_zero_to_tst(optimized)

if len(optimized) < prev_len:
changed = True
Expand Down Expand Up @@ -743,6 +744,39 @@ def _eliminate_redundant_flag_test(lines):
return optimized


def _optimize_cmp_zero_to_tst(lines):
"""Replace sized CMP-zero tests on data registers with TST."""
optimized = []

for line in lines:
line_ending = ""
body = line
if body.endswith("\r\n"):
body, line_ending = body[:-2], "\r\n"
elif body.endswith("\n"):
body, line_ending = body[:-1], "\n"

instruction, separator, comment = body.partition(";")
match = re.fullmatch(
r"(?P<indent>[ \t]*)cmp\.(?P<size>[bwl])[ \t]+#0,[ \t]*"
r"(?P<reg>d[0-7])(?P<trailing>[ \t]*)",
instruction,
)
if not match:
optimized.append(line)
continue

rewritten = (
f"{match.group('indent')}tst.{match.group('size')} {match.group('reg')}"
f"{match.group('trailing')}"
)
if separator:
rewritten += separator + comment
optimized.append(rewritten + line_ending)

return optimized


def _fold_neg_one(lines):
"""Fold ``moveq #N,dX ; neg.l dX`` -> ``moveq #-N,dX``.

Expand Down
2 changes: 1 addition & 1 deletion scripts/build_example.sh
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ echo "[1/3] HAS compile..."
(cd "$ROOT" && "$PYTHON" -m hasc.cli "$REL_SRC" -o "$OUT_S")

echo "[2/3] Assemble objects..."
VASM_FLAGS=(-Fhunk -devpac -I "$LIB_DIR")
VASM_FLAGS=(-Fhunk -devpac -opt-allbra -I "$LIB_DIR")
"$VASM" "${VASM_FLAGS[@]}" "$OUT_S" -o "$OUT_O"

OBJECTS=("$OUT_O")
Expand Down
6 changes: 3 additions & 3 deletions scripts/build_fileio_demo.sh
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ echo "[1/3] Compile HAS -> assembly"
(cd "$ROOT" && "$PYTHON" -m hasc.cli "$SRC" -o "$BUILD/fileio_demo.s")

echo "[2/3] Assemble objects"
"$VASM" -Fhunk -devpac -I "$LIB" "$BUILD/fileio_demo.s" -o "$BUILD/fileio_demo.o"
"$VASM" -Fhunk -devpac -I "$LIB" "$LIB/fileio.s" -o "$BUILD/fileio.o"
"$VASM" -Fhunk -devpac -I "$LIB" "$LIB/takeover.s" -o "$BUILD/takeover.o"
"$VASM" -Fhunk -devpac -opt-allbra -I "$LIB" "$BUILD/fileio_demo.s" -o "$BUILD/fileio_demo.o"
"$VASM" -Fhunk -devpac -opt-allbra -I "$LIB" "$LIB/fileio.s" -o "$BUILD/fileio.o"
"$VASM" -Fhunk -devpac -opt-allbra -I "$LIB" "$LIB/takeover.s" -o "$BUILD/takeover.o"

echo "[3/3] Link executable"
"$VLINK" -bamigahunk \
Expand Down
4 changes: 2 additions & 2 deletions scripts/build_game.sh
Original file line number Diff line number Diff line change
Expand Up @@ -233,14 +233,14 @@ echo "[1/3] HAS compile..."
(cd "$ROOT" && "$PYTHON" -m hasc.cli "$REL_SRC" -o "$OUT_S")

echo "[2/3] Assemble objects..."
VASM_FLAGS=(-Fhunk -devpac -I "$LIB_DIR")
VASM_FLAGS=(-Fhunk -devpac -opt-allbra -I "$LIB_DIR")
"$VASM" "${VASM_FLAGS[@]}" "$OUT_S" -o "$OUT_O"

OBJECTS=("$OUT_O")
for lib in "${SELECTED_LIBS[@]}"; do
obj="$BUILD/$(basename "${lib%.s}").o"
if [[ "$(basename "$lib")" == "heap.s" && -n "${HEAP_MEMORY:-}" ]]; then
heap_flags=(-Fhunk -devpac -I "$LIB_DIR" -D "HEAP_MEMORY=${HEAP_MEMORY}")
heap_flags=(-Fhunk -devpac -opt-allbra -I "$LIB_DIR" -D "HEAP_MEMORY=${HEAP_MEMORY}")
"$VASM" "${heap_flags[@]}" "$lib" -o "$obj"
else
"$VASM" "${VASM_FLAGS[@]}" "$lib" -o "$obj"
Expand Down
2 changes: 1 addition & 1 deletion scripts/build_msgbox_demo.sh
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ if ! command -v vlink &>/dev/null; then
exit 1
fi

VASM_FLAGS=(-Fhunk -devpac -I "$LIB")
VASM_FLAGS=(-Fhunk -devpac -opt-allbra -I "$LIB")

mkdir -p "$BUILD"

Expand Down
2 changes: 1 addition & 1 deletion scripts/build_snake.sh
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ fi
# ---------------------------------------------------------------------------
# Flags
# ---------------------------------------------------------------------------
VASM_FLAGS=(-Fhunk -devpac -I "$LIB")
VASM_FLAGS=(-Fhunk -devpac -opt-allbra -I "$LIB")

mkdir -p "$BUILD"

Expand Down
84 changes: 84 additions & 0 deletions tests/test_codegen_add_immediate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""Focused tests for immediate-add instruction selection and call cleanup."""

import re

import pytest

from hasc import codegen as has_codegen
from hasc import parser as has_parser
from hasc import validator as has_validator
from hasc.codegen_utils import emit_add_immediate
from hasc.target import CpuTarget, TargetSpec


@pytest.mark.parametrize(
("reg", "expected"),
[
(
"d0",
[
"add.l #0,d0",
"addq.l #1,d0",
"addq.l #8,d0",
"add.l #9,d0",
"add.l #32767,d0",
"add.l #32768,d0",
],
),
(
"a7",
[
"add.l #0,a7",
"addq.l #1,a7",
"addq.l #8,a7",
"lea 9(a7),a7",
"lea 32767(a7),a7",
"add.l #32768,a7",
],
),
],
)
def test_emit_add_immediate_boundaries(reg, expected):
values = (0, 1, 8, 9, 32767, 32768)
assert [emit_add_immediate("", reg, value) for value in values] == expected


def test_emit_add_immediate_falls_back_for_noncanonical_inputs():
assert emit_add_immediate("", "a8", 9) == "add.l #9,a8"
assert emit_add_immediate("", "result", 1) == "add.l #1,result"
assert emit_add_immediate("", "a0", -1) == "add.l #-1,a0"


def _compile(src, target):
module = has_parser.parse(src)
has_validator.Validator(module).validate()
return has_codegen.CodeGen(module, target).gen()


def _proc_body(asm, name):
match = re.search(
rf"(?ms)^\s*{name}:\s*$.*?(?=^\s*$\n\w+:\s*$|\Z)",
asm,
)
assert match is not None
return match.group(0)


@pytest.mark.parametrize("cpu", list(CpuTarget))
@pytest.mark.parametrize(
("caller_body", "return_type"),
[
("return callee(1, 2, 3);", "int"),
("call callee(1, 2, 3);", "void"),
],
)
def test_three_argument_call_uses_lea_for_stack_cleanup(cpu, caller_body, return_type):
src = f"""
code main:
proc callee(a: int, b: int, c: int) -> int {{ return a + b + c; }}
proc caller() -> {return_type} {{ {caller_body} }}
"""
body = _proc_body(_compile(src, TargetSpec.for_cpu(cpu)), "caller")
assert "jsr callee" in body
assert "lea 12(a7),a7" in body
assert "add.l #12,a7" not in body
49 changes: 49 additions & 0 deletions tests/test_peepholeopt.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from hasc.peepholeopt import (
peephole_optimize,
_eliminate_redundant_flag_test,
_optimize_cmp_zero_to_tst,
_fold_clr_to_memory,
_fold_neg_one,
_eliminate_tst_after_andi_neg,
Expand Down Expand Up @@ -171,6 +172,53 @@ def test_eor_l_then_tst_l(self):
assert "tst.l" not in _join(out)


# ---------------------------------------------------------------------------
# _optimize_cmp_zero_to_tst
# ---------------------------------------------------------------------------

class TestOptimizeCmpZeroToTst:
def test_rewrites_all_sizes_and_boundary_data_registers(self):
inp = _asm(
"cmp.b #0,d0",
"cmp.w #0,d7",
"cmp.l #0,d0",
"cmp.b #0,d7",
"cmp.w #0,d0",
"cmp.l #0,d7",
)
assert _optimize_cmp_zero_to_tst(inp) == _asm(
"tst.b d0",
"tst.w d7",
"tst.l d0",
"tst.b d7",
"tst.w d0",
"tst.l d7",
)

def test_preserves_indentation_comment_and_line_ending(self):
inp = ["\t cmp.w #0, d7 ; direction check\n"]
assert _optimize_cmp_zero_to_tst(inp) == [
"\t tst.w d7 ; direction check\n"
]

def test_does_not_rewrite_unsupported_operands_or_forms(self):
inp = _asm(
"cmp.l #0,a0",
"cmp.l #0,(a0)",
"cmp #0,d0",
"cmp.l #1,d0",
"cmp.l #-1,d0",
"cmp.l #0,d8",
"cmp.l #0,dx",
)
assert _optimize_cmp_zero_to_tst(inp) == inp

def test_full_pipeline_registers_cmp_zero_rewrite(self):
assert peephole_optimize(_asm("cmp.l #0,d2", "beq done")) == _asm(
"tst.l d2", "beq done"
)


# ---------------------------------------------------------------------------
# _fold_neg_one
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -397,6 +445,7 @@ def test_full_pipeline_preserves_unrelated_code(self):

test_classes = [
TestEliminateRedundantFlagTest,
TestOptimizeCmpZeroToTst,
TestFoldNegOne,
TestEliminateTstAfterAndiNeg,
TestPeepholeIntegration,
Expand Down
2 changes: 1 addition & 1 deletion tests/test_scc_dbcc_codegen.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,7 @@ def test_dynamic_step_emits_runtime_direction_checks(self):
"""
asm = proc_body(compile_src(src), "for_dynamic_step")
assert "dbra" not in asm
assert "cmp.l #0,d2" in asm
assert "tst.l d2" in asm
assert re.search(r"\bbeq\s+endfor\d+\b", asm)
assert re.search(r"\bblt\s+endfor\d+_desc\b", asm)
assert re.search(r"\bbgt\s+endfor\d+\b", asm)
Expand Down
29 changes: 29 additions & 0 deletions tests/test_vasm_build_flags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""Source-contract tests for vasm optimization flags in build scripts."""

from pathlib import Path


ROOT = Path(__file__).resolve().parents[1]
DEVPAC_BUILD_SCRIPTS = (
"build_example.sh",
"build_game.sh",
"build_msgbox_demo.sh",
"build_snake.sh",
"build_fileio_demo.sh",
)


def test_all_devpac_build_paths_enable_branch_relaxation() -> None:
devpac_lines = []
for script_name in DEVPAC_BUILD_SCRIPTS:
script = (ROOT / "scripts" / script_name).read_text(encoding="utf-8")
devpac_lines.extend(line for line in script.splitlines() if "-devpac" in line)

assert len(devpac_lines) == 8
assert all("-devpac -opt-allbra" in line for line in devpac_lines)
assert all("-opt-o1" not in line for line in devpac_lines)


def test_heap_override_preserves_branch_relaxation() -> None:
script = (ROOT / "scripts" / "build_game.sh").read_text(encoding="utf-8")
assert "heap_flags=(-Fhunk -devpac -opt-allbra -I" in script
Loading