summaryrefslogtreecommitdiff
path: root/codegen/vulkan/scripts/reflib.py
diff options
context:
space:
mode:
Diffstat (limited to 'codegen/vulkan/scripts/reflib.py')
-rw-r--r--codegen/vulkan/scripts/reflib.py666
1 files changed, 666 insertions, 0 deletions
diff --git a/codegen/vulkan/scripts/reflib.py b/codegen/vulkan/scripts/reflib.py
new file mode 100644
index 00000000..bab7d30e
--- /dev/null
+++ b/codegen/vulkan/scripts/reflib.py
@@ -0,0 +1,666 @@
+#!/usr/bin/python3
+#
+# Copyright 2016-2021 The Khronos Group Inc.
+#
+# SPDX-License-Identifier: Apache-2.0
+
+# Utility functions for automatic ref page generation and other script stuff
+
+import io
+import re
+import sys
+import subprocess
+
+# global errFile, warnFile, diagFile
+
+errFile = sys.stderr
+warnFile = sys.stdout
+diagFile = None
+logSourcefile = None
+logProcname = None
+logLine = None
+
+def unescapeQuotes(s):
+ """Remove \' escape sequences in a string (refpage description)"""
+ return s.replace('\\\'', '\'')
+
+def write(*args, **kwargs ):
+ file = kwargs.pop('file',sys.stdout)
+ end = kwargs.pop('end','\n')
+ file.write(' '.join(str(arg) for arg in args))
+ file.write(end)
+
+def setLogSourcefile(filename):
+ """Metadata which may be printed (if not None) for diagnostic messages"""
+ global logSourcefile
+ logSourcefile = filename
+
+def setLogProcname(procname):
+ global logProcname
+ logProcname = procname
+
+def setLogLine(line):
+ global logLine
+ logLine = line
+
+def logHeader(severity):
+ """Generate prefix for a diagnostic line using metadata and severity"""
+ global logSourcefile, logProcname, logLine
+
+ msg = severity + ': '
+ if logProcname:
+ msg = msg + ' in ' + logProcname
+ if logSourcefile:
+ msg = msg + ' for ' + logSourcefile
+ if logLine:
+ msg = msg + ' line ' + str(logLine)
+ return msg + ' '
+
+def setLogFile(setDiag, setWarn, filename):
+ """Set the file handle to log either or both warnings and diagnostics to.
+
+ - setDiag and setWarn are True if the corresponding handle is to be set.
+ - filename is None for no logging, '-' for stdout, or a pathname."""
+ global diagFile, warnFile
+
+ if filename is None:
+ return
+
+ if filename == '-':
+ fp = sys.stdout
+ else:
+ fp = open(filename, 'w', encoding='utf-8')
+
+ if setDiag:
+ diagFile = fp
+ if setWarn:
+ warnFile = fp
+
+def logDiag(*args, **kwargs):
+ file = kwargs.pop('file', diagFile)
+ end = kwargs.pop('end','\n')
+ if file is not None:
+ file.write(logHeader('DIAG') + ' '.join(str(arg) for arg in args))
+ file.write(end)
+
+def logWarn(*args, **kwargs):
+ file = kwargs.pop('file', warnFile)
+ end = kwargs.pop('end','\n')
+ if file is not None:
+ file.write(logHeader('WARN') + ' '.join(str(arg) for arg in args))
+ file.write(end)
+
+def logErr(*args, **kwargs):
+ file = kwargs.pop('file', errFile)
+ end = kwargs.pop('end','\n')
+
+ strfile = io.StringIO()
+ strfile.write(logHeader('ERROR') + ' '.join(str(arg) for arg in args))
+ strfile.write(end)
+
+ if file is not None:
+ file.write(strfile.getvalue())
+ sys.exit(1)
+
+def isempty(s):
+ """Return True if s is nothing but white space, False otherwise"""
+ return len(''.join(s.split())) == 0
+
+class pageInfo:
+ """Information about a ref page relative to the file it's extracted from."""
+ def __init__(self):
+ self.extractPage = True
+ """True if page should be extracted"""
+
+ self.Warning = None
+ """string warning if page is suboptimal or can't be generated"""
+
+ self.embed = False
+ """False or the name of the ref page this include is embedded within"""
+
+ self.type = None
+ """refpage type attribute - 'structs', 'protos', 'freeform', etc."""
+
+ self.name = None
+ """struct/proto/enumerant/etc. name"""
+
+ self.desc = None
+ """short description of ref page"""
+
+ self.begin = None
+ """index of first line of the page (heuristic or // refBegin)"""
+
+ self.include = None
+ """index of include:: line defining the page"""
+
+ self.param = None
+ """index of first line of parameter/member definitions"""
+
+ self.body = None
+ """index of first line of body text"""
+
+ self.validity = None
+ """index of validity include"""
+
+ self.end = None
+ """index of last line of the page (heuristic validity include, or // refEnd)"""
+
+ self.alias = ''
+ """aliases of this name, if supplied, or ''"""
+
+ self.refs = ''
+ """cross-references on // refEnd line, if supplied"""
+
+ self.spec = None
+ """'spec' attribute in refpage open block, if supplied, or None for the default ('api') type"""
+
+ self.anchor = None
+ """'anchor' attribute in refpage open block, if supplied, or inferred to be the same as the 'name'"""
+
+def printPageInfoField(desc, line, file):
+ """Print a single field of a pageInfo struct, possibly None.
+
+ - desc - string description of field
+ - line - field value or None
+ - file - indexed by line"""
+ if line is not None:
+ logDiag(desc + ':', line + 1, '\t-> ', file[line], end='')
+ else:
+ logDiag(desc + ':', line)
+
+def printPageInfo(pi, file):
+ """Print out fields of a pageInfo struct
+
+ - pi - pageInfo
+ - file - indexed by pageInfo"""
+ logDiag('TYPE: ', pi.type)
+ logDiag('NAME: ', pi.name)
+ logDiag('WARNING:', pi.Warning)
+ logDiag('EXTRACT:', pi.extractPage)
+ logDiag('EMBED: ', pi.embed)
+ logDiag('DESC: ', pi.desc)
+ printPageInfoField('BEGIN ', pi.begin, file)
+ printPageInfoField('INCLUDE ', pi.include, file)
+ printPageInfoField('PARAM ', pi.param, file)
+ printPageInfoField('BODY ', pi.body, file)
+ printPageInfoField('VALIDITY', pi.validity, file)
+ printPageInfoField('END ', pi.end, file)
+ logDiag('REFS: "' + pi.refs + '"')
+
+def prevPara(file, line):
+ """Go back one paragraph from the specified line and return the line number
+ of the first line of that paragraph.
+
+ Paragraphs are delimited by blank lines. It is assumed that the
+ current line is the first line of a paragraph.
+
+ - file is an array of strings
+ - line is the starting point (zero-based)"""
+ # Skip over current paragraph
+ while (line >= 0 and not isempty(file[line])):
+ line = line - 1
+ # Skip over white space
+ while (line >= 0 and isempty(file[line])):
+ line = line - 1
+ # Skip to first line of previous paragraph
+ while (line >= 1 and not isempty(file[line-1])):
+ line = line - 1
+ return line
+
+def nextPara(file, line):
+ """Go forward one paragraph from the specified line and return the line
+ number of the first line of that paragraph.
+
+ Paragraphs are delimited by blank lines. It is assumed that the
+ current line is standalone (which is bogus).
+
+ - file is an array of strings
+ - line is the starting point (zero-based)"""
+ maxLine = len(file) - 1
+ # Skip over current paragraph
+ while (line != maxLine and not isempty(file[line])):
+ line = line + 1
+ # Skip over white space
+ while (line != maxLine and isempty(file[line])):
+ line = line + 1
+ return line
+
+def lookupPage(pageMap, name):
+ """Return (creating if needed) the pageInfo entry in pageMap for name"""
+ if name not in pageMap:
+ pi = pageInfo()
+ pi.name = name
+ pageMap[name] = pi
+ else:
+ pi = pageMap[name]
+ return pi
+
+def loadFile(filename):
+ """Load a file into a list of strings. Return the list or None on failure"""
+ try:
+ fp = open(filename, 'r', encoding='utf-8')
+ except:
+ logWarn('Cannot open file', filename, ':', sys.exc_info()[0])
+ return None
+
+ file = fp.readlines()
+ fp.close()
+
+ return file
+
+def clampToBlock(line, minline, maxline):
+ """Clamp a line number to be in the range [minline,maxline].
+
+ If the line number is None, just return it.
+ If minline is None, don't clamp to that value."""
+ if line is None:
+ return line
+ if minline and line < minline:
+ return minline
+ if line > maxline:
+ return maxline
+
+ return line
+
+def fixupRefs(pageMap, specFile, file):
+ """Fill in missing fields in pageInfo structures, to the extent they can be
+ inferred.
+
+ - pageMap - dictionary of pageInfo structures
+ - specFile - filename
+ - file - list of strings making up the file, indexed by pageInfo"""
+ # All potential ref pages are now in pageMap. Process them to
+ # identify actual page start/end/description boundaries, if
+ # not already determined from the text.
+ for name in sorted(pageMap.keys()):
+ pi = pageMap[name]
+
+ # # If nothing is found but an include line with no begin, validity,
+ # # or end, this is not intended as a ref page (yet). Set the begin
+ # # line to the include line, so autogeneration can at least
+ # # pull the include out, but mark it not to be extracted.
+ # # Examples include the host sync table includes in
+ # # chapters/fundamentals.txt and the table of Vk*Flag types in
+ # # appendices/boilerplate.txt.
+ # if pi.begin is None and pi.validity is None and pi.end is None:
+ # pi.begin = pi.include
+ # pi.extractPage = False
+ # pi.Warning = 'No begin, validity, or end lines identified'
+ # continue
+
+ # Using open block delimiters, ref pages must *always* have a
+ # defined begin and end. If either is undefined, that's fatal.
+ if pi.begin is None:
+ pi.extractPage = False
+ pi.Warning = 'Can\'t identify begin of ref page open block'
+ continue
+
+ if pi.end is None:
+ pi.extractPage = False
+ pi.Warning = 'Can\'t identify end of ref page open block'
+ continue
+
+ # If there's no description of the page, infer one from the type
+ if pi.desc is None:
+ if pi.type is not None:
+ # pi.desc = pi.type[0:len(pi.type)-1] + ' (no short description available)'
+ pi.Warning = 'No short description available; could infer from the type and name'
+ else:
+ pi.extractPage = False
+ pi.Warning = 'No short description available, cannot infer from the type'
+ continue
+
+ # Try to determine where the parameter and body sections of the page
+ # begin. funcpointer, proto, and struct pages infer the location of
+ # the parameter and body sections. Other pages infer the location of
+ # the body, but have no parameter sections.
+ #
+ #@ Probably some other types infer this as well - refer to list of
+ #@ all page types in genRef.py:emitPage()
+ if pi.include is not None:
+ if pi.type in ['funcpointers', 'protos', 'structs']:
+ pi.param = nextPara(file, pi.include)
+ if pi.body is None:
+ pi.body = nextPara(file, pi.param)
+ else:
+ if pi.body is None:
+ pi.body = nextPara(file, pi.include)
+ else:
+ pi.Warning = 'Page does not have an API definition include::'
+
+ # It's possible for the inferred param and body lines to run past
+ # the end of block, if, for example, there is no parameter section.
+ pi.param = clampToBlock(pi.param, pi.include, pi.end)
+ pi.body = clampToBlock(pi.body, pi.param, pi.end)
+
+ # We can get to this point with .include, .param, and .validity
+ # all being None, indicating those sections weren't found.
+
+ logDiag('fixupRefs: after processing,', pi.name, 'looks like:')
+ printPageInfo(pi, file)
+
+ # Now that all the valid pages have been found, try to make some
+ # inferences about invalid pages.
+ #
+ # If a reference without a .end is entirely inside a valid reference,
+ # then it's intentionally embedded - may want to create an indirect
+ # page that links into the embedding page. This is done by a very
+ # inefficient double loop, but the loop depth is small.
+ for name in sorted(pageMap.keys()):
+ pi = pageMap[name]
+
+ if pi.end is None:
+ for embedName in sorted(pageMap.keys()):
+ logDiag('fixupRefs: comparing', pi.name, 'to', embedName)
+ embed = pageMap[embedName]
+ # Don't check embeddings which are themselves invalid
+ if not embed.extractPage:
+ logDiag('Skipping check for embedding in:', embed.name)
+ continue
+ if embed.begin is None or embed.end is None:
+ logDiag('fixupRefs:', name + ':',
+ 'can\'t compare to unanchored ref:', embed.name,
+ 'in', specFile, 'at line', pi.include )
+ printPageInfo(pi, file)
+ printPageInfo(embed, file)
+ # If an embed is found, change the error to a warning
+ elif (pi.include is not None and pi.include >= embed.begin and
+ pi.include <= embed.end):
+ logDiag('fixupRefs: Found embed for:', name,
+ 'inside:', embedName,
+ 'in', specFile, 'at line', pi.include )
+ pi.embed = embed.name
+ pi.Warning = 'Embedded in definition for ' + embed.name
+ break
+ else:
+ logDiag('fixupRefs: No embed match for:', name,
+ 'inside:', embedName, 'in', specFile,
+ 'at line', pi.include)
+
+
+# Patterns used to recognize interesting lines in an asciidoc source file.
+# These patterns are only compiled once.
+INCSVAR_DEF = re.compile(r':INCS-VAR: (?P<value>.*)')
+endifPat = re.compile(r'^endif::(?P<condition>[\w_+,]+)\[\]')
+beginPat = re.compile(r'^\[open,(?P<attribs>refpage=.*)\]')
+# attribute key/value pairs of an open block
+attribStr = r"([a-z]+)='([^'\\]*(?:\\.[^'\\]*)*)'"
+attribPat = re.compile(attribStr)
+bodyPat = re.compile(r'^// *refBody')
+errorPat = re.compile(r'^// *refError')
+
+# This regex transplanted from check_spec_links
+# It looks for either OpenXR or Vulkan generated file conventions, and for
+# the api/validity include (generated_type), protos/struct/etc path
+# (category), and API name (entity_name). It could be put into the API
+# conventions object.
+INCLUDE = re.compile(
+ r'include::(?P<directory_traverse>((../){1,4}|\{INCS-VAR\}/|\{generated\}/)(generated/)?)(?P<generated_type>[\w]+)/(?P<category>\w+)/(?P<entity_name>[^./]+).txt[\[][\]]')
+
+
+def findRefs(file, filename):
+ """Identify reference pages in a list of strings, returning a dictionary of
+ pageInfo entries for each one found, or None on failure."""
+ setLogSourcefile(filename)
+ setLogProcname('findRefs')
+
+ # To reliably detect the open blocks around reference pages, we must
+ # first detect the '[open,refpage=...]' markup delimiting the block;
+ # skip past the '--' block delimiter on the next line; and identify the
+ # '--' block delimiter closing the page.
+ # This can't be done solely with pattern matching, and requires state to
+ # track 'inside/outside block'.
+ # When looking for open blocks, possible states are:
+ # 'outside' - outside a block
+ # 'start' - have found the '[open...]' line
+ # 'inside' - have found the following '--' line
+ openBlockState = 'outside'
+
+ # Dictionary of interesting line numbers and strings related to an API
+ # name
+ pageMap = {}
+
+ numLines = len(file)
+ line = 0
+
+ # Track the pageInfo object corresponding to the current open block
+ pi = None
+ incsvar = None
+
+ while (line < numLines):
+ setLogLine(line)
+
+ # Look for a file-wide definition
+ matches = INCSVAR_DEF.match(file[line])
+ if matches:
+ incsvar = matches.group('value')
+ logDiag('Matched INCS-VAR definition:', incsvar)
+
+ line = line + 1
+ continue
+
+ # Perform INCS-VAR substitution immediately.
+ if incsvar and '{INCS-VAR}' in file[line]:
+ newLine = file[line].replace('{INCS-VAR}', incsvar)
+ logDiag('PERFORMING SUBSTITUTION', file[line], '->', newLine)
+ file[line] = newLine
+
+ # Only one of the patterns can possibly match. Add it to
+ # the dictionary for that name.
+
+ # [open,refpage=...] starting a refpage block
+ matches = beginPat.search(file[line])
+ if matches is not None:
+ logDiag('Matched open block pattern')
+ attribs = matches.group('attribs')
+
+ # If the previous open block wasn't closed, raise an error
+ if openBlockState != 'outside':
+ logErr('Nested open block starting at line', line, 'of',
+ filename)
+
+ openBlockState = 'start'
+
+ # Parse the block attributes
+ matches = attribPat.findall(attribs)
+
+ # Extract each attribute
+ name = None
+ desc = None
+ refpage_type = None
+ spec_type = None
+ anchor = None
+ alias = None
+ xrefs = None
+
+ for (key,value) in matches:
+ logDiag('got attribute', key, '=', value)
+ if key == 'refpage':
+ name = value
+ elif key == 'desc':
+ desc = unescapeQuotes(value)
+ elif key == 'type':
+ refpage_type = value
+ elif key == 'spec':
+ spec_type = value
+ elif key == 'anchor':
+ anchor = value
+ elif key == 'alias':
+ alias = value
+ elif key == 'xrefs':
+ xrefs = value
+ else:
+ logWarn('unknown open block attribute:', key)
+
+ if name is None or desc is None or refpage_type is None:
+ logWarn('missing one or more required open block attributes:'
+ 'refpage, desc, or type')
+ # Leave pi is None so open block delimiters are ignored
+ else:
+ pi = lookupPage(pageMap, name)
+ pi.desc = desc
+ # Must match later type definitions in interface/validity includes
+ pi.type = refpage_type
+ pi.spec = spec_type
+ pi.anchor = anchor
+ if alias:
+ pi.alias = alias
+ if xrefs:
+ pi.refs = xrefs
+ logDiag('open block for', name, 'added DESC =', desc,
+ 'TYPE =', refpage_type, 'ALIAS =', alias,
+ 'XREFS =', xrefs, 'SPEC =', spec_type,
+ 'ANCHOR =', anchor)
+
+ line = line + 1
+ continue
+
+ # '--' starting or ending and open block
+ if file[line].rstrip() == '--':
+ if openBlockState == 'outside':
+ # Only refpage open blocks should use -- delimiters
+ logWarn('Unexpected double-dash block delimiters')
+ elif openBlockState == 'start':
+ # -- delimiter following [open,refpage=...]
+ openBlockState = 'inside'
+
+ if pi is None:
+ logWarn('no pageInfo available for opening -- delimiter')
+ else:
+ pi.begin = line + 1
+ logDiag('opening -- delimiter: added BEGIN =', pi.begin)
+ elif openBlockState == 'inside':
+ # -- delimiter ending an open block
+ if pi is None:
+ logWarn('no pageInfo available for closing -- delimiter')
+ else:
+ pi.end = line - 1
+ logDiag('closing -- delimiter: added END =', pi.end)
+
+ openBlockState = 'outside'
+ pi = None
+ else:
+ logWarn('unknown openBlockState:', openBlockState)
+
+ line = line + 1
+ continue
+
+ matches = INCLUDE.search(file[line])
+ if matches is not None:
+ # Something got included, not sure what yet.
+ gen_type = matches.group('generated_type')
+ refpage_type = matches.group('category')
+ name = matches.group('entity_name')
+
+ # This will never match in OpenCL
+ if gen_type == 'validity':
+ logDiag('Matched validity pattern')
+ if pi is not None:
+ if pi.type and refpage_type != pi.type:
+ logWarn('ERROR: pageMap[' + name + '] type:',
+ pi.type, 'does not match type:', refpage_type)
+ pi.type = refpage_type
+ pi.validity = line
+ logDiag('added TYPE =', pi.type, 'VALIDITY =', pi.validity)
+ else:
+ logWarn('validity include:: line NOT inside block')
+
+ line = line + 1
+ continue
+
+ if gen_type == 'api':
+ logDiag('Matched include pattern')
+ if pi is not None:
+ if pi.include is not None:
+ logDiag('found multiple includes for this block')
+ if pi.type and refpage_type != pi.type:
+ logWarn('ERROR: pageMap[' + name + '] type:',
+ pi.type, 'does not match type:', refpage_type)
+ pi.type = refpage_type
+ pi.include = line
+ logDiag('added TYPE =', pi.type, 'INCLUDE =', pi.include)
+ else:
+ logWarn('interface include:: line NOT inside block')
+
+ line = line + 1
+ continue
+
+ logDiag('ignoring unrecognized include line ', matches.group())
+
+ # Vulkan 1.1 markup allows the last API include construct to be
+ # followed by an asciidoctor endif:: construct (and also preceded,
+ # at some distance).
+ # This looks for endif:: immediately following an include:: line
+ # and, if found, moves the include boundary to this line.
+ matches = endifPat.search(file[line])
+ if matches is not None and pi is not None:
+ if pi.include == line - 1:
+ logDiag('Matched endif pattern following include; moving include')
+ pi.include = line
+ else:
+ logDiag('Matched endif pattern (not following include)')
+
+ line = line + 1
+ continue
+
+ matches = bodyPat.search(file[line])
+ if matches is not None:
+ logDiag('Matched // refBody pattern')
+ if pi is not None:
+ pi.body = line
+ logDiag('added BODY =', pi.body)
+ else:
+ logWarn('// refBody line NOT inside block')
+
+ line = line + 1
+ continue
+
+ # OpenCL spec uses // refError to tag "validity" (Errors) language,
+ # instead of /validity/ includes.
+ matches = errorPat.search(file[line])
+ if matches is not None:
+ logDiag('Matched // refError pattern')
+ if pi is not None:
+ pi.validity = line
+ logDiag('added VALIDITY (refError) =', pi.validity)
+ else:
+ logWarn('// refError line NOT inside block')
+
+ line = line + 1
+ continue
+
+ line = line + 1
+ continue
+
+ if pi is not None:
+ logErr('Unclosed open block at EOF!')
+
+ setLogSourcefile(None)
+ setLogProcname(None)
+ setLogLine(None)
+
+ return pageMap
+
+
+def getBranch():
+ """Determine current git branch
+
+ Returns (branch name, ''), or (None, stderr output) if the branch name
+ can't be determined"""
+
+ command = [ 'git', 'symbolic-ref', '--short', 'HEAD' ]
+ results = subprocess.run(command,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+
+ # git command failed
+ if len(results.stderr) > 0:
+ return (None, results.stderr)
+
+ # Remove newline from output and convert to a string
+ branch = results.stdout.rstrip().decode()
+ if len(branch) > 0:
+ # Strip trailing newline
+ branch = results.stdout.decode()[0:-1]
+
+ return (branch, '')