Skip to content

Add queueMicrotask functionality#6

Merged
frostney merged 2 commits intomainfrom
feat-queuemicrotask
Feb 18, 2026
Merged

Add queueMicrotask functionality#6
frostney merged 2 commits intomainfrom
feat-queuemicrotask

Conversation

@frostney
Copy link
Copy Markdown
Owner

@frostney frostney commented Feb 18, 2026

Summary by CodeRabbit

  • New Features

    • Introduced queueMicrotask() global function for enqueueing callbacks in the microtask queue, sharing the same queue as Promise reactions.
  • Documentation

    • Updated architecture, built-ins, and design decision documentation to reflect queueMicrotask() integration, behavior, and error handling semantics.
  • Tests

    • Added comprehensive test suite covering functionality, FIFO ordering, Promise interleaving, nesting, and error cases.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 18, 2026

📝 Walkthrough

Walkthrough

The pull request adds a global queueMicrotask(callback) function that enqueues callbacks into the shared microtask queue alongside Promise reactions. Implementation includes argument validation, callback enqueueing, and integration with the existing microtask drain mechanism. Documentation updated across multiple files; comprehensive test suite added.

Changes

Cohort / File(s) Summary
Documentation Updates
AGENTS.md, docs/architecture.md, docs/built-ins.md, docs/design-decisions.md
Updated microtask queue documentation to reflect queueMicrotask integration. Clarified enqueue behavior, drain semantics with nested calls, error handling (silent callback errors), and execution timing relative to Promise reactions and synchronous code.
Global Function Implementation
units/Goccia.Builtins.Globals.pas
Added QueueMicrotaskCallback protected method and queueMicrotask global binding. Validates callback argument type, creates TGocciaMicrotask, and enqueues via TGocciaMicrotaskQueue. Returns undefined. Added Goccia.MicrotaskQueue import.
Test Suite
tests/built-ins/queueMicrotask/queueMicrotask.js
New comprehensive test file verifying queueMicrotask functionality: asynchronous execution, FIFO ordering, interaction with promise microtasks, nested calls, error handling (TypeError for non-callable), and integration with promise chains.
Microtask Queue Refactoring
units/Goccia.MicrotaskQueue.pas
Minor formatting adjustments in DrainQueue except handler; inlined Promise rejection without explicit block structure, added clarifying comments. No functional or error handling changes to semantics.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰✨ A queue for microtasks, now callbacks can queue,
Alongside the promises, all drained on cue,
FIFO order hopping, no errors to fear,
The global queueMicrotask brings joy to this sphere! 🎉

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add queueMicrotask functionality' directly and clearly summarizes the main change: implementing the queueMicrotask global function across documentation, tests, and implementation units.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat-queuemicrotask

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Feb 18, 2026

Benchmark Results

113 benchmarks · 🟢 3 improved · 110 unchanged · avg -0.1%

