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
42 changes: 40 additions & 2 deletions jac/jaclang/compiler/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,48 @@ this file is the map of the tree and the rules that keep it organized.

The loose modules at this level are the native frontend kernel
(`jc_unit`, `jc_materialize`, `native_compiler`, `native_scope`: the parser
compiled natively and loaded as a shared library) and registries shared by
analysis and codegen (`symbol_utils`, `expr_keys`, `type_registry`,
and its early analysis passes compiled natively and loaded as a shared library)
and registries shared by analysis and codegen (`symbol_utils`, `expr_keys`, `type_registry`,
`intrinsic_registry`).

## Native early analysis

When the driver knows a module's codespace before parsing, `jc_unit` runs the
existing `ASTValidationPass` and `SymTabBuildPass` after annex weaving, inside
the parse region. The tree and symbol graph cross into the host together.
Modules with wildcard imports defer symbol construction until the driver's
dependency resolver has made the imported names available. Parsing without a
compiler program, or without a known codespace, keeps the ordinary host schedule.

`PassResult` carries completed diagnostics and timing through the ordinary pass
driver, which applies diagnostic policy and records each pass once. Native field
and reference-container layouts come from the backend's ABI metadata;
`jc_materialize` preserves object identity when copying symbol indexes and edges.
Keep pass algorithms in `passes/`, and extend this shared boundary when another
pass moves into the kernel.

`scripts/native_compile_bench.jac` at the repository root measures uncached AOT
application builds with a warm compiler. Set `JAC_COMPILER_LIB` to each built
kernel when comparing revisions.

Measured on 2026-09-06 with `examples/chess/chess.jac`, Linux x86-64 on a
Threadripper 9980X: ten builds per kernel in two fresh-process batches, two
excluded warmups per batch, ordered baseline/new/new/baseline. Both kernels used
the same host compiler source; the baseline kernel predates native early passes.
Startup was excluded; application IR caching was disabled and linking included.

| Median | Parser-only kernel | Early-analysis kernel |
| --- | ---: | ---: |
| Full AOT build | 3.646 s | 3.537 s |
| AST validation (pass ledger) | 34.25 ms | 11.22 ms |
| Symbol construction (pass ledger) | 38.19 ms | 14.00 ms |
| Both passes combined (pass ledger) | 72.45 ms | 25.04 ms |

The observed total median improvement is 3.0%; the migrated passes are 2.9x
faster together. Total build ranges overlap (3.423–4.193 s baseline,
3.340–4.145 s new), so the end-to-end figure is a local measurement rather than a
guaranteed speedup. Both generated executables completed an automatic game.

## Rules

**Backends consume facts, they do not compute them.** Types are read from
Expand Down
23 changes: 23 additions & 0 deletions jac/jaclang/compiler/backends/common/kernel_units.jac
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,29 @@ def _jaclang_dir -> str {
return os.path.dirname(str(jaclang.__file__));
}

def kernel_unit_owner(path: str, module_name: str = "") -> str {
"""Canonical native unit supplying a directly imported runtime source.""";
import from jaclang.compiler.frontend.codeinfo { resolve_native_module }
resolved = os.path.realpath(path);
for part in OSP_CORE_PARTS {
if module_name == "jaclang." + part[:-4].replace("/", ".") {
return "osp_kernel";
}
if resolved == os.path.realpath(os.path.join(_jaclang_dir(), part)) {
return "osp_kernel";
}
if os.path.basename(resolved) == os.path.basename(part) {
(_, source) = resolve_native_module(
"jaclang." + part[:-4].replace("/", "."), os.path.dirname(resolved)
);
if source is not None and resolved == os.path.realpath(source) {
return "osp_kernel";
}
}
}
return "";
}

