Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
SkyyySi opened this issue Feb 1, 2025 · 0 comments
Open

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

SkyyySi opened this issue Feb 1, 2025 · 0 comments

Comments

@SkyyySi
Copy link

SkyyySi commented Feb 1, 2025

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, ->))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant