有 许多 方法来注释 AST!这是所谓的AST typing problem 的一半,另一半是您如何管理在编译过程中发生变化的 AST。问题并没有完全“解决”:所有解决方案都有权衡,选择哪一个取决于您预期的用例。最后,我将介绍一些您可能想调查的内容。
无论您选择哪种方法来组织实际数据类型,如果它使模式匹配变得丑陋或笨拙,自然的解决方案是PatternSynonyms。
考虑您的第一个示例:
{-# Language PatternSynonyms #-}
type Location = (Int, Int)
data Expr
= LocatedIf Value [Expr] Location
| LocatedVarDef String Value Location
-- Unidirectional pattern synonyms which ignore the location:
pattern If :: Value -> [Expr] -> Expr
pattern If val exprs <- LocatedIf val exprs _loc
pattern VarDef :: String -> Value -> Expr
pattern VarDef name expr <- LocatedVarDef name expr _loc
-- Inform GHC that matching ‘If’ and ‘VarDef’ is just as good
-- as matching ‘LocatedIf’ and ‘LocatedVarDef’.
{-# Complete If, VarDef #-}
这对于您的目的可能已经足够整洁了。但这里还有一些我认为有用的提示。
先放注解:直接给AST添加注解类型时,我往往更喜欢把它作为每个构造函数的第一个参数,这样可以方便地部分应用。
data LocatedExpr
= LocatedIf Location Value [Expr]
| LocatedVarDef Location String Value
如果注解是一个位置,那么这也使得在编写某些类型的解析器时更方便获取,沿着解析器组合库中的AnnotatedIf <$> (getSourceLocation <* ifKeyword) <*> value <*> many expr 行。
参数化你的注解:我经常把注解类型变成类型参数,这样GHC就可以派生出一些对我有用的类:
{-# Language
DeriveFoldable,
DeriveFunctor,
DeriveTraversable #-}
data AnnotatedExpr a
= AnnotatedIf a Value [Expr]
| AnnotatedVarDef a String Value
deriving (Functor, Foldable, Traversable)
type LocatedExpr = AnnotatedExpr Location
-- Get the annotation of an expression.
-- (Total as long as every constructor is annotated.)
exprAnnotation :: AnnotatedExpr a -> a
exprAnnotation = head
-- Update annotations purely.
mapAnnotations
:: (a -> b)
-> AnnotatedExpr a -> AnnotatedExpr b
mapAnnotations = fmap
-- traverse, foldMap, &c.
如果您想要“不干扰”,请使用多态性:您可以通过对注释类型进行多态性来强制评估器无法检查注释类型。模式同义词仍然让您可以方便地匹配这些表达式:
pattern If :: Value -> [AnnotatedExpr a] -> AnnotatedExpr a
pattern If val exprs <- AnnotatedIf _anno val exprs
-- …
eval :: AnnotatedExpr a -> Value
eval expr = case expr of
If val exprs -> -- …
VarDef name expr -> -- …
未注释的术语不是你的敌人:没有源位置的术语不利于错误报告,但我认为将模式同义词设为双向以方便构造未注释的术语仍然是个好主意带有单元() 注释的术语。 (或者等价的东西,如果你使用例如 Maybe Location 作为注释类型。)
原因是这对于编写单元测试非常方便,你想检查输出,但想使用Eq而不是模式匹配,并且不想比较所有源位置与他们无关的测试。使用派生类,void :: (Functor f) => f a -> f () 去除 AST 上的所有注释。
import Control.Monad (void)
type BareExpr = AnnotatedExpr ()
-- One way to define bidirectional synonyms, so e.g.
-- ‘If’ can be used as either a pattern or a constructor.
pattern If :: Value -> [BareExpr] -> BareExpr
pattern If val exprs = AnnotatedIf () val exprs
-- …
stripAnnotations :: AnnotatedExpr a -> BareExpr
stripAnnotations = void
同样,您可以使用GADTs / ExistentialQuantification 表示data AnyExpr where { AnyExpr :: AnnotatedExpr a -> AnyExpr } / data AnyExpr = forall a. AnyExpr (AnnotatedExpr a);这样,注释的信息与() 完全相同,但您不需要使用void 在整个树上使用fmap 来剥离它,只需应用AnyExpr 构造函数来隐藏类型.
最后,简单介绍一下几种 AST 类型的解决方案。
-
使用标签(例如唯一的 ID)注释每个 AST 节点,然后存储所有元数据,如源位置、类型和其他任何内容,与 AST 分开:
import Data.IntMap (IntMap)
-- More sophisticated/stronglier-typed tags are possible.
newtype Tag = Tag Int
newtype TagMap a = TagMap (IntMap a)
data Expr
= If !Tag Value [Expr]
| VarDef !Tag String Expr
type Span = (Location, Location)
type SourceMap = TagMap Span
type CommentMap = TagMap (Span, String)
parse
:: String -- Input
-> Either ParseError
( Expr -- Parsed expression
, SourceMap -- Source locations of tags
, CommentMap -- Sideband for comments
-- …
)
优点是你可以很容易地在任何地方混入任意新类型的注解,而不影响 AST 本身,并且避免仅仅为了改变注解而重写 AST。您可以将树和注释表视为一种数据库,其中标签是关联它们的“外键”。一个缺点是,当您做重写 AST 时,您必须小心维护这些标签。
我不知道这种方法是否有固定的名称;我认为它只是“标记”或“标记的 AST”。
-
recursion-schemes 和/或Data Types à la CartePDF:将带注释的表达式树的“递归”部分与“注释”部分分开,并使用Fix 将它们重新绑定在一起,用Compose(或Cofree)在中间添加注解。
data ExprF e
= IfF Value [e]
| VarDefF String e
-- …
deriving (Foldable, Functor, Traversable, …)
-- Unannotated: Expr ~ ExprF (ExprF (ExprF (…)))
type Expr = Fix ExprF
-- With a location at each recursive step:
--
-- LocatedExpr ~ Located (ExprF (Located (ExprF (…))))
type LocatedExpr = Fix (Compose Located ExprF)
data Located a = Located Location a
deriving (Foldable, Functor, Traversable, …)
-- or: type Located = (,) Location
一个明显的优势是您可以免费获得一堆不错的遍历内容,例如cata,因此您可以避免一遍又一遍地编写手动遍历您的 AST。缺点是它增加了一些模式混乱来清理,就像“点菜”方法一样,但它们确实提供了很大的灵活性。
-
Trees That GrowPDF 仅用于源位置有点过分,但在严肃的编译器中它非常有用。如果您希望有多个注释类型(例如推断类型或其他分析结果)或随时间变化的 AST,那么您为“编译阶段”添加一个类型参数(解析、重命名、类型检查、脱糖等) .) 并根据该索引选择字段类型或启用和禁用构造函数。
一个真的不幸的缺点是你经常不得不重写树,即使在没有改变的地方,因为一切都取决于“阶段”。我使用的另一种方法是为每种类型的阶段或注释添加一个类型参数可以独立变化,例如data Expr annotation termVarName typeVarName,并使用 type 和 pattern 同义词对其进行抽象。这使您可以独立更新索引,并且仍然使用 Functor 和 Bitraversable 等类。