;; Step 14: Source maps / error reporting ;; Source locations tracked through parse → compile → eval. ;; pos-to-loc converts character offset to {:line :col}. ;; Errors include source context. ;; ── pos-to-loc: offset → line/col ──────────────────────────────── (defsuite "pos-to-loc" (deftest "first char is line 1 col 0" (let ((loc (pos-to-loc "hello" 0))) (assert= (get loc :line) 1) (assert= (get loc :col) 0))) (deftest "mid-line position" (let ((loc (pos-to-loc "hello" 3))) (assert= (get loc :line) 1) (assert= (get loc :col) 3))) (deftest "second line" (let ((loc (pos-to-loc "abc\ndef" 4))) (assert= (get loc :line) 2) (assert= (get loc :col) 0))) (deftest "second line mid" (let ((loc (pos-to-loc "abc\ndef" 6))) (assert= (get loc :line) 2) (assert= (get loc :col) 2))) (deftest "third line" (let ((loc (pos-to-loc "a\nb\nc" 4))) (assert= (get loc :line) 3) (assert= (get loc :col) 0))) (deftest "end of input" (let ((loc (pos-to-loc "abc" 3))) (assert= (get loc :line) 1) (assert= (get loc :col) 3)))) ;; ── Parser combinator error locations ───────────────────────────── (defsuite "parse-error-loc" (deftest "failed parse includes line and col" (let ((result (run-parser digit "abc"))) (assert (not (ok? result))) (let ((loc (error-loc result "abc"))) (assert= (get loc :line) 1) (assert= (get loc :col) 0)))) (deftest "failed parse after consuming" (let ((result (run-parser (seq2 letter digit) "ab"))) (assert (not (ok? result))) (let ((loc (error-loc result "ab"))) (assert= (get loc :line) 1) (assert= (get loc :col) 1)))) (deftest "failed parse on second line" (let ((result (run-parser (seq2 (parse-char "\n") digit) "\na"))) (assert (not (ok? result))) (let ((loc (error-loc result "\na"))) (assert= (get loc :line) 2) (assert= (get loc :col) 0))))) ;; ── format-parse-error: human-readable error ────────────────────── (defsuite "format-parse-error" (deftest "single line error" (let ((result (run-parser digit "hello")) (msg (format-parse-error result "hello"))) (assert (string-contains? msg "line 1")) (assert (string-contains? msg "digit")))) (deftest "multi-line error shows line number" (let ((input "abc\ndef\n!!!") (result (run-parser (seq (list (parse-string "abc\ndef\n") letter)) input)) (msg (format-parse-error result input))) (assert (string-contains? msg "line 3")))) (deftest "error includes source context" (let ((result (run-parser digit "let x = foo")) (msg (format-parse-error result "let x = foo"))) (assert (string-contains? msg "let x = foo"))))) ;; ── source-loc on parsed AST ────────────────────────────────────── (defsuite "ast-source-loc" (deftest "sx-parse-loc returns forms with locations" (let ((forms (sx-parse-loc "(+ 1 2)"))) (assert (not (empty? forms))) (let ((loc (source-loc (first forms)))) (assert (not (nil? loc))) (assert= (get loc :line) 1) (assert= (get loc :col) 0)))) (deftest "multi-line sx-parse-loc" (let ((forms (sx-parse-loc "(define x 1)\n(+ x 2)"))) (assert= (len forms) 2) (let ((loc1 (source-loc (first forms))) (loc2 (source-loc (nth forms 1)))) (assert= (get loc1 :line) 1) (assert= (get loc1 :col) 0) (assert= (get loc2 :line) 2) (assert= (get loc2 :col) 0)))) (deftest "form value is accessible" (let ((forms (sx-parse-loc "(+ 1 2)"))) (let ((form (get (first forms) :form))) (assert (list? form)) (assert= (len form) 3)))))