capnp-0.3.0.0: exe/capnpc-haskell/FrontEnd.hs
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
module FrontEnd
( cgrToIR
) where
import Data.Word
import Data.Char (toUpper)
import Data.Function ((&))
import Data.List (partition)
import Data.Monoid ((<>))
import Data.ReinterpretCast (doubleToWord, floatToWord)
import Data.Text.Encoding (encodeUtf8)
import Util (Id, splitOn)
import qualified Data.ByteString as BS
import qualified Data.Map.Strict as M
import qualified Data.Text as T
import qualified Data.Vector as V
import Capnp.Capnp.Schema.Pure
import Backends.Common (dataFieldSize)
import qualified Data.Capnp.Untyped.Pure as Untyped
import qualified IR
type NodeMap = M.Map Id NodeMetaData
-- | Some useful metadata about a node.
data NodeMetaData = NodeMetaData
{ moduleId :: Id
-- ^ The id of the module that this node belongs to.
, namespace :: [T.Text]
-- ^ The namespace within a file that the node is defined.
, node :: Node
-- ^ The node itself.
}
deriving(Show, Read, Eq)
-- | @'identifierFromMetaData' thisModule meta@ return a haskell identifier
-- for a node based on the metadata @meta@, and @thisModule@, the id for
-- the module in which the name will be used.
identifierFromMetaData :: Id -> NodeMetaData -> IR.Name
identifierFromMetaData _ NodeMetaData{moduleId, namespace=(unqualified:localNS)} =
IR.Name
{ nameModule = IR.ByCapnpId moduleId
, nameLocalNS = IR.Namespace $ reverse localNS
, nameUnqualified = unqualified
}
identifierFromMetaData _ meta =
-- TODO: rule out this possibility statically; shouldn't be too hard.
error $ "Node metadata had an empty namespace field: " ++ show meta
-- Helper for makeNodeMap; recursively collect metadata for a node and
-- all of its descendants in the tree.
collectMetaData :: M.Map Id Node -> NodeMetaData -> [(Id, NodeMetaData)]
collectMetaData nodeMap meta@NodeMetaData{node=node@Node{..}, ..} = concat
[ [(id, meta)]
, concatMap collectNested $ V.toList nestedNodes
-- Child nodes can be in two places: most are in nestedNodes, but
-- group fields are not, and can only be found in the fields of
-- a struct union.
, case union' of
Node'struct{..} ->
concatMap collectField $ V.toList fields
_ ->
[]
]
where
-- Collect metadata for nodes under a Field.
collectField Field{..} = case union' of
Field'group{..} -> collectMetaData nodeMap
meta
{ node = nodeMap M.! typeId
-- in this case, name comes from the field, so for a field bar in a
-- struct Foo, we'll end up with a type for the group named Foo'bar.
, namespace = makeLegalName name : namespace
}
_ ->
[]
-- Collect metadata for nodes under a NestedNode.
collectNested nn@Node'NestedNode{..} = case M.lookup id nodeMap of
Just node -> collectMetaData nodeMap
meta
{ node = node
, namespace = makeLegalName name : namespace
}
Nothing ->
-- Imperically, this can happen if e.g. the node isn't in a subtree
-- of a RequestedFile, and not actually used anywhere else. This
-- crops up with c++.capnp:name when processing schema.capnp, for
-- example. In this case, just leave it out of the map:
[]
-- | Convert the argument into a valid haskell identifier. This doesn't handle
-- every possible violation, just ones that might occur in legal schema.
makeLegalName :: T.Text -> T.Text
makeLegalName txt
| txt `elem` keywords = txt <> "_"
| otherwise = txt
where
keywords =
[ "as", "case", "of", "class", "data", "family", "instance", "default"
, "deriving", "do", "forall", "foreign", "hiding", "if", "then", "else"
, "import", "infix", "infixl", "infixr", "let", "in", "mdo", "module"
, "newtype", "proc", "qualified", "rec", "type", "where"
]
-- | Build a NodeMap for all of the nodes in the CodeGeneratorRequest.
makeNodeMap :: CodeGeneratorRequest -> NodeMap
makeNodeMap CodeGeneratorRequest{..} =
V.map (\node@Node{..} -> collectMetaData baseMap NodeMetaData
{ moduleId = id
, namespace = []
, node = node
})
rootNodes
& V.toList
& concat
& M.fromList
where
rootNodes = V.filter (\Node{..} -> scopeId == 0) nodes
baseMap =
V.toList nodes
& map (\node@Node{..} -> (id, node))
& M.fromList
generateModule :: NodeMap -> CodeGeneratorRequest'RequestedFile -> IR.Module
generateModule nodeMap CodeGeneratorRequest'RequestedFile{..} =
IR.Module
{ modId = id
, modName = IR.Namespace
$ map (T.pack . mangleFileName)
$ filter (/= "")
$ splitOn '/' (T.unpack filename)
, modFile = filename
, modImports = map generateImport $ V.toList imports
, modDecls = M.fromList $ concatMap (generateDecls id nodeMap)
$ filter (\NodeMetaData{..} -> moduleId == id)
$ map snd
$ M.toList nodeMap
}
where
-- Transform the file name into a valid haskell module name.
-- TODO: this is a best-effort transformation; it gives good
-- results on the schema I've found in the wild, but may fail
-- to generate valid/non-overlapping module names in all cases.
mangleFileName "c++.capnp" = "Cxx"
mangleFileName "" = error "Unexpected empty file name"
mangleFileName (c:cs) = go (toUpper c : cs) where
go ('-':c:cs) = toUpper c : go cs
go ".capnp" = ""
go [] = ""
go (c:cs) = c : go cs
-- | Check whether the node's parent scope actually needs a type definition for
-- the node. This is true unless it is a group belonging to a union, which itself
-- has no anonymous union. In that case we just inline the fields, like:
--
-- @@@
-- data MyUnion =
-- MyUnion'variant1
-- { foo :: Bar
-- , baz :: Quux
-- }
-- @@@
--
-- ...and thus don't need an intervening type definition.
neededByParent :: NodeMap -> Node -> Bool
neededByParent nodeMap Node{id,scopeId,union'=Node'struct{isGroup,discriminantCount}} | isGroup =
case nodeMap M.! scopeId of
NodeMetaData{node=Node{union'=Node'struct{fields}}} ->
let me = V.filter
(\case
Field{union'=Field'group{typeId}} -> typeId == id
_ -> False)
fields
in if V.length me /= 1
then error "Invalid schema; group matched multiple fields in its scopeId!"
else not (isUnionField (me V.! 0) && discriminantCount == 0)
_ -> error "Invalid schema; group's scopeId references something that is not a struct!"
neededByParent _ _ = True
generateDecls :: Id -> NodeMap -> NodeMetaData -> [(IR.Name, IR.Decl)]
generateDecls thisModule nodeMap meta@NodeMetaData{..} =
let Node{..} = node
name = identifierFromMetaData moduleId meta
in case union' of
Node'struct{..} | neededByParent nodeMap node ->
let allFields = V.toList fields
(unionFields, commonFields) = partition isUnionField allFields
typeName = name
unionName = IR.subName name ""
structDef = IR.StructDef
{ fields = formatStructBody thisModule nodeMap typeName allFields
, info = if isGroup
then IR.IsGroup
else IR.IsStandalone
{ dataSz = dataWordCount
, ptrSz = pointerCount
}
}
bodyFields = IR.DeclDef (IR.DefStruct structDef)
bodyUnion = IR.DeclDef IR.DefUnion
{ dataVariants =
map (generateVariant thisModule nodeMap name) unionFields
, dataTagLoc = dataLoc
discriminantOffset
(IR.PrimWord IR.PrimInt{isSigned = False, size = 16})
-- The default value for a union tag is always zero:
(Value'uint16 0)
, parentStructName = name
, parentStruct = structDef
}
in case (unionFields, commonFields) of
([], []) ->
-- I(zenhack) don't fully understand this case. It seems like
-- it should apply to struct Foo {}, but it also imperically
-- shows up for type aliases in the schema. In this case, the
-- schema should still build without them since our generated
-- code just uses the original type. Furthermore, right now
-- our ast output doesn't handle types with no variants, so
-- we just don't output any code.
[]
([], _:_) ->
-- There's no anonymous union; just declare the fields.
[ ( typeName, bodyFields ) ]
(_:_, []) ->
-- The struct is just one big anonymous union; expand the variants
-- in-line, rather than making a wrapper.
[ ( typeName, bodyUnion ) ]
(_:_, _:_) ->
-- There are both common fields and an anonymous union. Generate
-- an auxiliary type for the union.
[ ( typeName, bodyFields )
, ( unionName, bodyUnion )
]
Node'enum{..} ->
[ ( name
, IR.DeclDef $ IR.DefEnum $ map
(\Enumerant{name=variantName} ->
IR.subName name variantName
)
(V.toList enumerants)
)
]
Node'const{type_=Type'void,value=Value'void} ->
[(name, IR.DeclConst IR.VoidConst)]
Node'const{type_=Type'bool,value=Value'bool v} ->
[(name, primWordConst IR.PrimBool (fromEnum v))]
Node'const{type_=Type'int8,value=Value'int8 v} ->
[(name, primWordConst IR.PrimInt { isSigned = True, size = 8 } v)]
Node'const{type_=Type'int16,value=Value'int16 v} ->
[(name, primWordConst IR.PrimInt { isSigned = True, size = 16 } v)]
Node'const{type_=Type'int32,value=Value'int32 v} ->
[(name, primWordConst IR.PrimInt { isSigned = True, size = 32 } v)]
Node'const{type_=Type'int64,value=Value'int64 v} ->
[(name, primWordConst IR.PrimInt { isSigned = True, size = 64 } v)]
Node'const{type_=Type'uint8,value=Value'uint8 v} ->
[(name, primWordConst IR.PrimInt { isSigned = False, size = 8 } v)]
Node'const{type_=Type'uint16,value=Value'uint16 v} ->
[(name, primWordConst IR.PrimInt { isSigned = False, size = 16 } v)]
Node'const{type_=Type'uint32,value=Value'uint32 v} ->
[(name, primWordConst IR.PrimInt { isSigned = False, size = 32 } v)]
Node'const{type_=Type'uint64,value=Value'uint64 v} ->
[(name, primWordConst IR.PrimInt { isSigned = False, size = 64 } v)]
Node'const{type_=Type'float32,value=Value'float32 v} ->
[(name, primWordConst IR.PrimFloat32 (floatToWord v))]
Node'const{type_=Type'float64,value=Value'float64 v} ->
[(name, primWordConst IR.PrimFloat64 (doubleToWord v))]
Node'const{type_=Type'text,value=Value'text v} ->
[ ( name
, IR.DeclConst IR.PtrConst
{ ptrType = IR.PrimPtr IR.PrimText
, ptrValue = Just $ Untyped.PtrList $ Untyped.List8 $
encodeUtf8 v
& BS.unpack
& (++ [0])
& V.fromList
}
)
]
Node'const{type_=Type'data_,value=Value'data_ v} ->
[ ( name
, IR.DeclConst IR.PtrConst
{ ptrType = IR.PrimPtr IR.PrimData
, ptrValue = Just $ Untyped.PtrList $ Untyped.List8 $
BS.unpack v
& V.fromList
}
)
]
Node'const{type_=Type'list{elementType},value=Value'list v} ->
[ ( name
, IR.DeclConst IR.PtrConst
{ ptrType = IR.ListOf (formatType thisModule nodeMap elementType)
, ptrValue = v
}
)
]
Node'const{type_=Type'enum{typeId},value=Value'enum v} ->
-- TODO: do something with brand.
[ ( name
, IR.DeclConst IR.WordConst
{ wordValue = fromIntegral v
, wordType = IR.EnumType $ identifierFromMetaData thisModule (nodeMap M.! typeId)
}
)
]
-- TODO: group constants?
Node'const{type_=Type'struct{typeId},value=Value'struct v} ->
[ ( name
, IR.DeclConst IR.PtrConst
{ ptrType = IR.PtrComposite $ IR.StructType
(identifierFromMetaData thisModule (nodeMap M.! typeId))
[]
, ptrValue = v
}
)
]
-- TODO: interface constants
Node'const
{ type_=Type'anyPointer{union'=Type'anyPointer'unconstrained{union'}}
, value=Value'anyPointer v
} ->
[ ( name
, IR.DeclConst IR.PtrConst
{ ptrValue = v
, ptrType = IR.PrimPtr $ IR.PrimAnyPtr $ case union' of
Type'anyPointer'unconstrained'anyKind -> IR.Ptr
Type'anyPointer'unconstrained'struct -> IR.Struct
Type'anyPointer'unconstrained'list -> IR.List
-- TODO: we'll want to restrict this once we actually
-- support interfaces:
Type'anyPointer'unconstrained'capability -> IR.Ptr
Type'anyPointer'unconstrained'unknown' _ -> IR.Ptr
}
)
]
-- TODO: "constrained" anyPointer constants. This has to wait until
-- we support type parameters.
_ -> [] -- TODO
primWordConst :: Integral a => IR.PrimWord -> a -> IR.Decl
primWordConst ty val = IR.DeclConst IR.WordConst
{ wordValue = fromIntegral val
, wordType = IR.PrimWord ty
}
-- | Given the offset field from the capnp schema, a type, and a
-- default value, return a DataLoc describing the location of a field.
dataLoc :: Word32 -> IR.WordType -> Value -> IR.DataLoc
dataLoc offset ty defaultVal =
let bitsOffset = fromIntegral offset * dataFieldSize ty
in IR.DataLoc
{ dataIdx = bitsOffset `div` 64
, dataOff = bitsOffset `mod` 64
, dataDef = valueBits defaultVal
}
-- | Return whether the field is part of a union within its struct.
isUnionField :: Field -> Bool
isUnionField Field{..} = discriminantValue /= field'noDiscriminant
formatStructBody :: Id -> NodeMap -> IR.Name -> [Field] -> [IR.Field]
formatStructBody thisModule nodeMap parentName fields =
let (unionFields, commonFields) = partition isUnionField fields in
map (generateField thisModule nodeMap) commonFields
<> case unionFields of
[] -> [] -- no union
_ ->
[ IR.Field
{ fieldName = "union'"
, fieldLocType =
-- This could use a bit of refactoring. Right now we call
-- formatSturctBody in two cases:
let fieldTypeName = case commonFields of
-- 1. A top-level struct, which also has non anonymous-union
-- fields. In this case, the anonymous union for a struct
-- named Foo will have a type named Foo'; we add an empty
-- segment to the union' field's type name.
_:_ -> IR.subName parentName ""
-- 2. An argument of a named union variant, which itself is
-- a union. In this case, parentName is the name of the outer
-- union's data constructor, and the *inner* union's type
-- constructor; we use parentName unchanged.
[] -> parentName
in IR.HereField $ IR.StructType fieldTypeName []
}
]
-- | Generate a variant of a type corresponding to an anonymous union in a
-- struct.
generateVariant :: Id -> NodeMap -> IR.Name -> Field -> IR.Variant
generateVariant thisModule nodeMap parentName Field{..} = case union' of
Field'slot{..} -> IR.Variant
{ variantName
, variantParams = IR.Unnamed
(formatType thisModule nodeMap type_)
(getFieldLoc thisModule nodeMap union')
, variantTag = discriminantValue
}
Field'group{..} ->
let NodeMetaData{node=node@Node{..},..} = nodeMap M.! typeId
in case union' of
Node'struct{..} -> IR.Variant
{ variantName
, variantParams =
IR.Record $ formatStructBody thisModule nodeMap variantName $ V.toList fields
, variantTag = discriminantValue
}
_ ->
error "A group field referenced a non-struct node."
Field'unknown' _ ->
-- Some sort of field we don't know about (newer version of capnp probably).
-- Generate the variant, but we don't know what the argument type should be,
-- so leave it out.
IR.Variant
{ variantName
, variantParams = IR.Unnamed IR.VoidType IR.VoidField
, variantTag = discriminantValue
}
where
variantName = IR.subName parentName (makeLegalName name)
generateField :: Id -> NodeMap -> Field -> IR.Field
generateField thisModule nodeMap Field{..} =
IR.Field
{ fieldName = makeLegalName name
, fieldLocType = getFieldLoc thisModule nodeMap union'
}
getFieldLoc :: Id -> NodeMap -> Field' -> IR.FieldLocType
getFieldLoc thisModule nodeMap = \case
Field'slot{..} ->
case formatType thisModule nodeMap type_ of
IR.VoidType ->
IR.VoidField
IR.PtrType ty
| hadExplicitDefault -> error $
"Error: capnpc-haskell does not support explicit default " ++
"field values for pointer types. See:\n" ++
"\n" ++
" https://github.com/zenhack/haskell-capnp/issues/28"
| otherwise ->
IR.PtrField (fromIntegral offset) ty
IR.WordType ty ->
IR.DataField
(dataLoc offset ty defaultValue)
ty
IR.CompositeType ty ->
IR.PtrField (fromIntegral offset) (IR.PtrComposite ty)
Field'group{..} ->
IR.HereField $ IR.StructType (identifierFromMetaData thisModule (nodeMap M.! typeId)) []
Field'unknown' _ ->
-- Don't know how to interpret this; we'll have to leave the argument
-- opaque.
IR.VoidField
-- | Return the raw bit-level representation of a value that is stored
-- in a struct's data section.
--
-- Returns 0 for any non-data values. TODO: would be nice to not have
-- an arbitrary default here.
valueBits :: Value -> Word64
valueBits = \case
Value'bool b -> fromIntegral $ fromEnum b
Value'int8 n -> fromIntegral n
Value'int16 n -> fromIntegral n
Value'int32 n -> fromIntegral n
Value'int64 n -> fromIntegral n
Value'uint8 n -> fromIntegral n
Value'uint16 n -> fromIntegral n
Value'uint32 n -> fromIntegral n
Value'uint64 n -> n
Value'float32 n -> fromIntegral $ floatToWord n
Value'float64 n -> doubleToWord n
Value'enum n -> fromIntegral n
_ -> 0 -- some non-word type.
formatType :: Id -> NodeMap -> Type -> IR.Type
formatType thisModule nodeMap ty = case ty of
Type'void -> IR.VoidType
Type'bool -> IR.WordType $ IR.PrimWord IR.PrimBool
Type'int8 -> IR.WordType $ IR.PrimWord IR.PrimInt {isSigned = True, size = 8}
Type'int16 -> IR.WordType $ IR.PrimWord IR.PrimInt {isSigned = True, size = 16}
Type'int32 -> IR.WordType $ IR.PrimWord IR.PrimInt {isSigned = True, size = 32}
Type'int64 -> IR.WordType $ IR.PrimWord IR.PrimInt {isSigned = True, size = 64}
Type'uint8 -> IR.WordType $ IR.PrimWord IR.PrimInt {isSigned = False, size = 8}
Type'uint16 -> IR.WordType $ IR.PrimWord IR.PrimInt {isSigned = False, size = 16}
Type'uint32 -> IR.WordType $ IR.PrimWord IR.PrimInt {isSigned = False, size = 32}
Type'uint64 -> IR.WordType $ IR.PrimWord IR.PrimInt {isSigned = False, size = 64}
Type'float32 -> IR.WordType $ IR.PrimWord IR.PrimFloat32
Type'float64 -> IR.WordType $ IR.PrimWord IR.PrimFloat64
Type'text -> IR.PtrType $ IR.PrimPtr IR.PrimText
Type'data_ -> IR.PtrType $ IR.PrimPtr IR.PrimData
Type'list elt -> IR.PtrType $ IR.ListOf (formatType thisModule nodeMap elt)
-- TODO: use 'brand' to generate type parameters.
Type'enum{..} -> IR.WordType $ IR.EnumType (typeName typeId)
Type'struct{..} -> IR.CompositeType $ IR.StructType (typeName typeId) []
-- TODO: interfaces are not structs; handle them separately.
Type'interface{..} -> IR.CompositeType $ IR.StructType (typeName typeId) []
Type'anyPointer anyPtr -> IR.PtrType $ IR.PrimPtr $ IR.PrimAnyPtr $
case anyPtr of
Type'anyPointer'unconstrained Type'anyPointer'unconstrained'anyKind ->
IR.Ptr
Type'anyPointer'unconstrained Type'anyPointer'unconstrained'struct ->
IR.Struct
Type'anyPointer'unconstrained Type'anyPointer'unconstrained'list ->
IR.List
Type'anyPointer'unconstrained Type'anyPointer'unconstrained'capability ->
IR.Cap
_ ->
-- Something we don't know about; assume it could be anything.
IR.Ptr
_ -> IR.VoidType -- TODO: constrained anyPointers
where
typeName typeId =
identifierFromMetaData thisModule (nodeMap M.! typeId)
generateImport :: CodeGeneratorRequest'RequestedFile'Import -> IR.Import
generateImport CodeGeneratorRequest'RequestedFile'Import{..} =
IR.Import (IR.ByCapnpId id)
cgrToIR :: CodeGeneratorRequest -> [IR.Module]
cgrToIR cgr@CodeGeneratorRequest{requestedFiles} =
map (generateModule $ makeNodeMap cgr) $ V.toList requestedFiles