Class Models for Lua

# Value Matcher

## `loop.debug.Matcher`

Class of objects used to compare pairs of values according to some criteria of similarity. By default, it matches pairs of same values or structurally isomorphic tables (including their meta-tables). Functions with same bytecodes, upvalue contents and isomorphic environments also match. However, such matching criteria may be redefined. This class is useful for implementing automated test mechanisms for Lua applications.

Each object provides a set of methods for comparison of values passed as parameters. The function must return `true` if the values match or `false` otherwise. When a pair of distinct values are matched their correspondence is stored in the object instance. Therefore, such correspondence is maintained in all further comparisons made by the same instance.

## Behavior

### Fields

`boolean`
  Field that defines the method implementation used to compare two different boolean values passed as parameters. The function must return `true` if the values match or `false` otherwise. If this field evaluates to `false` then boolean values are compared for equality only. The default value for this field is `nil`.
`envkey`
  Special value used to identify a function environment retrieval. See method `error` for further information.
`environment`
  Field that defines the function used to retrieve the environment of a function. If this field evaluates to `false` then function environments are ignored during matching. The default value for this field is function `getfenv` of the Lua library.
`isomorphic`
  If this field evaluates to `true` then table matching should be isomorphic, i.e. there is a one to one relation of each field of the first table to the second. However, if its value evaluates to `false` then table matching just guarantee that all fields of the first table have a matching field in the second. The default value for this field is `true`.
`metakey`
  Special value used to identify a meta-table retrieval. See method `error` for further information.
`metatable`
  Field that defines the method implementation used to compare meta-tables of matching values passed as parameters. The function must return `true` if the values match or `false` otherwise. If this field evaluates to `false` then meta-tables are ignored during matching. The default value for this field is method `match` provided by the class.
`function`
  Field that defines the method implementation used to compare two different function values passed as parameters. The function must return `true` if the values match or `false` otherwise. If this field evaluates to `false` then function values are compared for equality only. The default value for this field is method `matchfunction` provided by the class.
`number`
  Field that defines the method implementation used to compare two different number values passed as parameters. The function must return `true` if the values match or `false` otherwise. If this field evaluates to `false` then number values are compared for equality only. The default value for this field is `nil`.
`string`
  Field that defines the method implementation used to compare two different string values passed as parameters. The function must return `true` if the values match or `false` otherwise. If this field evaluates to `false` then string values are compared for equality only. The default value for this field is `nil`.
`table`
  Field that defines the method implementation used to compare two different table values passed as parameters. The function must return `true` if the values match or `false` otherwise. If this field evaluates to `false` then table values are compared for equality only. The default value for this field is method `matchtable` provided by the class.
`thread`
  Field that defines the method implementation used to compare two different thread values passed as parameters. The function must return `true` if the values match or `false` otherwise. If this field evaluates to `false` then thread values are compared for equality only. The default value for this field is `nil`.
`upvalue`
  Field that defines the function used to retrieve the contents of function upvalues. If this field evaluates to `false` then upvalues contents are ignored during matching. The default value for this field is function `debug.getupvalue` of the Lua debug library if it is loaded at the moment this class is required.
`userdata`
  Field that defines the method implementation used to compare two different userdata values passed as parameters. The function must return `true` if the values match or `false` otherwise. If this field evaluates to `false` then userdata values are compared for equality only. The default value for this field is `nil`.

### Methods

`error(reason)`
  Method that creates a matching error message. The parameter `reason` is a string with a message describing the error reason. Whenever this method is called, the object instance stores as an array (i.e. using integer keys) the sequence of values that identifies the path to the mismatched field. However, indexes 0 and 1 are used to store the values being compared, that are respectively the values of parameters `other` and `value` that are passed to method `match`. Fields `metakey` and `envkey` hold special values that are used to identify a meta-table or function environment retrival, respectively.
`match(value, other)`
  Method that tries to match values `value` and `other`. If they match then it returns `true`. Otherwise, it return `false` and an error message. The error message format is created by method `error`. The default format is "<path>: <reason>", where <path> is a string identifying which part doesn't match. Upvalues are represented as table fields, for example if `value` is a table which field `func` is a function which upvalue `up` differ then the path informed will be `value.func.up`. The <reason> is one of the following messages:

  `"no match found"` | no similar field key found
  `"missing"` | missing field found in `other` table
  `"bytecodes not matched"` | bytecodes of two different function differ
  `"wrong match"` | other value (e.g. previously bound) was expected
  `"not matched"` | different objects found

`matchfunction(value, other)`
  Method that compares two different functions informed as parameters `value` and `other` according to their bytecodes and the criteria defined by fields `environment`, `upvalue` and `metatable`. If the two values matches, then it returns `true`, otherwise it returns `false` and an error message.
`matchtable(value, other)`
  Method that compares two different tables informed as parameters `value` and `other` according to their contents and the criteria defined by field `metatable`. If the two values matches, then it returns `true`, otherwise it returns `false` and an error message.

### Meta-Fields

`__mode = "k"`
  Defines that matched values are weak references thus if other references to those values are lost then they becomes garbage.

## Remarks

- Each object instance keeps a mapping of the matched objects, therefore to restart comparisons without previous mapping you should create another object instance.
- Since it is only possible to check upvalue contents, matched functions are not guaranteed to produce the same effects since the upvalue variables may be actually different even though they store the same value.
- Since `string.dump` produces an error instead of returning an error message silently, values with unmatched C functions raises the error produced by `string.dump`. To handle such situation, you should use a `pcall`. But be warned that this function is somewhat restricted since it can't be used with yielding co-routines.
- Methods used to compare two different values (i.e. fields `boolean`, `number`, `string`, `function`, `thread` and `userdata`) may store in the object instance a mapping of each machted pairs so that `self[value] == other and self[other] == value` in order to avoid further evaluation of the same pair and that they are matched to other values. However, for positive integer numbers this should be avoided because these values are used to store the sequence of values that identifies the current path of the values being compared.
- Non-isomorphic comparisions of tables that have other tables as key values does not work well due to ambiguities when trying to find the corresponding match for each key-value pair.

## Examples

### Feedback-loop test suite for pull-parsers.

    local Viewer  = require "loop.debug.Viewer"
    local Matcher = require "loop.debug.Matcher"

    local message = "Testcase %d failed: %s\n"

    function compare(file, cases)
      local previous = io.open(file)
      if previous then
        previous:close()
        previous = dofile(file)
      else
        previous = {}
      end

      local results = {}
      for index, source in ipairs(cases) do
        local expected = previous[index]
        local actual = parser:evaluate(source)
        if expected then
          local matcher = Matcher{ metatable = false }
          local success, errmsg = matcher:match(expected, actual)
          if not success then
            io.stderr:write(message:format(index, errmsg))
          end
        else
          expected = actual
        end
        results[index] = expected
      end

      local serializer = Viewer{
        output = assert(io.open(file, "w")),
        maxdepth = -1,
      }
      serializer.output:write("return ")
      serializer:write(results)
    end

    local oo         = require "loop.base"
    local Expression = require "loop.compiler.Expression"

    List   = oo.class()
    Label  = oo.class()
    Lambda = oo.class()
    Apply  = oo.class()

    parser = Expression{
      values = {
        number = "(%-?%d+)",
        variable = "([%a_][%w_]*)",
      },
      operators = {
        list   = " , ",
        label  = "' = ",
        lambda = "\ . ",
        apply  = "  ",
      },
      precedence = { {"list"},{"label"},{"lambda"},{"apply"} },
    }
    function parser:number(value)      return tonumber(value) end
    function parser:variable(value)    return value end
    function parser:label(name, def)   return Label{ name = name, def = def } end
    function parser:lambda(args, body) return Lambda{ args = args, body = body } end
    function parser:apply(func, args)  return Apply{ func = func, args = args } end
    function parser:list(a, d)
      if oo.classof(a) == List then
        a[#a+1] = d
        return a
      else
        return List{ a, d }
      end
    end

    compare("results.lua", {
      "'fat=\x.if(eq(x,0),1,mul(x,fat(sub(x,-1))))",
      "'addto=\(x,y).cons(sum(car x,y),addto(cdr x,y))",
      -- TODO: add more testcases here
    })

Copyright (C) 2004-2008 Tecgraf, PUC-Rio

This project is currently being maintained by Tecgraf at PUC-Rio.