Files
haskell-wasm/src/Language/Wasm/Validate.hs
T
2018-02-25 12:28:51 -08:00

489 lines
18 KiB
Haskell

{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleInstances #-}
module Language.Wasm.Validate (
ValidationResult(..),
validate,
isValid
) where
import Language.Wasm.Structure
import qualified Data.Set as Set
import Data.List (foldl')
import qualified Data.Text.Lazy as TL
import Data.Maybe (fromMaybe, maybeToList, catMaybes, isNothing)
import Data.Monoid ((<>))
import Numeric.Natural (Natural)
import Control.Monad.State.Lazy (StateT, evalStateT, get, put)
import Control.Monad.Reader (ReaderT, runReaderT, withReaderT, ask)
import Control.Monad.Except (Except, runExcept, throwError)
import Debug.Trace as Debug
data ValidationResult =
DuplicatedExportNames [String]
| InvalidTableType
| MoreThanOneMemory
| MoreThanOneTable
| IndexOutOfRange
| ResultTypeDoesntMatch
| NoTableInModule
| NoMemoryInModule
| TypeMismatch
| Valid
deriving (Show, Eq)
instance Monoid ValidationResult where
mempty = Valid
mappend Valid vr = vr
mappend vr Valid = vr
mappend vr _ = vr
isValid :: ValidationResult -> Bool
isValid Valid = True
isValid reason = Debug.trace ("Module mismatched with reason " ++ show reason) $ False
type Validator = Module -> ValidationResult
data VType =
Val ValueType
| Var Int
| Any
deriving (Show, Eq)
type End = [VType]
empty :: [ValueType]
empty = []
class ToEnd a where
toEnd :: a -> [VType]
instance ToEnd VType where
toEnd val = [val]
instance ToEnd ValueType where
toEnd val = [Val val]
instance ToEnd [ValueType] where
toEnd = map Val
instance ToEnd [VType] where
toEnd = id
data Arrow = Arrow End End deriving (Show, Eq)
(==>) :: (ToEnd a, ToEnd b) => a -> b -> Arrow
(==>) a b = Arrow (toEnd a) (toEnd b)
asArrow :: FuncType -> Arrow
asArrow (FuncType params results) = Arrow (map Val params) (map Val results)
isArrowMatch :: Arrow -> Arrow -> Bool
isArrowMatch (f `Arrow` t) ( f' `Arrow` t') = isEndMatch f f' && isEndMatch t t'
where
isEndMatch :: End -> End -> Bool
isEndMatch (Any:l) (Any:r) =
let (leftTail, rightTail) = unzip $ zip (takeWhile (/= Any) $ reverse l) (takeWhile (/= Any) $ reverse r) in
isEndMatch (reverse leftTail) (reverse rightTail)
isEndMatch (Any:l) r =
let (leftTail, rightTail) = unzip $ zip (takeWhile (/= Any) $ reverse l) (takeWhile (/= Any) $ reverse r) in
isEndMatch (reverse leftTail) (reverse rightTail)
isEndMatch l (Any:r) =
let (leftTail, rightTail) = unzip $ zip (takeWhile (/= Any) $ reverse l) (takeWhile (/= Any) $ reverse r) in
isEndMatch (reverse leftTail) (reverse rightTail)
isEndMatch (Var v:l) (x:r) =
let subst = replace (Var v) x in
isEndMatch (subst l) (subst r)
isEndMatch (x:l) (Var v:r) =
let subst = replace (Var v) x in
isEndMatch (subst l) (subst r)
isEndMatch (Val v:l) (Val v':r) = v == v' && isEndMatch l r
isEndMatch [] [] = True
isEndMatch _ _ = False
data Ctx = Ctx {
types :: [FuncType],
funcs :: [FuncType],
tables :: [TableType],
mems :: [Limit],
globals :: [GlobalType],
locals :: [ValueType],
labels :: [Maybe ValueType],
returns :: Maybe ValueType
} deriving (Show, Eq)
type Checker = ReaderT Ctx (StateT Int (Except ValidationResult))
freshVar :: Checker VType
freshVar = do
i <- get
put (i + 1)
return $ Var i
runChecker :: Ctx -> Checker a -> Either ValidationResult a
runChecker ctx = runExcept . flip evalStateT 0 . flip runReaderT ctx
(!?) :: [a] -> Natural -> Maybe a
(!?) (x:_) 0 = Just x
(!?) (_:rest) n = rest !? (n - 1)
(!?) [] _ = Nothing
safeHead :: [a] -> Maybe a
safeHead (x: _) = Just x
safeHead [] = Nothing
maybeToEither :: ValidationResult -> Maybe a -> Checker a
maybeToEither _ (Just a) = return a
maybeToEither l Nothing = throwError l
asType :: GlobalType -> VType
asType (Const v) = Val v
asType (Mut v) = Val v
getLabel :: LabelIndex -> Checker (Maybe ValueType)
getLabel lbl = do
Ctx { labels } <- ask
case labels !? lbl of
Nothing -> throwError IndexOutOfRange
Just v -> return v
withLabel :: [ValueType] -> Checker a -> Checker a
withLabel result = withReaderT (\ctx -> ctx { labels = safeHead result : labels ctx })
getInstrType :: Instruction -> Checker Arrow
getInstrType Unreachable = return $ Any ==> Any
getInstrType Nop = return $ empty ==> empty
getInstrType Block { result, body } = do
let blockType = empty ==> result
t <- withLabel result $ getExpressionType body
if isArrowMatch t blockType
then return $ empty ==> result
else throwError TypeMismatch
getInstrType Loop { result, body } = do
let blockType = empty ==> result
t <- withLabel result $ getExpressionType body
if isArrowMatch t blockType
then return $ empty ==> result
else throwError TypeMismatch
getInstrType If { result, true, false } = do
let blockType = empty ==> result
l <- withLabel result $ getExpressionType true
r <- withLabel result $ getExpressionType false
if isArrowMatch l blockType && isArrowMatch r blockType
then return $ I32 ==> result
else throwError TypeMismatch
getInstrType (Br lbl) = do
r <- map Val . maybeToList <$> getLabel lbl
return $ (Any : r) ==> Any
getInstrType (BrIf lbl) = do
r <- map Val . maybeToList <$> getLabel lbl
return $ (r ++ [Val I32]) ==> r
getInstrType (BrTable lbls lbl) = do
r <- getLabel lbl
rs <- mapM getLabel lbls
-- this check for equality doesn't match the spec,
-- but a reference compiler does the same
if all (\r' -> r' == r || isNothing r' || isNothing r) rs
then return $ ([Any] ++ (map Val $ maybeToList r) ++ [Val I32]) ==> Any
else throwError ResultTypeDoesntMatch
getInstrType Return = do
Ctx { returns } <- ask
return $ (Any : (map Val $ maybeToList returns)) ==> Any
getInstrType (Call fun) = do
Ctx { funcs } <- ask
maybeToEither IndexOutOfRange $ asArrow <$> funcs !? fun
getInstrType (CallIndirect sign) = do
Ctx { types, tables } <- ask
if length tables < 1
then throwError NoTableInModule
else do
Arrow from to <- maybeToEither IndexOutOfRange $ asArrow <$> types !? sign
return $ (from ++ [Val I32]) ==> to
getInstrType Drop = do
var <- freshVar
return $ var ==> empty
getInstrType Select = do
var <- freshVar
return $ [var, var, Val I32] ==> var
getInstrType (GetLocal local) = do
Ctx { locals } <- ask
t <- maybeToEither IndexOutOfRange $ locals !? local
return $ empty ==> Val t
getInstrType (SetLocal local) = do
Ctx { locals } <- ask
t <- maybeToEither IndexOutOfRange $ locals !? local
return $ Val t ==> empty
getInstrType (TeeLocal local) = do
Ctx { locals } <- ask
t <- maybeToEither IndexOutOfRange $ locals !? local
return $ Val t ==> Val t
getInstrType (GetGlobal global) = do
Ctx { globals } <- ask
t <- maybeToEither IndexOutOfRange $ asType <$> globals !? global
return $ empty ==> t
getInstrType (SetGlobal global) = do
Ctx { globals } <- ask
t <- maybeToEither IndexOutOfRange $ asType <$> globals !? global
return $ t ==> empty
-- TODO: check memory alignment
getInstrType (I32Load _) = do
Ctx { mems } <- ask
if length mems < 1 then throwError NoMemoryInModule else return $ I32 ==> I32
getInstrType (I64Load _) = do
Ctx { mems } <- ask
if length mems < 1 then throwError NoMemoryInModule else return $ I32 ==> I64
getInstrType (F32Load _) = do
Ctx { mems } <- ask
if length mems < 1 then throwError NoMemoryInModule else return $ I32 ==> F32
getInstrType (F64Load _) = do
Ctx { mems } <- ask
if length mems < 1 then throwError NoMemoryInModule else return $ I32 ==> F64
getInstrType (I32Load8S _) = do
Ctx { mems } <- ask
if length mems < 1 then throwError NoMemoryInModule else return $ I32 ==> I32
getInstrType (I32Load8U _) = do
Ctx { mems } <- ask
if length mems < 1 then throwError NoMemoryInModule else return $ I32 ==> I32
getInstrType (I32Load16S _) = do
Ctx { mems } <- ask
if length mems < 1 then throwError NoMemoryInModule else return $ I32 ==> I32
getInstrType (I32Load16U _) = do
Ctx { mems } <- ask
if length mems < 1 then throwError NoMemoryInModule else return $ I32 ==> I32
getInstrType (I64Load8S _) = do
Ctx { mems } <- ask
if length mems < 1 then throwError NoMemoryInModule else return $ I32 ==> I64
getInstrType (I64Load8U _) = do
Ctx { mems } <- ask
if length mems < 1 then throwError NoMemoryInModule else return $ I32 ==> I64
getInstrType (I64Load16S _) = do
Ctx { mems } <- ask
if length mems < 1 then throwError NoMemoryInModule else return $ I32 ==> I64
getInstrType (I64Load16U _) = do
Ctx { mems } <- ask
if length mems < 1 then throwError NoMemoryInModule else return $ I32 ==> I64
getInstrType (I64Load32S _) = do
Ctx { mems } <- ask
if length mems < 1 then throwError NoMemoryInModule else return $ I32 ==> I64
getInstrType (I64Load32U _) = do
Ctx { mems } <- ask
if length mems < 1 then throwError NoMemoryInModule else return $ I32 ==> I64
getInstrType (I32Store _) = do
Ctx { mems } <- ask
if length mems < 1 then throwError NoMemoryInModule else return $ [I32, I32] ==> empty
getInstrType (I64Store _) = do
Ctx { mems } <- ask
if length mems < 1 then throwError NoMemoryInModule else return $ [I32, I64] ==> empty
getInstrType (F32Store _) = do
Ctx { mems } <- ask
if length mems < 1 then throwError NoMemoryInModule else return $ [I32, F32] ==> empty
getInstrType (F64Store _) = do
Ctx { mems } <- ask
if length mems < 1 then throwError NoMemoryInModule else return $ [I32, F64] ==> empty
getInstrType (I32Store8 _) = do
Ctx { mems } <- ask
if length mems < 1 then throwError NoMemoryInModule else return $ [I32, I32] ==> empty
getInstrType (I32Store16 _) = do
Ctx { mems } <- ask
if length mems < 1 then throwError NoMemoryInModule else return $ [I32, I32] ==> empty
getInstrType (I64Store8 _) = do
Ctx { mems } <- ask
if length mems < 1 then throwError NoMemoryInModule else return $ [I32, I64] ==> empty
getInstrType (I64Store16 _) = do
Ctx { mems } <- ask
if length mems < 1 then throwError NoMemoryInModule else return $ [I32, I64] ==> empty
getInstrType (I64Store32 _) = do
Ctx { mems } <- ask
if length mems < 1 then throwError NoMemoryInModule else return $ [I32, I64] ==> empty
getInstrType CurrentMemory = do
Ctx { mems } <- ask
if length mems < 1 then throwError NoMemoryInModule else return $ empty ==> I32
getInstrType GrowMemory = do
Ctx { mems } <- ask
if length mems < 1 then throwError NoMemoryInModule else return $ I32 ==> I32
getInstrType (I32Const _) = return $ empty ==> I32
getInstrType (I64Const _) = return $ empty ==> I64
getInstrType (F32Const _) = return $ empty ==> F32
getInstrType (F64Const _) = return $ empty ==> F64
getInstrType (IUnOp BS32 _) = return $ I32 ==> I32
getInstrType (IUnOp BS64 _) = return $ I64 ==> I64
getInstrType (IBinOp BS32 _) = return $ [I32, I32] ==> I32
getInstrType (IBinOp BS64 _) = return $ [I64, I64] ==> I64
getInstrType I32Eqz = return $ I32 ==> I32
getInstrType I64Eqz = return $ I64 ==> I32
getInstrType (IRelOp BS32 _) = return $ [I32, I32] ==> I32
getInstrType (IRelOp BS64 _) = return $ [I64, I64] ==> I32
getInstrType (FUnOp BS32 _) = return $ F32 ==> F32
getInstrType (FUnOp BS64 _) = return $ F64 ==> F64
getInstrType (FBinOp BS32 _) = return $ [F32, F32] ==> F32
getInstrType (FBinOp BS64 _) = return $ [F64, F64] ==> F64
getInstrType (FRelOp BS32 _) = return $ [F32, F32] ==> I32
getInstrType (FRelOp BS64 _) = return $ [F64, F64] ==> I32
getInstrType I32WrapI64 = return $ I64 ==> I32
getInstrType (ITruncFU BS32 BS32) = return $ F32 ==> I32
getInstrType (ITruncFU BS32 BS64) = return $ F64 ==> I32
getInstrType (ITruncFU BS64 BS32) = return $ F32 ==> I64
getInstrType (ITruncFU BS64 BS64) = return $ F64 ==> I64
getInstrType (ITruncFS BS32 BS32) = return $ F32 ==> I32
getInstrType (ITruncFS BS32 BS64) = return $ F64 ==> I32
getInstrType (ITruncFS BS64 BS32) = return $ F32 ==> I64
getInstrType (ITruncFS BS64 BS64) = return $ F64 ==> I64
getInstrType I64ExtendSI32 = return $ I32 ==> I64
getInstrType I64ExtendUI32 = return $ I32 ==> I64
getInstrType (FConvertIU BS32 BS32) = return $ I32 ==> F32
getInstrType (FConvertIU BS32 BS64) = return $ I64 ==> F32
getInstrType (FConvertIU BS64 BS32) = return $ I32 ==> F64
getInstrType (FConvertIU BS64 BS64) = return $ I64 ==> F64
getInstrType (FConvertIS BS32 BS32) = return $ I32 ==> F32
getInstrType (FConvertIS BS32 BS64) = return $ I64 ==> F32
getInstrType (FConvertIS BS64 BS32) = return $ I32 ==> F64
getInstrType (FConvertIS BS64 BS64) = return $ I64 ==> F64
getInstrType F32DemoteF64 = return $ F64 ==> F32
getInstrType F64PromoteF32 = return $ F32 ==> F64
getInstrType (IReinterpretF BS32) = return $ F32 ==> I32
getInstrType (IReinterpretF BS64) = return $ F64 ==> I64
getInstrType (FReinterpretI BS32) = return $ I32 ==> F32
getInstrType (FReinterpretI BS64) = return $ I64 ==> F64
replace :: (Eq a) => a -> a -> [a] -> [a]
replace _ _ [] = []
replace x y (v:r) = (if x == v then y else v) : replace x y r
unify :: Arrow -> Arrow -> Checker Arrow
unify (f `Arrow` t) (f' `Arrow` t') = unify' (f `Arrow` reverse t) (reverse f' `Arrow` t')
where
unify' :: Arrow -> Arrow -> Checker Arrow
unify' (f `Arrow` []) (f' `Arrow` t') =
return $ (reverse f' ++ f) `Arrow` t'
unify' (f `Arrow` t) ([] `Arrow` t') =
return $ f `Arrow` (reverse t ++ t')
unify' (f `Arrow` (Val v':t)) ((Val v:f') `Arrow` t') =
if v == v'
then unify' (f `Arrow` t) (f' `Arrow` t')
else Debug.trace ("type err " ++ show v' ++ " - " ++ show v) $ throwError TypeMismatch
unify' (f `Arrow` (Var r:t)) ((Val v:f') `Arrow` t') =
let subst = replace (Var r) (Val v) in
unify' (subst f `Arrow` subst t) (f' `Arrow` t')
unify' (f `Arrow` (Val v:t)) ((Var r:f') `Arrow` t') =
let subst = replace (Var r) (Val v) in
unify' (f `Arrow` t) (subst f' `Arrow` subst t')
unify' (f `Arrow` (Var r:t)) ((Var r':f') `Arrow` t') =
let subst = replace (Var r') (Var r) in
unify' (f `Arrow` t) (subst f' `Arrow` subst t')
unify' (f `Arrow` (Any:_)) (_ `Arrow` t') =
return $ f `Arrow` (Any : t')
unify' (f `Arrow` _) (f'@(Any:_) `Arrow` t') =
return $ (f' ++ f) `Arrow` t'
getExpressionType :: [Instruction] -> Checker Arrow
getExpressionType instrs =
case reverse instrs of
[] -> return $ Any ==> Any
(i:rest) -> do
arr <- getInstrType i
go arr rest
where
go :: Arrow -> [Instruction] -> Checker Arrow
go arr [] = return arr
go arr (i:rest) = do
a <- getInstrType i
arr' <- unify a arr
go arr' rest
ctxFromModule :: [ValueType] -> [Maybe ValueType] -> Maybe ValueType -> Module -> Ctx
ctxFromModule locals labels returns Module {types, functions, tables, mems, globals, imports} =
let funImports = catMaybes $ map getFuncType imports in
let tableImports = catMaybes $ map getTableType imports in
let memsImports = catMaybes $ map getMemType imports in
let globalImports = catMaybes $ map getGlobalType imports in
Ctx {
types,
funcs = funImports ++ map ((types !!) . fromIntegral . funcType) functions,
tables = tableImports ++ map (\(Table t) -> t) tables,
mems = memsImports ++ map (\(Memory l) -> l) mems,
globals = globalImports ++ map (\(Global g _) -> g) globals,
locals,
labels,
returns
}
where
getFuncType (Import _ _ (ImportFunc typeIdx)) = Just $ types !! (fromIntegral typeIdx)
getFuncType _ = Nothing
getTableType (Import _ _ (ImportTable tableType)) = Just tableType
getTableType _ = Nothing
getMemType (Import _ _ (ImportMemory lim)) = Just lim
getMemType _ = Nothing
getGlobalType (Import _ _ (ImportGlobal gl)) = Just gl
getGlobalType _ = Nothing
isFunctionValid :: Function -> Validator
isFunctionValid Function {funcType, locals, body} mod@Module {types} =
let FuncType params results = types !! fromIntegral funcType in
let r = safeHead results in
let ctx = ctxFromModule (params ++ locals) [r] r mod in
case runChecker ctx $ getExpressionType body of
Left err -> err
Right arr ->
if isArrowMatch arr (empty ==> results)
then Valid
else Debug.trace ("fun err " ++ show arr ++ " - " ++ show (empty ==> results)) $ TypeMismatch
functionsShouldBeValid :: Validator
functionsShouldBeValid mod@Module {functions} =
foldMap (flip isFunctionValid mod) functions
tablesShouldBeValid :: Validator
tablesShouldBeValid Module { imports, tables } =
let tableImports = filter isTableImport imports in
let res = foldMap (\Import { desc = ImportTable t } -> isValidTableType t) tableImports in
let res' = foldl' (\r (Table t) -> r <> isValidTableType t) res tables in
if length tableImports + length tables <= 1
then res'
else MoreThanOneTable
where
isValidTableType :: TableType -> ValidationResult
isValidTableType (TableType (Limit min max) _) = if min <= fromMaybe min max then Valid else InvalidTableType
isTableImport Import { desc = ImportTable _ } = True
isTableImport _ = False
shouldBeAtMostOneMemory :: Validator
shouldBeAtMostOneMemory Module { imports, mems } =
let memImports = filter isMemImport imports in
if length memImports + length mems <= 1
then Valid
else MoreThanOneMemory
where
isMemImport Import { desc = ImportMemory _ } = True
isMemImport _ = False
exportNamesShouldBeDifferent :: Validator
exportNamesShouldBeDifferent Module { exports } =
case foldl' go (Set.empty, []) exports of
(_, []) -> Valid
(_, dup) -> DuplicatedExportNames dup
where
go :: (Set.Set TL.Text, [String]) -> Export -> (Set.Set TL.Text, [String])
go (set, dup) (Export name _) =
if Set.member name set
then (set, show name : dup)
else (Set.insert name set, dup)
validate :: Validator
validate mod = foldMap ($ mod) validators
where
validators :: [Validator]
validators = [
tablesShouldBeValid,
shouldBeAtMostOneMemory,
exportNamesShouldBeDifferent,
functionsShouldBeValid
]