diff --git a/racket/prologos/parse-reader.rkt b/racket/prologos/parse-reader.rkt index 8b6a94288..f67416fcc 100644 --- a/racket/prologos/parse-reader.rkt +++ b/racket/prologos/parse-reader.rkt @@ -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 diff --git a/racket/prologos/parser.rkt b/racket/prologos/parser.rkt index a9cfc5914..3120c8207 100644 --- a/racket/prologos/parser.rkt +++ b/racket/prologos/parser.rkt @@ -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) diff --git a/racket/prologos/tests/test-negative-literals.rkt b/racket/prologos/tests/test-negative-literals.rkt index 77e86d7c0..6a486ef44 100644 --- a/racket/prologos/tests/test-negative-literals.rkt +++ b/racket/prologos/tests/test-negative-literals.rkt @@ -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") diff --git a/racket/prologos/tests/test-rat-literal-in-list.rkt b/racket/prologos/tests/test-rat-literal-in-list.rkt new file mode 100644 index 000000000..026acc00b --- /dev/null +++ b/racket/prologos/tests/test-rat-literal-in-list.rkt @@ -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")) diff --git a/racket/prologos/tree-parser.rkt b/racket/prologos/tree-parser.rkt index e386fe11f..d555cd6b1 100644 --- a/racket/prologos/tree-parser.rkt +++ b/racket/prologos/tree-parser.rkt @@ -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)] @@ -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)