LIPS-scheme / lips

Scheme based powerful lisp interpreter in JavaScript
https://lips.js.org
Other
396 stars 32 forks source link

Add full string interpolation as syntax extension #321

Open jcubic opened 4 months ago

jcubic commented 4 months ago

This is an implementation that works right now

(set-special! "$" 'interpolate)

(define (interpolate str)
  (typecheck "interpolate" str "string")
  (let* ((re #/(\$\{[^\}]+\})/)
         (parts (--> str (split re) (filter Boolean))))
    `(string-append ,@(map (lambda (part)
                             (if (not (null? (part.match re)))
                                 (let* ((expr (part.replace #/(^\$\{)|(\}$)/g ""))
                                        (port (open-input-string expr))
                                        (value (with-input-from-port port read)))
                                   `(repr ,value))
                                 part))
                           (vector->list parts)))))

(pprint (macroexpand-1 (let ((x 10)) $"x = ${(+ x 2)}")))
;; ==> (let ((x 10))
;; ==>   (string-append "x = " (repr (+ x 2))))

(let ((x 10))
  $"x = ${(+ x 2)}")
;; ==> "x = 12"

But it has limitations you can't put strings as an interpolated value, because it will break the parser, to be fully working as expected you should be able to use something like this:

(print $"x = ${(+ 1 2)}; y = ${(+ 2 3)}; s = ${(string-append "{" "hello" "}")}")

But this will not work because the parser doesn't know that it should process ${...} differently. So interpolation would need to be built into the parser but I don't want that.

The alternative is to wait to have (read) inside syntax extensions. So this will need to wait for https://github.com/jcubic/lips/issues/150

For better regex that will work with strings inside quoted expressions, see How to match everything except single character but not inside double quoted string on StackOverflow.

jcubic commented 3 months ago

After implementing #150 you can now read from the parser stream in syntax extensions:

(set-special! "$" 'raw-string lips.specials.SYMBOL)

(define (raw-string)
  (if (char=? (peek-char) #\")
      (begin
        (read-char)
        (let loop ((result (vector)) (char (peek-char)))
          (read-char)
          (if (char=? char #\")
              (apply string (vector->list result))
              (loop (vector-append result (vector char)) (peek-char)))))))

(print $"foo \ bar")

This is working example that read raw string like in Python. So it will be possible to add full string interpolation.

jcubic commented 3 months ago

Here is a working full string interpolation:

(set-special! "#\"" 'string-interpolation lips.specials.SYMBOL)

(define (string-interpolation)
  (let loop ((current "") (result (vector)) (char (read-char)))
    (cond ((and (char=? char #\$)
                (char=? (peek-char) #\{))
           (read-char)
           (let ((expr (read)))
             (let ((next (peek-char)))
               (if (char=? next #\})
                   (begin
                     (read-char)
                     (loop "" (vector-append result (vector current expr)) (read-char)))
                   (error (string-append "Parse Error: expecting } got " (repr next)))))))
          ((char=? char #\\)
           (loop (string-append part (repr (read-char))) result (read-char)))
          ((char=? char #\")
           `(string-append ,@(map (lambda (expr)
                                    (if (string? expr)
                                        expr
                                        `(repr ,expr)))
                                    (vector->list (vector-append result (vector current))))))
          ((eof-object? char)
           (error "Parse Error: expecting character #eof found"))
          (else
           (loop (string-append part (repr char)) result (read-char))))))

(let ((x 10) (y 20))
  (print #"this is string \" ${(+ x y)} hello ${(repr "hello" #t)}

world"))

I will probably split this into two functions.

NOTE the code doesn't handle indentation like LIPS string.