arrays.js — 🟢 3 improved, 8 unchanged · avg +3.6%
Benchmark Base (ops/sec) PR (ops/sec) Change
Array.from length 100 137,893 154,731 🟢 +12.2%
Array.of 10 elements 145,607 161,362 🟢 +10.8%
spread into new array 172,571 179,377 +3.9%
map over 50 elements 95,937 99,202 +3.4%
filter over 50 elements 90,313 98,945 🟢 +9.6%
reduce sum 50 elements 101,837 103,903 +2.0%
forEach over 50 elements 102,588 102,021 -0.6%
find in 50 elements 107,591 110,517 +2.7%
sort 20 elements 4,449 4,512 +1.4%
flat nested array 82,991 82,415 -0.7%
flatMap 42,106 39,797 -5.5%
classes.js — 10 unchanged · avg -0.2%
Benchmark Base (ops/sec) PR (ops/sec) Change
simple class new 79,237 79,911 +0.9%
class with defaults 58,019 60,714 +4.6%
50 instances via Array.from 83,915 85,341 +1.7%
instance method call 37,370 36,988 -1.0%
static method call 64,161 63,501 -1.0%
single-level inheritance 31,603 31,030 -1.8%
two-level inheritance 29,500 29,140 -1.2%
private field access 40,506 40,239 -0.7%
private methods 46,602 45,777 -1.8%
getter/setter access 41,442 40,839 -1.5%
closures.js — 11 unchanged · avg -0.8%
Benchmark Base (ops/sec) PR (ops/sec) Change
closure over single variable 60,975 60,995 +0.0%
closure over multiple variables 60,388 59,776 -1.0%
nested closures 66,244 65,543 -1.1%
function as argument 45,931 46,039 +0.2%
function returning function 60,575 59,478 -1.8%
compose two functions 37,006 36,966 -0.1%
fn.call 87,297 86,765 -0.6%
fn.apply 64,270 63,953 -0.5%
fn.bind 77,538 75,582 -2.5%
recursive sum to 50 5,311 5,336 +0.5%
recursive tree traversal 9,523 9,294 -2.4%
collections.js — 12 unchanged · avg -0.9%
Benchmark Base (ops/sec) PR (ops/sec) Change
add 50 elements 89,379 89,883 +0.6%
has lookup (50 elements) 71,209 70,272 -1.3%
delete elements 70,109 69,791 -0.5%
forEach iteration 85,720 85,508 -0.2%
spread to array 92,438 91,838 -0.6%
deduplicate array 56,606 55,968 -1.1%
set 50 entries 88,799 88,773 -0.0%
get lookup (50 entries) 69,556 67,371 -3.1%
has check 74,934 74,607 -0.4%
delete entries 69,129 67,936 -1.7%
forEach iteration 82,050 81,226 -1.0%
keys/values/entries 54,339 53,763 -1.1%
destructuring.js — 14 unchanged · avg -0.9%
Benchmark Base (ops/sec) PR (ops/sec) Change
simple array destructuring 187,340 188,003 +0.4%
with rest element 135,946 136,041 +0.1%
with defaults 187,774 186,444 -0.7%
skip elements 203,809 202,360 -0.7%
nested array destructuring 108,614 108,773 +0.1%
swap variables 226,434 225,035 -0.6%
simple object destructuring 144,668 140,605 -2.8%
with defaults 172,165 168,254 -2.3%
with renaming 170,504 165,285 -3.1%
nested object destructuring 87,322 86,219 -1.3%
rest properties 89,832 89,556 -0.3%
object parameter 53,871 53,864 -0.0%
array parameter 68,143 67,457 -1.0%
mixed destructuring in map 91,413 91,399 -0.0%
fibonacci.js — 3 unchanged · avg +0.3%
Benchmark Base (ops/sec) PR (ops/sec) Change
recursive fib(15) 144 145 +0.5%
recursive fib(20) 13 13 +0.9%
iterative fib(20) via reduce 68,351 68,080 -0.4%
json.js — 11 unchanged · avg -0.8%
Benchmark Base (ops/sec) PR (ops/sec) Change
parse simple object 129,985 126,999 -2.3%
parse nested object 78,253 77,822 -0.6%
parse array of objects 49,780 49,415 -0.7%
parse large flat object 52,929 52,704 -0.4%
parse mixed types 67,885 67,635 -0.4%
stringify simple object 108,541 106,291 -2.1%
stringify nested object 61,163 60,218 -1.5%
stringify array of objects 114,402 114,285 -0.1%
stringify mixed types 50,008 50,579 +1.1%
parse then stringify 39,961 39,205 -1.9%
stringify then parse 51,269 51,415 +0.3%
numbers.js — 11 unchanged · avg -0.5%
Benchmark Base (ops/sec) PR (ops/sec) Change
integer arithmetic 191,091 190,438 -0.3%
floating point arithmetic 211,605 209,591 -1.0%
number coercion 114,166 114,409 +0.2%
toFixed 77,556 76,875 -0.9%
toString 105,056 104,399 -0.6%
valueOf 142,385 141,034 -0.9%
toPrecision 98,478 97,570 -0.9%
Number.isNaN 168,997 167,651 -0.8%
Number.isFinite 161,914 162,385 +0.3%
Number.isInteger 165,678 164,833 -0.5%
Number.parseInt and parseFloat 147,380 147,970 +0.4%
objects.js — 7 unchanged · avg -0.2%
Benchmark Base (ops/sec) PR (ops/sec) Change
create simple object 228,684 229,468 +0.3%
create nested object 123,450 123,482 +0.0%
create 50 objects via Array.from 144,437 145,565 +0.8%
property read 106,384 105,143 -1.2%
Object.keys 73,715 72,925 -1.1%
Object.entries 47,284 47,661 +0.8%
spread operator 96,125 95,371 -0.8%
promises.js — 12 unchanged · avg -1.1%
Benchmark Base (ops/sec) PR (ops/sec) Change
Promise.resolve(value) 318,079 317,734 -0.1%
new Promise(resolve => resolve(value)) 125,502 121,980 -2.8%
Promise.reject(reason) 330,378 326,478 -1.2%
resolve + then (1 handler) 105,949 104,579 -1.3%
resolve + then chain (3 deep) 43,306 42,915 -0.9%
resolve + then chain (10 deep) 89,991 90,309 +0.4%
reject + catch + then 64,042 63,285 -1.2%
resolve + finally + then 56,836 55,810 -1.8%
Promise.all (5 resolved) 23,127 22,632 -2.1%
Promise.race (5 resolved) 24,314 23,890 -1.7%
Promise.allSettled (5 mixed) 18,603 18,546 -0.3%
Promise.any (5 mixed) 22,526 22,624 +0.4%
strings.js — 11 unchanged · avg +0.4%
Benchmark Base (ops/sec) PR (ops/sec) Change
string concatenation 240,153 239,306 -0.4%
template literal 258,075 257,139 -0.4%
string repeat 244,242 246,029 +0.7%
split and join 103,455 105,812 +2.3%
indexOf and includes 114,235 114,374 +0.1%
toUpperCase and toLowerCase 162,192 163,154 +0.6%
slice and substring 99,103 98,581 -0.5%
trim operations 119,908 121,514 +1.3%
replace and replaceAll 138,665 141,396 +2.0%
startsWith and endsWith 92,503 92,209 -0.3%
padStart and padEnd 128,315 127,264 -0.8%

