diff --git a/docs/adrs/0234-keep-unified-bytecode-property-writes-strict-and-directive-owned.md b/docs/adrs/0234-keep-unified-bytecode-property-writes-strict-and-directive-owned.md index 9779e8fbd..f0b5e57ee 100644 --- a/docs/adrs/0234-keep-unified-bytecode-property-writes-strict-and-directive-owned.md +++ b/docs/adrs/0234-keep-unified-bytecode-property-writes-strict-and-directive-owned.md @@ -2,7 +2,8 @@ ## Status -Accepted +Accepted; superseded in part by ADR 0252 for non-directive discarded +expression statements. ## Context @@ -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. @@ -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. diff --git a/docs/rules/unified-bytecode-prototypes.md b/docs/rules/unified-bytecode-prototypes.md index 9615af808..885750d28 100644 --- a/docs/rules/unified-bytecode-prototypes.md +++ b/docs/rules/unified-bytecode-prototypes.md @@ -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 diff --git a/docs/unified-bytecode-expansion-contract.md b/docs/unified-bytecode-expansion-contract.md index b7e56b443..8ee24f948 100644 --- a/docs/unified-bytecode-expansion-contract.md +++ b/docs/unified-bytecode-expansion-contract.md @@ -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. diff --git a/tests/Asynkron.JsEngine.Tests/ExpressionProgramCoverageMapTests.cs b/tests/Asynkron.JsEngine.Tests/ExpressionProgramCoverageMapTests.cs index e263480ee..27dadced8 100644 --- a/tests/Asynkron.JsEngine.Tests/ExpressionProgramCoverageMapTests.cs +++ b/tests/Asynkron.JsEngine.Tests/ExpressionProgramCoverageMapTests.cs @@ -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, @@ -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() { diff --git a/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionEligibilityTests.cs b/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionEligibilityTests.cs index 8f2dd3424..8c4c765db 100644 --- a/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionEligibilityTests.cs +++ b/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionEligibilityTests.cs @@ -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( """ diff --git a/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionInvocationTests.cs b/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionInvocationTests.cs index 97ffb24c0..134585215 100644 --- a/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionInvocationTests.cs +++ b/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionInvocationTests.cs @@ -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() {