<div dir="ltr"><div>Dear Marc & list,<br></div><div><br></div><div>First, the fun stuff suggested is in the <u>"Result, usage"</u> secton below - check it out first :D</div><div><br></div><div>This is to discuss the pull request <a href="https://github.com/feeley/gambit/pull/180">https://github.com/feeley/gambit/pull/180</a> that I just made.</div><div><br></div><div>I hope this provides a clean and practical solution to a problem that has been discussed here on the list last years, and to which not really clean solutions have been suggested until now.</div><div><br></div><div>Marc, in particular can you please check out my question secton "Implementation stuff I don't remember why - Marc clarify?" right at the bottom here, and let us know so everyone will know why it needs to be exactly like this? Thanks!</div><div><br></div><div><b>Purpose</b></div><div>There are times when as a user you want to enhance your s-expression format with a custom type.</div><div><br></div><div>E.g., sometimes you have a custom value type such as an FFI type, that you want to make serializable.</div><div><br></div><div><b>Problem</b></div><div>The writer is fairly easy to extend, you just overlap Gambit's |##wr| procedure with your own (lambda (write-env obj) ...) procedure, where |obj| is the value that Gambit is serializing right now, and that procedure either does its own serialization by outputting the customs structure using (##wr-str write-env string-serialized-form), or passes on execution to the underlying (original) |##wr|.</div><div><br></div><div>So so far so good.</div><div><br></div><div>The reader is more complex to extend though.</div><div><br></div><div>One syntax that theoretically could have worked would have been took into gambit's #. syntax, to inline actual code to be evaluated by the reader, in the serialized form of the sexp. That is not only unsafe though, but also, Gambit namespace management is a task that is difficult to impossible - Gambit is simply not really made for that, so having your custom forms of data serialized to #.(my-data-form ...) does not scale.</div><div><br></div><div><b>The solution I suggest</b></div><div>Luckily, internally Gambit has a procedure |##readtable-char-sharp-handler-set!|, which installs user-defined hash-sequence deserializer in a readtable!</div><div><br></div><div>E.g. install an #\X-character handler into it, and any read element #Xyour-data will have your-data readable by that custom handler.</div><div><br></div><div><b>Implementation problem 1</b></div><div>The problem then is that Gambit not exports any ways to make your own custom handlers - it just exports its own handlers, such as |##read-sharp-vector| (for "#(..."), |##read-sharp-char| (for "#\..."), |##read-sharp-dot| (for "#...."), |##read-sharp-bang| (for "#!..."), etc. . (All those are implemented in lib/_io.scm , and are installed in every readtable using - you guessed it - |##readtable-char-sharp-handler-set!|, in |##make-standard-readtable|.)</div><div><br></div><div>To implement your own handler, you need access to the lowlevel macros |macro-read-next-char-or-eof|, |macro-readenv-filepos-set!| and |macro-readenv-wrap|.</div><div><br></div><div>Also, perhaps, even if you had access to those macros in userland, because Gambit's IO could change at some point perhaps actually using them in userland would be a hack - and so, it would be better if Gambit exported a generalized mechanism for the user to implement his own readtable handlers, that itself takes care of the lowlevel stuff.</div><div><br></div><div><b>Implementation problem 1 solution</b></div><div>To enable users to implement their own readtable handlers, I suggest the inclusion into Gambit of a procedure |##make-readtable-char-sharp-sexp-reader-transformer|, which takes the arguments (transform #!optional (read ##read-datum-or-label)) and returns a readtable hash-sequence deserializer for use with |##readtable-char-sharp-handler-set!|.</div><div><br></div><div>|transform| is the user-provided function that receives as only argument the data that was read out of the sexp in an ordinary format (discussed below), and performs any conversion to that to the custom format (e.g. C FFI type etc.) - the transform procedure evaluates to the conversion result to be used in the sexp.</div><div><br></div><div>|##make-readtable-char-sharp-sexp-reader-transformer| abstracts away the need of taking care of/takes care of all the Gambit-IO-internal macros, as discussed above.</div><div><br></div><div><b>Implementation problem 2</b></div><div>The next problem is how the content of that custom hash-sequnce type is read -</div><div><br></div><div>The different ##read-sharp-* procedures all implement their own specific deserializer logics, to read the different hash-sequence formats e.g. "#(e1 e2)", "#\newline", "#!value", "#|| comment ||", etc. - rightly so as the formats are completely custom to the respective hash-sequence.</div><div><br></div><div>The ##read-sharp-* procedures frequently, but not always, implement the whole reading logic locally in themselves.</div><div><br></div><div>There are some instances of reading logics defined globally though -</div><div><br></div><div><ul><li>|##read-datum-or-label| reads a single datum (e.g. "abc", "123", "#f" or "(1 2 3)"), that one is used by |##read-sharp-ampersand|, for deserialization of the box form e.g. "#&#t", or for the datum-comment form e.g. "#;(im not read)".<br><br></li><li>|##read-expr-from-port| reads any expression from a port, for |##eval| to be fed with it. That one is used by |##read-sharp-dot|, e.g. for "#.(+ 1 2)".<br><br></li><li>There's a bunch of ##build- forms for reading vectors, lists, integers<br><br></li><li>|##read-datum-or-label-or-none-or-dot| seems to read pretty much anything, and is used for instance as continuation for the whole reading process.</li></ul></div><div><b>Implementation problem 2 solution</b><br></div><div>Of these, it seems to me that |##read-datum-or-label| is a perfect default - in particular, the list type makes perfect sense for storing complex custom types.</div><div><br></div><div><b>Implementation stuff I don't remember why - Marc clarify?</b></div><div>As of the moment of writing,</div><div><ul><li>What's the label-marker check and error handling for?<br><br></li><li>Why is the "unwrap":ping during the call to the |read| procedure needed?</li></ul></div><div><b>Result, usage</b></div><div>The result of this addition is that you can plug in your custom serialization code as follows:</div><div><br></div><blockquote style="margin:0px 0px 0px 40px;border:none;padding:0px"><div><font face="monospace, monospace">(define-type coord x y)</font></div><div><font face="monospace, monospace"><br></font></div><div><font face="monospace, monospace">(##readtable-char-sharp-handler-set! (input-port-readtable (repl-input-port))</font></div><div><font face="monospace, monospace">                                     #\$</font></div><div><font face="monospace, monospace">                                     (##make-readtable-char-sharp-sexp-reader-transformer</font></div><div><font face="monospace, monospace">                                        (lambda (v)  (apply make-coord v))))</font></div><div><font face="monospace, monospace"><br></font></div><div><font face="monospace, monospace">(set! ##wr (let* ((old-wr ##wr)) (lambda (we obj)</font></div><div><font face="monospace, monospace">                                   (if (coord? obj)</font></div><div><font face="monospace, monospace">                                       (begin (##wr-str we "#$") (##wr-str we (object->string (list (coord-x obj) (coord-y obj)))))</font></div><div><font face="monospace, monospace">                                       (old-wr we obj)))))</font></div></blockquote><div><br></div><div><br></div><div>And it's used as follows:</div><div><br></div><div><br></div><blockquote style="margin:0px 0px 0px 40px;border:none;padding:0px"><div><font face="monospace, monospace">> <b>(make-coord 15 16)</b></font></div><div><font face="monospace, monospace">#$(15 16)</font></div><div><font face="monospace, monospace">> <b>(define c #)</b></font></div><div><font face="monospace, monospace">> <b>(coord-x c)</b></font></div><div><font face="monospace, monospace">15</font></div><div><font face="monospace, monospace">> <b>c</b></font></div><div><font face="monospace, monospace">#$(15 16)</font></div><div><font face="monospace, monospace">> <b>(object->string c)</b></font></div><div><font face="monospace, monospace">"#$(15 16)"</font></div><div><font face="monospace, monospace">> <b>(call-with-input-string # read)</b></font></div><div><font face="monospace, monospace">#$(15 16)</font></div><div><font face="monospace, monospace">></font></div></blockquote><div><br></div><div>The only important catch to be aware of, is that actually typing in #$(15 16) in the REPL will evaluate to the object, which will then cause an exception:</div><div><br></div><blockquote style="margin:0px 0px 0px 40px;border:none;padding:0px"><div><font face="monospace, monospace">> <b>#$(15 16)</b></font></div><div><font face="monospace, monospace">*** ERROR IN (console)@19.3 -- Ill-formed expression</font></div></blockquote><div><br></div><div>That's analogous to:</div><div><br></div><blockquote style="margin:0px 0px 0px 40px;border:none;padding:0px"><div><font face="monospace, monospace">> <b>(eval print)</b></font></div><div><font face="monospace, monospace">*** ERROR -- Ill-formed expression</font></div><div><font face="monospace, monospace">#<procedure #16 print></font></div><div><font face="monospace, monospace">1></font></div></blockquote><div><br></div><div><br></div><div>As of the time of this post, the extention consists only of the following addition to lib/_io.scm :</div><div><br></div><blockquote style="margin:0px 0px 0px 40px;border:none;padding:0px"><div><font face="monospace, monospace">(define (##make-readtable-char-sharp-sexp-reader-transformer transform #!optional (read ##read-datum-or-label))</font></div><div><font face="monospace, monospace"><br></font></div><div><font face="monospace, monospace">  (define (read-without-wrap re)</font></div><div><font face="monospace, monospace">    (let* ((old-wrapper (macro-readenv-wrapper re))</font></div><div><font face="monospace, monospace">           (old-unwrapper (macro-readenv-unwrapper re)))</font></div><div><font face="monospace, monospace">      (macro-readenv-wrapper-set! re (lambda (re x) x))</font></div><div><font face="monospace, monospace">      (macro-readenv-unwrapper-set! re (lambda (re x) x))</font></div><div><font face="monospace, monospace">      (let ((result (read re)))</font></div><div><font face="monospace, monospace">        (macro-readenv-wrapper-set! re old-wrapper)</font></div><div><font face="monospace, monospace">        (macro-readenv-unwrapper-set! re old-unwrapper)</font></div><div><font face="monospace, monospace">        result)))</font></div><div><font face="monospace, monospace"><br></font></div><div><font face="monospace, monospace">  (lambda (re next start-pos)</font></div><div><font face="monospace, monospace">    (macro-read-next-char-or-eof re) ;; skip char after #\#</font></div><div><font face="monospace, monospace">    (macro-readenv-filepos-set! re start-pos) ;; set pos to start of datum</font></div><div><font face="monospace, monospace">    (let ((args (read-without-wrap re)))</font></div><div><font face="monospace, monospace">      (if (##label-marker? args)</font></div><div><font face="monospace, monospace">          (begin</font></div><div><font face="monospace, monospace">            (##raise-datum-parsing-exception 'datum-or-eof-expected re)</font></div><div><font face="monospace, monospace">            (##void))</font></div><div><font face="monospace, monospace">          (let ((obj (transform args)))</font></div><div><font face="monospace, monospace">            (macro-readenv-wrap re obj))))))</font></div><div><br></div></blockquote><div><br></div><div>Perhaps this can be simplified further.</div><div><br></div><div>Looking forward to your thoughts and feedback, and I hope to finally enable Gambit with this :)</div><div><br></div><div>In particular if you feel any name would be more relevant than "##make-readtable-char-sharp-sexp-reader-transformer" then let me know.</div><div><br></div><div>Thanks!</div><div><br></div></div>