[MUD-Dev] How much should be offloaded to Scripting?

Jay Carlson nop at mitre.org
Thu Feb 19 18:47:56 New Zealand Daylight Time 2004

On Fri, 2004-02-13 at 17:43, Brian Hook wrote:
> On Thu, 12 Feb 2004 17:38:50 -0800, Koster, Raph wrote:
>> I've been following this discussion, and I just want to play
>> Devil's Advocate here. I say that because I am a huge proponent
>> of scripting, but I've been bitten hard by it several times
> There are implementation issues that can really, REALLY kick your
> ass. Specifically, depending on the language choice, the
> availability of tools, the ability to catch common (static)
> errors, type checking, debugging, profiling, etc.
> I'm using Lua, and it's a love-hate relationship, because stupid
> errors that a typed language would catch are caught before they
> have a chance to screw you up.
>   function foo( a, b, c )
>      ...
>   end
>   foo( c, b, a ) -- assuming these are different types, the compiler
>                  -- saves your ass
> or something simple like this:
>   c.nname = "Davey Jones"
> In Lua, that will just automatically create a new slot in the 'c'
> table called 'nname', and that's not what you want.

(including some background for the list; I guess some is Lua
advocacy, but bear with me)

  <EdNote: At some point we need to differentiate between generic
  programming concerns and programming concerns that are either
  particularly relevent or unique to MUDs.  This doesn't mean I want
  to curb this thread, rather that I'm suggesting an eye to
  direction for its future.>

I think one thing that distinguishes the really good scripting
languages from the merely mediocre is how well you can deal with
issues of the language inside the language, without losing style
points in the code you read and write.

Lua is nicer than many other scripting languages because many
mechanisms are not built into the language itself, but
metamechanisms are.  OK, there's no hope of detecting this situation
at compile time, but in this case, it is relatively easy to detect
this condition at runtime.  Here's what it could look like:

  c = {}
  addslot(c, "name")
  addslot(c, "health")
  c.name = "Davey Jones"
  assert(c.health == nil)
  c.nname = "foo" --> error raised
  addslot(c, "nname")
  c.nname = "arrrr"

In Lua, a table may have a metatable.  If it does, when a key not
already present in the table is assigned to, the __newindex method
is called instead:

  t = {}
  setmetatable(t, mt)
  t.notpresent = "foo"   --> mt.__newindex(t, "notpresent", "foo")

To implement this explicit slot-checking, keep a copy of the valid
slots around, and check it:

  slot_metatable = {}

  function addslot(t, slotname)
    setmetatable(t, slot_metatable) -- only need to do this once, really
    -- Keep the list of valid slots in t._slots.  To avoid
    -- recursion, use rawget() to skip the metatable access
    local newslots = rawget(t, "_slots") or {}
    newslots[slotname] = true
    rawset(t, "_slots", newslots)

  function slot_metatable.__newindex(t, key, value)
    if t._slots[key] then
      rawset(t, key, value)
      error("attempt to assign to non-existent slot "..key)

  function slot_metatable.__index(t, key)
    if t._slots[key] then
      -- Because __index is only called when t[key] is not
      -- already present, we know this has to be nil
      return nil
      error("attempt to read from non-existent slot "..key)

This adds no runtime overhead except when manipulating slots that
are nil.

In a real system (instead of a post to a mailing list) you would
probably use a single metatable for each class, shared by
instances. That class metatable could contain the list of valid
slots for instances, or you could allow instances to have additional

Because of this flexibility, I like to think of Lua as a family of
languages.  There is no object system shipped with the distribution,
so lots of people write their own.

Wait, read that last sentence again.  People write their own object
systems.  The language must be doing something right if it's that

This sounds like an interoperability disaster, but it's usually not
too bad, as there's a common method call syntax.


OK, back up at the top I said that there was nothing to be done
about the fact that in dynamic scripting languages there's nothing
to be done about the fact that

  some_obj.existence = true


  if some_obj.existance then [...]

is not going to be caught at compile time, because potentially
somebody is going to intend to make some_obj have both "existance"
and "existence" fields.

There are some things that can be done about this, even though the
general case is hopeless.  Let's say we have code written in the
general style above, where slots are predeclared:

  addslot(some_obj, "existence")
  addslot(another_obj, "health")

  -- [...] in some other file...

  some_obj.existance = true
  some_obj.health = 1.0

We can preprocess all our source files to get a list of all the
valid slot names by gathering up the contents of all addslot()
calls.  (We could also run all the initialization code with a
special version of addslot() that dumped its second argument to a
file, of course.)

Given this information, we can now make a second pass through all
the source looking for references to slots that don't exist.
Although we could modify the compiler, the simplest way to do this
is to go poking through the bytecode:

  /tmp prentice$ luac -l foo.lua 
  main <foo.lua:0> (4 instructions, 16 bytes at 0x804b160)
  0 params, 2 stacks, 0 upvalues, 0 locals, 2 constants, 0 functions
          1       [1]     GETGLOBAL       0 0     ; some_obj
          2       [1]     LOADBOOL        1 1 0
          3       [1]     SETTABLE        0 251 1 ; "existance" 
          4       [1]     RETURN          0 1 0

I hope you weren't eating when you read that. :-)

This won't catch "some_obj.health" case, since there is a slot on a
different object with that name.  But it will make it a little
easier to catch the common problem of misspellings.  Smalltalk IDEs
already do this when they see a selector not mentioned anywhere else
in the system.

Another Lua annoyance is forgetting to mark variables as local:

  for k,v in t do
    stringv = tostring(v)
    -- oops, now we have a global stringv

and now that I think of it, the above technique works even better
for bad global detection at compile time.  Maybe I'll have to
implement it. :-)

The technique of trawling bytecode disassembly has been used a few
times in MOO, as nobody seems interested in writing a MOO-code to
syntax tree binding.  The above technique for finding misspelled
words might be fun to try there.  But it would break my favorite
first line of code:

  1:  caller == this || lose;

which throws a "variable not found" error if somebody is trying to
call this private verb from elsewhere...

Jay Carlson <nop at mitre.org>
MUD-Dev mailing list
MUD-Dev at kanga.nu

More information about the MUD-Dev mailing list