summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile.am2
-rw-r--r--guix/read-print.scm490
-rw-r--r--guix/scripts/import.scm4
-rw-r--r--guix/scripts/style.scm457
-rw-r--r--tests/read-print.scm209
-rw-r--r--tests/style.scm181
6 files changed, 705 insertions, 638 deletions
diff --git a/Makefile.am b/Makefile.am
index e5363140fb7..2cda20e61c1 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -130,6 +130,7 @@ MODULES = \
130 guix/cve.scm \ 130 guix/cve.scm \
131 guix/workers.scm \ 131 guix/workers.scm \
132 guix/least-authority.scm \ 132 guix/least-authority.scm \
133 guix/read-print.scm \
133 guix/ipfs.scm \ 134 guix/ipfs.scm \
134 guix/platform.scm \ 135 guix/platform.scm \
135 guix/platforms/arm.scm \ 136 guix/platforms/arm.scm \
@@ -524,6 +525,7 @@ SCM_TESTS = \
524 tests/profiles.scm \ 525 tests/profiles.scm \
525 tests/publish.scm \ 526 tests/publish.scm \
526 tests/pypi.scm \ 527 tests/pypi.scm \
528 tests/read-print.scm \
527 tests/records.scm \ 529 tests/records.scm \
528 tests/scripts.scm \ 530 tests/scripts.scm \
529 tests/search-paths.scm \ 531 tests/search-paths.scm \
diff --git a/guix/read-print.scm b/guix/read-print.scm
new file mode 100644
index 00000000000..69ab8ac8b3d
--- /dev/null
+++ b/guix/read-print.scm
@@ -0,0 +1,490 @@
1;;; GNU Guix --- Functional package management for GNU
2;;; Copyright © 2021-2022 Ludovic Courtès <ludo@gnu.org>
3;;;
4;;; This file is part of GNU Guix.
5;;;
6;;; GNU Guix is free software; you can redistribute it and/or modify it
7;;; under the terms of the GNU General Public License as published by
8;;; the Free Software Foundation; either version 3 of the License, or (at
9;;; your option) any later version.
10;;;
11;;; GNU Guix is distributed in the hope that it will be useful, but
12;;; WITHOUT ANY WARRANTY; without even the implied warranty of
13;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14;;; GNU General Public License for more details.
15;;;
16;;; You should have received a copy of the GNU General Public License
17;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
18
19(define-module (guix read-print)
20 #:use-module (ice-9 control)
21 #:use-module (ice-9 match)
22 #:use-module (ice-9 rdelim)
23 #:use-module (ice-9 vlist)
24 #:use-module (srfi srfi-1)
25 #:use-module (srfi srfi-9)
26 #:export (pretty-print-with-comments
27 read-with-comments
28 object->string*
29
30 comment?
31 comment->string
32 comment-margin?
33 canonicalize-comment))
34
35;;; Commentary:
36;;;
37;;; This module provides a comment-preserving reader and a comment-preserving
38;;; pretty-printer smarter than (ice-9 pretty-print).
39;;;
40;;; Code:
41
42
43;;;
44;;; Comment-preserving reader.
45;;;
46
47;; A comment.
48(define-record-type <comment>
49 (comment str margin?)
50 comment?
51 (str comment->string)
52 (margin? comment-margin?))
53
54(define (read-with-comments port)
55 "Like 'read', but include <comment> objects when they're encountered."
56 ;; Note: Instead of implementing this functionality in 'read' proper, which
57 ;; is the best approach long-term, this code is a layer on top of 'read',
58 ;; such that we don't have to rely on a specific Guile version.
59 (define dot (list 'dot))
60 (define (dot? x) (eq? x dot))
61
62 (define (reverse/dot lst)
63 ;; Reverse LST and make it an improper list if it contains DOT.
64 (let loop ((result '())
65 (lst lst))
66 (match lst
67 (() result)
68 (((? dot?) . rest)
69 (let ((dotted (reverse rest)))
70 (set-cdr! (last-pair dotted) (car result))
71 dotted))
72 ((x . rest) (loop (cons x result) rest)))))
73
74 (let loop ((blank-line? #t)
75 (return (const 'unbalanced)))
76 (match (read-char port)
77 ((? eof-object? eof)
78 eof) ;oops!
79 (chr
80 (cond ((eqv? chr #\newline)
81 (loop #t return))
82 ((char-set-contains? char-set:whitespace chr)
83 (loop blank-line? return))
84 ((memv chr '(#\( #\[))
85 (let/ec return
86 (let liip ((lst '()))
87 (liip (cons (loop (match lst
88 (((? comment?) . _) #t)
89 (_ #f))
90 (lambda ()
91 (return (reverse/dot lst))))
92 lst)))))
93 ((memv chr '(#\) #\]))
94 (return))
95 ((eq? chr #\')
96 (list 'quote (loop #f return)))
97 ((eq? chr #\`)
98 (list 'quasiquote (loop #f return)))
99 ((eq? chr #\,)
100 (list (match (peek-char port)
101 (#\@
102 (read-char port)
103 'unquote-splicing)
104 (_
105 'unquote))
106 (loop #f return)))
107 ((eqv? chr #\;)
108 (unread-char chr port)
109 (comment (read-line port 'concat)
110 (not blank-line?)))
111 (else
112 (unread-char chr port)
113 (match (read port)
114 ((and token '#{.}#)
115 (if (eq? chr #\.) dot token))
116 (token token))))))))
117
118;;;
119;;; Comment-preserving pretty-printer.
120;;;
121
122(define-syntax vhashq
123 (syntax-rules (quote)
124 ((_) vlist-null)
125 ((_ (key (quote (lst ...))) rest ...)
126 (vhash-consq key '(lst ...) (vhashq rest ...)))
127 ((_ (key value) rest ...)
128 (vhash-consq key '((() . value)) (vhashq rest ...)))))
129
130(define %special-forms
131 ;; Forms that are indented specially. The number is meant to be understood
132 ;; like Emacs' 'scheme-indent-function' symbol property. When given an
133 ;; alist instead of a number, the alist gives "context" in which the symbol
134 ;; is a special form; for instance, context (modify-phases) means that the
135 ;; symbol must appear within a (modify-phases ...) expression.
136 (vhashq
137 ('begin 1)
138 ('lambda 2)
139 ('lambda* 2)
140 ('match-lambda 1)
141 ('match-lambda* 2)
142 ('define 2)
143 ('define* 2)
144 ('define-public 2)
145 ('define*-public 2)
146 ('define-syntax 2)
147 ('define-syntax-rule 2)
148 ('define-module 2)
149 ('define-gexp-compiler 2)
150 ('let 2)
151 ('let* 2)
152 ('letrec 2)
153 ('letrec* 2)
154 ('match 2)
155 ('when 2)
156 ('unless 2)
157 ('package 1)
158 ('origin 1)
159 ('operating-system 1)
160 ('modify-inputs 2)
161 ('modify-phases 2)
162 ('add-after '(((modify-phases) . 3)))
163 ('add-before '(((modify-phases) . 3)))
164 ('replace '(((modify-phases) . 2))) ;different from 'modify-inputs'
165 ('substitute* 2)
166 ('substitute-keyword-arguments 2)
167 ('call-with-input-file 2)
168 ('call-with-output-file 2)
169 ('with-output-to-file 2)
170 ('with-input-from-file 2)))
171
172(define %newline-forms
173 ;; List heads that must be followed by a newline. The second argument is
174 ;; the context in which they must appear. This is similar to a special form
175 ;; of 1, except that indent is 1 instead of 2 columns.
176 (vhashq
177 ('arguments '(package))
178 ('sha256 '(origin source package))
179 ('base32 '(sha256 origin))
180 ('git-reference '(uri origin source))
181 ('search-paths '(package))
182 ('native-search-paths '(package))
183 ('search-path-specification '())))
184
185(define (prefix? candidate lst)
186 "Return true if CANDIDATE is a prefix of LST."
187 (let loop ((candidate candidate)
188 (lst lst))
189 (match candidate
190 (() #t)
191 ((head1 . rest1)
192 (match lst
193 (() #f)
194 ((head2 . rest2)
195 (and (equal? head1 head2)
196 (loop rest1 rest2))))))))
197
198(define (special-form-lead symbol context)
199 "If SYMBOL is a special form in the given CONTEXT, return its number of
200arguments; otherwise return #f. CONTEXT is a stack of symbols lexically
201surrounding SYMBOL."
202 (match (vhash-assq symbol %special-forms)
203 (#f #f)
204 ((_ . alist)
205 (any (match-lambda
206 ((prefix . level)
207 (and (prefix? prefix context) (- level 1))))
208 alist))))
209
210(define (newline-form? symbol context)
211 "Return true if parenthesized expressions starting with SYMBOL must be
212followed by a newline."
213 (match (vhash-assq symbol %newline-forms)
214 (#f #f)
215 ((_ . prefix)
216 (prefix? prefix context))))
217
218(define (escaped-string str)
219 "Return STR with backslashes and double quotes escaped. Everything else, in
220particular newlines, is left as is."
221 (list->string
222 `(#\"
223 ,@(string-fold-right (lambda (chr lst)
224 (match chr
225 (#\" (cons* #\\ #\" lst))
226 (#\\ (cons* #\\ #\\ lst))
227 (_ (cons chr lst))))
228 '()
229 str)
230 #\")))
231
232(define (string-width str)
233 "Return the \"width\" of STR--i.e., the width of the longest line of STR."
234 (apply max (map string-length (string-split str #\newline))))
235
236(define (canonicalize-comment c)
237 "Canonicalize comment C, ensuring it has the \"right\" number of leading
238semicolons."
239 (let ((line (string-trim-both
240 (string-trim (comment->string c) (char-set #\;)))))
241 (comment (string-append
242 (if (comment-margin? c)
243 ";"
244 (if (string-null? line)
245 ";;" ;no trailing space
246 ";; "))
247 line "\n")
248 (comment-margin? c))))
249
250(define* (pretty-print-with-comments port obj
251 #:key
252 (format-comment identity)
253 (indent 0)
254 (max-width 78)
255 (long-list 5))
256 "Pretty-print OBJ to PORT, attempting to at most MAX-WIDTH character columns
257and assuming the current column is INDENT. Comments present in OBJ are
258included in the output.
259
260Lists longer than LONG-LIST are written as one element per line. Comments are
261passed through FORMAT-COMMENT before being emitted; a useful value for
262FORMAT-COMMENT is 'canonicalize-comment'."
263 (define (list-of-lists? head tail)
264 ;; Return true if HEAD and TAIL denote a list of lists--e.g., a list of
265 ;; 'let' bindings.
266 (match head
267 ((thing _ ...) ;proper list
268 (and (not (memq thing
269 '(quote quasiquote unquote unquote-splicing)))
270 (pair? tail)))
271 (_ #f)))
272
273 (let loop ((indent indent)
274 (column indent)
275 (delimited? #t) ;true if comes after a delimiter
276 (context '()) ;list of "parent" symbols
277 (obj obj))
278 (define (print-sequence context indent column lst delimited?)
279 (define long?
280 (> (length lst) long-list))
281
282 (let print ((lst lst)
283 (first? #t)
284 (delimited? delimited?)
285 (column column))
286 (match lst
287 (()
288 column)
289 ((item . tail)
290 (define newline?
291 ;; Insert a newline if ITEM is itself a list, or if TAIL is long,
292 ;; but only if ITEM is not the first item. Also insert a newline
293 ;; before a keyword.
294 (and (or (pair? item) long?
295 (and (keyword? item)
296 (not (eq? item #:allow-other-keys))))
297 (not first?) (not delimited?)
298 (not (comment? item))))
299
300 (when newline?
301 (newline port)
302 (display (make-string indent #\space) port))
303 (let ((column (if newline? indent column)))
304 (print tail
305 (keyword? item) ;keep #:key value next to one another
306 (comment? item)
307 (loop indent column
308 (or newline? delimited?)
309 context
310 item)))))))
311
312 (define (sequence-would-protrude? indent lst)
313 ;; Return true if elements of LST written at INDENT would protrude
314 ;; beyond MAX-WIDTH. This is implemented as a cheap test with false
315 ;; negatives to avoid actually rendering all of LST.
316 (find (match-lambda
317 ((? string? str)
318 (>= (+ (string-width str) 2 indent) max-width))
319 ((? symbol? symbol)
320 (>= (+ (string-width (symbol->string symbol)) indent)
321 max-width))
322 ((? boolean?)
323 (>= (+ 2 indent) max-width))
324 (()
325 (>= (+ 2 indent) max-width))
326 (_ ;don't know
327 #f))
328 lst))
329
330 (define (special-form? head)
331 (special-form-lead head context))
332
333 (match obj
334 ((? comment? comment)
335 (if (comment-margin? comment)
336 (begin
337 (display " " port)
338 (display (comment->string (format-comment comment))
339 port))
340 (begin
341 ;; When already at the beginning of a line, for example because
342 ;; COMMENT follows a margin comment, no need to emit a newline.
343 (unless (= column indent)
344 (newline port)
345 (display (make-string indent #\space) port))
346 (display (comment->string (format-comment comment))
347 port)))
348 (display (make-string indent #\space) port)
349 indent)
350 (('quote lst)
351 (unless delimited? (display " " port))
352 (display "'" port)
353 (loop indent (+ column (if delimited? 1 2)) #t context lst))
354 (('quasiquote lst)
355 (unless delimited? (display " " port))
356 (display "`" port)
357 (loop indent (+ column (if delimited? 1 2)) #t context lst))
358 (('unquote lst)
359 (unless delimited? (display " " port))
360 (display "," port)
361 (loop indent (+ column (if delimited? 1 2)) #t context lst))
362 (('unquote-splicing lst)
363 (unless delimited? (display " " port))
364 (display ",@" port)
365 (loop indent (+ column (if delimited? 2 3)) #t context lst))
366 (('gexp lst)
367 (unless delimited? (display " " port))
368 (display "#~" port)
369 (loop indent (+ column (if delimited? 2 3)) #t context lst))
370 (('ungexp obj)
371 (unless delimited? (display " " port))
372 (display "#$" port)
373 (loop indent (+ column (if delimited? 2 3)) #t context obj))
374 (('ungexp-native obj)
375 (unless delimited? (display " " port))
376 (display "#+" port)
377 (loop indent (+ column (if delimited? 2 3)) #t context obj))
378 (('ungexp-splicing lst)
379 (unless delimited? (display " " port))
380 (display "#$@" port)
381 (loop indent (+ column (if delimited? 3 4)) #t context lst))
382 (('ungexp-native-splicing lst)
383 (unless delimited? (display " " port))
384 (display "#+@" port)
385 (loop indent (+ column (if delimited? 3 4)) #t context lst))
386 (((? special-form? head) arguments ...)
387 ;; Special-case 'let', 'lambda', 'modify-inputs', etc. so the second
388 ;; and following arguments are less indented.
389 (let* ((lead (special-form-lead head context))
390 (context (cons head context))
391 (head (symbol->string head))
392 (total (length arguments)))
393 (unless delimited? (display " " port))
394 (display "(" port)
395 (display head port)
396 (unless (zero? lead)
397 (display " " port))
398
399 ;; Print the first LEAD arguments.
400 (let* ((indent (+ column 2
401 (if delimited? 0 1)))
402 (column (+ column 1
403 (if (zero? lead) 0 1)
404 (if delimited? 0 1)
405 (string-length head)))
406 (initial-indent column))
407 (define new-column
408 (let inner ((n lead)
409 (arguments (take arguments (min lead total)))
410 (column column))
411 (if (zero? n)
412 (begin
413 (newline port)
414 (display (make-string indent #\space) port)
415 indent)
416 (match arguments
417 (() column)
418 ((head . tail)
419 (inner (- n 1) tail
420 (loop initial-indent column
421 (= n lead)
422 context
423 head)))))))
424
425 ;; Print the remaining arguments.
426 (let ((column (print-sequence
427 context indent new-column
428 (drop arguments (min lead total))
429 #t)))
430 (display ")" port)
431 (+ column 1)))))
432 ((head tail ...)
433 (let* ((overflow? (>= column max-width))
434 (column (if overflow?
435 (+ indent 1)
436 (+ column (if delimited? 1 2))))
437 (newline? (or (newline-form? head context)
438 (list-of-lists? head tail))) ;'let' bindings
439 (context (cons head context)))
440 (if overflow?
441 (begin
442 (newline port)
443 (display (make-string indent #\space) port))
444 (unless delimited? (display " " port)))
445 (display "(" port)
446
447 (let* ((new-column (loop column column #t context head))
448 (indent (if (or (>= new-column max-width)
449 (not (symbol? head))
450 (sequence-would-protrude?
451 (+ new-column 1) tail)
452 newline?)
453 column
454 (+ new-column 1))))
455 (when newline?
456 ;; Insert a newline right after HEAD.
457 (newline port)
458 (display (make-string indent #\space) port))
459
460 (let ((column
461 (print-sequence context indent
462 (if newline? indent new-column)
463 tail newline?)))
464 (display ")" port)
465 (+ column 1)))))
466 (_
467 (let* ((str (if (string? obj)
468 (escaped-string obj)
469 (object->string obj)))
470 (len (string-width str)))
471 (if (and (> (+ column 1 len) max-width)
472 (not delimited?))
473 (begin
474 (newline port)
475 (display (make-string indent #\space) port)
476 (display str port)
477 (+ indent len))
478 (begin
479 (unless delimited? (display " " port))
480 (display str port)
481 (+ column (if delimited? 0 1) len))))))))
482
483(define (object->string* obj indent . args)
484 "Pretty-print OBJ with INDENT columns as the initial indent. ARGS are
485passed as-is to 'pretty-print-with-comments'."
486 (call-with-output-string
487 (lambda (port)
488 (apply pretty-print-with-comments port obj
489 #:indent indent
490 args))))
diff --git a/guix/scripts/import.scm b/guix/scripts/import.scm
index 71ab4b4fedd..bd3cfd2dc3e 100644
--- a/guix/scripts/import.scm
+++ b/guix/scripts/import.scm
@@ -1,5 +1,5 @@
1;;; GNU Guix --- Functional package management for GNU 1;;; GNU Guix --- Functional package management for GNU
2;;; Copyright © 2012, 2013, 2014, 2020, 2021 Ludovic Courtès <ludo@gnu.org> 2;;; Copyright © 2012-2014, 2020-2022 Ludovic Courtès <ludo@gnu.org>
3;;; Copyright © 2014 David Thompson <davet@gnu.org> 3;;; Copyright © 2014 David Thompson <davet@gnu.org>
4;;; Copyright © 2018 Kyle Meyer <kyle@kyleam.com> 4;;; Copyright © 2018 Kyle Meyer <kyle@kyleam.com>
5;;; Copyright © 2019, 2022 Ricardo Wurmus <rekado@elephly.net> 5;;; Copyright © 2019, 2022 Ricardo Wurmus <rekado@elephly.net>
@@ -25,7 +25,7 @@
25(define-module (guix scripts import) 25(define-module (guix scripts import)
26 #:use-module (guix ui) 26 #:use-module (guix ui)
27 #:use-module (guix scripts) 27 #:use-module (guix scripts)
28 #:use-module (guix scripts style) 28 #:use-module (guix read-print)
29 #:use-module (guix utils) 29 #:use-module (guix utils)
30 #:use-module (srfi srfi-1) 30 #:use-module (srfi srfi-1)
31 #:use-module (srfi srfi-11) 31 #:use-module (srfi srfi-11)
diff --git a/guix/scripts/style.scm b/guix/scripts/style.scm
index 9fd652beb1f..e2530e80c03 100644
--- a/guix/scripts/style.scm
+++ b/guix/scripts/style.scm
@@ -37,468 +37,15 @@
37 #:use-module (guix utils) 37 #:use-module (guix utils)
38 #:use-module (guix i18n) 38 #:use-module (guix i18n)
39 #:use-module (guix diagnostics) 39 #:use-module (guix diagnostics)
40 #:use-module (guix read-print)
40 #:use-module (ice-9 control) 41 #:use-module (ice-9 control)
41 #:use-module (ice-9 match) 42 #:use-module (ice-9 match)
42 #:use-module (ice-9 rdelim)
43 #:use-module (ice-9 vlist)
44 #:use-module (srfi srfi-1) 43 #:use-module (srfi srfi-1)
45 #:use-module (srfi srfi-9) 44 #:use-module (srfi srfi-9)
46 #:use-module (srfi srfi-26) 45 #:use-module (srfi srfi-26)
47 #:use-module (srfi srfi-34) 46 #:use-module (srfi srfi-34)
48 #:use-module (srfi srfi-37) 47 #:use-module (srfi srfi-37)
49 #:export (pretty-print-with-comments 48 #:export (guix-style))
50 read-with-comments
51 canonicalize-comment
52
53 guix-style))
54
55
56;;;
57;;; Comment-preserving reader.
58;;;
59
60;; A comment.
61(define-record-type <comment>
62 (comment str margin?)
63 comment?
64 (str comment->string)
65 (margin? comment-margin?))
66
67(define (read-with-comments port)
68 "Like 'read', but include <comment> objects when they're encountered."
69 ;; Note: Instead of implementing this functionality in 'read' proper, which
70 ;; is the best approach long-term, this code is a layer on top of 'read',
71 ;; such that we don't have to rely on a specific Guile version.
72 (define dot (list 'dot))
73 (define (dot? x) (eq? x dot))
74
75 (define (reverse/dot lst)
76 ;; Reverse LST and make it an improper list if it contains DOT.
77 (let loop ((result '())
78 (lst lst))
79 (match lst
80 (() result)
81 (((? dot?) . rest)
82 (let ((dotted (reverse rest)))
83 (set-cdr! (last-pair dotted) (car result))
84 dotted))
85 ((x . rest) (loop (cons x result) rest)))))
86
87 (let loop ((blank-line? #t)
88 (return (const 'unbalanced)))
89 (match (read-char port)
90 ((? eof-object? eof)
91 eof) ;oops!
92 (chr
93 (cond ((eqv? chr #\newline)
94 (loop #t return))
95 ((char-set-contains? char-set:whitespace chr)
96 (loop blank-line? return))
97 ((memv chr '(#\( #\[))
98 (let/ec return
99 (let liip ((lst '()))
100 (liip (cons (loop (match lst
101 (((? comment?) . _) #t)
102 (_ #f))
103 (lambda ()
104 (return (reverse/dot lst))))
105 lst)))))
106 ((memv chr '(#\) #\]))
107 (return))
108 ((eq? chr #\')
109 (list 'quote (loop #f return)))
110 ((eq? chr #\`)
111 (list 'quasiquote (loop #f return)))
112 ((eq? chr #\,)
113 (list (match (peek-char port)
114 (#\@
115 (read-char port)
116 'unquote-splicing)
117 (_
118 'unquote))
119 (loop #f return)))
120 ((eqv? chr #\;)
121 (unread-char chr port)
122 (comment (read-line port 'concat)
123 (not blank-line?)))
124 (else
125 (unread-char chr port)
126 (match (read port)
127 ((and token '#{.}#)
128 (if (eq? chr #\.) dot token))
129 (token token))))))))
130
131;;;
132;;; Comment-preserving pretty-printer.
133;;;
134
135(define-syntax vhashq
136 (syntax-rules (quote)
137 ((_) vlist-null)
138 ((_ (key (quote (lst ...))) rest ...)
139 (vhash-consq key '(lst ...) (vhashq rest ...)))
140 ((_ (key value) rest ...)
141 (vhash-consq key '((() . value)) (vhashq rest ...)))))
142
143(define %special-forms
144 ;; Forms that are indented specially. The number is meant to be understood
145 ;; like Emacs' 'scheme-indent-function' symbol property. When given an
146 ;; alist instead of a number, the alist gives "context" in which the symbol
147 ;; is a special form; for instance, context (modify-phases) means that the
148 ;; symbol must appear within a (modify-phases ...) expression.
149 (vhashq
150 ('begin 1)
151 ('lambda 2)
152 ('lambda* 2)
153 ('match-lambda 1)
154 ('match-lambda* 2)
155 ('define 2)
156 ('define* 2)
157 ('define-public 2)
158 ('define*-public 2)
159 ('define-syntax 2)
160 ('define-syntax-rule 2)
161 ('define-module 2)
162 ('define-gexp-compiler 2)
163 ('let 2)
164 ('let* 2)
165 ('letrec 2)
166 ('letrec* 2)
167 ('match 2)
168 ('when 2)
169 ('unless 2)
170 ('package 1)
171 ('origin 1)
172 ('operating-system 1)
173 ('modify-inputs 2)
174 ('modify-phases 2)
175 ('add-after '(((modify-phases) . 3)))
176 ('add-before '(((modify-phases) . 3)))
177 ('replace '(((modify-phases) . 2))) ;different from 'modify-inputs'
178 ('substitute* 2)
179 ('substitute-keyword-arguments 2)
180 ('call-with-input-file 2)
181 ('call-with-output-file 2)
182 ('with-output-to-file 2)
183 ('with-input-from-file 2)))
184
185(define %newline-forms
186 ;; List heads that must be followed by a newline. The second argument is
187 ;; the context in which they must appear. This is similar to a special form
188 ;; of 1, except that indent is 1 instead of 2 columns.
189 (vhashq
190 ('arguments '(package))
191 ('sha256 '(origin source package))
192 ('base32 '(sha256 origin))
193 ('git-reference '(uri origin source))
194 ('search-paths '(package))
195 ('native-search-paths '(package))
196 ('search-path-specification '())))
197
198(define (prefix? candidate lst)
199 "Return true if CANDIDATE is a prefix of LST."
200 (let loop ((candidate candidate)
201 (lst lst))
202 (match candidate
203 (() #t)
204 ((head1 . rest1)
205 (match lst
206 (() #f)
207 ((head2 . rest2)
208 (and (equal? head1 head2)
209 (loop rest1 rest2))))))))
210
211(define (special-form-lead symbol context)
212 "If SYMBOL is a special form in the given CONTEXT, return its number of
213arguments; otherwise return #f. CONTEXT is a stack of symbols lexically
214surrounding SYMBOL."
215 (match (vhash-assq symbol %special-forms)
216 (#f #f)
217 ((_ . alist)
218 (any (match-lambda
219 ((prefix . level)
220 (and (prefix? prefix context) (- level 1))))
221 alist))))
222
223(define (newline-form? symbol context)
224 "Return true if parenthesized expressions starting with SYMBOL must be
225followed by a newline."
226 (match (vhash-assq symbol %newline-forms)
227 (#f #f)
228 ((_ . prefix)
229 (prefix? prefix context))))
230
231(define (escaped-string str)
232 "Return STR with backslashes and double quotes escaped. Everything else, in
233particular newlines, is left as is."
234 (list->string
235 `(#\"
236 ,@(string-fold-right (lambda (chr lst)
237 (match chr
238 (#\" (cons* #\\ #\" lst))
239 (#\\ (cons* #\\ #\\ lst))
240 (_ (cons chr lst))))
241 '()
242 str)
243 #\")))
244
245(define (string-width str)
246 "Return the \"width\" of STR--i.e., the width of the longest line of STR."
247 (apply max (map string-length (string-split str #\newline))))
248
249(define (canonicalize-comment c)
250 "Canonicalize comment C, ensuring it has the \"right\" number of leading
251semicolons."
252 (let ((line (string-trim-both
253 (string-trim (comment->string c) (char-set #\;)))))
254 (comment (string-append
255 (if (comment-margin? c)
256 ";"
257 (if (string-null? line)
258 ";;" ;no trailing space
259 ";; "))
260 line "\n")
261 (comment-margin? c))))
262
263(define* (pretty-print-with-comments port obj
264 #:key
265 (format-comment identity)
266 (indent 0)
267 (max-width 78)
268 (long-list 5))
269 "Pretty-print OBJ to PORT, attempting to at most MAX-WIDTH character columns
270and assuming the current column is INDENT. Comments present in OBJ are
271included in the output.
272
273Lists longer than LONG-LIST are written as one element per line. Comments are
274passed through FORMAT-COMMENT before being emitted; a useful value for
275FORMAT-COMMENT is 'canonicalize-comment'."
276 (define (list-of-lists? head tail)
277 ;; Return true if HEAD and TAIL denote a list of lists--e.g., a list of
278 ;; 'let' bindings.
279 (match head
280 ((thing _ ...) ;proper list
281 (and (not (memq thing
282 '(quote quasiquote unquote unquote-splicing)))
283 (pair? tail)))
284 (_ #f)))
285
286 (let loop ((indent indent)
287 (column indent)
288 (delimited? #t) ;true if comes after a delimiter
289 (context '()) ;list of "parent" symbols
290 (obj obj))
291 (define (print-sequence context indent column lst delimited?)
292 (define long?
293 (> (length lst) long-list))
294
295 (let print ((lst lst)
296 (first? #t)
297 (delimited? delimited?)
298 (column column))
299 (match lst
300 (()
301 column)
302 ((item . tail)
303 (define newline?
304 ;; Insert a newline if ITEM is itself a list, or if TAIL is long,
305 ;; but only if ITEM is not the first item. Also insert a newline
306 ;; before a keyword.
307 (and (or (pair? item) long?
308 (and (keyword? item)
309 (not (eq? item #:allow-other-keys))))
310 (not first?) (not delimited?)
311 (not (comment? item))))
312
313 (when newline?
314 (newline port)
315 (display (make-string indent #\space) port))
316 (let ((column (if newline? indent column)))
317 (print tail
318 (keyword? item) ;keep #:key value next to one another
319 (comment? item)
320 (loop indent column
321 (or newline? delimited?)
322 context
323 item)))))))
324
325 (define (sequence-would-protrude? indent lst)
326 ;; Return true if elements of LST written at INDENT would protrude
327 ;; beyond MAX-WIDTH. This is implemented as a cheap test with false
328 ;; negatives to avoid actually rendering all of LST.
329 (find (match-lambda
330 ((? string? str)
331 (>= (+ (string-width str) 2 indent) max-width))
332 ((? symbol? symbol)
333 (>= (+ (string-width (symbol->string symbol)) indent)
334 max-width))
335 ((? boolean?)
336 (>= (+ 2 indent) max-width))
337 (()
338 (>= (+ 2 indent) max-width))
339 (_ ;don't know
340 #f))
341 lst))
342
343 (define (special-form? head)
344 (special-form-lead head context))
345
346 (match obj
347 ((? comment? comment)
348 (if (comment-margin? comment)
349 (begin
350 (display " " port)
351 (display (comment->string (format-comment comment))
352 port))
353 (begin
354 ;; When already at the beginning of a line, for example because
355 ;; COMMENT follows a margin comment, no need to emit a newline.
356 (unless (= column indent)
357 (newline port)
358 (display (make-string indent #\space) port))
359 (display (comment->string (format-comment comment))
360 port)))
361 (display (make-string indent #\space) port)
362 indent)
363 (('quote lst)
364 (unless delimited? (display " " port))
365 (display "'" port)
366 (loop indent (+ column (if delimited? 1 2)) #t context lst))
367 (('quasiquote lst)
368 (unless delimited? (display " " port))
369 (display "`" port)
370 (loop indent (+ column (if delimited? 1 2)) #t context lst))
371 (('unquote lst)
372 (unless delimited? (display " " port))
373 (display "," port)
374 (loop indent (+ column (if delimited? 1 2)) #t context lst))
375 (('unquote-splicing lst)
376 (unless delimited? (display " " port))
377 (display ",@" port)
378 (loop indent (+ column (if delimited? 2 3)) #t context lst))
379 (('gexp lst)
380 (unless delimited? (display " " port))
381 (display "#~" port)
382 (loop indent (+ column (if delimited? 2 3)) #t context lst))
383 (('ungexp obj)
384 (unless delimited? (display " " port))
385 (display "#$" port)
386 (loop indent (+ column (if delimited? 2 3)) #t context obj))
387 (('ungexp-native obj)
388 (unless delimited? (display " " port))
389 (display "#+" port)
390 (loop indent (+ column (if delimited? 2 3)) #t context obj))
391 (('ungexp-splicing lst)
392 (unless delimited? (display " " port))
393 (display "#$@" port)
394 (loop indent (+ column (if delimited? 3 4)) #t context lst))
395 (('ungexp-native-splicing lst)
396 (unless delimited? (display " " port))
397 (display "#+@" port)
398 (loop indent (+ column (if delimited? 3 4)) #t context lst))
399 (((? special-form? head) arguments ...)
400 ;; Special-case 'let', 'lambda', 'modify-inputs', etc. so the second
401 ;; and following arguments are less indented.
402 (let* ((lead (special-form-lead head context))
403 (context (cons head context))
404 (head (symbol->string head))
405 (total (length arguments)))
406 (unless delimited? (display " " port))
407 (display "(" port)
408 (display head port)
409 (unless (zero? lead)
410 (display " " port))
411
412 ;; Print the first LEAD arguments.
413 (let* ((indent (+ column 2
414 (if delimited? 0 1)))
415 (column (+ column 1
416 (if (zero? lead) 0 1)
417 (if delimited? 0 1)
418 (string-length head)))
419 (initial-indent column))
420 (define new-column
421 (let inner ((n lead)
422 (arguments (take arguments (min lead total)))
423 (column column))
424 (if (zero? n)
425 (begin
426 (newline port)
427 (display (make-string indent #\space) port)
428 indent)
429 (match arguments
430 (() column)
431 ((head . tail)
432 (inner (- n 1) tail
433 (loop initial-indent column
434 (= n lead)
435 context
436 head)))))))
437
438 ;; Print the remaining arguments.
439 (let ((column (print-sequence
440 context indent new-column
441 (drop arguments (min lead total))
442 #t)))
443 (display ")" port)
444 (+ column 1)))))
445 ((head tail ...)
446 (let* ((overflow? (>= column max-width))
447 (column (if overflow?
448 (+ indent 1)
449 (+ column (if delimited? 1 2))))
450 (newline? (or (newline-form? head context)
451 (list-of-lists? head tail))) ;'let' bindings
452 (context (cons head context)))
453 (if overflow?
454 (begin
455 (newline port)
456 (display (make-string indent #\space) port))
457 (unless delimited? (display " " port)))
458 (display "(" port)
459
460 (let* ((new-column (loop column column #t context head))
461 (indent (if (or (>= new-column max-width)
462 (not (symbol? head))
463 (sequence-would-protrude?
464 (+ new-column 1) tail)
465 newline?)
466 column
467 (+ new-column 1))))
468 (when newline?
469 ;; Insert a newline right after HEAD.
470 (newline port)
471 (display (make-string indent #\space) port))
472
473 (let ((column
474 (print-sequence context indent
475 (if newline? indent new-column)
476 tail newline?)))
477 (display ")" port)
478 (+ column 1)))))
479 (_
480 (let* ((str (if (string? obj)
481 (escaped-string obj)
482 (object->string obj)))
483 (len (string-width str)))
484 (if (and (> (+ column 1 len) max-width)
485 (not delimited?))
486 (begin
487 (newline port)
488 (display (make-string indent #\space) port)
489 (display str port)
490 (+ indent len))
491 (begin
492 (unless delimited? (display " " port))
493 (display str port)
494 (+ column (if delimited? 0 1) len))))))))
495
496(define (object->string* obj indent . args)
497 (call-with-output-string
498 (lambda (port)
499 (apply pretty-print-with-comments port obj
500 #:indent indent
501 args))))
502 49
503 50
504;;; 51;;;
diff --git a/tests/read-print.scm b/tests/read-print.scm
new file mode 100644
index 00000000000..e9ba1127d4f
--- /dev/null
+++ b/tests/read-print.scm
@@ -0,0 +1,209 @@
1;;; GNU Guix --- Functional package management for GNU
2;;; Copyright © 2021-2022 Ludovic Courtès <ludo@gnu.org>
3;;;
4;;; This file is part of GNU Guix.
5;;;
6;;; GNU Guix is free software; you can redistribute it and/or modify it
7;;; under the terms of the GNU General Public License as published by
8;;; the Free Software Foundation; either version 3 of the License, or (at
9;;; your option) any later version.
10;;;
11;;; GNU Guix is distributed in the hope that it will be useful, but
12;;; WITHOUT ANY WARRANTY; without even the implied warranty of
13;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14;;; GNU General Public License for more details.
15;;;
16;;; You should have received a copy of the GNU General Public License
17;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
18
19(define-module (tests-style)
20 #:use-module (guix read-print)
21 #:use-module (guix gexp) ;for the reader extensions
22 #:use-module (srfi srfi-64))
23
24(define-syntax-rule (test-pretty-print str args ...)
25 "Test equality after a round-trip where STR is passed to
26'read-with-comments' and the resulting sexp is then passed to
27'pretty-print-with-comments'."
28 (test-equal str
29 (call-with-output-string
30 (lambda (port)
31 (let ((exp (call-with-input-string str
32 read-with-comments)))
33 (pretty-print-with-comments port exp args ...))))))
34
35
36(test-begin "read-print")
37
38(test-equal "read-with-comments: dot notation"
39 (cons 'a 'b)
40 (call-with-input-string "(a . b)"
41 read-with-comments))
42
43(test-pretty-print "(list 1 2 3 4)")
44(test-pretty-print "((a . 1) (b . 2))")
45(test-pretty-print "(a b c . boom)")
46(test-pretty-print "(list 1
47 2
48 3
49 4)"
50 #:long-list 3
51 #:indent 20)
52(test-pretty-print "\
53(list abc
54 def)"
55 #:max-width 11)
56(test-pretty-print "\
57(#:foo
58 #:bar)"
59 #:max-width 10)
60
61(test-pretty-print "\
62(#:first 1
63 #:second 2
64 #:third 3)")
65
66(test-pretty-print "\
67((x
68 1)
69 (y
70 2)
71 (z
72 3))"
73 #:max-width 3)
74
75(test-pretty-print "\
76(let ((x 1)
77 (y 2)
78 (z 3)
79 (p 4))
80 (+ x y))"
81 #:max-width 11)
82
83(test-pretty-print "\
84(lambda (x y)
85 ;; This is a procedure.
86 (let ((z (+ x y)))
87 (* z z)))")
88
89(test-pretty-print "\
90#~(string-append #$coreutils \"/bin/uname\")")
91
92(test-pretty-print "\
93(package
94 (inherit coreutils)
95 (version \"42\"))")
96
97(test-pretty-print "\
98(modify-phases %standard-phases
99 (add-after 'unpack 'post-unpack
100 (lambda _
101 #t))
102 (add-before 'check 'pre-check
103 (lambda* (#:key inputs #:allow-other-keys)
104 do things ...)))")
105
106(test-pretty-print "\
107(#:phases (modify-phases sdfsdf
108 (add-before 'x 'y
109 (lambda _
110 xyz))))")
111
112(test-pretty-print "\
113(description \"abcdefghijkl
114mnopqrstuvwxyz.\")"
115 #:max-width 30)
116
117(test-pretty-print "\
118(description
119 \"abcdefghijkl
120mnopqrstuvwxyz.\")"
121 #:max-width 12)
122
123(test-pretty-print "\
124(description
125 \"abcdefghijklmnopqrstuvwxyz\")"
126 #:max-width 33)
127
128(test-pretty-print "\
129(modify-phases %standard-phases
130 (replace 'build
131 ;; Nicely indented in 'modify-phases' context.
132 (lambda _
133 #t)))")
134
135(test-pretty-print "\
136(modify-inputs inputs
137 ;; Regular indentation for 'replace' here.
138 (replace \"gmp\" gmp))")
139
140(test-pretty-print "\
141(package
142 ;; Here 'sha256', 'base32', and 'arguments' must be
143 ;; immediately followed by a newline.
144 (source (origin
145 (method url-fetch)
146 (sha256
147 (base32
148 \"not a real base32 string\"))))
149 (arguments
150 '(#:phases %standard-phases
151 #:tests? #f)))")
152
153;; '#:key value' is kept on the same line.
154(test-pretty-print "\
155(package
156 (name \"keyword-value-same-line\")
157 (arguments
158 (list #:phases #~(modify-phases %standard-phases
159 (add-before 'x 'y
160 (lambda* (#:key inputs #:allow-other-keys)
161 (foo bar baz))))
162 #:make-flags #~'(\"ANSWER=42\")
163 #:tests? #f)))")
164
165(test-pretty-print "\
166(let ((x 1)
167 (y 2)
168 (z (let* ((a 3)
169 (b 4))
170 (+ a b))))
171 (list x y z))")
172
173(test-pretty-print "\
174(substitute-keyword-arguments (package-arguments x)
175 ((#:phases phases)
176 `(modify-phases ,phases
177 (add-before 'build 'do-things
178 (lambda _
179 #t))))
180 ((#:configure-flags flags)
181 `(cons \"--without-any-problem\"
182 ,flags)))")
183
184(test-equal "pretty-print-with-comments, canonicalize-comment"
185 "\
186(list abc
187 ;; Not a margin comment.
188 ;; Ditto.
189 ;;
190 ;; There's a blank line above.
191 def ;margin comment
192 ghi)"
193 (let ((sexp (call-with-input-string
194 "\
195(list abc
196 ;Not a margin comment.
197 ;;; Ditto.
198 ;;;;;
199 ; There's a blank line above.
200 def ;; margin comment
201 ghi)"
202 read-with-comments)))
203 (call-with-output-string
204 (lambda (port)
205 (pretty-print-with-comments port sexp
206 #:format-comment
207 canonicalize-comment)))))
208
209(test-end)
diff --git a/tests/style.scm b/tests/style.scm
index 55bad2b3ba1..4ac5ae7c092 100644
--- a/tests/style.scm
+++ b/tests/style.scm
@@ -113,17 +113,6 @@
113 (lambda (port) 113 (lambda (port)
114 (read-lines port line count))))) 114 (read-lines port line count)))))
115 115
116(define-syntax-rule (test-pretty-print str args ...)
117 "Test equality after a round-trip where STR is passed to
118'read-with-comments' and the resulting sexp is then passed to
119'pretty-print-with-comments'."
120 (test-equal str
121 (call-with-output-string
122 (lambda (port)
123 (let ((exp (call-with-input-string str
124 read-with-comments)))
125 (pretty-print-with-comments port exp args ...))))))
126
127 116
128(test-begin "style") 117(test-begin "style")
129 118
@@ -377,176 +366,6 @@
377 (list (package-inputs (@ (my-packages) my-coreutils)) 366 (list (package-inputs (@ (my-packages) my-coreutils))
378 (read-package-field (@ (my-packages) my-coreutils) 'inputs 4))))) 367 (read-package-field (@ (my-packages) my-coreutils) 'inputs 4)))))
379 368
380(test-equal "read-with-comments: dot notation"
381 (cons 'a 'b)
382 (call-with-input-string "(a . b)"
383 read-with-comments))
384
385(test-pretty-print "(list 1 2 3 4)")
386(test-pretty-print "((a . 1) (b . 2))")
387(test-pretty-print "(a b c . boom)")
388(test-pretty-print "(list 1
389 2
390 3
391 4)"
392 #:long-list 3
393 #:indent 20)
394(test-pretty-print "\
395(list abc
396 def)"
397 #:max-width 11)
398(test-pretty-print "\
399(#:foo
400 #:bar)"
401 #:max-width 10)
402
403(test-pretty-print "\
404(#:first 1
405 #:second 2
406 #:third 3)")
407
408(test-pretty-print "\
409((x
410 1)
411 (y
412 2)
413 (z
414 3))"
415 #:max-width 3)
416
417(test-pretty-print "\
418(let ((x 1)
419 (y 2)
420 (z 3)
421 (p 4))
422 (+ x y))"
423 #:max-width 11)
424
425(test-pretty-print "\
426(lambda (x y)
427 ;; This is a procedure.
428 (let ((z (+ x y)))
429 (* z z)))")
430
431(test-pretty-print "\
432#~(string-append #$coreutils \"/bin/uname\")")
433
434(test-pretty-print "\
435(package
436 (inherit coreutils)
437 (version \"42\"))")
438
439(test-pretty-print "\
440(modify-phases %standard-phases
441 (add-after 'unpack 'post-unpack
442 (lambda _
443 #t))
444 (add-before 'check 'pre-check
445 (lambda* (#:key inputs #:allow-other-keys)
446 do things ...)))")
447
448(test-pretty-print "\
449(#:phases (modify-phases sdfsdf
450 (add-before 'x 'y
451 (lambda _
452 xyz))))")
453
454(test-pretty-print "\
455(description \"abcdefghijkl
456mnopqrstuvwxyz.\")"
457 #:max-width 30)
458
459(test-pretty-print "\
460(description
461 \"abcdefghijkl
462mnopqrstuvwxyz.\")"
463 #:max-width 12)
464
465(test-pretty-print "\
466(description
467 \"abcdefghijklmnopqrstuvwxyz\")"
468 #:max-width 33)
469
470(test-pretty-print "\
471(modify-phases %standard-phases
472 (replace 'build
473 ;; Nicely indented in 'modify-phases' context.
474 (lambda _
475 #t)))")
476
477(test-pretty-print "\
478(modify-inputs inputs
479 ;; Regular indentation for 'replace' here.
480 (replace \"gmp\" gmp))")
481
482(test-pretty-print "\
483(package
484 ;; Here 'sha256', 'base32', and 'arguments' must be
485 ;; immediately followed by a newline.
486 (source (origin
487 (method url-fetch)
488 (sha256
489 (base32
490 \"not a real base32 string\"))))
491 (arguments
492 '(#:phases %standard-phases
493 #:tests? #f)))")
494
495;; '#:key value' is kept on the same line.
496(test-pretty-print "\
497(package
498 (name \"keyword-value-same-line\")
499 (arguments
500 (list #:phases #~(modify-phases %standard-phases
501 (add-before 'x 'y
502 (lambda* (#:key inputs #:allow-other-keys)
503 (foo bar baz))))
504 #:make-flags #~'(\"ANSWER=42\")
505 #:tests? #f)))")
506
507(test-pretty-print "\
508(let ((x 1)
509 (y 2)
510 (z (let* ((a 3)
511 (b 4))
512 (+ a b))))
513 (list x y z))")
514
515(test-pretty-print "\
516(substitute-keyword-arguments (package-arguments x)
517 ((#:phases phases)
518 `(modify-phases ,phases
519 (add-before 'build 'do-things
520 (lambda _
521 #t))))
522 ((#:configure-flags flags)
523 `(cons \"--without-any-problem\"
524 ,flags)))")
525
526(test-equal "pretty-print-with-comments, canonicalize-comment"
527 "\
528(list abc
529 ;; Not a margin comment.
530 ;; Ditto.
531 ;;
532 ;; There's a blank line above.
533 def ;margin comment
534 ghi)"
535 (let ((sexp (call-with-input-string
536 "\
537(list abc
538 ;Not a margin comment.
539 ;;; Ditto.
540 ;;;;;
541 ; There's a blank line above.
542 def ;; margin comment
543 ghi)"
544 read-with-comments)))
545 (call-with-output-string
546 (lambda (port)
547 (pretty-print-with-comments port sexp
548 #:format-comment
549 canonicalize-comment)))))
550 369
551(test-end) 370(test-end)
552 371