Measured on ubuntu-latest x64. Changes within ±7% are considered insignificant.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
tests/built-ins/queueMicrotask/queueMicrotask.js (1)

55-93: Add a test for a throwing callback to verify drain continues.

The docs say queueMicrotask errors are silently discarded; a small test asserting that subsequent microtasks still run would lock this in.

➕ Suggested test
+test("queueMicrotask errors don't stop the queue", () => {
+  const log = [];
+  queueMicrotask(() => { throw new Error("boom"); });
+  queueMicrotask(() => log.push("after"));
+  return Promise.resolve().then(() => {
+    expect(log).toEqual(["after"]);
+  });
+});
Based on learnings: "When implementing a new language feature, create test files under tests/ covering happy paths, edge cases, and error cases following existing directory and naming patterns."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/built-ins/queueMicrotask/queueMicrotask.js` around lines 55 - 93, Add a
new unit test in the same tests/built-ins/queueMicrotask/queueMicrotask.js file
that verifies a throwing callback passed to queueMicrotask is silently discarded
and does not prevent later microtasks from running: schedule a microtask that
throws, then schedule another microtask (or use Promise.resolve().then chain)
that records a value, and assert after microtask drainage that the recorder
contains the expected value while the throw did not propagate; keep the pattern
consistent with existing tests (use queueMicrotask, Promise.resolve().then(...)
and expect(...).toEqual(...)) and give the test a descriptive name like
"queueMicrotask throwing callback does not stop subsequent microtasks".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@units/Goccia.Builtins.Globals.pas`:
- Around line 152-170: QueueMicrotaskCallback currently assigns the callback to
Task.Handler and enqueues it without GC protection; call AddTempRoot(Callback)
before TGocciaMicrotaskQueue.Instance.Enqueue(Task) so the JS function is
GC-rooted while stored in the Pascal microtask queue, and then ensure matching
RemoveTempRoot(Task.Handler) calls are made when tasks are removed (e.g., in
TGocciaMicrotaskQueue.Enqueue/DrainQueue/Clear or the code paths that execute or
discard a TGocciaMicrotask) so the temp root is released once the task is
drained or cleared.
- Line 23: Rename the parameters of QueueMicrotaskCallback to use the A prefix
(e.g., change Args to AArgs and ThisValue to AThisValue) and update all
references inside the function body and its declaration to match; also ensure
the function signature and body use 2-space indentation consistent with
.editorconfig.

---

Nitpick comments:
In `@tests/built-ins/queueMicrotask/queueMicrotask.js`:
- Around line 55-93: Add a new unit test in the same
tests/built-ins/queueMicrotask/queueMicrotask.js file that verifies a throwing
callback passed to queueMicrotask is silently discarded and does not prevent
later microtasks from running: schedule a microtask that throws, then schedule
another microtask (or use Promise.resolve().then chain) that records a value,
and assert after microtask drainage that the recorder contains the expected
value while the throw did not propagate; keep the pattern consistent with
existing tests (use queueMicrotask, Promise.resolve().then(...) and
expect(...).toEqual(...)) and give the test a descriptive name like
"queueMicrotask throwing callback does not stop subsequent microtasks".

function TypeErrorConstructor(Args: TGocciaArgumentsCollection; ThisValue: TGocciaValue): TGocciaValue;
function ReferenceErrorConstructor(Args: TGocciaArgumentsCollection; ThisValue: TGocciaValue): TGocciaValue;
function RangeErrorConstructor(Args: TGocciaArgumentsCollection; ThisValue: TGocciaValue): TGocciaValue;
function QueueMicrotaskCallback(Args: TGocciaArgumentsCollection; ThisValue: TGocciaValue): TGocciaValue;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Rename parameters to use the A prefix.

The new method introduces parameters Args and ThisValue, which deviates from the Pascal naming rule used elsewhere in the codebase.

🔧 Suggested rename
-    function QueueMicrotaskCallback(Args: TGocciaArgumentsCollection; ThisValue: TGocciaValue): TGocciaValue;
+    function QueueMicrotaskCallback(AArgs: TGocciaArgumentsCollection; AThisValue: TGocciaValue): TGocciaValue;
-function TGocciaGlobals.QueueMicrotaskCallback(Args: TGocciaArgumentsCollection; ThisValue: TGocciaValue): TGocciaValue;
+function TGocciaGlobals.QueueMicrotaskCallback(AArgs: TGocciaArgumentsCollection; AThisValue: TGocciaValue): TGocciaValue;
-  if Args.Length = 0 then
+  if AArgs.Length = 0 then
     ThrowTypeError('Failed to execute ''queueMicrotask'': 1 argument required, but only 0 present.');

