Yuescript icon indicating copy to clipboard operation
Yuescript copied to clipboard

[Feature request] Add a Lua 5.1 polyfill for __len and rawlen

Open SkyyySi opened this issue 10 months ago • 2 comments

The length-operator isn't overloadable in PUC Lua 5.1 at all, and can only be overloaded in LuaJIT when using a build with the compilation-flag -DLUAJIT_ENABLE_LUA52COMPAT enabled (which isn't the case for basically all pre-built distributions).

I would appreciate it if a polyfill were to be automatically inlined when targeting Lua 5.1. A basic version could look like this:

global rawlen = (object) ->
    #object

_len_0 = (object) ->
    if metatable := getmetatable(object)
        if len_meta := rawget(metatable, "__len")
            return len_meta(object)

    #object

It could then be inserted whenever getting the length of an object, i.e.:

foo = ["x", "y", "z"]
bar = #foo

would compile to

rawlen = function(object)
    return #object
end
local _len_0
_len_0 = function(object)
    -- ...
end
local foo = {
    "x",
    "y",
    "z"
}
local bar = _len_0(foo)

The code above does work as-is, but I would personally not recommend using that. Instead, the code below is more robust / "correct", since it handles some additional edge cases.

--- Check whether this polyfill is actually needed before inserting it. It is
--- only required when using Lua 5.1, as well as not using LuaJIT with Lua 5.2
--- compatibility mode enabled. This is mainly here to avoid doing unnecessary
--- computations when creating a cross-version script.
const _len_0 = if (_G._VERSION == "Lua 5.1") and (_G.rawlen == nil)
    import type, error, rawget, select, debug from _G

    global rawlen = (object) ->
        #object

    const _getmetatable_0 = do
        const gloabl_getmetatable = _G.getmetatable

        --- The regular / global `getmetatable()`-function can be overloaded
        --- using the `__metatable`-field of a metatable. This is basically
        --- never used in practice (because why on earth would you do this?),
        --- but for the sake of correctness, this wrapper at least prevents
        --- issues with `__metatable` being set to an unexpected type.
        const wrapper = (object) ->
            const metatable = gloabl_getmetatable(object)

            if type(metatable) == "table"
                metatable
            else
                nil

        --- If possible, `debug.getmetatable()` is used instead of the
        --- global `getmetatable()`-function. This is because the latter
        --- could get tricked by something like this:
        ---
        --- ```yuescript
        --- tb = {
        ---     <metatable>: {
        ---         <len>: () => 5
        ---     }
        ---     <len>: () => 3
        --- }
        --- 
        --- --- This may incorrectly print 5 instead of 3 or 0
        --- print(#getmetatable(tb))
        --- ```
        ---
        --- Note that some environments disable access to the `debug`-library.
        if (type(debug) == "table") and (type(debug.getmetatable) == "function")
            debug.getmetatable
        else
            wrapper

    const get_function_name = if (type(debug) == "table") and (type(debug.getinfo) == "function")
        import getinfo from debug
        () -> getinfo(2, "n").name
    else
        () -> "_len_0"

    (...) ->
        do
            const argc = select("#", ...)
            if argc != 1
                const function_name = get_function_name()
                error("function %s() expected exactly one argument, got: %d"::format(
                    function_name
                    argc
                ))

        const object = ...

        do
            --- Throw an error if the type of `object` doesn't support getting
            --- it's length.
            const type_of_object = type(object)
            if type_of_object not in ["string", "table", "userdata"]
                --- This is the same error message that Lua would throw when
                --- doing something like this:
                ---
                --- ```lua
                --- print(#(false))
                --- ```
                error("attempt to get length of a %s value"::format(
                    type_of_object
                ))

        const metatable = _getmetatable_0(object)

        --- `object` has no metatable -> return its raw length.
        if metatable == nil
            return #object

        --- Use `rawget()` to prevent being tricked by `__index`, e.g.:
        ---
        --- ```yuescript
        --- tb = {
        ---     <>: {
        ---         <index>: {
        ---             __len: () => 7
        ---         }
        ---     }
        --- }
        ---
        --- --- This will incorrectly print 7 instead of throwing an error
        --- print(getmetatable(tb).__len(tb))
        --- ```
        const len_meta = rawget(metatable, "__len")

        --- `object` has no `__len`-metamethod -> return its raw length.
        --- Note: `__len` might also be a callable table or userdata-object
        --- instead of a function, which is why no strict type check is
        --- performed here.
        if len_meta == nil
            return #object

        len_meta(object)
else
    (object) ->
        #object

And here's a bunch of tests:

assert(_len_0({
    "foo"
    "bar"
    "biz"
    "baz"

    n: 3

    <len>: () => @n
}) == 3)

assert(_len_0({
    <>: {
        <index>: {
            __len: () => 7
        }
    }
}) == 0)

assert(_len_0({
    <>: {
        <metatable>: {
            __len: () => 7
        }
    }
}) == 0)

assert(_len_0({
    <metatable>: {
        <len>: () => 5
    }
    <len>: () => 3
}) == 3)

assert(_len_0("test") == 4)

assert(not pcall(_len_0, 123))

assert(not pcall(_len_0, ->))

SkyyySi avatar Feb 01 '25 17:02 SkyyySi

Thanks for the suggestion! What you're proposing would essentially involve adding a runtime library for YueScript. However, I'm recently considering implementing a feature that would generate the runtime module separately, rather than embedding the library code in every individual file. This approach might offer some advantages in terms of maintainability and efficiency.

pigpigyyy avatar Feb 19 '25 01:02 pigpigyyy

Can we just have jit as another target? It is based on 5.1, with some features on 5.2 and 5.3.

gphg avatar May 11 '25 09:05 gphg