gelisam / klister

an implementation of stuck macros
BSD 3-Clause "New" or "Revised" License
129 stars 11 forks source link

syntactic marker for binding sites #219

Open gelisam opened 1 year ago

gelisam commented 1 year ago

Can you spot the bug in the following code?

(defun sum (xs)
  (case xs
    [nil 0]
    [(:: x xs) (+ x (sum xs))]))
(example
  (the (-> (List Integer) Integer) sum))

The program type-checks but always returns zero, because the pattern for matching on the empty list is (nil), not nil. nil instead binds the list to a new identifier, nil, which shadows the (nil) data constructor.

I think this is very error-prone, not just for case, but in general: in a language in which a ton of custom macros use whatever syntax they want, it is easy to assume one syntax when the macro expects something else. For most of those mistakes, either the macro's parser will tell me that what I wrote is ill-formed, or the type-checker will tell me that what I wrote is ill-typed. But for this particular mistake, any identifier is well-formed and (if it isn't used in the body) well-typed, so the mistake is both common and not easily caught.

Thus, I propose to make most identifiers ill-formed, by using the convention (bind x) and the shorthand $x wherever identifiers are bound. For example:

(defun $sum ($xs)
  (case xs
    [nil 0]
    [(:: $x $xs) (+ x (sum xs))]))
(example
  (the (-> (List Integer) Integer) sum))

I know this is quite a sharp departure from traditional Scheme/Racket syntax. But remember, at the beginning of the project we were willing to experiment with bold syntactic choices, in order to improve over that conventional syntax! We tried using [] vs () to distinguishing between the places in a macro's syntax where an unlimited number of arguments is allowed, vs the places where a fixed number of arguments is allowed, e.g. [let ([x 0] [y 1]) [+ x y]]. We ended up deciding this was a mistake. I think we still have room for more experiments and mistakes :)