Haskell 演化数据结构
我正在尝试用Haskell为类C语言编写编译器。编译器通过转换AST进行编译。第一步解析输入以创建AST,与符号表结为一体,以允许在定义符号之前定位符号,而无需前向引用 AST包含关于类型和表达式的信息,它们之间可以有连接;e、 g.Haskell 演化数据结构,haskell,Haskell,我正在尝试用Haskell为类C语言编写编译器。编译器通过转换AST进行编译。第一步解析输入以创建AST,与符号表结为一体,以允许在定义符号之前定位符号,而无需前向引用 AST包含关于类型和表达式的信息,它们之间可以有连接;e、 g.sizeof(T)是一个依赖于类型T的表达式,T[e]是一个依赖于常量表达式e的数组类型 类型和表达式由Haskell数据类型表示,如下所示: data Type = TypeInt Id | TypePointer Id Type -- tar
sizeof(T)
是一个依赖于类型T
的表达式,T[e]
是一个依赖于常量表达式e
的数组类型
类型和表达式由Haskell数据类型表示,如下所示:
data Type = TypeInt Id
| TypePointer Id Type -- target type
| TypeArray Id Type Expr -- elt type, elt count
| TypeStruct Id [(String, Type)] -- [(field name, field type)]
| TypeOf Id Expr
| TypeDef Id Type
data Expr = ExprInt Int -- literal int
| ExprVar Var -- variable
| ExprSizeof Type
| ExprUnop Unop Expr
| ExprBinop Binop Expr Expr
| ExprField Bool Expr String -- Bool gives true=>s.field, false=>p->field
其中Unop
包括地址(&
)和解除引用(*
)等运算符,Binop
包括加号(+
)和时间(*
)等运算符
请注意,每个类型都分配了唯一的Id
。这用于构造类型依赖关系图,以检测导致无限类型的循环。一旦我们确定类型图中没有循环,就可以安全地对其应用递归函数,而不必进入无限循环
下一步是确定每种类型的大小,为结构字段指定偏移量,并用指针算法替换ExprField
s。这样,我们可以确定表达式的类型,并从类型图中删除ExprSizeof
s、ExprField
s、TypeDef
s和TypeOf
s,因此我们的类型和表达式已经进化,现在看起来更像这样:
data Type' = TypeInt Id
| TypePointer Id Type'
| TypeArray Id Type' Int -- constant expression has been eval'd
| TypeStruct Id [(String, Int, Type')] -- field offset has been determined
data Expr' = ExprInt Type' Int
| ExprVar Type' Var
| ExprUnop Type' Unop Expr'
| ExprBinop Type' Binop Expr' Expr'
请注意,我们已经删除了一些数据构造函数,并稍微更改了一些其他构造函数。特别是,Type'
不再包含任何Expr'
,并且每个Expr'
都已确定其类型'
因此,最后一个问题是:创建两个几乎相同的数据类型集,还是尝试将它们统一为一个数据类型更好
保留两个单独的数据类型可以明确地表明某些构造函数不能再出现。但是,执行常数折叠以计算常数表达式的函数将具有以下类型:
foldConstants :: Expr -> Either String Expr'
但这意味着我们以后不能用Expr'
s执行常量折叠(想象一下,某个过程操纵Expr'
,并希望折叠任何出现的常量表达式)。我们需要另一种实施方式:
foldConstants' :: Expr' -> Either String Expr'
另一方面,保留单个类型可以解决常量折叠问题,但会阻止类型检查器强制执行静态不变量
此外,在第一次过程中,我们在未知字段(如字段偏移量、数组大小和表达式类型)中放入了什么?我们可以用
未定义的
,或错误“*hole*”
,来填补漏洞,但这感觉就像是一场即将发生的灾难(比如你甚至无法检查的NULL
指针)。我们可以将未知字段更改为Maybe
s,并使用Nothing
堵住漏洞(比如我们可以检查的NULL
指针),但是,在随后的过程中,必须不断地从中提取值,这将是一件令人恼火的事情,因为每个值都有不同的优缺点
我个人会使用单一的数据类型“树”,并为需要区分的内容添加单独的数据构造函数。即:
data Type
= ...
| TypeArray Id Type Expr
| TypeResolvedArray Id Type Int
| ...
这样做的好处是,正如您所说,您可以在同一棵树上多次运行同一阶段,但推理要比这更深:假设您正在实现一个生成更多AST的语法元素(像)包括或C++模板的一种处理,它可以依赖于常数ExpRs,比如你的代码> TypeArray < /Cord>,因此你不能在第一次迭代中对其进行评估。使用统一数据类型方法,您只需在现有树中插入新的AST,不仅可以直接在该树上运行与以前相同的阶段,还可以免费获得缓存,即如果新的AST使用sizeof(typeof(myarr))引用数组
或其他,您不必再次确定myarr
的常量大小,因为它的类型已经是上一个解析阶段的TypeResolvedArray
当您完成所有编译阶段后,您可以使用不同的表示法,现在是解释代码(或其他内容)的时候了;然后您可以确定,不再需要对AST进行任何更改,更精简的表示法可能是一个好主意
顺便说一句,数组大小应该使用而不是。在C中,使用int
s索引数组是一个常见的错误,而C指针实际上是无符号的。请不要在您的语言中也犯这个错误,除非您真的想支持负大小的数组。希望有更多经验的人会有一个更精致、经过战斗考验和准备好的答案,但这是我的尝试
您可以用GADT以相对较低的成本吃一部分馅饼:
{-# LANGUAGE GADTs #-}
data P0 -- phase zero
data P1 -- phase one
data Type p where
TypeInt :: Id -> Type p
TypePointer :: Id -> Type p -> Type p -- target type
TypeArray :: Id -> Type p -> Expr p -> Type p -- elt type, elt count
TypeStruct :: Id -> [(String, Type p)] -> Type p -- [(field name, field type)]
TypeOf :: Id -> Expr P0 -> Type P0
TypeDef :: Id -> Type P0 -> Type P0
data Expr p where
ExprInt :: Int -> Expr p -- literal int
ExprVar :: Var -> Expr p -- variable
ExprSizeof :: Type P0 -> Expr P0
ExprUnop :: Unop -> Expr p -> Expr p
ExprBinop :: Binop -> Expr p -> Expr p -> Expr p
ExprField :: Bool -> Expr P0 -> String -> Expr P0 -- Bool gives true=>s.field, false=>p->field
我们改变的是:
- 数据类型现在使用GADT语法。这意味着构造函数是使用其类型签名声明的。
data Foo=Bar Int Char
变成data Foo,其中Bar::Int->Char->Foo
(除了语法之外,两者完全等效)
- 我们在
type
和Expr
中都添加了一个类型变量。这是一个所谓的幻影类型变量:没有存储p
类型的实际数据,它仅用于在类型系统中强制执行不变量
- 我们声明了虚拟类型来表示转换前后的阶段:阶段0和阶段1。(在一个包含多个阶段的更复杂系统中)
-- a couple of helper types
-- here I take advantage of the fact that of the things only present in one phase,
-- they're always present in P1 and not P0, and not vice versa
data MaybeP p a where
NothingP :: MaybeP P0 a
JustP :: a -> MaybeP P1 a
data EitherP p a b where
LeftP :: a -> EitherP P0 a b
RightP :: b -> EitherP P1 a b
data Type p where
TypeInt :: Id -> Type p
TypePointer :: Id -> Type p -> Type p
TypeArray :: Id -> Type p -> EitherP p (Expr p) Int -> Type p
TypeStruct :: Id -> [(String, MaybeP p Int, Type p)] -> Type p
TypeOf :: Id -> Expr P0 -> Type P0
TypeDef :: Id -> Type P0 -> Type P0
-- for brevity
type MaybeType p = MaybeP p (Type p)
data Expr p where
ExprInt :: MaybeType p -> Int -> Expr p
ExprVar :: MaybeType p -> Var -> Expr p
ExprSizeof :: Type P0 -> Expr P0
ExprUnop :: MaybeType p -> Unop -> Expr p -> Expr p
ExprBinop :: MaybeType p -> Binop -> Expr p -> Expr p -> Expr p
ExprField :: Bool -> Expr P0 -> String -> Expr P0
data Proxy a = Proxy
class Phase p where
type MaybeP p a
type EitherP p a b
maybeP :: Proxy p -> MaybeP p a -> Maybe a
eitherP :: Proxy p -> EitherP p a b -> Either a b
phase :: Proxy p
phase = Proxy
instance Phase P0 where
type MaybeP P0 a = ()
type EitherP P0 a b = a
maybeP _ _ = Nothing
eitherP _ a = Left a
instance Phase P1 where
type MaybeP P1 a = a
type EitherP P1 a b = b
maybeP _ a = Just a
eitherP _ a = Right a
asdf :: MaybeP p a -> MaybeP p a
asdf NothingP = NothingP
asdf (JustP a) = JustP a
maybeP :: Proxy p -> (p ~ P0 => r) -> (p ~ P1 => a -> r) -> MaybeP p a -> r
eitherP :: Proxy p -> (p ~ P0 => a -> r) -> (p ~ P1 => b -> r) -> EitherP p a b -> r
asdf :: Phase p => MaybeP p a -> MaybeP p a
asdf a = maybeP phase () id a
data.hs:116:29:
Could not deduce (MaybeP p a ~ MaybeP p0 a0)
from the context (Phase p)
bound by the type signature for
asdf :: Phase p => MaybeP p a -> MaybeP p a
at data.hs:116:1-29
NB: `MaybeP' is a type function, and may not be injective
In the fourth argument of `maybeP', namely `a'
In the expression: maybeP phase () id a
In an equation for `asdf': asdf a = maybeP phase () id a