Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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
13 changes: 12 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,21 @@ jobs:

- name: Cache Cargo dependencies
uses: Swatinem/rust-cache@v2
with:
# Older caches contain source overlays that the action treats as build artifacts.
prefix-key: self-tests-artifacts-v1

- name: Build backend and tools
run: python build.py ci

- name: Run compiler unit tests
id: compiler_unit_tests
continue-on-error: true
run: cargo test
shell: bash
run: |
cargo test
cargo test --manifest-path compiler-core/Cargo.toml
cargo test --manifest-path java-linker/Cargo.toml

- name: Check JVM binding macros
id: jvm_macro_tests
Expand All @@ -93,6 +100,10 @@ jobs:
continue-on-error: true
run: python Tester.py --release

- name: Remove generated sources before caching build artifacts
if: always()
run: python -c "from pathlib import Path; import shutil; p = Path('target/stdlib-overlay'); p.exists() and shutil.rmtree(p)"

- name: Require 100% self-test pass rate
if: >-
always() &&
Expand Down
47 changes: 10 additions & 37 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 1 addition & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ version = "0.1.0"
edition = "2024"

[dependencies]
num-bigint = "0.4.6"
num-traits = "0.2.19"
once_cell = "1.20.2"
jvm-compiler-core = { path = "compiler-core", features = ["serde"] }
ristretto_classfile = "0.31.0"
rustc-hash = "2.1.1"
serde = { version = "1.0.219", features = ["derive"] }
Expand Down
107 changes: 25 additions & 82 deletions Metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,43 +102,18 @@ def summarize(records: list[dict[str, Any]]) -> dict[str, Any]:
)
oomir_before: dict[str, int] = defaultdict(int)
oomir_after: dict[str, int] = defaultdict(int)
optimise2: dict[str, int] = defaultdict(int)
liveness: dict[str, int] = defaultdict(int)
selection: dict[str, int] = defaultdict(int)
type_cache: dict[str, int] = defaultdict(int)
classfiles: dict[str, int] = defaultdict(int)
passes: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int))
repeated_types: dict[str, int] = defaultdict(int)
amplified_classes: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int))
top_methods: list[dict[str, Any]] = []
largest_shards: list[dict[str, Any]] = []

for record in compilers:
add_fields(oomir_before, record.get("oomir_before_optimise1", {}), oomir_fields)
add_fields(oomir_after, record.get("oomir_after_optimise1", {}), oomir_fields)
compiler_optimise2 = record.get("optimise2", {})
add_fields(
optimise2,
compiler_optimise2,
(
"methods",
"input_instructions",
"output_instructions",
"input_max_locals",
"output_max_locals",
),
)
add_fields(
liveness,
compiler_optimise2.get("liveness", {}),
(
"analyses",
"instructions",
"locals",
"matrix_words",
"successor_edges",
"worklist_pops",
),
)
add_fields(oomir_before, record.get("oomir_construction", {}), oomir_fields)
add_fields(oomir_after, record.get("oomir_sealed", {}), oomir_fields)
add_fields(selection, record.get("selection", {}), ("methods", "ssa_instructions", "jvm_instructions", "locals"))
add_fields(type_cache, record.get("type_lowering_cache", {}), ("hits", "misses"))
for origin in record.get("classfiles_by_origin", []):
add_fields(
Expand All @@ -154,20 +129,7 @@ def summarize(records: list[dict[str, Any]]) -> dict[str, Any]:
"name_collisions",
),
)
for item in compiler_optimise2.get("passes", []):
add_fields(
passes[item.get("pass", "<unknown>")],
item,
(
"invocations",
"input_instructions",
"output_instructions",
"instructions_removed",
"instructions_added",
"length_changing_invocations",
),
)
for item in compiler_optimise2.get("top_methods_by_structural_work", []):
for item in record.get("top_methods", []):
top_methods.append({"crate": record.get("crate_name", "<unknown>"), **item})
for item in record.get("largest_shards", []):
largest_shards.append({"crate": record.get("crate_name", "<unknown>"), **item})
Expand Down Expand Up @@ -210,13 +172,11 @@ def summarize(records: list[dict[str, Any]]) -> dict[str, Any]:
"compiler_processes": len(compilers),
"linker_processes": len(linkers),
"parse_errors": parse_errors,
"oomir_before_optimise1": dict(oomir_before),
"oomir_after_optimise1": dict(oomir_after),
"oomir_construction": dict(oomir_before),
"oomir_sealed": dict(oomir_after),
"type_lowering_cache": dict(type_cache),
"optimise2": dict(optimise2),
"liveness": dict(liveness),
"selection": dict(selection),
"classfiles": dict(classfiles),
"passes": {name: dict(values) for name, values in passes.items()},
"repeated_data_types": sorted(
repeated_types.items(), key=lambda item: item[1], reverse=True
)[:20],
Expand All @@ -229,13 +189,13 @@ def summarize(records: list[dict[str, Any]]) -> dict[str, Any]:
reverse=True,
)[:20],
"top_methods": sorted(
top_methods, key=lambda item: item.get("work_units", 0), reverse=True
top_methods, key=lambda item: item.get("ssa_instructions", 0), reverse=True
)[:20],
"largest_shards": sorted(
largest_shards,
key=lambda item: (
item.get("before_optimise1", {}).get("instructions", 0),
item.get("before_optimise1", {}).get("data_types", 0),
item.get("construction", {}).get("instructions", 0),
item.get("construction", {}).get("data_types", 0),
),
reverse=True,
)[:20],
Expand All @@ -259,26 +219,20 @@ def format_result(result: Result, top: int) -> str:
f" records: {summary.get('compiler_processes', 0)} compiler, "
f"{summary.get('linker_processes', 0)} linker",
]
before = summary.get("oomir_before_optimise1", {})
after = summary.get("oomir_after_optimise1", {})
before = summary.get("oomir_construction", {})
after = summary.get("oomir_sealed", {})
lines.append(
" OOMIR: "
" Construction -> SSA: "
f"{before.get('instructions', 0):,} -> {after.get('instructions', 0):,} instructions; "
f"{before.get('data_types', 0):,} shard-local data-type definitions"
)
optimise2 = summary.get("optimise2", {})
selection = summary.get("selection", {})
lines.append(
" optimise2: "
f"{optimise2.get('methods', 0):,} methods, "
f"{optimise2.get('input_instructions', 0):,} -> "
f"{optimise2.get('output_instructions', 0):,} bytecode instructions"
)
liveness = summary.get("liveness", {})
lines.append(
" liveness: "
f"{liveness.get('analyses', 0):,} analyses, "
f"{liveness.get('matrix_words', 0):,} matrix words allocated, "
f"{liveness.get('worklist_pops', 0):,} worklist pops"
" JVM selection: "
f"{selection.get('methods', 0):,} methods, "
f"{selection.get('ssa_instructions', 0):,} SSA instructions -> "
f"{selection.get('jvm_instructions', 0):,} bytecode instructions, "
f"{selection.get('locals', 0):,} total local slots"
)
cache = summary.get("type_lowering_cache", {})
hits = cache.get("hits", 0)
Expand All @@ -293,17 +247,6 @@ def format_result(result: Result, top: int) -> str:
f"{classes.get('exact_duplicates', 0):,} exact duplicates discarded "
f"({classes.get('exact_duplicate_bytes', 0):,} generated bytes)"
)
hottest_passes = sorted(
summary.get("passes", {}).items(),
key=lambda item: item[1].get("input_instructions", 0),
reverse=True,
)[:top]
lines.append(" largest optimise2 pass inputs:")
for name, values in hottest_passes:
lines.append(
f" {name}: received {values.get('input_instructions', 0):,}, "
f"removed {values.get('instructions_removed', 0):,}"
)
repeated = summary.get("repeated_data_types", [])[:top]
if repeated:
lines.append(" most repeated shard-local data types:")
Expand All @@ -318,20 +261,20 @@ def format_result(result: Result, top: int) -> str:
)
methods = summary.get("top_methods", [])[:top]
if methods:
lines.append(" highest optimise2 structural work:")
lines.append(" largest SSA bodies:")
lines.extend(
f" {item.get('crate', '<unknown>')}::{item.get('item', '<unknown>')}: "
f"{item.get('work_units', 0):,} work units, "
f"{item.get('input_instructions', 0):,} input instructions"
f"{item.get('ssa_instructions', 0):,} SSA instructions, "
f"{item.get('jvm_instructions', 0):,} JVM instructions"
for item in methods
)
shards = summary.get("largest_shards", [])[:top]
if shards:
lines.append(" largest OOMIR shards:")
lines.extend(
f" {item.get('crate', '<unknown>')}::{item.get('shard', '<unknown>')}: "
f"{item.get('before_optimise1', {}).get('instructions', 0):,} instructions, "
f"{item.get('before_optimise1', {}).get('data_types', 0):,} data types"
f"{item.get('construction', {}).get('instructions', 0):,} instructions, "
f"{item.get('construction', {}).get('data_types', 0):,} data types"
for item in shards
)
linker = summary.get("linker", {})
Expand Down
30 changes: 27 additions & 3 deletions Tester.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,8 +345,9 @@ def javap_debug_info(output: str, included_methods: set[str] | None = None) -> s

