Log In  

Not sure if this is actually a bug or expected behaviour. Seems like a bug to me. That said, don't know if it's a problem with PICO-8 or just with Lua in general.

Here's the problem. If I define a metatable using the __metatable property then the __tostring method doesn't get called automatically (by print for example) when expected.

m = {}
x = setmetatable(
   {},
   {
      __index=m,
      __metatable=m,
      __tostring=function(self)
     return 'blah blah blah'
      end
   }
)

print(x)
print(tostring(x))

Output:

[table]
blah blah blah

So, I can call the tostring() function explicitly but it's not called automatically by print as I would expect. If I leave out the __metatable property though it works as expected:

m = {}
x = setmetatable(
   {},
   {
      __index=m,
      __tostring=function(self)
     return 'blah blah blah'
      end
   }
)

print(x)
print(tostring(x))

Output:

blah blah blah
blah blah blah

Putting the __tostring function in m also doesn't work:

m = {
   __tostring=function(self)
      return 'blah blah blah'
   end
}
x = setmetatable(
   {},
   {
      __index=m,
      __metatable=m
   }
)

print(x)
print(tostring(x))

Output:

table: 0x30bb9cc
table: 0x30bb9cc

Nor does giving m its own metatable with a __tostring method:

m = setmetatable({}, {
   __tostring=function(self)
      return 'blah blah blah'
   end
})
x = setmetatable(
   {},
   {
      __index=m,
      __metatable=m
   }
)

print(x)
print(tostring(x))

Output:

[table]
table: 0x30bb9cc

The __metatable property doesn't seem to interfere with other metamethods. Haven't checked them all though. Example:

m = {}
x = setmetatable(
   {},
   {
      __index=m,
      __metatable=m,
      __add=function(a, b)
     return 1
      end,
      __tostring=function(self)
     return 'blah blah blah'
      end
   }
)

print(x)
print(x + x)

Output:

[table]
1

This isn't really a major problem, I'm mostly just messing around but it seems weird so might be worth taking a look at at some point.

P#126465 2023-03-01 12:55

The __metatable value doesn't do quite what you think it does.

There is always a C-side native value in a table's native struct, holding the true metatable. Under normal circumstances, getmetatable(t) will return the value from the struct that holds info about t. However, if t contains a __metatable value, it will return that instead. This can be used to hide or protect the internal value from external Lua code, but the internal C code of the Lua interpreter usually looks directly at the internal value. The value doesn't even have to be a table, it just has to be non-nil so that calls to getmetatable(t) don't get t's true metatable.

That's why you're getting varying results: tostring() wraps the original internal C function in the Lua interpreter that has access to the true value, but I suspect zep's implementation of print() is directly or indirectly checking getmetatable() to see if there's a table attached, and then calling its __tostring() method.

More details here: https://www.lua.org/pil/13.3.html

I guess it's up to @zep to say if this is intended or not. To my mind, I think print() should probably be using the internal value, rather than calling getmetatable().

P#126528 2023-03-02 23:04 ( Edited 2023-03-02 23:07)

@Felice
Thanks! Yeah, I figured that's what was going on. I probably should have just given an example of what I was trying to do which was to create "types" which could be extended on an ad hoc basis sort of like this:

point_methods = {}
function point(x, y)
   return setmetatable(
      {x, y},
      {
         __index=point_methods,
         __metatable=point_methods,
         __tostring=function(self)
             return '('..self[1]..','..self[2]..')'
         end
      }
   )
end

p = point(1, 2)
getmetatable(p).some_method=function()
   -- do something
end

-- some_method works on all point objects not just p
p2 = point(3, 4)
p2:some_method() -- does the thing with p2

But adding methods to the actual metatable doesn't make them callable on the objects which is why I'm using both __index (so the methods are callable and available to all objects of that type) and __metatable (so getmetatable returns the index object to be extended and not the actual metatable.)

I realized that was a bad way to do it—honestly doing it at all is probably of questionable utility but I'm just playing around—and using __metatable isn't necessary at all but it did uncover this problem which seemed worth bringing up.

> "To my mind, I think print() should probably be using the internal value, rather than calling getmetatable()."

That's my thought also. Since calling tostring() explicitly worked my first fix was to just create a drop in replacement for print which did the trick.

_print = print
function print(s, ...)
   _print(tostring(s), unpack({...}))
end
P#126534 2023-03-03 03:13

@jasondelaat
Yeah, that seems like a reasonable bodge for the time being.


As an aside, I have two questions:

1) Why are you calling tostring() rather than tostr()? I have to admit that I didn't realize zep was exposing the vanilla Lua version, so I was wondering what the differences are and why you'd choose the vanilla tostring() over the PICO-8-specific tostr(). They do appear to be different function references, so I assume they behave differently. Edit: I notice they stringify a table reference differently, for instance.

2) Is there a reason you're using unpack({...}) to change the ... special tuple into a table, only to immedialy unpack it back into a tuple, when you could just write `_print(tostring(s), ...)"?

By the way, you should return the result of print(), since not only will that preserve the full behavior, but it'll also turn your code into proper tail-recursion, which is technically faster in the interpreter, though I dunno if @zep actually gives us credit for doing that by not costing as many cycles on the PICO-8 virtual CPU:

_print, print = print, function(s, ...)
   return _print(tostring(s), ...)
end

(I also demonstrate how I like to do function replacement, just in case you find it interesting. Not everyone would prefer it this way, and in PICO-8 it's the same number of tokens, so it's purely cosmetic, but hey, now you know about it, in case you like it. I do it like this because it makes it hard to accidentally refactor the _print= line away from the replacement code.)

P#126838 2023-03-09 07:53 ( Edited 2023-03-09 08:08)
1

@Felice

The honest, though unfortunate, answer to both of your questions basically boils down to, "oops."

  1. I actually just forgot that tostr() was the PICO-8 version and figured since the metamethod is __tostring the function must also be tostring(). And since it worked, it didn't even occur to me that it might be a mistake. I wonder if the recent updates with the preprocessor may have exposed tostring() unintentionally?

  2. This is just me running on auto-pilot without thinking. Usually with a variable number of arguments I'll do something like this:
function f(...)
   local args = {...}
   -- do stuff with args
   return some_other_func(unpack(args))
end

So, I'm pretty sure I did that, then realized I wasn't doing anything with the arguments so why not just inline the {...} and then completely failing to ask the next obvious question of why I need to pack/unpack the arguments at all.

So yeah, basically oops.

I actually didn't realize that print returned a value so that's good to know. I did know that Lua optimized tail calls but it honestly never occurred to me take advantage of that in simple situations like this. It would be interesting to know if it makes a difference on the virtual CPU. Definitely worth keeping in mind anyway.

P#126840 2023-03-09 10:48

[Please log in to post a comment]

Follow Lexaloffle:          
Generated 2024-04-20 07:51:48 | 0.028s | Q:18