Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- -- TODO: check that default values are the correct type and not nil after initialise
- -- TODO: do semi-colons work??
- -- TODO: prevent getters and setters on methods (maybe already fixed?)
- local args = { ... }
- if #args < 2 then
- error("Parameters: luo <output file> <input file> <input file> etc...")
- end
- local outputPath = args[1]
- table.remove(args, 1)
- local floor, insert = math.floor, table.insert
- local parseStartTime = os.clock()
- print("Starting parse...")
- local LINE_START_POSITION = 1
- local KEYWORD_CLASS, KEYWORD_EXTENDS, KEYWORD_PROPERTY, KEYWORD_DEFAULT, KEYWORD_SET, KEYWORD_DIDSET, KEYWORD_WILLSET, KEYWORD_GET, KEYWORD_EVENT, KEYWORD_FUNCTION, KEYWORD_STATIC, KEYWORD_EXTENDS, KEYWORD_IMPLEMENTS, KEYWORD_READONLY, KEYWORD_ALLOWSNIL, KEYWORD_LINK, KEYWORD_END, KEYWORD_IF, KEYWORD_FOR, KEYWORD_WHILE, KEYWORD_FUNCTION, KEYWORD_LOCAL, KEYWORD_ENUM, KEYWORD_SUPER, KEYWORD_RETURN, KEYWORD_DO, KEYWORD_INTERFACE = "class", "extends", "property", "default", "set", "didSet", "willSet", "get", "event", "function", "static", "extends", "implements", "readOnly", "allowsNil", "link", "end", "if", "for", "while", "function", "local", "enum", "super", "return", "do", "interface"
- local NON_CLASS_TYPE_DECLARATION_KEYWORDS = { -- keywords that indicate that the class definition has finished (i.e. not extends, implements, etc.)
- [KEYWORD_PROPERTY] = true,
- [KEYWORD_DEFAULT] = true,
- [KEYWORD_SET] = true,
- [KEYWORD_DIDSET] = true,
- [KEYWORD_WILLSET] = true,
- [KEYWORD_GET] = true,
- [KEYWORD_EVENT] = true,
- [KEYWORD_FUNCTION] = true,
- [KEYWORD_STATIC] = true,
- [KEYWORD_LOCAL] = true,
- [KEYWORD_END] = true,
- [KEYWORD_ENUM] = true
- }
- local BLOCK_START_KEYWORDS = {
- [KEYWORD_IF] = true,
- [KEYWORD_DO] = true,
- [KEYWORD_FUNCTION] = true
- }
- local STATIC_KEYWORDS = {
- [KEYWORD_PROPERTY] = true,
- [KEYWORD_DEFAULT] = true,
- [KEYWORD_SET] = true,
- [KEYWORD_DIDSET] = true,
- [KEYWORD_WILLSET] = true,
- [KEYWORD_GET] = true,
- [KEYWORD_EVENT] = true,
- [KEYWORD_FUNCTION] = true,
- [KEYWORD_LOCAL] = true,
- }
- local DEFAULT_DELIMITERS = {
- [KEYWORD_DEFAULT] = true,
- [KEYWORD_STATIC] = true,
- [KEYWORD_PROPERTY] = true,
- [KEYWORD_LOCAL] = true,
- [KEYWORD_ENUM] = true,
- [KEYWORD_SET] = true,
- [KEYWORD_GET] = true,
- [KEYWORD_DIDSET] = true,
- [KEYWORD_EVENT] = true,
- [KEYWORD_FUNCTION] = true,
- [KEYWORD_END] = true
- }
- local BLOCK_LEVEL_NONE, BLOCK_LEVEL_CLASS, BLOCK_LEVEL_FUNCTION = 0, 1, 2
- local IDENTIFIER_BLOCK_COMMENT_START, IDENTIFIER_BLOCK_STRING_START, IDENTIFIER_BLOCK_STOP, IDENTIFIER_DOUBLE_STRING, IDENTIFIER_SINGLE_STRING, IDENTIFIER_COMMENT, IDENTIFIER_ESCAPE = "--[[", "[[", "]]", "\"", "'", "--", "\\"
- local parsed = {}
- local fileEnvironments = {}
- for i, fileName in ipairs( args ) do
- local fileStartTime = os.clock()
- local file = io.open( fileName, "r" )
- if not file then
- error( "Failed to open file '" .. fileName .. "'." )
- end
- local function findAll( str, match, positions, identifier )
- local start, stop = nil, 0
- repeat
- start, stop = str:find( match, stop + 1 )
- if start and not positions[start] then positions[start] = identifier end
- until not stop
- end
- local lines, linesStringsPositions, linesCommentsPositions, linesComments, linesStrings, linesCommentsOrStrings, line = {}, {}, {}, {}, {}, {}, file:read()
- local currentLine, currentLinePosition
- function parserError( message, lineNumber, linePosition )
- lineNumber = lineNumber or currentLine
- linePosition = linePosition or currentLinePosition
- if lineNumber > #lines then
- error( "Parse error: " .. message )
- else
- error( "Parse error: " .. message .. "\n" ..
- ( lineNumber > 2 and lineNumber - 1 .. ": " .. lines[lineNumber - 1] .. "\n" or "" ) ..
- lineNumber .. ": " .. lines[lineNumber]:gsub("\t", " ") .. "\n" ..
- string.rep( " ", #tostring( lineNumber ) + 2 + linePosition ) .. "^" ..
- ( lineNumber < #lines and "\n" .. lineNumber + 1 .. ": " .. lines[lineNumber + 1] .. "\n" or "" ), 2 )
- end
- end
- local isBlockComment = false
- local isBlockString = false
- -- read all the lines and get start and end locations for comments and strings so we can ignore them
- while line do
- table.insert( lines, line )
- -- if we're in a block comment/string then find the first ]], if one isn't found then the whole line is a comment/string
- -- 2nd: find the start of a block string, if found go back to start
- -- 3rd: find all the starts of comments, then find the start and end of all strings
- -- if the comment is within a string then ignore, otherwise remove the start/end of the string
- local linePosition = 1
- local positions = {}
- findAll( line, "%-%-%[%[", positions, IDENTIFIER_BLOCK_COMMENT_START )
- findAll( line, "%[%[", positions, IDENTIFIER_BLOCK_STRING_START )
- findAll( line, "\"", positions, IDENTIFIER_DOUBLE_STRING )
- findAll( line, "'", positions, IDENTIFIER_SINGLE_STRING )
- findAll( line, "\\[\"\']", positions, IDENTIFIER_ESCAPE )
- findAll( line, "%-%-", positions, IDENTIFIER_COMMENT )
- local lineLength = #line
- local stringPositions, commentPositions = {}, {}
- local i = 1
- while linePosition <= lineLength do
- i = i + 1
- if isBlockComment or isBlockString then
- table.insert( isBlockComment and commentPositions or stringPositions, linePosition )
- local start, stop = line:find( "]]" )
- if stop then
- table.insert( isBlockComment and commentPositions or stringPositions, linePosition + stop )
- linePosition = linePosition + stop
- if isBlockComment then
- isBlockComment = false
- else
- isBlockString = false
- end
- else
- table.insert( isBlockComment and commentPositions or stringPositions, lineLength + 1 )
- break
- end
- else
- -- otherwise or then, 1st: find the start of a block comment if found go back to start
- local isDoubleString, isSingleString, isEscaped = false, false, false
- for position = linePosition, lineLength do
- local identifier = positions[position]
- if identifier then
- -- for blocks we can do another loop of the while loop to prevent repeating ourself
- if identifier == IDENTIFIER_ESCAPE then
- isEscaped = true
- else
- if identifier == IDENTIFIER_BLOCK_COMMENT_START then
- isBlockComment = true
- linePosition = position
- break
- elseif identifier == IDENTIFIER_BLOCK_STRING_START then
- isBlockString = true
- linePosition = position
- break
- elseif identifier == IDENTIFIER_COMMENT then
- table.insert( commentPositions, position )
- table.insert( commentPositions, lineLength + 1 )
- linePosition = lineLength + 1
- break
- elseif not isEscaped and not isSingleString and identifier == IDENTIFIER_DOUBLE_STRING then
- table.insert( stringPositions, position )
- isDoubleString = not isDoubleString
- elseif not isEscaped and not isDoubleString and identifier == IDENTIFIER_SINGLE_STRING then
- table.insert( stringPositions, position )
- isSingleString = not isSingleString
- end
- isEscaped = false
- end
- end
- end
- if not isBlockComment and not isBlockString then
- break
- end
- end
- end
- if #stringPositions % 2 ~= 0 then
- parserError( "string started but not finished", #lines, lineLength )
- end
- -- for i = 0, #stringPositions / 2 + 1, 2 do
- -- local start, stop = stringPositions[i] or 1, stringPositions[i + 1] or lineLength + 1
- -- lineNoStrings = lineNoStrings .. line:sub(start, stop - 1)
- -- end
- table.insert( linesStringsPositions, stringPositions )
- table.insert( linesCommentsPositions, commentPositions )
- local comments, strings, commentsOrStrings = {}, {}, {} -- a table where, for each position (index), it is true if there is a comment or string in that position
- for i = 1, #stringPositions / 2, 2 do
- for j = stringPositions[i], stringPositions[i + 1] do
- commentsOrStrings[j] = true
- strings[j] = true
- end
- end
- for i = 1, #commentPositions / 2, 2 do
- for j = commentPositions[i], commentPositions[i + 1] do
- commentsOrStrings[j] = true
- comments[j] = true
- end
- end
- table.insert( linesCommentsOrStrings, commentsOrStrings )
- table.insert( linesComments, comments )
- table.insert( linesStrings, strings )
- line = file:read()
- end
- local linesCount = #lines
- local blockLevel = BLOCK_LEVEL_NONE -- 1 is inside class, 2 is inside function and more will be
- local currentClass
- local isClassDefinition = false
- local isStatic = false
- currentLine, currentLinePosition = 1, LINE_START_POSITION
- local currentLineLength, isEndOfFile = #lines[1], false
- function getLine()
- if currentLinePosition <= currentLineLength then
- local line = lines[currentLine]
- if line then
- -- TODO: if comments/white space are needed return this: local sub = line:sub( currentLinePosition )
- -- if the line is all comments or space then skip the line
- local nonComments = ""
- local lineComments = linesComments[currentLine]
- for i = currentLinePosition, #line do
- if not lineComments[i] then
- nonComments = nonComments .. line:sub(i, i)
- end
- end
- if not nonComments:match("^%s*$") then
- return nonComments
- end
- end
- end
- -- otherwise...
- currentLine = currentLine + 1
- if currentLine > linesCount then
- isEndOfFile = true
- else
- currentLinePosition = LINE_START_POSITION
- currentLineLength = #lines[currentLine]
- return getLine()
- end
- end
- function nextMatch( match, index, allowEndOfFile )
- index = index or 3
- local keyword
- repeat
- keyword, _, endOfFile = firstMatch( match, index, allowEndOfFile )
- if endOfFile then
- return nil, true
- end
- if not keyword then
- currentLinePosition = currentLineLength + 1
- end
- until isEndOfFile or keyword
- if not keyword and isEndOfFile then
- if allowEndOfFile then
- return nil, true
- end
- error( "unexpected end of file" )
- end
- return keyword
- end
- function firstMatch( match, index, allowEndOfFile )
- index = index or 3
- local line = getLine()
- if not line then
- if allowEndOfFile then
- return nil, nil, true
- else
- parserError( "unexpected end of file (probably too many/too few 'end' keywords)" )
- end
- end
- local matchData = { line:find( match ) }
- local start, stop, keyword = matchData[1], matchData[2], matchData[index]
- if stop then
- local startPos, stopPos = start + currentLinePosition, stop + currentLinePosition - 1
- currentLinePosition = stop + currentLinePosition
- local commentsOrStrings = linesCommentsOrStrings[currentLine]
- if commentsOrStrings[startPos] or commentsOrStrings[stopPos] then
- while commentsOrStrings[currentLinePosition] do
- -- skip over this block of unuseable line
- currentLinePosition = currentLinePosition + 1
- end
- -- this match was actually in a comment, try again
- return firstMatch( match, index )
- end
- else
- if line:match( "^(%s*)$" ) then
- -- this line was all empty space, so it doesn't count as a first match, try the next line
- currentLine = currentLine + 1
- currentLinePosition = LINE_START_POSITION
- return firstMatch(match, index)
- end
- end
- if not keyword and isEndOfFile then
- error( "unexpected end of file" )
- end
- return keyword, matchData
- end
- -- capture the string up to a , or ) for a function parameter default value
- function captureParameterDefault()
- local lineIndex = currentLine
- local startLine, startLinePosition = currentLine, currentLinePosition
- local stopLine, stopLinePosition
- repeat
- local line = lines[lineIndex]
- local commentsOrStrings = linesCommentsOrStrings[lineIndex]
- local roundBracketLevel = 1
- local curlyBracketLevel = 0
- for i = (lineIndex == currentLine and currentLinePosition or 1), #line do
- if not commentsOrStrings[i] then
- local char = line:sub( i, i )
- if char == "(" then
- roundBracketLevel = roundBracketLevel + 1
- elseif char == ")" then
- roundBracketLevel = roundBracketLevel - 1
- elseif char == "{" then
- curlyBracketLevel = curlyBracketLevel + 1
- elseif char == "}" then
- curlyBracketLevel = curlyBracketLevel - 1
- end
- if roundBracketLevel == 0 or (char == "," and roundBracketLevel == 1 and curlyBracketLevel == 0 ) then
- stopLine, stopLinePosition = lineIndex, i - 1
- break
- end
- end
- end
- lineIndex = lineIndex + 1
- until (stopLine and stopLinePosition) or lineIndex > linesCount
- if stopLine and stopLinePosition then
- local defaultValue = ""
- for i = currentLine, stopLine do
- defaultValue = defaultValue .. lines[i]:sub(i == currentLine and currentLinePosition or 1, i == stopLine and stopLinePosition or nil) .. (i == stopLine and "" or "\n")
- end
- currentLine, currentLinePosition = stopLine, stopLinePosition + 1
- return defaultValue
- end
- end
- function captureReturnParameter()
- local lineIndex = currentLine
- local startLine, startLinePosition = currentLine, currentLinePosition
- local stopLine, stopLinePosition
- local hasNext = false
- repeat
- local line = lines[lineIndex]
- local commentsOrStrings = linesCommentsOrStrings[lineIndex]
- local blockLevel = 0
- local roundBracketLevel = 0
- local curlyBracketLevel = 0
- while true do
- if not linesCommentsOrStrings[currentLine][currentLinePosition] then
- local foundMatch = false
- for keyword, _ in pairs(BLOCK_START_KEYWORDS) do
- if isNext("^(%s*" .. keyword .. ")", true) then
- blockLevel = blockLevel + 1
- foundMatch = true
- break
- end
- end
- if not foundMatch then
- local priorLine, priorPosition = currentLine, currentLinePosition
- if isNext("^(%s*" .. KEYWORD_END .. ")", true) then
- blockLevel = blockLevel - 1
- if blockLevel < 0 then
- currentLine, currentLinePosition = priorLine, priorPosition
- stopLine, stopLinePosition = priorLine, priorPosition
- break
- end
- else
- local char = getLine():sub( 1, 1 )
- if char == "(" then
- roundBracketLevel = roundBracketLevel + 1
- elseif char == ")" then
- roundBracketLevel = roundBracketLevel - 1
- elseif char == "{" then
- curlyBracketLevel = curlyBracketLevel + 1
- elseif char == "}" then
- curlyBracketLevel = curlyBracketLevel - 1
- end
- if char == "," and roundBracketLevel == 0 and curlyBracketLevel == 0 then
- stopLine, stopLinePosition = priorLine, priorPosition
- currentLinePosition = currentLinePosition + 1
- hasNext = true
- break
- end
- end
- end
- else
- -- currentLinePosition = currentLinePosition + 1
- end
- currentLinePosition = currentLinePosition + 1
- end
- lineIndex = lineIndex + 1
- until (stopLine and stopLinePosition) or lineIndex > linesCount
- if stopLine and stopLinePosition then
- local parameter = ""
- for i = startLine, stopLine do
- parameter = parameter .. lines[i]:sub(i == startLine and startLinePosition or 1, i == stopLine and stopLinePosition - 1 or nil) .. (i == stopLine and "" or "\n")
- end
- -- parameter = parameter:gsub("\n$", "")
- startLine, startLinePosition = stopLine, stopLinePosition + 1
- return parameter, hasNext
- end
- end
- function isNext( match, captureIfFound )
- local matchData = { getLine():find( match ) }
- local start, stop = matchData[1], matchData[2]
- if stop then
- if captureIfFound then
- currentLinePosition = stop + currentLinePosition
- end
- return true, stop + currentLinePosition
- else
- return false
- end
- end
- -- capture the expression until one of the delimeters are reached and return it with comments removed
- function firstExpression( delimiters )
- local line = getLine()
- local capturedLines, capturedLineStarts, startLine, endLine = captureToKeywords( delimiters )
- local expression = ""
- for _, lineNumber in ipairs( capturedLines ) do
- local startPos = capturedLineStarts[lineNumber]
- local stopPos = lineNumber == endLine and startPos or math.huge
- startPos = lineNumber == endLine and 1 or startPos
- local line = lines[lineNumber]
- local lineNoComment = ""
- local commentPositions, lineLength = linesCommentsPositions[lineNumber], #line
- if startPos ~= stopPos then
- for i = 0, #commentPositions / 2 + 1, 2 do
- local start, stop = commentPositions[i] or 1, commentPositions[i + 1] or lineLength + 1
- lineNoComment = lineNoComment .. line:sub( math.max( start, startPos), math.min( stop, stopPos) - 1 )
- end
- expression = expression .. lineNoComment
- end
- end
- return expression
- end
- function demandFirstExpression( delimiters, description )
- description = description or "An expression"
- local expression = firstExpression( delimiters )
- if expression and not expression:match( "^%s*$" ) then
- return expression
- else
- parserError( description .. " was expected but wasn't found anywhere." )
- end
- end
- function nextName( allowEndOfFile )
- return nextMatch( "%s*([_%a][_%w]*)", nil, allowEndOfFile )
- end
- function immediateNextName()
- return firstMatch( "^(%s*)([_%a][_%w]*)", 4 )
- end
- function demandNextName()
- local name = nextName()
- if name then
- return name
- else
- parserError( "A name/keyword was expected but wasn't found anywhere." )
- end
- end
- function demandImmediateNextName( description )
- description = description or "name/keyword"
- local name = immediateNextName()
- if name then
- return name
- else
- parserError( description .. " was expected immediately but wasn't found." )
- end
- end
- function demandFirstMatch( match, index, description )
- local match = firstMatch( match, index )
- if match then
- return match
- else
- parserError( description .. " was expected immediately but wasn't found." )
- end
- end
- function nextType()
- -- {String = Number}
- local dictionary, matches = firstMatch( "^%s*{%s*([_%a][_%w]*)%s*=%s*([_%a][_%w]*)%s*}" )
- if dictionary then
- return "{" .. matches[3] .. "=" .. matches[4] .. "}", true
- else
- -- {String}
- local array = firstMatch( "^%s*{%s*([_%a][_%w]*)%s*}" )
- if array then
- return "{" .. array .. "}", true
- else
- -- Class.enum
- local className, matches = firstMatch( "^%s*([_%a][_%w]*)%.([_%a][_%w]*)" )
- if className then
- return matches[3] .. "." .. matches[4], true
- else
- -- String
- return firstMatch( "^%s*([_%a][_%w]*)" ), false
- end
- end
- end
- end
- function demandNextType( description )
- description = description or "variable type"
- local match, isTable = nextType()
- if match then
- return match, isTable
- else
- parserError( description .. " was expected immediately but wasn't found (or the type was invalid)." )
- end
- end
- function firstParameters()
- local openingBracket = isNext( "^%s*%(", true )
- if openingBracket then
- else
- parserError( "expected opening bracket for function parameters" )
- end
- end
- -- capture the single parameter for getter/setter functions
- function captureSingleParameter( name )
- local parameter = isNext( "^%s*%(%s*" .. name .. "%s*%)", true )
- if not parameter then
- if name == "" then
- parserError( "expected empty parameter brackets" )
- else
- parserError( "expected brackets and single parameter '" .. name .. "'" )
- end
- end
- end
- -- capture the single parameter for getter/setter functions
- function captureEventParameter()
- local didMatch, matches = firstMatch( "^%s*%(%s*([_%a][_%w]*)%.(%u+)%s+([_%a][_%w]*)%s*%)" )
- if not didMatch then
- didMatch, matches = firstMatch( "^%s*%(%s*([_%a][_%w]*)%s+([_%a][_%w]*)%s*%)" )
- if not didMatch then
- parserError( "expected parameter brackets, event class name, event phase (optional) and parameter name. For example: event explode( ReadyInterfaceEvent.AFTER event )" )
- else
- return matches[3], nil, matches[4]
- end
- else
- return matches[3], matches[4], matches[5]
- end
- end
- -- capture until the 'end' keyword of the current block
- function captureBlock()
- local keyword
- local startLine, startLinePosition = currentLine, currentLinePosition
- local level = 1
- repeat
- keyword = nextMatch( "(%l+)" )
- if not keyword then
- parserError( "expected 'end' keyword to complete function" )
- elseif keyword == KEYWORD_END then
- level = level - 1
- elseif BLOCK_START_KEYWORDS[keyword] then
- level = level + 1
- end
- until level == 0
- local contents = ""
- for i = startLine, currentLine do
- if i == startLine or i == currentLine then
- contents = contents .. lines[i]:sub( i == startLine and startLinePosition or 1, i == currentLine and currentLinePosition or nil )
- else
- contents = contents .. lines[i]
- end
- if i ~= currentLine then
- contents = contents .. "\n"
- end
- end
- return contents, startLine
- end
- -- capture until the 'end' keyword of the current block, separating out the return statements
- function captureReturnsBlock()
- local keyword
- local block = {}
- local blockStartLine = currentLine
- local startLine, startLinePosition = currentLine, currentLinePosition
- local level = 1
- local function addPassed()
- local contents = ""
- for i = startLine, currentLine do
- if i == startLine or i == currentLine then
- contents = contents .. lines[i]:sub( i == startLine and startLinePosition or 1, i == currentLine and currentLinePosition or nil )
- else
- contents = contents .. lines[i]
- end
- if i ~= currentLine then
- contents = contents .. "\n"
- end
- end
- return contents
- end
- repeat
- local priorLine, priorPosition = currentLine, currentLinePosition
- keyword = nextMatch( "(%l+)" )
- if not keyword then
- parserError( "expected 'end' keyword to complete function" )
- elseif keyword == KEYWORD_END then
- level = level - 1
- if level == 0 then
- table.insert(block, addPassed())
- end
- elseif keyword == KEYWORD_RETURN then
- -- capture everything up and and including the return
- local returnLinePosition = currentLinePosition
- currentLinePosition = currentLinePosition - #KEYWORD_RETURN - 1
- table.insert(block, addPassed())
- currentLinePosition = returnLinePosition
- -- capture the return values
- local values = {}
- while true do
- local expression, hasNext = captureReturnParameter()
- if expression then
- table.insert(values, expression)
- end
- if not expression or not hasNext then
- break
- end
- end
- if #values > 0 then
- table.insert(block, values)
- end
- startLine, startLinePosition = currentLine, currentLinePosition
- elseif BLOCK_START_KEYWORDS[keyword] then
- level = level + 1
- end
- until level == 0
- return block, blockStartLine
- end
- function demandParameters()
- end
- function stepBack( keyword )
- currentLinePosition = math.max( 1, currentLinePosition - #keyword )
- if currentLinePosition < 1 then
- currentLine = currentLine - 1
- end
- end
- function jumpToKeywords( toKeywords )
- local keyword
- local startLine, startLinePosition = currentLine, currentLinePosition
- repeat
- keyword, isEndOfFile = nextName( true )
- until not keyword or toKeywords[keyword]
- if not keyword then
- currentLine, currentLinePosition = #lines, #lines[#lines] + 1
- end
- local jumpedString = ""
- for i = startLine, currentLine do
- local line = lines[i]
- local lineOut = ""
- local lineComments = linesComments[i]
- for n = (i == startLine and startLinePosition or 1), (i == currentLine and currentLinePosition - (keyword and #keyword or 0) - 1 or #line) do
- if not lineComments or not lineComments[n] then
- lineOut = lineOut .. line:sub(n, n)
- end
- end
- jumpedString = jumpedString .. lineOut .. "\n"
- end
- return toKeywords[keyword], keyword, jumpedString, startLine
- end
- function isComment( lineNumber, linePosition )
- lineNumber = lineNumber or currentLine
- linePosition = linePosition or currentLinePosition
- return linesComments[lineNumber][linePosition] or false
- end
- function captureToKeywords( toKeywords )
- local keyword
- local startLine, startLinePosition = currentLine, currentLinePosition
- repeat
- keyword = nextName()
- until not keyword or toKeywords[keyword]
- stepBack( keyword )
- if startLine ~= currentLine or startLinePosition ~= currentLinePosition then
- local capturedLineStarts = {}
- local capturedLines = {}
- for i = startLine, currentLine do
- table.insert( capturedLines, i )
- capturedLineStarts[i] = i == startLine and startLinePosition or ( i == currentLine and currentLinePosition or 1 )
- end
- return capturedLines, capturedLineStarts, startLine, currentLine
- else
- return {}
- end
- end
- local fileEnvironment = {
- fileName = fileName
- }
- fileEnvironments[i] = fileEnvironment
- local isInterface = false
- while not isEndOfFile and not ( currentLinePosition > currentLineLength and currentLine + 1 > linesCount ) do
- if blockLevel == BLOCK_LEVEL_NONE then
- -- we're expecting a class definition
- local didJump, keyword, jumpedString, startLine = jumpToKeywords( {[KEYWORD_CLASS] = true, [KEYWORD_INTERFACE] = true}, position, line )
- if not jumpedString:match( "^(%s*)$" ) then
- -- we only jumped more than just white space or nothing, it might be important
- local existingLine = fileEnvironment[startLine]
- if existingLine then
- -- there might already be other code starting on this line (i.e. the class was on a single line), add to it rather than replacing
- fileEnvironment[startLine] = existingLine .. " " .. jumpedString
- else
- fileEnvironment[startLine] = jumpedString
- end
- end
- if didJump then
- local className = demandImmediateNextName( "class name" )
- if keyword == KEYWORD_CLASS then
- isInterface = false
- currentClass = {
- className = className,
- instance = {
- properties = {},
- propertyMethods = {[KEYWORD_SET] = {},[KEYWORD_DIDSET] = {},[KEYWORD_WILLSET] = {},[KEYWORD_GET] = {}},
- defaultValues = {},
- eventHandles = {},
- functions = {},
- instanceVariables = {}
- },
- static = {
- properties = {},
- propertyMethods = {[KEYWORD_SET] = {},[KEYWORD_DIDSET] = {},[KEYWORD_WILLSET] = {},[KEYWORD_GET] = {}},
- defaultValues = {},
- eventHandles = {},
- functions = {},
- instanceVariables = {}
- },
- implements = {},
- enums = {},
- fileEnvironment = i,
- lineNumber = currentLine
- }
- elseif keyword == KEYWORD_INTERFACE then
- isInterface = true
- currentClass = {
- isInterface = true,
- className = className,
- instance = {
- properties = {},
- eventHandles = {},
- functions = {},
- },
- static = {
- properties = {},
- eventHandles = {},
- functions = {},
- },
- enums = {}
- }
- end
- if parsed[className] then
- parserError( "A class/interface has already been defined with the name '" .. className .. "'. Class/interface names must be unique." )
- end
- blockLevel = BLOCK_LEVEL_CLASS
- isClassDefinition = true
- else
- -- we're not in a class block, we can ignore this line
- break
- end
- elseif blockLevel == BLOCK_LEVEL_CLASS then
- if isClassDefinition then
- -- we're expecting either the extends, implements, etc, statement OR a property declaration
- local keyword = immediateNextName()
- if keyword == KEYWORD_EXTENDS then
- if isInterface then
- parserError("attempted to extend interface '" .. currentClass.className .. "'. Interfaces cannot be extended.")
- end
- if currentClass.extends then
- parserError( "class already extends '" .. currentClass.extends .. "'. Polyinheritance is not supported." )
- end
- currentClass.extends = demandImmediateNextName()
- elseif keyword == KEYWORD_IMPLEMENTS then
- if isInterface then
- parserError("attempted to implement another interface in interface '" .. currentClass.className .. "'. Interfaces cannot implement other interfaces.")
- end
- local implements = demandImmediateNextName()
- for i, impl in ipairs(currentClass.implements) do
- if impl == implements then
- parserError( "duplicate interface implementation, class already implements '" .. impl .. "'." )
- end
- end
- table.insert(currentClass.implements, implements)
- elseif NON_CLASS_TYPE_DECLARATION_KEYWORDS[keyword] then
- isClassDefinition = false
- stepBack( keyword )
- else
- parserError( "unexpected keyword '" .. tostring(keyword) .. "', expected class type declaration (extends, implements, etc.) or property/function declaration." )
- end
- else
- local keyword = demandNextName( "property or function declaration or 'end'" )
- if isStatic and not STATIC_KEYWORDS[keyword] then
- parserError( "invalid keyword '" .. keyword .. "' after 'static'" )
- end
- if keyword == KEYWORD_END then
- blockLevel = BLOCK_LEVEL_NONE
- parsed[currentClass.className] = currentClass
- elseif keyword == KEYWORD_STATIC then
- isStatic = true
- elseif keyword == KEYWORD_LOCAL then
- if isInterface then
- parserError("interfaces cannot define instance variables.")
- end
- local name = demandImmediateNextName( "instance variable name" )
- if isStatic then
- if currentClass.static.instanceVariables[name] then
- parserError( "duplicate static instance variable for name '" .. name .. "'" )
- end
- else
- if currentClass.instance.instanceVariables[name] then
- parserError( "duplicate instance variable for name '" .. name .. "'" )
- end
- end
- -- try and get the default value out of it
- local hasEquals = isNext( "^(%s*)=(%s*)", true )
- local defaultValue
- if hasEquals then
- defaultValue = demandFirstExpression( DEFAULT_DELIMITERS, "default value expression" )
- end
- if isStatic then
- currentClass.static.instanceVariables[name] = defaultValue
- else
- currentClass.instance.instanceVariables[name] = defaultValue
- end
- elseif keyword == KEYWORD_ENUM then
- if isInterface then
- parserError("interfaces cannot define enums.")
- end
- local propertyType = demandNextType( "enum type" )
- local name = demandImmediateNextName( "enum name" )
- if currentClass.enums[name] then
- parserError( "duplicate enum for name '" .. name .. "'" )
- end
- -- try and get the default value out of it
- local hasEquals = isNext( "^(%s*)=(%s*)", true )
- local values
- if hasEquals then
- values = demandFirstExpression( DEFAULT_DELIMITERS, "default value expression" )
- else
- parserError( "expected table of enum values" )
- end
- currentClass.enums[name] = {
- type = propertyType,
- values = values
- }
- elseif keyword == KEYWORD_PROPERTY then
- local nextKeyword = demandNextType( "property type" )
- local readOnly, allowsNil, link, name, propertyType = false, false, false
- while true do
- if nextKeyword == KEYWORD_READONLY then
- readOnly = true
- elseif nextKeyword == KEYWORD_ALLOWSNIL then
- allowsNil = true
- elseif nextKeyword == KEYWORD_LINK then
- link = true
- elseif not nextKeyword then
- -- no type
- name = propertyType
- propertyType = nil
- if allowsNil then
- error("compiler error: unnecessary allowsNil modifier on property '" .. name .. "' in class '" .. currentClass.className .. "'. Properties without a specified type automatically allow nil values.")
- end
- allowsNil = true
- break
- elseif not propertyType then
- propertyType = nextKeyword
- else
- name = nextKeyword
- break
- end
- nextKeyword = immediateNextName( "property type modifiers (readOnly, allowsNil, etc.) or property name" )
- end
- if isStatic then
- if currentClass.static.properties[name] then
- parserError( "duplicate static property for name '" .. name .. "'" )
- end
- else
- if currentClass.instance.properties[name] then
- parserError( "duplicate property for name '" .. name .. "'" )
- end
- end
- -- try and get the default value out of it
- local hasEquals = isNext( "^(%s*)=(%s*)", true )
- local defaultValue
- if hasEquals then
- if isInterface then
- parserError("interfaces cannot set default property values.")
- end
- defaultValue = demandFirstExpression( DEFAULT_DELIMITERS, "default value expression" )
- end
- local propertyTable = {
- type = propertyType,
- allowsNil = allowsNil,
- readOnly = readOnly,
- link = link,
- defaultValue = defaultValue
- }
- if isStatic then
- currentClass.static.properties[name] = propertyTable
- else
- currentClass.instance.properties[name] = propertyTable
- end
- elseif keyword == KEYWORD_DEFAULT then
- if isInterface then
- parserError("interfaces cannot set default property values.")
- end
- local name = demandImmediateNextName( "property name" )
- local hasEquals = isNext( "^(%s*)=(%s*)", true )
- if hasEquals then
- local expression = demandFirstExpression( DEFAULT_DELIMITERS, "default value expression" )
- if isStatic then
- currentClass.static.defaultValues[name] = expression
- else
- currentClass.instance.defaultValues[name] = expression
- end
- else
- parserError( "default expects equals sign and new default value after property name" )
- end
- elseif keyword == KEYWORD_SET or keyword == KEYWORD_DIDSET or keyword == KEYWORD_WILLSET or keyword == KEYWORD_GET then
- if isInterface then
- parserError("interfaces cannot require property methods.")
- end
- local name = demandImmediateNextName( "property name" )
- local propertyMethodTable
- if isStatic then
- propertyMethodTable = currentClass.static.propertyMethods[keyword]
- else
- propertyMethodTable = currentClass.instance.propertyMethods[keyword]
- end
- if propertyMethodTable[name] then
- parserError( "Attempted to redefine '" .. keyword .. "' for property '" .. name .. "'." )
- end
- captureSingleParameter( keyword == KEYWORD_GET and "" or name )
- local block, startLine = captureBlock()
- propertyMethodTable[name] = { "(__CLASS__self_passed" .. ( keyword == KEYWORD_GET and "" or ( "," .. name ) ) .. ")" .. block, startLine }
- elseif keyword == KEYWORD_EVENT then
- local name = demandImmediateNextName( "event function handle name" )
- local eventClass, eventPhase, parameterName = captureEventParameter()
- local eventTable
- if not isInterface then
- local block, startLine = captureBlock()
- eventTable = { "(__CLASS__self_passed," .. parameterName ..")", block, startLine, eventClass, eventPhase }
- else
- eventTable = { eventClass, eventPhase }
- end
- if isStatic then
- currentClass.static.eventHandles[name] = eventTable
- else
- currentClass.instance.eventHandles[name] = eventTable
- end
- elseif keyword == KEYWORD_FUNCTION then
- local returnTypes = {}
- local currentReturnType
- local hasNext = false
- local name
- -- capture the return types
- repeat
- local nextKeyword = nextType( "return type modifiers (readOnly, allowsNil, etc.) or function name" )
- if not nextKeyword then
- -- no return type was given, hence we allow ANY return value and any amount of return values. If you don't want to return anything use nil
- if currentReturnType then
- -- the current return type is actually the function name, we don't have a type
- name = currentReturnType.type
- returnTypes = nil
- break
- else
- -- no function name was given
- parserError("expected function name")
- end
- elseif nextKeyword == KEYWORD_ALLOWSNIL then
- if not currentReturnType then
- parserError( "expected return type before 'allowsNil' modifier" )
- end
- currentReturnType.allowsNil = true
- -- we can't modify the property any further, add the return type to the stack
- table.insert( returnTypes, currentReturnType )
- currentReturnType = nil
- elseif currentReturnType then
- -- nextKeyword is actually the function name, add the return type to the stack
- name = nextKeyword
- table.insert( returnTypes, currentReturnType )
- currentReturnType = nil
- else
- currentReturnType = { type = nextKeyword, allowsNil = false }
- end
- hasNext = isNext( "^%s*%,", true )
- if hasNext and name then
- parserError( "invalid return type modifier '" .. name .. "'" )
- end
- if hasNext then
- table.insert( returnTypes, currentReturnType )
- currentReturnType = nil
- -- varargs might be next (which won't be captured as a keyword)
- local isVarArg = isNext( "^%s*%.%.%.%s*", true )
- if isVarArg then
- table.insert( returnTypes, { isVarArg = true } )
- hasNext = false
- end
- end
- until not currentReturnType and not hasNext
- if not name then
- name = demandImmediateNextName( "event function handle name" )
- end
- local functionTable = { false --[[ simply a placeholder to be once the block can be captured ]] }
- -- capture the parameters
- local parameters = {}
- hasOpeningBracket = isNext( "^%s*%(%s*", true )
- if not hasOpeningBracket then
- parserError( "expected opening bracket for function parameters" )
- end
- repeat
- local allowsNil, parameterName = false
- -- first we try to capture the type, however, this may also be the name. this is needed because the type isn't always a keyword (for tabes and dictionary types)
- local parameterType, parameterIsTable = nextType()
- if not parameterType then
- -- there wasn't a type/name given, try to capture varargs (...)
- local isVarArg = isNext( "^%s*%.%.%.%s*%)", true )
- if isVarArg then
- table.insert(parameters, {
- isVarArg = true,
- name = "..."
- })
- stepBack(")")
- end
- break
- end
- while true do
- local nextKeyword = immediateNextName()
- if not nextKeyword then
- if parameterType then
- if parameterIsTable then
- parserError( "expected modifier (" .. KEYWORD_ALLOWSNIL .. ") or property name" )
- else
- -- a type wasn't given, we'll allow anything (including nil)
- parameterName = parameterType
- parameterType = nil
- allowsNil = true
- break
- end
- else
- parserError( "expected parameter type, modifier (" .. KEYWORD_ALLOWSNIL .. ") or property name" )
- end
- elseif nextKeyword == KEYWORD_ALLOWSNIL then
- if not parameterType then
- parserError( "expected parameter type before modifier '" .. nextKeyword .. "'" )
- end
- allowsNil = true
- elseif not parameterType then
- parameterType = nextKeyword
- else
- parameterName = nextKeyword
- break
- end
- end
- if parameterName == "self" or parameterName == "super" then
- parserError("attempted to use '" .. parameterName .. "' as a parameter name. " .. parameterName .. " is automatically passed and cannot be used for another parameter.")
- end
- -- try and get the default value out of it
- local hasEquals = isNext( "^(%s*)=(%s*)", true )
- local defaultValue
- if hasEquals then
- defaultValue = captureParameterDefault()
- end
- table.insert(parameters, {
- allowsNil = allowsNil,
- type = parameterType,
- name = parameterName,
- defaultValue = defaultValue
- })
- hasNext = isNext( "^%s*%,", true )
- until not hasNext
- isNext( "^%s*%)", true ) -- capture the trailing bracket
- local functionTable
- if not isInterface then
- local block, startLine = captureReturnsBlock()
- local functionString = "(__CLASS__self_passed"
- local parameterNames = {}
- for i, parameter in ipairs( parameters ) do
- functionString = functionString .. "," .. parameter.name
- table.insert(parameterNames, parameter.name)
- end
- functionTable = { functionString .. ")", block, startLine, parameters, returnTypes, parameterNames }
- else
- functionTable = { parameters, returnTypes }
- end
- if isStatic then
- currentClass.static.functions[name] = functionTable
- else
- currentClass.instance.functions[name] = functionTable
- end
- else
- -- break
- parserError( "unexpected keyword '" .. tostring(keyword) .. "', expected property/function declaration or 'end'." )
- end
- if isStatic and keyword ~= KEYWORD_STATIC then
- isStatic = false
- end
- end
- end
- end
- print( "Parsed '" .. fileName .. "' without error in " .. os.clock() - fileStartTime .. "s!" )
- end
- print("Parsed in " .. os.clock() - parseStartTime .. "s!")
- -- END PARSER
- -- BEGIN COMPILER
- local compileStartTime = os.clock()
- print("Starting compile...")
- local file = ""
- local function add(str, ...)
- file = file .. string.format(str, ...)
- end
- -- escape square brackets
- local function escapeSquare(str)
- return str:gsub("%[(=*)%[", "[%1=["):gsub("%](=*)%]", "]%1=]")
- end
- local idN = 1
- local idPrefix = "__CLASS__"
- local idChars = {"0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z"}
- local idCharsLength = #idChars
- local function nextID()
- local id = ""
- local rem = idN
- while rem > 0 do
- id = idChars[(rem - 1) % idCharsLength + 1] .. id
- rem = floor((rem - 1) / idCharsLength)
- end
- idN = idN + 1
- return idPrefix .. id
- end
- local function addFunction(str, ...)
- local functionID = nextID()
- add("local function %s%s ", functionID, string.format(str, ...))
- return functionID
- end
- local lineNumberMap = {}
- local lineNumberingOutputLine, lineNumberingSourceFile, lineNumberingSourceLine
- -- add a line number anchor
- local function startLineNumbering(sourceFile, sourceLine)
- sourceFile = fileEnvironments[sourceFile].fileName
- local outputLine = 1
- for i in string.gmatch(file, "\n") do
- outputLine = outputLine + 1
- end
- if lineNumberMap[outputLine] then
- error("duplicate error line on " .. outputLine)
- else
- lineNumberingOutputLine, lineNumberingSourceFile, lineNumberingSourceLine = outputLine, sourceFile, sourceLine
- end
- end
- -- fill from the first line number anchor up to where we are now
- local function stopLineNumbering()
- local outputLine = 1
- for i in string.gmatch(file, "\n") do
- outputLine = outputLine + 1
- end
- add("\n")
- for i = 0, outputLine - lineNumberingOutputLine do
- lineNumberMap[lineNumberingOutputLine + i] = lineNumberingSourceFile .. ":" .. lineNumberingSourceLine + i .. ":"
- end
- lineNumberingOutputLine, lineNumberingSourceFile, lineNumberingSourceLine = nil, nil, nil
- end
- -- first add all the built in functions
- local functionErrorID = addFunction([[(m)error(m,3)end]])
- local functionErrorIDs = {
- metatable = {
- class = {
- index = addFunction([[(_,k)%s("attempted to access non-existant enum '"..k.."' of class '"..tostring(_).."'")end]], functionErrorID),
- newindex = addFunction([[(_,k,v)%s("attempted to mutate class '"..tostring(_).."' using key '"..k.."'")end]], functionErrorID)
- },
- interface = {
- index = addFunction([[(_)%s("attempted to access value from interface '"..tostring(_).."'")end]], functionErrorID),
- newindex = addFunction([[(_,k)%s("attempted to mutate interface '"..tostring(_).."' using key '"..k.."'")end]], functionErrorID)
- },
- instance = {
- index = addFunction([[(_,k)%s("attempted to access non-existant property '"..k.."' of '"..tostring(_).."'")end]], functionErrorID),
- newindex = addFunction([[(_,k,v)%s("attempted to set non-existant property '"..k.."' of '"..tostring(_).."' to '"..v.."'")end]], functionErrorID)
- },
- func = {
- newindex = addFunction([[(_,k,v)%s("attempted to set value of function '"..k.."' of '"..tostring(_).."' to '"..v.."'")end]], functionErrorID)
- },
- enum = {
- index = addFunction([[(_,k)%s("attempted to access non-existant key '"..k.."' from enum '"..tostring(_).."'")end]], functionErrorID),
- newindex = addFunction([[(_,k,v)%s("attempted to mutate enum '"..tostring(_).."' key '"..tostring(k).."' to '"..v.."'")end]], functionErrorID)
- },
- super = {
- index = addFunction([[(_,k)if k=="super"then %s("tried to access super of '" .. tostring(_) .. "', which does not have a super")else %s("attempted to access invalid index '" .. k .. "' of super '" .. tostring(_) .. "'")end end]], functionErrorID, functionErrorID),
- newindex = addFunction([[(_,k,v)%s("attempted to mutate super '"..tostring(_).."' using key '"..k.."'")end]], functionErrorID)
- },
- },
- readOnly = addFunction([[(_,k,v)%s("attempted to set read only property '"..k.."' of '"..tostring(_).."' to '"..v.."'")end]], functionErrorID),
- type = {
- property = addFunction([[(_,k,t,v)%s("attempted to set property '"..k.."' of '"..tostring(_).."' to an invalid value '"..tostring(v).."', expected type '"..t.."'")end]], functionErrorID),
- default = addFunction([[(_,k,t,v)%s("default value of property '"..k.."' of '"..tostring(_).."' was an invalid value '"..tostring(v).."', expected type '"..t.."'")end]], functionErrorID),
- parameter = addFunction([[(_,k,f,t,v)%s("attempted to pass parameter '"..k.."' of '"..tostring(_).."."..f.."' an invalid value '"..tostring(v).."', expected type '"..t.."'")end]], functionErrorID),
- enum = addFunction([[(_,k,t,v)%s("enum '".._.."' has an invalid value '"..tostring(v).."' for key '"..k.."', expected type '"..t.."'")end]], functionErrorID),
- returned = {
- get = addFunction([[(_,k,t,v)%s("return value from getter '"..k.."' of '"..tostring(_).."' was an invalid value '"..tostring(v).."', expected type '"..t.."'")end]], functionErrorID),
- func = addFunction([[(_,k,i,v,t)%s("return value "..i.." from function '"..k.."' of '"..tostring(_).."' was an invalid value '"..tostring(v).."', expected type '"..t.."'")end]], functionErrorID),
- }
- }
- }
- local functionExecuteID = addFunction([[(f,e,...)return setfenv(f, e)(...)end]])
- -- TODO: minify when it 100% works
- local functionErrorProxyID = addFunction([[(name, startLine, func)
- return select(2, xpcall(func, function(err)
- local _, trace = pcall(error, "<@", 3)
- local lineNumber = trace:match(":(%%d+): <@")
- if not lineNumber then print(err)
- else
- print(name .. ":" .. lineNumber + startLine .. ": " .. err:match(":" .. lineNumber .. ": (.*)$"))
- end
- end))
- end]])
- -- create tables for instances. every instance gets added to this table (but it is weak to prevent leaks). this is used for checking if a table is an instance
- local instancesID = nextID()
- add("local %s=setmetatable({},{__mode=\"k\"})", instancesID)
- local staticInstancesID = nextID()
- add("local %s=setmetatable({},{__mode=\"k\"})", staticInstancesID)
- -- add the type of function
- add("function typeOf(i,t)local j=%s[i]if j then return j[t] and true or false else local s=%s[i]if s then return s[t] and true or false end return false end end ", instancesID, staticInstancesID)
- local classNamesIDs = {}
- for className, _ in pairs(parsed) do
- local id = nextID()
- classNamesIDs[className] = id
- add("local %s=%q", id, className)
- end
- local instanceCreateID, localiseFunctionsID = nextID(), nextID()
- add([[local function %s(instance, funcs, instanceEnvironment, supers, supers__tostrings)
- local isFirst = true
- local superFuncs = {}
- local stack = {}
- local n = 1
- local f
- for i = 1, #supers + 1 do
- local func = funcs[i]
- if func then
- if not isFirst then
- stack[n] = {i, func}
- n = n + 1
- else
- f = func
- isFirst = false
- end
- end
- end
- for i = n - 1, 1, -1 do
- local details = stack[i]
- local superI = details[1]
- local func = setfenv(details[2](supers[superI], superFuncs[i + 1]), instanceEnvironment)
- local super = superFuncs[i + 1]
- local __index
- if super then
- __index = function(_, k, v)
- if k == "super" then
- return super
- else
- %s(_, k, v) -- __CLASS__error_function_super_no_index
- end
- end
- else
- __index = %s
- end
- superFuncs[i] = setmetatable({}, {__index = __index, __newindex = __CLASS__8, __tostring = supers__tostrings[superI], __call = function(_, ...)return func(super, ...) end})
- end
- return setfenv(f(instance, superFuncs[1]), instanceEnvironment)
- end
- local function %s(instance, instanceType, instanceVariables, instanceFunctions, defaultValues, defaultTypes, environmentClass, name, instance__index, instance__newindex, supers_names, propertyMethodFunctions, typeOfTree, ...)
- (instanceType == "static" and %s or %s)[instance] = typeOfTree -- instancesID/staticInstancesID
- local instanceEnvironment
- if instanceVariables then
- instanceEnvironment = setmetatable({}, {
- __index = function(_, k)
- if not instanceVariables[k] then -- if an instance variable is nil and there is an upvalue local with the same name the instance variable will hide the upvalue, even when nil
- return environmentClass[k]
- end
- end,
- __newindex = function(_, k, v)
- if not instanceVariables[k] then -- all instance variables hide upvalues, regardless of their value
- environmentClass[k] = v -- TODO: should this be the class environment, or _G?
- else
- rawset(_, k, v)
- end
- end
- })
- for k, v in pairs(instanceVariables) do
- instanceEnvironment[k] = %s(v, environmentClass)
- end
- else
- instanceEnvironment = environmentClass
- end
- local values = {}
- local getLocks, setLocks = {}, {}
- local __tostring = instanceType .. " of '" .. name .. "': " .. tostring(instance):sub(8)
- local getMethods, willSetMethods, setMethods, didSetMethods = {}, {}, {}, {}
- setmetatable(instance, {
- __index = function(_,k)
- return instance__index[k](instance, k, values, getMethods, getLocks)
- end,
- __newindex = function(_,k,v)
- instance__newindex[k](instance, k, v, values, willSetMethods, setMethods, didSetMethods, setLocks)
- end,
- __tostring = function() return __tostring end
- })
- if defaultValues then
- for k,v in pairs(defaultValues) do
- local newValue = %s(v, environmentClass)
- local neededType = defaultTypes[k]
- if neededType and neededType[1](newValue) then
- %s(instance,k,neededType[2],newValue)
- end
- values[k] = newValue
- end
- end
- local supers = {}
- local supers__tostrings = {}
- local supersEnvironments = {}
- local supersValues = {}
- if supers_names then
- for i, super_name in ipairs(supers_names) do
- local super = {}
- -- local superValues = setmetatable({}, {__index = values, __newindex = function(_,k,v)values[k] = v end})
- local super__tostring = "super '" .. super_name .. "': " .. tostring(super):sub(8) .. " of " .. __tostring
- local super__tostring_func = function() return super__tostring end
- setmetatable(super, {
- __index = function(_,k)
- return instance__index[k](super, k, values, instanceEnvironment, getLocks)
- end,
- __newindex = function(_,k,v)
- instance__newindex[k](super, k, v, values, instanceEnvironment, setLocks)
- end,
- __tostring = super__tostring_func
- })
- supers[i +1] = super
- supers__tostrings[i +1] = super__tostring_func
- end
- end
- if propertyMethodFunctions then
- -- getMethods, willSetMethods, setMethods, didSetMethods = {}, {}, {}, {}
- local propertyMethods = {get = getMethods, willSet = willSetMethods, set = setMethods, didSet = didSetMethods}
- for methodName, properties in pairs(propertyMethodFunctions) do
- for propertyName, funcs in pairs(properties) do
- propertyMethods[methodName][propertyName] = %s(instance, funcs, instanceEnvironment, supers, supers__tostrings)
- end
- end
- end
- if instanceFunctions then
- for k, funcs in pairs(instanceFunctions) do
- values[k] = %s(instance, funcs, instanceEnvironment, supers, supers__tostrings)
- end
- end
- values.typeOf = function(_,other)
- return typeOfTree[other] == 1
- end
- local initialiseFunction = values.initialise
- if initialiseFunction then
- initialiseFunction(instance,...)
- end
- return instance
- end ]], localiseFunctionsID, functionErrorIDs.metatable.super.newindex, functionErrorIDs.metatable.super.newindex, instanceCreateID, instancesID, staticInstancesID, functionExecuteID, functionExecuteID, functionErrorIDs.type.default, localiseFunctionsID, localiseFunctionsID)
- -- now add the file environments
- local fileEnvironmentIDs = {}
- for i, blocks in ipairs(fileEnvironments) do
- local id = nextID()
- fileEnvironmentIDs[i] = id
- add("local %s=setmetatable({},{__index=_G})", id)
- end
- -- figure out the order that the classes need to be loaded (supers first)
- local classOrder = {}
- local orderedClasses = {}
- local function addClassOrder(className, classDetails)
- -- don't add a class that's already loaded
- if not orderedClasses[className] then
- -- if the class has a super we need to place this AFTER the super
- if classDetails.extends then
- if not orderedClasses[classDetails.extends] then
- -- the super hasn't been added yet, do that now
- addClassOrder(classDetails.extends, parsed[classDetails.extends])
- end
- end
- -- the super has been added to the list or there isn't one, we can simply add ourself to the end
- insert(classOrder, classDetails)
- orderedClasses[className] = true
- end
- end
- for className, classDetails in pairs(parsed) do
- addClassOrder(className, classDetails)
- end
- -- now create empty tables for all the classes and their statics (which are filled later on) so we can reference them directly when needed
- local classStaticIDs = {}
- local classTostringIDs = {}
- local classesString = "local %s={"
- local didAdd = false
- for className, classDetails in pairs(parsed) do
- -- TODO: maybe add an option which makes these locals?
- didAdd = true
- classesString = classesString .. string.format("[%s]=true,", className)
- add("%s={}", className)
- local tostringID = nextID()
- add("local %s=\"" .. (classDetails.isInterface and "interface" or "class") .. " '%s': \"..tostring(%s):sub(8)", tostringID, className, className)
- classTostringIDs[className] = tostringID
- if not classDetails.isInterface then
- local staticID = nextID()
- classStaticIDs[className] = staticID
- add("local %s={}", staticID)
- end
- end
- if didAdd then
- classesString = classesString:sub(1, #classesString - 1)
- end
- local classesID = nextID()
- add(classesString .. "}", classesID)
- local classesEnumIDs = {}
- local classesEnumTypeOfTableIDs = {}
- local functionTypeCheckIDs = {
- [true] = {}, [false] = {}
- }
- local standardTypes = {
- String = true,
- Number = true,
- Boolean = true,
- Table = true,
- Thread = true,
- Function = true
- }
- local TYPE_ANY = "Any"
- local function typeCheck(typeName, allowsNil)
- if allowsNil == nil then error("missing allowsNil value") end
- -- allowsNil = allowsNil ~= nil and allowsNil or false
- if not typeName or (typeName == TYPE_ANY and allowsNil) then
- return -- Any allowsNil doesn't do any type checking
- end
- local ids = functionTypeCheckIDs[allowsNil]
- if ids[typeName] then
- return ids[typeName]
- end
- local id
- local allowsNilCheckOr = allowsNil and "v~=nil and " or ""
- local allowsNilCheckReturnIf = allowsNil and "if v==nil then return false end " or ""
- local allowsNilCheckReturnIfNotTable = allowsNil and "if v==nil then return false elseif type(v)~=\"table\" then return true end " or "if type(v)~=\"table\" then return true end "
- if typeName == TYPE_ANY then
- id = addFunction("(v)return v~=nil end")
- elseif standardTypes[typeName] then
- id = addFunction("(v)return %stype(v)~=\"%s\"end", allowsNilCheckOr, typeName:lower())
- elseif typeName == "Class" then -- this means a CLASS, not an instance, not a static instance, a plain old class
- id = addFunction("(v)return %snot %s[v]or true end", allowsNilCheckOr, classesID)
- elseif typeName:match("^{.+}$") then
- local dictionaryTypeName1, dictionaryTypeName2 = typeName:match("^{([_%a][_%w]*)=([_%a][_%w]*)}$")
- if dictionaryTypeName1 and dictionaryTypeName2 then
- id = addFunction("(v)%sfor k,V in pairs(v)do if %s(k)or %s(V)then return true end end return false end", allowsNilCheckReturnIfNotTable, typeCheck(dictionaryTypeName1, false), typeCheck(dictionaryTypeName2, false))
- else
- local arrayTypeName = typeName:match("^{([_%a][_%w]*)}$")
- if arrayTypeName then
- id = addFunction("(v)%sfor i,V in pairs(v)do if %s(k)then return true end end return false end", allowsNilCheckReturnIfNotTable, typeCheck(arrayTypeName, false))
- end
- end
- else
- local staticClassName = typeName:match("^([_%a][_%w]*)%.static$")
- if staticClassName then
- -- this is a static class
- -- first check if that class name is defined
- if not standardTypes[staticClassName] and parsed[staticClassName] then
- id = addFunction("(v)return %snot %s[v][%s]end", allowsNilCheckOr, staticInstancesID, staticClassName)
- else
- error("compiler error: unknown class type '" .. typeName .. "' (class isn't defined)")
- end
- else
- local className, enumName = typeName:match("^([_%a][_%w]*)%.([_%a][_%w]*)$")
- if className and enumName then
- -- this is an enum
- local classEnumIDs = classesEnumIDs[className]
- local classEnumTypeOfTableIDs = classesEnumTypeOfTableIDs[className]
- if classEnumIDs then
- local enumID = classEnumIDs[enumName]
- local typeOfTableID = classEnumTypeOfTableIDs[enumName]
- id = addFunction("(v)return %snot %s[v] end", allowsNilCheckOr, typeOfTableID)
- else
- end
- else
- end
- end
- end
- if not id then
- error("compiler error: unknown type '" .. typeName .. "'")
- end
- ids[typeName] = id
- return id
- end
- -- the blank __index and __newindex functions for properties
- local functionBlankIndexID = nextID()
- add("local function %s(_,k,v)return v[k] end ", functionBlankIndexID)
- local functionBlankNewindexIDs = {[true]={},[false]={}}
- local function blankNewindexIDs(typeName, allowsNil)
- if not typeName then typeName = TYPE_ANY allowsNil = true end
- if functionBlankNewindexIDs[allowsNil][typeName] then
- return functionBlankNewindexIDs[allowsNil][typeName]
- else
- local id = nextID()
- functionBlankNewindexIDs[allowsNil][typeName] = id
- if typeName == TYPE_ANY then
- if allowsNil then
- add("local function %s(_,k,n,v)v[k]=n end ", id)
- else
- add("local function %s(_,k,n,v)if n==nil then %s(_,k,%q,n)end v[k]=n end ", id, functionErrorIDs.type.property, typeName)
- end
- else
- add("local function %s(_,k,n,v)if %s(n)then %s(_,k,%q,n)end v[k]=n end ", id, typeCheck(typeName, allowsNil), functionErrorIDs.type.property, typeName .. (allowsNil and " (allowsNil)" or ""))
- end
- return id
- end
- end
- local classMetatableMetatableID = nextID()
- add("local %s={__index=%s,__newindex=%s}", classMetatableMetatableID, functionErrorIDs.metatable.class.index, functionErrorIDs.metatable.class.newindex)
- local instanceMetatableMetatableID = nextID()
- add("local %s={__index=%s,__newindex=%s}", instanceMetatableMetatableID, functionErrorIDs.metatable.instance.index, functionErrorIDs.metatable.instance.newindex)
- -- first create the enums (so they can be used for type checking)
- for i, classDetails in pairs(classOrder) do
- if not classDetails.isInterface then
- local className = classDetails.className
- -- create the enums
- local enumIDs = {}
- local enumTypeOfTableIDs = {}
- for enumName, content in pairs(classDetails.enums) do
- -- create enum's value table ids
- local enumID = nextID()
- enumIDs[enumName] = enumID
- add("local %s ", enumID)
- local typeOfTableID = nextID()
- add("local %s={}", typeOfTableID)
- enumTypeOfTableIDs[enumName] = typeOfTableID
- end
- classesEnumIDs[className] = enumIDs
- classesEnumTypeOfTableIDs[className] = enumTypeOfTableIDs
- end
- end
- -- now create each class one by one
- local metatableStrings = {}
- local valueTableStrings = {}
- local staticInitialiseStrings = {}
- for i, classDetails in pairs(classOrder) do
- local className = classDetails.className
- print("Compiling: " .. className)
- if not classDetails.isInterface then
- local interfaces = {}
- for i, interfaceName in ipairs(classDetails.implements) do
- table.insert(interfaces, parsed[interfaceName])
- end
- -- create the tree of super details
- local supersDetails = {classDetails}
- local nextSuperName = classDetails.extends
- local previousSuperName = className
- while nextSuperName do
- local superDetails = parsed[nextSuperName]
- if not superDetails then
- error("compile error: unable to find class '" .. nextSuperName .. "' to extend class '" .. previousSuperName .. "' from")
- end
- table.insert(supersDetails, superDetails)
- previousSuperName = nextSuperName
- nextSuperName = superDetails.extends
- end
- -- create the enums
- local enumIDs = classesEnumIDs[className]
- -- add the class' value table
- local valueTableId = nextID()
- local valueTableString = "local %s=setmetatable({static=%s"
- for enumName, id in pairs(enumIDs) do
- valueTableString = valueTableString .. string.format(",%s=%s", enumName, id)
- end
- valueTableStrings[className] = {valueTableString .. "},%s)", valueTableId, classStaticIDs[className], classMetatableMetatableID}
- -- setup the instance and static values and functions
- for mode, modeDetails in pairs({instance = classDetails.instance, static = classDetails.static}) do
- -- create the detault values
- local defaultValuesID = nextID()
- local defaultValuesTypesID = nextID()
- -- we need to collate all the places where default values are set and flatten it in to one table
- local flattenedDefaultValues = {}
- local flattenedDefaultValuesTypes = {}
- for i, superAllDetails in ipairs(supersDetails) do
- local superDetails = superAllDetails[mode]
- -- go through ourself and super
- for propertyName, defaultValue in pairs(superDetails.defaultValues) do
- -- first add the override default values
- if not flattenedDefaultValues[propertyName] then
- flattenedDefaultValues[propertyName] = defaultValue
- end
- end
- for propertyName, propertyDetails in pairs(superDetails.properties) do
- -- then try to get the default from the property definitions
- if not flattenedDefaultValues[propertyName] then
- flattenedDefaultValues[propertyName] = propertyDetails.defaultValue
- end
- if not flattenedDefaultValuesTypes[propertyName] and propertyDetails.type then
- flattenedDefaultValuesTypes[propertyName] = {propertyDetails.type, propertyDetails.allowsNil}
- end
- end
- end
- add("local %s={", defaultValuesID)
- for propertyName, defaultValue in pairs(flattenedDefaultValues) do
- add("[%q]=loadstring(%q),", propertyName, "return " .. defaultValue)
- end
- add("}")
- local defaultValuesTypesString = "local %s={"
- for propertyName, typeDetails in pairs(flattenedDefaultValuesTypes) do
- defaultValuesTypesString = defaultValuesTypesString .. string.format("[%q]={%s,%q},", propertyName, typeCheck(typeDetails[1], typeDetails[2]), typeDetails[1] .. (typeDetails[2] and " (allowsNil)" or ""))
- end
- add(defaultValuesTypesString .. "}", defaultValuesTypesID)
- -- add the instance variables
- local instanceVariablesID
- local instanceVariablesString = "local %s={"
- didAdd = false
- local addedInstanceVariables = {}
- for i, superDetails in ipairs(supersDetails) do
- for variableName, content in pairs(superDetails[mode].instanceVariables) do
- if not addedInstanceVariables[variableName] then
- instanceVariablesString = instanceVariablesString .. string.format("[%q]=loadstring([[return %s]]),", variableName, escapeSquare(content))
- didAdd = true
- addedInstanceVariables[variableName] = true
- end
- end
- end
- if didAdd then
- instanceVariablesID = nextID()
- add(instanceVariablesString:sub(1,#instanceVariablesString - 1) .. "}", instanceVariablesID)
- end
- -- add the functions
- local functionIDs = {}
- local functionsTable = "local %s={"
- local didAdd = false
- local allFunctionNames = {} -- all of the function names defined by us and the supers
- for functionName, functionDetails in pairs(modeDetails.functions) do
- -- first check that none of the parameters have same name as an instance variable (which would cause name clashes)
- for i, parameterName in ipairs(functionDetails[6]) do
- if addedInstanceVariables[parameterName] then
- error("compiler error: parameter '" .. parameterName .. "' of " .. mode .. " function '" .. functionName .. "' in class '" .. className .. "' has the same name as an instance variable. Parameters cannot use the name of an instance variable.")
- end
- end
- -- add the actual function code
- local functionID = nextID()
- local typeChecking = ""
- for i, argumentDetails in ipairs(functionDetails[4]) do
- if argumentDetails.defaultValue then
- typeChecking = typeChecking .. string.format("if %s==nil then %s=%s end ", argumentDetails.name, argumentDetails.name, argumentDetails.defaultValue, argumentDetails.name, argumentDetails.defaultValue)
- end
- if not argumentDetails.isVarArg and argumentDetails.type then
- typeChecking = typeChecking .. string.format("if %s(%s)then %s(self,%q,%q,%q,%s)end ", typeCheck(argumentDetails.type, argumentDetails.allowsNil), argumentDetails.name, functionErrorIDs.type.parameter, argumentDetails.name, functionName, argumentDetails.type .. (argumentDetails.allowsNil and " (allowsNil)" or ""), argumentDetails.name)
- end
- end
- local functionBody = ""
- local returnTypes = functionDetails[5]
- for n, part in ipairs(functionDetails[2]) do
- -- return vaule types
- if type(part) == "string" then
- functionBody = functionBody .. part
- elseif type(part) == "table" then
- -- these are return values
- local cachedValues = {}
- if returnTypes then
- -- there are a set number of return types
- local isVarArg = false
- for i = 1, math.max(#part, #returnTypes) do
- local returnType = returnTypes[i]
- local value = part[i]
- if returnType and returnType.isVarArg then
- -- we don't care about vararg values' types
- break
- end
- if returnType and not value and not returnType.allowsNil then
- -- no value was given for this value, but there should have been one
- error("compiler error: invalid return value for function '" .. functionName .. "' #" .. i .. ", nothing was given but expected type '" .. returnType.type .. (returnType.allowsNil and " (allowsNil)" or "") .. "'")
- elseif returnType then
- -- this value is fine
- cachedValues[i] = true
- -- TODO: this is going to break with unpack..
- functionBody = functionBody .. string.format("local %s%d=%s if %s(%s%d)then %s(self,%q,%d,%s%d,%q)end ", idPrefix, i, value:gsub("^%s*", ""):gsub("%s*$", ""), typeCheck(returnTypes[i].type, returnTypes[i].allowsNil), idPrefix, i, functionErrorIDs.type.returned.func, functionName, i, idPrefix, i, returnType.type .. (returnType.allowsNil and " (allowsNil)" or ""))
- elseif not returnType and value then
- -- a value was given, but there should've been one
- error("compiler error: invalid return value for function '" .. functionName .. "' #" .. i .. ", '" .. value:gsub("^%s*", ""):gsub("%s*$", "") .. "' was given but expected nothing (i.e. too many return values)")
- end
- end
- end
- functionBody = functionBody .. "return"
- for i, value in ipairs(part) do
- if cachedValues[i] then
- functionBody = functionBody .. (i == 1 and " " or "") .. idPrefix .. i .. (i ~= #part and "," or "")
- else
- functionBody = functionBody .. value .. (i ~= #part and "," or "")
- end
- end
- end
- end
- startLineNumbering(classDetails.fileEnvironment, functionDetails[3])
- add("local function %s(self,super)return function%s%s%s end", functionID, functionDetails[1], typeChecking, functionBody)
- stopLineNumbering()
- functionIDs[functionName] = functionID
- end
- classDetails[mode].functionIDs = functionIDs
- for i, superDetails in ipairs(supersDetails) do
- for functionName, id in pairs(superDetails[mode].functionIDs) do
- allFunctionNames[functionName] = true
- end
- end
- for functionName, _ in pairs(allFunctionNames) do
- -- create the functions table (the list of the function and all its supers)
- local functionsTableString = string.format("[%q]={", functionName)
- local isConsecutive = true
- for i, superDetails in ipairs(supersDetails) do
- local superFunctionID = superDetails[mode].functionIDs[functionName]
- if superFunctionID then
- -- this super class has another version of the same function
- if isConsecutive then
- functionsTableString = functionsTableString .. string.format("%s,", superFunctionID)
- else
- functionsTableString = functionsTableString .. string.format("[%d]=%s,", i, superFunctionID)
- end
- else
- isConsecutive = false
- end
- end
- functionsTable = functionsTable .. functionsTableString .. "},"
- didAdd = true
- end
- local functionsTableID
- if didAdd then
- functionsTableID = nextID()
- add(functionsTable:sub(1,#functionsTable - 1) .. "}", functionsTableID)
- end
- -- check that the functions align with the interfaces
- for i, interface in ipairs(interfaces) do
- for functionName, interfaceFunctionDetails in pairs(interface[mode].functions) do
- local functionDetails
- for i, superDetails in ipairs(supersDetails) do
- local superFunctionDetails = superDetails[mode].functions[functionName]
- if superFunctionDetails then
- functionDetails = superFunctionDetails
- break
- end
- end
- if not functionDetails then
- error("compiler error: class '" .. className .. "' does not define " .. mode .. " function '" .. functionName .. "' required by interface '" .. interface.className .. "'")
- else
- for n, interfaceTypes in ipairs(interfaceFunctionDetails) do
- local functionTypes = functionDetails[n + 3]
- local name = n == 1 and "parameter" or "return value"
- if #interfaceTypes ~= #functionTypes then
- error("compiler error: class '" .. className .. "' " .. mode .. " function '" .. functionName .. "' has a different number of " .. name .. "s (" .. #functionTypes .. ") than required by interface '" .. interface.className .. "' (" .. #interfaceTypes .. ").")
- end
- for j, interfaceType in ipairs(interfaceTypes) do
- local functionType = functionTypes[j]
- if interfaceType.allowsNil ~= functionType.allowsNil or interfaceType.type ~= functionType.type or interfaceType.isVarArg ~= functionType.isVarArg then
- error("compiler error: class '" .. className .. "' " .. mode .. " function '" .. functionName .. "' " .. name .. " #" .. j .. " is of a different type than the one required by interface '" .. interface.className .. "'.")
- end
- end
- end
- end
- end
- end
- -- add the properties
- -- start the index metatable
- local propertyMethodIDs = {}
- -- as with default values, we need to collate all the places where properties are defined and flatten it in to one table
- local flattenedProperties = {}
- local flattenedPropertyMethods = {}
- for i = #supersDetails, 1, -1 do
- -- we need to go backwards because properties should only be defined ONCE (i.e. if it tries to override then error)
- local superDetails = supersDetails[i][mode]
- for propertyName, propertyDetails in pairs(superDetails.properties) do
- -- first check that the property doesn't have same name as an instance variable (which would cause name clashes)
- if addedInstanceVariables[propertyName] then
- error("compiler error: " .. mode .. " property '" .. propertyName .. "' of class '" .. supersDetails[i].className .. "' has the same name as an instance variable. Properties cannot use the name of an instance variable.")
- end
- -- then try to get the default from the property definitions
- if flattenedProperties[propertyName] then
- error("compiler error: attempted to redeclare " .. mode .. " property '" .. propertyName .. " in " .. className .. " which was already defined by super class '" .. supersDetails[i].className .. "'. Subclasses are not allowed to change or redeclare properties defined by a super class.")
- else
- flattenedProperties[propertyName] = propertyDetails
- end
- end
- -- flatten all the property methods
- for methodName, methodDetails in pairs(superDetails.propertyMethods) do
- for propertyName, content in pairs(methodDetails) do
- if not flattenedPropertyMethods[propertyName] then
- flattenedPropertyMethods[propertyName] = {}
- end
- if not flattenedPropertyMethods[propertyName][methodName] then
- flattenedPropertyMethods[propertyName][methodName] = {[i] = content}
- else
- flattenedPropertyMethods[propertyName][methodName][i] = content
- end
- end
- end
- end
- -- check that the properties align with the interfaces
- for i, interface in ipairs(interfaces) do
- for propertyName, interfacePropertyDetails in pairs(interface[mode].properties) do
- local compiledProperty = flattenedProperties[propertyName]
- if not compiledProperty then
- error("compiler error: class '" .. className .. "' does not define " .. mode .. " property '" .. propertyName .. "' required by interface '" .. interface.className .. "'")
- else
- for k, v in pairs(interfacePropertyDetails) do
- if compiledProperty[k] ~= v then
- error("compiler error: class '" .. className .. "' " .. mode .. " property '" .. propertyName .. "' definition is different to the one required by interface '" .. interface.className .. "'. " .. k .. " was different.")
- end
- end
- end
- end
- end
- -- add all the property method function code
- local propertyMethodIDs = {}
- for methodName, methods in pairs(classDetails[mode].propertyMethods) do
- for propertyName, methodDetails in pairs(methods) do
- if not flattenedProperties[propertyName] then
- error("compiler error: found property method '" .. methodName .. "' for undefined property '" .. propertyName .. "'!")
- end
- if not propertyMethodIDs[propertyName] then
- propertyMethodIDs[propertyName] = {}
- end
- if methodName then
- local methodID = nextID()
- add("local function %s(self,super)return function%s end ", methodID, methodDetails[1])
- propertyMethodIDs[propertyName][methodName] = methodID
- end
- end
- end
- classDetails[mode].propertyMethodIDs = propertyMethodIDs
- -- create the super tree for property methods
- local propertyMethodsTables = {get = "get={", didSet = "didSet={", willSet = "willSet={", set = "set={"}
- local didAddTable = {get = false, didSet = false, willSet = false, set = false}
- for propertyName, methods in pairs(flattenedPropertyMethods) do
- propertyMethodsTableStrings = {get = string.format("[%q]={", propertyName), didSet = string.format("[%q]={", propertyName), willSet = string.format("[%q]={", propertyName), set = string.format("[%q]={", propertyName)}
- for methodName, methodDetails in pairs(methods) do
- local isConsecutive = true
- for i, superDetails in ipairs(supersDetails) do
- local superFunctionID = superDetails[mode].propertyMethodIDs[propertyName]
- superFunctionID = superFunctionID and superFunctionID[methodName] -- only get the method if the super has any methods for that property name
- if superFunctionID then
- -- this super class has another version of the same function
- if isConsecutive then
- propertyMethodsTableStrings[methodName] = propertyMethodsTableStrings[methodName] .. string.format("%s,", superFunctionID)
- else
- propertyMethodsTableStrings[methodName] = propertyMethodsTableStrings[methodName] .. string.format("[%d]=%s,", i, superFunctionID)
- end
- else
- isConsecutive = false
- end
- end
- propertyMethodsTables[methodName] = propertyMethodsTables[methodName] .. propertyMethodsTableStrings[methodName]:sub(1,#propertyMethodsTableStrings[methodName] - 1) .. "},"
- didAddTable[methodName] = true
- end
- end
- local propertyMethodsTable = "local %s={"
- didAdd = false
- for methodName, didAddMethod in pairs(didAddTable) do
- if didAddMethod then
- didAdd = true
- propertyMethodsTable = propertyMethodsTable .. propertyMethodsTables[methodName]:sub(1,#propertyMethodsTables[methodName] - 1) .. "},"
- end
- end
- local propertyMethodsTableID
- if didAdd then
- propertyMethodsTableID = nextID()
- add(propertyMethodsTable:sub(1,#propertyMethodsTable - 1) .. "}", propertyMethodsTableID)
- end
- -- create the super tree tables for all the property methods
- local flattenedPropertyMethodIDs = {}
- local propertyMethodIndicies = {}
- local index = 1
- for propertyName, properties in pairs(flattenedPropertyMethods) do
- flattenedPropertyMethodIDs[propertyName] = {}
- propertyMethodIndicies[propertyName] = {}
- for methodName, methods in pairs(properties) do
- local lastI = 0
- local didAddInner = false
- for superI, methodDetails in pairs(methods) do
- local superMethodID = supersDetails[superI][mode].propertyMethodIDs[propertyName]
- if superMethodID then
- superMethodID = superMethodID[methodName]
- end
- if superMethodID then
- if not flattenedPropertyMethodIDs[propertyName][methodName] then
- flattenedPropertyMethodIDs[propertyName][methodName] = superMethodID
- propertyMethodIndicies[superMethodID] = index
- end
- end
- end
- end
- end
- -- create the instance __index & __newindex methods for accessing properties and functions
- local instanceIndexTableID
- local instanceIndexTable = "local %s=setmetatable({"
- local instanceNewindexTableID
- local instanceNewindexTable = "local %s=setmetatable({"
- local didAdd = false
- for propertyName, propertyDetails in pairs(flattenedProperties) do
- local propertyMethods = flattenedPropertyMethods[propertyName]
- -- add the index method
- if propertyMethods and propertyMethods.get then
- -- there is a custom index property method
- instanceIndexTable = instanceIndexTable .. string.format("[%q]=function(_,k,v,g,l)", propertyName)
- instanceIndexTable = instanceIndexTable .. "if l[k]then return v[k]else l[k]=true "
- instanceIndexTable = instanceIndexTable .. "local v=g[k](_)l[k]=nil "
- if propertyDetails.type ~= TYPE_ANY or not propertyDetails.allowsNil then
- instanceIndexTable = instanceIndexTable .. string.format("if %s(v)then %s(_,k,%q,v)end ", typeCheck(propertyDetails.type, propertyDetails.allowsNil), functionErrorIDs.type.returned.get, propertyDetails.type .. (propertyDetails.allowsNil and " (allowsNil)" or ""))
- end
- instanceIndexTable = instanceIndexTable .. "return v end end,"
- else
- -- there aren't any methods, we just need to return the value
- instanceIndexTable = instanceIndexTable .. string.format("[%q]=%s,", propertyName, functionBlankIndexID)
- end
- if propertyDetails.readOnly then
- if propertyMethods and (propertyMethods.set or propertyMethods.didSet or propertyMethods.willSet) then
- -- the property is readonly, yet there are setter property methods
- error("compiler error: property '" .. propertyName .. "' of class '" .. className .. "' has willSet, set or didSet property methods, but the property is read only. These methods will never be called, remove them from your code.")
- end
- instanceNewindexTable = instanceNewindexTable .. string.format("[%q]=%s,", propertyName, functionErrorIDs.readOnly)
- elseif propertyMethods and (propertyMethods.set or propertyMethods.didSet or propertyMethods.willSet) then
- instanceNewindexTable = instanceNewindexTable .. string.format("[%q]=function(_,k,n,v,w,s,d,l)", propertyName)
- if propertyDetails.type ~= TYPE_ANY or not propertyDetails.allowsNil then
- instanceNewindexTable = instanceNewindexTable .. string.format("if %s(n)then %s(_,k,%q,n)end ", typeCheck(propertyDetails.type, propertyDetails.allowsNil), functionErrorIDs.type.property, propertyDetails.type .. (propertyDetails.allowsNil and " (allowsNil)" or ""))
- end
- instanceNewindexTable = instanceNewindexTable .. "if l[k]then v[k]=n else l[k]=true "
- if propertyMethods.willSet then
- instanceNewindexTable = instanceNewindexTable .. "w[k](_,n)"
- end
- if propertyMethods.set then
- instanceNewindexTable = instanceNewindexTable .. "s[k](_,n)"
- else
- instanceNewindexTable = instanceNewindexTable .. "v[k]=n "
- end
- if propertyMethods.didSet then
- instanceNewindexTable = instanceNewindexTable .. "d[k](_,n)"
- end
- instanceNewindexTable = instanceNewindexTable .. "l[k]=nil end end,"
- else
- -- there aren't any methods, we just need to set the value
- instanceNewindexTable = instanceNewindexTable .. string.format("[%q]=%s,", propertyName, blankNewindexIDs(propertyDetails.type, propertyDetails.allowsNil))
- end
- didAdd = true
- end
- -- add the functions to the tables
- for functionName, _ in pairs(allFunctionNames) do
- instanceNewindexTable = instanceNewindexTable .. string.format("[%q]=%s,", functionName, functionErrorIDs.metatable.func.newindex)
- instanceIndexTable = instanceIndexTable .. string.format("[%q]=%s,", functionName, functionBlankIndexID)
- end
- if didAdd then
- instanceIndexTableID = nextID()
- instanceNewindexTableID = nextID()
- add(instanceIndexTable:sub(1,#instanceIndexTable - 1) .. "},%s)", instanceIndexTableID, instanceMetatableMetatableID)
- add(instanceNewindexTable:sub(1,#instanceNewindexTable - 1) .. "},%s)", instanceNewindexTableID, instanceMetatableMetatableID)
- else
- instanceIndexTableID, instanceNewindexTableID = instanceMetatableMetatableID, instanceMetatableMetatableID
- end
- -- create the typeof tree and the list of supers
- local typeOfTreeString = "local %s={"
- local superNamesID
- local superNamesString = "local %s={"
- didAdd = false
- -- add the base classes & static instance
- for i, superDetails in ipairs(supersDetails) do
- local superName = superDetails.className
- typeOfTreeString = typeOfTreeString .. string.format("[%s]=1,", superName)
- if mode == "static" then
- -- add the static instances
- typeOfTreeString = typeOfTreeString .. string.format("[%s]=1,", classStaticIDs[superName])
- end
- if i ~= 1 then
- superNamesString = superNamesString .. string.format("%s,", classNamesIDs[superName])
- didAdd = true
- end
- -- add the interfaces
- for i, interfaceName in ipairs(superDetails.implements) do
- typeOfTreeString = typeOfTreeString .. string.format("[%s]=1,", interfaceName)
- end
- end
- local typeOfTreeID = nextID()
- add(typeOfTreeString:sub(1,#typeOfTreeString - 1) .. "}", typeOfTreeID)
- if didAdd then
- superNamesID = nextID()
- add(superNamesString:sub(1,#superNamesString - 1) .. "}", superNamesID)
- end
- -- add the class metatable
- if mode == "instance" then
- metatableStrings[className] = {"setmetatable(%s,{__index=%s,__newindex=%s,__call=function(_,...)return %s({},%q,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,...)end,__tostring=function()return %s end})", className, valueTableId, functionErrorIDs.metatable.class.newindex, instanceCreateID, mode, instanceVariablesID, functionsTableID, defaultValuesID, defaultValuesTypesID, fileEnvironmentIDs[classDetails.fileEnvironment], classNamesIDs[className], instanceIndexTableID, instanceNewindexTableID, superNamesID, propertyMethodsTableID, typeOfTreeID, classTostringIDs[className]}
- elseif mode == "static" then
- insert(staticInitialiseStrings, {"%s(%s,%q,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,...)",instanceCreateID, classStaticIDs[className], mode, instanceVariablesID, functionsTableID, defaultValuesID, defaultValuesTypesID, fileEnvironmentIDs[classDetails.fileEnvironment], classNamesIDs[className], instanceIndexTableID, instanceNewindexTableID, superNamesID, propertyMethodsTableID, typeOfTreeID})
- end
- end
- else
- add("setmetatable(%s,{__index=%s,__newindex=%s,__tostring=function()return %s end})", className, functionErrorIDs.metatable.interface.index, functionErrorIDs.metatable.interface.newindex, classTostringIDs[className])
- end
- end
- -- run the other code in the files inside their environments
- for i, blocks in ipairs(fileEnvironments) do
- local id = fileEnvironmentIDs[i]
- for startLine, content in pairs(blocks) do
- if startLine ~= "fileName" then
- add("%s(%q,%d,setfenv(loadstring([[%s]]), %s))", functionErrorProxyID, blocks.fileName, startLine, escapeSquare(content), id)
- end
- end
- end
- -- load the enum values
- for i, classDetails in pairs(classOrder) do
- if not classDetails.isInterface then
- local className = classDetails.className
- local enumIDs = classesEnumIDs[className]
- local enumTypeOfTableIDs = classesEnumTypeOfTableIDs[className]
- for enumName, content in pairs(classDetails.enums) do
- -- create enum's value table
- local enumTostringID = nextID()
- local enumID = enumIDs[enumName]
- add("%s=setfenv(loadstring([[return %s]]), %s)()", enumID, content.values, fileEnvironmentIDs[classDetails.fileEnvironment])
- add("local %s=\"enum '%s.%s': \" .. tostring(%s):sub(8)", enumTostringID, className, enumName, enumID)
- add("setmetatable(%s,{__index=%s,__newindex=%s,__tostring=function()return %s end})", enumID, functionErrorIDs.metatable.enum.index, functionErrorIDs.metatable.enum.newindex, enumTostringID)
- local typeOfTableID = enumTypeOfTableIDs[enumName]
- -- check the default values of the enum and create the type of table
- add("for k,v in pairs(%s)do if %s(v)then %s(%q,k,%q,v)end %s[v]=true end ", enumID, typeCheck(content.type, false), functionErrorIDs.type.enum, className .. "." .. enumName, content.type, typeOfTableID)
- end
- -- add the class' value table & metatable
- add(unpack(valueTableStrings[className]))
- add(unpack(metatableStrings[className]))
- end
- end
- -- initialise the statics
- for i, initialiseString in ipairs(staticInitialiseStrings) do
- add(unpack(initialiseString))
- end
- -- add the error map
- local lineNumberString = "local %s={"
- didAdd = false
- for lineNumber, errorPrefix in pairs(lineNumberMap) do
- lineNumberString = lineNumberString .. string.format("[%q]=%q,", lineNumber, errorPrefix)
- didAdd = true
- end
- if didAdd then
- local lineNumberID = nextID()
- add(lineNumberString:sub(1, #lineNumberString - 1) .. "}", lineNumberID)
- local filePathID = nextID()
- add("local _,__t=pcall(error,\"<@\",2)local %s=__t:match(\"^(.+):%%d+: <@\")", filePathID)
- add([[function catchErrors(f,...)local a = {...}local r = {pcall(function()return f(unpack(a))end)}if not r[1] then local e = r[2] local n,m=e:match("^"..%s .. ":(%%d+): (.+)$")if n then local p=%s[n]if p then error(p..m,0)end end error(e,0)else table.remove(r,1)return unpack(r)end end]], filePathID, lineNumberID)
- end
- print("Compiled in " .. os.clock() - compileStartTime .. "s!")
- print("Saving...")
- local h = io.open(outputPath, "w")
- if h then
- h:write(file)
- h:close()
- end
- print("Saved. Compile complete.")
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement