mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 08:00:25 +02:00
495 lines
22 KiB
C#
495 lines
22 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using Translator.Core.Analysis;
|
|
using Translator.Core.Analysis.Representation;
|
|
using Translator.Core.Analysis.Ssa;
|
|
using Translator.Core.CodeGen;
|
|
using Translator.Core.Ir;
|
|
using Translator.Core.Representation;
|
|
using Xunit;
|
|
|
|
namespace Translator.Tests;
|
|
|
|
public class RegisterResidencyCodeGenTests
|
|
{
|
|
private static string Emit(
|
|
IrFunction function,
|
|
RepresentationEnvironment? types = null,
|
|
bool stateFree = false,
|
|
uint entryPoint = 0x80001000u,
|
|
IReadOnlyDictionary<uint, GuestAbiContract>? guestAbiContracts = null,
|
|
IReadOnlySet<uint>? fullContextCallTargets = null)
|
|
{
|
|
var contract = GuestAbiContractAnalyzer.Analyze(function);
|
|
return new CxxLinearCodeGenerator().Emit(
|
|
entryPoint,
|
|
new SsaTransformer().Convert(function),
|
|
new FunctionAbiClassification(function.Name, ValueRepresentation.Void),
|
|
types ?? new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>()),
|
|
guestAbiContracts: guestAbiContracts,
|
|
fullContextCallTargets: fullContextCallTargets,
|
|
emitStateFreeLeafVariant: stateFree,
|
|
stateFreeAbiContracts: stateFree
|
|
? new Dictionary<uint, GuestAbiContract> { [entryPoint] = contract }
|
|
: null,
|
|
stateFreeCallSymbols: stateFree
|
|
? new Dictionary<uint, string> { [entryPoint] = $"{function.Name}_native" }
|
|
: null);
|
|
}
|
|
|
|
/// <summary>
|
|
/// A synthetic callee contract. Everything not named here is empty, which is
|
|
/// what makes the narrowing observable.
|
|
/// </summary>
|
|
private static GuestAbiContract NarrowContract(
|
|
uint gprRead = 0,
|
|
uint gprWrite = 0,
|
|
uint fprRead = 0,
|
|
uint fprWrite = 0,
|
|
byte crRead = 0,
|
|
byte crWrite = 0,
|
|
bool readsCtr = false,
|
|
bool writesCtr = false,
|
|
GuestCallBoundaryFlags flags = GuestCallBoundaryFlags.None) =>
|
|
new(
|
|
GprReadBeforeWriteMask: gprRead,
|
|
GprPossibleWriteMask: gprWrite,
|
|
GprReturnMask: 0,
|
|
FprReadBeforeWriteMask: fprRead,
|
|
FprPossibleWriteMask: fprWrite,
|
|
FprReturnMask: 0,
|
|
CrReadBeforeWriteMask: crRead,
|
|
CrPossibleWriteMask: crWrite,
|
|
ReadsXerBeforeWrite: false,
|
|
MayWriteXer: false,
|
|
ReadsCtrBeforeWrite: readsCtr,
|
|
MayWriteCtr: writesCtr,
|
|
ReadsLrBeforeWrite: false,
|
|
MayWriteLr: false,
|
|
BoundaryFlags: flags,
|
|
DirectCallTargets: Array.Empty<uint>());
|
|
|
|
private static IrFunction CallerAcross(string name, string target) =>
|
|
new(name, "entry", new[]
|
|
{
|
|
new IrBasicBlock("entry", new IrInstruction[]
|
|
{
|
|
new IrBinary("r3", IrValue.Register("r3"), IrValue.Register("r4"), "add"),
|
|
new IrBinary("r5", IrValue.Register("r5"), IrValue.Register("r6"), "add"),
|
|
new IrCall(string.Empty, target, Array.Empty<IrValue>()),
|
|
new IrBinary("r3", IrValue.Register("r3"), IrValue.Register("r5"), "add"),
|
|
new IrReturn(null)
|
|
})
|
|
});
|
|
|
|
private static string FunctionBody(string code, string name)
|
|
{
|
|
var declaration = $"extern \"C\" void {name}(CpuContext* MKW_RESTRICT ctx)";
|
|
var start = code.IndexOf(declaration, StringComparison.Ordinal);
|
|
Assert.True(start >= 0, $"missing definition of {name}");
|
|
var open = code.IndexOf('{', start);
|
|
var depth = 0;
|
|
for (var index = open; index < code.Length; ++index)
|
|
{
|
|
if (code[index] == '{') ++depth;
|
|
else if (code[index] == '}' && --depth == 0) return code[open..(index + 1)];
|
|
}
|
|
|
|
throw new InvalidOperationException("unterminated body");
|
|
}
|
|
|
|
[Fact]
|
|
public void ResidentBodyLoadsAtEntryAndWritesBackAtReturn()
|
|
{
|
|
var function = new IrFunction("resident_add", "entry", new[]
|
|
{
|
|
new IrBasicBlock("entry", new IrInstruction[]
|
|
{
|
|
new IrBinary("r3", IrValue.Register("r3"), IrValue.Register("r4"), "add"),
|
|
new IrReturn(null)
|
|
})
|
|
});
|
|
|
|
var code = Emit(function);
|
|
|
|
Assert.Contains("uint32_t r3 = ctx->gpr[3];", code, StringComparison.Ordinal);
|
|
Assert.Contains("uint32_t r4 = ctx->gpr[4];", code, StringComparison.Ordinal);
|
|
Assert.Contains(" r3 = (r3 + r4);", code, StringComparison.Ordinal);
|
|
// r4 is never written, so it is not part of the write-back set.
|
|
Assert.Contains(" ctx->gpr[3] = r3;", code, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("ctx->gpr[4] = r4;", code, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("ctx->gpr[3] = (", code, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void GuestCallIsBracketedByFlushAndReload()
|
|
{
|
|
var function = new IrFunction("resident_call", "entry", new[]
|
|
{
|
|
new IrBasicBlock("entry", new IrInstruction[]
|
|
{
|
|
new IrBinary("r3", IrValue.Register("r3"), IrValue.Register("r4"), "add"),
|
|
new IrCall(string.Empty, "0x80002000", Array.Empty<IrValue>()),
|
|
new IrBinary("r3", IrValue.Register("r3"), IrValue.Register("r4"), "add"),
|
|
new IrReturn(null)
|
|
})
|
|
});
|
|
|
|
var body = FunctionBody(Emit(function), "resident_call");
|
|
var flush = body.IndexOf(" ctx->gpr[3] = r3;", StringComparison.Ordinal);
|
|
var call = body.IndexOf("InvokeDirectCpu<0x80002000u>(ctx);", StringComparison.Ordinal);
|
|
var reload = body.IndexOf(" r3 = ctx->gpr[3];", StringComparison.Ordinal);
|
|
|
|
Assert.True(flush > 0 && call > flush && reload > call,
|
|
"the call must be preceded by a flush and followed by a reload");
|
|
// A register that is only read still has to be reloaded: the callee may
|
|
// clobber it. It must not be flushed, because it was never modified.
|
|
Assert.Contains(" r4 = ctx->gpr[4];", body, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("ctx->gpr[4] = r4;", body, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void IndirectJumpFlushesWithoutWritingLocalsBack()
|
|
{
|
|
var function = new IrFunction("resident_tail", "entry", new[]
|
|
{
|
|
new IrBasicBlock("entry", new IrInstruction[]
|
|
{
|
|
new IrBinary("r3", IrValue.Register("r3"), IrValue.Register("r4"), "add"),
|
|
new IrIndirectJump(IrValue.Register("r3"))
|
|
})
|
|
});
|
|
|
|
var body = FunctionBody(Emit(function), "resident_tail");
|
|
var flush = body.IndexOf("ctx->gpr[3] = r3;", StringComparison.Ordinal);
|
|
var jump = body.IndexOf("InvokeIndirectJump(r3, ctx);", StringComparison.Ordinal);
|
|
|
|
Assert.True(flush > 0 && jump > flush);
|
|
// Exactly one publication: nothing may be written back after the tail
|
|
// dispatch, because the callee owns the architectural state from there.
|
|
Assert.Equal(
|
|
1,
|
|
body.Split("ctx->gpr[3] = r3;", StringSplitOptions.None).Length - 1);
|
|
}
|
|
|
|
[Fact]
|
|
public void ConditionRegisterAndCountRegisterBecomeLocals()
|
|
{
|
|
var function = new IrFunction("resident_loop", "entry", new[]
|
|
{
|
|
new IrBasicBlock("entry", new IrInstruction[]
|
|
{
|
|
new IrAssign("ctr", IrValue.Register("r3")),
|
|
new IrSetCrField(0, IrValue.Register("r3"), IrValue.Imm(0), false),
|
|
new IrBranch("bdnz", "entry", "exit", "ctr")
|
|
}),
|
|
new IrBasicBlock("exit", new IrInstruction[] { new IrReturn(null) })
|
|
});
|
|
|
|
var code = Emit(function);
|
|
|
|
Assert.Contains("uint32_t cr = ctx->cr;", code, StringComparison.Ordinal);
|
|
Assert.Contains("uint32_t ctr = ctx->ctr;", code, StringComparison.Ordinal);
|
|
Assert.Contains("uint32_t xer = ctx->xer;", code, StringComparison.Ordinal);
|
|
Assert.Contains("SetCRResident(cr, xer, 0,", code, StringComparison.Ordinal);
|
|
Assert.Contains("if ((ctr != 0))", code, StringComparison.Ordinal);
|
|
Assert.Contains(" ctx->cr = cr;", code, StringComparison.Ordinal);
|
|
Assert.Contains(" ctx->ctr = ctr;", code, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("SetCR(ctx,", code, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void RawBranchConditionTextIsRoutedThroughTheResidentCondition()
|
|
{
|
|
// PpcLifter emits the general bc/bcctr condition as raw C++ text. It is
|
|
// the only register access that bypasses the expression layer, so a
|
|
// resident body must rewrite it or silently read a stale CR/CTR.
|
|
var function = new IrFunction("resident_raw_condition", "entry", new[]
|
|
{
|
|
new IrBasicBlock("entry", new IrInstruction[]
|
|
{
|
|
new IrBranch(
|
|
"raw",
|
|
"entry",
|
|
"exit",
|
|
"(((((ctx->ctr != 0)) ^ false)) && ((GetCRBit(ctx, 0, 2) == true)))")
|
|
}),
|
|
new IrBasicBlock("exit", new IrInstruction[] { new IrReturn(null) })
|
|
});
|
|
|
|
var code = Emit(function);
|
|
|
|
Assert.Contains("(((ctr != 0)) ^ false)", code, StringComparison.Ordinal);
|
|
Assert.Contains("GetCRBitResident(cr, 0, 2)", code, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("ctx->ctr !=", code, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("GetCRBit(ctx,", code, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void FloatRegistersUseValueLocalsAndPreservePairedLanes()
|
|
{
|
|
var types = new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>
|
|
{
|
|
["f1"] = ValueRepresentation.Float64,
|
|
["f2"] = ValueRepresentation.Float64
|
|
});
|
|
var function = new IrFunction("resident_float", "entry", new[]
|
|
{
|
|
new IrBasicBlock("entry", new IrInstruction[]
|
|
{
|
|
new IrCall("f1", "PPC_PsAdd", new[] { IrValue.Register("f1"), IrValue.Register("f2") }),
|
|
new IrReturn(null)
|
|
})
|
|
});
|
|
|
|
var code = Emit(function, types);
|
|
|
|
Assert.Contains("PPC_FPR f1 = ctx->fpr[1];", code, StringComparison.Ordinal);
|
|
Assert.Contains("PPC_FPR f2 = ctx->fpr[2];", code, StringComparison.Ordinal);
|
|
Assert.Contains("PpcSetPairedFprInline(f1,", code, StringComparison.Ordinal);
|
|
// The whole union is written back so PS1 survives the boundary.
|
|
Assert.Contains(" ctx->fpr[1] = f1;", code, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("ctx->fpr[2] = f2;", code, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void StateFreeVariantIsUnchangedWhileThePublicEntryBecomesResident()
|
|
{
|
|
var function = new IrFunction("resident_state_free", "entry", new[]
|
|
{
|
|
new IrBasicBlock("entry", new IrInstruction[]
|
|
{
|
|
new IrBinary("r3", IrValue.Register("r3"), IrValue.Register("r4"), "add"),
|
|
new IrReturn(null)
|
|
})
|
|
});
|
|
|
|
var code = Emit(function, stateFree: true);
|
|
|
|
// The explicit-state clone is derived from the legacy cached body, so it
|
|
// keeps the cached form while the public CpuContext entry is resident.
|
|
var start = code.IndexOf("resident_state_free_native(", StringComparison.Ordinal);
|
|
Assert.True(start >= 0);
|
|
var variant = code[start..code.IndexOf("RECOMP_STATE_FREE_ABI", start, StringComparison.Ordinal)];
|
|
Assert.DoesNotContain("ctx->gpr[3] = r3;", variant, StringComparison.Ordinal);
|
|
|
|
var publicBody = FunctionBody(code, "resident_state_free");
|
|
Assert.Contains("uint32_t r3 = ctx->gpr[3];", publicBody, StringComparison.Ordinal);
|
|
Assert.Contains(" r3 = (r3 + r4);", publicBody, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("cached_r3", publicBody, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void BoundaryNarrowingSyncsOnlyTheContractIntersectionAtADirectCall()
|
|
{
|
|
// The callee reads r4 before writing it and can only write r3, so the
|
|
// boundary needs r3 (it is in this frame's write set and in the callee
|
|
// write set), r4 (the callee reads it), and nothing else on the flush;
|
|
// the reload is limited to r3.
|
|
var contracts = new Dictionary<uint, GuestAbiContract>
|
|
{
|
|
[0x80002000u] = NarrowContract(gprRead: 1u << 4, gprWrite: 1u << 3)
|
|
};
|
|
|
|
var body = FunctionBody(
|
|
Emit(
|
|
CallerAcross("narrowed_call", "0x80002000"),
|
|
guestAbiContracts: contracts),
|
|
"narrowed_call");
|
|
var call = body.IndexOf("InvokeDirectCpu<0x80002000u>(ctx);", StringComparison.Ordinal);
|
|
Assert.True(call > 0);
|
|
var beforeCall = body[..call];
|
|
var afterCall = body[call..];
|
|
|
|
Assert.Contains("ctx->gpr[3] = r3;", beforeCall, StringComparison.Ordinal);
|
|
// r5 is written by this frame but the callee neither reads nor writes it.
|
|
Assert.DoesNotContain("ctx->gpr[5] = r5;", beforeCall, StringComparison.Ordinal);
|
|
Assert.Contains(" r3 = ctx->gpr[3];", afterCall, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("r4 = ctx->gpr[4];", afterCall, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("r5 = ctx->gpr[5];", afterCall, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void ContextObservingTargetForcesAFullDirectCallBoundary()
|
|
{
|
|
var target = 0x80002000u;
|
|
var contracts = new Dictionary<uint, GuestAbiContract>
|
|
{
|
|
[target] = NarrowContract(gprRead: 1u << 4, gprWrite: 1u << 3)
|
|
};
|
|
var body = FunctionBody(
|
|
Emit(
|
|
CallerAcross("observed_call", $"0x{target:X8}"),
|
|
guestAbiContracts: contracts,
|
|
fullContextCallTargets: new HashSet<uint> { target }),
|
|
"observed_call");
|
|
var call = body.IndexOf($"InvokeDirectCpu<0x{target:X8}u>(ctx);", StringComparison.Ordinal);
|
|
|
|
Assert.True(call > 0);
|
|
// r5 is outside the guest callee's contract, but a host observer may
|
|
// inspect it through CpuContext.
|
|
Assert.Contains("ctx->gpr[5] = r5;", body[..call], StringComparison.Ordinal);
|
|
Assert.Contains(" r4 = ctx->gpr[4];", body[call..], StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void ContextObserverForcesFullBoundariesThroughIntermediateCallers()
|
|
{
|
|
const uint root = 0x80001000u;
|
|
const uint middle = 0x80002000u;
|
|
const uint observed = 0x80003000u;
|
|
var rootFunction = CallerAcross("transitive_observed_call", $"0x{middle:X8}");
|
|
var functions = new Dictionary<uint, IrFunction>
|
|
{
|
|
[root] = rootFunction,
|
|
[middle] = new IrFunction("middle", "entry", new[]
|
|
{
|
|
new IrBasicBlock("entry", new IrInstruction[]
|
|
{
|
|
new IrCall(string.Empty, $"0x{observed:X8}", Array.Empty<IrValue>()),
|
|
new IrReturn(null)
|
|
})
|
|
}),
|
|
[observed] = new IrFunction("observed", "entry", new[]
|
|
{
|
|
new IrBasicBlock("entry", new IrInstruction[] { new IrReturn(null) })
|
|
})
|
|
};
|
|
var contracts = GuestAbiInterproceduralAnalyzer.Analyze(
|
|
functions,
|
|
contextObservingEntryPoints: new[] { observed }).Contracts;
|
|
|
|
var body = FunctionBody(
|
|
Emit(
|
|
rootFunction,
|
|
entryPoint: root,
|
|
guestAbiContracts: contracts,
|
|
fullContextCallTargets: new HashSet<uint> { observed }),
|
|
"transitive_observed_call");
|
|
var call = body.IndexOf($"InvokeDirectCpu<0x{middle:X8}u>(ctx);", StringComparison.Ordinal);
|
|
|
|
Assert.True(contracts[middle].HasFullSynchronizationFence);
|
|
Assert.True(call > 0);
|
|
Assert.Contains("ctx->gpr[5] = r5;", body[..call], StringComparison.Ordinal);
|
|
Assert.Contains(" r4 = ctx->gpr[4];", body[call..], StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void BoundaryNarrowingFlushesRegistersTheCalleeOnlyMayWrite()
|
|
{
|
|
// A register the callee may write has to be flushed even though the
|
|
// callee never reads it: the write is only possible, so on the paths
|
|
// where it does not happen the reload after the call would otherwise
|
|
// load a stale architectural value back into the local.
|
|
var contracts = new Dictionary<uint, GuestAbiContract>
|
|
{
|
|
[0x80002000u] = NarrowContract(gprWrite: (1u << 3) | (1u << 5))
|
|
};
|
|
|
|
var body = FunctionBody(
|
|
Emit(
|
|
CallerAcross("narrowed_possible_write", "0x80002000"),
|
|
guestAbiContracts: contracts),
|
|
"narrowed_possible_write");
|
|
var call = body.IndexOf("InvokeDirectCpu<0x80002000u>(ctx);", StringComparison.Ordinal);
|
|
var beforeCall = body[..call];
|
|
var afterCall = body[call..];
|
|
|
|
Assert.Contains("ctx->gpr[3] = r3;", beforeCall, StringComparison.Ordinal);
|
|
Assert.Contains("ctx->gpr[5] = r5;", beforeCall, StringComparison.Ordinal);
|
|
Assert.Contains(" r3 = ctx->gpr[3];", afterCall, StringComparison.Ordinal);
|
|
Assert.Contains(" r5 = ctx->gpr[5];", afterCall, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("r4 = ctx->gpr[4];", afterCall, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void BoundaryNarrowingSyncsConditionAndCountRegistersOnAnyIntersectingField()
|
|
{
|
|
// The contract describes CR per field while the local is the packed
|
|
// register, so a single intersecting field forces the whole sync. CTR is
|
|
// synced whenever the contract cannot prove the callee leaves it alone.
|
|
var function = new IrFunction("narrowed_special", "entry", new[]
|
|
{
|
|
new IrBasicBlock("entry", new IrInstruction[]
|
|
{
|
|
new IrAssign("ctr", IrValue.Register("r3")),
|
|
new IrSetCrField(2, IrValue.Register("r3"), IrValue.Imm(0), false),
|
|
new IrCall(string.Empty, "0x80002000", Array.Empty<IrValue>()),
|
|
new IrBranch("bdnz", "entry", "exit", "ctr")
|
|
}),
|
|
new IrBasicBlock("exit", new IrInstruction[] { new IrReturn(null) })
|
|
});
|
|
|
|
string BodyFor(GuestAbiContract contract) => FunctionBody(
|
|
Emit(
|
|
function,
|
|
guestAbiContracts: new Dictionary<uint, GuestAbiContract> { [0x80002000u] = contract }),
|
|
"narrowed_special");
|
|
|
|
var touched = BodyFor(NarrowContract(crRead: 1 << 5, crWrite: 1 << 1, writesCtr: true));
|
|
Assert.Contains("ctx->cr = cr;", touched, StringComparison.Ordinal);
|
|
Assert.Contains(" cr = ctx->cr;", touched, StringComparison.Ordinal);
|
|
Assert.Contains("ctx->ctr = ctr;", touched, StringComparison.Ordinal);
|
|
Assert.Contains(" ctr = ctx->ctr;", touched, StringComparison.Ordinal);
|
|
|
|
var untouched = BodyFor(NarrowContract(gprWrite: 1u << 3));
|
|
var call = untouched.IndexOf("InvokeDirectCpu<0x80002000u>(ctx);", StringComparison.Ordinal);
|
|
// The exit flush still publishes CR/CTR; only the call boundary drops it.
|
|
Assert.DoesNotContain("ctx->cr = cr;", untouched[..call], StringComparison.Ordinal);
|
|
Assert.DoesNotContain("ctx->ctr = ctr;", untouched[..call], StringComparison.Ordinal);
|
|
Assert.DoesNotContain(" cr = ctx->cr;", untouched[call..], StringComparison.Ordinal);
|
|
Assert.DoesNotContain(" ctr = ctx->ctr;", untouched[call..], StringComparison.Ordinal);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(true)]
|
|
[InlineData(false)]
|
|
public void BoundaryNarrowingFallsBackToAFullSyncWithoutAPreciseContract(bool haveContract)
|
|
{
|
|
// A missing contract and a full-synchronization fence are the same thing
|
|
// here: neither proves anything about the callee's register usage.
|
|
var contracts = haveContract
|
|
? new Dictionary<uint, GuestAbiContract>
|
|
{
|
|
[0x80002000u] = NarrowContract(
|
|
gprWrite: 1u << 3,
|
|
flags: GuestCallBoundaryFlags.RequiresCompleteContext)
|
|
}
|
|
: new Dictionary<uint, GuestAbiContract>();
|
|
|
|
var caller = CallerAcross("narrowed_fence", "0x80002000");
|
|
var narrowed = FunctionBody(
|
|
Emit(caller, guestAbiContracts: contracts),
|
|
"narrowed_fence");
|
|
|
|
// Full synchronization, asserted by shape: r5 is flushed even though the
|
|
// callee is declared unable to write it, and r4 is reloaded even though
|
|
// this frame only reads it. Narrowing would have dropped both.
|
|
var call = narrowed.IndexOf("InvokeDirectCpu<0x80002000u>(ctx);", StringComparison.Ordinal);
|
|
Assert.Contains("ctx->gpr[5] = r5;", narrowed[..call], StringComparison.Ordinal);
|
|
Assert.Contains(" r4 = ctx->gpr[4];", narrowed[call..], StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void BoundaryNarrowingDoesNotApplyToIndirectCalls()
|
|
{
|
|
var function = new IrFunction("narrowed_indirect", "entry", new[]
|
|
{
|
|
new IrBasicBlock("entry", new IrInstruction[]
|
|
{
|
|
new IrBinary("r3", IrValue.Register("r3"), IrValue.Register("r4"), "add"),
|
|
new IrIndirectCall(string.Empty, IrValue.Register("r12"), Array.Empty<IrValue>()),
|
|
new IrBinary("r3", IrValue.Register("r3"), IrValue.Register("r4"), "add"),
|
|
new IrReturn(null)
|
|
})
|
|
});
|
|
|
|
var narrowed = FunctionBody(Emit(function), "narrowed_indirect");
|
|
|
|
// There is no callee contract to narrow against, so the boundary stays
|
|
// full: r4 is reloaded after the call even though this frame only reads it.
|
|
Assert.Contains(" r4 = ctx->gpr[4];", narrowed, StringComparison.Ordinal);
|
|
}
|
|
}
|