Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 20 additions & 15 deletions docs/unified-bytecode-expansion-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ statement interpretation.
- `PrepareIdentifierCallTarget`
- `PrepareIdentifierOptionalCallTarget`
- `PrepareDynamicIdentifierCallTarget`
- `PrepareDynamicIdentifierOptionalCallTarget`
- `PrepareNamedCallTarget`
- `PrepareComputedCallTarget`
- `PrepareNamedOptionalCallTarget`
Expand Down Expand Up @@ -334,7 +335,7 @@ must still obey the no-mixed-execution rule.
| `ArgumentsObjectDependency` | Activation descriptor gate for unbounded real `arguments` object dependency; implicit `arguments` reads/assignments/updates/calls materialize the existing arguments object and use the bounded identifier route, while parameter/lexical `arguments` reads, `typeof`, assignments, updates, and call targets route as activation slots | Existing sync IR / arguments-object route | Arguments object lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionEligibilityTests&FullyQualifiedName~Evaluate_CallImplicitArgumentsObject_AcceptsDynamicIdentifierCallTarget"` |
| `ArrowLexicalThisDependency` | Activation descriptor gate for arrow lexical `this` / `new.target` ownership before ordinary sync routing | Existing arrow invocation route | Arrow route lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionEligibilityTests&FullyQualifiedName~Evaluate_OrdinarySyncActivationDescriptorBlockers_DeclineBeforeCompile"` |
| `ClassConstructorActivation` | Activation descriptor gate for class constructor activation outside the admitted simple base constructor route, explicit derived-constructor `super(...)` route with post-super `this` body reads/writes, and default-derived constructor `super(...args)` forwarding route | Constructor route | Constructor boundary lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionConstructCallTests&FullyQualifiedName~Constructor"` |
| `CallDependency` | Direct eval outside the one-argument non-spread eval-identifier boundary, out-of-boundary call-target preparation, and complex call arguments excluding admitted simple/binary template-literal substitutions, simple/binary computed object keys, zero-argument activation-resolved identifier-call computed object keys, and simple-return captured identifier calls | Existing sync IR call route | Wider call invocation lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionEligibilityTests"` |
| `CallDependency` | Direct eval outside the one-argument non-spread eval-identifier boundary, out-of-boundary call-target preparation, and complex call arguments excluding admitted simple/binary template-literal substitutions, simple/binary computed object keys, zero-argument activation-resolved identifier-call computed object keys, simple-return captured identifier calls, and simple-return captured optional identifier calls | Existing sync IR call route | Wider call invocation lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionEligibilityTests"` |
| `DynamicLookupDependency` | Unresolved identifier loads/stores/update outside the admitted ordinary, with-backed, direct-eval-backed, and simple-return captured-closure dynamic-name paths; plans that need direct eval plus post-eval dynamic identifier reads now route when captured activation, with-chain, and arguments-object dependencies are absent | Existing sync IR / environment lookup route | Dynamic-name lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionEligibilityTests&FullyQualifiedName~Evaluate_DirectEvalDeclaredVarRead_AcceptsOrdinaryDynamicNameProgram"` |
| `PropertyReadBoundaryOutOfScope` | Named/computed property reads outside the admitted activation-resolved and read-only dynamic-identifier-base boundaries, including captured closure dynamic bases | Existing sync IR property route | Property read widening lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionEligibilityTests&FullyQualifiedName~Evaluate_ComputedPropertyReadOutsideFirstBoundary_DeclinesWithBoundaryCode"` |
| `PropertyWriteDependency` | Property writes and compound/logical property writes outside the admitted direct property-write shapes, supported computed expression-key mutation shapes, simple-return dynamic-base named/computed property writes and compound/logical writes, simple nested named receiver assignment shape, nested named compound-write shape, and nested named logical-write shape | Existing sync IR property-write route | Property write widening lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionEligibilityTests&FullyQualifiedName~Evaluate_LogicalAndAssignment_UnsupportedShapes_DeclineWithExplicitCodes"` |
Expand Down Expand Up @@ -604,14 +605,15 @@ the final post-compile production subset check before VM entry.
invokes `[[Construct]]` with the caller's `new.target`, initializes `this`,
and preserves the existing double-super-call `ReferenceError`.
- Accepted identifier-call programs use `PrepareIdentifierCallTarget`,
`PrepareIdentifierOptionalCallTarget`, or
`PrepareDynamicIdentifierCallTarget` followed by `CallInvocationBoundary`;
the VM resolves the callable from unified bytecode-owned slot state or the
active dynamic environment and invokes it through existing callable invocation
helpers with the active `EvaluationContext` and caller `JsEnvironment` when
the callee needs environment-aware or debug-aware invocation state. The
optional variant packs a nullish short-circuit jump target and jumps past
argument lowering and the call boundary when the callee slot is nullish.
`PrepareIdentifierOptionalCallTarget`, `PrepareDynamicIdentifierCallTarget`,
or `PrepareDynamicIdentifierOptionalCallTarget` followed by
`CallInvocationBoundary`; the VM resolves the callable from unified
bytecode-owned slot state or the active dynamic environment and invokes it
through existing callable invocation helpers with the active
`EvaluationContext` and caller `JsEnvironment` when the callee needs
environment-aware or debug-aware invocation state. The optional variants pack
a nullish short-circuit jump target and jump past argument lowering and the
call boundary when the callee is nullish.
- Accepted named member-call programs use `PrepareNamedCallTarget` followed by
`CallInvocationBoundary`; the VM keeps the final receiver on the stack, loads
the named callee from that receiver, and invokes through the existing
Expand All @@ -637,17 +639,20 @@ the final post-compile production subset check before VM entry.
- Accepted optional call programs either use a jump-owned optional-start prefix
followed by an ordinary call-target preparation opcode (`a?.b.c(args)`,
`a.x?.b.c(args)`, `a?.b[k](args)`) or use
`PrepareIdentifierOptionalCallTarget`, `PrepareNamedOptionalCallTarget`, or
`PrepareComputedOptionalCallTarget` followed by `CallInvocationBoundary`.
`PrepareIdentifierOptionalCallTarget`,
`PrepareDynamicIdentifierOptionalCallTarget`,
`PrepareNamedOptionalCallTarget`, or `PrepareComputedOptionalCallTarget`
followed by `CallInvocationBoundary`.
The optional preparation opcodes pack a call-target constant index (low 16
bits) and a nullish short-circuit jump target (high 16 bits) into a single
operand. The VM checks the receiver or callee for nullish before argument
Comment on lines 646 to 648
evaluation proceeds; if nullish, the stack top becomes `Undefined` and
execution jumps past the call boundary. Admitted callee/receiver-optional
patterns include callee-optional identifier calls (`fn?.(args)`),
receiver-optional named calls (`box?.read(args)`), callee-optional named
calls (`box.read?.(args)`), and callee-optional computed calls
(`box[key]?.(args)`). The `IsOptionalReceiverCheck` flag in
patterns include activation-resolved and captured/dynamic callee-optional
identifier calls (`fn?.(args)`), receiver-optional named calls
(`box?.read(args)`), callee-optional named calls (`box.read?.(args)`), and
callee-optional computed calls (`box[key]?.(args)`). The
`IsOptionalReceiverCheck` flag in
`UnifiedBytecodeCallTarget` distinguishes the two named variants. (Optional
member calls admitted as of gh2689; ADR 0289.)
- Direct eval programs use `PrepareIdentifierCallTarget` followed by
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3280,6 +3280,7 @@ UnifiedBytecodeOpCode.UpdateDynamicIdentifier or
UnifiedBytecodeOpCode.TypeOfDynamicIdentifier or
UnifiedBytecodeOpCode.DeleteDynamicIdentifier or
UnifiedBytecodeOpCode.PrepareDynamicIdentifierCallTarget or
UnifiedBytecodeOpCode.PrepareDynamicIdentifierOptionalCallTarget or
UnifiedBytecodeOpCode.ApplyBindingTarget or
UnifiedBytecodeOpCode.ApplyDeclarationBindingTarget or
UnifiedBytecodeOpCode.DeclareClass or
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4815,6 +4815,7 @@ private static bool TryAppendFirstBoundaryCallTargetPreparation(
callTargetConstants,
call,
callIndex,
allowsDynamicIdentifiers,
out reason))
{
return true;
Expand Down Expand Up @@ -4985,6 +4986,7 @@ private static bool TryAppendCalleeOptionalIdentifierCallTarget(
ImmutableArray<UnifiedBytecodeCallTarget>.Builder callTargetConstants,
PackedExpressionOp call,
int callIndex,
bool allowsDynamicIdentifiers,
out string reason)
{
if (callIndex < 2)
Expand All @@ -5010,10 +5012,42 @@ private static bool TryAppendCalleeOptionalIdentifierCallTarget(
var identifier = callTarget.GetIdentifier(expressionProgram.IdentifierConstants.AsSpan());
if (!TryResolveActivationCallTargetSlot(identifier, slotLayout, out var slotIndex))
{
reason = callTarget.IsArguments
? "arguments call targets are outside the optional identifier call-target preparation boundary."
: "Optional identifier call targets require an activation-resolved identifier slot.";
return false;
if (callTarget.IsArguments ||
!allowsDynamicIdentifiers ||
identifier.FlatSlotId >= 0)
{
reason = callTarget.IsArguments
? "arguments call targets are outside the optional identifier call-target preparation boundary."
: "Optional identifier call targets require an activation-resolved identifier slot or admitted dynamic identifier operations.";
return false;
}

var dynamicNameIndex = stringConstants.Count;
stringConstants.Add(identifier.Name.Name ?? string.Empty);
var dynamicPrepareIndex = unified.Count;
unified.Add(new UnifiedBytecodeInstruction(
UnifiedBytecodeOpCode.PrepareDynamicIdentifierOptionalCallTarget,
dynamicNameIndex));

if (!TryAppendCallArguments(
expressionProgram,
slotLayout,
unified,
literalConstants,
stringConstants,
callTargetConstants,
argsStartIndex: 2,
call,
callIndex,
out reason))
{
return false;
}

unified[dynamicPrepareIndex] = new UnifiedBytecodeInstruction(
UnifiedBytecodeOpCode.PrepareDynamicIdentifierOptionalCallTarget,
dynamicNameIndex | (unified.Count << 16));
return true;
}

var nameIndex = stringConstants.Count;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2926,7 +2926,11 @@ private static bool TryIsFirstBoundaryCallTargetPreparationCandidate(
// Jump/SwapTopTwo/Pop structure that the non-optional branches below would
// reject (or never reach, because they end in Pop rather than Call). Detect
// them first so the dedicated optional candidates own these shapes.
if (TryIsFirstBoundaryCalleeOptionalIdentifierCallCandidate(program, identifierConstants, activationSlots) ||
if (TryIsFirstBoundaryCalleeOptionalIdentifierCallCandidate(
program,
identifierConstants,
activationSlots,
allowsDynamicIdentifiers) ||
TryIsFirstBoundaryReceiverOptionalNamedCallCandidate(program, identifierConstants, stringConstants, activationSlots) ||
TryIsFirstBoundaryCalleeOptionalNamedCallCandidate(program, identifierConstants, stringConstants, activationSlots) ||
TryIsFirstBoundaryCalleeOptionalComputedCallCandidate(program, identifierConstants, activationSlots) ||
Expand Down Expand Up @@ -3096,7 +3100,8 @@ private static bool IsDirectEvalSingleArgumentCandidate(PackedExpressionOp opera
private static bool TryIsFirstBoundaryCalleeOptionalIdentifierCallCandidate(
ExpressionProgram program,
ReadOnlySpan<IdentifierOperand> identifierConstants,
ActivationSlotShape activationSlots)
ActivationSlotShape activationSlots,
bool allowsDynamicIdentifiers)
{
if (program.OperationCount < 6)
{
Expand Down Expand Up @@ -3127,7 +3132,8 @@ private static bool TryIsFirstBoundaryCalleeOptionalIdentifierCallCandidate(
}

var identifier = callTarget.GetIdentifier(identifierConstants);
return TryResolveActivationSlot(identifier, activationSlots) &&
return (TryResolveActivationSlot(identifier, activationSlots) ||
allowsDynamicIdentifiers && identifier.FlatSlotId < 0) &&
HasSimpleCallArguments(
program,
identifierConstants,
Expand Down Expand Up @@ -5280,6 +5286,7 @@ private static bool TryFindPrototypeOnlyOpcode(
case UnifiedBytecodeOpCode.PrepareIdentifierCallTarget:
case UnifiedBytecodeOpCode.PrepareIdentifierOptionalCallTarget:
case UnifiedBytecodeOpCode.PrepareDynamicIdentifierCallTarget:
case UnifiedBytecodeOpCode.PrepareDynamicIdentifierOptionalCallTarget:
case UnifiedBytecodeOpCode.PrepareNamedCallTarget:
case UnifiedBytecodeOpCode.PrepareComputedCallTarget:
case UnifiedBytecodeOpCode.PrepareNamedOptionalCallTarget:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ internal enum UnifiedBytecodeOpCode : byte
PrepareIdentifierCallTarget,
PrepareIdentifierOptionalCallTarget,
PrepareDynamicIdentifierCallTarget,
PrepareDynamicIdentifierOptionalCallTarget,
PrepareNamedCallTarget,
PrepareComputedCallTarget,
PrepareNamedOptionalCallTarget,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,41 @@ bool TryHandleCurrentContextThrow(Span<JsValue> currentSlots)
programCounter++;
break;

case UnifiedBytecodeOpCode.PrepareDynamicIdentifierOptionalCallTarget:
{
var dynamicOptionalCallEnvironment = RequireDynamicEnvironment(currentCallingEnvironment);
var dynamicOptionalNameIndex = instruction.Operand & 0xFFFF;
var dynamicOptionalJumpTarget = instruction.Operand >> 16;
PrepareDynamicIdentifierCallTarget(
program.StringConstants[dynamicOptionalNameIndex],
dynamicOptionalCallEnvironment,
stack,
ref stackPointer,
context);
ClearTopTwoShortCircuitFlags();
if (context.ShouldStopEvaluation)
{
if (TryHandleCurrentContextThrow(slots))
{
break;
}

return JsValue.Undefined;
}

var dynamicOptionalCallable = stack[stackPointer - 1];
if (dynamicOptionalCallable.IsNullOrUndefined)
{
stackPointer -= 2;
PushValue(JsValue.Undefined);
programCounter = dynamicOptionalJumpTarget;
break;
}

programCounter++;
break;
}

case UnifiedBytecodeOpCode.PrepareNamedCallTarget:
var namedCallTarget = program.CallTargetConstants[instruction.Operand];
if (namedCallTarget.Kind != UnifiedBytecodeCallTargetKind.NamedMember ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1130,6 +1130,48 @@ function invoke(fn, args) {
Assert.NotEmpty(result.Program.CallSpreadMasks);
}

[Fact]
public void Evaluate_DynamicOptionalIdentifierCallTarget_AcceptsEnvironmentOpcode()
{
var plan = GetFunctionPlan("""
function invoke(value) {
return externalFn?.(value);
}
""",
"invoke");

var result = UnifiedBytecodeProductionEligibility.Evaluate(
plan,
new UnifiedBytecodeProductionActivationDescriptor(
AllowsOrdinaryDynamicIdentifierEnvironmentOperations: true));

Assert.True(result.IsEligible, result.Reason);
Assert.Equal(UnifiedBytecodeProductionDeclineCode.None, result.Code);
Assert.Contains(result.Program.Instructions, instruction =>
instruction.OpCode == UnifiedBytecodeOpCode.PrepareDynamicIdentifierOptionalCallTarget);
Assert.Contains(result.Program.Instructions, instruction =>
instruction.OpCode == UnifiedBytecodeOpCode.CallInvocationBoundary);
}

[Fact]
public void Evaluate_DynamicOptionalIdentifierCallTarget_WithComplexArgument_DeclinesOptionalChainDependency()
{
var plan = GetFunctionPlan("""
function invoke(value) {
return externalFn?.(value + 1);
}
""",
"invoke");

var result = UnifiedBytecodeProductionEligibility.Evaluate(
plan,
new UnifiedBytecodeProductionActivationDescriptor(
AllowsOrdinaryDynamicIdentifierEnvironmentOperations: true));

Assert.False(result.IsEligible);
Assert.Equal(UnifiedBytecodeProductionDeclineCode.OptionalChainDependency, result.Code);
}

[Fact]
public void Evaluate_NamedPropertyReadCandidate_AcceptsOwnedPropertyOpcode()
{
Expand Down
Loading