feat: add fstr (#1142)

* feat: add fstr

* test: add fstr test

* fix: memory error in test

* fix: fix backslash parser

* feat: add octal escape literals
This commit is contained in:
Veit Heller 2021-01-26 06:18:16 +01:00 committed by GitHub
parent 63291c53af
commit 8c1999d656
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 185 additions and 12 deletions

View File

@ -26,7 +26,10 @@
(defndynamic suffix [s from]
(String.slice s from (String.length s)))
(defndynamic tail [s ]
(defndynamic head [s]
(String.prefix s 1))
(defndynamic tail [s]
(String.suffix s 1))
)
)

View File

@ -41,3 +41,67 @@
(doc fmt "formats a string. It supports all of the string interpolations defined in format of the type that should be interpolated (e.g. %d and %x on integers).")
(defmacro fmt [s :rest args]
(list 'copy (fmt-internal s args)))
(hidden f-parse-expr-string)
(defndynamic f-parse-expr-string [s r]
(if (= (String.length s) 0)
-1
(let [h (String.head s)]
(if (= h "\"")
(inc r)
(let [i (if (= h "\\") 2 1)]
(f-parse-expr-string
(String.suffix s i)
(+ r i)))))))
(hidden f-parse-expr)
(defndynamic f-parse-expr [s idx r]
(cond
(= idx 0) r
(= (String.length s) 0) -1
(let [h (String.head s)
t (String.tail s)]
(if (= h "\"")
(let [l (f-parse-expr-string t 0)]
(if (= l -1)
-1
(f-parse-expr (String.suffix s (inc l)) idx (+ r (inc l)))))
(f-parse-expr
t
(cond
(= h "{") (inc idx)
(= h "}") (dec idx)
idx)
(inc r))))))
(hidden f-internal)
(defndynamic f-internal [s]
(let [idx (String.index-of s \{)
len (String.length s)]
(cond
(= idx -1) [(list 'copy s)]
(= len 1) (macro-error "error in format string: expected expression after last {")
(= \{ (String.char-at s (inc idx)))
(append [(list 'copy (String.prefix s (inc idx)))] (f-internal (String.suffix s (+ 2 idx))))
(let [ss (String.suffix s (inc idx))
endx (f-parse-expr ss 1 0)]
(if (= endx -1)
(macro-error "error in format string: unclosed open bracket")
(append [(list 'copy (String.prefix s idx)) (list 'str (parse (String.prefix ss (dec endx))))]
(f-internal (String.suffix ss endx))))))))
(doc fstr "formats a string. It allows arbitrary expression to be intercalated.
Example:
```
(def x 1)
(def y \"hi\")
(fstr \"this is x: {x}, and this is the first letter of y: {(head y)}\")
``")
(defmacro fstr [s]
(list 'String.concat (list 'ref (f-internal s))))

View File

@ -18,6 +18,8 @@
(list-to-array-internal (cdr xs) (append acc (array (car xs))))))
(defmodule Dynamic
(defndynamic /= [a b] (not (= a b)))
(defndynamic caar [pair] (car (car pair)))
(defndynamic cadr [pair] (car (cdr pair)))
(defndynamic cdar [pair] (cdr (car pair)))
@ -47,6 +49,7 @@
(defndynamic cdddar [pair] (cdr (cdr (cdr (car pair)))))
(defndynamic cddddr [pair] (cdr (cdr (cdr (cdr pair))))))
(meta-set! doc "doc" "Set documentation for a binding.")
(defmacro doc [name :rest strings]
(let [newline "

59
docs/Format_Strings.md Normal file
View File

@ -0,0 +1,59 @@
# Format Strings
Carp as to ways to format strings, `fmt` and `fstr`. In this document, we
explore both functions in detail.
## `fmt`
`fmt` allows for more control than `fstr`, but also requires more knowledge
about the data.
```clojure
(fmt "this is an integer %d and this is a string %s." 1 "hi")
```
Its works similarly to [`printf`](https://en.wikipedia.org/wiki/Printf_format_string)
in C. `fmt` will check that the amount of arguments and format specifiers in
the format string match.
All arguments to `fmt` must implement the `format` interface, which is defined
as:
```clojure
(definterface format (Fn [String a] String)
```
The types are expected to take a format specifier and format according to it.
As such, which format specifiers are supported is dependent on the
implementation of `format` on that type. Standard library types expose regular
format specifiers as in C.
Please note that, because `format` is often implemented on top of `snprintf`
and similar functions, using faulty format specifiers might lead to problems.
Also, all `fmt` format strings must be literals.
## `fstr`
Similarly to `fmt`, `fstr` takes a literal string. It uses a simpler interface
than `fmt`, however, in which the expressions are embedded directly into the
string and formatted using `str`. As such, the return types of all expressions
in a `fstr` must implement the `str` interface.
```clojure
(def x 1)
(def y "hi")
(fstr "this is an integer {x} and this is the first character of a string {(head x)}")
```
Any parseable expression may be embedded in a `fstr`. Expressions are
delimited using `{}`. Any lone `}` will be interpreted as a literal, whereas
literal `{` need to be escaped as `{{`.
```clojure
(fstr "{{}") ; => {}
```
While possible, it is discouraged to use complicated or even multiline
expressions inside `fstr`.

View File

@ -172,7 +172,7 @@ toC toCMode (Binder meta root) = emitterSrc (execState (visit startingIndent roo
(Match _) -> dontVisit
With -> dontVisit
MetaStub -> dontVisit
visitStr' indent str i =
visitStr' indent str i shouldEscape =
-- This will allocate a new string every time the code runs:
-- do let var = freshVar i
-- appendToSrc (addIndent indent ++ "String " ++ var ++ " = strdup(\"" ++ str ++ "\");\n")
@ -181,14 +181,21 @@ toC toCMode (Binder meta root) = emitterSrc (execState (visit startingIndent roo
do
let var = freshVar i
varRef = freshVar i ++ "_ref"
appendToSrc (addIndent indent ++ "static String " ++ var ++ " = \"" ++ escapeString str ++ "\";\n")
appendToSrc (addIndent indent ++ "static String " ++ var ++ " = \"" ++ (if shouldEscape then escapeString str else str) ++ "\";\n")
appendToSrc (addIndent indent ++ "String *" ++ varRef ++ " = &" ++ var ++ ";\n")
pure varRef
visitString indent (XObj (Str str) (Just i) _) = visitStr' indent str i
visitString indent (XObj (Pattern str) (Just i) _) = visitStr' indent str i
visitString indent (XObj (Str str) (Just i) _) = visitStr' indent str i True
visitString indent (XObj (Pattern str) (Just i) _) = visitStr' indent str i False
visitString _ _ = error "Not a string."
escaper '\"' acc = "\\\"" ++ acc
escaper '\\' acc = "\\\\" ++ acc
escaper '\n' acc = "\\n" ++ acc
escaper '\a' acc = "\\a" ++ acc
escaper '\b' acc = "\\b" ++ acc
escaper '\f' acc = "\\f" ++ acc
escaper '\r' acc = "\\r" ++ acc
escaper '\t' acc = "\\t" ++ acc
escaper '\v' acc = "\\v" ++ acc
escaper x acc = x : acc
escapeString = foldr escaper ""
visitSymbol :: Int -> XObj -> State EmitterState String

View File

@ -11,7 +11,7 @@ import Data.Bits (shift)
-- import Text.Parsec.Error (newErrorMessage, Message(..))
-- import Text.Parsec.Pos (newPos)
import Data.Char (ord)
import Data.Char (chr, ord)
import Data.List (foldl')
import Info
import Numeric (readHex)
@ -277,11 +277,37 @@ pat = do
escaped :: Parsec.Parsec String ParseState String
escaped = do
_ <- Parsec.char '\\'
c <- Parsec.oneOf ['\\', '\"']
pure $ case c of
'\\' -> "\\\\"
'\"' -> "\""
_ -> error "escaped"
c <- Parsec.anyChar
case c of
'\\' -> pure "\\"
'\"' -> pure "\""
'\'' -> pure "\'"
'a' -> pure "\a"
'b' -> pure "\b"
'f' -> pure "\f"
'n' -> pure "\n"
'r' -> pure "\r"
't' -> pure "\t"
'v' -> pure "\v"
'x' -> do
hex <- Parsec.many1 (Parsec.oneOf "0123456789abcdefABCDEF")
let [(p, "")] = readHex hex
return [chr p]
'u' -> do
hex <- Parsec.count 4 (Parsec.oneOf "0123456789abcdefABCDEF")
let [(p, "")] = readHex hex
return [chr p]
'U' -> do
hex <- Parsec.count 8 (Parsec.oneOf "0123456789abcdefABCDEF")
let [(p, "")] = readHex hex
return [chr p]
_ ->
if elem c "01234567"
then do
hex <- Parsec.many1 (Parsec.oneOf "01234567")
let [(p, "")] = readHex (c : hex)
return [chr p]
else pure ('\\' : [c])
escapedQuoteChar :: Parsec.Parsec String ParseState Char
escapedQuoteChar = do

View File

@ -33,4 +33,15 @@
(assert-equal test
"10 % 12.0 yay"
&(fmt "%d %% %.1f %s" 10 12.0 "yay")
"fmt macro works"))
"fmt macro works")
(assert-equal test
"1 [2 3] h"
&(let [x 1
y [2 3]
z "hi"]
(fstr "{x} {&y} {(head z)}"))
"fstr works")
(assert-equal test
"{}"
&(fstr "{{}")
"fstr escaping works"))