Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

macro from local function #229

Open
gelisam opened this issue Feb 2, 2024 · 0 comments
Open

macro from local function #229

gelisam opened this issue Feb 2, 2024 · 0 comments
Labels
enhancement New feature or request

Comments

@gelisam
Copy link
Owner

gelisam commented Feb 2, 2024

I still think we should add support for binding a local function of type (-> Syntax (Macro Syntax)) to an identifier, which a macro can then return as part of a syntax object.

In our previous discussions, we had two objections:

  1. the captured variables are meaningless at phases other than the one they were captured in
  2. syntax parameters cover most of the use cases

I have counter-arguments to both objections.

Variable capture

Identifiers already have a different meaning at different phases. Currently, code at level 0 can call let-syntax with a level 1 function, which captures level 1 variables, to bind it to a level 0 identifier my-macro. Then the level 0 body of let-syntax can call (my-macro) in order to run its body at level 1. Since the my-macro body now runs at level 1, it is fine that this body refers to the variables it captured at level 1.

-- level 0
#lang "prelude.kl"
(import (shift "prelude.kl" 1))

(meta
  -- level 1
  (define enable-ten 1))  -- bound in the regular environment at level 1

-- level 0
(example
  (let-syntax
    [my-macro  -- bound in the expander environment at level 0
     (lambda (stx)
       -- level 1
       (if (= enable-ten 1)  -- captures enable-ten
         (pure '10)
         (pure '5)))]
    -- level 0
    (+ 1 (my-macro))))  -- used at level 0, its body runs at level 1
-- returns 11

Similarly, I want code at level 1 to call, say, let-local-syntax with a level 1 function, which captures level 1 variables, to bind it to a level 0 identifier. This time, inner-macro-name is not that level 0 identifier, it is a level 1 identifier whose value is that level 0 identifier. Then the level 1 body of let-local-syntax can construct a syntax object which contains this level 0 identifier. When that code is spliced into level 0, the level 0 identifier is recognized as a macro, so it runs its body at level 1. Since this body now runs at level 1, it is fine that this body refers to the variables it captured at level 1.

#lang "prelude.kl"
(import (shift "prelude.kl" 1))

-- level 0
(define-macro (outer-macro)
  -- level 1
  (let [enable-ten 1]  -- bound in the regular environment at level 1, its value is 1
    (let-local-syntax
      [inner-macro-name  -- bound in the regular environment at level 1, its value is 'inner-macro
       (lambda (stx)
         -- still level 1
         (if (= enable-ten 1)  -- captures enable-ten
           (pure '10)
           (pure '5)))]
      (pure `(+ 1 (,inner-macro-name))))))

(example (outer-macro))
-- expands to
-- (+ 1 (inner-macro))  -- used at level 0, its body runs at level 1
-- returns 11

Use case

Currently, if I want a macro to cooperate with the type checker by indicating part of the type of the expression it wants to return, the definition of this macro must be divided into a main macro and an auxiliary macro:

#lang "prelude.kl"
(import "define-syntax-rule.kl")

(define-syntax-rule (aux-macro)
  -- imagine a more complex macro which does something different depending on
  -- whether A is Integer or String
  (lambda (x) x))

(define-syntax-rule (main-macro)
  (with-unknown-type [A]
    (the (-> A A)
      (aux-macro))))

(example (main-macro))

It would be nice to automate this work of splitting the definition of the macro:

(define-macro (main-macro)
  (with-unknown-type [A]
    (with-partially-known-type (-> A A)
      -- imagine a more complex macro which does something different depending on
      -- whether A is Integer or String
      (pure '(lambda (x) x)))))

With let-local-syntax, the implementation of with-partially-known-type is easy:

(define-macro (with-partially-known-type tp body)
  (let-local-syntax
    [inner-macro-name (lambda (_stx) body)]
    (pure `(the tp (,inner-macro-name)))))

And seamlessly allows local variables to remain in scope within the body of with-partially-known-type:

(define-macro (main-macro)
  (let [enable-ten 1]
    (with-unknown-type [A]
      (with-partially-known-type (-> A A)
        -- use enable-ten
        ...))))

Whereas without let-local-syntax, it is necessary for the implementer of main-macro to know that with-partially-known-type defines an intermediate macro under the hood, so that it can make enable-ten a syntax parameter instead of an ordinary local variable.

@gelisam gelisam added the enhancement New feature or request label Feb 2, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

1 participant