diff --git a/docs/adr/0101-closed-numeric-scalar-self-call-frames.md b/docs/adr/0101-closed-numeric-scalar-self-call-frames.md new file mode 100644 index 00000000..975bc2bc --- /dev/null +++ b/docs/adr/0101-closed-numeric-scalar-self-call-frames.md @@ -0,0 +1,34 @@ +# Closed numeric scalar self-call frames + +**Date:** 2026-07-21 +**Area:** `bytecode runtime` + +GocciaScript specializes direct self-calls only when the existing closed-world +numeric proof accepts the entire local arrow function and every external call. +For proven functions with one to three parameters, non-tail recursive call sites +emit `OP_CALL_SELF_NUM`. The instruction enters the same function template with +a compact scalar frame and a new register window while sharing the generic entry +frame's closure, lexical environment, local-cell and argument windows, realm, +`new.target`, and execution context. It still accounts for the ordinary stack +limit, deferred error-stack frames, function profiling, coverage, instruction +limits, timeouts, and GC-visible live registers. Tail calls continue through the +generic proper-tail-call path. + +This is a proof-backed ABI, not speculative specialization. There is no guard or +deoptimization path: escapes, mixed or wrong-arity calls, optional/spread calls, +defaults, rest/destructuring, unsupported expression forms, and functions with +more than three parameters retain `OP_CALL`. Generic function entry is +unchanged. The compiler-only proof name is not serialized; the opcode is, so the +bytecode format advances from version 74 to 75. + +On the retained untyped Fibonacci probe (25 inner iterations, nine interleaved +production samples), the scalar-frame build reduced median wall time from +241,359 us on `origin/main` to 72,211 us (70.1%), and from 228,870 us on the +numeric-superinstruction predecessor to 72,722 us (68.2%). Deterministic +one-iteration dispatch fell from 262,915 opcodes on `origin/main` to 164,408; +generic `OP_CALL` plus self `OP_GET_UPVALUE` dispatches fell from 43,785 to five +ordinary outer calls. The pinned AWFY Sieve guard remained within matched noise: +95,453 us versus 95,032 us on the predecessor (0.44% slower), and 95,254 us +versus 97,003 us on `origin/main` (1.8% faster). This narrowly satisfies the +end-to-end evidence requirement behind ADR 0089 without introducing a general +fixed-arity calling convention. diff --git a/docs/adr/README.md b/docs/adr/README.md index 841b23d5..65286c42 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -110,3 +110,4 @@ Durable architecture and implementation decisions for GocciaScript. New ADRs use - [0098 — Portable exact numeric text conversion](0098-portable-exact-numeric-text-conversion.md) - [0099 — Canonical UTF-16 text with explicit encoded-byte boundaries](0099-canonical-utf16-text-boundaries.md) - [0100 — Native binary64 execution with narrow semantic operations](0100-native-binary64-execution.md) +- [0101 — Closed numeric scalar self-call frames](0101-closed-numeric-scalar-self-call-frames.md) diff --git a/docs/bytecode-vm.md b/docs/bytecode-vm.md index 12523fd2..c847b0c7 100644 --- a/docs/bytecode-vm.md +++ b/docs/bytecode-vm.md @@ -39,6 +39,7 @@ Public bytecode artifacts use the `.gbc` extension. | VM execution | `Goccia.VM.pas` | | Frames / closures / upvalues | `Goccia.VM.CallFrame.pas`, `Goccia.VM.Closure.pas`, `Goccia.VM.Upvalue.pas` | | Bytecode executor | `Goccia.Executor.Bytecode.pas` (`TGocciaBytecodeExecutor`) | +| Numeric proof / lowering | `Goccia.Compiler.NumericProof.pas`, `Goccia.Compiler.Expressions.pas` | | Opcode name lookup | `Goccia.Bytecode.OpCodeNames.pas` | | Profiler | `Goccia.Profiler.pas`, `Goccia.Profiler.Report.pas` | @@ -116,7 +117,9 @@ Recent VM cleanup and optimization work has focused on reducing per-instruction - pre-size argument collections for calls and construction - hold call arguments in a stack-disciplined arena window (`FArgumentStack` with a base+count window, mirroring the register and local-cell stacks) instead of a per-call dynamic array, so an ordinary call performs no argument-array allocation; frame save/restore and native re-entry store `(base, count)` rather than copying - defer stack-trace frames on the hot call path: push the function-template pointer rather than copying its name/source strings, and materialise them only when a trace is captured (see [ADR 0074](adr/0074-deferred-bytecode-call-stack-frames.md)) +- execute compiler-proven closed-world numeric self-calls through `OP_CALL_SELF_NUM`: recursive calls with one to three scalar arguments use a compact register frame while sharing the generic entry frame's closure, lexical environment, local-cell and argument windows, realm, and execution context (see [ADR 0101](adr/0101-closed-numeric-scalar-self-call-frames.md)) - use unchecked template access in the dispatch loop where bounds are already guaranteed +- fuse `Number - Int16` as `OP_SUB_NUM_IMM` and conditional `Number <= Int16` as `OP_JUMP_IF_NUM_NOT_LTE_IMM` only when the compiler proves the source is an ECMAScript Number; these instructions remove literal-load and branch dispatches rather than merely replacing a generic arithmetic dispatch - keep fast register access limited to proven hot/simple paths; local-slot and complex property paths should only move to fast access when they stay correct and measurably improve throughput - the register, local-cell, and argument window fills are GC-safety/correctness critical (the GC marks the whole live window): they are deliberately retained rather than trimmed @@ -136,6 +139,18 @@ Computed property access (`OP_ARRAY_GET`/`OP_ARRAY_SET`, `OP_GET_INDEX`/`OP_SET_ The current optimization target is reducing bytecode-mode suite time further without diverging interpreter and bytecode semantics. +Numeric call-derived proof is deliberately closed-world. It applies only to a +simple, const-bound local arrow with no escape, optional call, default/rest/ +destructured parameter, mixed argument, or unsupported use. Every external +direct call must supply exactly one numeric literal per parameter, and the +expression body must establish a Number result recursively. The proof marks +parameters for the two numeric immediate superinstructions; for one to three +parameters it also emits `OP_CALL_SELF_NUM` at non-tail direct self-call sites. +The scalar frame performs no speculative type conversion or deoptimization: +unsupported shapes retain ordinary `OP_CALL`, and tail calls retain the generic +proper-tail-call path. The optimization does not turn the function into a +generally typed function or change generic `+` semantics. + ## Profiling The `--profile` option on GocciaScriptLoader enables language-level profiling of the bytecode VM. See [profiling.md](profiling.md) for the full guide. diff --git a/source/units/Goccia.Bytecode.Chunk.pas b/source/units/Goccia.Bytecode.Chunk.pas index bcf3a2f6..e91479a7 100644 --- a/source/units/Goccia.Bytecode.Chunk.pas +++ b/source/units/Goccia.Bytecode.Chunk.pas @@ -189,6 +189,10 @@ TGocciaFunctionTemplate = class FRejectArgumentsInDirectEval: Boolean; FProfileIndex: Integer; FSourceText: string; + // Compile-time-only marker for the closed-world numeric proof. The emitted + // direct self-call opcode carries the runtime contract, so this name is not + // part of the serialized bytecode format. + FClosedNumericSelfName: string; FTemplateSiteId: UInt64; FStringConstantIndex: TOrderedStringMap; // Runtime-only cache for bckTemplateObject constants. Indexed by the slot @@ -300,6 +304,8 @@ TGocciaFunctionTemplate = class property RejectArgumentsInDirectEval: Boolean read FRejectArgumentsInDirectEval write FRejectArgumentsInDirectEval; property ProfileIndex: Integer read FProfileIndex write FProfileIndex; property SourceText: string read FSourceText write FSourceText; + property ClosedNumericSelfName: string read FClosedNumericSelfName + write FClosedNumericSelfName; property TemplateSiteId: UInt64 read FTemplateSiteId; end; diff --git a/source/units/Goccia.Bytecode.OpCodeNames.pas b/source/units/Goccia.Bytecode.OpCodeNames.pas index 5a6cb682..d702417a 100644 --- a/source/units/Goccia.Bytecode.OpCodeNames.pas +++ b/source/units/Goccia.Bytecode.OpCodeNames.pas @@ -220,6 +220,9 @@ function OpCodeName(const AOp: UInt8): string; OP_SET_UPVALUE_DYNAMIC: Result := 'OP_SET_UPVALUE_DYNAMIC'; OP_WIDE: Result := 'OP_WIDE'; OP_CONSTRUCT_SPREAD: Result := 'OP_CONSTRUCT_SPREAD'; + OP_SUB_NUM_IMM: Result := 'OP_SUB_NUM_IMM'; + OP_JUMP_IF_NUM_NOT_LTE_IMM: Result := 'OP_JUMP_IF_NUM_NOT_LTE_IMM'; + OP_CALL_SELF_NUM: Result := 'OP_CALL_SELF_NUM'; else Result := Format('OP_UNKNOWN_%d', [AOp]); end; diff --git a/source/units/Goccia.Bytecode.pas b/source/units/Goccia.Bytecode.pas index 0f0f536e..839b7475 100644 --- a/source/units/Goccia.Bytecode.pas +++ b/source/units/Goccia.Bytecode.pas @@ -159,7 +159,11 @@ interface // little-endian UTF-16 code units so bytecode constants // preserve every ECMAScript string value, including lone // surrogates. - GOCCIA_FORMAT_VERSION = 73; + // v73 -> v74: added numeric immediate superinstructions for proven + // Number subtraction and less-than-or-equal branches. + // v74 -> v75: added a direct scalar-frame self-call opcode for functions + // accepted by the closed-world numeric-call proof. + GOCCIA_FORMAT_VERSION = 75; GOCCIA_BINARY_MAGIC: array[0..3] of Byte = (Ord('G'), Ord('B'), Ord('C'), 0); GOCCIA_NULLISH_MATCH_UNDEFINED = 0; GOCCIA_NULLISH_MATCH_NULL = 1; @@ -416,7 +420,16 @@ interface // the following instruction's A/B/C operands; the base word keeps the // ordinary compact encoding. OP_WIDE = 225, - OP_CONSTRUCT_SPREAD = 226 + OP_CONSTRUCT_SPREAD = 226, + // A = destination, B = proven Number source, C = signed Int16 immediate. + OP_SUB_NUM_IMM = 227, + // A = proven Number source, B = signed Int16 immediate, + // C = signed Int16 forward jump offset when A <= B is false. + OP_JUMP_IF_NUM_NOT_LTE_IMM = 228, + // A = destination, B = first contiguous argument register, + // C = argument count (1..3). Valid only in a compiler-proven closed-world + // numeric self-recursive template. + OP_CALL_SELF_NUM = 229 ); function EncodeABC(const AOp: TGocciaOpCode; const A, B, C: UInt16): UInt64; {$IFDEF FPC}inline;{$ENDIF} diff --git a/source/units/Goccia.Compiler.Context.pas b/source/units/Goccia.Compiler.Context.pas index c56a89f2..a216ad4b 100644 --- a/source/units/Goccia.Compiler.Context.pas +++ b/source/units/Goccia.Compiler.Context.pas @@ -5,6 +5,7 @@ interface uses + Generics.Collections, SysUtils, HashMap, @@ -29,12 +30,19 @@ interface TSetNonStrictModeProc = procedure(const AEnabled: Boolean) of object; TFormalParameterCountMap = THashMap; + TClosedNumericCallProof = record + ParameterMask: UInt64; + FunctionName: string; + end; + TNumericParameterProofMap = TDictionary; TGocciaCompilationContext = record Template: TGocciaFunctionTemplate; Scope: TGocciaCompilerScope; SourcePath: string; FormalParameterCounts: TFormalParameterCountMap; + NumericParameterProofs: TNumericParameterProofMap; GlobalBackedTopLevel: Boolean; PreinitializedTopLevelFunctions: Boolean; StrictTypes: Boolean; @@ -184,6 +192,24 @@ procedure PatchJumpTarget(const ACtx: TGocciaCompilationContext; ACtx.Template.PatchInstruction(AIndex, EncodeABC(TGocciaOpCode(Op), A, B, UInt16(Offset))); end + else if TGocciaOpCode(Op) = OP_JUMP_IF_NUM_NOT_LTE_IMM then + begin + A := DecodeA(Instruction); + B := DecodeB(Instruction); + if (AIndex > 0) and + (DecodeOp(ACtx.Template.GetInstruction(AIndex - 1)) = Ord(OP_WIDE)) then + begin + A := A or (UInt16(DecodeA( + ACtx.Template.GetInstruction(AIndex - 1))) shl 8); + B := B or (UInt16(DecodeB( + ACtx.Template.GetInstruction(AIndex - 1))) shl 8); + end; + if (Offset < Low(Int16)) or (Offset > High(Int16)) then + raise Exception.Create('Numeric immediate jump offset exceeds 16-bit range'); + ACtx.Template.PatchInstruction(AIndex, + EncodeABC(OP_JUMP_IF_NUM_NOT_LTE_IMM, A, B, + UInt16(Int16(Offset)))); + end else begin A := DecodeA(Instruction); diff --git a/source/units/Goccia.Compiler.Expressions.pas b/source/units/Goccia.Compiler.Expressions.pas index 355cd252..71c105e2 100644 --- a/source/units/Goccia.Compiler.Expressions.pas +++ b/source/units/Goccia.Compiler.Expressions.pas @@ -213,6 +213,43 @@ function InferredExpressionType(const AScope: TGocciaCompilerScope; Result := InferLocalType(AExpr); end; +function HasExactNumberProof(const AScope: TGocciaCompilerScope; + const AExpr: TGocciaExpression): Boolean; +var + LocalIndex: Integer; +begin + if IsKnownNumeric(ExpressionType(AScope, AExpr)) then + Exit(True); + if AExpr is TGocciaIdentifierExpression then + begin + LocalIndex := AScope.ResolveLocal( + TGocciaIdentifierExpression(AExpr).Name); + if LocalIndex >= 0 then + Exit(AScope.GetLocal(LocalIndex).IsCallProvenNumeric); + end; + Result := False; +end; + +function TrySignedInt16NumberLiteral(const AExpr: TGocciaExpression; + out AImmediate: Int16): Boolean; +var + NumberValue: Double; +begin + Result := False; + if not (AExpr is TGocciaLiteralExpression) or + not (TGocciaLiteralExpression(AExpr).Value is + TGocciaNumberLiteralValue) then + Exit; + NumberValue := TGocciaNumberLiteralValue( + TGocciaLiteralExpression(AExpr).Value).Value; + if IsNan(NumberValue) or IsInfinite(NumberValue) or + (NumberValue <> Trunc(NumberValue)) or + (NumberValue < Low(Int16)) or (NumberValue > High(Int16)) then + Exit; + AImmediate := Int16(Trunc(NumberValue)); + Result := True; +end; + function IsAnonymousFunctionNameExpression( const AExpr: TGocciaExpression): Boolean; begin @@ -1954,6 +1991,7 @@ procedure CompileBinary(const ACtx: TGocciaCompilationContext; JumpIdx, JumpIdx2: Integer; LeftType, RightType: TGocciaLocalType; PrivateExpr: TGocciaPrivateMemberExpression; + Immediate: Int16; begin if TryFoldBinary(ACtx, AExpr, ADest) then Exit; @@ -2001,6 +2039,18 @@ procedure CompileBinary(const ACtx: TGocciaCompilationContext; Exit; end; + if (AExpr.Operator = gttMinus) and + HasExactNumberProof(ACtx.Scope, AExpr.Left) and + TrySignedInt16NumberLiteral(AExpr.Right, Immediate) then + begin + RegB := ACtx.Scope.AllocateRegister; + ACtx.CompileExpression(AExpr.Left, RegB); + EmitInstruction(ACtx, EncodeABC(OP_SUB_NUM_IMM, ADest, RegB, + UInt16(Immediate))); + ACtx.Scope.FreeRegister; + Exit; + end; + RegB := ACtx.Scope.AllocateRegister; RegC := ACtx.Scope.AllocateRegister; @@ -3626,6 +3676,8 @@ procedure CompileArrowFunction(const ACtx: TGocciaCompilationContext; RestReg: Integer; I: Integer; OldDerivedGuard: Boolean; + NumericProof: TClosedNumericCallProof; + NumericLocalIndex: Integer; begin OldTemplate := ACtx.Template; OldScope := ACtx.Scope; @@ -3661,6 +3713,20 @@ procedure CompileArrowFunction(const ACtx: TGocciaCompilationContext; else ChildScope.DeclareLocal(AExpr.Parameters[I].Name, False); end; + if Assigned(ACtx.NumericParameterProofs) and + ACtx.NumericParameterProofs.TryGetValue(AExpr, + NumericProof) then + begin + if Length(AExpr.Parameters) <= 3 then + ChildTemplate.ClosedNumericSelfName := NumericProof.FunctionName; + for I := 0 to High(AExpr.Parameters) do + if (NumericProof.ParameterMask and (UInt64(1) shl I)) <> 0 then + begin + NumericLocalIndex := ChildScope.ResolveLocal(AExpr.Parameters[I].Name); + if NumericLocalIndex >= 0 then + ChildScope.SetLocalCallProvenNumeric(NumericLocalIndex, True); + end; + end; for I := 0 to High(AExpr.Parameters) do if AExpr.Parameters[I].IsPattern and Assigned(AExpr.Parameters[I].Pattern) then CollectDestructuringBindings(AExpr.Parameters[I].Pattern, ChildScope); @@ -4188,7 +4254,7 @@ procedure CompileCall(const ACtx: TGocciaCompilationContext; const ATail: Boolean = False); var ArgCount, I: Integer; - BaseReg, ObjReg, ArgsReg, SuperReg, KeyReg: UInt16; + BaseReg, ObjReg, ArgsReg, SuperReg, KeyReg, FirstArgReg: UInt16; ThisLocalIdx: Integer; ThisUpvalIdx: Integer; ThisLocal: TGocciaCompilerLocal; @@ -4201,13 +4267,37 @@ procedure CompileCall(const ACtx: TGocciaCompilationContext; IgnoredJumpCount: Integer; MethodTailFlag: UInt16; begin - if TryCompileOptionalChainCall(ACtx, AExpr, ADest) then - Exit; - ArgCount := AExpr.Arguments.Count; if ArgCount > High(UInt16) then raise Exception.Create('Compiler error: too many arguments (>65535)'); UseSpread := HasSpreadArgument(AExpr); + + // A closed-world numeric proof establishes both the callee identity and the + // Number type of every recursive argument. Emit the scalar-frame ABI before + // generic callee resolution so the hot path does not load or dispatch the + // closure at all. Tail calls retain the generic proper-tail-call machinery. + if not ATail and not AExpr.Optional and not UseSpread and + (ArgCount >= 1) and (ArgCount <= 3) and + (ACtx.Template.ClosedNumericSelfName <> '') and + (AExpr.Callee is TGocciaIdentifierExpression) and + (TGocciaIdentifierExpression(AExpr.Callee).Name = + ACtx.Template.ClosedNumericSelfName) then + begin + FirstArgReg := ACtx.Scope.AllocateRegister; + ACtx.CompileExpression(AExpr.Arguments[0], FirstArgReg); + for I := 1 to ArgCount - 1 do + ACtx.CompileExpression(AExpr.Arguments[I], + ACtx.Scope.AllocateRegister); + EmitInstruction(ACtx, EncodeABC(OP_CALL_SELF_NUM, ADest, FirstArgReg, + UInt16(ArgCount))); + for I := 0 to ArgCount - 1 do + ACtx.Scope.FreeRegister; + Exit; + end; + + if TryCompileOptionalChainCall(ACtx, AExpr, ADest) then + Exit; + CallNilJump := -1; IgnoredJumpCount := 0; // ES2026 §15.10.2: an optional call cannot be in tail position here because @@ -4640,7 +4730,31 @@ procedure CompileConditional(const ACtx: TGocciaCompilationContext; var ElseJump, EndJump: Integer; ConditionReg: UInt16; + ConditionBinary: TGocciaBinaryExpression; + Immediate: Int16; begin + if AExpr.Condition is TGocciaBinaryExpression then + begin + ConditionBinary := TGocciaBinaryExpression(AExpr.Condition); + if (ConditionBinary.Operator = gttLessEqual) and + HasExactNumberProof(ACtx.Scope, ConditionBinary.Left) and + TrySignedInt16NumberLiteral(ConditionBinary.Right, Immediate) then + begin + ConditionReg := ACtx.Scope.AllocateRegister; + ACtx.CompileExpression(ConditionBinary.Left, ConditionReg); + ElseJump := EmitInstruction(ACtx, + EncodeABC(OP_JUMP_IF_NUM_NOT_LTE_IMM, ConditionReg, + UInt16(Immediate), 0), True); + ACtx.Scope.FreeRegister; + ACtx.CompileExpression(AExpr.Consequent, ADest); + EndJump := EmitJumpInstruction(ACtx, OP_JUMP, 0); + PatchJumpTarget(ACtx, ElseJump); + ACtx.CompileExpression(AExpr.Alternate, ADest); + PatchJumpTarget(ACtx, EndJump); + Exit; + end; + end; + ConditionReg := ACtx.Scope.AllocateRegister; ACtx.CompileExpression(AExpr.Condition, ConditionReg); ElseJump := EmitJumpInstruction(ACtx, OP_JUMP_IF_FALSE, ConditionReg); diff --git a/source/units/Goccia.Compiler.NumericProof.pas b/source/units/Goccia.Compiler.NumericProof.pas new file mode 100644 index 00000000..32cfd109 --- /dev/null +++ b/source/units/Goccia.Compiler.NumericProof.pas @@ -0,0 +1,343 @@ +unit Goccia.Compiler.NumericProof; + +{$I Goccia.inc} + +interface + +uses + Goccia.AST.Statements, + Goccia.Compiler.Context; + +// Records a proof only for a const-bound, non-escaping local arrow whose +// recursive body and every external call establish Number arguments. +procedure DiscoverClosedCallNumericProof(const ABlock: TGocciaBlockStatement; + const AProofs: TNumericParameterProofMap); + +implementation + +uses + Goccia.AST.Expressions, + Goccia.AST.Node, + Goccia.Token, + Goccia.Values.Primitives; + +function IsNumberLiteral(const AExpr: TGocciaExpression): Boolean; +begin + Result := (AExpr is TGocciaLiteralExpression) and + (TGocciaLiteralExpression(AExpr).Value is TGocciaNumberLiteralValue); +end; + +function ParameterIndex(const AParameters: TGocciaParameterArray; + const AName: string): Integer; +var + I: Integer; +begin + for I := 0 to High(AParameters) do + if AParameters[I].Name = AName then + Exit(I); + Result := -1; +end; + +function ProvesNumber(const AExpr: TGocciaExpression; + const AFunctionName: string; + const AParameters: TGocciaParameterArray): Boolean; forward; + +function ProvesPredicate(const AExpr: TGocciaExpression; + const AFunctionName: string; + const AParameters: TGocciaParameterArray): Boolean; +var + Binary: TGocciaBinaryExpression; +begin + Result := False; + if not (AExpr is TGocciaBinaryExpression) then + Exit; + Binary := TGocciaBinaryExpression(AExpr); + case Binary.Operator of + gttEqual, gttNotEqual, gttLooseEqual, gttLooseNotEqual, + gttLess, gttGreater, gttLessEqual, gttGreaterEqual: + Result := ProvesNumber(Binary.Left, AFunctionName, AParameters) and + ProvesNumber(Binary.Right, AFunctionName, AParameters); + end; +end; + +function ProvesNumber(const AExpr: TGocciaExpression; + const AFunctionName: string; + const AParameters: TGocciaParameterArray): Boolean; +var + I: Integer; + Binary: TGocciaBinaryExpression; + Conditional: TGocciaConditionalExpression; + Call: TGocciaCallExpression; + Callee: TGocciaIdentifierExpression; +begin + if IsNumberLiteral(AExpr) then + Exit(True); + + if AExpr is TGocciaIdentifierExpression then + Exit(ParameterIndex(AParameters, + TGocciaIdentifierExpression(AExpr).Name) >= 0); + + if AExpr is TGocciaBinaryExpression then + begin + Binary := TGocciaBinaryExpression(AExpr); + case Binary.Operator of + gttPlus, gttMinus, gttStar, gttSlash, gttPercent, gttPower: + Exit(ProvesNumber(Binary.Left, AFunctionName, AParameters) and + ProvesNumber(Binary.Right, AFunctionName, AParameters)); + else + Exit(False); + end; + end; + + if AExpr is TGocciaConditionalExpression then + begin + Conditional := TGocciaConditionalExpression(AExpr); + Exit(ProvesPredicate(Conditional.Condition, AFunctionName, AParameters) and + ProvesNumber(Conditional.Consequent, AFunctionName, AParameters) and + ProvesNumber(Conditional.Alternate, AFunctionName, AParameters)); + end; + + if AExpr is TGocciaCallExpression then + begin + Call := TGocciaCallExpression(AExpr); + if Call.Optional or + not (Call.Callee is TGocciaIdentifierExpression) then + Exit(False); + Callee := TGocciaIdentifierExpression(Call.Callee); + if (Callee.Name <> AFunctionName) or + (Call.Arguments.Count <> Length(AParameters)) then + Exit(False); + for I := 0 to Call.Arguments.Count - 1 do + if not ProvesNumber(Call.Arguments[I], AFunctionName, AParameters) then + Exit(False); + Exit(True); + end; + + Result := False; +end; + +function ScanExpression(const AExpr: TGocciaExpression; + const AFunctionName: string; const AParameterCount: Integer; + var ACallCount: Integer): Boolean; forward; + +function ScanCall(const ACall: TGocciaCallExpression; + const AFunctionName: string; const AParameterCount: Integer; + var ACallCount: Integer): Boolean; +var + I: Integer; +begin + if (ACall.Callee is TGocciaIdentifierExpression) and + (TGocciaIdentifierExpression(ACall.Callee).Name = AFunctionName) then + begin + if ACall.Optional or (ACall.Arguments.Count <> AParameterCount) then + Exit(False); + for I := 0 to ACall.Arguments.Count - 1 do + if not IsNumberLiteral(ACall.Arguments[I]) then + Exit(False); + Inc(ACallCount); + Exit(True); + end; + + if not ScanExpression(ACall.Callee, AFunctionName, AParameterCount, + ACallCount) then + Exit(False); + for I := 0 to ACall.Arguments.Count - 1 do + if not ScanExpression(ACall.Arguments[I], AFunctionName, + AParameterCount, ACallCount) then + Exit(False); + Result := True; +end; + +function ScanExpression(const AExpr: TGocciaExpression; + const AFunctionName: string; const AParameterCount: Integer; + var ACallCount: Integer): Boolean; +var + I: Integer; + Binary: TGocciaBinaryExpression; + Sequence: TGocciaSequenceExpression; + Conditional: TGocciaConditionalExpression; + Assignment: TGocciaAssignmentExpression; + Compound: TGocciaCompoundAssignmentExpression; +begin + if not Assigned(AExpr) then + Exit(True); + if AExpr is TGocciaLiteralExpression then + Exit(True); + if AExpr is TGocciaIdentifierExpression then + Exit(TGocciaIdentifierExpression(AExpr).Name <> AFunctionName); + if AExpr is TGocciaBinaryExpression then + begin + Binary := TGocciaBinaryExpression(AExpr); + Exit(ScanExpression(Binary.Left, AFunctionName, AParameterCount, + ACallCount) and ScanExpression(Binary.Right, AFunctionName, + AParameterCount, ACallCount)); + end; + if AExpr is TGocciaSequenceExpression then + begin + Sequence := TGocciaSequenceExpression(AExpr); + for I := 0 to Sequence.Expressions.Count - 1 do + if not ScanExpression(Sequence.Expressions[I], AFunctionName, + AParameterCount, ACallCount) then + Exit(False); + Exit(True); + end; + if AExpr is TGocciaUnaryExpression then + Exit(ScanExpression(TGocciaUnaryExpression(AExpr).Operand, + AFunctionName, AParameterCount, ACallCount)); + if AExpr is TGocciaConditionalExpression then + begin + Conditional := TGocciaConditionalExpression(AExpr); + Exit(ScanExpression(Conditional.Condition, AFunctionName, + AParameterCount, ACallCount) and + ScanExpression(Conditional.Consequent, AFunctionName, + AParameterCount, ACallCount) and + ScanExpression(Conditional.Alternate, AFunctionName, + AParameterCount, ACallCount)); + end; + if AExpr is TGocciaCallExpression then + Exit(ScanCall(TGocciaCallExpression(AExpr), AFunctionName, + AParameterCount, ACallCount)); + if AExpr is TGocciaAssignmentExpression then + begin + Assignment := TGocciaAssignmentExpression(AExpr); + Exit((Assignment.Name <> AFunctionName) and + ScanExpression(Assignment.Value, AFunctionName, AParameterCount, + ACallCount)); + end; + if AExpr is TGocciaCompoundAssignmentExpression then + begin + Compound := TGocciaCompoundAssignmentExpression(AExpr); + Exit((Compound.Name <> AFunctionName) and + ScanExpression(Compound.Value, AFunctionName, AParameterCount, + ACallCount)); + end; + if AExpr is TGocciaIncrementExpression then + Exit(ScanExpression(TGocciaIncrementExpression(AExpr).Operand, + AFunctionName, AParameterCount, ACallCount)); + Result := False; +end; + +function ScanStatement(const ANode: TGocciaASTNode; + const AFunctionName: string; const AParameterCount: Integer; + var ACallCount: Integer): Boolean; +var + I: Integer; + Block: TGocciaBlockStatement; + Declaration: TGocciaVariableDeclaration; + ForStatement: TGocciaForStatement; + IfStatement: TGocciaIfStatement; +begin + if ANode is TGocciaExpressionStatement then + Exit(ScanExpression(TGocciaExpressionStatement(ANode).Expression, + AFunctionName, AParameterCount, ACallCount)); + if ANode is TGocciaVariableDeclaration then + begin + Declaration := TGocciaVariableDeclaration(ANode); + for I := 0 to High(Declaration.Variables) do + begin + if Declaration.Variables[I].Name = AFunctionName then + Exit(False); + if Declaration.Variables[I].HasInitializer and + not ScanExpression(Declaration.Variables[I].Initializer, + AFunctionName, AParameterCount, ACallCount) then + Exit(False); + end; + Exit(True); + end; + if ANode is TGocciaBlockStatement then + begin + Block := TGocciaBlockStatement(ANode); + for I := 0 to Block.Nodes.Count - 1 do + if not ScanStatement(Block.Nodes[I], AFunctionName, AParameterCount, + ACallCount) then + Exit(False); + Exit(True); + end; + if ANode is TGocciaForStatement then + begin + ForStatement := TGocciaForStatement(ANode); + Exit(ScanStatement(ForStatement.Init, AFunctionName, AParameterCount, + ACallCount) and ScanExpression(ForStatement.Condition, AFunctionName, + AParameterCount, ACallCount) and ScanExpression(ForStatement.Update, + AFunctionName, AParameterCount, ACallCount) and + ScanStatement(ForStatement.Body, AFunctionName, AParameterCount, + ACallCount)); + end; + if ANode is TGocciaIfStatement then + begin + IfStatement := TGocciaIfStatement(ANode); + Exit(ScanExpression(IfStatement.Condition, AFunctionName, + AParameterCount, ACallCount) and ScanStatement(IfStatement.Consequent, + AFunctionName, AParameterCount, ACallCount) and + (not Assigned(IfStatement.Alternate) or + ScanStatement(IfStatement.Alternate, AFunctionName, AParameterCount, + ACallCount))); + end; + if ANode is TGocciaReturnStatement then + Exit((not TGocciaReturnStatement(ANode).HasExpression) or + ScanExpression(TGocciaReturnStatement(ANode).Value, + AFunctionName, AParameterCount, ACallCount)); + if (ANode is TGocciaEmptyStatement) or + (ANode is TGocciaBreakStatement) or + (ANode is TGocciaContinueStatement) then + Exit(True); + Result := False; +end; + +procedure DiscoverClosedCallNumericProof(const ABlock: TGocciaBlockStatement; + const AProofs: TNumericParameterProofMap); +var + I: Integer; + CallCount: Integer; + Declaration: TGocciaVariableDeclaration; + Arrow: TGocciaArrowFunctionExpression; + FunctionName: string; + Mask: UInt64; + Proof: TClosedNumericCallProof; +begin + if not Assigned(AProofs) or (ABlock.Nodes.Count < 2) or + not (ABlock.Nodes[0] is TGocciaVariableDeclaration) then + Exit; + Declaration := TGocciaVariableDeclaration(ABlock.Nodes[0]); + if not Declaration.IsConst or (Length(Declaration.Variables) <> 1) or + Declaration.Variables[0].IsPattern or + not Declaration.Variables[0].HasInitializer or + not (Declaration.Variables[0].Initializer is + TGocciaArrowFunctionExpression) then + Exit; + + FunctionName := Declaration.Variables[0].Name; + Arrow := TGocciaArrowFunctionExpression( + Declaration.Variables[0].Initializer); + if Arrow.IsAsync or (Length(Arrow.Parameters) = 0) or + (Length(Arrow.Parameters) > 64) or + not (Arrow.Body is TGocciaExpression) then + Exit; + for I := 0 to High(Arrow.Parameters) do + if Arrow.Parameters[I].IsPattern or Arrow.Parameters[I].IsRest or + Arrow.Parameters[I].IsOptional or + Assigned(Arrow.Parameters[I].DefaultValue) or + (Arrow.Parameters[I].Name = FunctionName) then + Exit; + if not ProvesNumber(TGocciaExpression(Arrow.Body), FunctionName, + Arrow.Parameters) then + Exit; + + CallCount := 0; + for I := 1 to ABlock.Nodes.Count - 1 do + if not ScanStatement(ABlock.Nodes[I], FunctionName, + Length(Arrow.Parameters), CallCount) then + Exit; + if CallCount = 0 then + Exit; + + if Length(Arrow.Parameters) = 64 then + Mask := High(UInt64) + else + Mask := (UInt64(1) shl Length(Arrow.Parameters)) - 1; + Proof.ParameterMask := Mask; + Proof.FunctionName := FunctionName; + AProofs.AddOrSetValue(Arrow, Proof); +end; + +end. diff --git a/source/units/Goccia.Compiler.Scope.pas b/source/units/Goccia.Compiler.Scope.pas index 29f1d05d..ba075864 100644 --- a/source/units/Goccia.Compiler.Scope.pas +++ b/source/units/Goccia.Compiler.Scope.pas @@ -27,6 +27,7 @@ TGocciaCompilerLocal = record IsVar: Boolean; IsGlobalBacked: Boolean; IsArrayTyped: Boolean; + IsCallProvenNumeric: Boolean; TypeHint: TGocciaLocalType; IsStrictlyTyped: Boolean; ReturnTypeHint: TGocciaLocalType; @@ -123,6 +124,8 @@ TGocciaCompilerScope = class const AStrictlyTyped: Boolean); procedure SetLocalArrayTyped(const AIndex: Integer; const AArrayTyped: Boolean); + procedure SetLocalCallProvenNumeric(const AIndex: Integer; + const AProvenNumeric: Boolean); procedure SetLocalReturnTypeHint(const AIndex: Integer; const AReturnTypeHint: TGocciaLocalType); procedure SetLocalParamTypeSignature(const AIndex: Integer; @@ -255,6 +258,7 @@ function TGocciaCompilerScope.DeclareLocal(const AName: string; FLocals[FLocalCount].IsVar := False; FLocals[FLocalCount].IsGlobalBacked := False; FLocals[FLocalCount].IsArrayTyped := False; + FLocals[FLocalCount].IsCallProvenNumeric := False; FLocals[FLocalCount].TypeHint := sltUntyped; FLocals[FLocalCount].IsStrictlyTyped := False; FLocals[FLocalCount].ReturnTypeHint := sltUntyped; @@ -307,6 +311,7 @@ function TGocciaCompilerScope.DeclareVarLocal(const AName: string): UInt16; FLocals[FLocalCount].IsVar := True; FLocals[FLocalCount].IsGlobalBacked := False; FLocals[FLocalCount].IsArrayTyped := False; + FLocals[FLocalCount].IsCallProvenNumeric := False; FLocals[FLocalCount].TypeHint := sltUntyped; FLocals[FLocalCount].IsStrictlyTyped := False; FLocals[FLocalCount].ReturnTypeHint := sltUntyped; @@ -547,6 +552,12 @@ procedure TGocciaCompilerScope.SetLocalArrayTyped(const AIndex: Integer; FLocals[AIndex].IsArrayTyped := AArrayTyped; end; +procedure TGocciaCompilerScope.SetLocalCallProvenNumeric( + const AIndex: Integer; const AProvenNumeric: Boolean); +begin + FLocals[AIndex].IsCallProvenNumeric := AProvenNumeric; +end; + procedure TGocciaCompilerScope.SetLocalTypeAnnotation(const AIndex: Integer; const AAnnotation: string); begin diff --git a/source/units/Goccia.Compiler.Test.pas b/source/units/Goccia.Compiler.Test.pas index d606278f..06419089 100644 --- a/source/units/Goccia.Compiler.Test.pas +++ b/source/units/Goccia.Compiler.Test.pas @@ -73,6 +73,7 @@ TTestCompiler = class(TTestSuite) procedure TestCompileVariable; procedure TestCompileFunction; procedure TestBinaryRoundTrip; + procedure TestBinaryRoundTripClosedNumericSelfCall; procedure TestBinaryRoundTripUpvalueNames; procedure TestBinaryLittleEndian; procedure TestBinaryRoundTripConstants; @@ -86,6 +87,10 @@ TTestCompiler = class(TTestSuite) procedure TestConstPropagationSkipsGlobalBacked; procedure TestInferredNumericLocalsUseTypedArithmetic; procedure TestAnnotatedParametersUseTypedArithmetic; + procedure TestClosedNumericFibonacciUsesSuperinstructions; + procedure TestClosedNumericScalarSelfCallArityLimit; + procedure TestMixedOrEscapedCallsCancelNumericProof; + procedure TestKnownNumericLocalUsesSubtractImmediate; procedure TestGenericAdditionDefersToPrimitiveToOpcode; procedure TestAssignmentClearsStaleNumericHint; procedure TestGlobalBackedAssignmentClearsStaleNumericHint; @@ -123,6 +128,8 @@ procedure TTestCompiler.SetupTests; Test('Compile variable', TestCompileVariable); Test('Compile function', TestCompileFunction); Test('Binary round-trip', TestBinaryRoundTrip); + Test('Binary round-trip closed numeric self-call', + TestBinaryRoundTripClosedNumericSelfCall); Test('Binary round-trip upvalue names', TestBinaryRoundTripUpvalueNames); Test('Binary little-endian format', TestBinaryLittleEndian); Test('Binary round-trip constants', TestBinaryRoundTripConstants); @@ -136,6 +143,14 @@ procedure TTestCompiler.SetupTests; Test('Const propagation skips global-backed bindings', TestConstPropagationSkipsGlobalBacked); Test('Inferred numeric locals use typed arithmetic', TestInferredNumericLocalsUseTypedArithmetic); Test('Annotated parameters use typed arithmetic', TestAnnotatedParametersUseTypedArithmetic); + Test('Closed numeric Fibonacci uses superinstructions', + TestClosedNumericFibonacciUsesSuperinstructions); + Test('Closed numeric scalar self-call supports only small arities', + TestClosedNumericScalarSelfCallArityLimit); + Test('Mixed or escaped calls cancel numeric proof', + TestMixedOrEscapedCallsCancelNumericProof); + Test('Known numeric local uses subtract immediate', + TestKnownNumericLocalUsesSubtractImmediate); Test('Generic addition defers ToPrimitive to opcode', TestGenericAdditionDefersToPrimitiveToOpcode); Test('Assignment clears stale numeric hint', TestAssignmentClearsStaleNumericHint); @@ -312,6 +327,7 @@ function TTestCompiler.CountArithmeticOps( CountOp(ATemplate, OP_MUL_FLOAT) + CountOp(ATemplate, OP_DIV_FLOAT) + CountOp(ATemplate, OP_MOD_FLOAT); + Result := Result + CountOp(ATemplate, OP_SUB_NUM_IMM); end; function TTestCompiler.HasLoadInt(const ATemplate: TGocciaFunctionTemplate; @@ -549,6 +565,36 @@ procedure TTestCompiler.TestBinaryRoundTrip; end; end; +procedure TTestCompiler.TestBinaryRoundTripClosedNumericSelfCall; +var + Original, Loaded: TGocciaBytecodeModule; + LoadedFunction: TGocciaFunctionTemplate; + TempFile: string; +begin + Original := CompileSource( + 'const run = () => {' + + ' const fib = (n) => n <= 1 ? n : fib(n - 1) + fib(n - 2);' + + ' fib(10);' + + '}; run();', False, False, False, False, False, False); + TempFile := GetTempFileName + '.gbc'; + try + SaveModuleToFile(Original, TempFile); + Loaded := LoadModuleFromFile(TempFile); + try + LoadedFunction := FindFunctionWithOp(Loaded.TopLevel, + OP_CALL_SELF_NUM); + Expect(Assigned(LoadedFunction)).ToBe(True); + if Assigned(LoadedFunction) then + Expect(CountOp(LoadedFunction, OP_CALL_SELF_NUM)).ToBe(2); + finally + Loaded.Free; + end; + finally + Original.Free; + DeleteFile(TempFile); + end; +end; + procedure TTestCompiler.TestBinaryRoundTripUpvalueNames; var Original, Loaded: TGocciaBytecodeModule; @@ -852,6 +898,151 @@ procedure TTestCompiler.TestAnnotatedParametersUseTypedArithmetic; end; end; +procedure TTestCompiler.TestClosedNumericFibonacciUsesSuperinstructions; +var + Module: TGocciaBytecodeModule; + Func: TGocciaFunctionTemplate; +begin + Module := CompileSource( + 'const run = () => {' + + ' const fib = (n) => n <= 1 ? n : fib(n - 1) + fib(n - 2);' + + ' fib(20);' + + '}; run();', False, False, False, False, False, False); + try + Func := FindFunctionWithOp(Module.TopLevel, OP_SUB_NUM_IMM); + Expect(Assigned(Func)).ToBe(True); + if Assigned(Func) then + begin + Expect(CountOp(Func, OP_SUB_NUM_IMM)).ToBe(2); + Expect(CountOp(Func, OP_JUMP_IF_NUM_NOT_LTE_IMM)).ToBe(1); + Expect(CountOp(Func, OP_SUB)).ToBe(0); + Expect(CountOp(Func, OP_LTE)).ToBe(0); + Expect(CountOp(Func, OP_ADD)).ToBe(1); + Expect(CountOp(Func, OP_CALL_SELF_NUM)).ToBe(2); + Expect(CountOp(Func, OP_GET_UPVALUE)).ToBe(0); + Expect(CountOp(Func, OP_CALL)).ToBe(0); + end; + finally + Module.Free; + end; +end; + +procedure TTestCompiler.TestClosedNumericScalarSelfCallArityLimit; +var + Module: TGocciaBytecodeModule; + Func: TGocciaFunctionTemplate; +begin + Module := CompileSource( + 'const run = () => {' + + ' const sum = (n, acc) => n <= 0 ? acc : sum(n - 1, acc + 1) + 0;' + + ' sum(5, 2);' + + '}; run();', False, False, False, False, False, False); + try + Func := FindFunctionWithOp(Module.TopLevel, OP_CALL_SELF_NUM); + Expect(Assigned(Func)).ToBe(True); + if Assigned(Func) then + Expect(CountOp(Func, OP_CALL_SELF_NUM)).ToBe(1); + finally + Module.Free; + end; + + Module := CompileSource( + 'const run = () => {' + + ' const sum = (n, a, b) => n <= 0 ? a + b :' + + ' sum(n - 1, a + 1, b + 2) + 0;' + + ' sum(5, 2, 3);' + + '}; run();', False, False, False, False, False, False); + try + Func := FindFunctionWithOp(Module.TopLevel, OP_CALL_SELF_NUM); + Expect(Assigned(Func)).ToBe(True); + if Assigned(Func) then + Expect(CountOp(Func, OP_CALL_SELF_NUM)).ToBe(1); + finally + Module.Free; + end; + + Module := CompileSource( + 'const run = () => {' + + ' const wide = (n, a, b, c) => n <= 0 ? a + b + c :' + + ' wide(n - 1, a + 1, b + 2, c + 3) + 0;' + + ' wide(5, 2, 3, 4);' + + '}; run();', False, False, False, False, False, False); + try + Expect(FindFunctionWithOp(Module.TopLevel, + OP_CALL_SELF_NUM) = nil).ToBe(True); + finally + Module.Free; + end; +end; + +procedure TTestCompiler.TestMixedOrEscapedCallsCancelNumericProof; +var + Module: TGocciaBytecodeModule; +begin + Module := CompileSource( + 'const mixed = () => {' + + ' const fib = (n) => n <= 1 ? n : fib(n - 1) + fib(n - 2);' + + ' fib(20); fib("20");' + + '}; mixed();', False, False, False, False, False, False); + try + Expect(FindFunctionWithOp(Module.TopLevel, + OP_SUB_NUM_IMM) = nil).ToBe(True); + Expect(FindFunctionWithOp(Module.TopLevel, + OP_JUMP_IF_NUM_NOT_LTE_IMM) = nil).ToBe(True); + Expect(FindFunctionWithOp(Module.TopLevel, + OP_CALL_SELF_NUM) = nil).ToBe(True); + finally + Module.Free; + end; + + Module := CompileSource( + 'const wrongArity = () => {' + + ' const down = (a, b) => a <= 1 ? a : down(a - 1, b - 1);' + + ' down(20);' + + '}; wrongArity();', False, False, False, False, False, False); + try + Expect(FindFunctionWithOp(Module.TopLevel, + OP_SUB_NUM_IMM) = nil).ToBe(True); + Expect(FindFunctionWithOp(Module.TopLevel, + OP_JUMP_IF_NUM_NOT_LTE_IMM) = nil).ToBe(True); + Expect(FindFunctionWithOp(Module.TopLevel, + OP_CALL_SELF_NUM) = nil).ToBe(True); + finally + Module.Free; + end; + + Module := CompileSource( + 'const escaped = (sink) => {' + + ' const fib = (n) => n <= 1 ? n : fib(n - 1) + fib(n - 2);' + + ' sink(fib); fib(20);' + + '}; escaped(() => {});', False, False, False, False, False, False); + try + Expect(FindFunctionWithOp(Module.TopLevel, + OP_SUB_NUM_IMM) = nil).ToBe(True); + Expect(FindFunctionWithOp(Module.TopLevel, + OP_JUMP_IF_NUM_NOT_LTE_IMM) = nil).ToBe(True); + Expect(FindFunctionWithOp(Module.TopLevel, + OP_CALL_SELF_NUM) = nil).ToBe(True); + finally + Module.Free; + end; +end; + +procedure TTestCompiler.TestKnownNumericLocalUsesSubtractImmediate; +var + Module: TGocciaBytecodeModule; +begin + Module := CompileSource('let i = 2; i - 1;', + False, False, False, False, False, False); + try + Expect(CountOp(Module.TopLevel, OP_SUB_NUM_IMM)).ToBe(1); + Expect(CountOp(Module.TopLevel, OP_SUB)).ToBe(0); + Expect(CountOp(Module.TopLevel, OP_SUB_FLOAT)).ToBe(0); + finally + Module.Free; + end; +end; + procedure TTestCompiler.TestGenericAdditionDefersToPrimitiveToOpcode; var Module: TGocciaBytecodeModule; diff --git a/source/units/Goccia.Compiler.pas b/source/units/Goccia.Compiler.pas index d9f995fd..dfec4c5c 100644 --- a/source/units/Goccia.Compiler.pas +++ b/source/units/Goccia.Compiler.pas @@ -25,6 +25,7 @@ TGocciaCompiler = class FCurrentScope: TGocciaCompilerScope; FSourcePath: string; FFormalParameterCounts: TFormalParameterCountMap; + FNumericParameterProofs: TNumericParameterProofMap; FGlobalBackedTopLevel: Boolean; FAsyncTopLevel: Boolean; FPreinitializedTopLevelFunctions: Boolean; @@ -82,6 +83,7 @@ implementation Goccia.Bytecode.Debug, Goccia.Compiler.ConstantFolding, Goccia.Compiler.Expressions, + Goccia.Compiler.NumericProof, Goccia.Compiler.PatternMatching, Goccia.Compiler.Statements, Goccia.Keywords.Reserved, @@ -94,6 +96,7 @@ constructor TGocciaCompiler.Create(const ASourcePath: string); inherited Create; FSourcePath := ASourcePath; FFormalParameterCounts := TFormalParameterCountMap.Create; + FNumericParameterProofs := TNumericParameterProofMap.Create; FTemplateDerivedConstructorThisGuards := TDictionary.Create; FDerivedConstructorThisGuard := False; @@ -106,6 +109,7 @@ constructor TGocciaCompiler.Create(const ASourcePath: string); destructor TGocciaCompiler.Destroy; begin + FNumericParameterProofs.Free; FTemplateDerivedConstructorThisGuards.Free; FFormalParameterCounts.Free; inherited; @@ -117,6 +121,7 @@ function TGocciaCompiler.BuildContext: TGocciaCompilationContext; Result.Scope := FCurrentScope; Result.SourcePath := FSourcePath; Result.FormalParameterCounts := FFormalParameterCounts; + Result.NumericParameterProofs := FNumericParameterProofs; Result.GlobalBackedTopLevel := FGlobalBackedTopLevel and (FCurrentTemplate = FTopLevelTemplate); Result.PreinitializedTopLevelFunctions := FPreinitializedTopLevelFunctions and @@ -701,6 +706,8 @@ procedure TGocciaCompiler.DoCompileFunctionBody(const ABody: TGocciaASTNode); begin Block := TGocciaBlockStatement(ABody); + DiscoverClosedCallNumericProof(Block, FNumericParameterProofs); + // Hoist var declarations to function scope for I := 0 to Block.Nodes.Count - 1 do HoistVarLocals(Block.Nodes[I], FCurrentScope, @@ -1184,6 +1191,7 @@ function TGocciaCompiler.Compile( PredeclaredLexicalStart, PredeclaredLexicalIndex: Integer; PredeclaredLocal: TGocciaCompilerLocal; begin + FNumericParameterProofs.Clear; FModule := TGocciaBytecodeModule.Create(GOCCIA_RUNTIME_TAG, FSourcePath); FCurrentTemplate := TGocciaFunctionTemplate.Create(''); FTopLevelTemplate := FCurrentTemplate; diff --git a/source/units/Goccia.VM.Test.pas b/source/units/Goccia.VM.Test.pas index 8c7dbf30..b3e336fd 100644 --- a/source/units/Goccia.VM.Test.pas +++ b/source/units/Goccia.VM.Test.pas @@ -3,6 +3,7 @@ {$I Goccia.inc} uses + Math, SysUtils, TestingPascalLibrary, @@ -29,6 +30,8 @@ TTestGocciaVM = class(TTestSuite) procedure TestExecuteLiteralLoads; procedure TestExecuteConstString; procedure TestExecuteComparisons; + procedure TestExecuteSubtractNumberImmediate; + procedure TestExecuteNumberImmediateBranch; procedure TestExecuteArrayOps; procedure TestExecuteArrayPop; procedure TestExecuteObjectOps; @@ -69,6 +72,8 @@ procedure TTestGocciaVM.SetupTests; Test('Execute literal loads', TestExecuteLiteralLoads); Test('Execute constant string', TestExecuteConstString); Test('Execute comparisons', TestExecuteComparisons); + Test('Execute Number subtract immediate', TestExecuteSubtractNumberImmediate); + Test('Execute Number immediate branch', TestExecuteNumberImmediateBranch); Test('Execute array ops', TestExecuteArrayOps); Test('Execute array pop', TestExecuteArrayPop); Test('Execute object ops', TestExecuteObjectOps); @@ -198,6 +203,70 @@ procedure TTestGocciaVM.TestExecuteComparisons; end; end; +procedure TTestGocciaVM.TestExecuteSubtractNumberImmediate; +var + Template: TGocciaFunctionTemplate; + VM: TGocciaVM; + ResultValue: TGocciaValue; + FloatIndex: UInt16; +begin + Template := TGocciaFunctionTemplate.Create('subtract-number-immediate'); + VM := TGocciaVM.Create; + try + Template.MaxRegisters := 2; + Template.EmitInstruction(EncodeAsBx(OP_LOAD_INT, 0, 7)); + Template.EmitInstruction(EncodeABC(OP_SUB_NUM_IMM, 1, 0, + UInt16(Int16(-2)))); + Template.EmitInstruction(EncodeABC(OP_RETURN, 1, 0, 0)); + ResultValue := VM.ExecuteFunction(Template); + Expect(ResultValue.ToNumberLiteral.Value).ToBe(9); + + FloatIndex := Template.AddConstantFloat(7.5); + Template.PatchInstruction(0, EncodeABx(OP_LOAD_CONST, 0, FloatIndex)); + ResultValue := VM.ExecuteFunction(Template); + Expect(ResultValue.ToNumberLiteral.Value).ToBe(9.5); + finally + VM.Free; + Template.Free; + end; +end; + +procedure TTestGocciaVM.TestExecuteNumberImmediateBranch; +var + Template: TGocciaFunctionTemplate; + VM: TGocciaVM; + ResultValue: TGocciaValue; + NaNIndex: UInt16; +begin + Template := TGocciaFunctionTemplate.Create('number-immediate-branch'); + VM := TGocciaVM.Create; + try + Template.MaxRegisters := 2; + Template.EmitInstruction(EncodeAsBx(OP_LOAD_INT, 0, 2)); + Template.EmitInstruction(EncodeABC(OP_JUMP_IF_NUM_NOT_LTE_IMM, + 0, UInt16(Int16(1)), UInt16(Int16(2))), True); + Template.EmitInstruction(EncodeAsBx(OP_LOAD_INT, 1, 99)); + Template.EmitInstruction(EncodeABC(OP_RETURN, 1, 0, 0)); + Template.EmitInstruction(EncodeAsBx(OP_LOAD_INT, 1, 7)); + Template.EmitInstruction(EncodeABC(OP_RETURN, 1, 0, 0)); + + ResultValue := VM.ExecuteFunction(Template); + Expect(ResultValue.ToNumberLiteral.Value).ToBe(7); + + Template.PatchInstruction(0, EncodeAsBx(OP_LOAD_INT, 0, 1)); + ResultValue := VM.ExecuteFunction(Template); + Expect(ResultValue.ToNumberLiteral.Value).ToBe(99); + + NaNIndex := Template.AddConstantFloat(NaN); + Template.PatchInstruction(0, EncodeABx(OP_LOAD_CONST, 0, NaNIndex)); + ResultValue := VM.ExecuteFunction(Template); + Expect(ResultValue.ToNumberLiteral.Value).ToBe(7); + finally + VM.Free; + Template.Free; + end; +end; + procedure TTestGocciaVM.TestExecuteArrayOps; var Template: TGocciaFunctionTemplate; diff --git a/source/units/Goccia.VM.pas b/source/units/Goccia.VM.pas index 2ad9d97f..898f2ef4 100644 --- a/source/units/Goccia.VM.pas +++ b/source/units/Goccia.VM.pas @@ -126,6 +126,17 @@ TGocciaPropertyKey = record ); TGocciaComputedAccessOptions = set of TGocciaComputedAccessOption; + // Minimal saved state for a compiler-proven numeric self-call. These frames + // share the closure, lexical environment, local-cell window, argument + // window, realm, and execution context of their generic entry frame. + TGocciaClosedNumericFrame = record + IP: Integer; + ReturnRegister: UInt16; + RegisterBase: Integer; + PrevCovLine: UInt32; + ProfileEntryTimestamp: Int64; + end; + TGocciaVM = class private FRegisterStack: TGocciaRegisterArray; @@ -165,6 +176,8 @@ TGocciaVM = class FMemoryPressureCheckCountdown: Integer; FFrameStack: array of TGocciaVMCallFrame; FFrameStackCount: Integer; + FClosedNumericFrameStack: array of TGocciaClosedNumericFrame; + FClosedNumericFrameStackCount: Integer; FCurrentExecutionContextPushed: Boolean; FCurrentModuleSourcePath: string; FCurrentRuntimeModule: TGocciaModule; @@ -404,6 +417,17 @@ TGocciaVM = class out APrevCovLine: UInt32; out AProfileTimestamp: Int64): Integer; procedure TeardownCurrentFrame(const ATemplate: TGocciaFunctionTemplate; const AProfileTimestamp: Int64; const ATargetHandlerCount: Integer); + procedure PushClosedNumericFrame(const AResultRegister, AArgumentBase, + AArgumentCount: UInt16; var AFrame: TGocciaVMCallFrame; + const ATemplate: TGocciaFunctionTemplate; var APrevCovLine: UInt32; + var AProfileTimestamp: Int64; var AInitializedRegisterTop: Integer); + function PopClosedNumericFrame(var AFrame: TGocciaVMCallFrame; + const ATemplate: TGocciaFunctionTemplate; var APrevCovLine: UInt32; + var AProfileTimestamp: Int64): Integer; + procedure UnwindClosedNumericFrames(const ATargetCount: Integer; + var AFrame: TGocciaVMCallFrame; + const ATemplate: TGocciaFunctionTemplate; var APrevCovLine: UInt32; + var AProfileTimestamp: Int64); procedure PrepareTailCallFrameReuse(const ATemplate: TGocciaFunctionTemplate; const AProfileTimestamp: Int64; const AInitialFrameStackCount, ASavedHandlerCount: Integer); @@ -417,7 +441,8 @@ TGocciaVM = class var AFrame: TGocciaVMCallFrame; out ATemplate: TGocciaFunctionTemplate; out APrevCovLine: UInt32; out AProfileTimestamp: Int64); procedure HandleExceptionUnwind(const AErrorValue: TGocciaValue; - const AInitialFrameStackCount, ASavedHandlerCount: Integer; + const AInitialFrameStackCount, AInitialClosedNumericFrameCount, + ASavedHandlerCount: Integer; var AFrame: TGocciaVMCallFrame; var ATemplate: TGocciaFunctionTemplate; var APrevCovLine: UInt32; var AProfileTimestamp: Int64); procedure ExecuteGeneratorParameterPreamble(const AGenerator: TObject); @@ -6328,6 +6353,8 @@ constructor TGocciaVM.Create; FArgCount := 0; SetLength(FFrameStack, 64); FFrameStackCount := 0; + SetLength(FClosedNumericFrameStack, 64); + FClosedNumericFrameStackCount := 0; FStackRoot := TGocciaVMStackRoot.Create(Self); EnsureStackRootRegistered; end; @@ -12938,6 +12965,142 @@ procedure TGocciaVM.TeardownCurrentFrame(const ATemplate: TGocciaFunctionTemplat Dec(FFrameDepth); end; +procedure TGocciaVM.PushClosedNumericFrame(const AResultRegister, + AArgumentBase, AArgumentCount: UInt16; var AFrame: TGocciaVMCallFrame; + const ATemplate: TGocciaFunctionTemplate; var APrevCovLine: UInt32; + var AProfileTimestamp: Int64; var AInitializedRegisterTop: Integer); +var + Arguments: array[0..2] of TGocciaRegister; + I: Integer; + NewBase, Required, ClearStart: Integer; +begin + if (AArgumentCount < 1) or (AArgumentCount > 3) or + (AArgumentCount <> ATemplate.ParameterCount) then + raise Exception.Create('Invalid OP_CALL_SELF_NUM argument contract'); + + // Capture before growing the arena: SetLength may relocate FRegisterStack. + for I := 0 to AArgumentCount - 1 do + begin + Arguments[I] := FRegisters[AArgumentBase + I]; + if not RegisterIsNumericScalar(Arguments[I]) then + raise Exception.Create('Invalid non-numeric OP_CALL_SELF_NUM argument'); + end; + + CheckStackDepth(FFrameDepth + 1); + if FClosedNumericFrameStackCount >= Length(FClosedNumericFrameStack) then + SetLength(FClosedNumericFrameStack, + FClosedNumericFrameStackCount * 2 + 8); + FClosedNumericFrameStack[FClosedNumericFrameStackCount].IP := AFrame.IP; + FClosedNumericFrameStack[FClosedNumericFrameStackCount].ReturnRegister := + AResultRegister; + FClosedNumericFrameStack[FClosedNumericFrameStackCount].RegisterBase := + FRegisterBase; + FClosedNumericFrameStack[FClosedNumericFrameStackCount].PrevCovLine := + APrevCovLine; + FClosedNumericFrameStack[FClosedNumericFrameStackCount]. + ProfileEntryTimestamp := AProfileTimestamp; + Inc(FClosedNumericFrameStackCount); + + NewBase := FRegisterBase + FRegisterCount; + Required := NewBase + FRegisterCount; + if Required > Length(FRegisterStack) then + SetLength(FRegisterStack, Required * 2); + if Required > AInitializedRegisterTop then + begin + ClearStart := Max(NewBase, AInitializedRegisterTop); + if Required > ClearStart then + FillChar(FRegisterStack[ClearStart], + (Required - ClearStart) * SizeOf(TGocciaRegister), 0); + AInitializedRegisterTop := Required; + end; + FRegisterBase := NewBase; + FRegisters := @FRegisterStack[FRegisterBase]; + // Each depth is cleared on first use in this outer invocation. The proof + // then admits only scalar-number expressions, numeric predicates, and direct + // self-calls, so sibling reuse can contain only non-reference scalars or + // undefined and no stale object pointer can reach the GC. + FRegisters[0] := RegisterUndefined; + for I := 0 to AArgumentCount - 1 do + FRegisters[I + 1] := Arguments[I]; + AFrame.IP := 0; + + Inc(FFrameDepth); + if (TGocciaCallStack.Instance <> nil) then + if Assigned(ATemplate.DebugInfo) and + (ATemplate.DebugInfo.SourceFile <> '') then + TGocciaCallStack.Instance.PushTemplate(Pointer(ATemplate), '') + else + TGocciaCallStack.Instance.PushTemplate(Pointer(ATemplate), + FCurrentModuleSourcePath); + + if FCoverageEnabled and (TGocciaCoverageTracker.Instance <> nil) and + Assigned(ATemplate.DebugInfo) and + (ATemplate.DebugInfo.LineMapCount > 0) then + TGocciaCoverageTracker.Instance.RecordLineHit( + ATemplate.DebugInfo.SourceFile, + ATemplate.DebugInfo.GetLineMapEntry(0).Line); + + if FProfilingFunctions and (TGocciaProfiler.Instance <> nil) then + begin + if ATemplate.ProfileIndex < 0 then + begin + if Assigned(ATemplate.DebugInfo) and + (ATemplate.DebugInfo.LineMapCount > 0) then + ATemplate.ProfileIndex := TGocciaProfiler.Instance.RegisterTemplate( + ATemplate.Name, ATemplate.DebugInfo.SourceFile, + ATemplate.DebugInfo.GetLineMapEntry(0).Line) + else + ATemplate.ProfileIndex := TGocciaProfiler.Instance.RegisterTemplate( + ATemplate.Name, '', 0); + end; + AProfileTimestamp := GetNanoseconds; + TGocciaProfiler.Instance.PushFunction(ATemplate.ProfileIndex, + AProfileTimestamp); + end; + + if FCoverageEnabled and Assigned(ATemplate.DebugInfo) and + (ATemplate.DebugInfo.LineMapCount > 0) then + APrevCovLine := ATemplate.DebugInfo.GetLineMapEntry(0).Line + else + APrevCovLine := 0; +end; + +function TGocciaVM.PopClosedNumericFrame(var AFrame: TGocciaVMCallFrame; + const ATemplate: TGocciaFunctionTemplate; var APrevCovLine: UInt32; + var AProfileTimestamp: Int64): Integer; +begin + if FProfilingFunctions and Assigned(ATemplate) and + (ATemplate.ProfileIndex >= 0) then + TGocciaProfiler.Instance.PopFunction(ATemplate.ProfileIndex, + GetNanoseconds); + if (TGocciaCallStack.Instance <> nil) then + TGocciaCallStack.Instance.Pop; + Dec(FFrameDepth); + + Dec(FClosedNumericFrameStackCount); + AFrame.IP := FClosedNumericFrameStack[ + FClosedNumericFrameStackCount].IP; + FRegisterBase := FClosedNumericFrameStack[ + FClosedNumericFrameStackCount].RegisterBase; + FRegisters := @FRegisterStack[FRegisterBase]; + APrevCovLine := FClosedNumericFrameStack[ + FClosedNumericFrameStackCount].PrevCovLine; + AProfileTimestamp := FClosedNumericFrameStack[ + FClosedNumericFrameStackCount].ProfileEntryTimestamp; + Result := FClosedNumericFrameStack[ + FClosedNumericFrameStackCount].ReturnRegister; +end; + +procedure TGocciaVM.UnwindClosedNumericFrames(const ATargetCount: Integer; + var AFrame: TGocciaVMCallFrame; + const ATemplate: TGocciaFunctionTemplate; var APrevCovLine: UInt32; + var AProfileTimestamp: Int64); +begin + while FClosedNumericFrameStackCount > ATargetCount do + PopClosedNumericFrame(AFrame, ATemplate, APrevCovLine, + AProfileTimestamp); +end; + // ES2026 §15.10.3 PrepareForTailCall. Discards the current frame's resources so // the impending SetupNewFrame reuses this frame instead of stacking a new one. // The caller's saved frame slot (or the outermost return path) is left intact, @@ -13109,7 +13272,8 @@ procedure TGocciaVM.SetupNewFrame(const AClosure: TGocciaBytecodeClosure; end; procedure TGocciaVM.HandleExceptionUnwind(const AErrorValue: TGocciaValue; - const AInitialFrameStackCount, ASavedHandlerCount: Integer; + const AInitialFrameStackCount, AInitialClosedNumericFrameCount, + ASavedHandlerCount: Integer; var AFrame: TGocciaVMCallFrame; var ATemplate: TGocciaFunctionTemplate; var APrevCovLine: UInt32; var AProfileTimestamp: Int64); var @@ -13117,6 +13281,10 @@ procedure TGocciaVM.HandleExceptionUnwind(const AErrorValue: TGocciaValue; TargetHandlerCount: Integer; IsGeneratorReturnCompletion: Boolean; begin + // Proven numeric frames contain no handlers. Restore their generic entry + // frame before searching the ordinary handler stack. + UnwindClosedNumericFrames(AInitialClosedNumericFrameCount, AFrame, + ATemplate, APrevCovLine, AProfileTimestamp); IsGeneratorReturnCompletion := Assigned(GActiveBytecodeGenerator) and Assigned(GActiveBytecodeGenerator.FReturnSentinel) and (AErrorValue = GActiveBytecodeGenerator.FReturnSentinel); @@ -13194,6 +13362,7 @@ function TGocciaVM.ExecuteClosureRegistersInternal( SavedExecutionContextPushed: Boolean; SavedHandlerCount: Integer; InitialFrameStackCount: Integer; + InitialClosedNumericFrameCount: Integer; ReturnValue: TGocciaRegister; ResultReg: Integer; TargetHandlerCount: Integer; @@ -13238,6 +13407,7 @@ function TGocciaVM.ExecuteClosureRegistersInternal( CustomMatcherValue, MatchResultValue: TGocciaValue; MatchHintObject: TGocciaObjectValue; BuiltinConstructorMatch: Boolean; + NumericComparisonResult: Boolean; RegisterArgs: TGocciaRegisterArray; CallThisRegister: TGocciaRegister; CallGlobalThisValue: TGocciaValue; @@ -13259,6 +13429,7 @@ function TGocciaVM.ExecuteClosureRegistersInternal( ExecutionRealm: TGocciaRealm; RealmSwitched: Boolean; PreviousCallSite: TGocciaCallSite; + ClosedNumericInitializedRegisterTop: Integer; procedure CurrentInstructionDebugLocation(out ALine, AColumn: Integer); begin @@ -13307,6 +13478,7 @@ function TGocciaVM.ExecuteClosureRegistersInternal( SavedExecutionContextPushed := FCurrentExecutionContextPushed; SavedHandlerCount := FHandlerStack.Count; InitialFrameStackCount := FFrameStackCount; + InitialClosedNumericFrameCount := FClosedNumericFrameStackCount; FLastClosureThisValue := AThisValue; PushSavedStateRoot(SavedClosure, SavedNewTarget, SavedArgumentBase, SavedArgCount); @@ -13317,6 +13489,7 @@ function TGocciaVM.ExecuteClosureRegistersInternal( SetupNewFrame(AClosure, AThisValue, AArguments, AArgCount, AArg0, AArg1, AArg2, AUseFixedArgs, APushExecutionContext, Frame, Template, PrevCovLine, ProfileEntryTimestamp); + ClosedNumericInitializedRegisterTop := FRegisterBase + FRegisterCount; if Assigned(AClosure) and Assigned(AClosure.GlobalScope) then FGlobalScope := AClosure.GlobalScope; if Assigned(FPendingNewTarget) then @@ -13348,7 +13521,8 @@ function TGocciaVM.ExecuteClosureRegistersInternal( bgrkThrow: HandleExceptionUnwind( RegisterToValue(GActiveBytecodeGenerator.FResumeValue), - InitialFrameStackCount, SavedHandlerCount, + InitialFrameStackCount, InitialClosedNumericFrameCount, + SavedHandlerCount, Frame, Template, PrevCovLine, ProfileEntryTimestamp); bgrkReturn: begin @@ -13366,14 +13540,16 @@ function TGocciaVM.ExecuteClosureRegistersInternal( on E: EGocciaBytecodeThrow do begin HandleExceptionUnwind(E.ThrownValue, - InitialFrameStackCount, SavedHandlerCount, + InitialFrameStackCount, InitialClosedNumericFrameCount, + SavedHandlerCount, Frame, Template, PrevCovLine, ProfileEntryTimestamp); ReturnAwaitAbrupt := True; end; on E: TGocciaThrowValue do begin HandleExceptionUnwind(E.Value, - InitialFrameStackCount, SavedHandlerCount, + InitialFrameStackCount, InitialClosedNumericFrameCount, + SavedHandlerCount, Frame, Template, PrevCovLine, ProfileEntryTimestamp); ReturnAwaitAbrupt := True; end; @@ -13401,7 +13577,8 @@ function TGocciaVM.ExecuteClosureRegistersInternal( if not Assigned(GActiveBytecodeGenerator.FReturnSentinel) then GActiveBytecodeGenerator.FReturnSentinel := TGocciaObjectValue.Create; HandleExceptionUnwind(GActiveBytecodeGenerator.FReturnSentinel, - InitialFrameStackCount, SavedHandlerCount, + InitialFrameStackCount, InitialClosedNumericFrameCount, + SavedHandlerCount, Frame, Template, PrevCovLine, ProfileEntryTimestamp); end; end; @@ -13767,6 +13944,38 @@ function TGocciaVM.ExecuteClosureRegistersInternal( Template.DebugInfo.GetLineForPC(InstructionStartIP), Template.DebugInfo.GetColumnForPC(InstructionStartIP), 1); + OP_JUMP_IF_NUM_NOT_LTE_IMM: + begin + if FRegisters[A].Kind = grkInt then + NumericComparisonResult := FRegisters[A].IntValue <= Int16(B) + else if FRegisters[A].Kind = grkFloat then + NumericComparisonResult := FRegisters[A].FloatValue <= Int16(B) + else + raise Exception.Create( + 'Invalid non-numeric source for OP_JUMP_IF_NUM_NOT_LTE_IMM'); + if not NumericComparisonResult then + begin + if FCoverageEnabled and + (TGocciaCoverageTracker.Instance <> nil) and + Assigned(Template.DebugInfo) then + TGocciaCoverageTracker.Instance.RecordBranchHit( + Template.DebugInfo.SourceFile, + Template.DebugInfo.GetLineForPC(InstructionStartIP), + Template.DebugInfo.GetColumnForPC(InstructionStartIP), 0); + JumpOffset := Int16(C); + Inc(Frame.IP, JumpOffset); + if JumpOffset < 0 then + CheckExecutionTimeout; + end + else if FCoverageEnabled and + (TGocciaCoverageTracker.Instance <> nil) and + Assigned(Template.DebugInfo) then + TGocciaCoverageTracker.Instance.RecordBranchHit( + Template.DebugInfo.SourceFile, + Template.DebugInfo.GetLineForPC(InstructionStartIP), + Template.DebugInfo.GetColumnForPC(InstructionStartIP), 1); + end; + OP_JUMP_IF_NULLISH: if RegisterMatchesNullishKind(FRegisters[A], B) then begin @@ -13834,6 +14043,15 @@ function TGocciaVM.ExecuteClosureRegistersInternal( FRegisters[A] := VMNumberRegister(RegisterToDouble(FRegisters[B]) - RegisterToDouble(FRegisters[C])); + OP_SUB_NUM_IMM: + if FRegisters[B].Kind = grkInt then + FRegisters[A] := VMIntResult(FRegisters[B].IntValue - Int16(C)) + else if FRegisters[B].Kind = grkFloat then + FRegisters[A] := VMNumberRegister(FRegisters[B].FloatValue - Int16(C)) + else + raise Exception.Create( + 'Invalid non-numeric source for OP_SUB_NUM_IMM'); + OP_MUL_INT: if (FRegisters[B].Kind = grkInt) and (FRegisters[C].Kind = grkInt) then FRegisters[A] := VMIntResult(FRegisters[B].IntValue * @@ -15414,6 +15632,14 @@ // ES2022 §15.7.14: execute static block closure with this = class SetRegister(A, BytecodeFunction); end; + OP_CALL_SELF_NUM: + begin + CheckExecutionTimeout; + PushClosedNumericFrame(A, B, C, Frame, Template, PrevCovLine, + ProfileEntryTimestamp, ClosedNumericInitializedRegisterTop); + Continue; + end; + OP_CALL: begin CheckExecutionTimeout; @@ -17054,6 +17280,14 @@ // ES2022 §15.7.14: execute static block closure with this = class if Assigned(GActiveBytecodeGenerator) and (GActiveBytecodeGenerator.FClosure = AClosure) then GActiveBytecodeGenerator.FReturnRequiresAwait := B <> 0; + if FClosedNumericFrameStackCount > + InitialClosedNumericFrameCount then + begin + ResultReg := PopClosedNumericFrame(Frame, Template, PrevCovLine, + ProfileEntryTimestamp); + SetRegisterRaw(ResultReg, ReturnValue); + Continue; + end; // Outermost frame: let the finally block handle teardown if FFrameStackCount <= InitialFrameStackCount then begin @@ -17082,31 +17316,37 @@ // ES2022 §15.7.14: execute static block closure with this = class except on E: EGocciaBytecodeThrow do HandleExceptionUnwind(E.ThrownValue, - InitialFrameStackCount, SavedHandlerCount, + InitialFrameStackCount, InitialClosedNumericFrameCount, + SavedHandlerCount, Frame, Template, PrevCovLine, ProfileEntryTimestamp); on E: TGocciaThrowValue do HandleExceptionUnwind(E.Value, - InitialFrameStackCount, SavedHandlerCount, + InitialFrameStackCount, InitialClosedNumericFrameCount, + SavedHandlerCount, Frame, Template, PrevCovLine, ProfileEntryTimestamp); on E: TGocciaTypeError do HandleExceptionUnwind( CreateErrorObject(TYPE_ERROR_NAME, E.Message), - InitialFrameStackCount, SavedHandlerCount, + InitialFrameStackCount, InitialClosedNumericFrameCount, + SavedHandlerCount, Frame, Template, PrevCovLine, ProfileEntryTimestamp); on E: TGocciaReferenceError do HandleExceptionUnwind( CreateErrorObject(REFERENCE_ERROR_NAME, E.Message), - InitialFrameStackCount, SavedHandlerCount, + InitialFrameStackCount, InitialClosedNumericFrameCount, + SavedHandlerCount, Frame, Template, PrevCovLine, ProfileEntryTimestamp); on E: TGocciaSyntaxError do HandleExceptionUnwind( CreateErrorObject(SYNTAX_ERROR_NAME, E.Message), - InitialFrameStackCount, SavedHandlerCount, + InitialFrameStackCount, InitialClosedNumericFrameCount, + SavedHandlerCount, Frame, Template, PrevCovLine, ProfileEntryTimestamp); on E: TGocciaRuntimeError do HandleExceptionUnwind( CreateErrorObject(ERROR_NAME, E.Message), - InitialFrameStackCount, SavedHandlerCount, + InitialFrameStackCount, InitialClosedNumericFrameCount, + SavedHandlerCount, Frame, Template, PrevCovLine, ProfileEntryTimestamp); end; end; @@ -17114,6 +17354,8 @@ // ES2022 §15.7.14: execute static block closure with this = class finally Dec(FNativeExecutionDepth); try + UnwindClosedNumericFrames(InitialClosedNumericFrameCount, Frame, + Template, PrevCovLine, ProfileEntryTimestamp); // Unwind any remaining trampoline frames (exception escape path) while FFrameStackCount > InitialFrameStackCount do begin diff --git a/tests/language/functions/recursive-functions.js b/tests/language/functions/recursive-functions.js index 74bb80c4..f0ac9c88 100644 --- a/tests/language/functions/recursive-functions.js +++ b/tests/language/functions/recursive-functions.js @@ -52,3 +52,35 @@ test("multi-argument tail recursion reuses the argument window correctly", () => expect(sum(1000, 0)).toBe(500500); // 1+2+...+1000 }); + +test("closed numeric self-recursion preserves one to three scalar arguments", () => { + const fibonacciResult = () => { + const fibonacci = (n) => n <= 1 + ? n + : fibonacci(n - 1) + fibonacci(n - 2); + return fibonacci(10); + }; + const twoArgumentResult = () => { + const addSteps = (n, total) => n <= 0 + ? total + : addSteps(n - 1, total + 1) + 0; + return addSteps(5, 2); + }; + const threeArgumentResult = () => { + const addPairs = (n, left, right) => n <= 0 + ? left + right + : addPairs(n - 1, left + 1, right + 2) + 0; + return addPairs(4, 2, 3); + }; + const fractionalResult = () => { + const count = (n, total) => n <= 0 + ? total + : count(n - 1, total + 0.5) + 0; + return count(2.5, 1); + }; + + expect(fibonacciResult()).toBe(55); + expect(twoArgumentResult()).toBe(7); + expect(threeArgumentResult()).toBe(17); + expect(fractionalResult()).toBe(2.5); +}); diff --git a/tests/language/functions/stack-depth-limit.js b/tests/language/functions/stack-depth-limit.js index a7b35891..d481bd18 100644 --- a/tests/language/functions/stack-depth-limit.js +++ b/tests/language/functions/stack-depth-limit.js @@ -97,3 +97,19 @@ test("error has stack trace", () => { expect(caught.stack).toBeDefined(); expect(caught.stack.includes("deep")).toBe(true); }); + +test("closed numeric scalar recursion preserves the stack limit and trace", () => { + const run = () => { + const numeric = (n) => numeric(n - 1) + 0; + return numeric(1); + }; + let caught; + try { + run(); + } catch (e) { + caught = e; + } + expect(caught instanceof RangeError).toBe(true); + expect(caught.message).toBe("Maximum call stack size exceeded"); + expect(caught.stack.includes("numeric")).toBe(true); +});