Module:Arguments: Difference between revisions

    m>Mr. Stradivarius
    (fix undefined next() behaviour bug by checking for metatable.donePairs in the __index metamethod; also, format the module so it fits into 80 characters)
    m>Mr. Stradivarius
    (memoize nils using a separate nilArgs table; this fixes a bug where nil values were being iterated over with pairs())
    Line 7: Line 7:


    local arguments = {}
    local arguments = {}
    local nilArg = {} -- Used for memoizing nil arguments in metaArgs.


    -- Generate four different tidyVal functions, so that we don't have to check the
    -- Generate four different tidyVal functions, so that we don't have to check the
    Line 76: Line 74:
    luaArgs = frame
    luaArgs = frame
    end
    end
     
    -- Set up the args and metaArgs tables. args will be the one accessed from
    -- Set the order of precedence of the argument tables. If the variables are
    -- functions, and metaArgs will hold the actual arguments. The metatable
    -- nil, nothing will be added to the table, which is how we avoid clashes
    -- connects the two together.
    -- between the frame/parent args and the Lua args.
    local args, metaArgs, metatable = {}, {}, {}
    local argTables = {fargs}
    setmetatable(args, metatable)
    argTables[#argTables + 1] = pargs
    argTables[#argTables + 1] = luaArgs


    --[[
    --[[
    Line 113: Line 112:
    end
    end
    end
    end
    --[[
    -- Set up the args, metaArgs and nilArgs tables. args will be the one
    -- accessed from functions, and metaArgs will hold the actual arguments. Nil
    -- arguments are memoized in nilArgs, and the metatable connects all of them
    -- together.
    --]]
    local args, metaArgs, nilArgs, metatable = {}, {}, {}, {}
    setmetatable(args, metatable)


    local function mergeArgs(iterator, tables)
    local function mergeArgs(iterator, tables)
    Line 124: Line 132:
    for _, t in ipairs(tables) do
    for _, t in ipairs(tables) do
    for key, val in iterator(t) do
    for key, val in iterator(t) do
    local metaArgsVal = metaArgs[key]
    if metaArgs[key] == nil then
    if metaArgsVal == nil or metaArgsVal == nilArg then
    local tidiedVal = tidyVal(key, val)
    local tidiedVal = tidyVal(key, val)
    if tidiedVal == nil then
    if tidiedVal == nil then
    metaArgs[key] = nilArg
    nilArgs[key] = true
    else
    else
    metaArgs[key] = tidiedVal
    metaArgs[key] = tidiedVal
    Line 136: Line 143:
    end
    end
    end
    end
    -- Set the order of precedence of the argument tables. If the variables are
    -- nil, nothing will be added to the table, which is how we avoid clashes
    -- between the frame/parent args and the Lua args.
    local argTables = {fargs}
    argTables[#argTables + 1] = pargs
    argTables[#argTables + 1] = luaArgs


    --[[
    --[[
    -- Define metatable behaviour. Arguments are memoized in the metaArgs table,
    -- Define metatable behaviour. Arguments are memoized in the metaArgs table,
    -- and are only fetched from the argument tables once. Nil arguments are
    -- and are only fetched from the argument tables once. Fetching arguments
    -- also memoized using the nilArg variable in order to increase performance.
    -- from the argument tables is the most resource-intensive step in this
    -- Also, we keep a record in the metatable of when pairs and ipairs have
    -- module, so we try and avoid it where possible. For this reason, nil
    -- been called, so we do not run pairs and ipairs on fargs and pargs more
    -- arguments are also memoized, in the nilArgs table. Also, we keep a record
    -- than once. We also do not run ipairs on fargs and pargs if pairs has
    -- in the metatable of when pairs and ipairs have been called, so we do not
    -- already been run, as all the arguments will already have been copied
    -- run pairs and ipairs on the argument tables more than once. We also do
    -- over.
    -- not run ipairs on fargs and pargs if pairs has already been run, as all
    -- the arguments will already have been copied over.
    --]]
    --]]


    metatable.__index = function (t, key)
    metatable.__index = function (t, key)
    --[[
    -- Fetches an argument when the args table is indexed. First we check
    -- to see if the value is memoized, and if not we try and fetch it from
    -- the argument tables. When we check memoization, we need to check
    -- metaArgs before nilArgs, as both can be non-nil at the same time.
    -- If the argument is not present in metaArgs, we also check whether
    -- pairs has been run yet. If pairs has already been run, we return nil.
    -- This is because all the arguments will have already been copied into
    -- metaArgs by the mergeArgs function, meaning that any other arguments
    -- must be nil.
    --]]
    local val = metaArgs[key]
    local val = metaArgs[key]
    if metatable.donePairs or val ~= nil then
    if val ~= nil then
    --[[
    return val
    -- We have either memoized the argument already, or pairs has been
    elseif metatable.donePairs or nilArgs[key] then
    -- called, meaning that mergeArgs has already copied all of the
    return nil
    -- available arguments into the metaArgs table. We need to check for
    -- pairs as we can't memoize nils to the metaArgs table while pairs
    -- is iterating. Adding new instances of nilArg to the metaArgs
    -- table while pairs is iterating over it produces undefined
    -- behaviour in the next() function.
    --]]
    if val == nilArg then
    return nil
    else
    return val
    end
    end
    end
    for _, argTable in ipairs(argTables) do
    for _, argTable in ipairs(argTables) do
    local argTableVal = tidyVal(key, argTable[key])
    local argTableVal = tidyVal(key, argTable[key])
    if argTableVal == nil then
    if argTableVal == nil then
    metaArgs[key] = nilArg
    nilArgs[key] = true
    else
    else
    metaArgs[key] = argTableVal
    metaArgs[key] = argTableVal
    Line 186: Line 187:


    metatable.__newindex = function (t, key, val)
    metatable.__newindex = function (t, key, val)
    -- This function is called when a module tries to add a new value to the
    -- args table, or tries to change an existing value.
    if options.readOnly then
    if options.readOnly then
    error(
    error(
    Line 201: Line 204:
    )
    )
    elseif val == nil then
    elseif val == nil then
    metaArgs[key] = nilArg -- Memoize nils.
    --[[
    -- If the argument is to be overwritten with nil, we need to erase
    -- the value in metaArgs, so that __index, __pairs and __ipairs do
    -- not use a previous existing value, if present; and we also need
    -- to memoize the nil in nilArgs, so that the value isn't looked
    -- up in the argument tables if it is accessed again.
    --]]
    metaArgs[key] = nil
    nilArgs[key] = true -- Memoize nils.
    else
    else
    metaArgs[key] = val
    metaArgs[key] = val
    Line 208: Line 219:


    metatable.__pairs = function ()
    metatable.__pairs = function ()
    -- Called when pairs is run on the args table.
    if not metatable.donePairs then
    if not metatable.donePairs then
    mergeArgs(pairs, argTables)
    mergeArgs(pairs, argTables)
    Line 213: Line 225:
    metatable.doneIpairs = true
    metatable.doneIpairs = true
    end
    end
    return function (t, k)
    return pairs(metaArgs)
    local nk, val = next(metaArgs, k)
    if val == nilArg then
    val = nil
    end
    return nk, val
    end
    end
    end


    metatable.__ipairs = function ()
    metatable.__ipairs = function ()
    -- Called when ipairs is run on the args table.
    if not metatable.doneIpairs then
    if not metatable.doneIpairs then
    mergeArgs(ipairs, argTables)
    mergeArgs(ipairs, argTables)
    metatable.doneIpairs = true
    metatable.doneIpairs = true
    end
    end
    return function (t, i)
    return ipairs(metaArgs)
    local val = metaArgs[i + 1]
    if val == nil then
    return nil
    elseif val == nilArg then
    val = nil
    end
    return i + 1, val
    end, nil, 0
    end
    end