Skip to content

Infix implementation #731

@pardeike

Description

@pardeike

Add infix patches that wrap a specific inner call inside an outer method. Replace only the call instruction. Do not rewrite prior argument‑loading IL. Capture the stack at the call site into locals, run inner prefixes, conditionally call original, run inner postfixes, then restore the original stack effect. No data‑flow analysis. Reuse and extend the existing injection binder.

Read first:
docs/infix/README.md
docs/infix/01-goals-and-constraints.md
docs/infix/02-concepts-and-api.md
docs/infix/04-parameter-binding.md
docs/infix/05-il-emission-algorithm.md
docs/infix/08-refactor-plan.md
docs/infix/09-tests.md

One rule
Only replace the call/callvirt at the site. Do not alter prior arg loads. Use locals to capture operands, then proceed. Re‑emit required call‑only prefixes (constrained.) immediately before the inner call inside the block.

API surface
New attributes:
[HarmonyInfixTarget(MethodBase method, int index = -1)]
[HarmonyInfixPrefix]
[HarmonyInfixPostfix]
Call selection: index is 1‑based, -1 means all occurrences.

Injection binding (extend, do not fork)
Inner context
__instance → captured inner instance local (or null for static)
inner arg names → captured arg locals
_result → inner result local
Outer context via o
prefix
o___instance, o_param, o___field, o___result
Outer locals
_var → original outer local by index
Synthetic locals
_var → declared once per outer method and shared across inner pre/post
By‑ref: prefer passing managed pointers through; avoid value copies where possible.

IL emission at a call site (pseudocode)

/* At original call position; original arg loads left intact upstream */
POP args/instance into new locals in reverse push order
bool __runOriginal = true
TResult __result = default(TResult)   // if non-void

// Inner prefixes (sorted)
for each prefix:
  if canSkip: if (!__runOriginal) goto AfterPrefixes
  call prefix(bound params)
  if returns bool: __runOriginal = <ret>
AfterPrefixes:

if (!__runOriginal) goto AfterCall

// Re-emit call-only prefixes for this call (e.g., constrained.)
ldloc instance?
ldloc arg1 .. ldloc argN
call/callvirt inner
if (has result) stloc __result
AfterCall:

// Inner postfixes (sorted)
for each postfix:
  call postfix(bound params)
  // optional: if passthrough, __result = <ret>

// Write-backs if you created value locals for by-ref sources (avoid in v1)
 
// Restore original stack effect
if (has result) ldloc __result

Integration notes
Sort inner patches with existing PatchSorter.
Keep original try/catch regions.
When original code stored the call result into a local r, reuse that local for __result to preserve downstream ldloc r and branches. If not stored, push __result back to stack.
Transpilers run before infix insertion, on the finalized instruction list.

Supported v1
call, callvirt
Instance/static/generics
constrained. absorbed and re‑emitted

Not in v1
tail. preservation
calli, ldftn/ldvirtftn
newobj targets
Complex argument expressions that require IL data‑flow recovery

Files to touch (root‑relative)
Harmony/Public/Patch.cs
Harmony/Public/PatchInfo.cs
Harmony/Public/Patches.cs
Harmony/Public/PatchClassProcessor.cs
Harmony/Internal/PatchModels.cs
Harmony/Internal/PatchFunctions.cs
Harmony/Internal/MethodCreator.cs
Harmony/Internal/MethodCreatorTools.cs
Harmony/Internal/MethodCreatorConfig.cs
Harmony/Internal/Infix.cs (new or expanded)

Refactor plan (high level)
Models: add InnerMethod { MethodBase method; int[] positions; }; patch types InnerPrefix/InnerPostfix.
Discovery: parse [HarmonyInfixTarget] on methods with [HarmonyInfixPrefix/Postfix]; attach InnerMethod.
Binder: add InjectionScope { Outer, Inner }. Reuse existing resolver; map o_, _var*, inner __instance, inner __result.
Emitter: implement AddInfixes(...) pass in MethodCreator per docs/infix/05-il-emission-algorithm.md. No fork of the outer prefix/postfix pipeline; small hooks only.

Acceptance criteria
Stack effect identical to the original call at each site.
Skip semantics: any inner prefix returning false skips the call; inner postfixes still run.
Ordering honors priorities and before/after.
constrained. preserved; tail. unsupported and documented.
Binder resolves o_, _var, _var, inner __instance, and inner __result.
Coexists with outer prefixes/postfixes and transpilers.

Tests to add
Cover the matrix in docs/infix/09-tests.md: void/non‑void, instance/static, value/ref returns, multiple occurrences and index selection, skip behavior, by‑ref args passthrough, constrained. value‑type receiver, coexistence with outer patches and transpilers. Harmony has many target sdks and for this task it is almost guaranteed enough to test with one single sdk and only one architecture to speed things up. IL is universal.

Task list

  • Parse and store inner targets and positions during patch discovery.
  • Extend injection binder with Outer vs Inner sourcing and o_/_var* names.
  • Implement AddInfixes(...) to replace each target call per algorithm.
  • Handle result‑local reuse or stack push as required by downstream IL.
  • Preserve constrained. at the reissued call.
  • Add tests per matrix.
  • Update docs/infix/* references in code comments sparingly.

Definition of done: all acceptance criteria green, tests pass, and no stack‑imbalance or region errors on complex methods.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions