不断发展的数据结构

时间:2012-05-25 21:29:14

标签: haskell

我正在尝试在Haskell中为类C语言编写编译器。编译器通过转换AST来进行。第一遍解析输入以创建AST,使用符号表绑定结点,以允许在定义符号之前定位符号,而无需前向引用。

AST包含有关类型和表达式的信息,它们之间可以有连接;例如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。通过这样做,我们可以确定表达式的类型,并从类型图中消除ExprSizeofExprFieldTypeDefTypeOf 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'都已确定其Type'

所以,最后,问题是:创建两个几乎相同的数据类型集,或尝试将它们统一为单个数据类型是否更好?

保留两个单独的数据类型可以明确表示某些构造函数不再出现。但是,执行常量折叠以评估常量表达式的函数将具有类型:

foldConstants :: Expr -> Either String Expr'

但这意味着我们不能在Expr' s后执行常量折叠(想象一下操纵Expr'的一些传递,并想要折叠任何出现的常量表达式)。我们需要另一个实现:

foldConstants' :: Expr' -> Either String Expr'

另一方面,保持单一类型可以解决常量折叠问题,但会阻止类型检查器强制执行静态不变量。

此外,在第一次传递期间,我们将什么放入未知字段(如字段偏移,数组大小和表达式类型)?我们可以使用undefinederror "*hole*"插入漏洞,但这感觉就像是等待发生的灾难(比如NULL指针甚至无法检查)。我们可以将未知字段更改为Maybe,并使用Nothing插入漏洞(就像我们可以检查的NULL指针一样),但这会很烦人在后续的传递中,必须不断地从总是Maybe s的Just中提取值。

2 个答案:

答案 0 :(得分:17)

希望有更多经验的人会有更优雅,经过实战考验并准备好的答案,但这是我的目标。

你可以用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 where Bar :: Int -> Char -> Foo(除语法外,两者完全相同)。

  • 我们已为TypeExpr添加了一个类型变量。这是一个所谓的幻像类型变量:没有存储类型为p的实际数据,它仅用于强制类型系统中的不变量。

  • 我们已经声明了虚拟类型来表示转换前后的阶段:阶段0和阶段1。 (在一个具有多个阶段的更复杂的系统中,我们可能会使用类型级别的数字来表示它们。)

  • GADT让我们在数据结构中存储类型级不变量。这里我们有两个。首先,递归位置必须与包含它们的结构具有相同的相位。例如,查看TypePointer :: Id -> Type p -> Type p,您将Type p传递给TypePointer构造函数并获得Type p作为结果,那些p必须相同类型。 (如果我们想要允许不同的类型,我们可以使用pq。)

  • 第二,我们强制执行一些构造函数只能在第一阶段使用的事实。大多数构造函数在幻像类型变量p中都是多态的,但其中一些要求它是P0。这意味着这些构造函数只能用于构造Type P0Expr P0类型的值,而不能用于任何其他阶段。

GADT在两个方向上工作。第一个是如果你有一个返回Type P1的函数,并尝试使用其中一个返回Type P0的构造函数来构造它,你将得到一个类型错误。这就是所谓的"正确的构造":它是静态不可能构造一个无效的结构(前提是你可以编码类型系统中的所有相关不变量)。另一方面,如果您的值为Type P1,则可以确定其构造正确:TypeOfTypeDef构造函数无法使用(实际上,如果你尝试对它们进行模式匹配,编译器会抱怨),并且任何递归位置也必须是阶段P1。基本上,当您构建GADT时,您存储了满足类型约束的证据,并且当您对其进行模式匹配时,您将检索该证据并可以利用它。

这很容易。不幸的是,除了允许哪些构造函数之外,我们在两种类型之间存在一些差异:一些构造函数参数在各阶段之间是不同的,而一些仅在转换后阶段存在。我们可以再次使用GADT对其进行编码,但它并不是低成本和优雅的。一种解决方案是复制所有不同的构造函数,并为P0P1分别使用一个构造函数。但重复并不好。我们可以尝试做得更精细:

-- 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

这里有一些帮助器类型,我们强制执行一些构造函数参数只能出现在第一阶段(MaybeP)而有些在两个阶段(EitherP)之间不同的事实。虽然这使我们完全类型安全,但感觉有点特别,我们仍然需要始终将内容包装进出MaybePEitherP。我不知道在这方面是否有更好的解决方案。但完整的类型安全是这样的:我们可以写fromJustP :: MaybeP P1 a -> a并确保它完全安全。

更新:另一种方法是使用TypeFamilies

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

相对于先前版本,ExprType的唯一更改是构造函数需要添加Phase p约束,例如ExprInt :: Phase p => MaybeType p -> Int -> Expr p

如果已知pTypeExpr的类型,您可以静态地知道MaybeP()还是EitherP给定类型以及p所属的类型,并且可以直接将它们用作该类型而无需显式展开。如果maybeP未知,您可以使用eitherP课程中的PhaseProxy来了解它们是什么。 (p参数是必要的,因为否则编译器将无法确定您的意思是哪个阶段。)这类似于GADT版本,如果已知MaybeP,则为可以确定EitherP()包含的内容,否则您必须模式匹配两种可能性。这个解决方案在“缺失”方面并不完善。参数变为Expr而不是完全消失。

构建Typep s似乎在两个版本之间大致相似:如果您构建的值具有任何阶段特定的值,那么它必须指定该阶段在它的类型。当您想在asdf :: MaybeP p a -> MaybeP p a asdf NothingP = NothingP asdf (JustP a) = JustP a 中编写函数多态而仍然处理特定于阶段的部分时,似乎会遇到麻烦。使用GADT,这很简单:

asdf _ = NothingP

请注意,如果我只编写TypeFamilies编译器会抱怨,因为输出的类型不能保证与输入相同。通过模式匹配,我们可以确定输入的类型并返回相同类型的结果。

使用maybeP版本时,这要困难得多。只需使用Maybe和结果maybeP,就无法向编译器证明有关类型的任何内容。您可以通过eitherPMaybe返回Eithermaybe,而不是让eithermaybeP :: 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 成为解构函数,而不是Rank2TypesMaybeP 1}}也使类型相等:

EitherP

(请注意,我们需要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 MaybeP p a的GADT版本的CPS转换版本。)

然后我们可以写:

p

但这仍然不够,因为GHC说:

a

也许你可以在某个地方用类型签名来解决这个问题,但是在这一点上它似乎比它的价值更令人烦恼。所以等待来自其他人的进一步信息,我将建议使用GADT版本,更简单,更强大,以一点语法噪音为代价。

再次更新:这里的问题是因为Proxy p是一个类型函数,而且没有其他信息可供使用,GHC无法知道phasep应该是什么是。如果我传入a并使用{{1}}而不是{{1}}来解决{{1}},但{{1}}仍然未知。

答案 1 :(得分:4)

这个问题没有理想的解决方案,因为每个问题都有不同的优点和缺点。

我个人会使用单一数据类型“树”,并为需要区分的事物添加单独的数据构造函数。即:

data Type
  = ...
  | TypeArray Id Type Expr
  | TypeResolvedArray Id Type Int
  | ...

这样做的好处是你可以在同一棵树上多次运行相同的阶段,但是理由比这更深入:假设您正在实现一个生成更多AST的语法元素(类似于{ {1}}或C ++模板类型的交易,它可以依赖于像include这样的常量exprs,因此您无法在第一次迭代中对其进行求值)。使用统一数据类型方法,您只需在现有树中插入新AST,不仅可以直接在该树上运行与之前相同的阶段,而且可以免费获得缓存,即如果新AST引用了使用TypeArray或其他内容的数组,您不必再次确定sizeof(typeof(myarr))的常量大小,因为它的类型已经是您之前解析阶段的myarr

当您经过所有编译阶段时,您可以使用不同的表示形式,是时候解释代码(或其他东西);那么你肯定的是,不再需要AST更改,更简化的表示可能是一个好主意。

顺便说一下,对于数组大小,您应该使用Data.Word.Word而不是Data.Int.Int。在C中使用TypeResolvedArray来索引数组这是一个常见的错误,而C指针实际上是无符号的。请不要用你的语言犯这个错误,除非你真的想支持负数大小的数组。