Racket/Scheme converting "-0" to "-0.0"

275 views Asked by At

I am writing a simple interpreter that should output: +inf or -inf for the following computations:

(/ 0)
(/ 1 0)
(/ -0)

I notice that replaced 0 by 0.0 gives me the behavior I want. But I haven't figured out to convert -0 to -0.0. exact->inexact loses the negative sign. (exact->inexact -0) gives 0.0.

3

There are 3 answers

0
LiberalArtist On

This is because -0 in Racket is the same literal datum that can also be written as 0, +0, 0000000, #e0, #e-0/1, -00000/1, or in various other ways. All of these syntaxes produce the very same value from the reader, which is an exact integer. Unlike floating-point numbers, exact numbers don't have signed zeros. To illustrate:

> (exact-integer? 0)
#t
> (exact-integer? +0)
#t
> (exact-integer? -0)
#t
> (eq? +0 -0)
#t
> (eq? -0 0)
#t

To get the behavior you want, you need to adjust the reader layer so that 0 and -0 don't produce the same value. I don't think there is a built-in parameter for doing what you want (there are for many other reader customizations), but you can do it by creating a custom readtable.

I would need more context to give you further guidance (for example, you say you're writing an "interpreter," which isn't the usual way of making a DSL in Racket), but my first inclination would be that, rather than re-implementing number parsing, you might map - to a "non-terminating-macro" and handle the rest in the expander (or interpreter) layer.

1
Alex Knauth On

To do this, with the literal text -0 you would have to change the Reader so that -0 reads equal to -0.0, an inexact version. To be consistent you might want to read every number as an inexact number, whether it has a decimal point or not.

It's worth noting that you can do this by prefixing a number #i, for example #i-0 reads equal to -0.0. However, it sounds like you might want to change the reader so that every number reads the same as if it had #i prefixed onto it including -0.

One of the easier ways to extend the reader is with a readtable. You can make a function that extends a readtable like this:

;; Readtable -> Readtable
(define (extend-readtable orig-rt)
  ;; Char InputPort Any Nat Nat Nat -> Any
  (define (rt-proc char in src ln col pos)
    ....)
  ...
  (make-readtable orig-rt
    #f 'non-terminating-macro rt-proc
    ...))

To use this to define a #lang language, you need to put the reader implementation in your-language/lang/reader.rkt. Here that's inexact-number/lang/reader.rkt, where the inexact-number directory is installed as a single-collection package (raco pkg install path/to/inexact-number).

inexact-number/lang/reader.rkt

#lang racket

(provide (rename-out [-read read]
                     [-read-syntax read-syntax]
                     [-get-info get-info]))

(require syntax/readerr
         syntax/module-reader)

;; Readtable -> Readtable
(define (extend-readtable orig-rt)
  ;; Char InputPort Any Nat Nat Nat -> Any
  (define (rt-proc char in src ln col pos)
    ....)
  ...
  (make-readtable orig-rt
    #f 'non-terminating-macro rt-proc))

;; [X ... -> Y] -> [X ... -> Y]
(define ((wrap-reader rd) . args)
  (parameterize ([current-readtable (extend-readtable (current-readtable))])
    (apply rd args)))

(define-values [-read -read-syntax -get-info]
  (make-meta-reader 'inexact-number
                    "language path"
                    lang-reader-module-paths
                    wrap-reader
                    wrap-reader
                    identity))

The main work goes into filling in the .... holes in the extend-readtable function. Within the rt-proc, you can have it "peek" into the input port in to see if its a number, and if it is, call the Racket reader on a port that has #i appended to the front of in. You might be able to do this with something like input-port-append:

(input-port-append #f (open-input-string "#i") in)

In the context of extend-readtable, here's some code that does the peeking and re-reading:

;; Readtable -> Readtable
(define (extend-readtable orig-rt)
  ;; Char InputPort Any Nat Nat Nat -> Any
  (define (rt-proc char in src ln col pos)
    (define try-in (peeking-input-port in))
    (define try (read/recursive try-in char orig-rt))
    (cond
      [(number? try)
       ;; read it as if it had #i on the front
       (define prefix (string-append "#i" (string char)))
       (define inexact-in
         (input-port-append #f (open-input-string prefix) in))
       (read-syntax/recursive src inexact-in #f orig-rt)]
      [else
       ;; read normally
       (read-syntax/recursive src in char orig-rt)]))

  (make-readtable orig-rt
    #f 'non-terminating-macro rt-proc))

Once you have this done, you should be able to use it like this:

#lang inexact-number racket

-0
; => -0.0
(/ -0)
; => -inf.0

P.S. I have now made #lang inexact-number available on the Racket package server, as the package inexact-number-lang. By the way, there's also #lang exact-decimal, which has the opposite affect of making numbers like 5.2 into exact rational numbers.

0
Sylwester On

-0 doesn't exist! If you enter something silly like (define x -0) and press the Macro stepper #'> before any transformations you'll see it has read it in as (define x 0). The sign is truncated by the reader since integers doesn't have positive and negative zero like IEEE754 does. Thus unless you create your own language with your own reader that can discriminate on signed zero mapped to a data structure that supports it you cannot solve this.

IEEE754 floating points has the sign as it's own bit and thus every number, including 0, can be both positive and negative.

As for Scheme standard: Before R6RS there was no requirement for a full numeric tower and the report even mentions that a floating point only Scheme might be useful. Thus I believe some implementations of R5RS actually might read -0 as -0.0.