Bytecode offsets and local-variable live ranges legitimately vary with
constant-pool layout, the host toolchain, and optimization details. The
source-line sequence and each variable's slot/name/signature are the stable
metadata this test is intended to protect.
source-line sequence and each variable's slot/name/signature protect the
selected lowering path. SSA may have its own expectation because removing
source copies and reordering branches changes the executable line sequence.
"""
source: str | None = None
methods: dict[str, list[str]] = {}
Expand Down Expand Up @@ -405,6 +406,29 @@ def javap_debug_info(output: str, included_methods: set[str] | None = None) -> s
return "\n".join(result).strip()


def debug_semantics(text: str) -> tuple:
"""Source coverage and bindings survive register allocation and block layout."""
source = None
methods = {}
current = None
for line in text.splitlines():
line = line.strip()
if line.startswith('Compiled from '):
source = line
elif '(' in line and line.endswith(';'):
current = (set(), [])
methods[line] = current
elif current is not None:
if match := re.fullmatch(r'line (\d+)', line):
current[0].add(int(match[1]))
elif match := re.fullmatch(r'\d+ (\S+ \S+)', line):
# Keep repeated names: shadowed bindings are distinct even if
# their source name and descriptor happen to be identical.
current[1].append(match[1])
return source, tuple((name, tuple(sorted(lines)), tuple(sorted(locals)))
for name, (lines, locals) in sorted(methods.items()))


def check_javap_debug_info(
test: TestCase, jar: Path, release: bool, logs: list[str]
) -> bool:
Expand All @@ -431,7 +455,7 @@ def check_javap_debug_info(
if "(" in line and line.strip().endswith(";")
}
actual = javap_debug_info(proc.stdout, expected_methods)
if actual == expected:
if debug_semantics(actual) == debug_semantics(expected):
logs.append("|--- ✅ JVM debug metadata matches expected output!")
return True

Expand Down
7 changes: 5 additions & 2 deletions build.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,16 @@ class Config:
# Source File Lists (used for dependency tracking)
# Using glob patterns for automatic discovery
RUNTIME_SOURCES = sorted(RUNTIME_DIR.glob("src/**/*.java"))
COMPILER_CORE_SOURCES = [ROOT_DIR / "compiler-core/Cargo.toml"] + list(
ROOT_DIR.glob("compiler-core/src/**/*.rs")
)
BACKEND_RUST_SOURCES = [ROOT_DIR / "Cargo.toml", ROOT_DIR / "Cargo.lock"] + list(
ROOT_DIR.glob("src/**/*.rs")
)
) + COMPILER_CORE_SOURCES
LINKER_RUST_SOURCES = [
JAVA_LINKER_DIR / "Cargo.toml",
JAVA_LINKER_DIR / "Cargo.lock",
] + list(JAVA_LINKER_DIR.glob("src/**/*.rs"))
] + list(JAVA_LINKER_DIR.glob("src/**/*.rs")) + COMPILER_CORE_SOURCES
CARGO_JVM_RUST_SOURCES = [
CARGO_JVM_DIR / "Cargo.toml",
CARGO_JVM_DIR / "Cargo.lock",
Expand Down
Loading
Loading