def _units_dir -> str {
import from jaclang.jac0core.cache_paths { get_jir_cache_dir }
d = os.path.join(str(get_jir_cache_dir()), "kernel_units");
Expand Down
22 changes: 22 additions & 0 deletions jac/jaclang/compiler/backends/native/abi.jac
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
import from jaclang.compiler.backends.native.llvm { ir }

def struct_layouts(module: ir.Module) -> dict[str, tuple[list[int], int]] {
"""Read identified struct offsets and sizes from LLVM's target ABI.""";
import from jaclang.compiler.backends.native.llvm { binding }
probe = ir.Module(name="layout", context=module.context);
probe.data_layout = module.data_layout;
for (name, struct) in module.context.identified_types.items() {
if not struct.is_opaque {
ir.GlobalVariable(probe, struct, name=name);
}
}
layouts: dict[str, tuple[list[int], int]] = {};
with binding.parse_assembly(str(probe)) as bound, binding.create_target_data(
str(module.data_layout)
) as target_data {
for value in bound.global_variables {
count = len(module.context.identified_types[value.name].elements);
layouts[value.name] = target_data.get_global_struct_layout(value, count);
}
}
return layouts;
}

def scalar_layout(llt: (ir.Type | None)) -> tuple {
if isinstance(llt, ir.DoubleType) {
return (8, 8, [(0, True, 64)]);
Expand Down
57 changes: 36 additions & 21 deletions jac/jaclang/compiler/backends/native/impl/na_compile_pass.impl.jac
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,14 @@ impl native_compiler_fingerprint(options: any) -> str {
import from jaclang.compiler.driver.jir { FORMAT_VERSION }
_comp_h.update(str(FORMAT_VERSION).encode());
for _cp in [
_cfp_os.path.join(_nat_dir, "*.jac"),
_cfp_os.path.join(_nat_dir, "impl", "*.jac"),
_cfp_os.path.join(_nat_dir, "na_ir_gen", "*.jac"),
_cfp_os.path.join(_nat_dir, "na_ir_gen_pass.impl", "*.jac"),
_cfp_os.path.join(_jl_dir, "compiler", "backends", "common", "*.jac"),
_cfp_os.path.join(_jl_dir, "compiler", "passes", "layout_pass.jac"),
_cfp_os.path.join(
_jl_dir, "compiler", "passes", "impl", "layout_pass.impl.jac"
),
_cfp_os.path.join(_jl_dir, "compiler", "types", "*.jac"),
_cfp_os.path.join(_jl_dir, "compiler", "types", "*", "*.jac"),
_cfp_os.path.join(_jl_dir, "compiler", "type_registry.jac"),
Expand All @@ -47,14 +53,11 @@ impl native_compiler_fingerprint(options: any) -> str {
}

impl native_dep_cache_paths(real_path: str, options: any) -> tuple {
import hashlib as _dcp_hash;
import os as _dcp_os;
import from jaclang.compiler.driver.jir { get_native_cache_dir }
import from jaclang.compiler.driver.jir { ct_dep_digest, get_native_cache_dir }
_ir_cache_dir = str(get_native_cache_dir(real_path));
_src_stem = _dcp_os.path.splitext(_dcp_os.path.basename(real_path))[0];
with open(real_path, "rb") as _sf {
_source_hash = _dcp_hash.sha256(_sf.read()).hexdigest()[:12];
}
_source_hash = ct_dep_digest(real_path, with_annexes=True)[:12];
_fp = native_compiler_fingerprint(options);
return (
_dcp_os.path.join(_ir_cache_dir, f"{_src_stem}.{_source_hash}.{_fp}.ir_cache"),
Expand Down Expand Up @@ -647,16 +650,22 @@ impl NativeCompilePass._compile_and_link_native_imports(
}
}

_INTERNAL_LINKAGE = 8;
_PRIVATE_LINKAGE = 9;
_WEAK_LINKAGE = 5;
_WEAK_ODR_LINKAGE = 6;
import from jaclang.compiler.backends.native.llvm.binding.value { Linkage }
non_unique_linkages = {
Linkage.internal,
Linkage.private,
Linkage.weak_any,
Linkage.weak_odr,
Linkage.linkonce_any,
Linkage.linkonce_odr,
Linkage.linkonce_odr_autohide
};
for _mfn in _mod_functions(main_mod) {
_mfn_name = str(_mfn.name);
if _mfn.is_declaration or _mfn_name.startswith("llvm.") {
continue;
}
if _mfn.linkage == _INTERNAL_LINKAGE or _mfn.linkage == _PRIVATE_LINKAGE {
if _mfn.linkage in non_unique_linkages {
continue;
}
symbol_origins[_mfn_name] = _MAIN_ORIGIN;
Expand All @@ -667,7 +676,7 @@ impl NativeCompilePass._compile_and_link_native_imports(
if _mgv.is_declaration {
continue;
}
if _mgv.linkage == _INTERNAL_LINKAGE or _mgv.linkage == _PRIVATE_LINKAGE {
if _mgv.linkage in non_unique_linkages {
continue;
}
symbol_origins[_mgv_name] = _MAIN_ORIGIN;
Expand Down Expand Up @@ -728,6 +737,19 @@ impl NativeCompilePass._compile_and_link_native_imports(
}
linked_modules[real_path] = None;

import from jaclang.compiler.backends.common.kernel_units { kernel_unit_owner }
_kernel_owner = kernel_unit_owner(
real_path, source_mod_path.replace(os.sep, ".")
);
if _kernel_owner {
manifest = module.gen.interop_manifest;
if _kernel_owner not in manifest.native_transitive_sources {
manifest.native_transitive_sources.append(_kernel_owner);
manifest.native_transitive_keys.append("");
}
continue;
}

import from jaclang.compiler.driver.jir { get_native_cache_dir }
_src_stem = os.path.splitext(os.path.basename(real_path))[0];
_ir_cache_dir = str(get_native_cache_dir(real_path));
Expand Down Expand Up @@ -1067,10 +1089,7 @@ impl NativeCompilePass._compile_and_link_native_imports(
if _func_name.startswith("llvm.") or func.is_declaration {
continue;
}
if func.linkage == _INTERNAL_LINKAGE
or func.linkage == _PRIVATE_LINKAGE
or func.linkage == _WEAK_LINKAGE
or func.linkage == _WEAK_ODR_LINKAGE {
if func.linkage in non_unique_linkages {
continue;
}
exported_funcs.append(_func_name);
Expand All @@ -1096,11 +1115,7 @@ impl NativeCompilePass._compile_and_link_native_imports(
exported_globals: list[str] = [];
for gv in _mod_globals(imported_mod) {
_gv_name = str(gv.name);
if gv.is_declaration
or gv.linkage == _INTERNAL_LINKAGE
or gv.linkage == _PRIVATE_LINKAGE
or gv.linkage == _WEAK_LINKAGE
or gv.linkage == _WEAK_ODR_LINKAGE {
if gv.is_declaration or gv.linkage in non_unique_linkages {
continue;
}
exported_globals.append(_gv_name);
Expand Down
14 changes: 13 additions & 1 deletion jac/jaclang/compiler/backends/native/llvm/binding/targets.jac
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os;
import type from jaclang.compiler.backends.native.llvm.binding.value { ValueRef }
import from ctypes {
POINTER,
c_char_p,
Expand Down Expand Up @@ -95,7 +96,7 @@ def get_object_format(triple: any = None) -> object {
"""
Create a TargetData instance for the given *layout* string.
"""
def create_target_data(layout: any) -> object {
def create_target_data(layout: str) -> TargetData {
return TargetData(ffi.lib.LLVMPY_CreateTargetData(_encode_string(layout)));
}

Expand All @@ -104,6 +105,17 @@ def create_target_data(layout: any) -> object {
Use :func:`create_target_data` to create instances.
"""
class TargetData(ffi.ObjectRef) {
def get_global_struct_layout(
self: TargetData, value: ValueRef, field_count: int
) -> tuple[list[int], int] {
"""Query a global's struct type while its owning module is alive.""";
typ = ffi.lib.LLVMPY_GlobalGetValueType(value);
return (
[int(self.get_element_offset(typ, i)) for i in range(field_count)],
int(self.get_abi_size(typ))
);
}

def __str__(self: TargetData) -> object {
if self._closed {
return "<dead TargetData>";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,7 @@ impl NaIRGenPass._register_imported_struct_types -> None {
os.path.realpath(self.ir_in.loc.mod_path) if (self.ir_in?.loc) else None
);
import from jaclang.compiler.frontend.codeinfo { native_module_key }
import from jaclang.compiler.backends.common.kernel_units { kernel_unit_owner }

_canon_src: dict = {};
_src_keys: dict[str, str] = {};
Expand All @@ -633,7 +634,11 @@ impl NaIRGenPass._register_imported_struct_types -> None {
continue;
}

imported_mod = self.prog.mod.hub.get(full_path);
_kernel_owner = kernel_unit_owner(full_path, mod_name);
imported_mod = _load_native_osp_kernel_module() if _kernel_owner else None;
if imported_mod is None {
imported_mod = self.prog.mod.hub.get(full_path);
}
if imported_mod is None {
imported_mod = self.prog.mod.hub.get(abs_key);
}
Expand All @@ -655,9 +660,11 @@ impl NaIRGenPass._register_imported_struct_types -> None {
}
self._ensure_dep_inference(imported_mod);
processed_modules.add(abs_key);
_canon_src[abs_key] = src_mod_str;
imported_mods.append((src_mod_str, imported_mod));
_src_keys[src_mod_str] = native_module_key(full_path);
_canon_src[abs_key] = _kernel_owner or src_mod_str;
imported_mods.append((_kernel_owner or src_mod_str, imported_mod));
_src_keys[_canon_src[abs_key]] = ""
if _kernel_owner
else native_module_key(full_path);

sub_base = os.path.dirname(os.path.abspath(full_path));
for sub_imp in ModuleFacts.iter_module_imports(imported_mod)
Expand Down Expand Up @@ -734,8 +741,10 @@ impl NaIRGenPass._register_imported_struct_types -> None {
self.prog.scratch.na_dep_inflight.add(_self_real);
}
try {
for (_, imported_mod) in imported_mods {
self._walk_imported_module_abilities(imported_mod);
for (source, imported_mod) in imported_mods {
self._walk_imported_module_abilities(
imported_mod, with_fn_verdicts=source != "osp_kernel"
);
}
} finally {
if _self_real is not None {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -349,15 +349,6 @@ impl NaIRGenPass._codegen_enum_ctor(
);
return None;
}
if lay.is_string_enum {
self._emit_enum_ctor_refusal(
nd,
enum_name,
"Its members carry string values, whose runtime form is the "
"ordinal rather than the value, so the lookup is not the identity."
);
return None;
}
if not lay.all_values_known {
self._emit_enum_ctor_refusal(
nd,
Expand All @@ -372,6 +363,44 @@ impl NaIRGenPass._codegen_enum_ctor(
self._emit_enum_ctor_refusal(nd, enum_name, "Its argument did not lower.");
return None;
}
if lay.is_string_enum {
arg_key = self._type_key_of(_params[0]);
if arg_key == enum_name {
return _arg;
}
if arg_key != "str" {
self._emit_enum_ctor_refusal(
nd, enum_name, "Its argument must be a string or a member of this enum."
);
return None;
}
value = self._str_norm(_arg);
if value is None {
return None;
}
i64 = ir.IntType(64);
ordinal: ir.Value = ir.Constant(i64, -1);

for member in reversed(lay.members) {
text = self._make_global_string(
member.str_value or "", name_prefix=".enum.value"
);
cmp = self._call_str_cmp(self.builder, value, text, name="enum.value.cmp");
hit = self.builder.icmp_signed("==", cmp, ir.Constant(ir.IntType(32), 0));
ordinal = self.builder.select(
hit, ir.Constant(i64, member.ordinal), ordinal
);
}
if self._is_owned(value) {
self._emit_rc_release_simple(value, op="enum_ctor", loc="");
}
self._emit_runtime_raise(
self.builder.icmp_signed("==", ordinal, ir.Constant(i64, -1)),
"ValueError",
f"is not a valid {enum_name}"
);
return ordinal;
}
if not isinstance(_arg.type, ir.IntType) {
self._emit_enum_ctor_refusal(
nd,
Expand Down
Loading
Loading