PetterS / quickjs

Thin Python wrapper of https://bellard.org/quickjs/
https://github.com/bellard/QuickJS
MIT License
175 stars 19 forks source link

2.0 roadmap and future #107

Open qwenger opened 2 years ago

qwenger commented 2 years ago

Rationale & background

Some of the current open issues and PRs introduce potentially backward-incompatible changes. Backward incompatibility is only allowed when bumping the major version. Therefore it makes sense to plan a broader array of changes.

The general goals are to make this package more user-friendly, cover more features from upstream and reinforce the interaction with Python.

Planned changes & targets

qwenger commented 2 years ago

Attempt of an API for v2.0:

_quickjs module:

    def from_js(value: Value) -> Any:
        """
        Rationale:
            - Why not automatic conversion of JS results to Python objects:
                - because not all JS values have natural native Python counterparts
                - to enable access to JS properties on all results
            - Why not a method of Value:
                - to avoid shadowing of JS properties as much as possible.
        XXX: how to handle corner cases?
            - conversion of numbers? To int or float?
            - conversion of complex objects? Via json dump/load?
            - conversion of other values? E.g. Symbol's, BigFloat's, ...
        """

    def get_context(value: Value) -> Context:

    class Context:
        @property
        def globalThis(self) -> Value:
        def eval(self, code: str) -> Value:
        def module(self, code: str) -> Value:
        def execute_pending_job(self) -> bool:
        def set_memory_limit(self, limit: int) -> None:
            """
            NOTE: need to check what the range of values is; internally there seems to be
                  some back-and-forth between size_t and int64_t, and the value -1 is used.
                  Also check if any value can be used to disable (or is -1 merely the max?).
            """
        def set_time_limit(self, limit: float) -> None:
            """
            NOTE: negative value to disable.
            XXX: also allow 0 to disable?
            """
        def set_max_stack_size(self, size: int) -> None:
            """
            NOTE: positive; 0 to disable.
            XXX: also allow negative to disable?
            """
        def memory(self) -> dict[str, int]:
        def gc(self) -> None:

        def from_py(self, value: Any) -> Value:
            """
            Rationale:
                - Why not a method of the module:
                    - because we need a specific Context to store the value in.
            XXX: how to handle corner cases?
                - conversion of int's? To Number or BigInt?
                - conversion of complex objects? Via json dump/load?
            """

        operators: namedtuple
            """
            Rationale:
                - We need a way to use JS operators from Python, but QuickJS does not export
                  them in the API, so we emulate them using functions.
                  Something like
                    - `JS_Eval("function add(v1, v2) {return v1 + v2;}")`
                    - `JS_Eval("function new_(v1, ...args) {return new v1(...args);}")`
                  XXX: use strict? May be useful for property access.
            """
        operators.aeq
        operators.naeq
            """
            Rationale:
                - It seems more pythonic to use `===` to emulate `__eq__`. So `==` is
                  the abstract equal rather than the regular one.
            """
        operators.eq
        operators.neq
        operators.gt
        operators.ge
        operators.lt
        operators.le
        operators.add
        operators.sub
        operators.mul
        operators.div
        operators.mod
        operators.neg
        operators.pos
        operators.pow
        operators.and
        operators.or
        operators.xor
        operators.not
        operators.lshift
        operators.rshift
        operators.urshift
            """
            unsigned right shift (>>>), does not exist natively in Python
            """
        operators.typeof
        operators.instanceof
        operators.new
        operators.in_

    class Value:
        def __get__(self, name: str) -> Value:
        def __set__(self, name: str, value: Any) -> None:
        def __delete__(self, name: str) -> None:
        def __getitem__(self, name: str) -> Value:
        def __setitem__(self, name: str, value: Any) -> None:
        def __delitem__(self, name: str) -> None:

        def __eq__(self, other: Any) -> bool:
        def __neq__(self, other: Any) -> bool:
        def __gt__(self, other: Any) -> bool:
        def __ge__(self, other: Any) -> bool:
        def __lt__(self, other: Any) -> bool:
        def __le__(self, other: Any) -> bool:
        def __add__(self, other: Any) -> Value:
        def __sub__(self, other: Any) -> Value:
        def __mul__(self, other: Any) -> Value:
        def __truediv__(self, other: Any) -> Value:
        def __mod__(self, other: Any) -> Value:
        def __neg__(self) -> Value:
        def __pos__(self) -> Value:
        def __pow__(self, other: Any) -> Value:
        def __and__(self, other: Any) -> Value:
        def __or__(self, other: Any) -> Value:
        def __xor__(self, other: Any) -> Value:
        def __not__(self, other: Any) -> Value:
        def __lshift__(self, other: Any) -> Value:
        def __rshift__(self, other: Any) -> Value:
        def __invert__(self) -> Value:
            """
            NOTE: mainly implemented through `operators.*`
            """

        def __abs__(self) -> Value:?
        def __int__(self) -> int:?
        def __float__(self) -> float:?
        def __index__(self) -> int:?
        def __round__(self) -> int:?
        def __trunc__(self) -> int:?
        def __floor__(self) -> int:?
        def __ceil__(self) -> int:?

        def __bool__(self) -> Value:

        def __repr__(self) -> str:?
        def __str__(self) -> str:?
        def __bytes__(self) -> str:?

        def __dir__(self) -> list[str]:?

        def __len__(self) -> int:?
        def __contains__(self) -> bool:?

    class Number(Value):
    class BigInt(Value):
    class String(Value):
    class Boolean(Value):
    class Null(Value):
    class Undefined(Value):
    class Symbol(Value):
    class Object(Value):
    class Function(Object):
        def __call__(self, *args) -> Value:

    class Array(Object):?
        """
        NOTE: to be avoided if possible; can we implement array capabilities directly on Value without
              clashing with Number/Map capabilities?
        """

    class JSError(Exception):
        value: Value
        """
        Rationale:
            - Why not directly wrapping JS errors in an Exception subclass:
                - because JS can use any value as an error, so we cannot wrap selectively,
                  therefore need to only wrap (with an indirection level) if the error bubbles
                  up all the way to Python.
        """
        __cause__
        """
        NOTE: set to the Python exception that caused it (typically because of a call to a wrapped
              Python callable), if any.
        """
qwenger commented 2 years ago

Integer conversion

... so it seems natural to map Number to float and BigInt to int.

However:

A few cases are clear:

The question is how to implement the other cases: conversion of integer Number's and conversion of int's. Ideally, both directions should be consistent with each other, and precision loss be avoided when possible.

One way is to have a Context-global max_int parameter, which defines the range [-max_int, max_int - 1]:

The case max_int == 0 corresponds to the natural mapping Number<->float, BigInt<->int. It would be the default. The case max_int == 2**32 in the JS->Py direction corresponds to the native representation in QuickJS. The cases max_int > 2**53 may lead to precision loss. The cases max_int > int(sys.float_info.max) - apart from the precision loss - will lead to all integer Number's converted to int.

But this way is not very transparent nor predictable.

Simpler would be to have a Context-global int_conversion_mode (exact name TBD):

Rationale for storing this setting in a Context-global: