diff --git a/Readme.md b/Readme.md
index 7feef542..04518dd7 100644
--- a/Readme.md
+++ b/Readme.md
@@ -88,6 +88,21 @@ classes and interfaces rather than opaque native handles (see
without JNI glue
([test and demo](tests/integration/jvm_link_names/src/lib.rs)).
+Java exposure is explicit: `pub` controls Rust visibility, while
+`#[jvm_codegen::export]` on a public function, type, or module preserves its
+Java-facing API. Marking a module includes its public descendants; marking a type
+includes its public inherent methods. Register the tool with
+`#![feature(register_tool)]` and `#![register_tool(jvm_codegen)]`.
+For a library intended entirely for Java, add `#![feature(custom_inner_attributes)]`
+and `#![jvm_codegen::export]` at the crate root, as in the examples below.
+Mark the types Java constructs or inspects as well as the functions it calls.
+Use concrete, nongeneric exports as Java entry points for generic Rust APIs.
+Ordinary Rust dependencies remain internal: the compiler can flatten their
+representations, merge their classes, and remove unused methods. Rebuild Rust
+dependencies with the same backend; generated internal classes are not a stable
+Java API. Importing existing Java classes with `jvm::class` or `jvm::interface`
+does not require an export marker.
+
For example, one Rust API can expose an enum and accept both a JVM
implementation of a Rust trait and a standard JVM lambda. Its result can then
cross the Rust/Kotlin async bridge. The complete example is kept executable in
@@ -96,6 +111,10 @@ cross the Rust/Kotlin async bridge. The complete example is kept executable in
**Rust**
```rust
+#![feature(register_tool, custom_inner_attributes)]
+#![register_tool(jvm_codegen)]
+#![jvm_codegen::export]
+
pub trait BatchObserver {
fn accept(&mut self, processed: u32) -> bool;
}
@@ -247,8 +266,9 @@ Add `jvm = { package = "rcj", git = "https://github.com/IntegralPilot/rustc_code
to `[dependencies]`.
```rust
-#![feature(extern_types, register_tool)]
+#![feature(extern_types, register_tool, custom_inner_attributes)]
#![register_tool(jvm_codegen)]
+#![jvm_codegen::export]
#[jvm::class("java.time.LocalDate", rename_all = "camelCase")]
impl JavaLocalDate {
diff --git a/Tester.py b/Tester.py
index 56c0e7a2..e99b3943 100644
--- a/Tester.py
+++ b/Tester.py
@@ -8,6 +8,7 @@
import shutil
import subprocess
import sys
+import zipfile
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
@@ -31,6 +32,13 @@ def read(path: Path) -> str:
return path.read_text(encoding="utf-8")
+def normalize_internal_symbols(text: str) -> str:
+ """Private owner placement and Rust identity hashes are not debug semantics."""
+ text = re.sub(r"\b[\w.$]+\.mono\.Mono(?:Bucket)?(?:_[0-9a-f]+)?\b", "private.Mono", text)
+ text = re.sub(r"\bMono(?:Bucket)?_[0-9a-f]+\b", "Mono", text)
+ return re.sub(r"(?:\$[0-9a-f]{16}|\$m\$*[0-9a-f]+)(?=\()", "", text)
+
+
def write_failure(path: Path, proc) -> None:
path.write_text(
f"STDOUT:\n{proc.stdout}\n\nSTDERR:\n{proc.stderr}",
@@ -69,8 +77,10 @@ def check_results(proc, test: TestCase, release: bool, logs: list[str]) -> bool:
return True
# so it works on Windows
- expected = "".join(read(expected_path).strip().split()).replace("\\", "/")
- actual = "".join(f"STDOUT:{proc.stdout.strip()}STDERR:{proc.stderr.strip()}".split()).replace("\\", "/")
+ expected = normalize_internal_symbols(read(expected_path))
+ actual = normalize_internal_symbols(f"STDOUT:{proc.stdout.strip()}STDERR:{proc.stderr.strip()}")
+ expected = "".join(expected.strip().split()).replace("\\", "/")
+ actual = "".join(actual.split()).replace("\\", "/")
if actual == expected:
logs.append("|--- ✅ Output matches expected output!")
@@ -350,6 +360,7 @@ def javap_debug_info(output: str, included_methods: set[str] | None = None) -> s
source copies and reordering branches changes the executable line sequence.
"""
source: str | None = None
+ current_source: str | None = None
methods: dict[str, list[str]] = {}
local_rows: dict[str, set[str]] = {}
current_method: str | None = None
@@ -358,7 +369,7 @@ def javap_debug_info(output: str, included_methods: set[str] | None = None) -> s
for line in output.splitlines():
stripped = line.strip()
if stripped.startswith("Compiled from "):
- source = stripped
+ current_source = stripped
continue
if (
line.startswith(" ")
@@ -366,11 +377,14 @@ def javap_debug_info(output: str, included_methods: set[str] | None = None) -> s
and "(" in stripped
and stripped.endswith(";")
):
+ stripped = normalize_internal_symbols(stripped)
current_method = (
stripped
if included_methods is None or stripped in included_methods
else None
)
+ if current_method is not None:
+ source = current_source
table = None
continue
if stripped in {"LineNumberTable:", "LocalVariableTable:"}:
@@ -440,9 +454,16 @@ def check_javap_debug_info(
if not expected_path.exists():
return True
- class_name = f"{test.artifact_name}.{test.artifact_name}"
+ class_names = [f"{test.artifact_name}.{test.artifact_name}"]
+ # Private Rust methods can move between classes. Debug checks must include their current owners.
+ with zipfile.ZipFile(jar) as archive:
+ class_names.extend(sorted(
+ name.removesuffix(".class").replace("/", ".")
+ for name in archive.namelist()
+ if "/mono/Mono" in name and name.endswith(".class")
+ ))
logs.append("|--- 🔎 Checking JVM debug metadata with javap...")
- proc = run_command(["javap", "-classpath", str(jar), "-c", "-l", "-p", class_name])
+ proc = run_command(["javap", "-classpath", str(jar), "-c", "-l", "-p", *class_names])
if proc.returncode != 0:
write_failure(test.directory / "javap-fail.generated", proc)
logs.append(f"|---- ❌ javap exited with code {proc.returncode}")
@@ -466,6 +487,26 @@ def check_javap_debug_info(
return False
+def check_scalar_paths(test: TestCase, jar: Path, release: bool, logs: list[str]) -> bool:
+ expected = test.directory / "scalar-paths.expected"
+ if not release or not expected.exists():
+ return True
+ from test_codegen import scalar_path_errors
+ proc = run_command(["javap", "-classpath", str(jar), "-c", "-p", f"{test.artifact_name}.{test.artifact_name}"])
+ if proc.returncode:
+ write_failure(test.directory / "javap-fail.generated", proc)
+ logs.append("|---- ❌ Could not inspect generated bytecode")
+ return False
+ errors = scalar_path_errors(proc.stdout, set(read(expected).split()))
+ if errors:
+ (test.directory / "scalar-paths-fail.generated").write_text("\n".join(errors) + "\n", encoding="utf-8")
+ logs.append("|---- ❌ Runtime work remains on a normal return path")
+ ci_diagnostic(logs, "\n".join(errors))
+ return False
+ logs.append("|--- ✅ Scalar paths contain no runtime calls or object allocations")
+ return True
+
+
def run_test(
test: TestCase,
release: bool,
@@ -494,6 +535,8 @@ def run_test(
return False, logs
if not check_javap_debug_info(test, jar, release, logs):
return False, logs
+ if not check_scalar_paths(test, jar, release, logs):
+ return False, logs
logs.append("|--- ✅ Test passed!")
return True, logs
diff --git a/compiler-core/src/analysis/arrays.rs b/compiler-core/src/analysis/arrays.rs
new file mode 100644
index 00000000..b1a51c7f
--- /dev/null
+++ b/compiler-core/src/analysis/arrays.rs
@@ -0,0 +1,275 @@
+//! Prove that fresh primitive arrays have no decoded or encoded aliases.
+//! Escape checks cover calls, storage, addresses and mixed joins. Local checks
+//! also permit native writes before the first escape in a block.
+use super::{NO_ORIGIN, origins::origins_with};
+use crate::ir::*;
+use crate::opt::Live;
+
+pub(crate) fn native_array_accesses(
+ body: &Body,
+ types: &Types,
+ live: &Live,
+) -> Vec