Skip to content

Commit 05716be

Browse files
authored
Admit ternary property-read operands (#3023)
1 parent 1367478 commit 05716be

4 files changed

Lines changed: 91 additions & 14 deletions

File tree

docs/unified-bytecode-expansion-contract.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,7 @@ the final post-compile production subset check before VM entry.
378378
|---|---|---|---|---|
379379
| `prototype-guard:Binary` | `Binary` opcodes currently admit the selector-owned production subset: arithmetic (`+`, `-`, `*`, `/`, `%`, `**`), equality/comparison (`==`, `!=`, `===`, `!==`, `<`, `<=`, `>`, `>=`), bitwise/shift (`&`, `|`, `^`, `<<`, `>>`, `>>>`), and relational/object tests (`in`, `instanceof`); binary operators outside that subset (for example `&&`, `\|\|`, `??`) decline as `UnsupportedPlanShape` | Existing expression route | Binary operator widening lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionEligibilityTests&FullyQualifiedName~Evaluate_PropertyReadAdjacentFamilies_DeclineWithExplicitCodes"` |
380380
| `prototype-guard:Jump` | `Jump` and `JumpWithDriverCleanup` are admitted only for compiler-owned branch, loop, and driver cleanup shapes | Existing control-flow route if an unowned jump shape is introduced | Control-flow lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionEligibilityTests&FullyQualifiedName~Evaluate_LabeledBreakCrossingDriverLoop_DeclinesWithLabelControlFlow"` |
381-
| `prototype-guard:JumpIfFalse` | `JumpIfFalse` and short-circuit conditional jump opcodes are admitted only for proven compiler-owned expression and statement control flow | Existing control-flow route if an unowned conditional shape is introduced | Control-flow lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionEligibilityTests&FullyQualifiedName~Evaluate_ConditionalExpression_ThisPropertyConditionAndArms_DeclinesWithPropertyReadBoundaryOutOfScope"` |
381+
| `prototype-guard:JumpIfFalse` | `JumpIfFalse` and short-circuit conditional jump opcodes are admitted only for proven compiler-owned expression and statement control flow | Existing control-flow route if an unowned conditional shape is introduced | Control-flow lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionEligibilityTests&FullyQualifiedName~Evaluate_ConditionalExpression_ThisPropertyConditionAndArms_Accepts"` |
382382
| `prototype-guard:DefaultUnsupportedOpcode` | Any future `UnifiedBytecodeOpCode` not explicitly listed in the switch declines as `UnsupportedPlanShape` until the selector, compiler, VM, and proof pack are updated together | Existing execution-plan route | Opcode ownership lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~ExpressionProgramCoverageMapTests&FullyQualifiedName~UnifiedBytecodeExpansionContract_ListsRequiredHeadingsAndCurrentEnums"` |
383383

384384
### Statement Diagnostics Supported Kinds (current)

src/Asynkron.JsEngine/Execution/UnifiedBytecode/UnifiedBytecodeProductionEligibility.cs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1063,6 +1063,15 @@ private static bool TryFindExpressionDecline(
10631063
break;
10641064
}
10651065

1066+
if (TryIsConditionalExpressionActivationResolvedNamedPropertyReadOperand(
1067+
program,
1068+
operationIndex,
1069+
identifierConstants,
1070+
activationSlots))
1071+
{
1072+
break;
1073+
}
1074+
10661075
if (TryIsFirstBoundaryOptionalNamedChainCandidate(program, identifierConstants, activationSlots))
10671076
{
10681077
break;
@@ -1893,6 +1902,53 @@ private static bool TryIsFirstBoundaryBinaryNamedPropertyReadCandidate(
18931902
return true;
18941903
}
18951904

1905+
private static bool TryIsConditionalExpressionActivationResolvedNamedPropertyReadOperand(
1906+
ExpressionProgram program,
1907+
int operationIndex,
1908+
ReadOnlySpan<IdentifierOperand> identifierConstants,
1909+
ActivationSlotShape activationSlots)
1910+
{
1911+
if (FindFirstOperation(program, ExpressionOpKind.JumpIfConditionalFalse) < 0)
1912+
{
1913+
return false;
1914+
}
1915+
1916+
var stringConstants = program.StringConstants.AsSpan();
1917+
var operation = program.GetOperation(operationIndex);
1918+
if (operation.Kind != ExpressionOpKind.GetNamedProperty ||
1919+
operation.IsOptional ||
1920+
operation.ShortCircuitOnNullishTarget ||
1921+
operation.GetString(stringConstants).IsPrivateName())
1922+
{
1923+
return false;
1924+
}
1925+
1926+
var chainStartIndex = operationIndex;
1927+
while (chainStartIndex > 0)
1928+
{
1929+
var previous = program.GetOperation(chainStartIndex - 1);
1930+
if (previous.Kind != ExpressionOpKind.GetNamedProperty)
1931+
{
1932+
break;
1933+
}
1934+
1935+
if (previous.IsOptional ||
1936+
previous.ShortCircuitOnNullishTarget ||
1937+
previous.GetString(stringConstants).IsPrivateName())
1938+
{
1939+
return false;
1940+
}
1941+
1942+
chainStartIndex--;
1943+
}
1944+
1945+
return chainStartIndex > 0 &&
1946+
TryGetActivationResolvedValue(
1947+
program.GetOperation(chainStartIndex - 1),
1948+
identifierConstants,
1949+
activationSlots);
1950+
}
1951+
18961952
/// <summary>
18971953
/// Returns <c>true</c> when the <see cref="ExpressionOpKind.GetNamedProperty"/> operation at
18981954
/// <paramref name="operationIndex"/> is the last op before a logical short-circuit jump

tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionEligibilityTests.cs

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6289,17 +6289,8 @@ function classify(c1, c2, a, b, d) {
62896289
}
62906290

62916291
[Fact]
6292-
public void Evaluate_ConditionalExpression_ThisPropertyConditionAndArms_DeclinesWithPropertyReadBoundaryOutOfScope()
6293-
{
6294-
// AC-5 deviation (intentional): AC-5 originally required an accept test for this.flag ? a : b.
6295-
// TryIsFirstBoundaryPropertyReadShortCircuitExpressionCandidate handles only logical short-circuit
6296-
// opcodes (JumpIfFalse/JumpIfTrue/JumpIfNotNullish) — not JumpIfConditionalFalse, which is the
6297-
// ternary condition opcode. The ternary condition is not a "short-circuit property read" candidate;
6298-
// the property value is consumed as a boolean condition, not returned directly. Extending the
6299-
// method to admit JumpIfConditionalFalse would be architecturally incorrect. This decline test
6300-
// documents the boundary as the intentional AC-5 resolution.
6301-
// this.flag ? this.a : other has GetNamedProperty in ternary condition position,
6302-
// which is outside the admitted property-read boundary shapes for ternary.
6292+
public void Evaluate_ConditionalExpression_ThisPropertyConditionAndArms_Accepts()
6293+
{
63036294
var plan = GetClassMethodPlan("""
63046295
class Toggle {
63056296
select(other) {
@@ -6314,8 +6305,12 @@ class Toggle {
63146305
plan,
63156306
new UnifiedBytecodeProductionActivationDescriptor());
63166307

6317-
Assert.False(result.IsEligible);
6318-
Assert.Equal(UnifiedBytecodeProductionDeclineCode.PropertyReadBoundaryOutOfScope, result.Code);
6308+
Assert.True(result.IsEligible, result.Reason);
6309+
Assert.Equal(UnifiedBytecodeProductionDeclineCode.None, result.Code);
6310+
Assert.Contains(result.Program.Instructions, instruction =>
6311+
instruction.OpCode == UnifiedBytecodeOpCode.JumpIfFalse);
6312+
Assert.Contains(result.Program.Instructions, instruction =>
6313+
instruction.OpCode == UnifiedBytecodeOpCode.GetNamedProperty);
63196314
}
63206315

63216316
[Fact]

tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionInvocationTests.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7838,6 +7838,32 @@ static record => record.Message.Contains(
78387838
StringComparison.Ordinal));
78397839
}
78407840

7841+
[Fact(Timeout = 5000)]
7842+
public async Task Ternary_ThisPropertyConditionAndArm_UsesProductionFastPath()
7843+
{
7844+
await using var engine = CreateEngine();
7845+
var result = await engine.Evaluate("""
7846+
class Toggle {
7847+
constructor(flag, value) {
7848+
this.flag = flag;
7849+
this.value = value;
7850+
}
7851+
7852+
select(other) {
7853+
return this.flag ? this.value : other;
7854+
}
7855+
}
7856+
7857+
[new Toggle(true, 42).select(10), new Toggle(false, 42).select(10)].join(",");
7858+
""");
7859+
7860+
Assert.Equal("42,10", result?.ToString());
7861+
Assert.Contains(CurrentLogger!.Collector.Snapshot(),
7862+
static record => record.Message.Contains(
7863+
"unified-bytecode-production-fast-path func=<anonymous>",
7864+
StringComparison.Ordinal));
7865+
}
7866+
78417867
[Fact(Timeout = 5000)]
78427868
public async Task Ternary_ConditionIsConsumedNotPeeked_BothBranchesCorrect()
78437869
{

0 commit comments

Comments
 (0)