Harmony icon indicating copy to clipboard operation
Harmony copied to clipboard

Infix implementation

Open pardeike opened this issue 3 months ago • 0 comments

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.

pardeike avatar Sep 20 '25 12:09 pardeike