001  (ns beowulf.read
002    "This provides the reader required for boostrapping. It's not a bad
003    reader - it provides feedback on errors found in the input - but it isn't
004    the real Lisp reader.
005  
006    Intended deviations from the behaviour of the real Lisp reader are as follows:
007  
008    1. It reads the meta-expression language `MEXPR` in addition to the
009        symbolic expression language `SEXPR`, which I do not believe the Lisp 1.5
010        reader ever did;
011    2. It treats everything between a double semi-colon and an end of line as a comment,
012        as most modern Lisps do; but I do not believe Lisp 1.5 had this feature.
013  
014    Both these extensions can be disabled by using the `--strict` command line
015    switch."
016    (:require [beowulf.oblist :refer [*options*]]
017              [beowulf.reader.char-reader :refer [read-chars]]
018              [beowulf.reader.generate :refer [generate]]
019              [beowulf.reader.parser :refer [parse]]
020              [beowulf.reader.simplify :refer [simplify]]
021              [clojure.string :refer [join split starts-with? trim]])
022    (:import [instaparse.gll Failure]
023             [java.io InputStream]))
024  
025  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
026  ;;;
027  ;;; This file provides the reader required for boostrapping. It's not a bad
028  ;;; reader - it provides feedback on errors found in the input - but it isn't
029  ;;; the real Lisp reader.
030  ;;;
031  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
032  ;;;
033  ;;; Copyright (C) 2022-2023 Simon Brooke
034  ;;;
035  ;;; This program is free software; you can redistribute it and/or
036  ;;; modify it under the terms of the GNU General Public License
037  ;;; as published by the Free Software Foundation; either version 2
038  ;;; of the License, or (at your option) any later version.
039  ;;; 
040  ;;; This program is distributed in the hope that it will be useful,
041  ;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
042  ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
043  ;;; GNU General Public License for more details.
044  ;;; 
045  ;;; You should have received a copy of the GNU General Public License
046  ;;; along with this program; if not, write to the Free Software
047  ;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
048  ;;;
049  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
050  
051  (defn strip-line-comments
052    "Strip blank lines and comment lines from this string `s`, expected to
053     be Lisp source."
054    [^String s]
055    (join "\n"
056          (remove
057           #(or (empty? %)
058                (starts-with? (trim %) ";;"))
059           (split s #"\n"))))
060  
061  (defn number-lines
062    ([^String s]
063     (number-lines s nil))
064    ([^String s ^Failure e]
065     (let [l (-> e :line)
066           c (-> e :column)]
067       (join "\n"
068             (map #(str (format "%5d %s" (inc %1) %2)
069                        (when (= l (inc %1))
070                          (str "\n" (apply str (repeat c " ")) "^")))
071                  (range)
072                  (split s #"\n"))))))
073  
074  (defn gsp
075    "Shortcut macro - the internals of read; or, if you like, read-string.
076    Argument `s` should be a string representation of a valid Lisp
077    expression."
078    [s]
079    (let [source (strip-line-comments s)
080          parse-tree (parse source)]
081      (if (instance? Failure parse-tree)
082        (doall (println (number-lines source parse-tree))
083               (throw (ex-info "Ne can forstande " (assoc parse-tree :source source))))
084        (generate (simplify parse-tree)))))
085  
086  (defn- dummy-read-chars [prompt]
087    (loop [r "" p prompt]
088      (if (and (seq r)
089               (= (count (re-seq #"\(" r))
090                  (count (re-seq #"\)" r)))
091               (= (count (re-seq #"\[" r))
092                  (count (re-seq #"\]" r))))
093        r
094        (do
095          (print (str p " "))
096          (flush)
097          (recur (str r "\n" (read-line)) "::")))))
098  
099  (defn read-from-console
100    "Attempt to read a complete lisp expression from the console.
101     
102     There's a major problem here that the read-chars reader messes up testing.
103     We need to be able to disable it while testing!"
104    [prompt]
105    (if (:testing *options*)
106      (dummy-read-chars prompt)
107      (read-chars prompt)))
108  
109  (defn READ
110    "An implementation of a Lisp reader sufficient for bootstrapping; not necessarily
111    the final Lisp reader. `input` should be either a string representation of a LISP
112    expression, or else an input stream. A single form will be read."
113    ([]
114     (gsp (read-from-console (:prompt *options*))))
115    ([input]
116     (cond
117       (empty? input) (READ)
118       (string? input) (gsp input)
119       (instance? InputStream input) (READ (slurp input))
120       :else    (throw (ex-info "READ: `input` should be a string or an input stream" {})))))