Yuescript
Yuescript copied to clipboard
[Feature request] Add a Lua 5.1 polyfill for __len and rawlen
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, ->))
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.