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

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.

-Phil

Aug 17, 2024 23:23:32 Phil Hagelberg <phil@hagelb.org>:

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 -3Learn more about email & git

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.

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

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

builds.sr.ht <builds@sr.ht>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

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.

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