Documentation for this module may be created at Module:Signatures/doc
-- a MediaWiki module
--[[
Syntax description:
<name> <attr...> ...;;
<modName> [modSyn];;
]]
---@alias docs.ModelKind "field" | "method" | "type"
---Origin of this documentation item.
---@alias docs.ModelOrigin
---| "Lua" Lua 5.2 built-in types and APIs.
---| "Figura" Figura types and APIs. Usually the default.
---| "abstract" Describes ideas, but no concrete implementation exists.
---| "typedef" Custom on-the-spot types, like function protocols or array types.
---@alias Set<T> {[T]: true}
---@class docs.DocModel
---@field kind docs.ModelKind
---@field name string?
---@field prefixHint string?
---@field attributes Set<string>
---@field origin docs.ModelOrigin
---@class docs.FieldModel : docs.DocModel
---@field kind "field"
---@field canRead boolean
---@field canWrite boolean
---@field readType docs.TypeModel?
---@field writeType docs.TypeModel?
---@class docs.MethodModel : docs.DocModel
---@field kind "method"
---@field signatures docs.MethodSignature[]
---@field isStatic boolean
---@class docs.MethodSignature.Parameter
---@field name string
---@field type docs.TypeModel
---@field vararg boolean?
---@class docs.MethodSignature
---@field returns docs.TypeModel[]
---@field parameters docs.MethodSignature.Parameter[]
---@class docs.TypeModel : docs.DocModel
---@field kind "type"
---@field typeKind "explicit" | "union"
---@class docs.ExplicitType : docs.TypeModel
---@field typeKind "explicit"
---@field link string?
---@class docs.UnionType : docs.TypeModel
---@field typeKind "union"
---@field anyOf docs.TypeModel[]
---@type metatable
local type_meta = {
__tostring = function (t)
return ("<type %s from %s>"):format(t.name or '(anonymous)', t.origin)
end
}
---@class docs.spool : docs.DocModel
---@field src string
---@field cursor integer
---@field cursorNext integer?
local spool = {}
---Create a new spool.
---@param src string
---@return docs.spool
function spool.new(src)
return setmetatable({
src = src,
cursor = 1
}, {__index = spool})
end
---@return integer cursor
function spool:consumeSpace()
self.cursor = self.src:match("%s*()", self.cursor)
return self.cursor
end
local unpack = unpack or table.unpack -- 5.2 compat
---Starts matching from the current cursor position, and recieves the next cursor position from a position capture.
---@param pattern string
---@param noMatchMsg string Format pattern with 1 %d to hold the cursor value.
---@return any ...
function spool:matchc(pattern, noMatchMsg)
local results = {self.src:match(pattern, self.cursor)}
if #results == 0 then error(noMatchMsg:format(self.cursor)) end
self.cursorNext = results[#results]
results[#results] = nil
return unpack(results)
end
---@return string
function spool:peek()
return self.src:sub(self.cursor, self.cursor)
end
function spool:adv()
if self:isAtEOF() then return end
self.cursor = self.cursor + 1
end
function spool:matchadv(pattern)
local _, to = self.src:find(pattern, self.cursor)
if to then
self.cursor = to + 1
return true
end
return false
end
function spool:commit()
if not self.cursorNext then error "spool:commit(): Nothing to commit" end
self.cursor = self.cursorNext
self.cursorNext = nil
end
function spool:isAtEOF() return self.cursor > #self.src end
local LUA_TYPES_URL = "https://www.lua.org/manual/5.2/manual.html#2.1"
---@type table<string, docs.ExplicitType>
local typeDefinitions = {}
---@type table<string, string>
local typeAlias = {}
local types = setmetatable({}, {
__index = function (t, k)
local resolvedName = k
local i = 0
while typeAlias[resolvedName] do
i = i + 1
if i > 10 then error "internal: alias chain too long" end
resolvedName = typeAlias[resolvedName]
end
return typeDefinitions[resolvedName]
end
})
---@param name string
---@param origin docs.ModelOrigin
---@param link string?
---@return docs.ExplicitType
local function newExplicitType(name, origin, link)
if typeDefinitions[name] then error "Type already exists" end
---@type docs.ExplicitType
local t = setmetatable({
kind = "type",
typeKind = "explicit",
name = name,
attributes = {},
origin = origin,
link = link
}, type_meta)
typeDefinitions[name] = t
return t
end
---@param anyOf docs.TypeModel[]
---@return docs.UnionType
local function newUnionType(anyOf)
local function flatten(typ)
if not typ.kind or typ.typeKind == "union" then
local iterator
if not typ.kind then
iterator = typ
else
iterator = typ.anyOf
end
local seen = {}
local res = {}
for _, typ2 in ipairs(iterator) do
for _, v in ipairs(flatten(typ2)) do
if not seen[v] then
res[#res + 1] = v
seen[v] = true
end
end
end
return res
elseif typ.typeKind == "explicit" then
return { typ }
end
error "No type"
end
---@type docs.UnionType
return setmetatable({
anyOf = flatten(anyOf),
attributes = {},
kind = "type",
origin = "abstract",
typeKind = "union"
}, {
__tostring = function (t)
local chunks = {}
for _, type in ipairs(t.anyOf) do
chunks[#chunks+1] = tostring(type)
end
return table.concat(chunks, ' | ')
end
})
end
---@param name string
---@param origin docs.ModelOrigin
---@return docs.ExplicitType
local function getOrDefineType(name, origin)
local t = types[name]
if t then return t end
return newExplicitType(name, origin, name)
end
---Create a new type definition type.
---@param typedefname string without #
---@return docs.TypeModel
local function newTypedef(typedefname)
local prefixed = "#"..typedefname
if typeDefinitions[prefixed] then return typeDefinitions[prefixed] end
---@type docs.ExplicitType
local t = setmetatable({
kind = "type",
typeKind = "explicit",
name = typedefname,
attributes = {},
origin = "typedef",
link = "#typedef_"..typedefname
}, type_meta)
typeDefinitions[prefixed] = t
return t
end
do -- builtin types
-- currying
---@param sourceName string
---@return fun(list: string[])
local function alias(sourceName)
return function(list)
for _, v in ipairs(list) do
typeAlias[v] = sourceName
end
end
end
newExplicitType("any", "abstract")
alias "any" { "AnyType" }
newExplicitType("void", "abstract")
newExplicitType("nil", "Lua", LUA_TYPES_URL)
alias "nil" { "null" }
newExplicitType("boolean", "Lua", LUA_TYPES_URL)
alias "boolean" { "Boolean" }
newExplicitType("number", "Lua", LUA_TYPES_URL)
alias "number" { "Number", "double", "float" }
newExplicitType("integer", "Lua", LUA_TYPES_URL)
alias "integer" { "Integer" }
newExplicitType("string", "Lua", LUA_TYPES_URL)
alias "string" { "String" }
newExplicitType("function", "Lua", LUA_TYPES_URL)
alias "function" { "Function" }
newExplicitType("userdata", "Lua", LUA_TYPES_URL)
alias "userdata" { "Userdata" }
newExplicitType("table", "Lua", LUA_TYPES_URL)
alias "table" { "Table" }
end
---@param s docs.spool
local function parseComment(s)
s:matchc("%*/()", "No end of comment found")
s:commit()
end
---@param s docs.spool
---@return docs.TypeModel
local function parseSimpleType(s)
if s:isAtEOF() then error("Expected a type at position " .. s.cursor .. " but instead found nothing") end
local prefix = s:peek()
if prefix == "#" then
s:adv()
local typename = s:matchc("^([a-zA-Z0-9]+) ?()", "Expected a typedef name at position %d, but found nothing after the #")
s:commit()
return newTypedef(typename)
end
local typename = s:matchc("^([a-zA-Z0-9]+) ?()", "Expected a type name at position %d, but found nothing")
s:commit()
local t = getOrDefineType(typename, "Figura")
return t
end
---@param s docs.spool
local function parseType(s)
local t = parseSimpleType(s)
s:consumeSpace()
local nextC = s:peek()
if nextC == '|' then
local unions = {t}
while nextC == '|' do
s:adv()
s:consumeSpace()
unions[#unions+1] = parseSimpleType(s)
s:consumeSpace()
nextC = s:peek()
end
return newUnionType(unions)
else
return t
end
end
local function noop() end
---@type table<string, fun(line: string, model: docs.DocModel)>
local sharedModifiers = {
prefixHint = function (line, model)
if model.prefixHint then error "duplicate prefixHint declaration" end
model.prefixHint = line
end
}
---@type table<string, fun(model: docs.DocModel)>
local sharedAttributes = {
global = noop
}
---@type table<string, fun(line: string, model: docs.MethodModel)>
local methodModifiers = {
}
methodModifiers = setmetatable(methodModifiers, {__index = sharedModifiers})
---@type table<string, fun(model: docs.MethodModel)>
local methodAttributes = {
static = function (model)
model.isStatic = true
end,
hostOnly = noop
}
methodAttributes = setmetatable(methodAttributes, {__index = sharedAttributes})
---@type table<string, fun(line: string, model: docs.FieldModel)>
local fieldModifiers = {
["type"] = function (line, model)
local s = spool.new(line)
local t = parseType(s)
s:consumeSpace()
if not s:isAtEOF() then error "expected end of statement after field type" end
model.readType = t
model.writeType = t
end,
["readType"] = function (line, model)
local s = spool.new(line)
local t = parseType(s)
s:consumeSpace()
if not s:isAtEOF() then error "expected end of statement after field readType" end
model.readType = t
end,
["writeType"] = function (line, model)
local s = spool.new(line)
local t = parseType(s)
s:consumeSpace()
if not s:isAtEOF() then error "expected end of statement after field writeType" end
model.writeType = t
end,
}
fieldModifiers = setmetatable(fieldModifiers, {__index = sharedModifiers})
---@type table<string, fun(model: docs.FieldModel)>
local fieldAttributes = {
readOnly = function (model)
model.canWrite = false
end,
writeOnly = function (model)
model.canRead = false
end
}
fieldAttributes = setmetatable(fieldAttributes, {__index = sharedAttributes})
---@param sigtext string
local function parseMethodSignature(sigtext)
local s = spool.new(sigtext)
---@type docs.MethodSignature
local model = {
returns = {},
parameters = {}
}
local function parseParameter(assumeV)
---@type docs.MethodSignature.Parameter
local param = setmetatable({
name = "",
type = typeDefinitions.void,
vararg = false
}, {
__tostring = function (t)
return ("<param %s (%s)>"):format(t.name, tostring(t.type))
end
})
if assumeV then
param.name = "..."
param.vararg = true
else
-- already consumed: 'parameter '
if s:matchadv("^%.%.%. ?") then
param.name = "..."
param.vararg = true
else
param.name = s:matchc("^([a-zA-Z0-9]+) ?()", "Expected parameter name at %d but found nothing")
s:commit()
end
end
s:consumeSpace()
param.type = parseType(s)
s:consumeSpace()
if not s:matchadv('^;') then error("Expected a semicolon to end parameter declaration at position " .. s.cursor) end
model.parameters[#model.parameters+1] = param
end
local function parseReturns()
-- already consumed: 'returns '
model.returns[#model.returns+1] = parseType(s)
s:consumeSpace()
while s:matchadv('^,') do
s:consumeSpace()
model.returns[#model.returns+1] = parseType(s)
s:consumeSpace()
end
if not s:matchadv('^;') then error("Expected a semicolon to end returns declaration at position " .. s.cursor) end
end
local function parseStmt()
if s:matchadv "^/%*" then
return parseComment(s)
end
if s:matchadv "^returns " then
return parseReturns()
end
if s:matchadv "^parameter%.%.%." then
return parseParameter(true)
end
if s:matchadv "^parameter " then
return parseParameter()
end
error("No valid signature statement at " .. s.cursor)
end
local function parse()
s:consumeSpace()
while not s:isAtEOF() do
parseStmt()
s:consumeSpace()
end
end
parse()
return model
end
---@param docstr string
local function parseMethod(docstr)
local s = spool.new(docstr)
---@type docs.MethodModel
local model = {
kind = "method",
attributes = {},
isStatic = false,
name = "<unnamed>",
origin = "Figura",
signatures = {},
}
local parseTopLevelStmt, parseMethodHeader, parseSignatureOuter
local blockKw = {}
function parseSignatureOuter()
s:consumeSpace()
---@type string
local block_content = s:matchc("^(%b{})%s*;()", "Expected a bracketed block after 'signature' at position %d")
local inside = block_content:sub(2, -2)
local sig = parseMethodSignature(inside)
model.signatures[#model.signatures+1] = sig
s:commit()
end
blockKw.signature = parseSignatureOuter
function parseMethodHeader()
local name = s:matchc("^([^%s;]+) ?()", "Expected method name at %d")
model.name = name
s:commit()
s:consumeSpace()
local peek = s:peek()
while #peek ~= 0 and peek ~= ';' do
local attr = s:matchc("^([^%s;]+) ?()", "Expected something else!")
if not methodAttributes[attr] then
error(("Unknown method attribute '%s' at position %d in method header"):format(attr, s.cursor))
end
methodAttributes[attr](model)
model.attributes[attr] = true
s:commit()
s:consumeSpace()
peek = s:peek()
end
if peek == ';' then
s:adv() -- advance past the ;
else
error("Reached end of method documentation while parsing header")
end
end
function parseTopLevelStmt()
local startAt = s.cursor
---@type string
local action = s:matchc("^([^%s;]+) ?()", "Expected a modifier or block keyword, but got nothing instead at position %d")
s:commit()
if methodModifiers[action] then
---@type string
local remainder = s:matchc("^(.-);%s*()", "Expected semicolon to end statement around %d")
s:commit()
return methodModifiers[action](remainder, model)
end
if blockKw[action] then return blockKw[action]() end
error(("Expected a modifier or block keyword, but got '%s' instead at position %d"):format(action, startAt))
end
local function parse()
s:consumeSpace()
parseMethodHeader()
s:consumeSpace()
while not s:isAtEOF() do
parseTopLevelStmt()
s:consumeSpace()
end
end
parse()
return model
end
---@param docstr string
---@return docs.FieldModel
local function parseField(docstr)
local s = spool.new(docstr)
---@type docs.FieldModel
local model = {
kind = "field",
attributes = {},
name = "<unnamed>",
origin = "Figura",
canRead = true,
canWrite = true
}
local parseTopLevelStmt, parseFieldHeader, parseSignatureOuter
function parseFieldHeader()
local name = s:matchc("^([^%s;]+) ?()", "Expected field name at %d")
model.name = name
s:commit()
s:consumeSpace()
local peek = s:peek()
while #peek ~= 0 and peek ~= ';' do
local attr = s:matchc("^([^%s;]+) ?()", "Expected something else!")
if not fieldAttributes[attr] then
error(("Unknown field attribute '%s' at position %d in field header"):format(attr, s.cursor))
end
fieldAttributes[attr](model)
model.attributes[attr] = true
s:commit()
s:consumeSpace()
peek = s:peek()
end
if peek == ';' then
s:adv() -- advance past the ;
else
error("Reached end of field documentation while parsing header")
end
end
function parseTopLevelStmt()
local startAt = s.cursor
---@type string
local action = s:matchc("^([^%s;]+) ?()", "Expected a modifier, but got nothing instead at position %d")
s:commit()
if fieldModifiers[action] then
---@type string
local remainder = s:matchc("^(.-);%s*()", "Expected semicolon to end statement around %d")
s:commit()
return fieldModifiers[action](remainder, model)
end
error(("Expected a modifier, but got '%s' instead at position %d"):format(action, startAt))
end
local function parse()
s:consumeSpace()
parseFieldHeader()
s:consumeSpace()
while not s:isAtEOF() do
parseTopLevelStmt()
s:consumeSpace()
end
end
parse()
return model
end
---@param typeM docs.TypeModel
local function renderType(typeM)
return tostring(typeM)
end
---@param o any
---@param i number?
local function printFancy(o, i)
i = i or 2
local indent = (" "):rep(i)
if type(o) == "table" then
if getmetatable(o) and getmetatable(o).__tostring then
return tostring(o)
end
local b = "{\n"
for k, v in pairs(o) do
b = b .. indent .. printFancy(k, i + 2) .. " = " .. printFancy(v, i + 2) .. ",\n"
end
b = b .. (" "):rep(i - 2) .. "}"
return b
else
return tostring(o)
end
end
local function parseAndRenderType(frame)
local input_args = frame:getParent().args
local nargs = 0
while input_args[nargs + 1] do nargs = nargs + 1 end
if nargs == 0 then error "Need to specify at least one type" end
if nargs == 1 then
local s = spool.new(input_args[1])
local t = parseType(s)
s:consumeSpace()
if not s:isAtEOF() then error("Expected end of type at position " .. s.cursor) end
return renderType(t)
else
local tabulated = {}
for i = 1, nargs do tabulated[i] = input_args[i] end
local s = spool.new(table.concat(tabulated, "|"))
local t = parseType(s)
s:consumeSpace()
if not s:isAtEOF() then error("Expected end of type at position " .. s.cursor) end
return renderType(t)
end
end
-- local m = parseField [[
-- player global readOnly;
-- type PlayerAPI;
-- ]]
-- print(printFancy(m))
return {
type = parseAndRenderType
}