-  Callback := Args.GetElement(0);
+  Callback := AArgs.GetElement(0);
As per coding guidelines: "Use F prefix for private fields, A prefix for parameters; use 2 spaces for indentation as specified in .editorconfig".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@units/Goccia.Builtins.Globals.pas` at line 23, Rename the parameters of
QueueMicrotaskCallback to use the A prefix (e.g., change Args to AArgs and
ThisValue to AThisValue) and update all references inside the function body and
its declaration to match; also ensure the function signature and body use
2-space indentation consistent with .editorconfig.

Comment on lines +152 to +170
function TGocciaGlobals.QueueMicrotaskCallback(Args: TGocciaArgumentsCollection; ThisValue: TGocciaValue): TGocciaValue;
var
Callback: TGocciaValue;
Task: TGocciaMicrotask;
begin
if Args.Length = 0 then
ThrowTypeError('Failed to execute ''queueMicrotask'': 1 argument required, but only 0 present.');

Callback := Args.GetElement(0);
if not Callback.IsCallable then
ThrowTypeError('Failed to execute ''queueMicrotask'': parameter 1 is not of type ''Function''.');

Task.Handler := Callback;
Task.ResultPromise := nil;
Task.Value := TGocciaUndefinedLiteralValue.UndefinedValue;
Task.ReactionType := prtFulfill;

TGocciaMicrotaskQueue.Instance.Enqueue(Task);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Ensure queued callbacks are GC-rooted between enqueue and drain.

queueMicrotask can enqueue callbacks that are no longer referenced in any JS scope after the call returns. If GC runs before DrainQueue, those values can be collected because they are only held by the Pascal queue. Please add temp-rooting for microtasks at enqueue time and remove roots when tasks are drained or cleared.

🛡️ Example fix (in `units/Goccia.MicrotaskQueue.pas`)
 procedure TGocciaMicrotaskQueue.Enqueue(const AMicrotask: TGocciaMicrotask);
 begin
+  if Assigned(TGocciaGC.Instance) then
+  begin
+    if Assigned(AMicrotask.Handler) then
+      TGocciaGC.Instance.AddTempRoot(AMicrotask.Handler);
+    if Assigned(AMicrotask.Value) then
+      TGocciaGC.Instance.AddTempRoot(AMicrotask.Value);
+    if Assigned(AMicrotask.ResultPromise) then
+      TGocciaGC.Instance.AddTempRoot(AMicrotask.ResultPromise);
+  end;
   FQueue.Add(AMicrotask);
 end;
-    if Assigned(TGocciaGC.Instance) then
-    begin
-      if Assigned(Task.Handler) then
-        TGocciaGC.Instance.AddTempRoot(Task.Handler);
-      if Assigned(Task.Value) then
-        TGocciaGC.Instance.AddTempRoot(Task.Value);
-      if Assigned(Promise) then
-        TGocciaGC.Instance.AddTempRoot(Promise);
-    end;
+    // Temp roots already added at Enqueue; just remove in finally.
 procedure TGocciaMicrotaskQueue.ClearQueue;
+var
+  Task: TGocciaMicrotask;
 begin
+  if Assigned(TGocciaGC.Instance) then
+    for Task in FQueue do
+    begin
+      if Assigned(Task.Handler) then
+        TGocciaGC.Instance.RemoveTempRoot(Task.Handler);
+      if Assigned(Task.Value) then
+        TGocciaGC.Instance.RemoveTempRoot(Task.Value);
+      if Assigned(Task.ResultPromise) then
+        TGocciaGC.Instance.RemoveTempRoot(Task.ResultPromise);
+    end;
   FQueue.Clear;
 end;
Based on learnings: "Values held only by Pascal code (not in any GocciaScript scope) must be protected with AddTempRoot/RemoveTempRoot for the duration they are needed."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@units/Goccia.Builtins.Globals.pas` around lines 152 - 170,
QueueMicrotaskCallback currently assigns the callback to Task.Handler and
enqueues it without GC protection; call AddTempRoot(Callback) before
TGocciaMicrotaskQueue.Instance.Enqueue(Task) so the JS function is GC-rooted
while stored in the Pascal microtask queue, and then ensure matching
RemoveTempRoot(Task.Handler) calls are made when tasks are removed (e.g., in
TGocciaMicrotaskQueue.Enqueue/DrainQueue/Clear or the code paths that execute or
discard a TGocciaMicrotask) so the temp root is released once the task is
drained or cleared.

@frostney frostney merged commit e23196a into main Feb 18, 2026
3 checks passed
@frostney frostney deleted the feat-queuemicrotask branch February 18, 2026 14:25
@frostney frostney added the new feature New feature or request label Apr 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant