Commit0e639830Recorded17 Jul 2026Repositorysigil-system
Use list-sort for the canonical listing order
Message
The first cut of this hand-rolled a merge sort on the belief that the runtime exposes no sort. It does: (sigil list)'s list-sort, predicate first per SRFI-132. sort is unbound, which is what sends people looking — and two modules independently wrote their own as a result.
The hand-rolled one was also worse, and worse exactly here. Its merge built results with (cons x (merge ...)) — not tail position — so it cost O(n) stack and overflowed at 10,000 elements (measured). A directory listing is precisely the unbounded input: /gnu/store is ~190k entries. list-sort's merge accumulates and append-reverses, so it is tail-recursive and stable.
Changed
src/sigil/system/fs.sgl | 58 ++++++++++++++++++++++++++++++++++++++--------------------
1 file changed, 38 insertions(+), 20 deletions(-)Diff
src/sigil/system/fs.sglmodified
@@ -27,12 +27,14 @@
27
(import (sigil fs) 28
(sigil path) 29
(sigil io)+30
(sigil list) ; list-sort (stable, stack-safe — listing order) 31
(sigil string) ; string<? / string-downcase (listing order) 32
(sigil process) ; getenv (fs-home) 33
(sigil system grant)) 34
35
(export 36
fs-home+37
fs-sort-entries 38
fs-read-dir 39
fs-stat 40
fs-read-file@@ -109,8 +111,16 @@
111
(define (hidden-name? name) 112
(and (> (string-length name) 0) (char=? (string-ref name 0) #\.))) 113
+114
;; `type:` is a SYMBOL in a sigil-system stat dict, but a wrapper that has+115
;; already marshalled for the wire carries the string. Accept both, so a caller+116
;; can sort on either side of its own encoding step rather than being forced to+117
;; sort before it (or to re-implement the rule).+118
(define (entry-directory? e)+119
(let ((t (dict-ref e type: #f)))+120
(or (eq? t 'directory) (equal? t "directory"))))+121
122
(define (entry-tier e)−113
(let ((dir? (eq? (dict-ref e type: #f) 'directory))+123
(let ((dir? (entry-directory? e)) 124
(hidden? (hidden-name? (dict-ref e name: "")))) 125
(cond ((and dir? hidden?) 0) 126
(dir? 1)@@ -131,35 +141,43 @@
141
((string<? nb na) #f) 142
(else (string<? (dict-ref a name: "") (dict-ref b name: ""))))))))) 143
−134
;; A self-contained stable merge sort. The runtime exposes no `sort` (the same−135
;; reason (slate selector) carries its own), and a listing can be large enough−136
;; that the O(n log n) matters.−137
(define (msort-split xs)−138
(cond ((null? xs) (cons '() '()))−139
((null? (cdr xs)) (cons xs '()))−140
(else (let ((r (msort-split (cddr xs))))−141
(cons (cons (car xs) (car r)) (cons (cadr xs) (cdr r)))))))−142
(define (msort-merge before? a b)−143
(cond ((null? a) b)−144
((null? b) a)−145
((before? (car b) (car a)) (cons (car b) (msort-merge before? a (cdr b))))−146
(else (cons (car a) (msort-merge before? (cdr a) b)))))−147
(define (msort before? xs)−148
(if (or (null? xs) (null? (cdr xs)))−149
xs−150
(let ((s (msort-split xs)))−151
(msort-merge before? (msort before? (car s)) (msort before? (cdr s))))))+144
;; Sorting is (sigil list)'s `list-sort`: a stable merge sort whose own merge is+145
;; tail-recursive (it accumulates and append-reverses), so it costs O(log n)+146
;; stack rather than O(n). That matters here more than anywhere — a directory+147
;; listing is exactly the unbounded-length input, and /gnu/store is ~190k+148
;; entries. A hand-rolled merge that builds with (cons x (merge …)) is not in+149
;; tail position and overflows at 10k (measured), so `sort` being unbound is NOT+150
;; a reason to write one: the name is just `list-sort`, predicate first+151
;; (SRFI-132).+152
+153
;;; Put a list of entry dicts (anything carrying `name:` and `type:`)+154
;;; into the canonical order described above.+155
;;;+156
;;; Exported because `fs-read-dir` is NOT the only listing path. A+157
;;; wrapper serving a browsable UI cannot always use it: fs-read-dir+158
;;; stats EVERY name, which at /gnu/store scale (~190k entries) is+159
;;; prohibitive, so such a wrapper lists names, caps, and stats only+160
;;; the survivors. That wrapper still wants THIS order, and should get+161
;;; it by calling this — not by re-deriving the rule and drifting from+162
;;; it. (lantern-system's fs.read-dir is exactly that caller.)+163
(define (fs-sort-entries entries)+164
(: list? -> list?)+165
(list-sort entry-before? entries)) 166
167
;;; List a directory. Returns one stat dict per entry (the one 168
;;; round-trip a directory view needs), in the canonical order above. 169
;;; Entries that vanish between the listing and the stat are skipped.+170
;;;+171
;;; Stats every entry: fine for ordinary directories, prohibitive for+172
;;; a pathological one. A caller that must cap should list + cap+173
;;; itself and call `fs-sort-entries` on what it kept. 174
(define (fs-read-dir g path) 175
(: any? string? -> list?) 176
(grant-assert! g 'fs-read path) 177
(let ((names (directory-list path))) 178
(unless names 179
(error "fs-read-dir: cannot list directory" path))−162
(msort entry-before?+180
(fs-sort-entries 181
(fold-right 182
(lambda (name acc) 183
(let* ((entry-path (path-join path name))