Skip to content

Fix mixin adaptation for Fabric mods on 26.1.2 - #13

Open
1499501762 wants to merge 16 commits into
Sinytra:26.1.xfrom
1499501762:connector-26.1.2-mixin-fixes
Open

1499501762 wants to merge 16 commits into
Sinytra:26.1.xfrom
1499501762:connector-26.1.2-mixin-fixes

Conversation

@1499501762

@1499501762 1499501762 commented Oct 1, 2026 •

Copy link
Copy Markdown

Fix mixin adaptation for Fabric mods on 26.1.2

I hit these while getting carpet and a few other mods running on NeoForge 26.1.2. Most are bugs in the adapter, two are just missing mixin types.

  • Accessor, Invoker, ModifyArgs, ModifyConstant and Overwrite now have mixin types. They were skipped before, so mods using them silently lost the feature.
  • The pipeline re-checks the final target and injection point after the resolvers rewrite them. Nothing looked at the result before, so a mixin that could never apply was still reported as adapted.
  • TARGET_CONSTANT was missing the null guard TARGET_AT already has, and SLICE cast the annotation value straight to AnnotationNode. A repeated annotation took the whole mod down with an NPE or a CCE.
  • Captured locals are re-derived from the injection point the pipeline produced, not the one on the annotation.
  • InheritanceHandler.parentCache is shared across mixins but was a plain HashMap while ART transforms in parallel. It's a ConcurrentHashMap now.
  • MOVED_INTO_LAMBDA skips handlers that use mixinextras' @Local sugar. Relocating the target re-points the
    injection point, and @Local is resolved against the local variable frame at that point (MixinExtras'
    LocalSugarApplicator#validate -> makeLvtContext(target, node)), so the sugar ends up re-resolved against
    the lambda's frame and local variable table instead of the original method's. Whether that still matches
    depends on the shape - a captured local becomes a lambda parameter - but when it does not, MixinExtras throws
    SugarApplicationException, which is a MixinException and not an InvalidInjectionException; Mixin then
    escalates it to a fatal Critical injection failure ... (0/1) succeeded that can no longer be stripped.
    Only @Local is gated: @Share's validate is a type check that never looks at the injection point.
  • When the target class isn't in the vanilla jar, the handler is only passed through if the class is the mod's own, or if the injection point already resolves in the patched class. Passing platform classes through unchecked caused a fatal InvalidInjectionException (create's generated CommonHooks mixin).

Testing:

  • :test:mc-1.21.1:test and :test:mc-26.1.2:test pass.
  • carpet on a 26.1.2.112 server: starts up, no injection errors.
  • Offline over ~46 mods: carpet 301 to 303/0, viafabricplus 480 to 528; shuttfup and cookeymod went from crashing out to producing a report at all.

Not fixed here:

  • create-fly still won't start, but that's its own registry timing (Registry is already frozen), not the adapter.
  • Removed unadaptable still comes out as 6 or 8 on some runs, for a pair of handlers. Looks like the class view isn't complete while adapting; I'll open a separate issue.
  • The adapter's injection-point check and Mixin's don't always agree - create-fly's generated CommonHooks mixin passes our check but Mixin rejects it at runtime.

Update: one more fix, "Sync the callback modifier when a member moves onto a static target".

When the adapter moves a mixin method into a generated class, the member can move onto a static method (a
Fabric instance method mapped onto a NeoForge static hook). Mixin rejects a non-static callback that targets a
static method, fatally. The extract path now syncs the modifier - but only when the handler does not read the
receiver.

Removing the receiver shifts every slot reference down by one. Slot numbers are fixed at compile time and do
not depend on branches, exception handlers or variable lifetimes, so a uniform shift is correct. A body that
reads local 0 (a @Shadow member access compiles to exactly aload 0 + getfield/invokevirtual) cannot become
static at all, so those handlers are reported as unadapted before anything is moved and get stripped instead
of emitting a class that no longer verifies.

Dropping the receiver also invalidates the method's StackMapTable - an explicit full_frame still describes
local 0 and every slot after it - so the transform now returns COMPUTE_FRAMES and Connector re-derives the
frames.

With both parts, Create Fly has no injection failures left. It still does not start - it fails later, on its
own registry timing (Registry is already frozen: it registers from an @Inject at the TAIL of
BuiltInRegistries.freeze), which is not something the adapter can fix. The same jar runs fine on Fabric.

@1499501762
1499501762 force-pushed the connector-26.1.2-mixin-fixes branch 2 times, most recently from 28749db to fe6066e Compare October 1, 2026 13:54
Mixin refuses a non-static callback that targets a static method, and the error is fatal. The member can end
up on a static method after being moved into a generated class, so sync the modifier - but only when the
handler does not read the receiver.

As an instance method the body reserved local 0 for the receiver, so its parameters sat at 1..n; once the
receiver is gone every slot reference shifts down by one. Slot numbers are fixed at compile time and do not
depend on branches, exception handlers or variable lifetimes, so the uniform shift is correct - the one shape
it is not safe for is a body that reads local 0.

A body that reads local 0 cannot become static at all: a @shadow member access compiles to exactly
"aload 0 + getfield/invokevirtual", and Candidates#canMove deliberately lets that shape through. Those
handlers are now reported as unadapted before anything is moved, so Patcher#stripFailedMixins drops them
instead of emitting a class that no longer verifies.

Dropping the receiver also invalidates the method's StackMapTable: an explicit full_frame still describes
local 0 and every slot after it, while frame deltas relative to the descriptor happen to survive. The
transform now returns COMPUTE_FRAMES so Connector re-derives them.

Without the shift Mixin sees a body whose locals no longer line up and the game dies with
"VerifyError: Bad local variable type" (locals[3] = top); without the frames the same happens for any handler
whose encoding contains a full_frame ("Stack map does not match the one at exception handler N").
@1499501762
1499501762 force-pushed the connector-26.1.2-mixin-fixes branch from fe6066e to b21b373 Compare October 8, 2026 10:25
Most of them restated the code or recorded how the change was found. Keep the reason where it is not
visible in the code itself, and drop the rest.

No code changes: the diff is comment lines only.
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