Skip to content

refactor(queue): back Queue by Deque instead of a linked list - #3918

Merged
bobzhang merged 2 commits into
mainfrom
Yu-zh/queue
Aug 5, 2026
Merged

refactor(queue): back Queue by Deque instead of a linked list#3918
bobzhang merged 2 commits into
mainfrom
Yu-zh/queue

Conversation

@Yu-zh

@Yu-zh Yu-zh commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

What

Reimplements @queue.Queue as a thin wrapper around @deque.Deque, replacing the cons-cell linked list. Elements now live in one contiguous circular buffer instead of a heap cell per element.

How

  • Most Queue methods are one-line delegations: pushpush_back, pop/peekpop_front/front, plus clear, length, is_empty, copy, iter, from_iter, and the Queue(arr) constructor. transfer becomes Deque::append + clear, keeping the self-transfer no-op (guarded by physical equality of the inner deques).
  • each/eachi/fold keep their #locals(f) guarantee by iterating with for-in loops instead of passing f to deque's methods — locality does not propagate across the package boundary.
  • The cell_equal test poked at the removed Cons internals; its intent (empty ≠ non-empty) is folded into the equal test.

API impact

queue/pkg.generated.mbti has a zero diff — the public queue API is unchanged, including the abort("Queue is empty") messages of unsafe_peek/unsafe_pop. The deque package is untouched.

Deferred: #valtype unboxing

Marking the wrapper #valtype (so Queue[A] is represented as the bare deque reference, no wrapper allocation) was tried and works, but requires Deque to become a non-abstract pub struct { // private fields }, because #valtype fields cannot be abstract types. That's a public representation commitment on the deque package, so it's left for a follow-up discussion. The extra cost of the boxed wrapper is one small heap object per queue instance, not per element.

Verification

  • moon check: clean, whole repo
  • moon test: 6775/6775 pass (wasm-gc)
  • moon test --target native -p queue -p deque -p debug: 434/434 pass
  • moon test --target js -p queue -p deque -p debug: 448/448 pass

@coveralls

coveralls commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Coverage Report for CI Build 5738

Coverage decreased (-0.02%) to 90.427%

Details

  • Coverage decreased (-0.02%) from the base build.
  • Patch coverage: 25 of 25 lines across 1 file are fully covered (100%).
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 17727
Covered Lines: 16030
Line Coverage: 90.43%
Coverage Strength: 170839.16 hits per line

💛 - Coveralls

Yu-zh added 2 commits August 5, 2026 12:05
Replace the cons-cell linked list with a #valtype wrapper around
@deque.Deque, so elements live in one contiguous circular buffer
instead of a heap cell per element, and the wrapper itself is unboxed.

- Most Queue methods delegate directly to the deque (push_back,
  pop_front/front, clear, copy, iter, from_iter, append for transfer).
- each/eachi/fold keep their #locals(f) guarantee via for-in loops,
  since locality does not propagate across the package boundary.
- Deque becomes a pub struct with priv fields: #valtype fields cannot
  be abstract types, and this exposes only "Deque is a concrete boxed
  struct" while keeping buf/len/head inaccessible.
- transfer's self-transfer guard compares the inner deques physically,
  as wrapper identity is gone once the struct is unboxed.

Queue's generated interface (pkg.generated.mbti) is unchanged.
Unboxing the Queue wrapper requires Deque to be a non-abstract pub
struct, since #valtype fields cannot be abstract types. Revert that
API commitment for now: Queue stays a plain boxed single-field
wrapper, and the deque package is untouched by this branch again.
The #valtype optimization can be revisited later.
@bobzhang

bobzhang commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review sign-off (Claude)

Reviewed the full diff, rebased onto latest main, and re-verified. LGTM — sign-off.

Semantics checked against @deque's public API

  • Eq: Deque's Eq is structural (length check + elementwise compare) — identical semantics to the old length && first chain comparison. The removed cell_equal test's intent (empty ≠ non-empty) is preserved in the equal test.
  • iter: Deque::iter carries size_hint=len, matching the old hand-rolled Iter::new(..., size_hint=len).
  • unsafe_peek / unsafe_pop: abort("Queue is empty") messages preserved; queue/pkg.generated.mbti has a zero diff.
  • transfer: Deque::append + clear is correct for distinct deques, and the physical_equal(self.inner, dst.inner) guard covers self-transfer — inner is never shared between distinct Queue wrappers (constructors and copy always allocate a fresh deque), so the guard is equivalent to the old wrapper-level check. Note: transfer goes from O(1) list splicing to an O(n) element copy; no documented complexity contract is broken, and it's the expected trade-off of a contiguous buffer.
  • #locals(f): preserved by iterating with for-in loops and calling f in the loop body instead of passing f across the package boundary — each/eachi/fold all follow this pattern.
  • Arbitrary: core/quickcheck (which now owns the impl after refactor: replace fall-through match with guard ... is #3946-era move on main) builds queues via public Queue::from_iter, so it composes with the new representation unchanged.

Rebase

Conflicts came from main's removal of the in-package Arbitrary impl + quickcheck import (moved to core/quickcheck) and doc-comment sweeps. Resolutions:

  • queue/moon.pkg: kept main's quickcheck import removal, added the deque import.
  • queue/queue.mbt: took the deque-backed bodies; the tail conflict deleted both sides (main's removed Arbitrary impl and this branch's removed cell_equal test).
  • deque/ is net-untouched by the branch, as intended.

Verification (after rebase, moon 0.1.20260731 nightly)

  • moon check: clean, whole repo
  • moon test (wasm-gc): 7002/7002
  • moon test --target native -p queue -p deque -p debug: 355/355
  • moon test --target js -p queue -p deque -p debug: 367/367
  • moon fmt + moon info: zero drift; queue/pkg.generated.mbti unchanged

🤖 Generated with Claude Code

@bobzhang
bobzhang marked this pull request as ready for review August 5, 2026 04:48
Copilot AI lite review requested due to automatic review settings August 5, 2026 04:48
@bobzhang
bobzhang merged commit 45fc229 into main Aug 5, 2026
16 checks passed
@bobzhang
bobzhang deleted the Yu-zh/queue branch August 5, 2026 04:50

Copilot AI left a comment

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.

Pull request overview

Refactors @queue.Queue to be implemented as a thin wrapper over @deque.Deque, replacing the previous cons-cell singly-linked-list representation while keeping the public API unchanged (as indicated by the zero-diff generated .mbti).

Changes:

  • Replaced internal queue storage from Cons cells to an inner : @deque.Deque[A] buffer.
  • Updated core queue operations (push, pop, peek, copy, iter, from_iter, transfer) to delegate to Deque equivalents, with each/eachi/fold using for-in loops to preserve #locals(f) behavior.
  • Updated tests to remove Cons-internal assertions and keep intent via higher-level equality checks.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
queue/types.mbt Replaces the linked-list cell type with a Deque-backed Queue wrapper.
queue/queue.mbt Reimplements queue operations as Deque delegations and adjusts iteration/fold and tests accordingly.
queue/moon.pkg Adds moonbitlang/core/deque dependency for the new backing implementation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread queue/queue.mbt
Comment on lines 64 to 66
pub fn[A] Queue::clear(self : Queue[A]) -> Unit {
self.length = 0
self.first = None
self.last = None
self.inner.clear()
}
Comment thread queue/queue.mbt
Comment on lines 266 to +271
pub fn[A] Queue::transfer(self : Queue[A], dst : Queue[A]) -> Unit {
if physical_equal(self, dst) {
if physical_equal(self.inner, dst.inner) {
return // no-op for self-transfer
}
if self.length > 0 {
match dst.last {
None => {
dst.length = self.length
dst.first = self.first
dst.last = self.last
self.clear()
}
Some(last) => {
last.next = self.first
dst.length += self.length
dst.last = self.last
self.clear()
}
}
}
dst.inner.append(self.inner)
self.inner.clear()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants