~technomancy/fennel

This thread contains a patchset. You're looking at the original emails, but you may wish to use the patch review UI. Review patch
5 3

[PATCH fennel] add syntax for infinity and NaN

Details
Message ID
<20240816000739.462664-1-andreyorst@gmail.com>
DKIM signature
pass
Download raw message
Patch: +34 -11
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] build success

builds.sr.ht <builds@sr.ht>
Details
Message ID
<D3GWD6M0C97C.3GLQHGI172YQ7@fra02>
In-Reply-To
<20240816000739.462664-1-andreyorst@gmail.com> (view parent)
DKIM signature
missing
Download raw message
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]: andreyorst@gmail.com

✓ #1304729 SUCCESS fennel/patches/.build.yml https://builds.sr.ht/~technomancy/job/1304729
Details
Message ID
<87y14ww5xs.fsf@asthra>
In-Reply-To
<20240816000739.462664-1-andreyorst@gmail.com> (view parent)
DKIM signature
pass
Download raw message
Patch: +35 -50
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
Details
Message ID
<a1f9be87-f30e-4467-8b89-aa9bcdbcc2a7@gmail.com>
In-Reply-To
<87y14ww5xs.fsf@asthra> (view parent)
DKIM signature
pass
Download raw message
Aug 16, 2024 23:47:47 Phil Hagelberg <phil@hagelb.org>:

> 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 have an idea on more portable solution without the math table.

> 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.

Good to know!

> I've also factored out the exponential-notation part for readability.
>
> What do you think of this on top of your changes?

Thanks! I'll get vack to it in a few days, and look closer.
Details
Message ID
<87ttfix5j2.fsf@asthra>
In-Reply-To
<a1f9be87-f30e-4467-8b89-aa9bcdbcc2a7@gmail.com> (view parent)
DKIM signature
pass
Download raw message
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
Details
Message ID
<5b92f81e-d46f-4796-8124-921d1e6257a2@gmail.com>
In-Reply-To
<87ttfix5j2.fsf@asthra> (view parent)
DKIM signature
pass
Download raw message
Aug 17, 2024 23:23:32 Phil Hagelberg <phil@hagelb.org>:

> 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

I mentioned that in the ticket too.

> 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.

Huh, I didn't know that &into is a thing, always used :into. &:inf is a 
mix of two notations, even more confusing. W also have @ for reader 
macros, maybe it's ok to use it here? @inf -@inf.

dot notation seems ok as well.
Reply to thread Export thread (mbox)