packages feed

hslua 1.1.0 → 1.1.1

raw patch · 20 files changed

+1017/−335 lines, 20 filesdep −criteriondep −deepseqdep ~basedep ~containersPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies removed: criterion, deepseq

Dependency ranges changed: base, containers

API changes (from Hackage documentation)

+ Foreign.Lua.Peek: PeekError :: NonEmpty Text -> PeekError
+ Foreign.Lua.Peek: [fromPeekError] :: PeekError -> NonEmpty Text
+ Foreign.Lua.Peek: errorMsg :: Text -> PeekError
+ Foreign.Lua.Peek: force :: Either PeekError a -> Lua a
+ Foreign.Lua.Peek: instance GHC.Classes.Eq Foreign.Lua.Peek.PeekError
+ Foreign.Lua.Peek: instance GHC.Show.Show Foreign.Lua.Peek.PeekError
+ Foreign.Lua.Peek: newtype PeekError
+ Foreign.Lua.Peek: peekBool :: Peeker Bool
+ Foreign.Lua.Peek: peekByteString :: Peeker ByteString
+ Foreign.Lua.Peek: peekIntegral :: (Integral a, Read a) => Peeker a
+ Foreign.Lua.Peek: peekKeyValuePairs :: Peeker a -> Peeker b -> Peeker [(a, b)]
+ Foreign.Lua.Peek: peekLazyByteString :: Peeker ByteString
+ Foreign.Lua.Peek: peekList :: Peeker a -> Peeker [a]
+ Foreign.Lua.Peek: peekMap :: Ord a => Peeker a -> Peeker b -> Peeker (Map a b)
+ Foreign.Lua.Peek: peekRealFloat :: (RealFloat a, Read a) => Peeker a
+ Foreign.Lua.Peek: peekSet :: Ord a => Peeker a -> Peeker (Set a)
+ Foreign.Lua.Peek: peekString :: Peeker String
+ Foreign.Lua.Peek: peekText :: Peeker Text
+ Foreign.Lua.Peek: toPeeker :: (StackIndex -> Lua a) -> Peeker a
+ Foreign.Lua.Peek: type Peeker a = StackIndex -> Lua (Either PeekError a)
+ Foreign.Lua.Push: pushBool :: Pusher Bool
+ Foreign.Lua.Push: pushByteString :: Pusher ByteString
+ Foreign.Lua.Push: pushIntegral :: (Integral a, Show a) => a -> Lua ()
+ Foreign.Lua.Push: pushLazyByteString :: Pusher ByteString
+ Foreign.Lua.Push: pushList :: Pusher a -> [a] -> Lua ()
+ Foreign.Lua.Push: pushMap :: Pusher a -> Pusher b -> Pusher (Map a b)
+ Foreign.Lua.Push: pushRealFloat :: RealFloat a => a -> Lua ()
+ Foreign.Lua.Push: pushSet :: Pusher a -> Pusher (Set a)
+ Foreign.Lua.Push: pushString :: String -> Lua ()
+ Foreign.Lua.Push: pushText :: Pusher Text
+ Foreign.Lua.Push: type Pusher a = a -> Lua ()
- Foreign.Lua: pushList :: Pushable a => [a] -> Lua ()
+ Foreign.Lua: pushList :: Pusher a -> [a] -> Lua ()
- Foreign.Lua.Types.Pushable: pushList :: Pushable a => [a] -> Lua ()
+ Foreign.Lua.Types.Pushable: pushList :: Pusher a -> [a] -> Lua ()

Files

CHANGELOG.md view
@@ -1,5 +1,33 @@ ## Changelog +### 1.1.1++Released 2020-06-02++- New module Foreign.Lua.Push: provides functions which marshal+  and push Haskell values onto Lua's stack.++  Most functions in Foreign.Lua.Types.Pushable are now defined+  using functions from this module.++- New module Foreign.Lua.Peek: provides functions which unmarshal+  and retrieve Haskell values from Lua's stack. Contrary to `peek`+  from Foreign.Lua.Types.Peekable, the peeker functions in this+  module will never throw errors, but use an `Either` type to+  signal retrieval failure.++  The error type `PeekError` should not be considered final and+  will likely be subject to change in later versions.++- Module Foreign.Lua.Utf8: never throw errors when decoding UTF-8+  strings. Invalid UTF-8 input bytes no longer cause exceptions,+  but are replaced with the Unicode replacement character U+FFFD.++- Fixed missing and faulty Haddock documentation.++- Fixed a bug which caused unnecessary use of strings to represent+  floating point numbers under certain configurations.+ ### 1.1.0  Released 2020-03-25.
− benchmark/benchmark-functions.c
@@ -1,40 +0,0 @@-#include <string.h>-#include "benchmark-functions.h"--/*-** getlfield-*/-int hslua__getlfield(lua_State *L)-{-  lua_gettable(L, 1);-  return 1;-}--int hslua_getlfield(lua_State *L, int index, const char *k, size_t len)-{-  lua_pushvalue(L, index);-  lua_pushlstring(L, k, len);-  lua_pushcfunction(L, hslua__getlfield);-  lua_insert(L, -3);-  return -lua_pcall(L, 2, 1, 0);-}--/*-** setfield-*/-int hslua__setfield(lua_State *L)-{-  const char *k = lua_tostring(L, 3);-  lua_pushvalue(L, 1);-  lua_setfield(L, 2, k);-  return 0;-}--int hslua_setfield(lua_State *L, int index, const char *k)-{-  lua_pushvalue(L, index);-  lua_pushstring(L, k);-  lua_pushcfunction(L, hslua__setfield);-  lua_insert(L, -4);-  return -lua_pcall(L, 3, 0, 0);-}
− benchmark/benchmark-functions.h
@@ -1,5 +0,0 @@-#include "lua.h"--int hslua_getlfield(lua_State *L, int index, const char *k, size_t len);--int hslua_setfield(lua_State *L, int index, const char *k);
− benchmark/benchmark-hslua.hsc
@@ -1,103 +0,0 @@-{--Copyright © 2018-2020 Albert Krewinkel--Permission is hereby granted, free of charge, to any person obtaining a copy-of this software and associated documentation files (the "Software"), to deal-in the Software without restriction, including without limitation the rights-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell-copies of the Software, and to permit persons to whom the Software is-furnished to do so, subject to the following conditions:--The above copyright notice and this permission notice shall be included in-all copies or substantial portions of the Software.--THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN-THE SOFTWARE.--}-{-# LANGUAGE ForeignFunctionInterface #-}-{-# LANGUAGE OverloadedStrings #-}--import Control.DeepSeq-import Control.Monad (void)-import Criterion.Main-import Criterion.Types (Config(..))-import Data.ByteString (ByteString)-import Foreign.C (CString (..), CSize (..), CInt (..))-import Foreign.Lua (Lua, StackIndex)--import qualified Foreign.Lua as Lua-import qualified Data.ByteString as B-import qualified Data.ByteString.Unsafe as B--#include "benchmark-functions.h"--luaBench :: NFData b-         => String-         -> Lua a    -- ^ Setup-         -> Lua b    -- ^ Operation to benchmark-         -> Benchmark-luaBench name setupOp benchOp = do-  bench name .-    perRunEnvWithCleanup (setupLua setupOp) teardownLua $ \l ->-      Lua.runWith l benchOp--setupLua :: Lua a -> IO Lua.State-setupLua setupOp = do-  l <- Lua.newstate-  _ <- Lua.runWith l setupOp-  return l--teardownLua :: Lua.State -> IO ()-teardownLua = Lua.close--setupTableWithFooField :: Lua ()-setupTableWithFooField = do-  Lua.newtable-  Lua.pushstring "foo"-  Lua.setfield (Lua.nthFromTop 2) "bar"--main :: IO ()-main = defaultMain-  [ luaBench "getfield" setupTableWithFooField (Lua.getfield Lua.stackTop "foo")-  , luaBench "getlfield" setupTableWithFooField (getlfield Lua.stackTop "foo")-  , luaBench "setfield"-             (Lua.newtable *> Lua.pushboolean True)-             (Lua.setfield (Lua.nthFromTop 2) "foo")-  , luaBench "setfield_old"-             (Lua.newtable *> Lua.pushboolean True)-             (setfield_old (Lua.nthFromTop 2) "foo")-  , luaBench "getglobal" (return ()) (Lua.getglobal "foo")-  , luaBench "setglobal" (Lua.pushboolean True) (Lua.setglobal "foo")-  , luaBench "setraw"-             (Lua.newtable *> Lua.pushstring "foo" *> Lua.pushboolean True)-             (Lua.rawset (Lua.nthFromTop 3))-  ]--instance NFData Lua.State----- Functions for comparison---- | Getting a string field with lua_pushlstring and lua_gettable-foreign import ccall "hslua_getlfield"-  hslua_getlfield :: Lua.State -> StackIndex -> CString -> CSize -> IO CInt--getlfield :: StackIndex -> ByteString -> Lua CInt-getlfield i s = do-  l <- Lua.state-  Lua.liftIO $ B.unsafeUseAsCStringLen s $ \(strPtr, len) ->-    hslua_getlfield l i strPtr (fromIntegral len)---- | Getting a string field with lua_pushlstring and lua_gettable-foreign import ccall "hslua_setfield"-  hslua_setfield :: Lua.State -> StackIndex -> CString -> IO CInt--setfield_old :: StackIndex -> ByteString -> Lua CInt-setfield_old i s = do-  l <- Lua.state-  Lua.liftIO $ B.useAsCString s (hslua_setfield l i)
hslua.cabal view
@@ -1,5 +1,5 @@ name:                hslua-version:             1.1.0+version:             1.1.1 synopsis:            Bindings to Lua, an embeddable scripting language description:         HsLua provides bindings, wrappers, types, and helper                      functions to bridge Haskell and <https://www.lua.org/ Lua>.@@ -23,14 +23,11 @@ build-type:          Simple extra-source-files:  cbits/lua-5.3.5/*.h                      cbits/error-conversion/*.h-                     benchmark/benchmark-functions.h-                     benchmark/benchmark-functions.c                      README.md                      CHANGELOG.md                      test/lua/*.lua cabal-version:       >=1.10-tested-with:         GHC == 7.10.3-                   , GHC == 8.0.2+tested-with:         GHC == 8.0.2                    , GHC == 8.2.2                    , GHC == 8.4.3                    , GHC == 8.6.5@@ -95,6 +92,8 @@                      , Foreign.Lua.Core.Types                      , Foreign.Lua.FunctionCalling                      , Foreign.Lua.Module+                     , Foreign.Lua.Peek+                     , Foreign.Lua.Push                      , Foreign.Lua.Types                      , Foreign.Lua.Types.Peekable                      , Foreign.Lua.Types.Pushable@@ -213,6 +212,8 @@                      , Foreign.Lua.Core.ErrorTests                      , Foreign.Lua.FunctionCallingTests                      , Foreign.Lua.ModuleTests+                     , Foreign.Lua.PeekTests+                     , Foreign.Lua.PushTests                      , Foreign.Lua.TypesTests                      , Foreign.Lua.Types.PeekableTests                      , Foreign.Lua.Types.PushableTests@@ -222,7 +223,7 @@                      , Test.HsLua.Util   build-depends:       base                 >= 4.8    && < 5                      , bytestring           >= 0.10.2 && < 0.11-                     , containers           >= 0.5    && < 0.7+                     , containers           >= 0.5.9  && < 0.7                      , exceptions           >= 0.8    && < 0.11                      , mtl                  >= 2.2    && < 2.3                      , text                 >= 1.0    && < 1.3@@ -241,18 +242,3 @@     build-depends:       base-compat          >= 0.10     hs-source-dirs:      prelude     other-modules:       Prelude---benchmark benchmark-hslua-  type:                exitcode-stdio-1.0-  main-is:             benchmark-hslua.hs-  hs-source-dirs:      benchmark-  default-language:    Haskell2010-  build-depends:       hslua-                     , base                 >= 4.8-                     , bytestring           >= 0.10.2 && < 0.11-                     , criterion            >= 1.0    && < 1.6-                     , deepseq              >= 1.4.1  && < 1.5-  c-sources:           benchmark/benchmark-functions.c-  include-dirs:        benchmark-                     , cbits/lua-5.3.5
src/Foreign/Lua/Core/Auxiliary.hsc view
@@ -90,8 +90,8 @@  -- | Loads and runs the given string. ----- Returns @'OK'@ on success, or an error if either loading of the string or--- calling of the thunk failed.+-- Returns 'Lua.OK' on success, or an error if either loading of the+-- string or calling of the thunk failed. dostring :: ByteString -> Lua Status dostring s = do   loadRes <- loadstring s@@ -158,9 +158,9 @@  -- | Loads a ByteString as a Lua chunk. ----- This function returns the same results as @'load'@. @name@ is the chunk name,--- used for debug information and error messages. Note that @name@ is used as a--- C string, so it may not contain null-bytes.+-- This function returns the same results as @'Lua.load'@. @name@ is the+-- chunk name, used for debug information and error messages. Note that+-- @name@ is used as a C string, so it may not contain null-bytes. -- -- See <https://www.lua.org/manual/5.3/manual.html#luaL_loadbuffer luaL_loadbuffer>. loadbuffer :: ByteString -- ^ Program to load@@ -176,17 +176,18 @@                   -> IO Lua.StatusCode  --- | Loads a file as a Lua chunk. This function uses @lua_load@ (see @'load'@)--- to load the chunk in the file named filename. The first line in the file is--- ignored if it starts with a @#@.+-- | Loads a file as a Lua chunk. This function uses @lua_load@ (see+-- @'Lua.load'@) to load the chunk in the file named filename. The first+-- line in the file is ignored if it starts with a @#@. ----- The string mode works as in function @'load'@.+-- The string mode works as in function @'Lua.load'@. ----- This function returns the same results as @'load'@, but it has an extra error--- code @'ErrFile'@ for file-related errors (e.g., it cannot open or read the--- file).+-- This function returns the same results as @'Lua.load'@, but it has an+-- extra error code @'Lua.ErrFile'@ for file-related errors (e.g., it+-- cannot open or read the file). ----- As @'load'@, this function only loads the chunk; it does not run it.+-- As @'Lua.load'@, this function only loads the chunk; it does not run+-- it. -- -- Note that the file is opened by Haskell, not Lua. --@@ -203,13 +204,15 @@   contentOrError = try (B.readFile fp)  --- | Loads a string as a Lua chunk. This function uses @lua_load@ to load the--- chunk in the given ByteString. The given string may not contain any NUL--- characters.+-- | Loads a string as a Lua chunk. This function uses @lua_load@ to+-- load the chunk in the given ByteString. The given string may not+-- contain any NUL characters. ----- This function returns the same results as @lua_load@ (see @'load'@).+-- This function returns the same results as @lua_load@ (see+-- @'Lua.load'@). ----- Also as @'load'@, this function only loads the chunk; it does not run it.+-- Also as @'Lua.load'@, this function only loads the chunk; it does not+-- run it. -- -- See <https://www.lua.org/manual/5.3/manual.html#luaL_loadstring luaL_loadstring>. loadstring :: ByteString -> Lua Status@@ -238,11 +241,11 @@   luaL_newmetatable :: Lua.State -> CString -> IO Lua.LuaBool  --- | Creates a new Lua state. It calls @'lua_newstate'@ with an allocator based--- on the standard C @realloc@ function and then sets a panic function (see--- <https://www.lua.org/manual/5.3/manual.html#4.6 §4.6> of the Lua 5.3--- Reference Manual) that prints an error message to the standard error output--- in case of fatal errors.+-- | Creates a new Lua state. It calls @lua_newstate@ with an allocator+-- based on the standard C @realloc@ function and then sets a panic+-- function (see <https://www.lua.org/manual/5.3/manual.html#4.6 §4.6>+-- of the Lua 5.3 Reference Manual) that prints an error message to the+-- standard error output in case of fatal errors. -- -- See also: -- <https://www.lua.org/manual/5.3/manual.html#luaL_newstate luaL_newstate>.@@ -267,9 +270,9 @@ -- @rawgeti t r@. Function @'unref'@ frees a reference and its associated -- object. ----- If the object at the top of the stack is nil, @'ref'@ returns the constant--- @'refnil'@. The constant @'noref'@ is guaranteed to be different from any--- reference returned by @'ref'@.+-- If the object at the top of the stack is nil, @'ref'@ returns the+-- constant @'Lua.refnil'@. The constant @'Lua.noref'@ is guaranteed to+-- be different from any reference returned by @'ref'@. -- -- See also: <https://www.lua.org/manual/5.3/manual.html#luaL_ref luaL_ref>. ref :: StackIndex -> Lua Reference
src/Foreign/Lua/Core/Functions.hs view
@@ -133,11 +133,11 @@ -- metamethods). Otherwise returns @False@. Also returns @False@ if any of the -- indices is not valid. ----- The value of op must be of type @'LuaComparerOp'@:+-- The value of op must be of type @RelationalOperator@: -----    OpEQ: compares for equality (==)---    OpLT: compares for less than (<)---    OpLE: compares for less or equal (<=)+--    EQ: compares for equality (==)+--    LT: compares for less than (<)+--    LE: compares for less or equal (<=) -- -- This is a wrapper function of -- <https://www.lua.org/manual/5.3/manual.html#lua_compare lua_compare>.@@ -181,17 +181,21 @@  -- TODO: implement dump --- | Returns @True@ if the two values in acceptable indices index1 and index2--- are equal, following the semantics of the Lua @==@ operator (that is, may--- call metamethods). Otherwise returns False. Also returns False if any of the--- indices is non valid. Uses @'compare'@ internally.-equal :: StackIndex -> StackIndex -> Lua Bool+-- | Returns @True@ if the two values in acceptable indices index1 and+-- index2 are equal, following the semantics of the Lua @==@ operator+-- (that is, may call metamethods). Otherwise returns @False@. Also+-- returns @False@ if any of the indices is non valid. Uses @'compare'@+-- internally.+equal :: StackIndex  -- ^ index1+      -> StackIndex  -- ^ index2+      -> Lua Bool equal index1 index2 = compare index1 index2 EQ --- | This is a convenience function to implement error propagation convention--- described in [Error handling in hslua](#g:1). hslua doesn't implement--- `lua_error` function from Lua C API because it's never safe to use. (see--- [Error handling in hslua](#g:1) for details)+-- | This is a convenience function to implement error propagation+-- convention described in [Error handling in hslua](#g:1). hslua+-- doesn't implement the @lua_error@ function from Lua C API because+-- it's never safe to use. (see [Error handling in hslua](#g:1) for+-- details) error :: Lua NumResults error = liftLua hslua_error @@ -602,6 +606,10 @@ pushcfunction :: CFunction -> Lua () pushcfunction f = pushcclosure f 0 +-- | Pushes the global environment onto the stack.+--+-- Wraps <https://www.lua.org/manual/5.3/manual.html#lua_pushglobaltable \+-- lua_pushglobaltable>. pushglobaltable :: Lua () pushglobaltable = liftLua lua_pushglobaltable @@ -689,7 +697,7 @@  -- | Returns the raw "length" of the value at the given index: for strings, this -- is the string length; for tables, this is the result of the length operator--- ('#') with no metamethods; for userdata, this is the size of the block of+-- (@#@) with no metamethods; for userdata, this is the size of the block of -- memory allocated for the userdata; for other values, it is 0. -- -- See also:@@ -807,13 +815,13 @@  -- |  Returns the status of this Lua thread. ----- The status can be @'OK'@ for a normal thread, an error value if the thread--- finished the execution of a @'lua_resume'@ with an error, or @'Yield'@ if--- the thread is suspended.+-- The status can be 'OK' for a normal thread, an error value if the+-- thread finished the execution of a @lua_resume@ with an error, or+-- 'Yield' if the thread is suspended. ----- You can only call functions in threads with status @'OK'@. You can resume--- threads with status @'OK'@ (to start a new coroutine) or @'Yield'@ (to--- resume a coroutine).+-- You can only call functions in threads with status 'OK'. You can+-- resume threads with status 'OK' (to start a new coroutine) or 'Yield'+-- (to resume a coroutine). -- -- See also: <https://www.lua.org/manual/5.3/manual.html#lua_status lua_status>. status :: Lua Status@@ -840,7 +848,7 @@   return (if fnPtr == nullFunPtr then Nothing else Just fnPtr)  -- | Converts the Lua value at the given acceptable index to the signed integral--- type @'lua_Integer'@. The Lua value must be an integer, a number or a string+-- type 'Lua.Integer'. The Lua value must be an integer, a number or a string -- convertible to an integer (see -- <https://www.lua.org/manual/5.3/manual.html#3.4.3 §3.4.3> of the Lua 5.3 -- Reference Manual); otherwise, @tointeger@ returns @Nothing@.
src/Foreign/Lua/Core/RawBindings.hsc view
@@ -284,7 +284,7 @@   ----------------------------------------------------------------------------------- * 'load' and 'call' functions (load and run Lua code)+-- * \'load\' and \'call\' functions (load and run Lua code)  -- lua_call is inherently unsafe, we do not support it. @@ -343,6 +343,8 @@ foreign import ccall "error-conversion.h hslua_concat"   hslua_concat :: Lua.State -> NumArgs -> IO (Failable ()) +-- | See <https://www.lua.org/manual/5.3/manual.html#lua_pushglobaltable \+-- lua_pushglobaltable> foreign import capi unsafe "lua.h lua_pushglobaltable"   lua_pushglobaltable :: Lua.State -> IO () 
src/Foreign/Lua/Core/Types.hsc view
@@ -155,27 +155,30 @@ -- |  Type for C functions. -- -- In order to communicate properly with Lua, a C function must use the--- following protocol, which defines the way parameters and results are passed:--- a C function receives its arguments from Lua in its stack in direct order--- (the first argument is pushed first). So, when the function starts,--- @'gettop'@ returns the number of arguments received by the function. The--- first argument (if any) is at index 1 and its last argument is at index--- @gettop@. To return values to Lua, a C function just pushes them onto the--- stack, in direct order (the first result is pushed first), and returns the--- number of results. Any other value in the stack below the results will be--- properly discarded by Lua. Like a Lua function, a C function called by Lua--- can also return many results.+-- following protocol, which defines the way parameters and results are+-- passed: a C function receives its arguments from Lua in its stack in+-- direct order (the first argument is pushed first). So, when the+-- function starts, @'Foreign.Lua.Core.Functions.gettop'@ returns the+-- number of arguments received by the function. The first argument (if+-- any) is at index 1 and its last argument is at index+-- @'Foreign.Lua.Core.Functions.gettop'@. To return values to Lua, a C+-- function just pushes them onto the stack, in direct order (the first+-- result is pushed first), and returns the number of results. Any other+-- value in the stack below the results will be properly discarded by+-- Lua. Like a Lua function, a C function called by Lua can also return+-- many results. -- -- See <https://www.lua.org/manual/5.3/manual.html#lua_CFunction lua_CFunction>. type CFunction = FunPtr (State -> IO NumResults) --- | The reader function used by @'lua_load'@. Every time it needs another piece--- of the chunk, lua_load calls the reader, passing along its data parameter.--- The reader must return a pointer to a block of memory with a new piece of the--- chunk and set size to the block size. The block must exist until the reader--- function is called again. To signal the end of the chunk, the reader must--- return @NULL@ or set size to zero. The reader function may return pieces of any--- size greater than zero.+-- | The reader function used by @'Foreign.Lua.Core.Functions.load'@.+-- Every time it needs another piece of the chunk, lua_load calls the+-- reader, passing along its data parameter. The reader must return a+-- pointer to a block of memory with a new piece of the chunk and set+-- size to the block size. The block must exist until the reader+-- function is called again. To signal the end of the chunk, the reader+-- must return @NULL@ or set size to zero. The reader function may+-- return pieces of any size greater than zero. -- -- See <https://www.lua.org/manual/5.3/manual.html#lua_Reader lua_Reader>. type Reader = FunPtr (State -> Ptr () -> Ptr CSize -> IO (Ptr CChar))@@ -320,7 +323,7 @@   | ErrFile   -- ^ opening or reading a file failed.   deriving (Eq, Show) --- | Convert C integer constant to @'LuaStatus'@.+-- | Convert C integer constant to @'Status'@. toStatus :: StatusCode -> Status toStatus (StatusCode c) = case c of   #{const LUA_OK}        -> OK@@ -331,7 +334,7 @@   #{const LUA_ERRGCMM}   -> ErrGcmm   #{const LUA_ERRERR}    -> ErrErr   #{const LUA_ERRFILE}   -> ErrFile-  n -> error $ "Cannot convert (" ++ show n ++ ") to LuaStatus"+  n -> error $ "Cannot convert (" ++ show n ++ ") to Status" {-# INLINABLE toStatus #-}  -- | Integer code used to signal the status of a thread or computation.
src/Foreign/Lua/FunctionCalling.hsc view
@@ -80,9 +80,9 @@ -- @'Lua.Exception'@; exception handling must be done by the converted -- Haskell function. Failure to do so will cause the program to crash. ----- E.g., the following code could be used to handle an Exception of type--- FooException, if that type is an instance of @'MonadCatch'@ and--- @'Pushable'@:+-- E.g., the following code could be used to handle an Exception+-- of type FooException, if that type is an instance of+-- 'Control.Monad.Catch.MonadCatch' and 'Pushable': -- -- > toHaskellFunction (myFun `catchM` (\e -> raiseError (e :: FooException))) --
+ src/Foreign/Lua/Peek.hs view
@@ -0,0 +1,242 @@+{-# LANGUAGE OverloadedStrings #-}+{-|+Module      : Foreign.Lua.Peek+Copyright   : © 2020 Albert Krewinkel+License     : MIT+Maintainer  : Albert Krewinkel <tarleb+hslua@zeitkraut.de>+Stability   : beta+Portability : Portable++Functions which unmarshal and retrieve Haskell values from Lua's stack.+-}+module Foreign.Lua.Peek+  ( Peeker+  , PeekError (..)+  , errorMsg+  , force+  , toPeeker+  -- * Primitives+  , peekBool+  , peekIntegral+  , peekRealFloat+  -- * Strings+  , peekByteString+  , peekLazyByteString+  , peekString+  , peekText+  -- * Collections+  , peekKeyValuePairs+  , peekList+  , peekMap+  , peekSet+  ) where++import Control.Applicative ((<|>))+import Data.Bifunctor (first, second)+import Data.ByteString (ByteString)+import Data.List.NonEmpty (NonEmpty (..), (<|))+import Data.Map (Map)+import Data.Maybe (fromMaybe)+import Data.Set (Set)+import Data.Text (Text)+import Foreign.Lua.Core as Lua+import Text.Read (readMaybe)++import qualified Data.ByteString.Lazy as BL+import qualified Data.List.NonEmpty as NonEmpty+import qualified Data.Map as Map+import qualified Data.Set as Set+import qualified Data.Text as T+import qualified Foreign.Lua.Utf8 as Utf8++-- | List of errors which occurred while retrieving a value from+-- the stack.+newtype PeekError = PeekError { fromPeekError :: NonEmpty Text }+  deriving (Eq, Show)++formatPeekError :: PeekError -> String+formatPeekError (PeekError msgs) = T.unpack $+  T.intercalate "\n\t" (NonEmpty.toList msgs)++-- | Function to retrieve a value from Lua's stack.+type Peeker a = StackIndex -> Lua (Either PeekError a)++-- | Create a peek error from an error message.+errorMsg :: Text -> PeekError+errorMsg  = PeekError . pure++-- | Add a message to the peek traceback stack.+pushMsg :: Text -> PeekError -> PeekError+pushMsg msg (PeekError lst) = PeekError $ msg <| lst++-- | Add context information to the peek traceback stack.+retrieving :: Text -> Either PeekError a -> Either PeekError a+retrieving msg = first $ pushMsg ("retrieving " <> msg)++-- | Force creation of a result, throwing an exception if that's+-- not possible.+force :: Either PeekError a -> Lua a+force = either (throwMessage . formatPeekError) return++-- | Convert an old peek funtion to a 'Peeker'.+toPeeker :: (StackIndex -> Lua a)+         -> Peeker a+toPeeker op idx =+  (Right <$> op idx) <|> return (Left $ errorMsg "retrieving failed")++-- | Use @test@ to check whether the value at stack index @n@ has+-- the correct type and use @peekfn@ to convert it to a Haskell+-- value if possible. A successfully received value is wrapped+-- using the 'Right' constructor, while a type mismatch results+-- in @Left PeekError@ with the given error message.+typeChecked :: Text                      -- ^ expected type+            -> (StackIndex -> Lua Bool)  -- ^ pre-condition checker+            -> Peeker a+            -> Peeker a+typeChecked expectedType test peekfn idx = do+  v <- test idx+  if v+    then peekfn idx+    else Left <$> mismatchError expectedType idx++-- | Report the expected and actual type of the value under the given index if+-- conversion failed.+reportValueOnFailure :: Text+                     -> (StackIndex -> Lua (Maybe a))+                     -> Peeker a+reportValueOnFailure expected peekMb idx = do+  res <- peekMb idx+  case res of+    Just x  -> return $ Right x+    Nothing -> Left  <$> mismatchError expected idx++-- | Return a Result error containing a message about the assertion failure.+mismatchError :: Text -> StackIndex -> Lua PeekError+mismatchError expected idx = do+  actualType  <- ltype idx >>= typename+  actualValue <- Utf8.toText <$> tostring' idx <* pop 1+  return . errorMsg $+    "expected " <> expected <> ", got '" <>+    actualValue <> "' (" <> T.pack actualType <> ")"++-- | Retrieves a 'Bool' as a Lua boolean.+peekBool :: Peeker Bool+peekBool = fmap Right . toboolean++--+-- Strings+--++-- | Like @'tostring', but ensures that the value at the given index is+-- not silently converted to a string, as would happen with numbers.+toByteString :: StackIndex -> Lua (Maybe ByteString)+toByteString idx = do+  -- copy value, as tostring converts numbers to strings *in-place*.+  pushvalue idx+  tostring stackTop <* pop 1++-- | Retrieves a 'ByteString' as a raw string.+peekByteString :: Peeker ByteString+peekByteString = reportValueOnFailure "string" toByteString++-- | Retrieves a lazy 'BL.ByteString' as a raw string.+peekLazyByteString :: Peeker BL.ByteString+peekLazyByteString = fmap (second BL.fromStrict) . peekByteString++-- | Retrieves a 'String' as an UTF-8 encoded Lua string.+peekString :: Peeker String+peekString = fmap (second Utf8.toString) . peekByteString++-- | Retrieves a 'T.Text' value as an UTF-8 encoded string.+peekText :: Peeker T.Text+peekText = fmap (second Utf8.toText) . peekByteString++--+-- Numbers+--++-- | Retrieves an 'Integral' value from the Lua stack.+peekIntegral :: (Integral a, Read a) => Peeker a+peekIntegral idx =+  ltype idx >>= \case+    TypeNumber  -> second fromIntegral <$>+                   reportValueOnFailure "Integral" tointeger idx+    TypeString  -> do+      str <- Utf8.toString .+             fromMaybe (Prelude.error "programming error in peekIntegral")+             <$> tostring idx+      let msg = "expected Integral, got '" <> T.pack str <> "' (string)"+      return $ maybe (Left $ errorMsg msg) Right $ readMaybe str+    _ -> Left <$> mismatchError "Integral" idx++-- | Retrieve a 'RealFloat' (e.g., 'Float' or 'Double') from the stack.+peekRealFloat :: (RealFloat a, Read a) => Peeker a+peekRealFloat idx =+  ltype idx >>= \case+    TypeString  -> do+      str <- Utf8.toString .+             fromMaybe (Prelude.error "programming error in peekRealFloat")+             <$> tostring idx+      let msg = "expected RealFloat, got '" <> T.pack str <> "' (string)"+      return $ maybe (Left $ errorMsg msg) Right $ readMaybe str+    _ -> second realToFrac <$>+         reportValueOnFailure "RealFloat" tonumber idx++-- | Reads a numerically indexed table @t@ into a list, where the 'length' of+-- the list is equal to @#t@. The operation will fail if a numerical field @n@+-- with @1 ≤ n < #t@ is missing.+peekList :: Peeker a -> Peeker [a]+peekList peekElement = typeChecked "table" istable $ \idx -> do+  let elementsAt [] = return (Right [])+      elementsAt (i : is) = do+        eitherX <- rawgeti idx i *> peekElement (nthFromTop 1) <* pop 1+        case eitherX of+          Right x  -> second (x:) <$> elementsAt is+          Left err -> return . Left $+                      pushMsg ("in field " <> T.pack (show i)) err+  listLength <- fromIntegral <$> rawlen idx+  elementsAt [1..listLength]++-- | Retrieves a key-value Lua table as 'Map'.+peekMap :: Ord a => Peeker a -> Peeker b -> Peeker (Map a b)+peekMap keyPeeker valuePeeker =+    fmap (retrieving "Map" . second Map.fromList)+  . peekKeyValuePairs keyPeeker valuePeeker++-- | Read a table into a list of pairs.+peekKeyValuePairs :: Peeker a -> Peeker b -> Peeker [(a, b)]+peekKeyValuePairs keyPeeker valuePeeker =+  typeChecked "table" istable $ \idx -> do+    idx' <- absindex idx+    let remainingPairs = do+          res <- nextPair keyPeeker valuePeeker idx'+          case res of+            Left err       -> return $ Left err+            Right Nothing  -> return $ Right []+            Right (Just a) -> second (a:) <$> remainingPairs+    pushnil+    remainingPairs++-- | Get the next key-value pair from a table. Assumes the last+-- key to be on the top of the stack and the table at the given+-- index @idx@. The next key, if it exists, is left at the top of+-- the stack.+nextPair :: Peeker a -> Peeker b -> Peeker (Maybe (a, b))+nextPair keyPeeker valuePeeker idx = retrieving "key-value pair" <$> do+  hasNext <- next idx+  if not hasNext+    then return $ Right Nothing+    else do+      key   <- retrieving "key"   <$> keyPeeker   (nthFromTop 2)+      value <- retrieving "value" <$> valuePeeker (nthFromTop 1)+      pop 1    -- remove value, leave the key+      return $ curry Just <$> key <*> value++-- | Retrieves a 'Set' from an idiomatic Lua representation. A+-- set in Lua is idiomatically represented as a table with the+-- elements as keys. Elements with falsy values are omitted.+peekSet :: Ord a => Peeker a -> Peeker (Set a)+peekSet elementPeeker =+    fmap (retrieving "Set" .+          second (Set.fromList . map fst . filter snd))+  . peekKeyValuePairs elementPeeker peekBool
+ src/Foreign/Lua/Push.hs view
@@ -0,0 +1,108 @@+{-|+Module      : Foreign.Lua.Push+Copyright   : © 2020 Albert Krewinkel+License     : MIT+Maintainer  : Albert Krewinkel <tarleb+hslua@zeitkraut.de>+Stability   : beta+Portability : Portable++Functions which marshal and push Haskell values onto Lua's stack.+-}+module Foreign.Lua.Push+  ( Pusher+  -- * Primitives+  , pushBool+  , pushIntegral+  , pushRealFloat+  -- * Strings+  , pushByteString+  , pushLazyByteString+  , pushString+  , pushText+  -- * Collections+  , pushList+  , pushMap+  , pushSet+  ) where++import Control.Monad (zipWithM_)+import Data.ByteString (ByteString)+import Data.Map (Map, toList)+import Data.Set (Set)+import Foreign.Lua.Core as Lua+import Numeric (showGFloat)++import qualified Data.Text as T+import qualified Data.ByteString.Lazy as BL+import qualified Foreign.Lua.Utf8 as Utf8++-- | Function to push a value to Lua's stack.+type Pusher a = a -> Lua ()++-- | Pushes a 'Bool' as a Lua boolean.+pushBool :: Pusher Bool+pushBool = pushboolean++-- | Pushes a 'T.Text' value as an UTF-8 encoded string.+pushText :: Pusher T.Text+pushText = pushstring . Utf8.fromText++-- | Pushes a 'ByteString' as a raw string.+pushByteString :: Pusher ByteString+pushByteString = pushstring++-- | Pushes a lazy 'BL.ByteString' as a raw string.+pushLazyByteString :: Pusher BL.ByteString+pushLazyByteString = pushstring . BL.toStrict++-- | Pushes a 'String' as an UTF-8 encoded Lua string.+pushString :: String -> Lua ()+pushString = pushstring . Utf8.fromString++-- | Pushes an @Integer@ to the Lua stack. Values representable as Lua+-- integers are pushed as such; bigger integers are represented using+-- their string representation.+pushIntegral :: (Integral a, Show a) => a -> Lua ()+pushIntegral i =+  let maxInt = fromIntegral (maxBound :: Lua.Integer)+      minInt = fromIntegral (minBound :: Lua.Integer)+      i' = fromIntegral i :: Prelude.Integer+  in if i' >= minInt && i' <= maxInt+     then pushinteger $ fromIntegral i+     else pushString  $ show i++-- | Push a floating point number to the Lua stack. Uses a string+-- representation for all types which do not match the float properties+-- of the 'Lua.Number' type.+pushRealFloat :: RealFloat a => a -> Lua ()+pushRealFloat f =+  let+    number = 0 :: Lua.Number+    realFloatFitsInNumber = floatRadix number == floatRadix f+      && floatDigits number == floatDigits f+      && floatRange number == floatRange f+  in if realFloatFitsInNumber+     then pushnumber (realToFrac f :: Lua.Number)+     else pushString (showGFloat Nothing f "")++-- | Push list as numerically indexed table.+pushList :: Pusher a -> [a] -> Lua ()+pushList push xs = do+  let setField i x = push x *> rawseti (-2) i+  newtable+  zipWithM_ setField [1..] xs++-- | Push 'Map' as default key-value Lua table.+pushMap :: Pusher a -> Pusher b -> Pusher (Map a b)+pushMap pushKey pushValue m = do+  let addValue (k, v) = pushKey k *> pushValue v *> rawset (-3)+  newtable+  mapM_ addValue (toList m)++-- | Push a 'Set' as idiomatic Lua set, i.e., as a table with the set+-- elements as keys and @true@ as values.+pushSet :: Pusher a -> Pusher (Set a)+pushSet pushElement set = do+  let addItem item = pushElement item *> pushboolean True *> rawset (-3)+  newtable+  mapM_ addItem set
src/Foreign/Lua/Types/Peekable.hs view
@@ -21,26 +21,27 @@   , reportValueOnFailure   ) where +import Control.Monad ((>=>)) import Data.ByteString (ByteString) import Data.Map (Map, fromList) import Data.Set (Set) import Foreign.Lua.Core as Lua import Foreign.Ptr (Ptr)-import Text.Read (readMaybe)  import qualified Control.Monad.Catch as Catch import qualified Data.Set as Set import qualified Data.Text as T import qualified Data.ByteString.Lazy as BL+import qualified Foreign.Lua.Peek as Peek import qualified Foreign.Lua.Utf8 as Utf8  -- | Use @test@ to check whether the value at stack index @n@ has the correct--- type and use @peekfn@ to convert it to a haskell value if possible. A--- successfully received value is wrapped using the @'Success'@ constructor,--- while a type mismatch results in an @Error@ with the given error message.-typeChecked :: String-            -> (StackIndex -> Lua Bool)-            -> (StackIndex -> Lua a)+-- type and use @peekfn@ to convert it to a haskell value if possible. Throws+-- and exception if the test failes with the expected type name as part of the+-- message.+typeChecked :: String                   -- ^ expected type+            -> (StackIndex -> Lua Bool) -- ^ pre-condition Checker+            -> (StackIndex -> Lua a)    -- ^ retrieval function             -> StackIndex -> Lua a typeChecked expectedType test peekfn idx = do   v <- test idx@@ -84,10 +85,7 @@   peek = reportValueOnFailure "number" tonumber  instance Peekable ByteString where-  peek = reportValueOnFailure "string" $ \idx -> do-    -- copy value, as tostring converts numbers to strings *in-place*.-    pushvalue idx-    tostring stackTop <* pop 1+  peek = Peek.peekByteString >=> Peek.force  instance Peekable Bool where   peek = toboolean@@ -102,25 +100,25 @@   peek = reportValueOnFailure "Lua state (i.e., a thread)" tothread  instance Peekable T.Text where-  peek = fmap Utf8.toText . peek+  peek = Peek.peekText >=> Peek.force  instance Peekable BL.ByteString where-  peek = fmap BL.fromStrict . peek+  peek = Peek.peekLazyByteString >=> Peek.force  instance Peekable Prelude.Integer where-  peek = peekInteger+  peek = Peek.peekIntegral >=> Peek.force  instance Peekable Int where-  peek = fmap fromIntegral <$> peekInteger+  peek = Peek.peekIntegral >=> Peek.force  instance Peekable Float where-  peek = peekRealFloat+  peek = Peek.peekRealFloat >=> Peek.force  instance Peekable Double where-  peek = peekRealFloat+  peek = Peek.peekRealFloat >=> Peek.force  instance {-# OVERLAPS #-} Peekable [Char] where-  peek = fmap Utf8.toString . peek+  peek = Peek.peekString >=> Peek.force  instance Peekable a => Peekable [a] where   peek = peekList@@ -131,26 +129,6 @@ instance (Ord a, Peekable a) => Peekable (Set a) where   peek = -- All keys with non-nil values are in the set     fmap (Set.fromList . map fst . filter snd) . peekKeyValuePairs---- | Retrieve an @Int@ value from the stack.-peekInteger :: StackIndex -> Lua Prelude.Integer-peekInteger idx = ltype idx >>= \case-  TypeString -> do-    s <- peek idx-    case readMaybe s of-      Just x -> return x-      Nothing -> mismatchError "integer" idx-  _ -> fromIntegral <$> (peek idx :: Lua Lua.Integer)---- | Retrieve a @'RealFloat'@ (e.g., Float or Double) from the stack.-peekRealFloat :: (Read a, RealFloat a) => StackIndex -> Lua a-peekRealFloat idx = ltype idx >>= \case-  TypeString -> do-    s <- peek idx-    case readMaybe s of-      Just x -> return x-      Nothing -> mismatchError "number" idx-  _ -> realToFrac <$> (peek idx :: Lua Lua.Number)  -- | Read a table into a list peekList :: Peekable a => StackIndex -> Lua [a]
src/Foreign/Lua/Types/Pushable.hs view
@@ -18,16 +18,15 @@   , pushList   ) where -import Control.Monad (zipWithM_) import Data.ByteString (ByteString)-import Data.Map (Map, toList)+import Data.Map (Map)+import Data.Text (Text) import Data.Set (Set) import Foreign.Lua.Core as Lua+import Foreign.Lua.Push as Push import Foreign.Ptr (Ptr) -import qualified Data.Text as T import qualified Data.ByteString.Lazy as BL-import qualified Foreign.Lua.Utf8 as Utf8  -- | A value that can be pushed to the Lua stack. class Pushable a where@@ -56,17 +55,17 @@ instance Pushable (Ptr a) where   push = pushlightuserdata -instance Pushable T.Text where-  push = push . Utf8.fromText+instance Pushable Text where+  push = pushText  instance Pushable BL.ByteString where-  push = push . BL.toStrict+  push = pushLazyByteString  instance Pushable Prelude.Integer where-  push = pushInteger+  push = pushIntegral  instance Pushable Int where-  push = pushInteger . fromIntegral+  push = pushIntegral  instance Pushable Float where   push = pushRealFloat@@ -75,53 +74,16 @@   push = pushRealFloat  instance {-# OVERLAPS #-} Pushable [Char] where-  push = push . Utf8.fromString+  push = pushString  instance Pushable a => Pushable [a] where-  push = pushList----- | Push an @Int@ to the Lua stack. Numbers representable as Lua integers are--- pushed as such; bigger integers are represented using their string--- representation.-pushInteger :: Prelude.Integer -> Lua ()-pushInteger i =-  let maxInt = fromIntegral (maxBound :: Lua.Integer)-      minInt = fromIntegral (minBound :: Lua.Integer)-  in if i >= minInt && i <= maxInt-     then push (fromIntegral i :: Lua.Integer)-     else push (show i)---- | Push a floating point number to the Lua stack.-pushRealFloat :: (RealFloat a, Show a) => a -> Lua ()-pushRealFloat f =-  let-    number = 0 :: Lua.Number-    doubleFitsInNumber = floatRadix number == floatRadix f-      && floatDigits number == floatDigits f-      && floatRange number == floatRange f-  in if doubleFitsInNumber-     then push (realToFrac f :: Lua.Number)-     else push (show f)---- | Push list as numerically indexed table.-pushList :: Pushable a => [a] -> Lua ()-pushList xs = do-  let setField i x = push x *> rawseti (-2) i-  newtable-  zipWithM_ setField [1..] xs+  push = pushList push  instance (Pushable a, Pushable b) => Pushable (Map a b) where-  push m = do-    let addValue (k, v) = push k *> push v *> rawset (-3)-    newtable-    mapM_ addValue (toList m)+  push = pushMap push push  instance Pushable a => Pushable (Set a) where-  push set = do-    let addItem item = push item *> push True *> rawset (-3)-    newtable-    mapM_ addItem set+  push = pushSet push  -- -- Tuples
src/Foreign/Lua/Userdata.hs view
@@ -11,8 +11,9 @@  Convenience functions to convert Haskell values into Lua userdata. -The main purpose of this module is to allow fast and simple creation of-instances for @'Peekable'@ and @'Pushable'@. E.g., given a data type Person+The main purpose of this module is to allow fast and simple+creation of instances for @Peekable@ and @Pushable@. E.g., given+a data type Person  > data Person = Person { name :: String, age :: Int } >    deriving (Eq, Show, Typeable, Data)
src/Foreign/Lua/Utf8.hs view
@@ -20,15 +20,18 @@  import qualified Data.Text as T import qualified Data.Text.Encoding as TextEncoding+import qualified Data.Text.Encoding.Error as TextEncoding --- | Decode @'ByteString'@ to @'String'@ using UTF-8.+-- | Decode @'ByteString'@ to @'String'@ using UTF-8. Invalid input+-- bytes are replaced with the Unicode replacement character U+FFFD. toString :: ByteString -> String-toString = T.unpack . TextEncoding.decodeUtf8+toString = T.unpack . toText {-# INLINABLE toString #-} --- | Decode @'ByteString'@ to @'Text'@ using UTF-8.+-- | Decode @'ByteString'@ to @'Text'@ using UTF-8. Invalid input+-- bytes are replaced with the Unicode replacement character U+FFFD. toText :: ByteString -> Text-toText = TextEncoding.decodeUtf8+toText = TextEncoding.decodeUtf8With TextEncoding.lenientDecode {-# INLINABLE toText #-}  -- | Encode @'String'@ to @'ByteString'@ using UTF-8.
src/Foreign/Lua/Util.hs view
@@ -57,8 +57,8 @@ runEither = try . run  -- | Run Lua computation with the given Lua state and the default--- error-to-exception converter (@'throwTopStringAsException'@). Exception--- handling is left to the caller.+-- error-to-exception converter. Exception handling is left to+-- the caller. runWith :: Lua.State -> Lua a -> IO a runWith = Lua.runWithConverter defaultErrorConversion 
+ test/Foreign/Lua/PeekTests.hs view
@@ -0,0 +1,273 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}+{-|+Module      : Foreign.Lua.PeekTests+Copyright   : © 2020 Albert Krewinkel+License     : MIT+Maintainer  : Albert Krewinkel <tarleb+hslua@zeitkraut.de>+Stability   : alpha+Portability : OverloadedStrings, TypeApplications++Tests for Haskell-value retriever functions.+-}+module Foreign.Lua.PeekTests (tests) where++import Control.Monad (forM_, zipWithM_)+import Data.Either (isLeft)+import Foreign.Lua.Peek++import Test.HsLua.Arbitrary ()+import Test.HsLua.Util ( (=:), pushLuaExpr, shouldBeResultOf+                       , shouldHoldForResultOf)+import Test.QuickCheck.Instances ()+import Test.QuickCheck.Monadic (monadicIO, run, assert)+import Test.Tasty (TestTree, testGroup)+import Test.Tasty.QuickCheck (testProperty)++import qualified Data.ByteString.Char8 as Char8+import qualified Data.List.NonEmpty as NonEmpty+import qualified Data.Map as Map+import qualified Data.Set as Set+import qualified Data.Text as T+import qualified Foreign.Lua as Lua+import qualified Foreign.Lua.Utf8 as Utf8++-- | Calling Haskell functions from Lua.+tests :: TestTree+tests = testGroup "Peek"+  [ testGroup "peekBool"+    [ "True" =:+      Right True `shouldBeResultOf` do+        Lua.pushboolean True+        peekBool Lua.stackTop++    , "False" =:+      Right False `shouldBeResultOf` do+        Lua.pushboolean False+        peekBool Lua.stackTop++    , "Numbers are truthy" =:+      Right True `shouldBeResultOf` do+        Lua.pushnumber 0+        peekBool Lua.stackTop++    , "Nil is falsy" =:+      Right False `shouldBeResultOf` do+        Lua.pushnil+        peekBool Lua.stackTop++    -- no tests for failing cases, this function always succeeds.+    ]++  , testGroup "peekIntegral"+    [ "negative Int" =:+      Right (-5) `shouldBeResultOf` do+        Lua.pushinteger (-5)+        peekIntegral @Int Lua.stackTop++    , "Int as string" =:+      Right 720 `shouldBeResultOf` do+        Lua.pushstring "720"+        peekIntegral @Int Lua.stackTop++    , "fail on boolean" =:+      let msg = "expected Integral, got 'true' (boolean)"+      in Left (errorMsg msg) `shouldBeResultOf` do+        Lua.pushboolean True+        peekIntegral @Int Lua.stackTop++    , "fail on non-numeric string" =:+      let msg = "expected Integral, got 'not a number' (string)"+      in Left (errorMsg msg) `shouldBeResultOf` do+        Lua.pushstring "not a number"+        peekIntegral @Integer Lua.stackTop+    ]++  , testGroup "peekRealFloat"+    [ "negative Float" =:+      Right (-13.37) `shouldBeResultOf` do+        Lua.pushnumber (-13.37)+        peekRealFloat @Float Lua.stackTop++    , "number as string" =:+      Right (-720.0) `shouldBeResultOf` do+        Lua.pushstring "-720"+        peekRealFloat @Float Lua.stackTop++    , "scientific notation string" =:+      Right 0.00071 `shouldBeResultOf` do+        Lua.pushstring "7.1e-4"+        peekRealFloat @Float Lua.stackTop++    , "fail on boolean" =:+      let msg = "expected RealFloat, got 'true' (boolean)"+      in Left (errorMsg msg) `shouldBeResultOf` do+        Lua.pushboolean True+        peekRealFloat @Float Lua.stackTop++    , "fail on non-numeric string" =:+      let msg = "expected RealFloat, got 'not a number' (string)"+      in Left (errorMsg msg) `shouldBeResultOf` do+        Lua.pushstring "not a number"+        peekRealFloat @Double Lua.stackTop+    ]++  , testGroup "Strings"+    [ testGroup "peekByteString"+      [ testProperty "retrieve any string" $ \bs -> monadicIO $ do+          retrieved <- run $ Lua.run $ do+            Lua.pushstring bs+            peekByteString Lua.stackTop+          assert (retrieved == Right bs)++      , testProperty "retrieve integer as string" $ \n -> monadicIO $ do+          retrieved <- run . Lua.run $ do+            Lua.pushinteger n+            peekByteString Lua.stackTop+          let numberAsByteString = Char8.pack . show @Integer . fromIntegral $ n+          assert (retrieved == Right numberAsByteString)++      , "fails on boolean" =:+      let msg = "expected string, got 'true' (boolean)"+      in Left (errorMsg msg) `shouldBeResultOf` do+        Lua.pushboolean True+        peekByteString Lua.stackTop+      ]++    , testGroup "peekText"+      [ testProperty "retrieve any string" $ \bs -> monadicIO $ do+          retrieved <- run $ Lua.run $ do+            Lua.pushstring bs+            peekText Lua.stackTop+          assert (retrieved == Right (Utf8.toText bs))++      , testProperty "retrieve UTF-8 encoded Text" $ \txt -> monadicIO $ do+          retrieved <- run $ Lua.run $ do+            Lua.pushstring (Utf8.fromText txt)+            peekText Lua.stackTop+          assert (retrieved == Right txt)++      , testProperty "retrieve integer as Text" $ \n -> monadicIO $ do+          retrieved <- run . Lua.run $ do+            Lua.pushinteger n+            peekText Lua.stackTop+          let numberAsByteString = T.pack . show @Integer . fromIntegral $ n+          assert (retrieved == Right numberAsByteString)++      , "fails on nil" =:+        let msg = "expected string, got 'nil' (nil)"+        in Left (errorMsg msg) `shouldBeResultOf` do+          Lua.pushnil+          peekByteString Lua.stackTop+      ]++    , testGroup "peekString"+      [ testProperty "retrieve UTF-8 encoded string" $ \txt -> monadicIO $ do+          retrieved <- run $ Lua.run $ do+            Lua.pushstring (Utf8.fromString txt)+            peekString Lua.stackTop+          assert (retrieved == Right txt)++      , "fails on table" =:+        isLeft `shouldHoldForResultOf` do+          _ <- Lua.pushglobaltable+          peekString Lua.stackTop++      , "fails on thread" =:+        isLeft `shouldHoldForResultOf` do+          _ <- Lua.pushthread+          peekString Lua.stackTop+      ]+    ]++  , testGroup "Containers"+    [ testGroup "peekList"+      [ "empty list" =:+        Right [] `shouldBeResultOf` do+          Lua.newtable+          peekList peekBool Lua.stackTop++      , testProperty "list of strings" $ \lst -> monadicIO $ do+          retrieved <- run $ Lua.run $ do+            Lua.newtable+            zipWithM_+              (\i s -> Lua.pushstring s *>+                       Lua.rawseti (Lua.nthFromTop 2) i)+              [1..]+              lst+            peekList peekByteString Lua.stackTop+          assert (retrieved == Right lst)++      , "string keys are not in list" =:+        Right [] `shouldBeResultOf` do+          pushLuaExpr "{['1'] = 'hello', ['2'] = 'world'}"+          peekList peekByteString Lua.stackTop++      , "missing pair causes an error" =:+        isLeft `shouldHoldForResultOf` do+          pushLuaExpr "{[1] = 'hello', [2] = 'world', [4] = 'nope'}"+          peekList peekByteString Lua.stackTop+      ]++    , testGroup "peekSet"+      [ "empty set" =:+        Right Set.empty `shouldBeResultOf` do+          Lua.newtable+          peekSet peekBool Lua.stackTop++      , testProperty "set of strings" $ \set -> monadicIO $ do+          retrieved <- run $ Lua.run $ do+            Lua.newtable+            forM_ (Set.toList set) $ \x -> do+              Lua.pushstring x+              Lua.pushboolean True+              Lua.rawset (Lua.nthFromTop 3)+            peekSet peekByteString Lua.stackTop+          assert (retrieved == Right set)++      , "keys with falsy values are not in set" =:+        Right (Set.fromList [1,3]) `shouldBeResultOf` do+          pushLuaExpr "{['1'] = 'hello', ['2'] = false, [3] = 5}"+          peekSet (peekIntegral @Int) Lua.stackTop++      , "fails if element peeker fails" =:+        let errorStack = [ "retrieving Set"+                         , "retrieving key-value pair"+                         , "retrieving key"+                         , "expected string, got 'true' (boolean)"]+        in Left (PeekError $ NonEmpty.fromList errorStack) `shouldBeResultOf` do+          pushLuaExpr "{ NaN = true, [true] = false }"+          peekSet peekText Lua.stackTop+      ]++    , testGroup "peekMap"+      [ "empty map" =:+        Right Map.empty `shouldBeResultOf` do+          Lua.newtable+          peekMap peekText peekText Lua.stackTop++      , "tables become maps" =:+        Right (Map.fromList [("one", 1), ("two", 2)]) `shouldBeResultOf` do+          pushLuaExpr "{ one = 1, two = 2}"+          peekMap peekText (peekIntegral @Int) Lua.stackTop++      , "fails if key peeker fails" =:+        let errorStack = [ "retrieving Map"+                         , "retrieving key-value pair"+                         , "retrieving key"+                         , "expected Integral, got 'NaN' (string)"]+        in Left (PeekError $ NonEmpty.fromList errorStack) `shouldBeResultOf` do+          pushLuaExpr "{ NaN = true }"+          peekMap (peekIntegral @Int) peekBool Lua.stackTop++      , "fails if value peeker fails" =:+        let errorStack = [ "retrieving Map"+                         , "retrieving key-value pair"+                         , "retrieving value"+                         , "expected string, got 'true' (boolean)"]+        in Left (PeekError $ NonEmpty.fromList errorStack) `shouldBeResultOf` do+          pushLuaExpr "{ [42] = true }"+          peekMap (peekIntegral @Int) peekText Lua.stackTop+      ]+    ]+  ]
+ test/Foreign/Lua/PushTests.hs view
@@ -0,0 +1,229 @@+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}+{-|+Module      : Foreign.Lua.PushTests+Copyright   : © 2020 Albert Krewinkel+License     : MIT+Maintainer  : Albert Krewinkel <tarleb+hslua@zeitkraut.de>+Stability   : alpha+Portability : OverloadedStrings, TypeApplications++Test pushing Haskell values to the stack.+-}+module Foreign.Lua.PushTests (tests) where++import Control.Monad (forM)+import Data.ByteString (ByteString)+import Data.Maybe (fromMaybe)+import Foreign.Lua.Core (Lua, Number)+import Foreign.Lua.Push++import Test.HsLua.Arbitrary ()+import Test.HsLua.Util ((=:), pushLuaExpr)+import Test.QuickCheck.Instances ()+import Test.QuickCheck.Monadic (monadicIO, run, assert)+import Test.Tasty (TestTree, testGroup)+import Test.Tasty.HUnit (Assertion, assertFailure)+import Test.Tasty.QuickCheck (Arbitrary, testProperty)++import qualified Data.Map as Map+import qualified Data.Set as Set+import qualified Foreign.Lua as Lua+import qualified Foreign.Lua.Utf8 as Utf8++-- | Calling Haskell functions from Lua.+tests :: TestTree+tests = testGroup "Push"+  [ testGroup "pushBool"+    [ "True" =:+      assertLuaEqual (pushBool True) "true"+    , "False" =:+      assertLuaEqual (pushBool False) "false"++    , testSingleElementProperty pushBool+    ]++  , testGroup "pushIntegral"+    [ testGroup "@Int"+      [ "0" =:+        assertLuaEqual (pushIntegral @Int 0) "0"+      , "23" =:+        assertLuaEqual (pushIntegral @Int 23) "23"+      , "-5" =:+        assertLuaEqual (pushIntegral @Int (-5)) "-5"+      , testSingleElementProperty (pushIntegral @Int)+      ]+    , testGroup "@Integer"+      [ "2^128 + 1" =:+        assertLuaEqual (pushIntegral @Integer+                         340282366920938463463374607431768211457)+                       "'340282366920938463463374607431768211457'"+      , "-2^129 + 1" =:+        assertLuaEqual (pushIntegral @Integer+                        (-680564733841876926926749214863536422911))+                       "'-680564733841876926926749214863536422911'"+      , testSingleElementProperty (pushIntegral @Integer)+      ]+    ]++  , testGroup "pushRealFloat"+    [ testGroup "@Number"+      [ "0.0" =:+        assertLuaEqual (pushRealFloat @Number 0.0) "0.0"+      , "42.0" =:+        assertLuaEqual (pushRealFloat @Number 42.0) "42.0"+      , "0.1" =:+        assertLuaEqual (pushRealFloat @Number 0.1) "0.1"+      , "-13.37" =:+        assertLuaEqual (pushRealFloat @Number (-13.37)) "-13.37"+      , testSingleElementProperty (pushRealFloat @Number)+      ]++    -- This test may fail if Lua is compiled with Float as the Number+    -- type. Usually though, numbers are doubles.+    , testGroup "@Float pushes strings"+      [ "0.0" =:+        assertLuaEqual (pushRealFloat @Float 0.0) "'0.0'"+      , "42.0" =:+        assertLuaEqual (pushRealFloat @Float 42.0) "'42.0'"+      , "-0.00071" =:+        assertLuaEqual (pushRealFloat @Float (-0.00071)) "'-7.1e-4'"+      , "-13.37" =:+        assertLuaEqual (pushRealFloat @Float (-13.37)) "'-13.37'"+      , testSingleElementProperty (pushRealFloat @Float)+      ]+    ]++  , testGroup "Strings"+    [ testGroup "pushByteString"+      [ "\"test\"" =:+        assertLuaEqual (pushByteString "test") "\"test\""+      , testSingleElementProperty pushByteString+      ]++    , testGroup "pushString"+      [ "\"test\"" =:+        assertLuaEqual (pushString "test") "\"test\""+      , "unicode" =:+        assertLuaEqual (pushString "ÄÉÏøûßð") (Utf8.fromString "'ÄÉÏøûßð'")+      , testSingleElementProperty pushString+      ]++    , testGroup "pushText"+      [ "\"test\"" =:+        assertLuaEqual (pushText "test") "\"test\""+      , "unicode" =:+        assertLuaEqual (pushText "ÄÉÏøûßð") (Utf8.fromString "'ÄÉÏøûßð'")+      , testSingleElementProperty pushText+      ]+    ]++  , testGroup "Collections"+    [ testGroup "pushList"+      [ testProperty "creates a table" $ \x -> monadicIO $ do+          producesTable <- run $ Lua.run $ do+            pushList pushBool x+            listType <- Lua.ltype Lua.stackTop+            return $ Lua.TypeTable == listType+          assert producesTable++      , testProperty "numeric indices start at 1" $ \list -> monadicIO $ do+          retrievedList <- run $ Lua.run $ do+            pushList (pushIntegral @Lua.Integer) list+            listIdx <- Lua.absindex Lua.stackTop+            forM [1..(fromIntegral $ length list)] $ \n ->+              Lua.rawgeti listIdx n+              *> (fromMaybe 0 <$> Lua.tointeger Lua.stackTop)+              <* Lua.pop 1+          assert $ retrievedList == list++      , testProperty "table size equals list length" $ \list -> monadicIO $ do+          tableSize <- run $ Lua.run $ do+            pushList pushString list+            Lua.rawlen Lua.stackTop+          assert $ fromIntegral tableSize == length list++      , testSingleElementProperty (pushList pushText)+      ]++    , testGroup "pushSet"+      [ testProperty "creates a table" $ \x -> monadicIO $ do+          producesTable <- run $ Lua.run $ do+            pushSet pushString x+            listType <- Lua.ltype Lua.stackTop+            return $ Lua.TypeTable == listType+          assert producesTable++      , testProperty "set values become table keys" $ \set -> monadicIO $+          case Set.lookupMin set of+            Nothing -> return ()+            Just el -> do+              hasKey <- run $ Lua.run $ do+                pushSet (pushIntegral @Lua.Integer) set+                pushIntegral el+                Lua.gettable (Lua.nthFromTop 2)+                Lua.toboolean Lua.stackTop+              assert hasKey++      , testSingleElementProperty (pushSet pushText)+      ]++    , testGroup "pushMap"+      [ testProperty "creates a table" $ \m -> monadicIO $ do+          producesTable <- run $ Lua.run $ do+            pushMap pushString pushString m+            listType <- Lua.ltype Lua.stackTop+            return $ Lua.TypeTable == listType+          assert producesTable++      , testProperty "pairs are in table" $ \m -> monadicIO $+          case Map.lookupMax m of+            Nothing -> return ()+            Just (k, v) -> do+              tabVal <- run $ Lua.run $ do+                pushMap pushText (pushRealFloat @Lua.Number) m+                pushText k+                Lua.gettable (Lua.nthFromTop 2)+                fromMaybe (error "key not found") <$>+                  Lua.tonumber Lua.stackTop+              assert (tabVal == v)++      , testSingleElementProperty (pushMap pushText (pushRealFloat @Double))+      ]+    ]+  ]++-- | Executes a Lua action and checks whether a the value at the top of the+-- stack equals the value represented by the string.+assertLuaEqual :: Lua () -> ByteString -> Assertion+assertLuaEqual action lit =+  let comparison = Lua.run $ do+        action+        pushLuaExpr lit+        isSame <- Lua.rawequal (Lua.nthFromTop 1) (Lua.nthFromTop 2)+        if isSame+          then return Nothing+          else do+            expectedType <- Lua.ltype (Lua.nthFromTop 1) >>= Lua.typename+            actualType <- Lua.ltype (Lua.nthFromTop 2) >>= Lua.typename+            actual <- Lua.tostring' (Lua.nthFromTop 2)+            return . Just $+              "Expected '" <> Utf8.toString lit <> "' (" <> expectedType <>+              ") but got '" <> Utf8.toString actual <> "'" <>+              " (" <> actualType <> ")"+  in comparison >>= \case+    Nothing -> return ()+    Just err -> assertFailure err+++-- | Verifies that the operation adds exactly one element to the Lua stack.+testSingleElementProperty :: (Arbitrary a, Show a) => Pusher a -> TestTree+testSingleElementProperty push = testProperty "pushes single element" $ \x ->+  monadicIO $ do+    (oldSize, newSize) <- run . Lua.run $ do+      old <- Lua.gettop+      push x+      new <- Lua.gettop+      return (old, new)+    assert (newSize == succ oldSize)
test/test-hslua.hs view
@@ -25,6 +25,8 @@ import qualified Foreign.Lua.CoreTests import qualified Foreign.Lua.FunctionCallingTests import qualified Foreign.Lua.ModuleTests+import qualified Foreign.Lua.PeekTests+import qualified Foreign.Lua.PushTests import qualified Foreign.Lua.TypesTests import qualified Foreign.Lua.Types.PeekableTests import qualified Foreign.Lua.Types.PushableTests@@ -38,6 +40,7 @@ tests :: [TestTree] tests =   [ Foreign.Lua.CoreTests.tests+  , Foreign.Lua.PushTests.tests   , Foreign.Lua.FunctionCallingTests.tests   , Foreign.Lua.UtilTests.tests   , testGroup "Sendings and receiving values from the stack"@@ -48,4 +51,5 @@   , Foreign.Lua.UserdataTests.tests   , Foreign.Lua.ModuleTests.tests   , Foreign.LuaTests.tests+  , Foreign.Lua.PeekTests.tests   ]