From: Andrey Listopadov <andreyorst@gmail.com>
.inf and -.inf were added to represent positive and negative infinity.
.nan and -.nan were added to represent positive and negative NaN (not
a number) values. For some reason, on x86 in PUC Lua 0/0 gives -nan,
but nan on ARM, so to generate positive NaN portably across most Lua
implementations, the nan is first converted to a string and is checked
to contain a minus sign. LuaJIT and many other implementations do not
differentiate between NaN and negative NaN, so the tests only check
for positive NaN
---
changelog.md | 1 +
src/fennel/compiler.fnl | 42 ++++++++++-------------------
src/fennel/parser.fnl | 16 ++++++++++-
src/fennel/view.fnl | 60 ++++++++++++++++++++++++-----------------
test/parser.fnl | 20 +++++++++-----
5 files changed, 80 insertions(+), 59 deletions(-)
diff --git a/changelog.md b/changelog.md
index 398af18..7df6bf0 100644
--- a/changelog.md
+++ b/changelog.md
@@ -16,6 +16,7 @@ deprecated forms.
* Bring `fennel.traceback` behavior closer to Lua's `traceback` by
not modifying non-string and non-`nil` values.
* Avoid losing precision when compiling large numbers on LuaJIT.
+* Add syntax for representing infinity and NaN values.
## 1.5.0 / 2024-06-23
diff --git a/src/fennel/compiler.fnl b/src/fennel/compiler.fnl
index 0f34b65..b28563c 100644
--- a/src/fennel/compiler.fnl
+++ b/src/fennel/compiler.fnl
@@ -5,6 +5,7 @@
(local utils (require :fennel.utils))
(local parser (require :fennel.parser))
(local friend (require :fennel.friend))
+(local view (require :fennel.view))
(local unpack (or table.unpack _G.unpack))
@@ -531,36 +532,21 @@ (fn compile-sym
(symbol-to-expression ast scope true))]
(handle-compile-opts [e] parent opts ast))))
-;; We do gsub transformation because some locales use , for
-;; decimal separators, which will not be accepted by Lua.
-;; Makes best effort to keep the original notation of the number.
-(fn serialize-number [n]
- (let [val (if (= (math.floor n) n)
- (let [s1 (string.format "%.f" n)]
- (if (= s1 "inf") "(1/0)" ; portable inf
- (= s1 "-inf") "(-1/0)"
- (= s1 (tostring n)) s1 ; no precision loss
- (or (faccumulate [s nil
- i 0 308 ; beyond 308 every number turns to inf
- :until s]
- (let [s (string.format (.. "%." i "e") n)]
- (when (= n (tonumber s))
- (let [exp (s:match "e%+?(%d+)$")]
- ;; Lua keeps numbers in standard notation up to e+14
- (if (and exp (> (tonumber exp) 14))
- s
- s1)))))
- s1)))
- (tostring n))]
- (pick-values 1 (string.gsub val "," "."))))
+(local view-opts
+ (let [nan (tostring (/ 0 0))]
+ {:infinity "(1/0)"
+ :negative-infinity "(-1/0)"
+ ;; byte 45 is -
+ :nan (if (= 45 (nan:byte)) "(- (0/0))" "(0/0)")
+ :negative-nan (if (= 45 (nan:byte)) "(0/0)" "(- (0/0))")}))
(fn compile-scalar [ast _scope parent opts]
- (let [serialize (match (type ast)
- :nil tostring
- :boolean tostring
- :string serialize-string
- :number serialize-number)]
- (handle-compile-opts [(utils.expr (serialize ast) :literal)] parent opts)))
+ (let [compiled (case (type ast)
+ :nil :nil
+ :boolean (tostring ast)
+ :string (serialize-string ast)
+ :number (view ast view-opts))]
+ (handle-compile-opts [(utils.expr compiled :literal)] parent opts)))
(fn compile-table [ast scope parent opts compile1]
(fn escape-key [k]
diff --git a/src/fennel/parser.fnl b/src/fennel/parser.fnl
index dc15571..11a2035 100644
--- a/src/fennel/parser.fnl
+++ b/src/fennel/parser.fnl
@@ -58,6 +58,12 @@ (fn sym-char?
44 :unquote
96 :quote})
+;; NaN parsing is tricky, because in PUC Lua 0/0 is -nan not nan
+(local (nan negative-nan)
+ (if (= 45 (string.byte (tostring (/ 0 0)))) ; -
+ (values (- (/ 0 0)) (/ 0 0))
+ (values (/ 0 0) (- (/ 0 0)))))
+
(fn char-starter? [b] (or (< 1 b 127) (< 192 b 247)))
(fn parser-fn [getbyte filename {: source : unfriendly : comments &as options}]
@@ -327,7 +333,7 @@ (fn parser-fn
(utils.warn "expected whitespace before token" nil filename line))
rawstr)
- (fn parse-sym [b] ; not just syms actually...
+ (fn parse-sym [b] ; not just syms actually...
(let [source {:bytestart byteindex : filename : line :col (- col 1)}
rawstr (table.concat (parse-sym-loop [(string.char b)] (getb)))]
(set-source-fields source)
@@ -337,6 +343,14 @@ (fn parser-fn
(dispatch false source)
(= rawstr "...")
(dispatch (utils.varg source))
+ (= rawstr ".inf")
+ (dispatch (/ 1 0) source rawstr)
+ (= rawstr "-.inf")
+ (dispatch (/ -1 0) source rawstr)
+ (= rawstr ".nan")
+ (dispatch nan source rawstr)
+ (= rawstr "-.nan")
+ (dispatch negative-nan source rawstr)
(rawstr:match "^:.+$")
(dispatch (rawstr:sub 2) source rawstr)
(not (parse-number rawstr source))
diff --git a/src/fennel/view.fnl b/src/fennel/view.fnl
index 952ec8f..d3d767f 100644
--- a/src/fennel/view.fnl
+++ b/src/fennel/view.fnl
@@ -282,26 +282,35 @@ (fn pp-table
(set options.level (- options.level 1))
x))
-;; A modified copy of compiler.serialize-number that doesn't handle
-;; the infinity cases
-(fn number->string [n]
- ;; Transform number to a string without depending on correct `os.locale`
- ;; Makes best effort to keep the original notation of the number.
- (let [val (if (= (math.floor n) n)
+;; sadly luajit tostring is imprecise https://todo.sr.ht/~technomancy/fennel/231
+(fn exponential-notation [n fallback]
+ (faccumulate [s nil
+ i 0 308 ; beyond 308 every number turns to inf
+ :until s]
+ (let [s (string.format (.. "%." i "e") n)]
+ (when (= n (tonumber s))
+ (let [exp (s:match "e%+?(%d+)$")]
+ ;; Lua keeps numbers in standard notation up to e+14
+ (if (and exp (< 14 (tonumber exp)))
+ s
+ fallback))))))
+
+(local inf-str (tostring (/ 1 0)))
+(local neg-inf-str (tostring (/ -1 0)))
+
+(fn number->string [n options]
+ (let [val (if (not= n n)
+ (if (= 45 (string.byte (tostring n))) ; -
+ (or options.negative-nan "-.nan")
+ (or options.nan ".nan"))
+ (= (math.floor n) n)
(let [s1 (string.format "%.f" n)]
- (if (= s1 (tostring n)) s1 ; no precision loss
- (or (faccumulate [s nil
- i 0 308 ; beyond 308 every number turns to inf
- :until s]
- (let [s (string.format (.. "%." i "e") n)]
- (when (= n (tonumber s))
- (let [exp (s:match "e%+?(%d+)$")]
- ;; Lua keeps numbers in standard notation up to e+14
- (if (and exp (> (tonumber exp) 14))
- s
- s1)))))
- s1)))
+ (if (= s1 inf-str) (or options.infinity ".inf")
+ (= s1 neg-inf-str) (or options.negative-infinity "-.inf")
+ (= s1 (tostring n)) s1 ; no precision loss
+ (or (exponential-notation n s1) s1)))
(tostring n))]
+ ;; Transform number to a string without depending on correct `os.locale`
(pick-values 1 (string.gsub val "," "."))))
(fn colon-string? [s]
@@ -414,7 +423,7 @@ (fn make-options
(case (getmetatable x) {: __fennelview} __fennelview)))
(pp-table x options indent)
(= tv :number)
- (number->string x)
+ (number->string x options)
(and (= tv :string) (colon-string? x)
(if (not= colon? nil) colon?
(= :function (type options.prefer-colon?)) (options.prefer-colon? x)
@@ -429,7 +438,8 @@ (fn make-options
(fn _view [x ?options]
"Return a string representation of x.
-Can take an options table with these keys:
+Can take an options table with the following keys:
+
* :one-line? (default: false) keep the output string as a one-liner
* :depth (number, default: 128) limit how many levels to go (default: 128)
* :detect-cycles? (default: true) don't try to traverse a looping table
@@ -439,17 +449,19 @@ (fn _view
multi-line output for tables is forced
* :escape-newlines? (default: false) emit strings with \\n instead of newline
* :prefer-colon? (default: false) emit strings in colon notation when possible
-* :utf8? (default: true) whether to use utf8 module to compute string lengths
+* :utf8? (default: true) whether to use the utf8 module to compute string lengths
* :max-sparse-gap (integer, default 10) maximum gap to fill in with nils in
- sparse sequential tables.
+ sparse sequential tables
* :preprocess (function) if present, called on x (and recursively on each value
in x), and the result is used for pretty printing; takes the same arguments as
`fennel.view`
+* :infinity, :negative-infinity - how to serialize infinity and negative infinity
+* :nan, :negative-nan - how to serialize NaN and negative NaN values
All options can be set to `{:once some-value}` to force their value to be
-`some-value` but only for the current level. After that, such option is reset
+`some-value` but only for the current level. After that, the option is reset
to its default value. Alternatively, `{:once value :after other-value}` can
-be used, with the difference that after first use, the options will be set to
+be used, with the difference that after the first use, the options will be set to
`other-value` instead of the default value.
You can set a `__fennelview` metamethod on a table to override its serialization
diff --git a/test/parser.fnl b/test/parser.fnl
index 2a1810f..5a6af11 100644
--- a/test/parser.fnl
+++ b/test/parser.fnl
@@ -30,18 +30,26 @@ (fn test-basics
(fennel.view (fennel.eval "23456789012000000000000000000000000000000000000000000000000000000000000000000")))
(t.= "1.23456789e-13"
(fennel.view (fennel.eval "1.23456789e-13")))
- (t.= "inf"
+ (t.= ".inf"
(fennel.view (fennel.eval "1e+999999")))
- (t.= "-inf"
+ (t.= "-.inf"
(fennel.view (fennel.eval "-1e+999999")))
(t.= "1e+308"
(fennel.view (fennel.eval (faccumulate [res "" _ 1 308] (.. res "9")))))
- (t.= "inf"
+ (t.= ".inf"
(fennel.view (fennel.eval (faccumulate [res "" _ 1 309] (.. res "9")))))
- (t.= "inf"
+ (t.= ".inf"
(fennel.view (fennel.eval "(/ 1 0)")))
- (t.= "-inf"
- (fennel.view (fennel.eval "(/ -1 0)"))))
+ (t.= "-.inf"
+ (fennel.view (fennel.eval "(/ -1 0)")))
+ (t.= ".inf"
+ (fennel.view (fennel.eval ".inf")))
+ (t.= "-.inf"
+ (fennel.view (fennel.eval "-.inf")))
+ (t.= ".nan"
+ (fennel.view (fennel.eval "(math.acos 2)")))
+ (t.= ".nan"
+ (fennel.view (fennel.eval ".nan"))))
(fn test-comments []
(let [(ok? ast) ((fennel.parser (fennel.string-stream ";; abc")
--
2.45.0
Andrey Listopadov <andreyorst@gmail.com> writes:
> I'm not sure why it's failing. I've tested locally with 5.1, 5.3, 5.4
> and luajit, and it seems to build for me. The error message is weird
> tho:
>
>> FENNEL_PATH=src/?.fnl lua5.1 bootstrap/aot.lua src/fennel.fnl --require-as-include >> fennel.lua
>> Compile error in 'negative-nan' src/fennel/parser.fnl:62: unable to bind number nan
I figured this out and it's very weird.
$ lua5.1 -e "print(tonumber('nan'))"
nan
$ lua5.2 -e "print(tonumber('nan'))"
nil
All other versions turn this into nil as well.
The reason it's failing in CI and not locally is that it only happens
when you build Fennel itself with 5.1. When you run `make` followed
by `make ci` you won't hit it because fennel itself isn't building with
5.1; you can trigger it with `make clean && make test LUA=lua5.1`.
We'll need a special-case in the parser for this but it's pretty
straightforward once you see the problem; I'll handle it.
-Phil