mark multi-form macro expansions with $

A bare list was read as several forms, so a macro returning
`((make-adder 10) 5)' -- a call of what make-adder returns -- was
spliced into two. Add a new $ char to denote splicing, so ($ form ...)
splices.
This commit is contained in:
2026-09-29 23:51:41 +03:00
parent 6f54bfbe08
commit 53f92727a5
3 changed files with 62 additions and 10 deletions

View File

@@ -212,6 +212,20 @@ Sex has support for syntactic macros. Macro definitions look like
functions: they have a name, an argument list and a body. Macro should functions: they have a name, an argument list and a body. Macro should
return Sex code. return Sex code.
A macro returns *one* form. To return several --- a function beside the
struct it works on, say --- return them under =$=, which splices them in
where the macro was written:
#+begin_src scheme
(defmacro (pair-of-fns a b)
`($ (fn ,a () int (return 1))
(fn ,b () int (return 2))))
#+end_src
=($)= expands to nothing. Everything else is a single form, including
one whose head is itself a form: =`((make-adder 10) 5)= calls what
=make-adder= returned, and is not two forms.
*** Examples: *** Examples:
**** Structure with templated value type **** Structure with templated value type
#+begin_src scheme #+begin_src scheme

View File

@@ -52,22 +52,29 @@
(cons (car forms) (take-until (cdr forms) tail)))) (cons (car forms) (take-until (cdr forms) tail))))
(define (macroexpand macro-form rest-forms) (define (macroexpand macro-form rest-forms)
;; We want to replace macro with its expansion. The problem is, ;; `(defmacro (two) 2)' expands to `2', and `($ (fn a ...) (fn b ...))'
;; top-level macro can return either a single form, or a list of ;; to two forms, spliced where the macro was written. `($)' expands to
;; forms, when it for example generates some aux ;; nothing.
;; structures/functions/typedefs.
;; ;;
;; Single form we just cons to the top of rest-forms, but multiple ;; Everything else is one form, a list whose head is itself a form
;; forms have to be appended to the rest-forms. ;; included: `((make-adder 10) 5)' calls what `make-adder' returned,
;; and without `$' to mark a splice there is no telling that from a
;; list of the two forms `(make-adder 10)' and `5'.
(let ((res (apply-macro macro-form)) (let ((res (apply-macro macro-form))
(src (form-source macro-form))) (src (form-source macro-form)))
;; An expansion is fresh structure with no location of its own. Give ;; An expansion is fresh structure with no location of its own. Give
;; it the call site's, the way cpp attributes a macro body to where ;; it the call site's, the way cpp attributes a macro body to where
;; the macro was used ;; the macro was used
(if (list? (car res)) (cond
(append (map (lambda (f) (stamp-form-source! f src)) res) ((splice-form? res)
rest-forms) (append (map (lambda (f) (stamp-form-source! f src)) (cdr res))
(cons (stamp-form-source! res src) rest-forms)))) rest-forms))
((null? res) rest-forms)
(else
(cons (stamp-form-source! res src) rest-forms)))))
(define (splice-form? form)
(and (pair? form) (list? form) (eq? '$ (car form))))
(define (match-sex-form sex-form acc) (define (match-sex-form sex-form acc)
(match sex-form (match sex-form

View File

@@ -327,6 +327,37 @@ compiles."
(fn f ((c (closure ((int)) int))) int (return (g c)))" (fn f ((c (closure ((int)) int))) int (return (g c)))"
"g(c)"))) "g(c)")))
;; `(car res)' on the expansion assumed it was a pair, so a macro
;; computing a value rather than building a form crashed the compiler.
(test-group "macro expanding to an atom"
(test-assert "a number"
(emits? "(defmacro (two) 2) (fn f () int (return (two)))"
"return 2;"))
(test-assert "a string"
(emits? "(defmacro (who) \"sex\") (fn f () void (g (who)))"
"g(\"sex\")"))
;; a symbol expansion can stand where a type does, which is what
;; makes a macro able to compute one
(test-assert "a symbol, used as a type"
(emits? "(defmacro (ty) 'int) (fn f () void (var x (ty) 0))"
"int x = 0"))
;; ...and nothing at all, for a macro that only registers something
(test-assert "nothing, at toplevel"
(emits? "(defmacro (quiet) (list)) (quiet) (fn f () int (return 1))"
"return 1;"))
(test-assert "nothing, in a body"
(emits? "(defmacro (quiet) (list)) (fn f () int (quiet) (return 1))"
"return 1;"))
;; several forms need `$', which is what tells a splice from a call
(test-assert "$ splices"
(emits? "(defmacro (pair) (list '$ '(fn a () int (return 1))
'(fn b () int (return 2))))
(pair)"
"b (void)"))
(test-assert "and ($) is nothing at all"
(emits? "(defmacro (quiet) (list '$)) (quiet) (fn f () int (return 1))"
"return 1;")))
;; A unary expression parenthesised its operand rather than itself, so ;; A unary expression parenthesised its operand rather than itself, so
;; the parens landed inside: `*(p).x', which C reads as `*(p.x)'. ;; the parens landed inside: `*(p).x', which C reads as `*(p.x)'.
(test-group "unary operand precedence" (test-group "unary operand precedence"