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

execute macro actions in phase 1 #240

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
8 changes: 4 additions & 4 deletions src/Expander.hs
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,7 @@
[ ( "open-syntax"
, Scheme [] $ tFun [tSyntax] (Prims.primitiveDatatype "Syntax-Contents" [tSyntax])
, ValueClosure $ HO $
\(ValueSyntax stx) ->

Check warning on line 413 in src/Expander.hs

View workflow job for this annotation

GitHub Actions / stack / ghc 9.2.8

Pattern match(es) are non-exhaustive

Check warning on line 413 in src/Expander.hs

View workflow job for this annotation

GitHub Actions / ubuntu-latest / ghc 9.2.5

Pattern match(es) are non-exhaustive
case syntaxE stx of
Id name ->
primitiveCtor "identifier-contents" [ValueString name]
Expand All @@ -428,9 +428,9 @@
, Scheme [] $
tFun [tSyntax, tSyntax, Prims.primitiveDatatype "Syntax-Contents" [tSyntax]] tSyntax
, ValueClosure $ HO $
\(ValueSyntax locStx) ->

Check warning on line 431 in src/Expander.hs

View workflow job for this annotation

GitHub Actions / stack / ghc 9.2.8

Pattern match(es) are non-exhaustive

Check warning on line 431 in src/Expander.hs

View workflow job for this annotation

GitHub Actions / ubuntu-latest / ghc 9.2.5

Pattern match(es) are non-exhaustive
ValueClosure $ HO $
\(ValueSyntax scopesStx) ->

Check warning on line 433 in src/Expander.hs

View workflow job for this annotation

GitHub Actions / stack / ghc 9.2.8

Pattern match(es) are non-exhaustive

Check warning on line 433 in src/Expander.hs

View workflow job for this annotation

GitHub Actions / ubuntu-latest / ghc 9.2.5

Pattern match(es) are non-exhaustive
ValueClosure $ HO $
-- N.B. Assuming correct constructors
\(ValueCtor ctor [arg]) ->
Expand Down Expand Up @@ -1306,10 +1306,10 @@
ValueSyntax $ addScope p stepScope stx
case macroVal of
ValueMacroAction act -> do
res <- interpretMacroAction prob act
res <- inEarlierPhase $ interpretMacroAction prob act
case res of
StuckOnType loc ty env cases kont ->
forkAwaitingTypeCase loc prob ty env cases kont
inEarlierPhase $ forkAwaitingTypeCase loc prob ty env cases kont
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm afraid I have opened a can of worms.

First, without this inEarlierPhase, the Macro actions which precede (type-case ...) execute in phase 1 while the Macro actions which follow it execute in phase 0. So something needs to be fixed.

Unfortunately, adding this inEarlierPhase is not the correct solution, because when the macro returns a Syntax object after the (type-case ...), that Syntax is expanded in phase 1 instead of phase 0. For example,

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

(datatype (T)
  (mkT))

(define-macros
  ([my-macro
    (lambda (stx)
      (pure '(mkT)))]))
(example (the (T) (my-macro)))

returns (mkT) as expected, but

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

(datatype (T)
  (mkT))

(define-macros
  ([my-macro
    (lambda (stx)
      (>>= (which-problem)
        (lambda (problem)
          (case problem
            [(expression type)
             (type-case type
               [(else _)
                (pure '(mkT))])]))))]))
(example (the (T) (my-macro)))

Fails with Unknown: <mkT> because mkT is only bound in phase 0, not in phase 1. This seems relatively easy to fix: instead of forking the job in an earlier phase, fork a job which executes the Macro action in an earlier phase and then returns the Syntax in the current phase.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The second, more important problem occurs when type-case matches on (T). Since the Macro action now executes in phase 1, type T, which is only bound in phase 0, is not found. The solution seems simple: just look up T in the later phase, right? Well... what about else? If we look up else in the later phase, then it is now else which is not found!

This makes sense: type-pattern is a Problem, so it should be possible to write macros which expand to a type-pattern. Since the body of my-macro is in phase 1, those macros should be from phase 2, and so should else. So type-case is in a difficult situation where it encounters identifiers and it does not know in which phase it should expand them.

I think the solution is, sadly, to reject the syntax

(type-case type
  [(T) ...]
  [(else _) ...])

In favor of a dedicated phase 2 macro which specifies that its argument is a type from phase 0.

(type-case type
  [(the T) ...]
  [(else _) ...])

or perhaps

(type-case type
  ['(T) ...]
  [(else _) ...])

Copy link
Owner Author

@gelisam gelisam May 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or maybe datatype should bind the type-pattern macros at a different phase? That is,

(datatype (Maybe A)
  (nothing)
  (just A))

should bind a Maybe macro at phase 0 which expects to be run in the type Problem at phase 0, and a Maybe macro at phase 1 which expects to be run in the type-pattern Problem at phase 1.

One obvious problem with this idea is that if I then try to make Maybe available in the macro definition:

(import "maybe-datatype.kl")
(import (shift "maybe-datatype.kl" 1))

I now have two conflicting Maybe macros at phase 1: the one which expects to run in the type-pattern Problem at phase 1, and the shifted one which expects to run in the type Problem at phase 1. Maybe it's time to implement the strategy we discussed, where multiple macros are allowed to have the same name, as long as all but one indicate that they do not expect to be called in that context (#241)?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of giving a new meaning to the or quote, I am currently implementing a new primitive macro called type-constructor, used like this:

(type-case type
  [(type-constructor Either a b) ...])

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The worms continue to come out of the can.

A datatype is uniquely identified by a module, a phase, and their true name (a String). But when we (import "either-datatype.kl"), we don't add that true name to the bindings of that phase. We add the primitive type macro named Either to the bindings of that phase. The name of the datatype is not necessarily the same as the name of the macro, because of scopes and shadowing. For example,

(datatype (Either A B)
  (left A)
  (right B))
(datatype (Either A B)
  (left A)
  (right B))

will define two datatypes, whose true names are Either and Either0 respectively. And since the second Either shadows the first, after importing that module the primitive type macro named Either now refers to the datatype whose true name is Either0.

Anyway, for that reason, (type-constructor Either a b) should not simply look up the datatype named Either, because the true name for that datatype might be Either0. Instead, it must use the name Either, and find that it refers to a primitive type macro.

A primitive type macro happens to be represented by the data constructor EPrimTypeMacro, but there is nothing in the Klister codebase which ever matches on a data constructor of EValue, so it would be weird for type-constructor to look up the EValue for Either, check if it is an EPrimTypeMacro, and then find the true name from that somehow. Instead, macros are always expanded (expandOneForm is the one function which does match on the EValue data constructors in order to accomplish this).

What does a type macro expand to? While we usually think of a macro as a function which produces a Syntax object, it is actually only user macros which expand to Syntax objects. Primitive macros have the side-effect of filling in ("linking") the mortise (e.g. a TypePatternPtr) at which the macro is expanded with a tenon (e.g. a TypePattern).

Primitive type macros, in particular, can either be expanded to fill a type mortise or a type-pattern mortise. In this case, a type-pattern mortise makes more sense.

This means that (type-constructor Either a b) is a phase 0 type-pattern would work by expanding (Either a b) in phase 0 type-pattern. I don't like the idea of (Either a b) being a valid type-pattern, it is too error-prone because matching on (Either a b) will work some of the time (when "either-datatype.kl" and (shift "either-datatype.kl" 1) are both imported), whereas at other time it will mysteriously fail (when Either refers to a different type at phase 0 and phase 1... which it does) and the type-constructor wrapper will be needed.

Which means that the final worm coming out the can is that I should create a new Problem, the type-constructor Problem. The type-pattern (type-constructor Either a b) at phase 1 will expand Either at phase 0 in the type-constructor Problem, obtaining the true name, and then using it to construct a TypePattern.

Phew, what an adventure!

Done expanded ->
case expanded of
ValueSyntax expansionResult ->
Expand Down Expand Up @@ -1432,8 +1432,8 @@
getIdent (ValueSyntax stx) = mustBeIdent stx
getIdent _other = throwError $ InternalError $ "Not a syntax object in " ++ opName
compareFree id1 id2 = do
b1 <- resolve id1
b2 <- resolve id2
b1 <- inLaterPhase $ resolve id1
b2 <- inLaterPhase $ resolve id2
return $ Done $
flip primitiveCtor [] $
if b1 == b2 then "true" else "false"
Expand Down
5 changes: 5 additions & 0 deletions src/Expander/Monad.hs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ module Expander.Monad
, getDecl
, getState
, inEarlierPhase
, inLaterPhase
, inPhase
, isExprChecked
, importing
Expand Down Expand Up @@ -407,6 +408,10 @@ inEarlierPhase :: Expand a -> Expand a
inEarlierPhase act =
Expand $ local (over (expanderLocal . expanderPhase) prior) $ runExpand act

inLaterPhase :: Expand a -> Expand a
inLaterPhase act =
Expand $ local (over (expanderLocal . expanderPhase) posterior) $ runExpand act

moduleScope :: ModuleName -> Expand Scope
moduleScope mn = moduleScope' mn

Expand Down
5 changes: 4 additions & 1 deletion src/Phase.hs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}

module Phase (Phase(..), runtime, prior, Phased(..)) where
module Phase (Phase(..), runtime, prior, posterior, Phased(..)) where

import Control.Lens
import Data.Data (Data)
Expand Down Expand Up @@ -34,6 +34,9 @@ runtime = Phase 0
prior :: Phase -> Phase
prior (Phase i) = Phase (i + 1)

posterior :: Phase -> Phase
posterior (Phase i) = Phase (i - 1)

class Phased a where
shift :: Natural -> a -> a

Expand Down
Loading