From 53f92727a51c012f731bcd1e74b98f72c1cff8ca Mon Sep 17 00:00:00 2001 From: alex-eg Date: Tue, 29 Sep 2026 23:51:41 +0300 Subject: [PATCH] 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. --- Readme.org | 14 ++++++++++++++ semen.scm | 27 +++++++++++++++++---------- tests/codegen.scm | 31 +++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 10 deletions(-) diff --git a/Readme.org b/Readme.org index 622d6ab..57d7e66 100644 --- a/Readme.org +++ b/Readme.org @@ -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 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: **** Structure with templated value type #+begin_src scheme diff --git a/semen.scm b/semen.scm index 7c79852..e8ae7b2 100644 --- a/semen.scm +++ b/semen.scm @@ -52,22 +52,29 @@ (cons (car forms) (take-until (cdr forms) tail)))) (define (macroexpand macro-form rest-forms) - ;; We want to replace macro with its expansion. The problem is, - ;; top-level macro can return either a single form, or a list of - ;; forms, when it for example generates some aux - ;; structures/functions/typedefs. + ;; `(defmacro (two) 2)' expands to `2', and `($ (fn a ...) (fn b ...))' + ;; to two forms, spliced where the macro was written. `($)' expands to + ;; nothing. ;; - ;; Single form we just cons to the top of rest-forms, but multiple - ;; forms have to be appended to the rest-forms. + ;; Everything else is one form, a list whose head is itself a form + ;; 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)) (src (form-source macro-form))) ;; 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 ;; the macro was used - (if (list? (car res)) - (append (map (lambda (f) (stamp-form-source! f src)) res) - rest-forms) - (cons (stamp-form-source! res src) rest-forms)))) + (cond + ((splice-form? res) + (append (map (lambda (f) (stamp-form-source! f src)) (cdr res)) + 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) (match sex-form diff --git a/tests/codegen.scm b/tests/codegen.scm index 4124dbd..e20ee7a 100644 --- a/tests/codegen.scm +++ b/tests/codegen.scm @@ -327,6 +327,37 @@ compiles." (fn f ((c (closure ((int)) int))) int (return (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 ;; the parens landed inside: `*(p).x', which C reads as `*(p.x)'. (test-group "unary operand precedence"