Musings on record constructors and overall architecture

Posted on Sun Sep 18, 2016

Work is going on in private branches of Vicare. Slowly.

Development of the typed language’s strict-type-checking option is on its way: it causes a warning to be raised whenever a typed variable is set to a value that is only compatible rather than matching. For example:

(define {O <fixnum>}
  (cast-signature (<integer>) 123))

will raise a warning when strict-type-checking is enabled, because <integer> is only compatible with <fixnum> and will cause further validation at run–time.

On record constructors

I am unhappy with the state of the record constructors. The r6rs constructors infrastructure works like this:

(define-record-type <duo>
  (fields one two)
  (protocol
    (lambda (make-record)
      make-record)))

(define-record-type <trio>
  (parent <duo>)
  (fields three)
  (protocol
    (lambda (make-duo)
      (lambda (x y z)
        ((make-duo x y) z)))))

(make-<trio> 1 2 3)     ⇒ #[<trio> one=1 two=2 three=3]

we specify a protocol clause that builds and returns a closure object used as record constructor. I like it: I find it very flexible and very Schemey.

It also has a significant impedance mismatch with the way records are built using the typed language’s extensions: it has no way to provide a type signature for the constructor and overloaded constructors are possible but only with run–time dispatching. With the code as it is now, we can do this:

(define-record-type <duo>
  (fields one two)
  (protocol
    (lambda (make-record)
      (define/overload ({make-duo <duo>} {x <real>} {y <real>})
        (make-record x y))
      (define/overload ({make-duo <duo>} {z <complex>})
        (make-record (real-part z) (imag-part z)))
      make-duo))
  (constructor-signature
    (case-lambda
      ((<real> <real>)        => (<duo>))
      ((<complex>)            => (<duo>))))

(new <duo> 1 2)         ⇒ #[<duo> one=1 two=2]
(new <duo> 1+2i)        ⇒ #[<duo> one=1 two=2]

but it is not optimal: in this code there is everything needed to select the specialised constructor at expand–time, but the selection happens at run–time.

A possible solution: introduce “named protocols”; have new call a constructor other than the one defined by protocol. Something like this:

(define-record-type <duo>
  (fields one two)
  (protocol
    (lambda (make-record)
      make-record))
  (named-protocol make-duo-from-complex
    (lambda (make-record)
      (lambda (z)
        (make-record (real-part z) (imag-part z)))))
  (constructor ({x <real>} {y <real>})
    (make-<duo> x y))
  (constructor ({z <complex>})
    (make-duo-from-complex z)))

it works like this:

The constructor clauses expand into the overloaded function:

(define/overload ({new-<duo> <duo>} {x <real>} {y <real>})
  (make-<duo> x y))

(define/overload ({new-<duo> <duo>} {z <complex>})
  (make-duo-from-complex z))

Obviously we can define the constructor clauses to use the default constructor:

(define-record-type <duo>
  (fields one two)
  (protocol
    (lambda (make-record)
      make-record))
  (constructor ({x <real>} {y <real>})
    (make-<duo> x y))
  (constructor ({z <complex>})
    (make-<duo> (real-part z) (imag-part z))))

With this solution the clause constructor-signature would be removed.

Overall architecture

At some point, in one of the possible futures, I would like to split the expander and compiler from the boot image; this means (rnrs eval (6)), (vicare expander), (vicare compiler) become external libraries. Not an easy thing to do.

Right now there is a single executable program installed with Vicare, but with this change there would be more of them:

vicare-repl

A precompiled Scheme program that runs the repl. It would load an external library (vicare cafe), which is now an internal set of features.

vicare-compiler

A Scheme program that implements the compiler.

vicare

The run–time we have now. It would become a program that runs precompiled Scheme programs.

Things should change internally.

For example the record–type describing compiled libraries should be restructured. To be able to run compiled programs that load compiled libraries without loading the expander: a library’s invoke code must be loaded without loading the visit code (which needs the expander library). This could mean that the invoke code is split from .fasl files into its own file; it would make it possible to install a package holding only precompiled programs and libraries, without including the development stuff. Cool.

The boot process would be greatly revolutionised. In addition to shipping precompiled boot images, the package would include precompiled compiler and expander libraries under the boot directory. A lot of processing that is now performed in the build script makefile.sps should be restructured.

I will think of this. I want to start by putting the repl into an external program; it will also be a test for compiled programs that is now absent from the package.