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
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

## Status

Accepted
Accepted; superseded in part by ADR 0252 for non-directive discarded
expression statements.

## Context

Expand Down Expand Up @@ -44,7 +45,10 @@ compiler.
3. Allow strict directive prologues by treating only string-literal
`EvaluateAndDiscard` instructions as no-op directive discards in the
unified compiler.
4. Keep every non-directive discarded expression declined before VM execution.
4. Historical boundary, superseded by ADR 0252: do not keep a blanket
non-directive discard decline. `EvaluateAndDiscard` may route when the
underlying expression operation family is already selector/compiler/VM-owned,
and the compiler appends `Pop` after the owned expression program.
5. Prove strict/sloppy failed writes with public invocation logs for both arms,
so the strict function's TypeError behavior is known to come from the owned
unified path.
Expand All @@ -57,9 +61,9 @@ compiler.
- Strict failed writes now throw through owned unified property set/update
semantics instead of accidentally inheriting sloppy behavior from the current
outer scope.
- Directive prologues do not authorize generic expression-discard support.
Supporting arbitrary discarded expressions still needs its own selector,
compiler, VM, and route-proof slice.
- Directive prologues do not authorize generic expression-discard support, but
ADR 0252 later admitted discarded expression statements whose underlying
operation families already have selector, compiler, VM, and route proof.
- Property-write proof tests must prove both semantics and route selection.
A passing behavior test is not enough when the accepted function body could
have declined and executed through an older path.
Expand Down
8 changes: 5 additions & 3 deletions docs/rules/unified-bytecode-prototypes.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,9 +250,11 @@ all-or-nothing until a separate routing issue proves production readiness.
create the normal function `JsEnvironment`, so property handle resolution
must not rely only on `context.CurrentScope.IsStrict`. Directive prologue
support in the compiler must stay no-op and narrow: only string-literal
`EvaluateAndDiscard` instructions may be skipped to reach the owned return
payload, while non-directive discarded expressions still decline before VM
execution. For computed write proofs, keep the admitted write function
`EvaluateAndDiscard` instructions may be skipped as directive prologue
no-ops. Non-directive `EvaluateAndDiscard` is now governed by rule 27 /
ADR 0252: compile the supported expression program and append `Pop`;
decline only when the underlying operation family is not yet
selector/compiler/VM-owned. For computed write proofs, keep the admitted write function
call-free and place unrelated RHS side effects at the call site if needed,
then assert evaluation order and route logging on the admitted function.
WHY: issue
Expand Down
7 changes: 7 additions & 0 deletions docs/unified-bytecode-expansion-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,13 @@ statement interpretation.
expression loop. `JumpIfShortCircuited` lowers directly to unified bytecode,
and the VM tracks a packed side flag per operand-stack slot so short-circuited
`undefined` remains distinct from ordinary `undefined`.
- There is no retained discard-specific production decline. `EvaluateAndDiscard`
compiles the supported expression program first and appends `Pop`, so
discarded statements route when their underlying operation family is already
selector/compiler/VM-owned. Remaining discarded-expression declines are
declines of the underlying family, such as dynamic lookup, private names,
unsupported optional-chain neighbors, or other semantic lanes not yet
admitted.
- The current `ExpressionOpKind` names with no unified compiler reference list is
empty. Computed super-property operations also route their required
`EnsureSuperReference` op through the general unified expression loop.
Expand Down
28 changes: 28 additions & 0 deletions tests/Asynkron.JsEngine.Tests/ExpressionProgramCoverageMapTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ public sealed class ExpressionProgramCoverageMapTests
[
];

private static readonly string[] StaleDiscardDeclinePhrases =
[
"non-directive discarded expressions still decline before VM execution",
"Keep every non-directive discarded expression declined before VM execution"
];

private sealed record ProductionUnifiedBytecodeProofPackShape(
string Key,
string ContractEvidenceText,
Expand Down Expand Up @@ -319,6 +325,28 @@ public void UnifiedBytecodeCompiler_GeneralExpressionLoopDeclinesOnlyDocumentedG
"Documented general expression lowering gaps");
}

[Fact]
public void UnifiedBytecodeDiscardDocumentation_DoesNotPreserveStaleBlanketDeclineRule()
{
var repositoryRoot = FindRepositoryRoot();
var checkedPaths = new[]
{
Path.Combine(repositoryRoot.FullName, "docs", "rules", "unified-bytecode-prototypes.md"),
Path.Combine(repositoryRoot.FullName, "docs", "adrs", "0234-keep-unified-bytecode-property-writes-strict-and-directive-owned.md"),
Path.Combine(repositoryRoot.FullName, "docs", "unified-bytecode-expansion-contract.md")
};

foreach (var checkedPath in checkedPaths)
{
Assert.True(File.Exists(checkedPath), $"Expected discard documentation at '{checkedPath}'.");
var text = File.ReadAllText(checkedPath);
foreach (var stalePhrase in StaleDiscardDeclinePhrases)
{
Assert.DoesNotContain(stalePhrase, text, StringComparison.Ordinal);
}
}
}

[Fact]
public void UnifiedBytecodeResumableEligibility_AllowsEveryResumableVmOpcode()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2282,6 +2282,31 @@ function invokeEval(source) {
instruction.OpCode == UnifiedBytecodeOpCode.CallInvocationBoundary);
}

[Fact]
public void Evaluate_DiscardedIdentifierCallCandidate_AcceptsInvocationBoundaryAndPop()
{
var plan = GetFunctionPlan("""
function invokeDiscarded(callback, value) {
callback(value);
return value;
}
""",
"invokeDiscarded");

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

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

[Theory]
[InlineData(
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1814,6 +1814,31 @@ static record => record.Message.Contains(
StringComparison.Ordinal));
}

[Fact(Timeout = 5000)]
public async Task DiscardedIdentifierCall_UsesUnifiedBytecodeProductionFastPathAndKeepsSideEffects()
{
await using var engine = CreateEngine();
var result = await engine.Evaluate("""
var hits = 0;
function observe(value) {
hits = hits + value;
}

function invokeDiscarded(callback, value) {
callback(value);
return value;
}

invokeDiscarded(observe, 42) + hits;
""");

Assert.Equal(84d, result);
Assert.Contains(CurrentLogger!.Collector.Snapshot(),
static record => record.Message.Contains(
"unified-bytecode-production-fast-path func=invokeDiscarded argc=2",
StringComparison.Ordinal));
}

[Fact(Timeout = 5000)]
public async Task DirectEvalIdentifierCall_UsesOrdinaryCallWhenEvalIsShadowed()
{
Expand Down
Loading