Skip to content

Keep boxed class-method params boxed - #3043

Open
plajjan wants to merge 2 commits into
mainfrom
boxed-method-params
Open

Keep boxed class-method params boxed#3043
plajjan wants to merge 2 commits into
mainfrom
boxed-method-params

Conversation

@plajjan

@plajjan plajjan commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

A method that overrides a method declared generic in its base class
receives its arguments boxed: calls can arrive through the base class's
method table, where the argument type is not known, so a Deriv(Base[int])
override of Base[T].f takes a B_int, never a raw machine integer.

The Boxing pass assumed that every scalar-typed local holds a raw value.
For these parameters that assumption is wrong. Code generation quietly
compensated at call boundaries, at the cost of box/unbox round-trips, but
the primitive operator paths cancelled the compensation away entirely, so
a body like "return x > 3" compared the pointer instead of the value:
Deriv().f(1) returned True on main.

Boxing now tracks these parameters and leaves their reads boxed; where a
raw value is needed, the unboxing survives to code generation and is
emitted as ->val. Assignments cannot follow the read convention: x += 1
would update the caller's box in place, and a plain rebinding would store
a raw value that later reads unbox again. So a boxed parameter that the
body assigns is renamed aside and copied into an ordinary raw local at
function entry, after which reads and stores follow the standard
raw-local rules and the caller's box is never written.

The first commit adds a small fixture and commits the golden files
produced by the unfixed compiler, so the second commit's diff shows
exactly what the fix changes in the generated code. The churn in the
deact and lines goldens is the removal of the now-unnecessary box/unbox
round-trips at call boundaries; behavior there was already correct.

Kristian Larsson added 2 commits July 29, 2026 00:27
Golden files (Boxing output and generated C) for a small test
program, boxparam.act.

Base is a generic class whose methods take an argument of type T.
Deriv inherits from Base[int], so its versions of those methods take
an int. Because the methods can also be called through Base, where
the argument type is not known, the argument is always passed boxed
(a B_int), never as a raw machine integer -- even when the Deriv
version is called directly.

The goldens cover such an argument being compared, reassigned, and
passed on to another method. Two of the recorded outputs are plain
bugs, committed on purpose so that the diff of the next commit shows
exactly what the fix changes:

    bool ...cmp (... self, B_int x) {
        bool N_tmp = (x > 3LL);            // compares the pointer

    B_int ...bump (... self, B_int x) {
        x += 1LL;                          // moves the pointer
        return toB_int(((B_int)x)->val);   // reads through it
A method slot's C calling convention is decided by the oldest
declaration of the slot in the hierarchy (rtypeOf). When that
declaration is generic, the slot passes the argument as a boxed
object, because callers may invoke it through the base class's method
table, where every argument is a pointer-sized word:

    class Base[T](object):
        def f(self, x: T) -> bool:
            return True

An override in a class that narrows T to a scalar type inherits that
convention: Deriv(Base[int]).f receives a boxed B_int, not a raw
int64_t, even though its body computes with raw machine integers.
(Only this shape is affected. A slot whose oldest declaration already
names a scalar type passes it raw everywhere.)

The Boxing pass, however, assumed that every scalar-typed local holds
a raw value. It wrapped reads of such parameters in Box like raw
locals, and the Box/UnBox cancellation in the primitive operator
reduction then dropped the representation conversion entirely, so

    class Deriv(Base[int]):
        def f(self, x: int) -> bool:
            return x > 3

    Deriv().f(1)   # returned True: compared the B_int pointer to 3

Track boxed parameters in the Boxing env and leave their reads
unwrapped. Where the body needs the raw value, a real UnBox now
survives to CodeGen and renders as ->val: the parameter is unboxed at
each use inside the callee.

Assignments cannot follow the read convention: an augmented
assignment would update the caller's box in place, and a plain
rebinding would store a raw value that later reads would unbox again.
A boxed parameter assigned in the body is therefore renamed aside and
copied into an ordinary raw local at function entry, after which
every read and store follows the standard raw-local convention and
the caller's box is never written.
@plajjan

plajjan commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

@nordlander do you want to take a look at this? it's a correctness fix but also a general speedup since we avoid boxing+unboxing for variables in quite a lot of common scenarios. I think the updated testing snapshots show pretty clearly what is going on

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant