From a9c773f39315350549114f13c491613e19846b3c Mon Sep 17 00:00:00 2001 From: marsninja Date: Sun, 6 Sep 2026 10:18:34 -0400 Subject: [PATCH 1/6] compiler: move AST validation and symbol construction into native --- jac/jaclang/compiler/README.md | 42 +++- .../compiler/backends/common/kernel_units.jac | 11 ++ jac/jaclang/compiler/backends/native/abi.jac | 22 +++ .../native/impl/na_compile_pass.impl.jac | 55 ++++-- .../backends/native/llvm/binding/targets.jac | 12 ++ .../native/na_ir_gen_pass.impl/core.impl.jac | 21 +- .../native/na_ir_gen_pass.impl/enums.impl.jac | 47 ++++- .../native/na_ir_gen_pass.impl/expr.impl.jac | 42 +++- .../na_ir_gen_pass.impl/objects.impl.jac | 182 +++++++++++++++--- .../native/na_ir_gen_pass.impl/osp.impl.jac | 16 +- .../native/na_ir_gen_pass.impl/stmt.impl.jac | 10 + .../na_ir_gen_pass.impl/vtable.impl.jac | 6 +- .../backends/native/na_ir_gen_pass.jac | 8 +- .../compiler/driver/boundary_classify.jac | 14 +- .../compiler/driver/impl/compiler.impl.jac | 13 +- .../compiler/driver/impl/program.impl.jac | 1 + jac/jaclang/compiler/driver/pass_driver.jac | 26 +++ jac/jaclang/compiler/driver/progstate.jac | 29 ++- .../compiler/frontend/parser/frontend.jac | 21 +- .../frontend/unitree.impl/nodes.impl.jac | 34 +++- jac/jaclang/compiler/frontend/unitree.jac | 3 + jac/jaclang/compiler/jc_materialize.jac | 98 +++++++++- jac/jaclang/compiler/jc_unit.jac | 117 ++++++++++- jac/jaclang/compiler/native_compiler.jac | 100 +++++++++- jac/jaclang/compiler/native_scope.jac | 9 + .../compiler/passes/ast_validation_pass.jac | 4 +- .../passes/impl/ast_validation_pass.impl.jac | 10 +- .../compiler/passes/impl/layout_pass.impl.jac | 10 +- .../passes/impl/sym_tab_build_pass.impl.jac | 8 +- .../compiler/passes/impl/transform.impl.jac | 36 ++-- jac/jaclang/compiler/passes/layout_pass.jac | 11 +- .../compiler/passes/sym_tab_build_pass.jac | 5 +- jac/jaclang/compiler/passes/transform.jac | 27 ++- jac/jaclang/runtime/osp_tag.jac | 9 +- .../native/fixtures/boxed_return_lifetime.jac | 30 +++ .../native/fixtures/enum_ctor_string.jac | 21 ++ .../native/fixtures/osp_runtime_import.jac | 27 +++ .../native/fixtures/type_identity.jac | 18 ++ .../native/fixtures/type_identity_library.jac | 9 + .../native/fixtures/type_identity_types.jac | 2 + .../native/fixtures/union_properties.jac | 49 +++++ .../native/fixtures/walker_base_dispatch.jac | 28 +++ .../native/fixtures/walker_base_library.jac | 22 +++ .../native/fixtures/xmod_demote_dyn_lib.jac | 12 +- .../native/test_native_abi_layout.jac | 15 ++ .../native/test_native_annex_cache.jac | 21 ++ .../test_native_boxed_return_lifetime.jac | 12 ++ .../backends/native/test_native_enum_ctor.jac | 23 ++- .../native/test_native_osp_runtime_import.jac | 12 ++ .../native/test_native_type_identity.jac | 10 + .../native/test_native_union_properties.jac | 14 ++ .../test_native_walker_base_dispatch.jac | 14 ++ .../compiler/test_layout_import_order.jac | 35 ++++ .../compiler/test_native_early_passes.jac | 122 ++++++++++++ jac/tests/runtimelib/test_osp_tag.jac | 20 ++ scripts/native_compile_bench.py | 62 ++++++ 56 files changed, 1467 insertions(+), 170 deletions(-) create mode 100644 jac/tests/compiler/backends/native/fixtures/boxed_return_lifetime.jac create mode 100644 jac/tests/compiler/backends/native/fixtures/enum_ctor_string.jac create mode 100644 jac/tests/compiler/backends/native/fixtures/osp_runtime_import.jac create mode 100644 jac/tests/compiler/backends/native/fixtures/type_identity.jac create mode 100644 jac/tests/compiler/backends/native/fixtures/type_identity_library.jac create mode 100644 jac/tests/compiler/backends/native/fixtures/type_identity_types.jac create mode 100644 jac/tests/compiler/backends/native/fixtures/union_properties.jac create mode 100644 jac/tests/compiler/backends/native/fixtures/walker_base_dispatch.jac create mode 100644 jac/tests/compiler/backends/native/fixtures/walker_base_library.jac create mode 100644 jac/tests/compiler/backends/native/test_native_abi_layout.jac create mode 100644 jac/tests/compiler/backends/native/test_native_annex_cache.jac create mode 100644 jac/tests/compiler/backends/native/test_native_boxed_return_lifetime.jac create mode 100644 jac/tests/compiler/backends/native/test_native_osp_runtime_import.jac create mode 100644 jac/tests/compiler/backends/native/test_native_type_identity.jac create mode 100644 jac/tests/compiler/backends/native/test_native_union_properties.jac create mode 100644 jac/tests/compiler/backends/native/test_native_walker_base_dispatch.jac create mode 100644 jac/tests/compiler/test_layout_import_order.jac create mode 100644 jac/tests/compiler/test_native_early_passes.jac create mode 100644 jac/tests/runtimelib/test_osp_tag.jac create mode 100644 scripts/native_compile_bench.py diff --git a/jac/jaclang/compiler/README.md b/jac/jaclang/compiler/README.md index 2ddf7829b03..f2663802783 100644 --- a/jac/jaclang/compiler/README.md +++ b/jac/jaclang/compiler/README.md @@ -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.py` 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 diff --git a/jac/jaclang/compiler/backends/common/kernel_units.jac b/jac/jaclang/compiler/backends/common/kernel_units.jac index 8989bf662f2..07371e73281 100644 --- a/jac/jaclang/compiler/backends/common/kernel_units.jac +++ b/jac/jaclang/compiler/backends/common/kernel_units.jac @@ -26,6 +26,17 @@ def _jaclang_dir -> str { return os.path.dirname(str(jaclang.__file__)); } +def kernel_unit_owner(path: str) -> str { + """Canonical native unit supplying a directly imported runtime source."""; + resolved = os.path.realpath(path); + for part in OSP_CORE_PARTS { + if resolved == os.path.realpath(os.path.join(_jaclang_dir(), part)) { + 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"); diff --git a/jac/jaclang/compiler/backends/native/abi.jac b/jac/jaclang/compiler/backends/native/abi.jac index 1314a7f4226..249e8309654 100644 --- a/jac/jaclang/compiler/backends/native/abi.jac +++ b/jac/jaclang/compiler/backends/native/abi.jac @@ -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)]); diff --git a/jac/jaclang/compiler/backends/native/impl/na_compile_pass.impl.jac b/jac/jaclang/compiler/backends/native/impl/na_compile_pass.impl.jac index 6f36029fc98..43b388c2f84 100644 --- a/jac/jaclang/compiler/backends/native/impl/na_compile_pass.impl.jac +++ b/jac/jaclang/compiler/backends/native/impl/na_compile_pass.impl.jac @@ -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"), @@ -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"), @@ -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; @@ -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; @@ -728,6 +737,17 @@ 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); + 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)); @@ -1067,10 +1087,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); @@ -1096,11 +1113,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); diff --git a/jac/jaclang/compiler/backends/native/llvm/binding/targets.jac b/jac/jaclang/compiler/backends/native/llvm/binding/targets.jac index 608556d63ab..253d2f6e84a 100644 --- a/jac/jaclang/compiler/backends/native/llvm/binding/targets.jac +++ b/jac/jaclang/compiler/backends/native/llvm/binding/targets.jac @@ -1,4 +1,5 @@ import os; +import type from jaclang.compiler.backends.native.llvm.binding.value { ValueRef } import from ctypes { POINTER, c_char_p, @@ -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 ""; diff --git a/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/core.impl.jac b/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/core.impl.jac index 720092df936..ffca323de58 100644 --- a/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/core.impl.jac +++ b/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/core.impl.jac @@ -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] = {}; @@ -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); + 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); } @@ -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) @@ -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 { diff --git a/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/enums.impl.jac b/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/enums.impl.jac index 6b7832417ae..31d141bf164 100644 --- a/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/enums.impl.jac +++ b/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/enums.impl.jac @@ -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, @@ -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); + # Reverse selection gives aliases the first declared member's ordinal. + 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, diff --git a/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/expr.impl.jac b/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/expr.impl.jac index 6db4d1c4de7..617e1e7db0d 100644 --- a/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/expr.impl.jac +++ b/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/expr.impl.jac @@ -127,7 +127,8 @@ impl NaIRGenPass._codegen_expr(nd: (uni.UniNode | None)) -> (ir.Value | None) { } } - if isinstance(_result.type, ir.PointerType) { + if isinstance(_result.type, ir.PointerType) + or (self?.jacval_type and _result.type == self.jacval_type) { import from jaclang.compiler.passes.rc_facts_pass { Ownership, result_ownership @@ -139,6 +140,9 @@ impl NaIRGenPass._codegen_expr(nd: (uni.UniNode | None)) -> (ir.Value | None) { _narrowed = self._apply_flow_narrowing(nd, _result); if _narrowed is not _result { + if self._is_owned(_result) { + self._mark_owned(_narrowed); + } _narrowed.jac_loc = _jac_loc; _result = _narrowed; } @@ -757,17 +761,25 @@ impl NaIRGenPass._codegen_kernel_spawn( _h["append"], [_starts, self._coerce_type(loc_val, i64)], op="append", loc="" ); - _ks_model = self.ir_in.gen.osp_model; - _wtag = _ks_model.tags.get(walker_name, 0) if (_ks_model is not None) else 0; + # A base-class method can spawn a derived walker. Dispatch through the + # instance tag, just as node callbacks do, rather than its static type. + _tag_slot = self.osp_type_tag_field.get(walker_name); + if _tag_slot is None { + return None; + } + _wtag = self.builder.load( + self.builder.gep( + walker_val, [ir.Constant(i32, 0), ir.Constant(i32, _tag_slot)] + ), + name="spawn.walker_tag" + ); _nd_fn = self.func_symtab.get("osp_node_desc"); if _nd_fn is None { return walker_val; } _nd_args = _nd_fn.function_type.args; _descp = self.builder.call( - _nd_fn, - [self._coerce_type(ir.Constant(i64, _wtag), _nd_args[0])], - name="spawn.descp" + _nd_fn, [self._coerce_type(_wtag, _nd_args[0])], name="spawn.descp" ); _dead_fn = self.func_symtab.get("osp_is_dead"); if _dead_fn is not None { @@ -2088,6 +2100,24 @@ impl NaIRGenPass._codegen_atom_trailer(nd: uni.AtomTrailer) -> (ir.Value | None) } } + _property_parts = self._receiver_union_parts(nd); + if _property_parts is not None + and any( + self._accessor_slot_of(part, right_name, "property") is not None + for part in _property_parts + ) + and all( + self._accessor_slot_of(part, right_name, "property") is not None + or right_name in self.struct_field_indices.get(part, {}) + for part in _property_parts + ) { + _property_receiver = self._codegen_expr(nd.target); + if _property_receiver is not None { + return self._codegen_union_member( + _property_receiver, right_name, nd, _property_parts + ); + } + } _fa_type_name = self._struct_name_of(nd.target); if _fa_type_name is not None { type_name = _fa_type_name; diff --git a/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/objects.impl.jac b/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/objects.impl.jac index ca2bcc267f6..27fabfd3594 100644 --- a/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/objects.impl.jac +++ b/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/objects.impl.jac @@ -1987,6 +1987,11 @@ impl NaIRGenPass._codegen_instantiation( _tt_idx = self.osp_type_tag_field.get(type_name); if _tt_idx is not None { + # Constructors may enter inherited walker methods in another module. + # Register this module's concrete descriptors before that can happen. + if self._osp_ensure_fn is not None { + self.builder.call(self._osp_ensure_fn, []); + } _tt_slot = self.builder.gep( obj_ptr, [ir.Constant(ir.IntType(32), 0), ir.Constant(ir.IntType(32), _tt_idx)], @@ -3283,7 +3288,8 @@ impl NaIRGenPass._codegen_accessor_call( impl NaIRGenPass._own_accessor_result(result: ir.Value) -> ir.Value { if isinstance(result.type, ir.PointerType) - or self._opt_ptr_payload_of(result.type) is not None { + or self._opt_ptr_payload_of(result.type) is not None + or (self?.jacval_type and result.type == self.jacval_type) { return self._mark_owned(result); } return result; @@ -3305,6 +3311,78 @@ impl NaIRGenPass._receiver_union_parts(nd: (uni.UniNode | None)) -> (list[str] | return _rp if len(_rp) >= 2 else None; } +impl NaIRGenPass._codegen_union_member( + obj_val: ir.Value, field_name: str, nd: uni.AtomTrailer, parts: list[str] +) -> (ir.Value | None) { + """Read each union member through its own field layout or accessor."""; + result_type = self._lower_type(nd.type); + if result_type is None { + return None; + } + arms: list[str] = []; + for member in parts { + slot = self._accessor_slot_of(member, field_name, "property"); + if slot is not None { + if self._resolve_method(member, slot) is None { + return None; + } + } elif field_name not in self.struct_field_indices.get(member, {}) { + return None; + } + arms.append(member); + } + receiver = self._narrow_receiver_ptr(obj_val, parts[0]); + if receiver is None { + return None; + } + obj_val = receiver; + if self._emit_runtime_class_name_fn() is None { + return None; + } + b = self.builder; + self._emit_runtime_raise( + b.icmp_unsigned("==", obj_val, ir.Constant(obj_val.type, None)), + "AttributeError", + "'NoneType' object has no attribute" + ); + done = b.append_basic_block(name="union.property.done"); + incoming: list[tuple[ir.Value, ir.Block]] = []; + for member in arms { + hit = b.append_basic_block(name="union.property.hit"); + next_arm = b.append_basic_block(name="union.property.next"); + matches = self._isinstance_class_name_hit(obj_val, [member]); + if matches is None { + return None; + } + b.cbranch(matches, hit, next_arm); + b.position_at_end(hit); + value = self._codegen_field_access(obj_val, field_name, member); + if value is None { + return None; + } + if not self._is_owned(value) { + self._emit_rc_retain(value, op="union_member", loc=""); + } + value = self._coerce_type(value, result_type); + incoming.append((value, b.block)); + b.branch(done); + b.position_at_end(next_arm); + } + self._emit_runtime_raise( + ir.Constant(ir.IntType(1), 1), + "AttributeError", + f"object has no attribute '{field_name}'" + ); + incoming.append((self._default_arg_fallback(result_type), b.block)); + b.branch(done); + b.position_at_end(done); + result = b.phi(result_type, name="union.property.value"); + for (value, block) in incoming { + result.add_incoming(value, block); + } + return self._own_accessor_result(result); +} + impl NaIRGenPass._emit_dispatched_field_ptr( obj_val: ir.Value, type_name: str, @@ -4144,41 +4222,68 @@ impl NaIRGenPass._emit_class_routed_call( } -impl NaIRGenPass._jc_abi_offsets(field_types: list) -> tuple { - offsets: list[int] = []; - off = 0; - for ft in field_types { - sz = 8; - if isinstance(ft, ir.IntType) { - sz = 1 if ft.width <= 8 else (ft.width // 8); - } elif isinstance(ft, ir.DoubleType) { - sz = 8; - } elif isinstance(ft, ir.PointerType) { - sz = 8; - } elif ft?.elements and ft.elements is not None { - (_sub, _ssz) = self._jc_abi_offsets(list(ft.elements)); - _align = 8; - if off % _align != 0 { - off += _align - (off % _align); - } - offsets.append(off); - off += _ssz; - continue; - } - if off % sz != 0 { - off += sz - (off % sz); +impl NaIRGenPass._jc_reference_schema( + ft: ir.Type, layouts: dict, semantic: object = None +) -> (dict | None) { + """Combine semantic reference types with the backend's container ABI."""; + args = semantic.private.type_args or [] if isinstance(semantic, ClassType) else []; + semantic_name = semantic.shared.class_name + if isinstance(semantic, ClassType) + else ""; + if not isinstance(ft, ir.PointerType) { + return None; + } + if isinstance(ft.pointee, ir.IntType) and ft.pointee.width == 8 { + if semantic_name in self.struct_field_indices { + return {"code": "obj", "class": semantic_name}; } - offsets.append(off); - off += sz; + return {"code": "str"}; + } + name = getattr(ft.pointee, "name", "") or ""; + if name in self.struct_field_indices { + return {"code": "obj", "class": name}; } - if off % 8 != 0 { - off += 8 - (off % 8); + layout = self.container_layouts.get(name); + if layout is None { + return None; } - return (offsets, off); + data_fields = layout["data_fields"]; + (offsets, _) = layouts[name]; + if layout["kind"] == "dense" and len(data_fields) == 1 { + child = self._jc_reference_schema( + data_fields[0][1], layouts, args[0] if args else None + ); + if child is not None { + return { + "code": "list", + "element": child, + "data_offset": offsets[data_fields[0][0]] + }; + } + } elif layout["kind"] == "hash" and len(data_fields) == 2 { + key = self._jc_reference_schema( + data_fields[0][1], layouts, args[0] if args else None + ); + child = self._jc_reference_schema( + data_fields[1][1], layouts, args[1] if len(args) > 1 else None + ); + if key is not None and key["code"] == "str" and child is not None { + return { + "code": "dict", + "element": child, + "key_offset": offsets[data_fields[0][0]], + "data_offset": offsets[data_fields[1][0]], + "order_offset": offsets[layout["order_idx"]] + }; + } + } + return None; } impl NaIRGenPass._build_jc_layout -> str { import json; + import from jaclang.compiler.backends.native.abi { struct_layouts } + layouts = struct_layouts(self.llvm_module); out: dict = {"classes": {}}; _model = self.ir_in.gen.osp_model; _tags = _model.tags if _model is not None else {}; @@ -4187,10 +4292,18 @@ impl NaIRGenPass._build_jc_layout -> str { if st is None or st?.elements is None { continue; } - (offsets, total) = self._jc_abi_offsets(list(st.elements)); + (offsets, total) = layouts[arch_name]; idx_map = self.struct_field_indices.get(arch_name, {}); ftypes = self.struct_field_types.get(arch_name, {}); enum_map = self.struct_field_enum_type.get(arch_name, {}); + arch_layout = self._arch_layout(arch_name); + semantic_fields = { + f.name: f.decl_node.type + for f in arch_layout.fields + if f.decl_node is not None + } + if arch_layout is not None + else {}; fields: dict = {}; for (fname, fidx) in idx_map.items() { if fidx >= len(offsets) { @@ -4254,6 +4367,15 @@ impl NaIRGenPass._build_jc_layout -> str { "code": code, "aux": aux }; + if code == "ptr" and isinstance(ft, ir.PointerType) { + schema = self._jc_reference_schema( + ft, layouts, semantic_fields.get(fname) + ); + if schema is not None and schema["code"] == "dict" { + fields[fname]["code"] = "natdict"; + fields[fname]["schema"] = schema; + } + } } _lay_tag_idx = self.osp_type_tag_field.get(arch_name); out["classes"][arch_name] = { diff --git a/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/osp.impl.jac b/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/osp.impl.jac index 3f9a9ecb5b2..5095fd439a0 100644 --- a/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/osp.impl.jac +++ b/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/osp.impl.jac @@ -209,9 +209,19 @@ impl NaIRGenPass._osp_collect_slots(arch_name: str, is_entry: bool) -> list { if _model is None { return slots; } - _src = _model.entry_slots(arch_name) if is_entry else _model.exit_slots(arch_name); - for _slot in _src { - thunk = self.osp_thunk_funcs.get(f"{arch_name}.{_slot.ability}"); + # Match host event dispatch: merge base events in reverse MRO order, + # replacing overridden abilities without changing their dispatch position. + merged: dict = {}; + layout = self._arch_layout(arch_name); + order = layout.mro if layout is not None else [arch_name]; + for owner in reversed(order) { + source = _model.entry_slots(owner) if is_entry else _model.exit_slots(owner); + for slot in source { + merged[slot.ability] = slot; + } + } + for _slot in merged.values() { + thunk = self.osp_thunk_funcs.get(f"{_slot.owner}.{_slot.ability}"); if thunk is None { continue; } diff --git a/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/stmt.impl.jac b/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/stmt.impl.jac index 14114f4ff47..46f4811c6ff 100644 --- a/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/stmt.impl.jac +++ b/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/stmt.impl.jac @@ -437,6 +437,16 @@ impl NaIRGenPass._codegen_return(nd: uni.ReturnStmt) -> None { ); self._mark_owned(val); } + if skip_var is None + and self?.jacval_type + and val.type == self.jacval_type + and not self._is_owned(val) { + # A boxed return still owns its reference payload. In + # particular, callers may release a temporary argument as + # soon as this function returns it through a union or any. + self._emit_elem_retain(self.builder, val.type, val); + self._mark_owned(val); + } if ( skip_var is None and self._opt_ptr_payload_of(val.type) is not None diff --git a/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/vtable.impl.jac b/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/vtable.impl.jac index 03e9f1b18c6..bff70d3f3d0 100644 --- a/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/vtable.impl.jac +++ b/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/vtable.impl.jac @@ -394,10 +394,10 @@ impl NaIRGenPass._emit_class_name_const(arch_name: str) -> ir.Value { (arch_name + "\0").encode("utf8") ); global_str = ir.GlobalVariable( - self.llvm_module, struct_type, name=f".clsname.{self._str_count}" + self.llvm_module, struct_type, name=f".clsname.{arch_name}" ); - self._str_count = self._str_count + 1; - global_str.linkage = "private"; + # Class references and vtables must carry the same identity across units. + global_str.linkage = "linkonce_odr"; global_str.global_constant = True; global_str.initializer = struct_val; diff --git a/jac/jaclang/compiler/backends/native/na_ir_gen_pass.jac b/jac/jaclang/compiler/backends/native/na_ir_gen_pass.jac index 199e450d2a3..fbc25137578 100644 --- a/jac/jaclang/compiler/backends/native/na_ir_gen_pass.jac +++ b/jac/jaclang/compiler/backends/native/na_ir_gen_pass.jac @@ -1294,7 +1294,6 @@ walker NaIRGenPass(Transform) { nd: uni.Archetype, arch_name: str, block: uni.ModuleCode ) -> None; - def _jc_abi_offsets(field_types: list) -> tuple; def _build_jc_layout -> str; def _register_one_class_const( arch_name: str, @@ -1339,6 +1338,13 @@ walker NaIRGenPass(Transform) { ) -> (ir.Value | None); def _receiver_union_parts(nd: (uni.UniNode | None)) -> (list[str] | None); + def _jc_reference_schema( + ft: ir.Type, layouts: dict, semantic: object = None + ) -> (dict | None); + def _codegen_union_member( + obj_val: ir.Value, field_name: str, nd: uni.AtomTrailer, parts: list[str] + ) -> (ir.Value | None); + def _narrow_receiver_ptr(obj_val: ir.Value, type_name: str) -> (ir.Value | None); def _class_const_global( arch_name: str, field_name: str diff --git a/jac/jaclang/compiler/driver/boundary_classify.jac b/jac/jaclang/compiler/driver/boundary_classify.jac index 93b4468b600..54f0bfcd63a 100644 --- a/jac/jaclang/compiler/driver/boundary_classify.jac +++ b/jac/jaclang/compiler/driver/boundary_classify.jac @@ -9,19 +9,7 @@ import from jaclang.compiler.placement.workspace { } def _coerce_module(module: uni.Module, context: CodeContext) { - module.decided_codespace = context.value; - for elem in module.body { - if isinstance(elem, uni.ContextAwareNode) { - elem.code_context = context; - if (isinstance(elem, uni.ModuleCode) and elem.body) { - for inner in elem.body { - if isinstance(inner, uni.ContextAwareNode) { - inner.code_context = context; - } - } - } - } - } + module.set_code_context(context); } def _coerce_client_module(module: uni.Module) { diff --git a/jac/jaclang/compiler/driver/impl/compiler.impl.jac b/jac/jaclang/compiler/driver/impl/compiler.impl.jac index 49e10f25b17..fcbda91ebb9 100644 --- a/jac/jaclang/compiler/driver/impl/compiler.impl.jac +++ b/jac/jaclang/compiler/driver/impl/compiler.impl.jac @@ -721,7 +721,18 @@ impl JacCompiler.parse_str( } } (mod, had_error) = parse_woven( - source_str, file_path, target_program, annex_sources(file_path) + source_str, + file_path, + target_program, + annex_sources(file_path), + early_context=( + codespace + or ( + 'server' + if _default_codespace_for(target_program) != 'native' + else '' + ) + ) ); if (codespace == 'client') { _coerce_client_module(mod); diff --git a/jac/jaclang/compiler/driver/impl/program.impl.jac b/jac/jaclang/compiler/driver/impl/program.impl.jac index f98e1d58f0d..32326b48117 100644 --- a/jac/jaclang/compiler/driver/impl/program.impl.jac +++ b/jac/jaclang/compiler/driver/impl/program.impl.jac @@ -348,6 +348,7 @@ impl JacProgram.release_compile_closure( self.scratch.clear(); self.analysis.analyses_run.clear(); self.analysis.placement_state.pop('work', None); + self.analysis.early_passes.clear(); if self._iface is not None { self._iface.on_closure_release(); } diff --git a/jac/jaclang/compiler/driver/pass_driver.jac b/jac/jaclang/compiler/driver/pass_driver.jac index e9013e2dbc2..d8883b2da2e 100644 --- a/jac/jaclang/compiler/driver/pass_driver.jac +++ b/jac/jaclang/compiler/driver/pass_driver.jac @@ -136,6 +136,32 @@ def run_pass( if getattr(pass_cls, 'HOST_PASS', False) { kwargs['prog'] = prog; } + completed = prog.analysis.take_early(ir_in, pass_cls.__name__); + if completed is not None { + if completed.errors_had or completed.warnings_had { + sources = {ir_in.source.file_path: ir_in.source}; + for nd in ir_in._in_mod_nodes { + sources[nd.loc.mod_path] = nd.loc.orig_src; + } + completed.errors_had = [ + alert + for alert in completed.errors_had + if alert.code is None + or not pol.is_suppressed( + alert.code, alert.loc, source=sources.get(alert.loc.mod_path) + ) + ]; + completed.warnings_had = [ + alert + for alert in completed.warnings_had + if alert.code is None + or not pol.is_suppressed( + alert.code, alert.loc, source=sources.get(alert.loc.mod_path) + ) + ]; + } + kwargs['completed'] = completed; + } try { inst = pass_cls( ir_in=ir_in, diff --git a/jac/jaclang/compiler/driver/progstate.jac b/jac/jaclang/compiler/driver/progstate.jac index b012200a508..9fac5d889ef 100644 --- a/jac/jaclang/compiler/driver/progstate.jac +++ b/jac/jaclang/compiler/driver/progstate.jac @@ -2,7 +2,7 @@ import os; import sys; import jaclang.compiler.frontend.unitree as uni; import from jaclang.compiler.frontend.relations { ScopeChild } -import from jaclang.compiler.passes.transform { Alert } +import from jaclang.compiler.passes.transform { Alert, PassResult } import type from jaclang.compiler.driver.mtp { Info } import type from jaclang.compiler.frontend.codeinfo { ClientArtifact } @@ -307,6 +307,11 @@ class AnalysisLedger { } } +obj PendingPassResults { + has module: uni.Module, + results: dict[str, PassResult]; +} + class AnalysisCaches { has placement_summaries: dict[str, any], placement_state: dict[str, any], @@ -314,6 +319,7 @@ class AnalysisCaches { mtir_map: dict[(str, Info)], pass_runs: dict[str, int], pass_time: dict[str, float], + early_passes: dict[int, PendingPassResults], cache_events: dict[str, int]; def init(self: AnalysisCaches) { @@ -323,8 +329,29 @@ class AnalysisCaches { self.mtir_map: dict[(str, Info)] = {}; self.pass_runs: dict[str, int] = {}; self.pass_time: dict[str, float] = {}; + self.early_passes: dict[int, PendingPassResults] = {}; self.cache_events: dict[str, int] = {}; } + + def record_early( + self: AnalysisCaches, module: uni.Module, results: dict[str, PassResult] + ) { + self.early_passes[id(module)] = PendingPassResults(module, results); + } + + def take_early( + self: AnalysisCaches, module: uni.Module, name: str + ) -> (PassResult | None) { + pending = self.early_passes.get(id(module)); + if pending is None { + return None; + } + result = pending.results.pop(name, None); + if not pending.results { + self.early_passes.pop(id(module)); + } + return result; + } } glob UNIT_COLLECT_EVERY: int = 16, diff --git a/jac/jaclang/compiler/frontend/parser/frontend.jac b/jac/jaclang/compiler/frontend/parser/frontend.jac index 0318cd0c78e..190a7788c51 100644 --- a/jac/jaclang/compiler/frontend/parser/frontend.jac +++ b/jac/jaclang/compiler/frontend/parser/frontend.jac @@ -109,13 +109,18 @@ def _finish_parse( } def _native_parse_woven( - source_str: str, file_path: str, prog: (JacProgram | None), annexes: list + source_str: str, + file_path: str, + prog: (JacProgram | None), + annexes: list, + early_context: str = "" ) -> (tuple | None) { import jaclang.compiler.native_compiler as _jc; kernel = _jc.ensure_loaded(); if kernel is None { return None; } + kernel.prepare_early(early_context if prog is not None else ""); for (a_src, a_path) in annexes { kernel.annex(a_src, a_path); } @@ -134,13 +139,23 @@ def _native_parse_woven( had_error = _finish_parse( module, p_errs, p_diags, comments, lex_diags, lex_errs, prog ); + if prog is not None { + results = kernel.take_pass_results(); + if results { + prog.analysis.record_early(module, results); + } + } return (module, had_error); } def parse_woven( - source_str: str, file_path: str, prog: (JacProgram | None), annexes: list + source_str: str, + file_path: str, + prog: (JacProgram | None), + annexes: list, + early_context: str = "" ) -> tuple { - nat = _native_parse_woven(source_str, file_path, prog, annexes); + nat = _native_parse_woven(source_str, file_path, prog, annexes, early_context); if nat is not None { return nat; } diff --git a/jac/jaclang/compiler/frontend/unitree.impl/nodes.impl.jac b/jac/jaclang/compiler/frontend/unitree.impl/nodes.impl.jac index ab312fd60bb..9ea5071142e 100644 --- a/jac/jaclang/compiler/frontend/unitree.impl/nodes.impl.jac +++ b/jac/jaclang/compiler/frontend/unitree.impl/nodes.impl.jac @@ -1527,6 +1527,22 @@ impl Symbol.__repr__ -> str { return f"Symbol({self.sym_name}, {self.sym_type}, {self.access},{self.defn})"; } +impl Module.set_code_context(context: CodeContext) -> None { + self.decided_codespace = context.value; + for elem in self.body { + if isinstance(elem, ContextAwareNode) { + elem.code_context = context; + if isinstance(elem, ModuleCode) and elem.body { + for inner in elem.body { + if isinstance(inner, ContextAwareNode) { + inner.code_context = context; + } + } + } + } + } +} + impl UniScopeNode._init_scope( name: str, parent_scope: (UniScopeNode | None) = None ) -> None { @@ -1715,9 +1731,13 @@ impl UniScopeNode.adopt_primary(sym: Symbol, alias: str = "") -> None { } impl UniScopeNode._drop_primary(sym: Symbol) -> None { - for e in [edge self->:ScopePrimary:->] { - if getattr(e, "__jac__").target.archetype is sym { - del e; + edges = [edge self->:ScopePrimary:->]; + targets = [self->:ScopePrimary:->]; + for i in range(len(targets)) { + target: Symbol = targets[i]; + if target is sym { + edge_to_drop = edges[i]; + del edge_to_drop; break; } } @@ -1914,7 +1934,10 @@ impl ContextAwareNode.postinit -> None {} impl ContextAwareNode.code_context.getter -> CodeContext { found = [self->:PlacedIn:->]; - return CodeContext(found[0].kind) if found else CodeContext.SERVER; + target = found[0] if found else None; + return CodeContext(target.kind) + if isinstance(target, Codespace) + else CodeContext.SERVER; } impl ContextAwareNode.code_context.setter(code_context: CodeContext) { @@ -1924,7 +1947,8 @@ impl ContextAwareNode.code_context.setter(code_context: CodeContext) { impl Module.decided_codespace.getter -> str { found = [self->:DecidedCodespace:->]; - return found[0].kind if found else ""; + target = found[0] if found else None; + return target.kind if isinstance(target, Codespace) else ""; } impl Module.decided_codespace.setter(decided_codespace: str) { diff --git a/jac/jaclang/compiler/frontend/unitree.jac b/jac/jaclang/compiler/frontend/unitree.jac index de46abf01c2..746378a43fd 100644 --- a/jac/jaclang/compiler/frontend/unitree.jac +++ b/jac/jaclang/compiler/frontend/unitree.jac @@ -136,6 +136,7 @@ import from jaclang.compiler.frontend.relations { CfgSucc, CfgTrue, DecidedCodespace, + Codespace, Defines, InScope, PlacedIn, @@ -628,6 +629,8 @@ node SubTag[T](UniNode) { } node Module(AstDocNode, UniScopeNode) { + def set_code_context(context: CodeContext) -> None; + has name: str, source: Source, terminals: list[Token], diff --git a/jac/jaclang/compiler/jc_materialize.jac b/jac/jaclang/compiler/jc_materialize.jac index 09f14afe823..3dbedd4b4b7 100644 --- a/jac/jaclang/compiler/jc_materialize.jac +++ b/jac/jaclang/compiler/jc_materialize.jac @@ -29,7 +29,39 @@ glob CODE_SKIP: int = 0, CODE_OBJ: int = 6, CODE_NATLIST: int = 7, CODE_UNION: int = 8, - CODE_OPTENUM: int = 9; + CODE_OPTENUM: int = 9, + CODE_NATDICT: int = 10; + +obj ValueRow { + has code: int, + tag: int, + child: int, + key_off: int, + data_off: int, + order_off: int; +} + +obj ContainerCopy { + has owner: int, + key: int, + value: int, + layout: int; +} + +glob _VALUES: dict[int, ValueRow] = {}; + +def:pub jc_reg_value( + index: int, + code: int, + tag: int, + child: int, + key_off: int, + data_off: int, + order_off: int +) -> int { + _VALUES[index] = ValueRow(code, tag, child, key_off, data_off, order_off); + return 0; +} obj FieldRow { has key_h: int, @@ -223,7 +255,8 @@ obj Mz { lst_l: list[int] = [], lst_i: list[int] = [], lst_t: list[int] = [], - skipped: int = 0; + skipped: int = 0, + containers: list[ContainerCopy] = []; } glob _LAST: list[Mz] = []; @@ -235,6 +268,58 @@ def _enqueue(m: Mz, h: int, hint: int) { } } +def _enqueue_value(m: Mz, h: int, layout: int) { + if h == 0 { + return; + } + row = _VALUES[layout]; + if row.code == CODE_OBJ { + _enqueue(m, h, row.tag); + } elif row.code == CODE_NATLIST or row.code == CODE_NATDICT { + n = _peek64(h); + data = _peek64(h + row.data_off); + order = _peek64(h + row.order_off) if row.code == CODE_NATDICT else 0; + for i in range(n) { + slot = _peek64(order + i * 8) if row.code == CODE_NATDICT else i; + _enqueue_value(m, _peek64(data + slot * 8), row.child); + } + } +} + +def _copy_value(m: Mz, h: int, layout: int) -> int { + if h == 0 { + Py_IncRef(_K.none_h); + return _K.none_h; + } + row = _VALUES[layout]; + if row.code == CODE_OBJ { + obj_h = m.map.get(h, 0) or _K.none_h; + Py_IncRef(obj_h); + return obj_h; + } + if row.code == CODE_STR { + return PyUnicode_FromString(h); + } + n = _peek64(h); + data = _peek64(h + row.data_off); + out = PyDict_New() if row.code == CODE_NATDICT else PyList_New(n); + keys = _peek64(h + row.key_off) if row.code == CODE_NATDICT else 0; + order = _peek64(h + row.order_off) if row.code == CODE_NATDICT else 0; + for i in range(n) { + slot = _peek64(order + i * 8) if row.code == CODE_NATDICT else i; + value = _copy_value(m, _peek64(data + slot * 8), row.child); + if row.code == CODE_NATDICT { + key = PyUnicode_FromString(_peek64(keys + slot * 8)); + PyDict_SetItem(out, key, value); + Py_DecRef(key); + Py_DecRef(value); + } else { + PyList_SetItem(out, i, value); + } + } + return out; +} + def _defer_ref(m: Mz, o: int, k: int, t: int, hint: int) { m.ref_o.append(o); m.ref_k.append(k); @@ -369,6 +454,12 @@ def _copy_fields(m: Mz, row: ClassRow, h: int, o: int) { } else { PyObject_SetAttr(o, fr.key_h, _K.none_h); } + } elif code == CODE_NATDICT { + value = _ld64u(buf, fr.off); + if value != 0 { + m.containers.append(ContainerCopy(o, fr.key_h, value, fr.aux_tag)); + _enqueue_value(m, value, fr.aux_tag); + } } } } @@ -573,6 +664,9 @@ def _run(start_h: int, start_hint: int, do_edges: int) -> int { _visit(m, h, hint); } _patch_refs(m); + for copy in m.containers { + _set_fresh(copy.owner, copy.key, _copy_value(m, copy.value, copy.layout)); + } if do_edges != 0 { for h in m.map { if m.map[h] != 0 { diff --git a/jac/jaclang/compiler/jc_unit.jac b/jac/jaclang/compiler/jc_unit.jac index b9643bd907a..5492d34ac69 100644 --- a/jac/jaclang/compiler/jc_unit.jac +++ b/jac/jaclang/compiler/jc_unit.jac @@ -15,6 +15,11 @@ import from jaclang.compiler.frontend.parser.parser { parse_program } import from jaclang.compiler.frontend.parser.tokens { SrcDiag, CommentData } import from jaclang.compiler.frontend.unitree { Module, UniNode } import from jaclang.compiler.frontend.roles { AccessorsRole, BodyRole, SpecRole } +import from jaclang.compiler.frontend.constant { CodeContext } +import from jaclang.compiler.frontend.unitree { Import } +import from jaclang.compiler.passes.ast_validation_pass { ASTValidationPass } +import from jaclang.compiler.passes.sym_tab_build_pass { SymTabBuildPass } +import from jaclang.compiler.passes.transform { Alert } obj JcState { has p_errs: int = 0, @@ -22,7 +27,15 @@ obj JcState { mod_h: int = 0, diags_h: int = 0, lexdiags_h: int = 0, - comments_h: int = 0; + comments_h: int = 0, + early_context: int = 0, + completed: int = 0, + validation_errors_h: int = 0, + validation_warnings_h: int = 0, + symbols_errors_h: int = 0, + symbols_warnings_h: int = 0, + validation_time: float = 0.0, + symbols_time: float = 0.0; } glob _JC: JcState = JcState(), @@ -44,6 +57,17 @@ def _clear_results { _drop_h(_JC.diags_h); _drop_h(_JC.lexdiags_h); _drop_h(_JC.comments_h); + _drop_h(_JC.validation_errors_h); + _drop_h(_JC.validation_warnings_h); + _drop_h(_JC.symbols_errors_h); + _drop_h(_JC.symbols_warnings_h); + _JC.validation_errors_h = 0; + _JC.validation_warnings_h = 0; + _JC.symbols_errors_h = 0; + _JC.symbols_warnings_h = 0; + _JC.completed = 0; + _JC.validation_time = 0.0; + _JC.symbols_time = 0.0; _JC.mod_h = 0; _JC.diags_h = 0; _JC.lexdiags_h = 0; @@ -52,6 +76,41 @@ def _clear_results { _JC.lex_errs = 0; } +def:pub jc_set_early(context: int) -> int { + _JC.early_context = context; + return 0; +} + +def:pub jc_completed -> int { + return _JC.completed; +} + +def:pub jc_pass_time(pass_id: int) -> float { + return _JC.validation_time if pass_id == 1 else _JC.symbols_time; +} + +def:pub jc_take_pass_alerts(pass_id: int, warnings: int) -> int { + h = 0; + if pass_id == 1 { + if warnings != 0 { + h = _JC.validation_warnings_h; + _JC.validation_warnings_h = 0; + } else { + h = _JC.validation_errors_h; + _JC.validation_errors_h = 0; + } + } else { + if warnings != 0 { + h = _JC.symbols_warnings_h; + _JC.symbols_warnings_h = 0; + } else { + h = _JC.symbols_errors_h; + _JC.symbols_errors_h = 0; + } + } + return h; +} + def _clear_annexes { while _ANNEX_SRC { _ANNEX_SRC.pop(); @@ -105,6 +164,59 @@ def _materialize( return 0; } +def _finish_unit( + r: Region, + mod: Module, + p_diags: list[SrcDiag], + comments: list[CommentData], + lex_diags: list[SrcDiag] +) -> int { + validation: (ASTValidationPass | None) = None; + symbols: (SymTabBuildPass | None) = None; + if _JC.early_context != 0 and _JC.p_errs == 0 and _JC.lex_errs == 0 { + if _JC.early_context == 2 { + mod.set_code_context(CodeContext.CLIENT); + } elif _JC.early_context == 3 { + mod.set_code_context(CodeContext.NATIVE); + } + validation = ASTValidationPass(ir_in=mod); + _JC.completed = 1; + has_absorb = False; + for imp in mod.get_all_sub_nodes(Import) { + if imp.is_absorb { + has_absorb = True; + break; + } + } + # Wildcard imports need the driver's dependency resolver before binding. + if not has_absorb { + symbols = SymTabBuildPass(ir_in=mod); + _JC.completed = 3; + } + } + status = _materialize(r, mod, p_diags, comments, lex_diags); + if status != 0 { + return status; + } + if validation is not None { + _JC.validation_time = validation.time_taken; + _JC.validation_errors_h = _mz_list(validation.errors_had, _alert_tag()); + _JC.validation_warnings_h = _mz_list(validation.warnings_had, _alert_tag()); + } + if symbols is not None { + _JC.symbols_time = symbols.time_taken; + _JC.symbols_errors_h = _mz_list(symbols.errors_had, _alert_tag()); + _JC.symbols_warnings_h = _mz_list(symbols.warnings_had, _alert_tag()); + } + jc_mz_dispose(); + return 0; +} + +def _alert_tag -> int { + import from jaclang.runtime.osp_tag { stable_osp_tag } + return stable_osp_tag("Alert"); +} + def _run_in_region(src: str, path: str) -> int { status = 0; r = Region(); @@ -123,7 +235,7 @@ def _run_in_region(src: str, path: str) -> int { } } if status == 0 { - status = _materialize(r, mod, p_diags, comments, lex_diags); + status = _finish_unit(r, mod, p_diags, comments, lex_diags); } } return status; @@ -133,6 +245,7 @@ def:pub jc_run(src: str, path: str) -> int { _clear_results(); status = _run_in_region(src, path); _clear_annexes(); + _JC.early_context = 0; return status; } diff --git a/jac/jaclang/compiler/native_compiler.jac b/jac/jaclang/compiler/native_compiler.jac index 9358c7cc89a..388b0e04844 100644 --- a/jac/jaclang/compiler/native_compiler.jac +++ b/jac/jaclang/compiler/native_compiler.jac @@ -2,8 +2,9 @@ import ctypes; import json; import os; import sys; -import from ctypes { c_char_p, c_int64, c_void_p, cast, py_object } -import from jaclang.runtime.osp_model { stable_osp_tag } +import from ctypes { c_char_p, c_double, c_int64, c_void_p, cast, py_object } +import from jaclang.runtime.osp_tag { stable_osp_tag } +import type from jaclang.compiler.passes.transform { PassResult } glob CODE_INT: int = 1, CODE_BOOL: int = 2, @@ -13,7 +14,8 @@ glob CODE_INT: int = 1, CODE_OBJ: int = 6, CODE_NATLIST: int = 7, CODE_UNION: int = 8, - CODE_OPTENUM: int = 9; + CODE_OPTENUM: int = 9, + CODE_NATDICT: int = 10; glob CLASS_MODULES: tuple = ( "jaclang.compiler.frontend.unitree", @@ -23,6 +25,8 @@ glob CLASS_MODULES: tuple = ( "jaclang.compiler.frontend.parser.tokens", "jaclang.compiler.frontend.roles", "jaclang.compiler.frontend.relations", + "jaclang.compiler.frontend.diagnostics", + "jaclang.compiler.passes.transform", ); @@ -31,7 +35,8 @@ def _enum_class(name: str) -> any { import importlib; for modname in ( "jaclang.compiler.frontend.constant", - "jaclang.compiler.frontend.parser.tokens" + "jaclang.compiler.frontend.parser.tokens", + "jaclang.compiler.frontend.diagnostics" ) { mod = importlib.import_module(modname); cls = getattr(mod, name, None); @@ -67,6 +72,7 @@ class NativeCompiler { } self._keep = []; self.classes = {}; + self._value_rows: dict[str, int] = {}; self._bind(); self._register(); } @@ -109,6 +115,17 @@ class NativeCompiler { L.jc_reg_edge.restype = c_int64; L.jc_reg_hint_tags.argtypes = [c_int64] * 2; L.jc_reg_hint_tags.restype = c_int64; + self.supports_early: bool = hasattr(L, "jc_set_early"); + if self.supports_early { + L.jc_set_early.argtypes = [c_int64]; + L.jc_set_early.restype = c_int64; + L.jc_completed.argtypes = []; + L.jc_completed.restype = c_int64; + L.jc_pass_time.argtypes = [c_int64]; + L.jc_pass_time.restype = c_double; + L.jc_take_pass_alerts.argtypes = [c_int64, c_int64]; + L.jc_take_pass_alerts.restype = c_void_p; + } } def _hold(self: NativeCompiler, o: any) -> int { @@ -189,6 +206,9 @@ class NativeCompiler { enum_h = self._hold(table); } elif code == "natlist" { nat_code = CODE_NATLIST; + } elif code == "natdict" { + nat_code = CODE_NATDICT; + aux_tag = self._register_value(f["schema"]); } elif code == "union" { nat_code = CODE_UNION; parts = [x.strip(" ()") for x in anns.get(fname, "").split("|")]; @@ -226,6 +246,36 @@ class NativeCompiler { } } + def _register_value(self: NativeCompiler, schema: dict) -> int { + key = json.dumps(schema, sort_keys=True); + cached = self._value_rows.get(key); + if cached is not None { + return cached; + } + child = self._register_value(schema["element"]) if "element" in schema else 0; + index = len(self._value_rows) + 1; + self._value_rows[key] = index; + codes = { + "str": CODE_STR, + "obj": CODE_OBJ, + "list": CODE_NATLIST, + "dict": CODE_NATDICT + }; + fn = self.lib.jc_reg_value; + fn.argtypes = [c_int64] * 7; + fn.restype = c_int64; + fn( + index, + codes[schema["code"]], + self._class_tag(schema["class"]) if "class" in schema else 0, + child, + schema.get("key_offset", 0), + schema.get("data_offset", 0), + schema.get("order_offset", 0) + ); + return index; + } + def _register_defaults(self: NativeCompiler, cls: any, tag: int) { import dataclasses; try { @@ -293,6 +343,7 @@ class NativeCompiler { } def _register(self: NativeCompiler) { + import from jaclang.runtime.archetype { NodeArchetype } import from jaclang.compiler.frontend.roles { AccessorsRole, BodyRole, @@ -323,7 +374,7 @@ class NativeCompiler { if info.get("tag") and info.get("tag_offset", -1) >= 0 { tagged = 1; } - is_node = 1 if "_role_shapes" in info["fields"] else 0; + is_node = 1 if issubclass(cls, NodeArchetype) else 0; self.lib.jc_reg_class(tag, self._hold(cls), info["size"], tagged, is_node); self._register_defaults(cls, tag); self._register_fields(name, cls, tag); @@ -361,6 +412,45 @@ class NativeCompiler { return self._call_str2("jc_run", source, path); } + def prepare_early(self: NativeCompiler, context: str) -> None { + if self.supports_early { + self.lib.jc_set_early( + {"server": 1, "client": 2, "native": 3}.get(context, 0) + ); + } + } + + def take_pass_results(self: NativeCompiler) -> dict[str, PassResult] { + import from jaclang.compiler.passes.transform { PassResult } + import from jaclang.compiler.passes.ast_validation_pass { ASTValidationPass } + import from jaclang.compiler.passes.sym_tab_build_pass { SymTabBuildPass } + out: dict[str, PassResult] = {}; + if not self.supports_early { + return out; + } + completed = self.lib.jc_completed(); + for (pass_id, pass_cls) in [(1, ASTValidationPass), (2, SymTabBuildPass)] { + if not (completed & pass_id) { + continue; + } + errors = self._take(self.lib.jc_take_pass_alerts(pass_id, 0)) or []; + warnings = self._take(self.lib.jc_take_pass_alerts(pass_id, 1)) or []; + for alert in errors + warnings { + alert.from_pass = pass_cls; + if alert.code is not None { + import from jaclang.compiler.frontend.diagnostics { DIAGNOSTICS } + alert.code = DIAGNOSTICS[alert.code.code]; + } + } + out[pass_cls.__name__] = PassResult( + errors_had=errors, + warnings_had=warnings, + time_taken=self.lib.jc_pass_time(pass_id) + ); + } + return out; + } + def _take(self: NativeCompiler, ptr: any) -> any { if not ptr { return None; diff --git a/jac/jaclang/compiler/native_scope.jac b/jac/jaclang/compiler/native_scope.jac index d63afb9bf59..c502a4818c2 100644 --- a/jac/jaclang/compiler/native_scope.jac +++ b/jac/jaclang/compiler/native_scope.jac @@ -20,6 +20,15 @@ glob NATIVE_SCOPE: tuple[NativeScopeEntry, ...] = ( ), NativeScopeEntry(module="jaclang.compiler.jc_materialize", strict=True), NativeScopeEntry(module="jaclang.compiler.jc_unit", strict=True), + NativeScopeEntry(module="jaclang.compiler.frontend.diagnostics", strict=True), + NativeScopeEntry(module="jaclang.compiler.passes.transform", strict=True), + NativeScopeEntry(module="jaclang.compiler.passes.uni_pass", strict=True), + NativeScopeEntry( + module="jaclang.compiler.passes.ast_validation_pass", strict=True + ), + NativeScopeEntry( + module="jaclang.compiler.passes.sym_tab_build_pass", strict=True + ), ); diff --git a/jac/jaclang/compiler/passes/ast_validation_pass.jac b/jac/jaclang/compiler/passes/ast_validation_pass.jac index 83e4b68d931..5c4e389de85 100644 --- a/jac/jaclang/compiler/passes/ast_validation_pass.jac +++ b/jac/jaclang/compiler/passes/ast_validation_pass.jac @@ -109,8 +109,8 @@ walker ASTValidationPass(UniPass) { can enter_has_var with HasVar entry; can enter_expr_as_item with ExprAsItem entry; can enter_delete_stmt with DeleteStmt entry; - def _reject_non_bindable(target: object) -> None; - def _reject_undeletable(target: object) -> None; + def _reject_non_bindable(target: UniNode) -> None; + def _reject_undeletable(target: UniNode) -> None; can enter_param_var with ParamVar entry; can enter_yield_expr with YieldExpr entry; can enter_ctrl_stmt with CtrlStmt entry; diff --git a/jac/jaclang/compiler/passes/impl/ast_validation_pass.impl.jac b/jac/jaclang/compiler/passes/impl/ast_validation_pass.impl.jac index bd3fe643243..64bf74f1634 100644 --- a/jac/jaclang/compiler/passes/impl/ast_validation_pass.impl.jac +++ b/jac/jaclang/compiler/passes/impl/ast_validation_pass.impl.jac @@ -29,7 +29,7 @@ impl ASTValidationPass.enter_binary_expr with BinaryExpr entry { } impl ASTValidationPass.enter_name with Name entry { - if here.__class__ is not Name { + if type(here) is not Name { return; } if here.value == "pass" { @@ -38,7 +38,7 @@ impl ASTValidationPass.enter_name with Name entry { } impl ASTValidationPass.enter_param_var with ParamVar entry { - ctx_owner = here.find_parent_of_type(ContextAwareNode); + ctx_owner: (ContextAwareNode | None) = here.find_parent_of_type(ContextAwareNode); if isinstance(here.name, Name) and keyword.iskeyword(here.name.value) and (ctx_owner is None or ctx_owner.code_context == CodeContext.SERVER) { @@ -117,7 +117,7 @@ impl ASTValidationPass.enter_switch_stmt with SwitchStmt entry { } } -impl ASTValidationPass._reject_non_bindable(target: object) -> None { +impl ASTValidationPass._reject_non_bindable(target: UniNode) -> None { non_bindable = {Tok.KW_ROOT, Tok.KW_SUPER}; if isinstance(target, (TupleVal, ListVal)) { for el in target.values { @@ -215,7 +215,7 @@ impl ASTValidationPass.enter_inner_compr with InnerCompr entry { } impl ASTValidationPass.enter_has_var with HasVar entry { - ctx_owner = here.find_parent_of_type(ContextAwareNode); + ctx_owner: (ContextAwareNode | None) = here.find_parent_of_type(ContextAwareNode); if isinstance(here.name, Name) and keyword.iskeyword(here.name.value) and (ctx_owner is None or ctx_owner.code_context == CodeContext.SERVER) { @@ -239,7 +239,7 @@ impl ASTValidationPass.enter_delete_stmt with DeleteStmt entry { } } -impl ASTValidationPass._reject_undeletable(target: object) -> None { +impl ASTValidationPass._reject_undeletable(target: UniNode) -> None { inner = target; while isinstance(inner, AtomUnit) { inner = inner.value; diff --git a/jac/jaclang/compiler/passes/impl/layout_pass.impl.jac b/jac/jaclang/compiler/passes/impl/layout_pass.impl.jac index 7e922342994..3b75510109f 100644 --- a/jac/jaclang/compiler/passes/impl/layout_pass.impl.jac +++ b/jac/jaclang/compiler/passes/impl/layout_pass.impl.jac @@ -204,11 +204,15 @@ impl LayoutPass._compute_c3_mro(class_name: str) -> list[str] { } impl LayoutPass._topo_sort(arch_map: dict[str, Archetype]) -> list[str] { + return self.registry.dependency_order(list(arch_map)); +} + +impl LayoutRegistry.dependency_order(names: list[str]) -> list[str] { + """Order layouts after their ancestors, including incomplete imported MROs."""; visited: set[str] = set(); order: list[str] = []; - reg = self.registry; - for aname in arch_map { + for aname in names { if aname in visited { continue; } @@ -224,7 +228,7 @@ impl LayoutPass._topo_sort(arch_map: dict[str, Archetype]) -> list[str] { continue; } stack.append((name, True)); - mro = reg.mro_order.get(name, [name]); + mro = self.mro_order.get(name, [name]); for ancestor in reversed(mro[1:]) { if ancestor not in visited { stack.append((ancestor, False)); diff --git a/jac/jaclang/compiler/passes/impl/sym_tab_build_pass.impl.jac b/jac/jaclang/compiler/passes/impl/sym_tab_build_pass.impl.jac index 5fe0507756c..c7e3073bc41 100644 --- a/jac/jaclang/compiler/passes/impl/sym_tab_build_pass.impl.jac +++ b/jac/jaclang/compiler/passes/impl/sym_tab_build_pass.impl.jac @@ -393,14 +393,14 @@ impl SymTabBuildPass.exit_open_stmt with OpenStmt exit { } impl SymTabBuildPass.enter_if_stmt with IfStmt entry { - if here.__class__ is not IfStmt { + if type(here) is not IfStmt { return; } self.push_scope_and_link(here); } impl SymTabBuildPass.exit_if_stmt with IfStmt exit { - if here.__class__ is not IfStmt { + if type(here) is not IfStmt { return; } self.pop_scope(); @@ -505,7 +505,7 @@ impl SymTabBuildPass.exit_lambda_expr with LambdaExpr exit { } impl SymTabBuildPass.enter_list_compr with ListCompr entry { - if here.__class__ is not ListCompr { + if type(here) is not ListCompr { return; } self.push_scope_and_link(here); @@ -515,7 +515,7 @@ impl SymTabBuildPass.enter_list_compr with ListCompr entry { } impl SymTabBuildPass.exit_list_compr with ListCompr exit { - if here.__class__ is not ListCompr { + if type(here) is not ListCompr { return; } self.pop_scope(); diff --git a/jac/jaclang/compiler/passes/impl/transform.impl.jac b/jac/jaclang/compiler/passes/impl/transform.impl.jac index 317d4ce912b..165a1586997 100644 --- a/jac/jaclang/compiler/passes/impl/transform.impl.jac +++ b/jac/jaclang/compiler/passes/impl/transform.impl.jac @@ -45,9 +45,11 @@ impl Alert.pretty_print(*, colors: bool = False) -> str { } impl DiagnosticPolicy.is_suppressed( - diag: DiagnosticInfo, loc: (CodeLocInfo | Span | None) = None + diag: DiagnosticInfo, + loc: (CodeLocInfo | Span | None) = None, + source: (Source | None) = None ) -> bool { - if loc is not None and self._inline_suppressed(diag, loc) { + if loc is not None and self._inline_suppressed(diag, loc, source) { return True; } if diag.code in self.suppress_codes { @@ -92,13 +94,14 @@ impl DiagnosticPolicy.is_suppressed( } impl DiagnosticPolicy._inline_suppressed( - diag: DiagnosticInfo, loc: (CodeLocInfo | Span) + diag: DiagnosticInfo, loc: (CodeLocInfo | Span), source: (Source | None) = None ) -> bool { - if not isinstance(loc, CodeLocInfo) { - return False; + if isinstance(loc, CodeLocInfo) { + source = loc.orig_src; } - source = loc.orig_src; - if source is None or not source.inline_suppressions { + if source is None + or source.file_path != loc.mod_path + or not source.inline_suppressions { return False; } line = loc.first_line; @@ -116,6 +119,15 @@ impl DiagnosticPolicy._inline_suppressed( } impl BaseTransform.postinit -> None { + completed = self.completed; + if completed is not None { + self.ir_out = self.ir_in; + self.errors_had.extend(completed.errors_had); + self.warnings_had.extend(completed.warnings_had); + self.time_taken = completed.time_taken; + self.completed = None; + return; + } self.pre_transform(); self.ir_out = self.timed_transform(ir_in=self.ir_in); self.post_transform(); @@ -127,7 +139,7 @@ impl BaseTransform.post_transform -> None {} impl BaseTransform.log_info(msg: str) -> None { import logging; - logging.getLogger(self.__class__.__name__).info(msg); + logging.getLogger(type(self).__name__).info(msg); } impl BaseTransform.is_canceled -> bool { @@ -144,7 +156,7 @@ impl Transform.emit( node_override: (UniNode | None) = None, related: (list[tuple[str, UniNode]] | None) = None, help_text: (str | None) = None, - **kwargs: any + **kwargs: object ) -> bool { loc = node_override.loc if node_override else self.cur_node.loc; @@ -164,7 +176,7 @@ impl Transform.emit( alrt = Alert( msg=msg, loc=loc.span(), - from_pass=self.__class__, + from_pass=type(self), code=diag, related=related_locs, help_text=resolved_help @@ -185,6 +197,6 @@ impl Transform._is_suppressed( } impl Transform.ice(msg: str = 'Something went horribly wrong!') -> str { - self.emit(E9001, pass_name=self.__class__.__name__, details=msg); - return f"Internal Compiler Error: Pass {self.__class__.__name__} - {msg}"; + self.emit(E9001, pass_name=type(self).__name__, details=msg); + return f"Internal Compiler Error: Pass {type(self).__name__} - {msg}"; } diff --git a/jac/jaclang/compiler/passes/layout_pass.jac b/jac/jaclang/compiler/passes/layout_pass.jac index 64875fc422f..e87f89511b2 100644 --- a/jac/jaclang/compiler/passes/layout_pass.jac +++ b/jac/jaclang/compiler/passes/layout_pass.jac @@ -148,6 +148,14 @@ def layout_slot_count(lay: ArchetypeLayout) -> int { def declaring_module_path(base_expr: UniNode) -> str { import os; + import from jaclang.compiler.types.types { ClassType } + base_type = base_expr.type; + if isinstance(base_type, ClassType) and base_type._shared is not None { + scope = base_type.shared.symbol_table; + if isinstance(scope, Archetype) and scope.loc.mod_path { + return os.path.realpath(scope.loc.mod_path); + } + } if not isinstance(base_expr, AstSymbolNode) { return ""; } @@ -256,7 +264,7 @@ def adopt_imported_bases(module: Module) -> list[str] { } } layouts = reg.layouts; - order = sorted(layouts.keys(), key=lambda (n: str) { len(layouts[n].mro or [n]); }); + order = reg.dependency_order(list(layouts)); adopted: list[str] = []; for name in order { lay = reg.layouts.get(name); @@ -587,6 +595,7 @@ obj LayoutRegistry { def resolve_method_owner(arch_name: str, method_name: str) -> (str | None); def get_mro(name: str) -> list[str]; def get_topo_order -> list[str]; + def dependency_order(names: list[str]) -> list[str]; def is_primary_ancestor(child: str, ancestor: str) -> bool; } diff --git a/jac/jaclang/compiler/passes/sym_tab_build_pass.jac b/jac/jaclang/compiler/passes/sym_tab_build_pass.jac index bacd609f4ed..abf51b9cf67 100644 --- a/jac/jaclang/compiler/passes/sym_tab_build_pass.jac +++ b/jac/jaclang/compiler/passes/sym_tab_build_pass.jac @@ -1,4 +1,3 @@ -import from dataclasses { field } import from jaclang.compiler.frontend.unitree { Ability, ArchHas, @@ -56,7 +55,7 @@ import from jaclang.compiler.frontend.constant { SymbolAccess } import from jaclang.compiler.passes.uni_pass { UniPass } import from jaclang.compiler.frontend.diagnostics { E0064, E0077 } -glob TEST_ASSERT_METHOD_NAMES = ( +glob TEST_ASSERT_METHOD_NAMES: tuple[str, ...] = ( 'assertAlmostEqual', 'assertCountEqual', 'assertDictEqual', @@ -102,7 +101,7 @@ glob TEST_ASSERT_METHOD_NAMES = ( walker SymTabBuildPass(UniPass) { static has PROVIDES: tuple = ('SymOf', 'Defines', 'InScope', 'ScopeChild'); - has cur_sym_tab: list[UniScopeNode] = field(default_factory=`list); + has cur_sym_tab: list[UniScopeNode] = []; def before_pass -> None; def push_scope_and_link(key_node: UniScopeNode) -> None; diff --git a/jac/jaclang/compiler/passes/transform.jac b/jac/jaclang/compiler/passes/transform.jac index f272138dc15..5168554fcd2 100644 --- a/jac/jaclang/compiler/passes/transform.jac +++ b/jac/jaclang/compiler/passes/transform.jac @@ -7,8 +7,7 @@ import from jaclang.compiler.frontend.diagnostics { Severity } import from jaclang.compiler.frontend.diagnostic_utils { resolve_diagnostic_tokens } -import from abc { ABC } -import from jaclang.compiler.frontend.unitree { UniNode } +import from jaclang.compiler.frontend.unitree { Source, UniNode } obj Alert { has msg: str, @@ -35,20 +34,32 @@ obj DiagnosticPolicy { _inline_cache: dict[(tuple[str, int], list[str])] = {}; def is_suppressed( - diag: DiagnosticInfo, loc: (CodeLocInfo | Span | None) = None + diag: DiagnosticInfo, + loc: (CodeLocInfo | Span | None) = None, + source: (Source | None) = None ) -> bool; - def _inline_suppressed(diag: DiagnosticInfo, loc: (CodeLocInfo | Span)) -> bool; + def _inline_suppressed( + diag: DiagnosticInfo, loc: (CodeLocInfo | Span), source: (Source | None) = None + ) -> bool; +} + +"""Diagnostics and timing from a pass already applied to an IR unit.""" +obj PassResult { + has errors_had: list[Alert] = [], + warnings_had: list[Alert] = [], + time_taken: float = 0.0; } -obj BaseTransform(ABC) { +walker BaseTransform { has ir_in: UniNode, cancel_token: any = None, cancel_requested: bool = False, errors_had: list[Alert] = [], warnings_had: list[Alert] = [], time_taken: float = 0.0, - ir_out: UniNode postinit; + ir_out: UniNode postinit, + completed: (PassResult | None) = None; def postinit -> None; def timed_transform(ir_in: UniNode) -> UniNode { @@ -68,7 +79,7 @@ obj BaseTransform(ABC) { def is_canceled -> bool; } -obj Transform(BaseTransform) { +walker Transform(BaseTransform) { has cur_node: UniNode postinit, policy: (DiagnosticPolicy | None) = None; @@ -82,7 +93,7 @@ obj Transform(BaseTransform) { node_override: (UniNode | None) = None, related: (list[tuple[str, UniNode]] | None) = None, help_text: (str | None) = None, - **kwargs: any + **kwargs: object ) -> bool; def _is_suppressed(diag: DiagnosticInfo, loc: CodeLocInfo | None = None) -> bool; diff --git a/jac/jaclang/runtime/osp_tag.jac b/jac/jaclang/runtime/osp_tag.jac index 6ede5e94684..26bd36ed797 100644 --- a/jac/jaclang/runtime/osp_tag.jac +++ b/jac/jaclang/runtime/osp_tag.jac @@ -1,7 +1,12 @@ def stable_osp_tag(name: str) -> int { - _h = 14695981039346656037; + # Only the low 53 bits form a tag. Split multiplication into limbs so + # the same FNV hash also works with checked native 64-bit integers. + _h = 5239054864098085; for _c in name { - _h = ((_h ^ ord(_c)) * 1099511628211) & 18446744073709551615; + _h = _h ^ ord(_c); + low = (_h & 67108863) * 435; + high = ((_h >> 26) * 435 + (_h & 67108863) * 16384 + (low >> 26)) & 134217727; + _h = (high << 26) | (low & 67108863); } return _h & 9007199254740991; } diff --git a/jac/tests/compiler/backends/native/fixtures/boxed_return_lifetime.jac b/jac/tests/compiler/backends/native/fixtures/boxed_return_lifetime.jac new file mode 100644 index 00000000000..32112c386a6 --- /dev/null +++ b/jac/tests/compiler/backends/native/fixtures/boxed_return_lifetime.jac @@ -0,0 +1,30 @@ +obj Item { + has value: int; +} + +def forward(items: list[Item]) -> (list[Item] | Item) { + return items; +} + +obj Source { + has body: (list[Item] | Item) { + getter { + return forward([Item(2), Item(3), Item(5)]); + } + } +} + +def probe -> int { + source = Source(); + total = 0; + if isinstance(source.body, list) { + for item in source.body { + total += item.value; + } + } + return total; +} + +with entry { + assert probe() == 10; +} diff --git a/jac/tests/compiler/backends/native/fixtures/enum_ctor_string.jac b/jac/tests/compiler/backends/native/fixtures/enum_ctor_string.jac new file mode 100644 index 00000000000..568a109adf8 --- /dev/null +++ b/jac/tests/compiler/backends/native/fixtures/enum_ctor_string.jac @@ -0,0 +1,21 @@ +enum Hue { RED = "red", GREEN = "green", EMPTY = "" } + +def probe(which: int) -> int { + text = "red" + if which == 0 + else "green" if which == 1 else "" if which == 2 else "bad"; + try { + member = Hue(text); + again = Hue(member); + return 1 if member == again and member.value == text else 0; + } except ValueError { + return -1; + } +} + +with entry { + assert probe(0) == 1; + assert probe(1) == 1; + assert probe(2) == 1; + assert probe(3) == -1; +} diff --git a/jac/tests/compiler/backends/native/fixtures/osp_runtime_import.jac b/jac/tests/compiler/backends/native/fixtures/osp_runtime_import.jac new file mode 100644 index 00000000000..6c8102152a5 --- /dev/null +++ b/jac/tests/compiler/backends/native/fixtures/osp_runtime_import.jac @@ -0,0 +1,27 @@ +import from jaclang.runtime.osp_kernel { osp_skip_rest } + +node Item {} + +walker Inspect { + has count: int = 0; + + can first with Item entry { + self.count += 1; + osp_skip_rest(); + } + + can second with Item entry { + self.count += 100; + } +} + +def probe -> int { + item = Item(); + visitor = Inspect(); + item spawn visitor; + return visitor.count; +} + +with entry { + assert probe() == 1; +} diff --git a/jac/tests/compiler/backends/native/fixtures/type_identity.jac b/jac/tests/compiler/backends/native/fixtures/type_identity.jac new file mode 100644 index 00000000000..7c9f0e8dae0 --- /dev/null +++ b/jac/tests/compiler/backends/native/fixtures/type_identity.jac @@ -0,0 +1,18 @@ +import from .type_identity_types { RemoteValue } +import from .type_identity_library { make_value, make_child } + +def probe -> int { + value = make_value(); + child = make_child(); + if type(value) is not RemoteValue { + return 1; + } + if type(child) is RemoteValue { + return 2; + } + return 0; +} + +with entry { + assert probe() == 0; +} diff --git a/jac/tests/compiler/backends/native/fixtures/type_identity_library.jac b/jac/tests/compiler/backends/native/fixtures/type_identity_library.jac new file mode 100644 index 00000000000..36a7f23b784 --- /dev/null +++ b/jac/tests/compiler/backends/native/fixtures/type_identity_library.jac @@ -0,0 +1,9 @@ +import from .type_identity_types { RemoteValue, RemoteChild } + +def make_value -> RemoteValue { + return RemoteValue(); +} + +def make_child -> RemoteValue { + return RemoteChild(); +} diff --git a/jac/tests/compiler/backends/native/fixtures/type_identity_types.jac b/jac/tests/compiler/backends/native/fixtures/type_identity_types.jac new file mode 100644 index 00000000000..e82babf0cc8 --- /dev/null +++ b/jac/tests/compiler/backends/native/fixtures/type_identity_types.jac @@ -0,0 +1,2 @@ +node RemoteValue {} +node RemoteChild(RemoteValue) {} diff --git a/jac/tests/compiler/backends/native/fixtures/union_properties.jac b/jac/tests/compiler/backends/native/fixtures/union_properties.jac new file mode 100644 index 00000000000..d64d9d7ec74 --- /dev/null +++ b/jac/tests/compiler/backends/native/fixtures/union_properties.jac @@ -0,0 +1,49 @@ +obj Left { + has value: int = 3; + has items: list[int] { getter; } +} + +impl Left.items.getter -> list[int] { + return [self.value, self.value + 1]; +} + +obj Right { + has padding: str = "different layout", + value: int = 10; + + has items: list[int] { getter; } +} + +impl Right.items.getter -> list[int] { + return [self.value]; +} + +obj Stored { + has items: list[int] = [5, 6]; +} + +def total(value: Left | Right | Stored) -> int { + result = 0; + for item in value.items { + result += item; + } + return result; +} + +def probe(which: int) -> int { + if which == 0 { + return total(Left()); + } + if which == 2 { + value = Stored(); + first = total(value); + return first + total(value); + } + return total(Right()); +} + +with entry { + assert probe(0) == 7; + assert probe(1) == 10; + assert probe(2) == 22; +} diff --git a/jac/tests/compiler/backends/native/fixtures/walker_base_dispatch.jac b/jac/tests/compiler/backends/native/fixtures/walker_base_dispatch.jac new file mode 100644 index 00000000000..74dcf643371 --- /dev/null +++ b/jac/tests/compiler/backends/native/fixtures/walker_base_dispatch.jac @@ -0,0 +1,28 @@ +import from .walker_base_library { Base, Item } + +walker Derived(Base) { + can inspect with Item entry { + self.count = self.count * 10 + 2; + } +} + +walker Override(Base) { + can first with Item entry { + self.count = 9; + } +} + +def probe -> int { + item = Item(); + return Derived().run(item) * 10 + item.visited; +} + +def override_probe -> int { + item = Item(); + return Override().run(item) * 10 + item.visited; +} + +with entry { + assert probe() == 121; + assert override_probe() == 91; +} diff --git a/jac/tests/compiler/backends/native/fixtures/walker_base_library.jac b/jac/tests/compiler/backends/native/fixtures/walker_base_library.jac new file mode 100644 index 00000000000..7ca7b4cc290 --- /dev/null +++ b/jac/tests/compiler/backends/native/fixtures/walker_base_library.jac @@ -0,0 +1,22 @@ +node Parent { + has visited: int = 0; + + can touch with Base entry { + self.visited += 1; + } +} + +node Item(Parent) {} + +walker Base { + has count: int = 0; + + can first with Item entry { + self.count = self.count * 10 + 1; + } + + def run(item: Item) -> int { + item spawn self; + return self.count; + } +} diff --git a/jac/tests/compiler/backends/native/fixtures/xmod_demote_dyn_lib.jac b/jac/tests/compiler/backends/native/fixtures/xmod_demote_dyn_lib.jac index 38c5021c965..98c016c2f40 100644 --- a/jac/tests/compiler/backends/native/fixtures/xmod_demote_dyn_lib.jac +++ b/jac/tests/compiler/backends/native/fixtures/xmod_demote_dyn_lib.jac @@ -4,18 +4,14 @@ refuses it, so the demotion verdict exists only after NaIRGenPass lowers the dependency. `double` must stay native. -`Hue` carries string values on purpose. #8266 gave the int-valued spelling a -real lowering -- validate the value against the declared member table, raise -`ValueError` when it misses -- and a string-valued enum's runtime form is the -member's ordinal rather than its value, so the lookup is not the identity and -the emitter still refuses it by name. The day that refusal goes, this fixture -needs a different emit-time-only shape and the test above says so. +`Hue` receives a floating-point argument. Native integer-enum construction +does not coerce floats to integers, so its emitter still refuses this shape. """ -enum Hue { RED = "red", GREEN = "green" } +enum Hue { RED = 1, GREEN = 2 } def from_code(x: str) -> int { - h = Hue(x); + h = Hue(float(x)); return len(x) if h is not None else 0; } diff --git a/jac/tests/compiler/backends/native/test_native_abi_layout.jac b/jac/tests/compiler/backends/native/test_native_abi_layout.jac new file mode 100644 index 00000000000..303ca8480df --- /dev/null +++ b/jac/tests/compiler/backends/native/test_native_abi_layout.jac @@ -0,0 +1,15 @@ +"""Bridge metadata uses the target ABI, including small nested aggregates.""" + +import from jaclang.compiler.backends.native.abi { struct_layouts } +import from jaclang.compiler.backends.native.llvm { binding, ir } + +test "a nullable boolean does not add eight bytes of padding" { + binding.initialize_native_target(); + target = binding.Target.from_default_triple().create_target_machine(); + module = ir.Module(context=ir.Context()); + module.data_layout = str(target.target_data); + small = ir.LiteralStructType([ir.IntType(8), ir.IntType(1)]); + example = module.context.get_identified_type("Example"); + example.set_body(ir.IntType(8), small, ir.IntType(8)); + assert struct_layouts(module)["Example"] == ([0, 1, 3], 4); +} diff --git a/jac/tests/compiler/backends/native/test_native_annex_cache.jac b/jac/tests/compiler/backends/native/test_native_annex_cache.jac new file mode 100644 index 00000000000..c6f37039f8b --- /dev/null +++ b/jac/tests/compiler/backends/native/test_native_annex_cache.jac @@ -0,0 +1,21 @@ +"""Native dependency cache keys include implementation annexes.""" + +import os; +import tempfile; +import from pathlib { Path } +import from jaclang.compiler.backends.native.na_compile_pass { native_dep_cache_paths } +import from jaclang.compiler.driver.compile_options { CompileOptions } + +test "editing an implementation invalidates its native dependency artifact" { + with tempfile.TemporaryDirectory() as directory { + source = Path(directory) / "library.jac"; + annex = Path(directory) / "library.impl.jac"; + source.write_text("def value -> int;\n"); + annex.write_text("impl value -> int { return 1; }\n"); + options = CompileOptions(aot_mode=True, default_codespace="native"); + before = native_dep_cache_paths(os.path.realpath(str(source)), options); + annex.write_text("impl value -> int { return 2; }\n"); + after = native_dep_cache_paths(os.path.realpath(str(source)), options); + assert before != after; + } +} diff --git a/jac/tests/compiler/backends/native/test_native_boxed_return_lifetime.jac b/jac/tests/compiler/backends/native/test_native_boxed_return_lifetime.jac new file mode 100644 index 00000000000..1b3617c4a02 --- /dev/null +++ b/jac/tests/compiler/backends/native/test_native_boxed_return_lifetime.jac @@ -0,0 +1,12 @@ +"""Boxed reference returns survive release of the caller's temporary argument.""" + +import ctypes; +import from tests.support { compile_native } + +test "iteration keeps a boxed getter result alive" { + (engine, _) = compile_native("boxed_return_lifetime.jac"); + probe = ctypes.CFUNCTYPE(ctypes.c_int64)(engine.get_function_address("probe")); + for i in range(20) { + assert probe() == 10; + } +} diff --git a/jac/tests/compiler/backends/native/test_native_enum_ctor.jac b/jac/tests/compiler/backends/native/test_native_enum_ctor.jac index 1fbe34a93b5..d3491d1e4bf 100644 --- a/jac/tests/compiler/backends/native/test_native_enum_ctor.jac +++ b/jac/tests/compiler/backends/native/test_native_enum_ctor.jac @@ -13,13 +13,9 @@ the declared table, so the emitter tests the argument against it and calls the same raise primitive `int()` uses for a bad literal. The call then returns the value it validated, which is the member's runtime form. -What stays refused is named rather than generic. A string-valued enum's -runtime form is the member's *ordinal*, so the lookup is not the identity, and -a member whose value no fold reaches -- `ParameterCategory`'s `auto()` -- has -no table to validate against at all. Both refuse under E5092 with a help text -that says which of the two it is; `EnumLayout.all_values_known` is what makes -the second answerable rather than guessed, because the layout builder already -skipped the members it could not fold. +String-valued enums look up the declared value and return its ordinal. +Members whose values cannot be folded still have no table to validate against, +and noninteger inputs to integer enums remain refused under E5092. The subprocess pair is the contract: the server pathway and a native binary print the same lines, and CPython's own answers for the same six calls are @@ -148,16 +144,25 @@ test "declared and undeclared values answer as CPython does" { } -test "a string-valued enum constructor refuses by name" { +test "a noninteger input to an integer enum refuses by name" { seams = seam_report("xmod_demote_dyn_lib.jac"); assert "enum constructor 'Hue'" in seams , ( - f"the string-valued spelling lowered silently:\n{seams}" + f"the floating-point input lowered silently:\n{seams}" ); assert "from_code" in seams , ( f"the refusal did not demote the callable that owns it:\n{seams}" ); } +test "string enum constructors resolve values and reject missing members" { + import ctypes; + import from tests.support { get_func } + (prog, ir) = compile_native("enum_ctor_string.jac"); + assert not prog.errors_had , f"{[str(e) for e in prog.errors_had]}"; + probe = get_func(ir.gen.native_engine, "probe", ctypes.c_int64, ctypes.c_int64); + assert [probe(i) for i in range(4)] == [1, 1, 1, -1]; +} + test "a same-named local function is not read as the enum" { (prog, ir) = compile_native("enum_ctor_shadow_main.jac"); diff --git a/jac/tests/compiler/backends/native/test_native_osp_runtime_import.jac b/jac/tests/compiler/backends/native/test_native_osp_runtime_import.jac new file mode 100644 index 00000000000..30ba7fa12bf --- /dev/null +++ b/jac/tests/compiler/backends/native/test_native_osp_runtime_import.jac @@ -0,0 +1,12 @@ +"""Explicit runtime imports share the kernel used by native walker dispatch.""" + +import ctypes; +import from tests.support { compile_native } + +test "an imported walker operation acts on the active kernel" { + (engine, _) = compile_native("osp_runtime_import.jac"); + address = engine.get_function_address("probe"); + assert address != 0; + probe = ctypes.CFUNCTYPE(ctypes.c_int64)(address); + assert probe() == 1; +} diff --git a/jac/tests/compiler/backends/native/test_native_type_identity.jac b/jac/tests/compiler/backends/native/test_native_type_identity.jac new file mode 100644 index 00000000000..e0b4b250f4b --- /dev/null +++ b/jac/tests/compiler/backends/native/test_native_type_identity.jac @@ -0,0 +1,10 @@ +"""Exact archetype identity survives native module boundaries.""" + +import ctypes; +import from tests.support { compile_native, get_func } + +test "type identity recognizes imported instances and rejects subclasses" { + (engine, _) = compile_native("type_identity.jac"); + probe = get_func(engine, "probe", ctypes.c_int64); + assert probe() == 0; +} diff --git a/jac/tests/compiler/backends/native/test_native_union_properties.jac b/jac/tests/compiler/backends/native/test_native_union_properties.jac new file mode 100644 index 00000000000..b78484af91a --- /dev/null +++ b/jac/tests/compiler/backends/native/test_native_union_properties.jac @@ -0,0 +1,14 @@ +"""Union property access uses each concrete class's accessor and layout.""" + +import ctypes; +import from tests.support { compile_native } + +test "unrelated union members dispatch their collection properties" { + (engine, _) = compile_native("union_properties.jac"); + address = engine.get_function_address("probe"); + assert address != 0; + probe = ctypes.CFUNCTYPE(ctypes.c_int64, ctypes.c_int64)(address); + assert probe(0) == 7; + assert probe(1) == 10; + assert probe(2) == 22; +} diff --git a/jac/tests/compiler/backends/native/test_native_walker_base_dispatch.jac b/jac/tests/compiler/backends/native/test_native_walker_base_dispatch.jac new file mode 100644 index 00000000000..ebb8f4cdff8 --- /dev/null +++ b/jac/tests/compiler/backends/native/test_native_walker_base_dispatch.jac @@ -0,0 +1,14 @@ +"""Inherited methods spawn the concrete walker across module boundaries.""" + +import ctypes; +import from tests.support { compile_native } + +test "base method dispatches derived walker" { + (engine, _) = compile_native("walker_base_dispatch.jac"); + probe = ctypes.CFUNCTYPE(ctypes.c_int64)(engine.get_function_address("probe")); + assert probe() == 121; + overridden = ctypes.CFUNCTYPE(ctypes.c_int64)( + engine.get_function_address("override_probe") + ); + assert overridden() == 91; +} diff --git a/jac/tests/compiler/test_layout_import_order.jac b/jac/tests/compiler/test_layout_import_order.jac new file mode 100644 index 00000000000..794428f062b --- /dev/null +++ b/jac/tests/compiler/test_layout_import_order.jac @@ -0,0 +1,35 @@ +"""Imported layouts are completed before their children adopt their slots.""" + +import from jaclang.compiler.frontend.parser.frontend { parse } +import from jaclang.compiler.passes.layout_pass { + ArchetypeLayout, + FieldInfo, + LayoutRegistry, + adopt_imported_bases +} + +test "partial imported MROs still adopt fields in ancestor order" { + (mod, failed) = parse("", "layout_order.jac"); + assert not failed; + reg = LayoutRegistry(); + mod.gen.layout_registry = reg; + for (name, parent, field) in [ + ("Leaf", "Middle", "leaf"), + ("Middle", "Base", "middle"), + ("Base", "", "base") + ] { + mro = [name, parent] if parent else [name]; + reg.layouts[name] = ArchetypeLayout( + name=name, + arch_type="obj", + parents=[parent] if parent else [], + mro=mro, + fields=[FieldInfo(name=field, index=0, type_tag="int", owner=name)], + field_index={field: 0} + ); + reg.mro_order[name] = mro; + } + adopt_imported_bases(mod); + assert reg.layouts["Leaf"].field_index == {"base": 0, "middle": 1, "leaf": 2}; + assert reg.layouts["Middle"].field_index == {"base": 0, "middle": 1}; +} diff --git a/jac/tests/compiler/test_native_early_passes.jac b/jac/tests/compiler/test_native_early_passes.jac new file mode 100644 index 00000000000..6fdf367e721 --- /dev/null +++ b/jac/tests/compiler/test_native_early_passes.jac @@ -0,0 +1,122 @@ +"""Early native analysis preserves scopes, diagnostics, and pass scheduling.""" + +import from jaclang.compiler.driver.program { JacProgram } +import from jaclang.compiler.driver.pass_driver { run_pass } +import from jaclang.compiler.frontend.parser.frontend { parse, parse_woven } +import from jaclang.compiler.frontend.unitree { Module, UniScopeNode } +import from jaclang.compiler.native_compiler { ensure_loaded } +import from jaclang.compiler.passes.ast_validation_pass { ASTValidationPass } +import from jaclang.compiler.passes.sym_tab_build_pass { SymTabBuildPass } + +glob SOURCE: str = """ +obj Item { + has value: int = 0; + def add(amount: int) -> int { + result = self.value + amount; + return result; + } +} +def total(items: list[int]) -> int { + result = 0; + for item in items { + result += item; + } + if result > 0 { + copies = [item for item in items]; + result += len(copies); + } + return result; +} +@overload def choose(value: int) -> int; +@overload def choose(value: str) -> str; +"""; + +def scope_profile(mod: Module) -> list { + out: list = []; + for scope in [mod] + + [ + n + for n in mod._in_mod_nodes + if isinstance(n, UniScopeNode) and n is not mod + ] { + names: list = []; + for (name, symbol) in scope.names_in_scope.items() { + names.append((name, symbol.imported, [d.sym_name for d in symbol.defn])); + assert symbol.parent_tab is scope; + assert symbol.provider is None; + } + overloads: list = []; + for (name, symbols) in scope.names_in_scope_overload.items() { + overloads.append((name, [[d.sym_name for d in s.defn] for s in symbols])); + for symbol in symbols { + assert symbol.parent_tab is scope; + } + } + out.append( + ( + type(scope).__name__, + scope.scope_name, + scope.parent_scope.scope_name if scope.parent_scope else None, + [kid.scope_name for kid in scope.kid_scope], + sorted(names), + sorted(overloads) + ) + ); + } + return out; +} + +test "native early passes preserve the store parser's symbol graph" { + kernel = ensure_loaded(); + if kernel is not None { + assert kernel.supports_early , "rebuild the compiler kernel for early analysis"; + native = JacProgram(); + (nat_mod, failed) = parse_woven( + SOURCE, "early_scopes.jac", native, [], early_context="server" + ); + assert not failed; + assert "SymTabBuildPass" in native.analysis.early_passes[id(nat_mod)].results; + before = scope_profile(nat_mod); + run_pass(ASTValidationPass, nat_mod, native); + run_pass(SymTabBuildPass, nat_mod, native); + assert id(nat_mod) not in native.analysis.early_passes; + assert native.analysis.pass_runs["SymTabBuildPass"] == 1; + assert scope_profile(nat_mod) == before; + + store = JacProgram(); + (store_mod, store_failed) = parse(SOURCE, "early_scopes.jac", store); + assert not store_failed; + run_pass(ASTValidationPass, store_mod, store); + run_pass(SymTabBuildPass, store_mod, store); + assert scope_profile(nat_mod) == scope_profile(store_mod); + assert not native.errors_had; + assert not store.errors_had; + } +} + +test "native validation diagnostics retain inline suppression" { + kernel = ensure_loaded(); + if kernel is not None { + assert kernel.supports_early; + for (suffix, expected) in [("", 1), (" # jac:ignore[E0052]", 0)] { + prog = JacProgram(); + source = "def missing(value) -> int { return 1; }" + suffix; + (mod, failed) = parse_woven( + source, "early_diagnostic.jac", prog, [], early_context="server" + ); + assert not failed; + run_pass(ASTValidationPass, mod, prog); + errors = [ + e + for e in prog.errors_had + if e.code and e.code.code == "E0052" + ]; + assert len(errors) == expected; + if errors { + assert errors[0].from_pass is ASTValidationPass; + assert errors[0].loc.mod_path == "early_diagnostic.jac"; + assert errors[0].loc.first_line == 1; + } + } + } +} diff --git a/jac/tests/runtimelib/test_osp_tag.jac b/jac/tests/runtimelib/test_osp_tag.jac new file mode 100644 index 00000000000..2bc2719252d --- /dev/null +++ b/jac/tests/runtimelib/test_osp_tag.jac @@ -0,0 +1,20 @@ +"""ASCII compiler type tags retain their values across compiler backends.""" + +import from jaclang.runtime.osp_tag { stable_osp_tag } + +test "bounded tag arithmetic matches known reference hashes" { + # Generated with the original unbounded FNV-1a calculation, masked to 53 bits. + # Fixed vectors let this test itself run with native checked integers. + for (name, expected) in [ + ("", 5239054864098085), + ("Alert", 3459937832554201), + ("Module", 4351073476296893), + ("Symbol", 1077154556107505), + ("TreeWalker", 500708216478327), + ("ASTValidationPass", 1000785363432933), + ("SymTabBuildPass", 4500574274975052) + ] { + actual = stable_osp_tag(name); + assert actual == expected , f"{name}: expected {expected}, got {actual}"; + } +} diff --git a/scripts/native_compile_bench.py b/scripts/native_compile_bench.py new file mode 100644 index 00000000000..821b6a91486 --- /dev/null +++ b/scripts/native_compile_bench.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +"""Measure uncached AOT builds with a warm compiler. + + JAC_COMPILER_LIB=/path/to/kernel.so jac scripts/native_compile_bench.py \ + jac/examples/chess/chess.jac --rounds 5 + +Run each kernel in a fresh process. Compiler startup and warmup are excluded; +each measured build reparses the application and emits and links an executable. +""" + +import argparse +import gc +import json +import os +import statistics +import tempfile +import time +from pathlib import Path + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("source", type=Path) + parser.add_argument("--rounds", type=int, default=5) + parser.add_argument("--warmups", type=int, default=2) + parser.add_argument("--output", type=Path) + args = parser.parse_args() + if args.rounds < 1 or args.warmups < 1: + parser.error("rounds and warmups must be positive") + + from jaclang.cli.commands.nacompile import nacompile + from jaclang.compiler.native_compiler import ensure_loaded + + kernel = ensure_loaded() + if kernel is None: + parser.error("set JAC_COMPILER_LIB to a built compiler kernel") + times = [] + with tempfile.TemporaryDirectory(prefix="jac-compile-bench-") as directory: + output = args.output or Path(directory) / "program" + for round_number in range(-args.warmups, args.rounds): + gc.collect() + start = time.perf_counter() + result = nacompile(str(args.source.resolve()), output=str(output), scrub=True) + elapsed = time.perf_counter() - start + if result: + raise RuntimeError(f"compile failed in round {round_number}") + print(json.dumps({"round": round_number, "seconds": elapsed}), flush=True) + if round_number >= 0: + times.append(elapsed) + print(json.dumps({ + "source": str(args.source), + "kernel": os.environ.get("JAC_COMPILER_LIB", "auto"), + "early_passes": kernel.supports_early, + "seconds": times, + "median_seconds": statistics.median(times), + "min_seconds": min(times), + "max_seconds": max(times), + }), flush=True) + + +if __name__ == "__main__": + main() From 7a110b66279203f2937fe71af30731ff7d8aad95 Mon Sep 17 00:00:00 2001 From: marsninja Date: Sun, 6 Sep 2026 10:26:26 -0400 Subject: [PATCH 2/6] docs: add release note for native compiler analysis --- release_notes/unreleased/jaclang/8996.bugfix.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 release_notes/unreleased/jaclang/8996.bugfix.md diff --git a/release_notes/unreleased/jaclang/8996.bugfix.md b/release_notes/unreleased/jaclang/8996.bugfix.md new file mode 100644 index 00000000000..4feaed2538b --- /dev/null +++ b/release_notes/unreleased/jaclang/8996.bugfix.md @@ -0,0 +1 @@ +- **Fix: Faster native compiler analysis**: Run AST validation and symbol-table construction in the native compiler kernel while preserving diagnostics and symbol relationships, and fix native inherited walker dispatch, cross-module type identity, union property access, and boxed return lifetimes. From c6c667a8f17545b3dd897320189dd4d19c80ff67 Mon Sep 17 00:00:00 2001 From: marsninja Date: Sun, 6 Sep 2026 10:39:09 -0400 Subject: [PATCH 3/6] tools: convert native compile benchmark to Jac --- jac/jaclang/compiler/README.md | 2 +- scripts/native_compile_bench.jac | 77 ++++++++++++++++++++++++++++++++ scripts/native_compile_bench.py | 62 ------------------------- 3 files changed, 78 insertions(+), 63 deletions(-) create mode 100644 scripts/native_compile_bench.jac delete mode 100644 scripts/native_compile_bench.py diff --git a/jac/jaclang/compiler/README.md b/jac/jaclang/compiler/README.md index f2663802783..1b3af1f079f 100644 --- a/jac/jaclang/compiler/README.md +++ b/jac/jaclang/compiler/README.md @@ -43,7 +43,7 @@ and reference-container layouts come from the backend's ABI metadata; Keep pass algorithms in `passes/`, and extend this shared boundary when another pass moves into the kernel. -`scripts/native_compile_bench.py` at the repository root measures uncached AOT +`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. diff --git a/scripts/native_compile_bench.jac b/scripts/native_compile_bench.jac new file mode 100644 index 00000000000..40d5ef3525f --- /dev/null +++ b/scripts/native_compile_bench.jac @@ -0,0 +1,77 @@ +"""Measure uncached AOT builds with a warm compiler. + + JAC_COMPILER_LIB=/path/to/kernel.so jac scripts/native_compile_bench.jac \ + jac/examples/chess/chess.jac --rounds 5 + +Run each kernel in a fresh process. Compiler startup and warmup are excluded; +each measured build reparses the application and emits and links an executable. +""" + +import argparse; +import gc; +import json; +import os; +import statistics; +import tempfile; +import time; +import from pathlib { Path } + +def main -> None { + parser = argparse.ArgumentParser( + description="Measure uncached AOT builds with a warm compiler." + ); + parser.add_argument("source", type=Path); + parser.add_argument("--rounds", type=int, `default=5); + parser.add_argument("--warmups", type=int, `default=2); + parser.add_argument("--output", type=Path); + args = parser.parse_args(); + if args.rounds < 1 or args.warmups < 1 { + parser.error("rounds and warmups must be positive"); + } + + import from jaclang.cli.commands.nacompile { nacompile } + import from jaclang.compiler.native_compiler { ensure_loaded } + + kernel = ensure_loaded(); + if kernel is None { + parser.error("set JAC_COMPILER_LIB to a built compiler kernel"); + return; + } + times: list[float] = []; + with tempfile.TemporaryDirectory(prefix="jac-compile-bench-") as directory { + output = args.output or Path(directory) / "program"; + for round_number in range(-args.warmups, args.rounds) { + gc.collect(); + start = time.perf_counter(); + result = nacompile( + str(args.source.resolve()), output=str(output), scrub=True + ); + elapsed = time.perf_counter() - start; + if result { + raise RuntimeError(f"compile failed in round {round_number}"); + } + print(json.dumps({"round": round_number, "seconds": elapsed}), flush=True); + if round_number >= 0 { + times.append(elapsed); + } + } + } + print( + json.dumps( + { + "source": str(args.source), + "kernel": os.environ.get("JAC_COMPILER_LIB", "auto"), + "early_passes": kernel.supports_early, + "seconds": times, + "median_seconds": statistics.median(times), + "min_seconds": min(times), + "max_seconds": max(times) + } + ), + flush=True + ); +} + +with entry:__main__ { + main(); +} diff --git a/scripts/native_compile_bench.py b/scripts/native_compile_bench.py deleted file mode 100644 index 821b6a91486..00000000000 --- a/scripts/native_compile_bench.py +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env python3 -"""Measure uncached AOT builds with a warm compiler. - - JAC_COMPILER_LIB=/path/to/kernel.so jac scripts/native_compile_bench.py \ - jac/examples/chess/chess.jac --rounds 5 - -Run each kernel in a fresh process. Compiler startup and warmup are excluded; -each measured build reparses the application and emits and links an executable. -""" - -import argparse -import gc -import json -import os -import statistics -import tempfile -import time -from pathlib import Path - - -def main() -> None: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("source", type=Path) - parser.add_argument("--rounds", type=int, default=5) - parser.add_argument("--warmups", type=int, default=2) - parser.add_argument("--output", type=Path) - args = parser.parse_args() - if args.rounds < 1 or args.warmups < 1: - parser.error("rounds and warmups must be positive") - - from jaclang.cli.commands.nacompile import nacompile - from jaclang.compiler.native_compiler import ensure_loaded - - kernel = ensure_loaded() - if kernel is None: - parser.error("set JAC_COMPILER_LIB to a built compiler kernel") - times = [] - with tempfile.TemporaryDirectory(prefix="jac-compile-bench-") as directory: - output = args.output or Path(directory) / "program" - for round_number in range(-args.warmups, args.rounds): - gc.collect() - start = time.perf_counter() - result = nacompile(str(args.source.resolve()), output=str(output), scrub=True) - elapsed = time.perf_counter() - start - if result: - raise RuntimeError(f"compile failed in round {round_number}") - print(json.dumps({"round": round_number, "seconds": elapsed}), flush=True) - if round_number >= 0: - times.append(elapsed) - print(json.dumps({ - "source": str(args.source), - "kernel": os.environ.get("JAC_COMPILER_LIB", "auto"), - "early_passes": kernel.supports_early, - "seconds": times, - "median_seconds": statistics.median(times), - "min_seconds": min(times), - "max_seconds": max(times), - }), flush=True) - - -if __name__ == "__main__": - main() From 1af1df68d9d6920e0c3f684b52621a2b97fdd259 Mon Sep 17 00:00:00 2001 From: marsninja Date: Sun, 6 Sep 2026 11:57:01 -0400 Subject: [PATCH 4/6] fix(compiler): preserve TargetData factory typing during sealing --- .../backends/native/llvm/binding/targets.jac | 2 +- .../native/test_native_abi_layout.jac | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/jac/jaclang/compiler/backends/native/llvm/binding/targets.jac b/jac/jaclang/compiler/backends/native/llvm/binding/targets.jac index 253d2f6e84a..9b04e1428f3 100644 --- a/jac/jaclang/compiler/backends/native/llvm/binding/targets.jac +++ b/jac/jaclang/compiler/backends/native/llvm/binding/targets.jac @@ -96,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))); } diff --git a/jac/tests/compiler/backends/native/test_native_abi_layout.jac b/jac/tests/compiler/backends/native/test_native_abi_layout.jac index 303ca8480df..03c23bc33a4 100644 --- a/jac/tests/compiler/backends/native/test_native_abi_layout.jac +++ b/jac/tests/compiler/backends/native/test_native_abi_layout.jac @@ -2,6 +2,30 @@ import from jaclang.compiler.backends.native.abi { struct_layouts } import from jaclang.compiler.backends.native.llvm { binding, ir } +import from jaclang.compiler.driver.compile_options { CompileOptions } +import from jaclang.compiler.driver.program { JacProgram } + +test "target data retains its type through the context manager" { + source = """ +import from jaclang.compiler.backends.native.llvm.binding.targets { create_target_data } +import from jaclang.compiler.backends.native.llvm.binding.value { ValueRef } + +def layout(value: ValueRef) -> tuple[list[int], int] { + with create_target_data("e-p:64:64") as data { + return data.get_global_struct_layout(value, 1); + } +} +"""; + prog = JacProgram(); + prog.compile( + use_str=source, + file_path="abi_type_contract.jac", + options=CompileOptions( + no_cgen=True, no_ir_cache=True, force_target_program=True + ) + ); + assert not prog.errors_had , str(prog.errors_had); +} test "a nullable boolean does not add eight bytes of padding" { binding.initialize_native_target(); From 21857129561672c28b9a014d045fb58616ef2c2a Mon Sep 17 00:00:00 2001 From: marsninja Date: Sun, 6 Sep 2026 12:44:49 -0400 Subject: [PATCH 5/6] Fix native analysis context, runtime imports, and walker cleanup --- .../compiler/backends/common/kernel_units.jac | 5 +++- .../native/impl/na_compile_pass.impl.jac | 4 ++- .../native/na_ir_gen_pass.impl/core.impl.jac | 2 +- .../native/na_ir_gen_pass.impl/enums.impl.jac | 2 +- .../native/na_ir_gen_pass.impl/expr.impl.jac | 5 ++-- .../na_ir_gen_pass.impl/objects.impl.jac | 2 -- .../native/na_ir_gen_pass.impl/osp.impl.jac | 3 +-- .../native/na_ir_gen_pass.impl/stmt.impl.jac | 3 --- .../na_ir_gen_pass.impl/vtable.impl.jac | 2 +- .../backends/native/na_ir_gen_pass.jac | 1 + .../frontend/unitree.impl/nodes.impl.jac | 16 +++++++----- jac/jaclang/compiler/jc_unit.jac | 2 +- jac/jaclang/compiler/native_compiler.jac | 2 +- jac/jaclang/compiler/passes/transform.jac | 1 - jac/jaclang/runtime/osp_tag.jac | 2 -- .../native/test_native_osp_runtime_import.jac | 13 ++++++++++ .../compiler/test_native_early_passes.jac | 25 +++++++++++++++++++ 17 files changed, 64 insertions(+), 26 deletions(-) diff --git a/jac/jaclang/compiler/backends/common/kernel_units.jac b/jac/jaclang/compiler/backends/common/kernel_units.jac index 07371e73281..8f19e21658c 100644 --- a/jac/jaclang/compiler/backends/common/kernel_units.jac +++ b/jac/jaclang/compiler/backends/common/kernel_units.jac @@ -26,10 +26,13 @@ def _jaclang_dir -> str { return os.path.dirname(str(jaclang.__file__)); } -def kernel_unit_owner(path: str) -> str { +def kernel_unit_owner(path: str, module_name: str = "") -> str { """Canonical native unit supplying a directly imported runtime source."""; 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"; } diff --git a/jac/jaclang/compiler/backends/native/impl/na_compile_pass.impl.jac b/jac/jaclang/compiler/backends/native/impl/na_compile_pass.impl.jac index 43b388c2f84..3b0301df0da 100644 --- a/jac/jaclang/compiler/backends/native/impl/na_compile_pass.impl.jac +++ b/jac/jaclang/compiler/backends/native/impl/na_compile_pass.impl.jac @@ -738,7 +738,9 @@ 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); + _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 { diff --git a/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/core.impl.jac b/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/core.impl.jac index ffca323de58..38276dd3a81 100644 --- a/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/core.impl.jac +++ b/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/core.impl.jac @@ -634,7 +634,7 @@ impl NaIRGenPass._register_imported_struct_types -> None { continue; } - _kernel_owner = kernel_unit_owner(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); diff --git a/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/enums.impl.jac b/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/enums.impl.jac index 31d141bf164..12cca0054ed 100644 --- a/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/enums.impl.jac +++ b/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/enums.impl.jac @@ -380,7 +380,7 @@ impl NaIRGenPass._codegen_enum_ctor( } i64 = ir.IntType(64); ordinal: ir.Value = ir.Constant(i64, -1); - # Reverse selection gives aliases the first declared member's ordinal. + for member in reversed(lay.members) { text = self._make_global_string( member.str_value or "", name_prefix=".enum.value" diff --git a/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/expr.impl.jac b/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/expr.impl.jac index 617e1e7db0d..8270442fe15 100644 --- a/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/expr.impl.jac +++ b/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/expr.impl.jac @@ -761,8 +761,6 @@ impl NaIRGenPass._codegen_kernel_spawn( _h["append"], [_starts, self._coerce_type(loc_val, i64)], op="append", loc="" ); - # A base-class method can spawn a derived walker. Dispatch through the - # instance tag, just as node callbacks do, rather than its static type. _tag_slot = self.osp_type_tag_field.get(walker_name); if _tag_slot is None { return None; @@ -795,7 +793,7 @@ impl NaIRGenPass._codegen_kernel_spawn( ); } _rt = self.builder.load(self._osp_rt_global, name="spawn.rt"); - self.builder.call( + _spawn_result = self.builder.call( osp_spawn_fn, [ self._coerce_type(_rt, _argtys[0]), @@ -804,6 +802,7 @@ impl NaIRGenPass._codegen_kernel_spawn( self._coerce_type(_starts, _argtys[3]) ] ); + self._emit_elem_release(self.builder, _spawn_result.type, _spawn_result); self._emit_rc_release_simple(_starts, op="spawn_starts", loc=""); return walker_val; } diff --git a/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/objects.impl.jac b/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/objects.impl.jac index 27fabfd3594..1cfcd5eeff0 100644 --- a/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/objects.impl.jac +++ b/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/objects.impl.jac @@ -1987,8 +1987,6 @@ impl NaIRGenPass._codegen_instantiation( _tt_idx = self.osp_type_tag_field.get(type_name); if _tt_idx is not None { - # Constructors may enter inherited walker methods in another module. - # Register this module's concrete descriptors before that can happen. if self._osp_ensure_fn is not None { self.builder.call(self._osp_ensure_fn, []); } diff --git a/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/osp.impl.jac b/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/osp.impl.jac index 5095fd439a0..95eeba00dd4 100644 --- a/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/osp.impl.jac +++ b/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/osp.impl.jac @@ -209,8 +209,7 @@ impl NaIRGenPass._osp_collect_slots(arch_name: str, is_entry: bool) -> list { if _model is None { return slots; } - # Match host event dispatch: merge base events in reverse MRO order, - # replacing overridden abilities without changing their dispatch position. + merged: dict = {}; layout = self._arch_layout(arch_name); order = layout.mro if layout is not None else [arch_name]; diff --git a/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/stmt.impl.jac b/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/stmt.impl.jac index 46f4811c6ff..0b6cbbed14f 100644 --- a/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/stmt.impl.jac +++ b/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/stmt.impl.jac @@ -441,9 +441,6 @@ impl NaIRGenPass._codegen_return(nd: uni.ReturnStmt) -> None { and self?.jacval_type and val.type == self.jacval_type and not self._is_owned(val) { - # A boxed return still owns its reference payload. In - # particular, callers may release a temporary argument as - # soon as this function returns it through a union or any. self._emit_elem_retain(self.builder, val.type, val); self._mark_owned(val); } diff --git a/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/vtable.impl.jac b/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/vtable.impl.jac index bff70d3f3d0..2e5adcdfe57 100644 --- a/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/vtable.impl.jac +++ b/jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/vtable.impl.jac @@ -396,7 +396,7 @@ impl NaIRGenPass._emit_class_name_const(arch_name: str) -> ir.Value { global_str = ir.GlobalVariable( self.llvm_module, struct_type, name=f".clsname.{arch_name}" ); - # Class references and vtables must carry the same identity across units. + global_str.linkage = "linkonce_odr"; global_str.global_constant = True; global_str.initializer = struct_val; diff --git a/jac/jaclang/compiler/backends/native/na_ir_gen_pass.jac b/jac/jaclang/compiler/backends/native/na_ir_gen_pass.jac index fbc25137578..3062c81225d 100644 --- a/jac/jaclang/compiler/backends/native/na_ir_gen_pass.jac +++ b/jac/jaclang/compiler/backends/native/na_ir_gen_pass.jac @@ -1341,6 +1341,7 @@ walker NaIRGenPass(Transform) { def _jc_reference_schema( ft: ir.Type, layouts: dict, semantic: object = None ) -> (dict | None); + def _codegen_union_member( obj_val: ir.Value, field_name: str, nd: uni.AtomTrailer, parts: list[str] ) -> (ir.Value | None); diff --git a/jac/jaclang/compiler/frontend/unitree.impl/nodes.impl.jac b/jac/jaclang/compiler/frontend/unitree.impl/nodes.impl.jac index 9ea5071142e..81f671ad5a0 100644 --- a/jac/jaclang/compiler/frontend/unitree.impl/nodes.impl.jac +++ b/jac/jaclang/compiler/frontend/unitree.impl/nodes.impl.jac @@ -1934,10 +1934,11 @@ impl ContextAwareNode.postinit -> None {} impl ContextAwareNode.code_context.getter -> CodeContext { found = [self->:PlacedIn:->]; - target = found[0] if found else None; - return CodeContext(target.kind) - if isinstance(target, Codespace) - else CodeContext.SERVER; + if found { + target: Codespace = found[0]; + return CodeContext(target.kind); + } + return CodeContext.SERVER; } impl ContextAwareNode.code_context.setter(code_context: CodeContext) { @@ -1947,8 +1948,11 @@ impl ContextAwareNode.code_context.setter(code_context: CodeContext) { impl Module.decided_codespace.getter -> str { found = [self->:DecidedCodespace:->]; - target = found[0] if found else None; - return target.kind if isinstance(target, Codespace) else ""; + if found { + target: Codespace = found[0]; + return target.kind; + } + return ""; } impl Module.decided_codespace.setter(decided_codespace: str) { diff --git a/jac/jaclang/compiler/jc_unit.jac b/jac/jaclang/compiler/jc_unit.jac index 5492d34ac69..9db9f0758da 100644 --- a/jac/jaclang/compiler/jc_unit.jac +++ b/jac/jaclang/compiler/jc_unit.jac @@ -188,7 +188,7 @@ def _finish_unit( break; } } - # Wildcard imports need the driver's dependency resolver before binding. + if not has_absorb { symbols = SymTabBuildPass(ir_in=mod); _JC.completed = 3; diff --git a/jac/jaclang/compiler/native_compiler.jac b/jac/jaclang/compiler/native_compiler.jac index 388b0e04844..1d8a1b22bd9 100644 --- a/jac/jaclang/compiler/native_compiler.jac +++ b/jac/jaclang/compiler/native_compiler.jac @@ -412,7 +412,7 @@ class NativeCompiler { return self._call_str2("jc_run", source, path); } - def prepare_early(self: NativeCompiler, context: str) -> None { + def prepare_early(self: NativeCompiler, context: str) { if self.supports_early { self.lib.jc_set_early( {"server": 1, "client": 2, "native": 3}.get(context, 0) diff --git a/jac/jaclang/compiler/passes/transform.jac b/jac/jaclang/compiler/passes/transform.jac index 5168554fcd2..6f8df5bd8b3 100644 --- a/jac/jaclang/compiler/passes/transform.jac +++ b/jac/jaclang/compiler/passes/transform.jac @@ -44,7 +44,6 @@ obj DiagnosticPolicy { ) -> bool; } -"""Diagnostics and timing from a pass already applied to an IR unit.""" obj PassResult { has errors_had: list[Alert] = [], warnings_had: list[Alert] = [], diff --git a/jac/jaclang/runtime/osp_tag.jac b/jac/jaclang/runtime/osp_tag.jac index 26bd36ed797..25a2b1a077f 100644 --- a/jac/jaclang/runtime/osp_tag.jac +++ b/jac/jaclang/runtime/osp_tag.jac @@ -1,6 +1,4 @@ def stable_osp_tag(name: str) -> int { - # Only the low 53 bits form a tag. Split multiplication into limbs so - # the same FNV hash also works with checked native 64-bit integers. _h = 5239054864098085; for _c in name { _h = _h ^ ord(_c); diff --git a/jac/tests/compiler/backends/native/test_native_osp_runtime_import.jac b/jac/tests/compiler/backends/native/test_native_osp_runtime_import.jac index 30ba7fa12bf..a732cce50a6 100644 --- a/jac/tests/compiler/backends/native/test_native_osp_runtime_import.jac +++ b/jac/tests/compiler/backends/native/test_native_osp_runtime_import.jac @@ -2,6 +2,19 @@ import ctypes; import from tests.support { compile_native } +import from jaclang.compiler.backends.common.kernel_units { kernel_unit_owner } + +test "runtime unit identity is independent of the compiler installation" { + assert kernel_unit_owner( + "/checkout/jaclang/runtime/osp_kernel.jac", "jaclang.runtime.osp_kernel" + ) + == "osp_kernel"; + assert kernel_unit_owner( + "/checkout/jaclang/runtime/osp_graph.jac", "jaclang.runtime.osp_graph" + ) + == "osp_kernel"; + assert kernel_unit_owner("/checkout/osp_kernel.jac", "app.osp_kernel") == ""; +} test "an imported walker operation acts on the active kernel" { (engine, _) = compile_native("osp_runtime_import.jac"); diff --git a/jac/tests/compiler/test_native_early_passes.jac b/jac/tests/compiler/test_native_early_passes.jac index 6fdf367e721..5b53a970681 100644 --- a/jac/tests/compiler/test_native_early_passes.jac +++ b/jac/tests/compiler/test_native_early_passes.jac @@ -8,6 +8,31 @@ import from jaclang.compiler.native_compiler { ensure_loaded } import from jaclang.compiler.passes.ast_validation_pass { ASTValidationPass } import from jaclang.compiler.passes.sym_tab_build_pass { SymTabBuildPass } +test "native validation preserves client code context" { + kernel = ensure_loaded(); + if kernel is not None { + assert kernel.supports_early; + for (context, expected) in [("client", 0), ("server", 1)] { + prog = JacProgram(); + (mod, failed) = parse_woven( + "def f(`from: str) -> str { return `from; }", + "early_context.jac", + prog, + [], + early_context=context + ); + assert not failed; + run_pass(ASTValidationPass, mod, prog); + errors = [ + e + for e in prog.errors_had + if e.code and e.code.code == "E0067" + ]; + assert len(errors) == expected; + } + } +} + glob SOURCE: str = """ obj Item { has value: int = 0; From 04261c38137c2ec6928c66965821827ba63c24e1 Mon Sep 17 00:00:00 2001 From: marsninja Date: Sun, 6 Sep 2026 13:15:03 -0400 Subject: [PATCH 6/6] Resolve runtime source ownership and tighten compiler annotations --- jac/jaclang/compiler/backends/common/kernel_units.jac | 9 +++++++++ jac/jaclang/compiler/native_compiler.jac | 3 ++- jac/jaclang/compiler/passes/layout_pass.jac | 3 ++- .../backends/native/test_native_osp_runtime_import.jac | 5 ++++- 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/jac/jaclang/compiler/backends/common/kernel_units.jac b/jac/jaclang/compiler/backends/common/kernel_units.jac index 8f19e21658c..616d3382283 100644 --- a/jac/jaclang/compiler/backends/common/kernel_units.jac +++ b/jac/jaclang/compiler/backends/common/kernel_units.jac @@ -28,6 +28,7 @@ def _jaclang_dir -> str { 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("/", ".") { @@ -36,6 +37,14 @@ def kernel_unit_owner(path: str, module_name: str = "") -> str { 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 ""; } diff --git a/jac/jaclang/compiler/native_compiler.jac b/jac/jaclang/compiler/native_compiler.jac index 1d8a1b22bd9..1e88b39e567 100644 --- a/jac/jaclang/compiler/native_compiler.jac +++ b/jac/jaclang/compiler/native_compiler.jac @@ -429,7 +429,8 @@ class NativeCompiler { return out; } completed = self.lib.jc_completed(); - for (pass_id, pass_cls) in [(1, ASTValidationPass), (2, SymTabBuildPass)] { + passes: list[tuple[int, type]] = [(1, ASTValidationPass), (2, SymTabBuildPass)]; + for (pass_id, pass_cls) in passes { if not (completed & pass_id) { continue; } diff --git a/jac/jaclang/compiler/passes/layout_pass.jac b/jac/jaclang/compiler/passes/layout_pass.jac index e87f89511b2..aee917d138c 100644 --- a/jac/jaclang/compiler/passes/layout_pass.jac +++ b/jac/jaclang/compiler/passes/layout_pass.jac @@ -8,6 +8,7 @@ import from jaclang.compiler.frontend.unitree { BinaryExpr, BuiltinType, Enum, + Expr, FuncCall, FuncSignature, HasVar, @@ -146,7 +147,7 @@ def layout_slot_count(lay: ArchetypeLayout) -> int { return top; } -def declaring_module_path(base_expr: UniNode) -> str { +def declaring_module_path(base_expr: Expr) -> str { import os; import from jaclang.compiler.types.types { ClassType } base_type = base_expr.type; diff --git a/jac/tests/compiler/backends/native/test_native_osp_runtime_import.jac b/jac/tests/compiler/backends/native/test_native_osp_runtime_import.jac index a732cce50a6..5d3d26e59e4 100644 --- a/jac/tests/compiler/backends/native/test_native_osp_runtime_import.jac +++ b/jac/tests/compiler/backends/native/test_native_osp_runtime_import.jac @@ -1,10 +1,13 @@ """Explicit runtime imports share the kernel used by native walker dispatch.""" import ctypes; -import from tests.support { compile_native } +import os; +import from tests.support { JAC_ROOT, compile_native } import from jaclang.compiler.backends.common.kernel_units { kernel_unit_owner } test "runtime unit identity is independent of the compiler installation" { + assert kernel_unit_owner(os.path.join(JAC_ROOT, "jaclang/runtime/osp_kernel.jac")) + == "osp_kernel"; assert kernel_unit_owner( "/checkout/jaclang/runtime/osp_kernel.jac", "jaclang.runtime.osp_kernel" )