Extract io-free (sigil format tokenize) sub-library
Move the positioned Sigil tokenizer (token type + accessors, tokenize, tokenize-result-*) out of the monolithic (sigil format) library into a standalone (sigil format tokenize) module that depends only on (sigil core). Downstream consumers (e.g. Slate Tier-B highlighting, compiled to WASM) can now import just the tokenizer without pulling in the formatter or its deps (sigil-json, io/ports/fs).
(sigil format) imports and re-exports the same tokenizer bindings, so existing consumers keep working unchanged with no API break and no duplicate definitions. Drop the unused char-whitespace? helper in the move.
Add test/test-tokenize.sgl exercising the sub-library via a direct import; existing test-format.sgl continues to cover the re-export path.
Bump to 0.16.2 (additive patch).
package.sgl | 2 +-
src/sigil/format.sgl | 375 +++++----------------------------------------------------------------------------------------------------------------------------------------------------
src/sigil/format/tokenize.sgl | 395 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
test/test-tokenize.sgl | 64 +++++++++++++++++++++++++++
4 files changed, 472 insertions(+), 364 deletions(-)package.sglmodified
(package name: "sigil-format" version: "0.16.1" version: "0.16.2" sigil: "^0.17" description: "Sigil code formatter (paren-inference + AST-aware reflow)" url: "https://codeberg.org/sigil/sigil-format"src/sigil/format.sglmodified
(import (sigil struct) (sigil string) (sigil io) (sigil fs)) (sigil fs) (sigil format tokenize)) (export ;; Core API paren-inference-confidence paren-inference-reason ;; Token inspection (for debugging/testing) ;; Positioned tokenizer — re-exported from (sigil format tokenize) ;; so existing (sigil format) consumers keep working unchanged. token? token-type token-value token-column token-indent ;; Tokenizer (for testing) tokenize tokenize-result-chars tokenize-result-tokens) (reason)) ; explanation string ;; ============================================================ ;;; Tokens ;;; Tokens & Tokenizer ;; ============================================================ ;; Token layout: #(type start end line column indent) ;; Using raw vectors avoids keyword-arg overhead in the hot path. ;;; Check if value is a token (define (token? x) (and (vector? x) (= (vector-length x) 6))) ;;; Token type accessor (define (token-type tok) (vector-ref tok 0)) ;;; Token start position accessor (define (token-start tok) (vector-ref tok 1)) ;;; Token end position accessor (define (token-end tok) (vector-ref tok 2)) ;;; Token line accessor (define (token-line tok) (vector-ref tok 3)) ;;; Token column accessor (define (token-column tok) (vector-ref tok 4)) ;;; Token indent accessor (define (token-indent tok) (vector-ref tok 5)) ;;; Extract the text value of a token from the source chars vector (define (token-value tok chars) (let* ((s (vector-ref tok 1)) (e (vector-ref tok 2)) (n (- e s)) (v (make-string n))) (let loop ((i 0)) (if (>= i n) v (begin (string-set! v i (vector-ref chars (+ s i))) (loop (+ i 1))))))) ;;; Get the chars vector from a tokenize result (define (tokenize-result-chars result) (car result)) ;;; Get the token list from a tokenize result (define (tokenize-result-tokens result) (cdr result)) ;; ============================================================ ;;; Tokenizer Implementation ;; ============================================================ ;;; Check if character is whitespace (define (char-whitespace? c) (and c (or (char=? c #\space) (char=? c #\tab) (char=? c #\newline) (char=? c #\return)))) ;;; Check if character is a digit (define (char-digit? c) (and c (char>=? c #\0) (char<=? c #\9))) ;; Delimiter lookup table — indexed by char code, #t if delimiter (define delimiter-table (let ((t (make-vector 128 #f))) (for-each (lambda (c) (vector-set! t (char->integer c) #t)) '(#\( #\) #\[ #\] #\{ #\} #\" #\; #\' #\` #\, #\space #\tab #\newline #\return)) t)) ;;; Check if character is a delimiter (define (delimiter? c) (or (not c) (let ((code (char->integer c))) (and (< code 128) (vector-ref delimiter-table code))))) ;;; Check if string looks like a number (define (looks-like-number? s) (let ((len (string-length s))) (if (= len 0) #f (let ((first (string-ref s 0))) (cond ((char-digit? first) #t) ((and (or (char=? first #\+) (char=? first #\-)) (> len 1) (char-digit? (string-ref s 1))) #t) ((and (char=? first #\.) (> len 1) (char-digit? (string-ref s 1))) #t) (else #f)))))) ;;; Tokenize entire source into a list of tokens ;;; Returns (cons chars tokens) where chars is the source as a vector (define (tokenize source filename) (let* ((chars (string->vector source)) (len (vector-length chars)) (pos 0) (cur-line 1) (col 1) (indent 0)) ;; Advance one non-newline char (hot path) (define (advance-one!) (set! pos (+ pos 1)) (set! col (+ col 1))) ;; Advance past a known newline, recalculate indent (define (advance-newline!) (set! pos (+ pos 1)) (set! cur-line (+ cur-line 1)) (set! col 1) (let loop ((off 0)) (if (>= (+ pos off) len) (set! indent off) (let ((ch (vector-ref chars (+ pos off)))) (cond ((char=? ch #\space) (loop (+ off 1))) ((char=? ch #\tab) (loop (+ off 2))) (else (set! indent off))))))) ;; Full advance with newline handling (for block comments only) (define (advance!) (when (< pos len) (let ((c (vector-ref chars pos))) (if (char=? c #\newline) (advance-newline!) (advance-one!))))) ;; Skip whitespace (not newlines) — inlined, no closure calls (define (skip-whitespace start-pos start-line start-col start-indent) (let loop () (if (< pos len) (let ((c (vector-ref chars pos))) (cond ((or (char=? c #\space) (char=? c #\tab)) (set! pos (+ pos 1)) (set! col (+ col 1)) (loop)) ((char=? c #\return) (set! pos (+ pos 1)) (loop)) (else (vector 'whitespace start-pos pos start-line start-col start-indent)))) (vector 'whitespace start-pos pos start-line start-col start-indent)))) ;; Skip to delimiter — inlined with direct table lookup (define (skip-to-delimiter) (let loop () (if (>= pos len) pos (let ((code (char->integer (vector-ref chars pos)))) (if (and (< code 128) (vector-ref delimiter-table code)) pos (begin (set! pos (+ pos 1)) (set! col (+ col 1)) (loop))))))) ;; Skip until newline — inlined (define (skip-until-newline) (let loop () (if (>= pos len) pos (if (char=? (vector-ref chars pos) #\newline) pos (begin (set! pos (+ pos 1)) (set! col (+ col 1)) (loop)))))) ;; Count leading spaces from current position (define (count-leading-spaces) (let loop ((off 0)) (if (>= (+ pos off) len) off (let ((c (vector-ref chars (+ pos off)))) (cond ((char=? c #\space) (loop (+ off 1))) ((char=? c #\tab) (loop (+ off 2))) (else off)))))) ;; Read a comment token — inlined semicolons and skip (define (read-comment start-pos start-line start-col start-indent) (let sloop ((count 0)) (if (and (< pos len) (char=? (vector-ref chars pos) #\;)) (begin (set! pos (+ pos 1)) (set! col (+ col 1)) (sloop (+ count 1))) (begin (skip-until-newline) (vector (if (>= count 3) 'doc-comment 'comment) start-pos pos start-line start-col start-indent))))) ;; Read a string token — inlined with fast path for regular chars (define (read-string-token start-pos start-line start-col start-indent) (advance-one!) ; consume opening quote (let loop () (if (>= pos len) (vector 'string start-pos pos start-line start-col start-indent) (let ((c (vector-ref chars pos))) (cond ((char=? c #\") (advance-one!) (vector 'string start-pos pos start-line start-col start-indent)) ((char=? c #\\) (advance-one!) ; backslash (when (< pos len) (if (char=? (vector-ref chars pos) #\newline) (advance-newline!) (advance-one!))) (loop)) ((char=? c #\newline) (advance-newline!) (loop)) (else (set! pos (+ pos 1)) (set! col (+ col 1)) (loop))))))) ;; Read a hash token — advance-one! for non-newline chars (define (read-hash-token start-pos start-line start-col start-indent) (advance-one!) ; consume # (if (>= pos len) (vector 'hash-other start-pos pos start-line start-col start-indent) (let ((c (vector-ref chars pos))) (cond ((or (char=? c #\t) (char=? c #\T)) (advance-one!) (vector 'hash-t start-pos pos start-line start-col start-indent)) ((or (char=? c #\f) (char=? c #\F)) (advance-one!) (vector 'hash-f start-pos pos start-line start-col start-indent)) ((char=? c #\\) ;; Character literal (advance-one!) ; consume backslash (if (>= pos len) (vector 'char start-pos pos start-line start-col start-indent) (begin (advance-one!) ; consume first char after backslash (skip-to-delimiter) (vector 'char start-pos pos start-line start-col start-indent)))) ((char=? c #\|) ;; Block comment — uses full advance! since it can span lines (advance-one!) ; consume | (let loop ((depth 1)) (if (= depth 0) (vector 'block-comment start-pos pos start-line start-col start-indent) (if (>= pos len) (vector 'block-comment start-pos pos start-line start-col start-indent) (let ((c (vector-ref chars pos))) (cond ((char=? c #\|) (advance-one!) ; | is not newline (if (and (< pos len) (char=? (vector-ref chars pos) #\#)) (begin (advance-one!) (loop (- depth 1))) (loop depth))) ((char=? c #\#) (advance-one!) ; # is not newline (if (and (< pos len) (char=? (vector-ref chars pos) #\|)) (begin (advance-one!) (loop (+ depth 1))) (loop depth))) (else (advance!) ; could be newline (loop depth)))))))) ((char=? c #\{) (advance-one!) (vector 'hash-lbrace start-pos pos start-line start-col start-indent)) ((char=? c #\[) (advance-one!) (vector 'hash-lbracket start-pos pos start-line start-col start-indent)) (else ;; Hash datum like #:keyword (skip-to-delimiter) (vector 'hash-other start-pos pos start-line start-col start-indent)))))) ;; Initialize indent for first line (set! indent (count-leading-spaces)) ;; Main tokenization loop (let loop ((tokens '())) (if (>= pos len) ;; EOF token (let ((eof-tok (vector 'eof pos pos cur-line col indent))) (cons chars (reverse (cons eof-tok tokens)))) (let ((c (vector-ref chars pos)) (start-pos pos) (start-line cur-line) (start-col col) (start-indent indent)) (cond ;; Whitespace (not newline) ((or (char=? c #\space) (char=? c #\tab) (char=? c #\return)) (loop (cons (skip-whitespace start-pos start-line start-col start-indent) tokens))) ;; Newline — inline advance-newline! ((char=? c #\newline) (advance-newline!) (loop (cons (vector 'newline start-pos pos start-line start-col start-indent) tokens))) ;; Comment ((char=? c #\;) (loop (cons (read-comment start-pos start-line start-col start-indent) tokens))) ;; Parens and brackets — advance-one! (none are newlines) ((char=? c #\() (advance-one!) (loop (cons (vector 'lparen start-pos pos start-line start-col start-indent) tokens))) ((char=? c #\)) (advance-one!) (loop (cons (vector 'rparen start-pos pos start-line start-col start-indent) tokens))) ((char=? c #\[) (advance-one!) (loop (cons (vector 'lbracket start-pos pos start-line start-col start-indent) tokens))) ((char=? c #\]) (advance-one!) (loop (cons (vector 'rbracket start-pos pos start-line start-col start-indent) tokens))) ((char=? c #\{) (advance-one!) (loop (cons (vector 'lbrace start-pos pos start-line start-col start-indent) tokens))) ((char=? c #\}) (advance-one!) (loop (cons (vector 'rbrace start-pos pos start-line start-col start-indent) tokens))) ;; Quote forms ((char=? c #\') (advance-one!) (loop (cons (vector 'quote start-pos pos start-line start-col start-indent) tokens))) ((char=? c #\`) (advance-one!) (loop (cons (vector 'quasiquote start-pos pos start-line start-col start-indent) tokens))) ((char=? c #\,) (advance-one!) (if (and (< pos len) (char=? (vector-ref chars pos) #\@)) (begin (advance-one!) (loop (cons (vector 'unquote-splicing start-pos pos start-line start-col start-indent) tokens))) (loop (cons (vector 'unquote start-pos pos start-line start-col start-indent) tokens)))) ;; String ((char=? c #\") (loop (cons (read-string-token start-pos start-line start-col start-indent) tokens))) ;; Hash forms ((char=? c #\#) (loop (cons (read-hash-token start-pos start-line start-col start-indent) tokens))) ;; Dot ((char=? c #\.) (let ((next (if (< (+ pos 1) len) (vector-ref chars (+ pos 1)) #f))) (if (delimiter? next) (begin (advance-one!) (loop (cons (vector 'dot start-pos pos start-line start-col start-indent) tokens))) (begin (skip-to-delimiter) (let* ((text (token-value (vector 'symbol start-pos pos start-line start-col start-indent) chars)) (tok-type (if (looks-like-number? text) 'number 'symbol))) (loop (cons (vector tok-type start-pos pos start-line start-col start-indent) tokens))))))) ;; Number or +/- ((or (char=? c #\+) (char=? c #\-)) (skip-to-delimiter) (let* ((text (token-value (vector 'symbol start-pos pos start-line start-col start-indent) chars)) (tok-type (if (looks-like-number? text) 'number 'symbol))) (loop (cons (vector tok-type start-pos pos start-line start-col start-indent) tokens)))) ;; Number ((and (char>=? c #\0) (char<=? c #\9)) (skip-to-delimiter) (loop (cons (vector 'number start-pos pos start-line start-col start-indent) tokens))) ;; Symbol (everything else) (else (skip-to-delimiter) (loop (cons (vector 'symbol start-pos pos start-line start-col start-indent) tokens))))))))) ;; ;; The positioned tokenizer (token type + accessors, `tokenize`, and the ;; `tokenize-result-*` accessors) lives in the io-free sub-library ;; (sigil format tokenize). It is imported above and re-exported below so ;; existing (sigil format) consumers keep working unchanged, while WASM ;; consumers (e.g. Slate Tier-B highlighting) can import just the ;; tokenizer without pulling in the formatter or its deps. ;; ============================================================ ;;; Paren Stack for Trackingsrc/sigil/format/tokenize.sgladded
;;; (sigil format tokenize) - Positioned Sigil tokenizer (io-free);;;;;; The positioned tokenizer extracted from (sigil format) so downstream;;; consumers (e.g. Slate's Tier-B syntax highlighting, compiled to WASM);;; can import JUST the tokenizer without pulling in the formatter or its;;; dependencies (sigil-json, io/ports/fs).;;;;;; This library is deliberately io-free: it depends only on (sigil core).;;; `tokenize` takes a source string + filename and returns a;;; `(cons chars tokens)` result; a token is a raw 6-slot vector.;;;;;; Example:;;; (let* ((r (tokenize "(+ 1 2)" "test.sgl"));;; (chars (tokenize-result-chars r));;; (toks (tokenize-result-tokens r)));;; (token-type (car toks)) ; => lparen;;; (token-value (car toks) chars)) ; => "("(define-library (sigil format tokenize) (import (sigil core)) (export ;; Token predicate + accessors token? token-type token-start token-end token-line token-column token-indent token-value ;; Tokenize result accessors tokenize-result-chars tokenize-result-tokens ;; Tokenizer entry point tokenize) (begin ;; ============================================================ ;;; Tokens ;; ============================================================ ;; Token layout: #(type start end line column indent) ;; Using raw vectors avoids keyword-arg overhead in the hot path. ;;; Check if value is a token (define (token? x) (and (vector? x) (= (vector-length x) 6))) ;;; Token type accessor (define (token-type tok) (vector-ref tok 0)) ;;; Token start position accessor (define (token-start tok) (vector-ref tok 1)) ;;; Token end position accessor (define (token-end tok) (vector-ref tok 2)) ;;; Token line accessor (define (token-line tok) (vector-ref tok 3)) ;;; Token column accessor (define (token-column tok) (vector-ref tok 4)) ;;; Token indent accessor (define (token-indent tok) (vector-ref tok 5)) ;;; Extract the text value of a token from the source chars vector (define (token-value tok chars) (let* ((s (vector-ref tok 1)) (e (vector-ref tok 2)) (n (- e s)) (v (make-string n))) (let loop ((i 0)) (if (>= i n) v (begin (string-set! v i (vector-ref chars (+ s i))) (loop (+ i 1))))))) ;;; Get the chars vector from a tokenize result (define (tokenize-result-chars result) (car result)) ;;; Get the token list from a tokenize result (define (tokenize-result-tokens result) (cdr result)) ;; ============================================================ ;;; Tokenizer Implementation ;; ============================================================ ;;; Check if character is a digit (define (char-digit? c) (and c (char>=? c #\0) (char<=? c #\9))) ;; Delimiter lookup table — indexed by char code, #t if delimiter (define delimiter-table (let ((t (make-vector 128 #f))) (for-each (lambda (c) (vector-set! t (char->integer c) #t)) '(#\( #\) #\[ #\] #\{ #\} #\" #\; #\' #\` #\, #\space #\tab #\newline #\return)) t)) ;;; Check if character is a delimiter (define (delimiter? c) (or (not c) (let ((code (char->integer c))) (and (< code 128) (vector-ref delimiter-table code))))) ;;; Check if string looks like a number (define (looks-like-number? s) (let ((len (string-length s))) (if (= len 0) #f (let ((first (string-ref s 0))) (cond ((char-digit? first) #t) ((and (or (char=? first #\+) (char=? first #\-)) (> len 1) (char-digit? (string-ref s 1))) #t) ((and (char=? first #\.) (> len 1) (char-digit? (string-ref s 1))) #t) (else #f)))))) ;;; Tokenize entire source into a list of tokens ;;; Returns (cons chars tokens) where chars is the source as a vector (define (tokenize source filename) (let* ((chars (string->vector source)) (len (vector-length chars)) (pos 0) (cur-line 1) (col 1) (indent 0)) ;; Advance one non-newline char (hot path) (define (advance-one!) (set! pos (+ pos 1)) (set! col (+ col 1))) ;; Advance past a known newline, recalculate indent (define (advance-newline!) (set! pos (+ pos 1)) (set! cur-line (+ cur-line 1)) (set! col 1) (let loop ((off 0)) (if (>= (+ pos off) len) (set! indent off) (let ((ch (vector-ref chars (+ pos off)))) (cond ((char=? ch #\space) (loop (+ off 1))) ((char=? ch #\tab) (loop (+ off 2))) (else (set! indent off))))))) ;; Full advance with newline handling (for block comments only) (define (advance!) (when (< pos len) (let ((c (vector-ref chars pos))) (if (char=? c #\newline) (advance-newline!) (advance-one!))))) ;; Skip whitespace (not newlines) — inlined, no closure calls (define (skip-whitespace start-pos start-line start-col start-indent) (let loop () (if (< pos len) (let ((c (vector-ref chars pos))) (cond ((or (char=? c #\space) (char=? c #\tab)) (set! pos (+ pos 1)) (set! col (+ col 1)) (loop)) ((char=? c #\return) (set! pos (+ pos 1)) (loop)) (else (vector 'whitespace start-pos pos start-line start-col start-indent)))) (vector 'whitespace start-pos pos start-line start-col start-indent)))) ;; Skip to delimiter — inlined with direct table lookup (define (skip-to-delimiter) (let loop () (if (>= pos len) pos (let ((code (char->integer (vector-ref chars pos)))) (if (and (< code 128) (vector-ref delimiter-table code)) pos (begin (set! pos (+ pos 1)) (set! col (+ col 1)) (loop))))))) ;; Skip until newline — inlined (define (skip-until-newline) (let loop () (if (>= pos len) pos (if (char=? (vector-ref chars pos) #\newline) pos (begin (set! pos (+ pos 1)) (set! col (+ col 1)) (loop)))))) ;; Count leading spaces from current position (define (count-leading-spaces) (let loop ((off 0)) (if (>= (+ pos off) len) off (let ((c (vector-ref chars (+ pos off)))) (cond ((char=? c #\space) (loop (+ off 1))) ((char=? c #\tab) (loop (+ off 2))) (else off)))))) ;; Read a comment token — inlined semicolons and skip (define (read-comment start-pos start-line start-col start-indent) (let sloop ((count 0)) (if (and (< pos len) (char=? (vector-ref chars pos) #\;)) (begin (set! pos (+ pos 1)) (set! col (+ col 1)) (sloop (+ count 1))) (begin (skip-until-newline) (vector (if (>= count 3) 'doc-comment 'comment) start-pos pos start-line start-col start-indent))))) ;; Read a string token — inlined with fast path for regular chars (define (read-string-token start-pos start-line start-col start-indent) (advance-one!) ; consume opening quote (let loop () (if (>= pos len) (vector 'string start-pos pos start-line start-col start-indent) (let ((c (vector-ref chars pos))) (cond ((char=? c #\") (advance-one!) (vector 'string start-pos pos start-line start-col start-indent)) ((char=? c #\\) (advance-one!) ; backslash (when (< pos len) (if (char=? (vector-ref chars pos) #\newline) (advance-newline!) (advance-one!))) (loop)) ((char=? c #\newline) (advance-newline!) (loop)) (else (set! pos (+ pos 1)) (set! col (+ col 1)) (loop))))))) ;; Read a hash token — advance-one! for non-newline chars (define (read-hash-token start-pos start-line start-col start-indent) (advance-one!) ; consume # (if (>= pos len) (vector 'hash-other start-pos pos start-line start-col start-indent) (let ((c (vector-ref chars pos))) (cond ((or (char=? c #\t) (char=? c #\T)) (advance-one!) (vector 'hash-t start-pos pos start-line start-col start-indent)) ((or (char=? c #\f) (char=? c #\F)) (advance-one!) (vector 'hash-f start-pos pos start-line start-col start-indent)) ((char=? c #\\) ;; Character literal (advance-one!) ; consume backslash (if (>= pos len) (vector 'char start-pos pos start-line start-col start-indent) (begin (advance-one!) ; consume first char after backslash (skip-to-delimiter) (vector 'char start-pos pos start-line start-col start-indent)))) ((char=? c #\|) ;; Block comment — uses full advance! since it can span lines (advance-one!) ; consume | (let loop ((depth 1)) (if (= depth 0) (vector 'block-comment start-pos pos start-line start-col start-indent) (if (>= pos len) (vector 'block-comment start-pos pos start-line start-col start-indent) (let ((c (vector-ref chars pos))) (cond ((char=? c #\|) (advance-one!) ; | is not newline (if (and (< pos len) (char=? (vector-ref chars pos) #\#)) (begin (advance-one!) (loop (- depth 1))) (loop depth))) ((char=? c #\#) (advance-one!) ; # is not newline (if (and (< pos len) (char=? (vector-ref chars pos) #\|)) (begin (advance-one!) (loop (+ depth 1))) (loop depth))) (else (advance!) ; could be newline (loop depth)))))))) ((char=? c #\{) (advance-one!) (vector 'hash-lbrace start-pos pos start-line start-col start-indent)) ((char=? c #\[) (advance-one!) (vector 'hash-lbracket start-pos pos start-line start-col start-indent)) (else ;; Hash datum like #:keyword (skip-to-delimiter) (vector 'hash-other start-pos pos start-line start-col start-indent)))))) ;; Initialize indent for first line (set! indent (count-leading-spaces)) ;; Main tokenization loop (let loop ((tokens '())) (if (>= pos len) ;; EOF token (let ((eof-tok (vector 'eof pos pos cur-line col indent))) (cons chars (reverse (cons eof-tok tokens)))) (let ((c (vector-ref chars pos)) (start-pos pos) (start-line cur-line) (start-col col) (start-indent indent)) (cond ;; Whitespace (not newline) ((or (char=? c #\space) (char=? c #\tab) (char=? c #\return)) (loop (cons (skip-whitespace start-pos start-line start-col start-indent) tokens))) ;; Newline — inline advance-newline! ((char=? c #\newline) (advance-newline!) (loop (cons (vector 'newline start-pos pos start-line start-col start-indent) tokens))) ;; Comment ((char=? c #\;) (loop (cons (read-comment start-pos start-line start-col start-indent) tokens))) ;; Parens and brackets — advance-one! (none are newlines) ((char=? c #\() (advance-one!) (loop (cons (vector 'lparen start-pos pos start-line start-col start-indent) tokens))) ((char=? c #\)) (advance-one!) (loop (cons (vector 'rparen start-pos pos start-line start-col start-indent) tokens))) ((char=? c #\[) (advance-one!) (loop (cons (vector 'lbracket start-pos pos start-line start-col start-indent) tokens))) ((char=? c #\]) (advance-one!) (loop (cons (vector 'rbracket start-pos pos start-line start-col start-indent) tokens))) ((char=? c #\{) (advance-one!) (loop (cons (vector 'lbrace start-pos pos start-line start-col start-indent) tokens))) ((char=? c #\}) (advance-one!) (loop (cons (vector 'rbrace start-pos pos start-line start-col start-indent) tokens))) ;; Quote forms ((char=? c #\') (advance-one!) (loop (cons (vector 'quote start-pos pos start-line start-col start-indent) tokens))) ((char=? c #\`) (advance-one!) (loop (cons (vector 'quasiquote start-pos pos start-line start-col start-indent) tokens))) ((char=? c #\,) (advance-one!) (if (and (< pos len) (char=? (vector-ref chars pos) #\@)) (begin (advance-one!) (loop (cons (vector 'unquote-splicing start-pos pos start-line start-col start-indent) tokens))) (loop (cons (vector 'unquote start-pos pos start-line start-col start-indent) tokens)))) ;; String ((char=? c #\") (loop (cons (read-string-token start-pos start-line start-col start-indent) tokens))) ;; Hash forms ((char=? c #\#) (loop (cons (read-hash-token start-pos start-line start-col start-indent) tokens))) ;; Dot ((char=? c #\.) (let ((next (if (< (+ pos 1) len) (vector-ref chars (+ pos 1)) #f))) (if (delimiter? next) (begin (advance-one!) (loop (cons (vector 'dot start-pos pos start-line start-col start-indent) tokens))) (begin (skip-to-delimiter) (let* ((text (token-value (vector 'symbol start-pos pos start-line start-col start-indent) chars)) (tok-type (if (looks-like-number? text) 'number 'symbol))) (loop (cons (vector tok-type start-pos pos start-line start-col start-indent) tokens))))))) ;; Number or +/- ((or (char=? c #\+) (char=? c #\-)) (skip-to-delimiter) (let* ((text (token-value (vector 'symbol start-pos pos start-line start-col start-indent) chars)) (tok-type (if (looks-like-number? text) 'number 'symbol))) (loop (cons (vector tok-type start-pos pos start-line start-col start-indent) tokens)))) ;; Number ((and (char>=? c #\0) (char<=? c #\9)) (skip-to-delimiter) (loop (cons (vector 'number start-pos pos start-line start-col start-indent) tokens))) ;; Symbol (everything else) (else (skip-to-delimiter) (loop (cons (vector 'symbol start-pos pos start-line start-col start-indent) tokens)))))))))))test/test-tokenize.sgladded
;;; Tests for the io-free (sigil format tokenize) sub-library.;;;;;; These import the tokenizer DIRECTLY (not via (sigil format)) to prove the;;; sub-library stands alone with the full tokenizer API and no formatter deps.(import (sigil test) (sigil format tokenize))(test-group "tokenize (direct sub-library import)" (test "simple expression: types and values" (let* ((result (tokenize "(+ 1 2)" "test.sgl")) (chars (tokenize-result-chars result)) (tokens (tokenize-result-tokens result))) (assert-true (token? (car tokens))) ;; ( + <ws> 1 <ws> 2 ) eof (assert-equal 'lparen (token-type (list-ref tokens 0))) (assert-equal "(" (token-value (list-ref tokens 0) chars)) (assert-equal 'symbol (token-type (list-ref tokens 1))) (assert-equal "+" (token-value (list-ref tokens 1) chars)) (assert-equal 'number (token-type (list-ref tokens 3))) (assert-equal "1" (token-value (list-ref tokens 3) chars)) (assert-equal 'rparen (token-type (list-ref tokens 6))) (assert-equal 'eof (token-type (list-ref tokens 7))))) (test "line/column/indent positions on a multi-line snippet" (let* ((result (tokenize "(define (foo x)\n (+ x 1))" "test.sgl")) (tokens (tokenize-result-tokens result)) (tok0 (car tokens))) ;; First token: the opening paren at line 1, column 1, indent 0. (assert-equal 'lparen (token-type tok0)) (assert-equal 1 (token-line tok0)) (assert-equal 1 (token-column tok0)) (assert-equal 0 (token-indent tok0)) ;; Find the lparen that opens the second line's (+ ...) form. (let loop ((ts tokens)) (cond ((null? ts) (assert-true #f)) ; must exist ((and (eq? (token-type (car ts)) 'lparen) (= (token-line (car ts)) 2)) (assert-equal 3 (token-column (car ts))) (assert-equal 2 (token-indent (car ts)))) (else (loop (cdr ts))))))) (test "start/end accessors delimit the source slice" (let* ((result (tokenize "abc" "test.sgl")) (chars (tokenize-result-chars result)) (tok (car (tokenize-result-tokens result)))) (assert-equal 'symbol (token-type tok)) (assert-equal 0 (token-start tok)) (assert-equal 3 (token-end tok)) (assert-equal "abc" (token-value tok chars)))) (test "string, comment, and hash tokens" (let* ((sresult (tokenize "\"hi\"" "t.sgl")) (stoks (tokenize-result-tokens sresult)) (cresult (tokenize "; note\n" "t.sgl")) (ctoks (tokenize-result-tokens cresult)) (hresult (tokenize "#t" "t.sgl")) (htoks (tokenize-result-tokens hresult))) (assert-equal 'string (token-type (car stoks))) (assert-equal 'comment (token-type (car ctoks))) (assert-equal 'hash-t (token-type (car htoks))))))(run-tests)