[Feature request] Add a Lua 5.1 polyfill for __len and rawlen
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
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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