Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions racket/prologos/parse-reader.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -1844,6 +1844,18 @@
(make-stx (list (make-stx '$nat-literal source line col pos1 0)
(make-stx value source line col pos1 span))
source line col pos1 span)]
;; Slash-containing number lexeme (e.g. 0/1, 1/2, -3/7) → ($rat-literal n).
;; A user-written `0/1` IS a Rat literal even when string->number simplifies
;; the value to the integer 0; the `/` is a load-bearing source token. The
;; sentinel preserves Rat-ness through the parse pipeline so downstream
;; typing sees `0/1` as Rat, not as Int.
[(number)
(cond
[(string-contains? lexeme "/")
(make-stx (list (make-stx '$rat-literal source line col pos1 0)
(make-stx value source line col pos1 span))
source line col pos1 span)]
[else (make-stx value source line col pos1 span)])]
[(rest-param)
(if (string=? lexeme "...")
;; standalone ... → $rest symbol
Expand Down
12 changes: 12 additions & 0 deletions racket/prologos/parser.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,18 @@
(parse-error loc (format "N suffix requires a non-negative integer, got: ~a" v) #f)))
(parse-error loc "N suffix requires exactly one argument" #f))]

;; $rat-literal sentinel: a number lexeme containing `/` (e.g. 0/1, 1/2,
;; -3/7) is a Rat literal even when string->number simplifies it (e.g.
;; `0/1` → 0). The WS reader emits this sentinel for any slash-containing
;; number token to preserve Rat-ness through the parse pipeline.
[(and (symbol? head) (eq? head '$rat-literal))
(if (= (length args) 1)
(let ([v (stx->datum (car args))])
(if (and (number? v) (exact? v) (rational? v))
(surf-rat-lit v loc)
(parse-error loc (format "rat literal requires an exact rational, got: ~a" v) #f)))
(parse-error loc "rat literal requires exactly one argument" #f))]

;; $approx-literal sentinel: ~N → surf-approx-literal
[(and (symbol? head) (eq? head '$approx-literal))
(if (= (length args) 1)
Expand Down
6 changes: 5 additions & 1 deletion racket/prologos/tests/test-negative-literals.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,12 @@
'((eval -42))))

(test-case "neg-lit/roundtrip: eval -3/7"
;; Slash-containing number lexemes are wrapped in a $rat-literal sentinel
;; by the WS reader so that `0/1`, `1/1`, etc. retain Rat-ness even when
;; string->number would simplify them to integers. (See eigentrust pitfalls
;; doc #3 and tests/test-rat-literal-in-list.rkt.)
(check-equal? (read-all-forms-string "eval -3/7")
'((eval -3/7))))
'((eval ($rat-literal -3/7)))))

(test-case "neg-lit/roundtrip: def x -5"
(check-equal? (read-all-forms-string "def x -5")
Expand Down
147 changes: 147 additions & 0 deletions racket/prologos/tests/test-rat-literal-in-list.rkt
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
#lang racket/base

;;;
;;; Tests for eigentrust pitfalls doc #3:
;;; `0/1` and `1/1` inside nested list literals silently became Int.
;;;
;;; Root cause: the WS reader called `string->number "0/1"` which Racket
;;; simplifies to the integer 0, losing the user's `Rat`-literal intent.
;;; Downstream typing then saw a mix of Int and Rat in `'[0/1 1/2 ...]`
;;; and either picked Int or rejected the outer `[List Rat]` annotation.
;;;
;;; Fix shape (a — preserve at parse time): a number-token lexeme that
;;; contains `/` is wrapped in a `($rat-literal val)` sentinel by the
;;; WS reader, mirroring `($nat-literal val)` for `42N`. Both surface
;;; parsers (parser.rkt for the preparse path, tree-parser.rkt for the
;;; cell pipeline path) then route the sentinel / slash-lexeme to
;;; `surf-rat-lit`, preserving Rat-ness even when the simplified value
;;; is an integer.
;;;
;;; Files touched:
;;; - racket/prologos/parse-reader.rkt (emit $rat-literal sentinel)
;;; - racket/prologos/parser.rkt (handle $rat-literal sentinel)
;;; - racket/prologos/tree-parser.rkt (slash-lexeme → surf-rat-lit;
;;; flatten-ws-datum exemption)
;;;

(require rackunit
racket/string
"test-support.rkt")

;; ========================================
;; A. The eigentrust reproducer
;; ========================================
;; The original failing form, scaled to 2x2 to keep the test small.
;; Before the fix this raised "Type mismatch [List [List Rat]] ...
;; '['[0 1/2] '[1/2 0]]" because `0/1` had been collapsed to Int 0.

(test-case "rat-literal-in-list/eigentrust-reproducer-2x2"
;; Before the fix this raised "Type mismatch [List [List Rat]] ...".
;; After: elaborates and evaluates cleanly, and the inferred type is
;; the annotated `[List [List Rat]]`. (Pretty-print collapses 0/1 → 0
;; in the value display, but the surrounding type is what matters.)
(define out
(run-ns-ws-last
"ns rl1\ndef C : [List [List Rat]] := \x27[\x27[0/1 1/2] \x27[1/2 0/1]]\neval C"))
(check-true (string-contains? out "List [prologos::data::list::List Rat]"))
(check-false (string-contains? out "Type mismatch")))

(test-case "rat-literal-in-list/eigentrust-reproducer-3x3"
;; The exact 3x3 form from the eigentrust pitfalls doc.
(check-equal?
(run-ns-ws-last
"ns rl2\ndef C : [List [List Rat]]\n := \x27[\x27[0/1 1/2 1/2] \x27[1/2 0/1 1/2] \x27[1/2 1/2 0/1]]\ninfer C")
"[prologos::data::list::List [prologos::data::list::List Rat]]"))

;; ========================================
;; B. Top-level Rat literal regressions
;; ========================================
;; Bare `0/1` and `1/1` outside a list must still be Rat (not Int).

(test-case "rat-literal-in-list/bare-zero-over-one-is-rat"
(check-equal? (run-ns-ws-last "ns rl3\ninfer 0/1") "Rat"))

(test-case "rat-literal-in-list/bare-one-over-one-is-rat"
(check-equal? (run-ns-ws-last "ns rl4\ninfer 1/1") "Rat"))

(test-case "rat-literal-in-list/bare-half-is-rat"
(check-equal? (run-ns-ws-last "ns rl5\ninfer 1/2") "Rat"))

(test-case "rat-literal-in-list/bare-negative-rat"
(check-equal? (run-ns-ws-last "ns rl6\ninfer -3/7") "Rat"))

(test-case "rat-literal-in-list/bare-negative-zero-over-one"
(check-equal? (run-ns-ws-last "ns rl7\ninfer -0/1") "Rat"))

;; ========================================
;; C. Bare integer regression: `0` and `42` are still Int
;; ========================================
;; The fix MUST NOT promote bare integers to Rat — only slash-containing
;; lexemes are Rat literals.

(test-case "rat-literal-in-list/bare-zero-is-int"
(check-equal? (run-ns-ws-last "ns ri1\ninfer 0") "Int"))

(test-case "rat-literal-in-list/bare-42-is-int"
(check-equal? (run-ns-ws-last "ns ri2\ninfer 42") "Int"))

(test-case "rat-literal-in-list/bare-negative-int"
(check-equal? (run-ns-ws-last "ns ri3\ninfer -7") "Int"))

;; ========================================
;; D. Single-level list literals
;; ========================================
;; The simplest list-literal case: `'[0/1 1/2]` should be `List Rat`,
;; not `List Int`.

(test-case "rat-literal-in-list/single-list-zero-and-half"
(check-equal?
(run-ns-ws-last "ns rl8\ninfer \x27[0/1 1/2]")
"[prologos::data::list::List Rat]"))

(test-case "rat-literal-in-list/single-list-with-one-over-one"
(check-equal?
(run-ns-ws-last "ns rl9\ninfer \x27[1/1 1/2 0/1]")
"[prologos::data::list::List Rat]"))

;; ========================================
;; E. PVec literals — companion path
;; ========================================
;; Per the eigentrust pitfalls doc #10, `@[0/1 1/2]` already worked for PVec.
;; Confirm it continues to work and that this fix doesn't regress the PVec path.

(test-case "rat-literal-in-list/pvec-zero-and-half"
(check-equal?
(run-ns-ws-last "ns rp1\ninfer @[0/1 1/2]")
"(PVec Rat)"))

(test-case "rat-literal-in-list/pvec-with-one-over-one"
(check-equal?
(run-ns-ws-last "ns rp2\ninfer @[1/1 1/2 0/1]")
"(PVec Rat)"))

;; ========================================
;; F. Annotated def with mixed simplifying / non-simplifying rationals
;; ========================================
;; The annotation `[List Rat]` declares the target type. Before the fix,
;; `0/1` collapsed to Int and the annotation check failed. After the fix,
;; the slash sentinel preserves Rat-ness and the def elaborates cleanly.

(test-case "rat-literal-in-list/annotated-list-with-zero-over-one"
(check-true
(string-contains?
(run-ns-ws-last
"ns rl10\ndef xs : [List Rat] := \x27[0/1 1/2 1/1]\neval xs")
"List Rat")))

;; ========================================
;; G. Sexp-mode (rat ...) constructor still works
;; ========================================
;; Defensive: the explicit `(rat 0/1)` constructor was never affected,
;; but confirm the explicit-constructor path remains intact.

(test-case "rat-literal-in-list/explicit-rat-zero-over-one"
(check-equal? (run-ns-last "(ns rs1)\n(infer (rat 0/1))") "Rat"))

(test-case "rat-literal-in-list/explicit-rat-one-over-one"
(check-equal? (run-ns-last "(ns rs2)\n(infer (rat 1/1))") "Rat"))
8 changes: 6 additions & 2 deletions racket/prologos/tree-parser.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -226,10 +226,14 @@
[(and (> (string-length lex) 1) (char=? (string-ref lex 0) #\:))
(surf-keyword (string->symbol (substring lex 1)) loc)]

;; Number — integer
;; Number — integer or rational.
;; A slash-containing lexeme (e.g. 0/1, 1/2, -3/7) is a Rat literal even
;; when string->number simplifies the value to an integer (e.g. `0/1` → 0).
;; The `/` is a load-bearing source token that survives simplification.
[(string->number lex)
=> (lambda (n)
(cond
[(and (exact-integer? n) (string-contains? lex "/")) (surf-rat-lit n loc)]
[(and (exact-integer? n) (>= n 0)) (surf-int-lit n loc)]
[(exact-integer? n) (surf-int-lit n loc)]
[(rational? n) (surf-rat-lit n loc)]
Expand Down Expand Up @@ -613,7 +617,7 @@
item]
[(and (pair? item)
(not (memq (car item) '($brace-params $angle-type $list-literal
$nat-literal $approx-literal $decimal-literal
$nat-literal $rat-literal $approx-literal $decimal-literal
$set-literal $vec-literal $foreign-block
$typed-hole $solver-config quote $quote)))
(>= (length item) 2)
Expand Down
Loading