IppClub / IppClub/YueScript

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

Open
#189 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Lua
Stars
613
Forks
45
PR merge metrics
No merged PRs in 30d

Description

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, ->))

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

No source files or test paths are named in the issue. Start by locating the compiler path for the length operator and Lua 5.1 target handling, then review existing cross-version polyfills and test conventions. Done means length expressions support the requested Lua 5.1 behavior, avoid unnecessary insertion for compatible runtimes, and cover the listed edge cases.

Written by the indexing model from the issue text.

Assessment

Tech stack
lua
Domain
compilers
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
30/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.