packages feed

mquickjs-hs-0.1.2.4: src/MQuickJS.hs

{-# LANGUAGE BangPatterns, QuasiQuotes, TemplateHaskell, OverloadedStrings, ScopedTypeVariables, FlexibleContexts #-}

{-|
Module      : MQuickJS
Description : Haskell bindings to the [Micro QuickJS](https://github.com/bellard/mquickjs) library
License     : MIT

This is a very basic wrapper for the [Micro QuickJS](https://github.com/bellard/mquickjs) library.

The current functionality includes evaluating JS code, calling a JS function in the global scope
and marshalling 'Value's to and from 'JSValue's.
-}
module MQuickJS (JSValue, JSContextPtr, mquickjs, mquickjsWithMemory, mquickjsMultithreaded, mquickjsMultithreadedWithMemory, call, eval, eval_, withJSValue, fromJSValue_) where

import           Foreign
import           Foreign.C                   (CString, CInt, CDouble, CSize)
import           Data.ByteString             (ByteString, useAsCString, useAsCStringLen, packCString)
import           Data.Text.Encoding          (encodeUtf8)
import qualified Language.C.Inline           as C
import           Control.Monad.Catch         (MonadThrow(..), MonadCatch(..), MonadMask(..), finally)
import           Control.Monad               (when, forM_)
import           Control.Monad.Reader        (MonadReader, runReaderT, ask)
import           Control.Monad.Trans.Reader  (ReaderT)
import           Control.Monad.IO.Class      (MonadIO, liftIO)
import           Control.Monad.IO.Unlift     (MonadUnliftIO(..), UnliftIO(..), askUnliftIO)
import           Data.Aeson                  (Key,Value(..), encode, toJSON)
import qualified Data.Aeson                  as Aeson
import qualified Data.Aeson.Key              as Key
import           Data.Scientific             (fromFloatDigits, toRealFloat, toBoundedInteger, isInteger)
import           Data.Text                   (Text)
import           Data.Vector                 (fromList, imapM_)
import           Data.Aeson.KeyMap         (KeyMap, empty, insert, toList)
import           Data.String.Conv            (toS)
import           Data.Time.Clock.POSIX       (posixSecondsToUTCTime)
import           Control.Concurrent          (rtsSupportsBoundThreads, runInBoundThread)

import           MQuickJS.Types
import           MQuickJS.Error


C.context mquickjsCtx
C.include "<stddef.h>"
C.include "mquickjs.h"

-- Reference to the stdlib defined in example_stdlib.c
C.verbatim "extern const JSSTDLibraryDef js_stdlib;"

foreign import ccall "JS_NewContext"
  jsNewContext :: Ptr Word8 -> CSize -> Ptr () -> IO (Ptr JSContext)

foreign import ccall "JS_FreeContext"
  jsFreeContext :: Ptr JSContext -> IO ()



-- Micro QuickJS uses automatic GC, no manual reference counting needed
jsFreeValue :: JSContextPtr -> JSValue -> IO ()
jsFreeValue _ _ = return ()  -- No-op in Micro QuickJS



type JSContextPtr = Ptr JSContext
type JSValueConstPtr = Ptr JSValueConst

jsIs_ :: (MonadIO m, Storable p, Eq n, Num n) => p -> (Ptr p -> IO n) -> m Bool
jsIs_ val fun = do
  b <- liftIO $ with val fun
  return $ b == 1

-- jsIsNumber :: MonadIO m => JSValue -> m Bool
-- jsIsNumber val = jsIs_ val $ \valPtr -> [C.block| int { return JS_IsNumber(*$(JSValueConst *valPtr)); } |]

-- Micro QuickJS uses JS_GetClassID instead of JS_IsArray
jsIsArray :: MonadIO m => JSContextPtr -> JSValue -> m Bool
jsIsArray ctxPtr val = do
  classId <- liftIO $ [C.block| int { return JS_GetClassID($(JSContext *ctxPtr), $(JSValue val)); } |]
  return $ classId == 1  -- JS_CLASS_ARRAY = 1

-- Micro QuickJS uses JS_GetClassID for Date detection (JS_CLASS_DATE = 7)
jsIsDate :: MonadIO m => JSContextPtr -> JSValue -> m Bool
jsIsDate ctxPtr val = do
  classId <- liftIO $ [C.block| int { return JS_GetClassID($(JSContext *ctxPtr), $(JSValue val)); } |]
  return $ classId == 7  -- JS_CLASS_DATE = 7


jsIsTryAll :: MonadThrow m =>
  JSValue -> [JSValue -> m Bool] -> [JSTypeEnum] -> JSTypeEnum -> m JSTypeEnum
jsIsTryAll _ [] _ def = return def
jsIsTryAll jsval (f:funs)(l:lbls) def = do
  b <- f jsval
  if b then return l else jsIsTryAll jsval funs lbls def
jsIsTryAll _ _ _ _ = throwM $ InternalError $ "jsIsTryAll_ unreachable case"


-- Micro QuickJS type detection - simpler approach using helper functions
jsIs :: (MonadIO m, MonadThrow m) => JSContextPtr -> JSValue -> m JSTypeEnum
jsIs ctx jsval = liftIO $ do
  -- Check for exception first
  isException <- [C.block| int { return JS_IsException($(JSValue jsval)); } |]
  if isException /= 0 then return $ JSTypeFromTag JSTagException
  else do
    -- Check for null/undefined
    isNull <- [C.block| int { return $(JSValue jsval) == JS_NULL; } |]
    if isNull /= 0 then return $ JSTypeFromTag JSTagNull
    else do
      isUndef <- [C.block| int { return $(JSValue jsval) == JS_UNDEFINED; } |]
      if isUndef /= 0 then return $ JSTypeFromTag JSTagUndefined
      else do
        -- Check for boolean
        isBoolTrue <- [C.block| int { return $(JSValue jsval) == JS_TRUE; } |]
        isBoolFalse <- [C.block| int { return $(JSValue jsval) == JS_FALSE; } |]
        if isBoolTrue /= 0 || isBoolFalse /= 0 then return $ JSTypeFromTag JSTagBool
        else do
          -- Check for number (int or float)
          isNum <- [C.block| int { return JS_IsNumber($(JSContext *ctx), $(JSValue jsval)); } |]
          if isNum /= 0 then return JSIsNumber
          else do
            -- Check for string
            isStr <- [C.block| int { return JS_IsString($(JSContext *ctx), $(JSValue jsval)); } |]
            if isStr /= 0 then return $ JSTypeFromTag JSTagPtr  -- Using JSTagPtr for strings
            else do
              -- Check class ID for arrays, dates, objects
              classId <- [C.block| int { return JS_GetClassID($(JSContext *ctx), $(JSValue jsval)); } |]
              case classId of
                1 -> return JSIsArray   -- JS_CLASS_ARRAY
                7 -> return JSIsDate    -- JS_CLASS_DATE
                _ -> return $ JSTypeFromTag JSTagPtr  -- Generic object



-- Micro QuickJS null value constant
jsNullValue :: JSValue
jsNullValue = 7  -- JS_NULL = JS_VALUE_MAKE_SPECIAL(JS_TAG_NULL, 0) = 7

-- Micro QuickJS: JS_NewBool doesn't need ctx parameter
jsNewBool :: JSContextPtr -> Bool -> IO JSValue
jsNewBool _ bool = do
  let b = if bool then 1 else 0
  return $ [C.pure| JSValue { JS_NewBool($(int b)) } |]

jsNewFloat64 :: JSContextPtr -> CDouble -> IO JSValue
jsNewFloat64 ctxPtr num =
  C.withPtr_ $ \ptr -> [C.block| void { *$(JSValue *ptr) = JS_NewFloat64($(JSContext *ctxPtr), $(double num)); } |]

jsNewInt64 :: JSContextPtr -> Int64 -> IO JSValue
jsNewInt64 ctxPtr num = do
  C.withPtr_ $ \ptr -> [C.block| void { *$(JSValue *ptr) = JS_NewInt64($(JSContext *ctxPtr), $(int64_t num)); } |]

jsNewString :: JSContextPtr -> ByteString -> IO JSValue
jsNewString ctxPtr s = C.withPtr_ $ \ptr -> useAsCStringLen s $ \(cstringPtr, cstringLen) -> do
  let len = fromIntegral cstringLen
  [C.block| void { *$(JSValue *ptr) = JS_NewStringLen($(JSContext *ctxPtr), $(const char *cstringPtr), $(size_t len)); } |]



checkIsException :: (MonadThrow m, MonadIO m) => Text -> JSContextPtr -> JSValue -> m ()
checkIsException loc ctxPtr val = do
  isEx <- liftIO $ [C.block| int { return JS_IsException($(JSValue val)); } |]
  when (isEx /= 0) $ do
    err <- getErrorMessage ctxPtr
    throwM $ JSException loc err



jsonToJSValue :: (MonadThrow m, MonadIO m) => JSContextPtr -> Value -> m JSValue
jsonToJSValue _ Null = pure jsNullValue
jsonToJSValue ctx (Bool b) = liftIO $ jsNewBool ctx b
jsonToJSValue ctx (Number n) =
  if not (isInteger n) then liftIO $ jsNewFloat64 ctx (toRealFloat n)
  else case toBoundedInteger n of
    Just i -> liftIO $ jsNewInt64 ctx i
    Nothing -> throwM $ InternalError "Value does not fit in Int64"
jsonToJSValue ctx (String s) = liftIO $ jsNewString ctx $ toS s
jsonToJSValue ctxPtr (Array xs) = do
  -- Micro QuickJS: JS_NewArray takes initial length parameter
  arrVal <- liftIO $ [C.block| JSValue { return JS_NewArray($(JSContext *ctxPtr), 0); } |]

  checkIsException "jsonToJSValue/Array/1" ctxPtr arrVal

  flip imapM_ xs $ \index value -> do
    val <- jsonToJSValue ctxPtr value
    checkIsException "jsonToJSValue/Array/2" ctxPtr val

    let idx = fromIntegral index
    -- Micro QuickJS: JS_SetPropertyUint32 returns JSValue (exception or undefined)
    res <- liftIO $ [C.block| JSValue {
      return JS_SetPropertyUint32($(JSContext *ctxPtr), $(JSValue arrVal), $(uint32_t idx), $(JSValue val));
    } |]
    checkIsException "jsonToJSValue/Array/set" ctxPtr res

  return arrVal
jsonToJSValue ctxPtr (Object o) = do
  objVal <- liftIO $ [C.block| JSValue { return JS_NewObject($(JSContext *ctxPtr)); } |]

  checkIsException "jsonToJSValue/Object/1" ctxPtr objVal

  forM_ (toList o) $ \(key,value) -> do
    val <- jsonToJSValue ctxPtr value
    checkIsException "jsonToJSValue/Object/2" ctxPtr val

    -- Micro QuickJS: JS_SetPropertyStr returns JSValue (exception or undefined)
    res <- liftIO $ useAsCString (encodeUtf8 $ Key.toText key) $ \cstringPtr ->
      [C.block| JSValue {
        return JS_SetPropertyStr($(JSContext *ctxPtr), $(JSValue objVal), $(const char *cstringPtr), $(JSValue val));
      } |]
    checkIsException "jsonToJSValue/Object/set" ctxPtr res

  return objVal


-- Micro QuickJS: Use JS_ToNumber for boolean conversion (no JS_ToBool)
jsToBool :: (MonadThrow m, MonadIO m) => JSContextPtr -> JSValue -> m Bool
jsToBool ctxPtr val = do
    (res, code) <- liftIO $ C.withPtr $ \doublePtr ->
      [C.block| int { return JS_ToNumber($(JSContext *ctxPtr), $(double *doublePtr), $(JSValue val)); } |]
    if code == 0 then return (res /= 0)
    else getErrorMessage ctxPtr >>= throwM . JSException "jsToBool"

-- Micro QuickJS: Use JS_ToInt32 (no JS_ToInt64)
jsToInt64 :: (MonadThrow m, MonadIO m) => JSContextPtr -> JSValue -> m Int64
jsToInt64 ctxPtr val = do
  (res, code) <- liftIO $ C.withPtr $ \intPtr ->
    [C.block| int { return JS_ToInt32($(JSContext *ctxPtr), $(int *intPtr), $(JSValue val)); } |]
  if code == 0 then return (fromIntegral res)
  else getErrorMessage ctxPtr >>= throwM . JSException "jsToInt64"

-- Micro QuickJS: JS_ToNumber instead of JS_ToFloat64
jsToFloat64 :: (MonadThrow m, MonadIO m) => JSContextPtr -> JSValue -> m CDouble
jsToFloat64 ctxPtr val = do
  (res, code) <- liftIO $ C.withPtr $ \doublePtr ->
    [C.block| int { return JS_ToNumber($(JSContext *ctxPtr), $(double *doublePtr), $(JSValue val)); } |]
  if code == 0 then return res
  else getErrorMessage ctxPtr >>= throwM . JSException "jsToFloat64"



-- Micro QuickJS: JS_ToCString requires JSCStringBuf parameter, no JS_FreeCString needed
jsToString :: MonadIO m => JSContextPtr -> JSValue -> m ByteString
jsToString ctxPtr val = liftIO $ do
    -- JSCStringBuf is 5 bytes
    allocaBytes 5 $ \bufPtr -> do
      cstring <- [C.block| const char * {
        return JS_ToCString($(JSContext *ctxPtr), $(JSValue val), (JSCStringBuf *)$(void *bufPtr));
      } |]
      if cstring == nullPtr then return ""
      else packCString cstring  -- No free needed in Micro QuickJS


jsToJSON :: (MonadCatch m, MonadIO m) => JSContextPtr -> JSValue -> m Value
jsToJSON ctx jsval = do
  ty <- jsIs ctx jsval
  case ty of
    JSTypeFromTag JSTagException -> do
      err <- getErrorMessage ctx
      throwM $ JSException "jsToJSON/JSTagException" err
    JSTypeFromTag JSTagNull -> return Null
    JSTypeFromTag JSTagUndefined -> return Null
    JSTypeFromTag JSTagBool -> do
      b <- jsToBool ctx jsval
      return $ Bool b
    JSIsNumber -> do
      n <- jsToFloat64 ctx jsval
      return $ Number $ fromFloatDigits n
    JSIsArray -> do
      len <- do
        lenVal <- jsGetPropertyStr ctx jsval "length"
        len' <- jsToInt64 ctx lenVal
        return len'
      vs <- jsArrayToJSON ctx jsval 0 (fromIntegral len)
      return $ Array $ fromList vs
    JSIsDate -> do
      -- Micro QuickJS: Use stack-based calling for Date.getTime()
      timestampRaw <- liftIO $ [C.block| JSValue {
        JSValue date = $(JSValue jsval);
        JSContext *ctx = $(JSContext *ctx);
        JSValue getter = JS_GetPropertyStr(ctx, date, "getTime");
        // Stack-based call: push this, func, then call
        JS_PushArg(ctx, date);      // this
        JS_PushArg(ctx, getter);    // func
        return JS_Call(ctx, 0);     // 0 args
      } |]
      checkIsException "jsToJSON/Date" ctx timestampRaw
      timestamp <- jsToFloat64 ctx timestampRaw
      return $ toJSON $ posixSecondsToUTCTime $ realToFrac $ timestamp / 1000
    JSTypeFromTag JSTagPtr -> do
      -- JSTagPtr can be string or object - check which one
      isStr <- liftIO $ [C.block| int { return JS_IsString($(JSContext *ctx), $(JSValue jsval)); } |]
      if isStr /= 0 then do
        s <- jsToString ctx jsval
        return $ String $ toS s
      else do
        o <- jsObjectToJSON ctx jsval
        return $ Object o
    JSTypeFromTag f -> throwM $ UnsupportedTypeTag f
    JSIsError -> throwM $ InternalError "JSIsError unreachable"


jsArrayToJSON :: (MonadCatch m, MonadIO m) => JSContextPtr -> JSValue -> Int -> Int -> m [Value]
jsArrayToJSON ctxPtr jsval index len =
  if index < len then do
    v <- do
      let idx = fromIntegral index
      val <- liftIO $ [C.block| JSValue {
        return JS_GetPropertyUint32($(JSContext *ctxPtr), $(JSValue jsval), $(uint32_t idx));
      } |]

      checkIsException "jsArrayToJSON" ctxPtr val
      jsToJSON ctxPtr val

    vs <- jsArrayToJSON ctxPtr jsval (index+1) len
    return $ v:vs
  else return []






forLoop :: (Num a, Ord a, Monad m) => a -> (a -> m ()) -> m ()
forLoop end f = go 0
  where
    go !x | x < end   = f x >> go (x+1)
          | otherwise = return ()




-- Micro QuickJS: Use JavaScript Object.keys() for property enumeration
-- since JS_GetOwnPropertyNames is not available in the public API
jsObjectToJSON :: (MonadCatch m, MonadIO m) => JSContextPtr -> JSValue -> m (KeyMap Value)
jsObjectToJSON ctxPtr obj = do
    -- Get Object.keys(obj) via JavaScript evaluation
    keysArray <- liftIO $ [C.block| JSValue {
      JSContext *ctx = $(JSContext *ctxPtr);
      JSValue obj = $(JSValue obj);

      // Get Object constructor from global
      JSValue global = JS_GetGlobalObject(ctx);
      JSValue objectCtor = JS_GetPropertyStr(ctx, global, "Object");
      JSValue keysFunc = JS_GetPropertyStr(ctx, objectCtor, "keys");

      // Stack-based call: Object.keys(obj)
      JS_PushArg(ctx, obj);        // arg
      JS_PushArg(ctx, keysFunc);   // func
      JS_PushArg(ctx, objectCtor); // this
      return JS_Call(ctx, 1);      // 1 argument
    } |]

    checkIsException "jsObjectToJSON/keys" ctxPtr keysArray

    -- Get array length
    lenVal <- jsGetPropertyStr ctxPtr keysArray "length"
    len <- jsToInt64 ctxPtr lenVal

    -- Iterate through keys
    collectProps ctxPtr obj keysArray 0 (fromIntegral len)
  where
    collectProps :: (MonadCatch m, MonadIO m) => JSContextPtr -> JSValue -> JSValue -> Int -> Int -> m (KeyMap Value)
    collectProps ctx objVal keysArr !index end
      | index < end = do
          let idx = fromIntegral index

          -- Get key at index
          keyVal <- liftIO $ [C.block| JSValue {
            return JS_GetPropertyUint32($(JSContext *ctx), $(JSValue keysArr), $(uint32_t idx));
          } |]
          checkIsException "jsObjectToJSON/getKey" ctx keyVal
          keyStr <- jsToString ctx keyVal

          -- Get property value
          propVal <- liftIO $ useAsCString keyStr $ \ckey ->
            [C.block| JSValue {
              return JS_GetPropertyStr($(JSContext *ctx), $(JSValue objVal), $(const char *ckey));
            } |]
          checkIsException "jsObjectToJSON/getProp" ctx propVal
          val <- jsToJSON ctx propVal

          rest <- collectProps ctx objVal keysArr (index + 1) end
          return $ insert (Key.fromText $ toS keyStr) val rest

      | otherwise = return empty



getErrorMessage :: MonadIO m => JSContextPtr -> m Text
getErrorMessage ctxPtr = liftIO $ do
  ex <- [C.block| JSValue { return JS_GetException($(JSContext *ctxPtr)); } |]
  res <- jsToString ctxPtr ex
  return $ toS res



jsGetPropertyStr :: MonadIO m => JSContextPtr -> JSValue -> ByteString -> m JSValue
jsGetPropertyStr ctxPtr val str = liftIO $
  useAsCString str $ \prop ->
    [C.block| JSValue { return JS_GetPropertyStr($(JSContext *ctxPtr), $(JSValue val), $(const char *prop)); } |]


-- Micro QuickJS: Stack-based function calling
-- Push args in reverse order, then func, then this, then call with argc
-- See mquickjs.c: arg[n-1] first, then ..., then arg[0], then func, then this_obj
jsCall :: JSContextPtr -> JSValue -> CInt -> (Ptr JSValue) -> IO JSValue
jsCall ctxt fun_obj argc argv = do
  -- Push arguments in reverse order (last arg first), then func, then this (JS_NULL)
  forM_ (reverse [0..(fromIntegral argc - 1)]) $ \i -> do
    arg <- peekElemOff argv i
    let argVal = arg
    [C.block| void {
      JSContext *ctx = $(JSContext *ctxt);
      JSValue v = $(JSValue argVal);
      JS_PushArg(ctx, v);
    } |]
  let funVal = fun_obj
  [C.block| void {
    JSContext *ctx = $(JSContext *ctxt);
    JS_PushArg(ctx, $(JSValue funVal));
    JS_PushArg(ctx, JS_NULL);
  } |]
  [C.block| JSValue { return JS_Call($(JSContext *ctxt), $(int argc)); } |]


jsEval :: JSContextPtr -> CString -> CSize -> CString -> CInt -> IO JSValue
jsEval ctxPtr input input_len filename eval_flags =
  [C.block| JSValue { return JS_Eval($(JSContext *ctxPtr), $(const char *input), $(size_t input_len), $(const char *filename), $(int eval_flags)); } |]


evalRaw :: JSContextPtr -> JSEvalType -> ByteString -> IO JSValue
evalRaw ctx eTyp code =
    useAsCString "script.js" $ \cfilename ->
        useAsCStringLen code $ \(ccode, ccode_len) ->
            jsEval ctx ccode (fromIntegral ccode_len) cfilename (toCType eTyp)




evalAs :: (MonadMask m, MonadReader JSContextPtr m, MonadIO m) => JSEvalType -> ByteString -> m Value
evalAs eTyp code = do
  ctx <- ask
  val <- liftIO $ evalRaw ctx eTyp code
  -- checkIsException "evalAs" ctx val
  jsToJSON ctx val `finally` freeJSValue val



{-|
Evaluates the given string and returns a 'Value' (if the result can be converted).
-}
eval :: (MonadMask m, MonadReader JSContextPtr m, MonadIO m) => ByteString -> m Value
eval = evalAs WithRetval  -- Micro QuickJS needs JS_EVAL_RETVAL flag to return the value

evalAs_ :: (MonadThrow m, MonadReader JSContextPtr m, MonadIO m) => JSEvalType -> ByteString -> m ()
evalAs_ eTyp code = do
  ctx <- ask
  val <- liftIO $ evalRaw ctx eTyp code
  checkIsException "evalAs_" ctx val
  freeJSValue val



{-|
More efficient than 'eval' if we don't care about the value of the expression,
e.g. if we are evaluating a function definition or performing other side-effects such as
printing to console/modifying state.
-}
eval_ :: (MonadThrow m, MonadReader JSContextPtr m, MonadIO m) => ByteString -> m ()
eval_ = evalAs_ Global


fromJSValue_ :: (MonadCatch m, MonadReader JSContextPtr m, MonadIO m) => JSValue -> m Value
fromJSValue_ val = do
  ctx <- ask
  jsToJSON ctx val



-- fromJSValue :: (Aeson.FromJSON a, MonadCatch m, MonadReader JSContextPtr m, MonadIO m) => JSValue -> m a
-- fromJSValue val = do
--   jsonval <- fromJSValue_ val

--   case Aeson.fromJSON jsonval of
--     Aeson.Success a -> return a
--     Aeson.Error err -> throwM $ InternalError err



{-|
Takes a value with a defined 'ToJSON' instance. This value is marshalled to a 'JSValue'
and passed as an argument to the callback function, provided as the second argument to 'withJSValue'
-}
withJSValue :: (MonadMask m, MonadReader JSContextPtr m, MonadIO m, Aeson.ToJSON a) => a -> (JSValue -> m b) -> m b
withJSValue v f = do

  ctx <- ask
  val <- jsonToJSValue ctx (Aeson.toJSON v)
  f val `finally` freeJSValue val




callRaw :: (MonadThrow m, MonadIO m) => JSContextPtr -> ByteString -> [JSValue] -> m JSValue
callRaw ctxPtr funName args = do
    globalObject <- liftIO $ [C.block| JSValue { return JS_GetGlobalObject($(JSContext *ctxPtr)); } |]

    fun <- jsGetPropertyStr ctxPtr globalObject funName

    ty <- jsIs ctxPtr fun
    case ty of
      JSTypeFromTag JSTagException -> do
        err <- getErrorMessage ctxPtr
        throwM $ JSException "callRaw" err
      JSTypeFromTag JSTagUndefined -> throwM $ JSValueUndefined $ toS funName
      JSTypeFromTag JSTagPtr -> do
        -- Micro QuickJS: Stack-based calling
        res <- liftIO $ withArrayLen args $ \len argv -> jsCall ctxPtr fun (fromIntegral len) argv
        return res
      _ -> throwM $ JSValueIncorrectType {name = toS funName, expected = JSTypeFromTag JSTagPtr, found = ty }


-- call :: (MonadThrow m, MonadReader JSContextPtr m, MonadIO m) => String -> [JSValue] -> m JSValue
-- call funName args = do
--   ctx <- ask
--   val <- callRaw ctx funName args
--   checkIsException ctx val
--   return val



call :: (MonadMask m, MonadReader JSContextPtr m, MonadIO m) => ByteString -> [JSValue] -> m Value
call funName args = do
  ctx <- ask
  val <- callRaw ctx funName args
  jsToJSON ctx val `finally` freeJSValue val


freeJSValue :: (MonadThrow m, MonadReader JSContextPtr m, MonadIO m) => JSValue -> m ()
freeJSValue val = do
  ctx <- ask
  liftIO $ jsFreeValue ctx val

{-|
This function initialises a new JS runtime and performs the given computation within this context.

For example, we can evaluate an expression:

>mquickjs $ do
>  res <- eval "1+2"
>  liftIO $ print res

Declare a function and call it on an argument:

>mquickjs $ do
>  _ <- eval_ "f = (x) => x+1"
>  res <- eval "f(2)"
>  liftIO $ print res

Pass a Haskell value to the JS runtime:

>mquickjs $ do
>  _ <- eval_ "f = (x) => x+1"
>  res <- withJSValue (3::Int) $ \x -> call "f" [x]
>  liftIO $ print res

-}
mquickjs :: MonadIO m => ReaderT (Ptr JSContext) m b -> m b
mquickjs = mquickjsWithMemory (10 * 1024 * 1024)  -- 10MB default

mquickjsWithMemory :: MonadIO m => Int -> ReaderT (Ptr JSContext) m b -> m b
mquickjsWithMemory memSize f = do
  (memBuf, ctx) <- liftIO $ do
    _memBuf <- mallocBytes memSize
    _ctx <- [C.block| JSContext * {
      return JS_NewContext($(uint8_t *_memBuf), $(size_t memSizeC), &js_stdlib);
    } |]
    return (_memBuf, _ctx)

  res <- runReaderT f ctx
  cleanup memBuf ctx
  return res
  where
    memSizeC = fromIntegral memSize
    cleanup memBuf ctx = liftIO $ do
      jsFreeContext ctx
      free memBuf

{-|
This env differs from regular 'mquickjs', in that it wraps the computation in the 'runInBoundThread' function.
This is needed when running the Haskell program mutithreaded (e.g. when using the testing framework Tasty),
since mquickjs does not like being called from an OS thread other than the one it was started in.
Because Haskell uses lightweight threads, this might happen if threaded mode is enabled, as is the case in Tasty.
This problem does not occur when running via Main.hs, if compiled as single threaded...
For more info see the paper [Extending the Haskell Foreign Function Interface with Concurrency](https://simonmar.github.io/bib/papers/conc-ffi.pdf)
-}
mquickjsMultithreaded :: MonadUnliftIO m => ReaderT (Ptr JSContext) m b -> m b
mquickjsMultithreaded = mquickjsMultithreadedWithMemory (10 * 1024 * 1024)  -- 10MB default

mquickjsMultithreadedWithMemory :: MonadUnliftIO m => Int -> ReaderT (Ptr JSContext) m b -> m b
mquickjsMultithreadedWithMemory memSize f
  | rtsSupportsBoundThreads = do
    (u :: UnliftIO m) <- askUnliftIO

    liftIO $ runInBoundThread $ do
      memBuf <- mallocBytes memSize
      ctx <- [C.block| JSContext * {
        return JS_NewContext($(uint8_t *memBuf), $(size_t memSizeC), &js_stdlib);
      } |]

      res <- unliftIO u $ runReaderT f ctx
      cleanup memBuf ctx
      return res
  | otherwise = mquickjsWithMemory memSize f
  where
    memSizeC = fromIntegral memSize
    cleanup memBuf ctx = do
      jsFreeContext ctx
      free memBuf