Skip to content
Open
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
4 changes: 2 additions & 2 deletions .agent/HANDOFF.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Fib is the best relative row because it already uses `OP_CALL_SELF_NUM` / `OP_SU

## Profile facts (function-wrapped equivalents)

- `loop-dispatch-floor`: original baseline was 38% `OP_GET_LOCAL`, 15% `OP_LOAD_INT`, 11% `OP_SET_LOCAL`, 8% `OP_ADD_FLOAT`. Counted-for now matches `i = i + 1` and emits `OP_ADD_INT` (`e6f3e177`); standalone assignment still uses `OP_INC_NUMERIC`. Loop/if `<` fuses as `OP_JUMP_IF_NOT_LT` (`d398e8c5`); `<=` loops (Sieve) still use the LTE jump. Number literals type as `sltFloat`.
- `loop-dispatch-floor`: original baseline was 38% `OP_GET_LOCAL`, 15% `OP_LOAD_INT`, 11% `OP_SET_LOCAL`, 8% `OP_ADD_FLOAT`. Counted-for now matches `i = i + 1` and emits `OP_ADD_INT` for ascending loops and `OP_SUB_INT` for descending loops (`e6f3e177`); standalone assignment still uses `OP_INC_NUMERIC`. Loop/if `<` fuses as `OP_JUMP_IF_NOT_LT` (`d398e8c5`); `<=` loops (Sieve) still use the LTE jump. Number literals type as `sltFloat`.
- `nbody-minimal`: original baseline was 31% `OP_GET_LOCAL`, 12% `OP_GET_PROP_CONST`, 10% `OP_LOAD_HOLE`, 8% `OP_MOVE`. Hot pair `GET_LOCAL → GET_PROP_CONST` (11%) now fuses as `OP_GET_LOCAL_PROP_CONST` (`db9567bd`). Generic `OP_MUL`/`OP_ADD` still 100% scalar hit rate.
- Script-level `let` in a non-function profiled as `OP_GET_GLOBAL` (29% of opcodes) — not the AWFY/probe shape.

Expand Down Expand Up @@ -150,7 +150,7 @@ Json 24.96, Permute 21.86, Sieve 21.63, CD 20.41, Bounce 19.94, Havlak 19.09, To
- **Broader read-PIC:** still rejected (ADR 0088). Own+proto read ICs already ship.
- **Write-IC:** landed this wave (`15e8eca7`). Own writable-data stores on `OP_SET_PROP_CONST` hit a shape-keyed IC; misses still go through `AssignProperty`. Broader read-PIC remains rejected (ADR 0088).
- **`OP_SET_PROP_CONST`** uses the write IC for ordinary own writable data; `VMTrySetOwnWritableDataProperty` remains available for non-IC paths.
- **Counted-for** now matches `i = i + 1` / `i += 1` and minus (`e6f3e177`), emitting `OP_ADD_INT`. Loop `<` fuses as `OP_JUMP_IF_NOT_LT` (`d398e8c5`). Integer-valued number literals still type as `sltFloat`; wave-2 TypeHint retry was measured and rejected.
- **Counted-for** now matches `i = i + 1` / `i += 1` and minus (`e6f3e177`), emitting `OP_ADD_INT` for ascending loops and `OP_SUB_INT` for descending loops. Loop `<` fuses as `OP_JUMP_IF_NOT_LT` (`d398e8c5`). Integer-valued number literals still type as `sltFloat`; wave-2 TypeHint retry was measured and rejected.
- **CALL:** bytecode→bytecode already trampolines; `ExecuteClosureRegisters0–3` are native ingress only. Revisit `OP_CALL_METHOD` staging only with AWFY transfer (ADR 0089 previously noise).
- **Dispatch preamble:** prod vs instrumented dual loop landed (`c776b95b`). Do not retry a sparse hot/cold opcode split or duplicating the full `case` (FPC register-pressure failure).
- **ALLOC:** do not revive value caches. Property-store `RegisterToValue` boxing is the live allocation tax.
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,4 @@ runtime command lists here.
- **Contribution requirements:** [CONTRIBUTING.md](CONTRIBUTING.md)
- **Engine shape:** [docs/architecture.md](docs/architecture.md), [docs/interpreter.md](docs/interpreter.md), [docs/bytecode-vm.md](docs/bytecode-vm.md), [docs/core-patterns.md](docs/core-patterns.md)
- **Optional extended agent skills:** [.agents/skills/](.agents/skills/) (installable playbooks; not a substitute for CONTRIBUTING)
- **Runtime optimization waves:** [.agents/skills/optimize-runtime/SKILL.md](.agents/skills/optimize-runtime/SKILL.md) — benchmark-gated bytecode/interpreter speed work vs QuickJS; keep only measured wins
- **Runtime optimization waves:** [.agents/skills/optimize-runtime/SKILL.md](.agents/skills/optimize-runtime/SKILL.md) — Use when closing the bytecode-vs-QuickJS gap or running a measured runtime optimization wave. Benchmark-gated; keep only measured wins
12 changes: 4 additions & 8 deletions source/units/Goccia.Compiler.Expressions.pas
Original file line number Diff line number Diff line change
Expand Up @@ -2561,15 +2561,11 @@ procedure CompileAssignment(const ACtx: TGocciaCompilationContext;
IsNumericSelfIncrementByOne(ACtx.Scope, AExpr.Name, AExpr.Value) then
begin
Local := ACtx.Scope.GetLocal(LocalIdx);
if (not Local.IsGlobalBacked) and (not Local.IsImportBinding) then
// Const locals must use the generic assignment path so the RHS is
// evaluated before const-assignment / TDZ behavior (ES2026 §13.15.2).
if (not Local.IsGlobalBacked) and (not Local.IsImportBinding) and
(not Local.IsConst) then
begin
if Local.IsConst then
begin
if ShouldIgnoreNonStrictImmutableLocalAssignment(ACtx, Local) then
Exit;
EmitConstAssignmentError(ACtx);
Exit;
end;
Slot := Local.Slot;
if Local.IsCaptured then
EmitInstruction(ACtx, EncodeABx(OP_GET_LOCAL, Slot, UInt16(Slot)));
Expand Down
6 changes: 5 additions & 1 deletion source/units/Goccia.Compiler.Statements.pas
Original file line number Diff line number Diff line change
Expand Up @@ -3332,7 +3332,11 @@ function TryMatchCountedForLimit(const ACtx: TGocciaCompilationContext;
// writes are rejected; mutable captured bindings and bodies that create
// closures (which can assign through the capture) fall back too. const
// bindings cannot be assigned, so they remain snapshot-safe even when the
// body creates closures that capture the loop index.
// body creates closures that capture the loop index. Global-backed vars can
// still change through the global object from a callee the body analysis
// does not see.
if LimitLocal.IsGlobalBacked then
Exit;
if ForBodyAssignsIdentifier(ABody, LimitIdent.Name) then
Exit;
if not LimitLocal.IsConst then
Expand Down
40 changes: 40 additions & 0 deletions source/units/Goccia.Compiler.Test.pas
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ TTestCompiler = class(TTestSuite)
procedure TestForOfUsesHandlerForExpressionBody;
procedure TestForOfUsesOneIteratorCloseHandler;
procedure TestCountedForLessThanUsesJumpIfNotLt;
procedure TestConstSelfIncrementUsesGenericAssignment;
procedure TestGlobalBackedCountedForLimitIsNotSnapshotted;
procedure TestIfAndConditionalLessThanUseJumpIfNotLt;
procedure TestLessThanValueKeepsGenericCompare;
procedure TestConstantIfEliminatesBranch;
Expand Down Expand Up @@ -197,6 +199,10 @@ procedure TTestCompiler.SetupTests;
Test('for-of uses one iterator-close handler', TestForOfUsesOneIteratorCloseHandler);
Test('counted-for less-than uses jump-if-not-lt',
TestCountedForLessThanUsesJumpIfNotLt);
Test('const self-increment uses generic assignment',
TestConstSelfIncrementUsesGenericAssignment);
Test('global-backed counted-for limit is not snapshotted',
TestGlobalBackedCountedForLimitIsNotSnapshotted);
Test('if and conditional less-than use jump-if-not-lt',
TestIfAndConditionalLessThanUseJumpIfNotLt);
Test('less-than value keeps generic compare',
Expand Down Expand Up @@ -1661,6 +1667,40 @@ procedure TTestCompiler.TestCountedForLessThanUsesJumpIfNotLt;
end;
end;

procedure TTestCompiler.TestConstSelfIncrementUsesGenericAssignment;
var
Module: TGocciaBytecodeModule;
begin
Module := CompileSource('const x = (x = x + 1);');
try
Expect<Integer>(CountOp(Module.TopLevel, OP_INC_NUMERIC)).ToBe(0);
finally
Module.Free;
end;

Module := CompileSource('let x = 0; x = x + 1;');
try
Expect<Integer>(CountOp(Module.TopLevel, OP_INC_NUMERIC)).ToBe(1);
finally
Module.Free;
end;
end;

procedure TTestCompiler.TestGlobalBackedCountedForLimitIsNotSnapshotted;
var
Module: TGocciaBytecodeModule;
begin
Module := CompileSource(
'let n = 5; for (let i = 0; i < n; i = i + 1) { i; }',
False, False, True, True, True, True, True);
try
Expect<Integer>(CountOp(Module.TopLevel, OP_ADD_INT)).ToBe(0);
Expect<Boolean>(CountOp(Module.TopLevel, OP_INC_NUMERIC) > 0).ToBe(True);
finally
Module.Free;
end;
end;

procedure TTestCompiler.TestIfAndConditionalLessThanUseJumpIfNotLt;
var
Module: TGocciaBytecodeModule;
Expand Down
13 changes: 13 additions & 0 deletions tests/language/expressions/arithmetic/self-increment-assignment.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,16 @@ test("self-increment assignment result can be captured after writes", () => {
};
expect(f()).toEqual([11, 12, 12]);
});

test("const initializer self-increment is TDZ not const-assignment", () => {
expect(() => {
const x = (x = x + 1);
}).toThrow(ReferenceError);
});

test("initialized const self-increment throws TypeError", () => {
expect(() => {
const x = 1;
x = x + 1;
}).toThrow(TypeError);
});
15 changes: 15 additions & 0 deletions tests/language/for-loop-var/global-limit-mutation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*---
description: counted-for reevaluates a global-backed var limit
features: [compat-traditional-for-loop, compat-var]
---*/

var countedForGlobalLimit = 3;
let countedForGlobalLimitCount = 0;
for (let i = 0; i < countedForGlobalLimit; i = i + 1) {
countedForGlobalLimitCount += 1;
globalThis.countedForGlobalLimit = 0;
}

test("counted-for reevaluates a global-backed var limit mutated via globalThis", () => {
expect(countedForGlobalLimitCount).toBe(1);
});
Loading