Commit78ee3610Recorded3 Mar 2026Repositorysigil-format
Optimize formatter: vector tokens and inlined tokenizer hot loops
Message
Replace define-struct tokens with raw vectors (#(type start end line col indent)) to avoid keyword-arg construction overhead. Inline all tokenizer hot paths (skip-whitespace, skip-to-delimiter, skip-until-newline, read-comment, read-string-token) to use direct vector-ref instead of closure-based peek/advance! calls. Add delimiter lookup table for O(1) checks.
Tokenize phase: 30s -> 1.7s on build.sgl (2829 lines, 135KB). Total format time: 30s -> 4.5s.
Changed
src/sigil/format.sgl | 787 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------------------------------------------------------------------------
test/test-format.sgl | 45 ++++++----
2 files changed, 387 insertions(+), 445 deletions(-)Diff
src/sigil/format.sglmodified
@@ -15,7 +15,8 @@
15
(import (sigil struct) 16
(sigil string) 17
(sigil io)−18
(sigil json))+18
(sigil json)+19
(sigil fs)) 20
21
(export 22
;; Core API@@ -64,7 +65,9 @@
65
token-indent 66
67
;; Tokenizer (for testing)−67
tokenize)+68
tokenize+69
tokenize-result-chars+70
tokenize-result-tokens) 71
72
(begin 73
@@ -103,94 +106,52 @@
106
;;; Tokens 107
;; ============================================================ 108
−106
(define-struct token−107
(type) ; 'lparen 'rparen 'lbracket 'rbracket−108
; 'lbrace 'rbrace−109
; 'hash-lbrace 'hash-lbracket−110
; 'symbol 'string 'number 'char−111
; 'quote 'quasiquote 'unquote 'unquote-splicing−112
; 'comment 'doc-comment 'block-comment−113
; 'hash-t 'hash-f 'hash-other−114
; 'dot 'eof 'whitespace 'newline−115
(value) ; the actual text−116
(line) ; 1-based line number−117
(column) ; 1-based column−118
(indent)) ; indentation at start of this line+109
;; Token layout: #(type start end line column indent)+110
;; Using raw vectors avoids keyword-arg overhead in the hot path. 111
−120
;; ============================================================−121
;;; Tokenizer State−122
;; ============================================================+112
;;; Check if value is a token+113
(define (token? x) (and (vector? x) (= (vector-length x) 6))) 114
−124
(define-struct tokenizer−125
(source)−126
(filename)−127
(pos mutable: #t)−128
(line mutable: #t)−129
(column mutable: #t)−130
(line-indent mutable: #t))+115
;;; Token type accessor+116
(define (token-type tok) (vector-ref tok 0)) 117
−132
;; ============================================================−133
;;; Tokenizer Implementation−134
;; ============================================================+118
;;; Token start position accessor+119
(define (token-start tok) (vector-ref tok 1)) 120
−136
;;; Check if at end of source−137
(define (tokenizer-eof? t)−138
(>= (tokenizer-pos t) (string-length (tokenizer-source t))))+121
;;; Token end position accessor+122
(define (token-end tok) (vector-ref tok 2)) 123
−140
;;; Peek at current character without advancing−141
(define (tokenizer-peek t)−142
(if (tokenizer-eof? t)−143
#f−144
(string-ref (tokenizer-source t) (tokenizer-pos t))))+124
;;; Token line accessor+125
(define (token-line tok) (vector-ref tok 3)) 126
−146
;;; Peek at character at offset from current position−147
(define (tokenizer-peek-ahead t offset)−148
(let ((new-pos (+ (tokenizer-pos t) offset)))−149
(if (>= new-pos (string-length (tokenizer-source t)))−150
#f−151
(string-ref (tokenizer-source t) new-pos))))−152
−153
;;; Advance position by one character−154
(define (tokenizer-advance! t)−155
(let ((c (tokenizer-peek t)))−156
(if c−157
(begin−158
(set-tokenizer-pos! t (+ (tokenizer-pos t) 1))−159
(if (char=? c #\newline)−160
(begin−161
(set-tokenizer-line! t (+ (tokenizer-line t) 1))−162
(set-tokenizer-column! t 1)−163
;; Calculate indentation of next line−164
(set-tokenizer-line-indent! t (count-leading-spaces t)))−165
(set-tokenizer-column! t (+ (tokenizer-column t) 1)))−166
c)−167
#f)))−168
−169
;;; Count leading spaces from current position (for indent calculation)−170
(define (count-leading-spaces t)−171
(let loop ((offset 0))−172
(let ((c (tokenizer-peek-ahead t offset)))−173
(cond−174
((not c) offset)−175
((char=? c #\space) (loop (+ offset 1)))−176
((char=? c #\tab) (loop (+ offset 2))) ; treat tab as 2 spaces−177
(else offset)))))+127
;;; Token column accessor+128
(define (token-column tok) (vector-ref tok 4)) 129
−179
;;; Check if character is a delimiter−180
(define (delimiter? c)−181
(or (not c)−182
(char=? c #\()−183
(char=? c #\))−184
(char=? c #\[)−185
(char=? c #\])−186
(char=? c #\{)−187
(char=? c #\})−188
(char=? c #\")−189
(char=? c #\;)−190
(char=? c #\')−191
(char=? c #\`)−192
(char=? c #\,)−193
(char-whitespace? c)))+130
;;; Token indent accessor+131
(define (token-indent tok) (vector-ref tok 5))+132
+133
;;; Extract the text value of a token from the source chars vector+134
(define (token-value tok chars)+135
(let* ((s (vector-ref tok 1))+136
(e (vector-ref tok 2))+137
(n (- e s))+138
(v (make-string n)))+139
(let loop ((i 0))+140
(if (>= i n)+141
v+142
(begin+143
(string-set! v i (vector-ref chars (+ s i)))+144
(loop (+ i 1)))))))+145
+146
;;; Get the chars vector from a tokenize result+147
(define (tokenize-result-chars result) (car result))+148
+149
;;; Get the token list from a tokenize result+150
(define (tokenize-result-tokens result) (cdr result))+151
+152
;; ============================================================+153
;;; Tokenizer Implementation+154
;; ============================================================ 155
156
;;; Check if character is whitespace 157
(define (char-whitespace? c)@@ -206,319 +167,19 @@
167
(char>=? c #\0) 168
(char<=? c #\9))) 169
−209
;;; Check if character can start a number−210
(define (number-start? c next)−211
(or (char-digit? c)−212
(and (or (and c (char=? c #\-))−213
(and c (char=? c #\+)))−214
(char-digit? next))))−215
−216
;;; Make a token at current position−217
(define (make-token-here t tok-type value)−218
(token−219
type: tok-type−220
value: value−221
line: (tokenizer-line t)−222
column: (tokenizer-column t)−223
indent: (tokenizer-line-indent t)))−224
−225
;;; Tokenize the next token from source−226
(define (tokenize-next t)−227
(if (tokenizer-eof? t)−228
(make-token-here t 'eof "")−229
(let ((c (tokenizer-peek t))−230
(start-line (tokenizer-line t))−231
(start-col (tokenizer-column t))−232
(start-indent (tokenizer-line-indent t)))−233
(cond−234
;; Whitespace (but not newline)−235
((and c (or (char=? c #\space) (char=? c #\tab) (char=? c #\return)))−236
(let ((ws (read-whitespace t)))−237
(token type: 'whitespace value: ws−238
line: start-line column: start-col indent: start-indent)))−239
−240
;; Newline−241
((and c (char=? c #\newline))−242
(tokenizer-advance! t)−243
(token type: 'newline value: "\n"−244
line: start-line column: start-col indent: start-indent))−245
−246
;; Comments−247
((and c (char=? c #\;))−248
(read-comment t start-line start-col start-indent))−249
−250
;; Parentheses and brackets−251
((and c (char=? c #\())−252
(tokenizer-advance! t)−253
(token type: 'lparen value: "("−254
line: start-line column: start-col indent: start-indent))−255
((and c (char=? c #\)))−256
(tokenizer-advance! t)−257
(token type: 'rparen value: ")"−258
line: start-line column: start-col indent: start-indent))−259
((and c (char=? c #\[))−260
(tokenizer-advance! t)−261
(token type: 'lbracket value: "["−262
line: start-line column: start-col indent: start-indent))−263
((and c (char=? c #\]))−264
(tokenizer-advance! t)−265
(token type: 'rbracket value: "]"−266
line: start-line column: start-col indent: start-indent))−267
((and c (char=? c #\{))−268
(tokenizer-advance! t)−269
(token type: 'lbrace value: "{"−270
line: start-line column: start-col indent: start-indent))−271
((and c (char=? c #\}))−272
(tokenizer-advance! t)−273
(token type: 'rbrace value: "}"−274
line: start-line column: start-col indent: start-indent))−275
−276
;; Quote forms−277
((and c (char=? c #\'))−278
(tokenizer-advance! t)−279
(token type: 'quote value: "'"−280
line: start-line column: start-col indent: start-indent))−281
((and c (char=? c #\`))−282
(tokenizer-advance! t)−283
(token type: 'quasiquote value: "`"−284
line: start-line column: start-col indent: start-indent))−285
((and c (char=? c #\,))−286
(tokenizer-advance! t)−287
(if (and (not (tokenizer-eof? t))−288
(char=? (tokenizer-peek t) #\@))−289
(begin−290
(tokenizer-advance! t)−291
(token type: 'unquote-splicing value: ",@"−292
line: start-line column: start-col indent: start-indent))−293
(token type: 'unquote value: ","−294
line: start-line column: start-col indent: start-indent)))−295
−296
;; String−297
((and c (char=? c #\"))−298
(read-string-token t start-line start-col start-indent))−299
−300
;; Hash forms−301
((and c (char=? c #\#))−302
(read-hash-token t start-line start-col start-indent))−303
−304
;; Dot−305
((and c (char=? c #\.))−306
(let ((next (tokenizer-peek-ahead t 1)))−307
(if (delimiter? next)−308
(begin−309
(tokenizer-advance! t)−310
(token type: 'dot value: "."−311
line: start-line column: start-col indent: start-indent))−312
(read-symbol-or-number t start-line start-col start-indent))))−313
−314
;; Number or symbol starting with +/-−315
((and c (or (char=? c #\+) (char=? c #\-)))−316
(read-symbol-or-number t start-line start-col start-indent))−317
−318
;; Number−319
((char-digit? c)−320
(read-number t start-line start-col start-indent))−321
−322
;; Symbol−323
(else−324
(read-symbol t start-line start-col start-indent))))))−325
−326
;;; Read whitespace (not including newlines)−327
(define (read-whitespace t)−328
(let loop ((chars '()))−329
(let ((c (tokenizer-peek t)))−330
(if (and c (or (char=? c #\space) (char=? c #\tab) (char=? c #\return)))−331
(begin−332
(tokenizer-advance! t)−333
(loop (cons c chars)))−334
(list->string (reverse chars))))))−335
−336
;;; Read a comment (line comment or doc comment)−337
(define (read-comment t start-line start-col start-indent)−338
;; Count semicolons−339
(let loop ((count 0))−340
(if (and (not (tokenizer-eof? t))−341
(char=? (tokenizer-peek t) #\;))−342
(begin−343
(tokenizer-advance! t)−344
(loop (+ count 1)))−345
(let ((text (read-until-newline t)))−346
(token type: (if (>= count 3) 'doc-comment 'comment)−347
value: (string-append (string-repeat ";" count) text)−348
line: start-line column: start-col indent: start-indent)))))−349
−350
;;; Read until end of line−351
(define (read-until-newline t)−352
(let loop ((chars '()))−353
(let ((c (tokenizer-peek t)))−354
(if (or (not c) (char=? c #\newline))−355
(list->string (reverse chars))−356
(begin−357
(tokenizer-advance! t)−358
(loop (cons c chars)))))))−359
−360
;;; Read a string token−361
(define (read-string-token t start-line start-col start-indent)−362
(tokenizer-advance! t) ; consume opening quote−363
(let loop ((chars '(#\")))−364
(let ((c (tokenizer-peek t)))−365
(cond−366
((not c)−367
;; Unterminated string−368
(token type: 'string value: (list->string (reverse chars))−369
line: start-line column: start-col indent: start-indent))−370
((char=? c #\")−371
(tokenizer-advance! t)−372
(token type: 'string value: (list->string (reverse (cons #\" chars)))−373
line: start-line column: start-col indent: start-indent))−374
((char=? c #\\)−375
(tokenizer-advance! t)−376
(let ((escaped (tokenizer-peek t)))−377
(if escaped−378
(begin−379
(tokenizer-advance! t)−380
(loop (cons escaped (cons #\\ chars))))−381
(loop (cons #\\ chars)))))−382
(else−383
(tokenizer-advance! t)−384
(loop (cons c chars)))))))−385
−386
;;; Read a hash token (#t, #f, #\char, #|...|#, #{...}, #[...], etc.)−387
(define (read-hash-token t start-line start-col start-indent)−388
(tokenizer-advance! t) ; consume #−389
(let ((c (tokenizer-peek t)))−390
(cond−391
((not c)−392
(token type: 'hash-other value: "#"−393
line: start-line column: start-col indent: start-indent))−394
((or (char=? c #\t) (char=? c #\T))−395
(tokenizer-advance! t)−396
(token type: 'hash-t value: "#t"−397
line: start-line column: start-col indent: start-indent))−398
((or (char=? c #\f) (char=? c #\F))−399
(tokenizer-advance! t)−400
(token type: 'hash-f value: "#f"−401
line: start-line column: start-col indent: start-indent))−402
((char=? c #\\)−403
(read-char-token t start-line start-col start-indent))−404
((char=? c #\|)−405
(read-block-comment t start-line start-col start-indent))−406
((char=? c #\{)−407
;; Dict literal: #{...}−408
(tokenizer-advance! t)−409
(token type: 'hash-lbrace value: "#{"−410
line: start-line column: start-col indent: start-indent))−411
((char=? c #\[)−412
;; Array literal: #[...]−413
(tokenizer-advance! t)−414
(token type: 'hash-lbracket value: "#["−415
line: start-line column: start-col indent: start-indent))−416
(else−417
(let ((text (read-hash-datum t)))−418
(token type: 'hash-other value: (string-append "#" text)−419
line: start-line column: start-col indent: start-indent))))))−420
−421
;;; Read character literal−422
(define (read-char-token t start-line start-col start-indent)−423
(tokenizer-advance! t) ; consume backslash−424
(let ((chars (list #\\ #\#)))−425
;; Always consume the first character after #\ (it IS the char value,−426
;; even if it's a delimiter like " ( ) ;)−427
(let ((c (tokenizer-peek t)))−428
(if (not c)−429
(token type: 'char value: (list->string (reverse chars))−430
line: start-line column: start-col indent: start-indent)−431
(begin−432
(tokenizer-advance! t)−433
;; For named chars like #\space, continue reading non-delimiter chars−434
(let loop ((cs (cons c chars)))−435
(let ((c (tokenizer-peek t)))−436
(if (delimiter? c)−437
(token type: 'char value: (list->string (reverse cs))−438
line: start-line column: start-col indent: start-indent)−439
(begin−440
(tokenizer-advance! t)−441
(loop (cons c cs)))))))))))−442
−443
;;; Read block comment #| ... |#−444
(define (read-block-comment t start-line start-col start-indent)−445
(tokenizer-advance! t) ; consume |−446
(let loop ((chars (list #\| #\#)) (depth 1))−447
(if (= depth 0)−448
(token type: 'block-comment value: (list->string (reverse chars))−449
line: start-line column: start-col indent: start-indent)−450
(let ((c (tokenizer-peek t)))−451
(cond−452
((not c)−453
(token type: 'block-comment value: (list->string (reverse chars))−454
line: start-line column: start-col indent: start-indent))−455
((char=? c #\|)−456
(tokenizer-advance! t)−457
(if (and (not (tokenizer-eof? t))−458
(char=? (tokenizer-peek t) #\#))−459
(begin−460
(tokenizer-advance! t)−461
(loop (cons #\# (cons #\| chars)) (- depth 1)))−462
(loop (cons c chars) depth)))−463
((char=? c #\#)−464
(tokenizer-advance! t)−465
(if (and (not (tokenizer-eof? t))−466
(char=? (tokenizer-peek t) #\|))−467
(begin−468
(tokenizer-advance! t)−469
(loop (cons #\| (cons #\# chars)) (+ depth 1)))−470
(loop (cons c chars) depth)))−471
(else−472
(tokenizer-advance! t)−473
(loop (cons c chars) depth)))))))−474
−475
;;; Read hash datum (like #:keyword or similar)−476
(define (read-hash-datum t)−477
(let loop ((chars '()))−478
(let ((c (tokenizer-peek t)))−479
(if (delimiter? c)−480
(list->string (reverse chars))−481
(begin−482
(tokenizer-advance! t)−483
(loop (cons c chars)))))))−484
−485
;;; Read a symbol−486
(define (read-symbol t start-line start-col start-indent)−487
(let loop ((chars '()))−488
(let ((c (tokenizer-peek t)))−489
(if (delimiter? c)−490
(token type: 'symbol value: (list->string (reverse chars))−491
line: start-line column: start-col indent: start-indent)−492
(begin−493
(tokenizer-advance! t)−494
(loop (cons c chars)))))))−495
−496
;;; Read a number−497
(define (read-number t start-line start-col start-indent)−498
(let loop ((chars '()))−499
(let ((c (tokenizer-peek t)))−500
(if (delimiter? c)−501
(token type: 'number value: (list->string (reverse chars))−502
line: start-line column: start-col indent: start-indent)−503
(begin−504
(tokenizer-advance! t)−505
(loop (cons c chars)))))))−506
−507
;;; Read symbol or number (for things starting with +/-)−508
(define (read-symbol-or-number t start-line start-col start-indent)−509
(let loop ((chars '()))−510
(let ((c (tokenizer-peek t)))−511
(if (delimiter? c)−512
(let ((text (list->string (reverse chars))))−513
;; Determine if it's a number or symbol−514
(if (looks-like-number? text)−515
(token type: 'number value: text−516
line: start-line column: start-col indent: start-indent)−517
(token type: 'symbol value: text−518
line: start-line column: start-col indent: start-indent)))−519
(begin−520
(tokenizer-advance! t)−521
(loop (cons c chars)))))))+170
;; Delimiter lookup table — indexed by char code, #t if delimiter+171
(define delimiter-table+172
(let ((t (make-vector 128 #f)))+173
(for-each (lambda (c) (vector-set! t (char->integer c) #t))+174
'(#\( #\) #\[ #\] #\{ #\} #\" #\; #\' #\` #\,+175
#\space #\tab #\newline #\return))+176
t))+177
+178
;;; Check if character is a delimiter+179
(define (delimiter? c)+180
(or (not c)+181
(let ((code (char->integer c)))+182
(and (< code 128) (vector-ref delimiter-table code))))) 183
184
;;; Check if string looks like a number 185
(define (looks-like-number? s)@@ -537,15 +198,276 @@
198
(else #f)))))) 199
200
;;; Tokenize entire source into a list of tokens+201
;;; Returns (cons chars tokens) where chars is the source as a vector 202
(define (tokenize source filename)−541
(let ((t (tokenizer source: source filename: filename pos: 0 line: 1 column: 1 line-indent: 0)))−542
;; Initialize line indent for first line−543
(set-tokenizer-line-indent! t (count-leading-spaces t))+203
(let* ((chars (string->vector source))+204
(len (vector-length chars))+205
(pos 0)Showing the first 500 of 949 diff lines for this file. This diff is INCOMPLETE; read the file or clone the repository for the rest.
test/test-format.sglmodified
@@ -7,48 +7,63 @@
7
8
(test-group "tokenize" 9
(test "simple expression tokens"−10
(let ((tokens (tokenize "(+ 1 2)" "test.sgl")))+10
(let* ((result (tokenize "(+ 1 2)" "test.sgl"))+11
(chars (tokenize-result-chars result))+12
(tokens (tokenize-result-tokens result))) 13
;; Should have lparen, symbol, whitespace, number, whitespace, number, rparen, eof 14
(assert-equal 'lparen (token-type (car tokens)))−13
(assert-equal "(" (token-value (car tokens)))))+15
(assert-equal "(" (token-value (car tokens) chars)))) 16
17
(test "string token"−16
(let ((tokens (tokenize "\"hello\"" "test.sgl")))+18
(let* ((result (tokenize "\"hello\"" "test.sgl"))+19
(chars (tokenize-result-chars result))+20
(tokens (tokenize-result-tokens result))) 21
(assert-equal 'string (token-type (car tokens)))−18
(assert-equal "\"hello\"" (token-value (car tokens)))))+22
(assert-equal "\"hello\"" (token-value (car tokens) chars)))) 23
24
(test "hash forms"−21
(let ((tokens (tokenize "#t #f" "test.sgl")))+25
(let* ((result (tokenize "#t #f" "test.sgl"))+26
(tokens (tokenize-result-tokens result))) 27
(assert-equal 'hash-t (token-type (car tokens))))) 28
29
(test "comment token"−25
(let ((tokens (tokenize "; a comment\n" "test.sgl")))+30
(let* ((result (tokenize "; a comment\n" "test.sgl"))+31
(tokens (tokenize-result-tokens result))) 32
(assert-equal 'comment (token-type (car tokens))))) 33
34
(test "line/column tracking"−29
(let ((tokens (tokenize "(define x 1)" "test.sgl")))+35
(let* ((result (tokenize "(define x 1)" "test.sgl"))+36
(tokens (tokenize-result-tokens result))) 37
(assert-equal 1 (token-line (car tokens))) 38
(assert-equal 1 (token-column (car tokens))))) 39
40
(test "char literal with double-quote delimiter"−34
(let ((tokens (tokenize "#\\\"" "test.sgl")))+41
(let* ((result (tokenize "#\\\"" "test.sgl"))+42
(chars (tokenize-result-chars result))+43
(tokens (tokenize-result-tokens result))) 44
(assert-equal 'char (token-type (car tokens)))−36
(assert-equal "#\\\"" (token-value (car tokens)))))+45
(assert-equal "#\\\"" (token-value (car tokens) chars)))) 46
47
(test "char literal with open-paren delimiter"−39
(let ((tokens (tokenize "#\\(" "test.sgl")))+48
(let* ((result (tokenize "#\\(" "test.sgl"))+49
(chars (tokenize-result-chars result))+50
(tokens (tokenize-result-tokens result))) 51
(assert-equal 'char (token-type (car tokens)))−41
(assert-equal "#\\(" (token-value (car tokens)))))+52
(assert-equal "#\\(" (token-value (car tokens) chars)))) 53
54
(test "char literal with close-paren delimiter"−44
(let ((tokens (tokenize "#\\)" "test.sgl")))+55
(let* ((result (tokenize "#\\)" "test.sgl"))+56
(chars (tokenize-result-chars result))+57
(tokens (tokenize-result-tokens result))) 58
(assert-equal 'char (token-type (car tokens)))−46
(assert-equal "#\\)" (token-value (car tokens)))))+59
(assert-equal "#\\)" (token-value (car tokens) chars)))) 60
61
(test "char literal with semicolon delimiter"−49
(let ((tokens (tokenize "#\\;" "test.sgl")))+62
(let* ((result (tokenize "#\\;" "test.sgl"))+63
(chars (tokenize-result-chars result))+64
(tokens (tokenize-result-tokens result))) 65
(assert-equal 'char (token-type (car tokens)))−51
(assert-equal "#\\;" (token-value (car tokens))))))+66
(assert-equal "#\\;" (token-value (car tokens) chars))))) 67
68
;; ============================================================ 69
;; format-string