Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions a816/symbols.py
Original file line number Diff line number Diff line change
Expand Up @@ -506,12 +506,18 @@ def _export_name(name: str, scope: "Scope", idx: int, mangle_nested: bool) -> st
"""
if isinstance(scope, NamedScope) and "." not in name:
return f"{scope.name}.{name}"
# AllocBodyScope opts out of the `__sc<idx>__` mangle: cross-alloc
# public refs and `.extern` declarations in the same module need
# to see bare names. Underscore privacy is enforced by the
# `_bubble_anon_exportables` filter (which keeps them in the body
# scope) plus the object-mode LOCAL classifier downstream.
# AllocBodyScope keeps PUBLIC (non-underscore) labels bare so
# cross-alloc refs + `.extern` resolve them by their source name.
# But underscore-PRIVATE labels are local to THIS alloc body and must
# be unique per alloc: two sibling allocs both declaring `_loop`/`_done`
# otherwise collide in the flat symbol table, and a `jmp.w _loop`
# relocation binds to whichever duplicate the linker placed last (a
# wild, layout-dependent branch). Mangle them with the body's scope
# index so each alloc's private label exports (and relocates)
# under a distinct name.
if isinstance(scope, AllocBodyScope):
if name.startswith("_") and mangle_nested and idx > 0:
return f"__sc{idx}__{name}"
return name
if mangle_nested and idx > 0 and not isinstance(scope, NamedScope):
return f"__sc{idx}__{name}"
Expand Down
15 changes: 8 additions & 7 deletions tests/test_intra_module_extern_resolves.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,11 @@ def test_public_label_in_alloc_body_visible_to_extern_in_same_module() -> None:

def test_underscore_label_export_classification_stays_local() -> None:
"""Underscore-prefixed labels classify as LOCAL on export — that's
the privacy convention's actual teeth. The linker still permits
cross-module references at link time (`_resolve_aliases` finds
LOCALs in `symbol_map`), but the `.o` advertises `_foo` as LOCAL
rather than GLOBAL so callers can't introspect a "public api"
that includes private names. Modules wanting durable
cross-module references must drop the underscore."""
the privacy convention's actual teeth. They also export under a
per-alloc-mangled name (`__sc<idx>___foo`) so two sibling allocs both
declaring `_foo` stay distinct; the bare private name never leaks as a
GLOBAL. Modules wanting durable cross-module references must drop the
underscore."""
with tempfile.TemporaryDirectory() as tmpdir:
tmp = Path(tmpdir)
(tmp / "mod.s").write_text(
Expand All @@ -72,4 +71,6 @@ def test_underscore_label_export_classification_stays_local() -> None:
assert program.assemble_as_object(str(tmp / "mod.s"), obj_path) == 0
obj = ObjectFile.from_file(str(obj_path))
kinds = {n: st for n, _, st, _ in obj.symbols}
assert kinds.get("_foo") == SymbolType.LOCAL, kinds
# Exported per-alloc-mangled and LOCAL; no bare `_foo` GLOBAL leaks.
private = {n: st for n, st in kinds.items() if n.endswith("_foo")}
assert private == {"__sc1___foo": SymbolType.LOCAL}, kinds
26 changes: 26 additions & 0 deletions tests/test_pool_label_diff.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,3 +189,29 @@ def test_jmpw_to_local_label_resolves_per_module() -> None:
assert b.placed_base == 0xC10009
# nop, jmp _end ($C10010 -> $0010), jmp _loop ($C10009 -> $0009), rts.
assert b.code == b"\xea\x4c\x10\x00\x4c\x09\x00\x60"


def test_jmpw_to_local_label_resolves_per_alloc_in_one_module() -> None:
"""`jmp.w <local>` must target ITS OWN alloc's label, not a same-named
private label in a sibling alloc of the SAME module.

Eight allocs each declaring `_done`/`_scan` collided in one flat symbol
table, so `jmp.w _scan` bound to whichever duplicate the linker placed
last (the reported wild branch). Underscore-private alloc-body labels now
export per-alloc-mangled (`__sc<idx>__`), keeping each distinct.
"""
src = (
".pool engine { range 0xc10000 0xc1ffff strategy order }\n"
".alloc a in engine {\n_loop:\n nop\n jmp.w _loop\n_done:\n rts\n}\n"
".alloc b in engine {\n_loop:\n nop\n nop\n jmp.w _loop\n_done:\n rts\n}\n"
)
with tempfile.TemporaryDirectory() as tmpdir:
tmp = Path(tmpdir)
(tmp / "m.s").write_text(src)
linked = Linker([_compile(tmp / "m.s")]).link(base_address=0x8000)
a = next(s for s in linked.sections if s.code == b"\xea\x4c\x00\x00\x60")
b = next(s for s in linked.sections if s.code.startswith(b"\xea\xea\x4c"))
# a placed at $C10000: jmp _loop -> $0000. b at $C10005: jmp _loop -> $0005.
assert a.placed_base == 0xC10000
assert b.placed_base == 0xC10005
assert b.code == b"\xea\xea\x4c\x05\x00\x60"
Loading