~technomancy/fennel

fennel: add syntax for infinity and NaN v1 PROPOSED

: 1
 add syntax for infinity and NaN
Phil Hagelberg: 1
 add syntax for infinity and NaN

 7 files changed, 69 insertions(+), 61 deletions(-)
#1304729 .build.yml success
Aug 16, 2024 23:47:47 Phil Hagelberg <phil@hagelb.org>:
Next
We had an interesting discussion today on the Fennel meetup call. Some
people were surprised that we could have `&inf` as the notation for
infinity because the current precedent for ampersand is to use it for
directives like `&as`, `&into`, or `&until`, and this uses a similar
notation but a very different meaning.
The original intent of marking `&` as reserved was just to have a
character that's guaranteed not to ever be a valid identifier in scope,
but the perception or common understanding of it is that it's for
directives.

So we started talking about alternatives which might be clearer. One
that was suggested was `.inf` because the dot suggests a numeric
meaning. Another suggestion was `&:inf` which looks different enough
from `&into`; the colon makes it look more "value-like", similarly to
colon strings.

Would like to hear what you think about that.
-Phil
Aug 17, 2024 23:23:32 Phil Hagelberg <phil@hagelb.org>:
Next
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~technomancy/fennel/patches/54487/mbox | git am -3
Learn more about email & git

[PATCH fennel] add syntax for infinity and NaN Export this patch

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) value. For some reason, in PUC Lua 0/0 gives -nan so in
order to generate positive NaN portably across most Lua
implementations, math.acos(2) is used. If there's a way to do it
without depending on math table, it should be used instead, as math,
theoretically, may be absent in some implementations. Theoretically,
n%0 is always NaN, but it's an error in PUC Lua specifically.
---
 changelog.md            |  1 +
 src/fennel/compiler.fnl |  8 +++++---
 src/fennel/parser.fnl   |  8 ++++++++
 src/fennel/view.fnl     |  8 ++++++--
 test/parser.fnl         | 20 ++++++++++++++------
 5 files changed, 34 insertions(+), 11 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..1e8eaf4 100644
--- a/src/fennel/compiler.fnl
+++ b/src/fennel/compiler.fnl
@@ -535,10 +535,12 @@ (fn compile-sym
;; 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 [val (if (not= n n)
                (if (: (tostring n) :match "^%-") "(0/0)" "(-(0/0))")
                (= (math.floor n) n)
                (let [s1 (string.format "%.f" n)]
                  (if (= s1 "inf") "(1/0)" ; portable inf
                      (= s1 "-inf") "(-1/0)"
                  (if (= s1 (tostring (/ 1 0))) "(1/0)" ; portable inf
                      (= s1 (tostring (/ -1 0))) "(-1/0)"
                      (= s1 (tostring n)) s1 ; no precision loss
                      (or (faccumulate [s nil
                                        i 0 308 ; beyond 308 every number turns to inf
diff --git a/src/fennel/parser.fnl b/src/fennel/parser.fnl
index dc15571..7017923 100644
--- a/src/fennel/parser.fnl
+++ b/src/fennel/parser.fnl
@@ -337,6 +337,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 (math.acos 2) source rawstr)
            (= rawstr "-&nan")
            (dispatch (- (math.acos 2)) 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..747223f 100644
--- a/src/fennel/view.fnl
+++ b/src/fennel/view.fnl
@@ -287,9 +287,13 @@ (fn pp-table
(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)
  (let [val (if (not= n n)
                (if (: (tostring n) :match "^%-") "-&nan" "&nan")
                (= (math.floor n) n)
                (let [s1 (string.format "%.f" n)]
                  (if (= s1 (tostring n)) s1 ; no precision loss
                  (if (= s1 (tostring (/ 1 0))) "&inf"
                      (= s1 (tostring (/ -1 0))) "-&inf"
                      (= s1 (tostring n)) s1 ; no precision loss
                      (or (faccumulate [s nil
                                        i 0 308 ; beyond 308 every number turns to inf
                                        :until s]
diff --git a/test/parser.fnl b/test/parser.fnl
index 2a1810f..21ed167 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
fennel/patches/.build.yml: SUCCESS in 30s

[add syntax for infinity and NaN][0] from [][1]

[0]: https://lists.sr.ht/~technomancy/fennel/patches/54487
[1]: mailto:andreyorst@gmail.com

✓ #1304729 SUCCESS fennel/patches/.build.yml https://builds.sr.ht/~technomancy/job/1304729

Re: [PATCH fennel] add syntax for infinity and NaN Export this patch

andreyorst@gmail.com writes:

> &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) value. For some reason, in PUC Lua 0/0 gives -nan so in
> order to generate positive NaN portably across most Lua
> implementations, math.acos(2) is used. If there's a way to do it
> without depending on math table, it should be used instead, as math,
> theoretically, may be absent in some implementations. Theoretically,
> n%0 is always NaN, but it's an error in PUC Lua specifically.

This looks good! It's very weird that 0/0 is considered negative in Lua,
and boy there are a lot of quirks to work around from one supported
version to another, but I think this does a good job.
I had one thought; we can't have a dependency on fennel.compiler from
fennel.lua, but going the other way is actually OK because
fennel.compiler already loads fennel.utils which already loads fennel.view.
So it's OK to use fennel.view from the compiler; we can deal with the
discrepancies using the options table.
I've also factored out the exponential-notation part for readability.

What do you think of this on top of your changes?
-Phil

---
 src/fennel/compiler.fnl | 40 +++++++++---------------------------
 src/fennel/view.fnl     | 45 +++++++++++++++++++++++------------------
 2 files changed, 35 insertions(+), 50 deletions(-)

diff --git a/src/fennel/compiler.fnl b/src/fennel/compiler.fnl
index 1e8eaf4..67f3ad3 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,38 +532,17 @@ if opts contains the nval option."
                (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 (not= n n)
                (if (: (tostring n) :match "^%-") "(0/0)" "(-(0/0))")
                (= (math.floor n) n)
                (let [s1 (string.format "%.f" n)]
                  (if (= s1 (tostring (/ 1 0))) "(1/0)" ; portable inf
                      (= s1 (tostring (/ -1 0))) "(-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 {:infinity "(1/0)" :negative-infinity "(-1/0)"
                  ;; looks wrong but it's right; 0/0 -> -nan somehow?!
                  :nan "(-(0/0))" :negative-nan "(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/view.fnl b/src/fennel/view.fnl
index 747223f..23e7511 100644
--- a/src/fennel/view.fnl
+++ b/src/fennel/view.fnl
@@ -282,30 +282,35 @@
    (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.
;; 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 (: (tostring n) :match "^%-") "-&nan" "&nan")
                (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 (/ 1 0))) "&inf"
                      (= s1 (tostring (/ -1 0))) "-&inf"
                  (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 (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)))
                      (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]
@@ -418,7 +423,7 @@ as numeric escapes rather than letter-based escapes, which is ugly."
                         (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)
-- 
2.39.2
Aug 16, 2024 23:47:47 Phil Hagelberg <phil@hagelb.org>: