• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/python3
2#
3# Copyright (c) 2016-2018 The Khronos Group Inc.
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#     http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17# Utility functions for automatic ref page generation
18
19import io,os,re,sys
20
21# global errFile, warnFile, diagFile
22
23errFile = sys.stderr
24warnFile = sys.stdout
25diagFile = None
26logSourcefile = None
27logProcname = None
28logLine = None
29
30# Remove \' escape sequences in a string (refpage description)
31def unescapeQuotes(str):
32    return str.replace('\\\'', '\'')
33
34def write(*args, **kwargs ):
35    file = kwargs.pop('file',sys.stdout)
36    end = kwargs.pop('end','\n')
37    file.write(' '.join([str(arg) for arg in args]))
38    file.write(end)
39
40# Metadata which may be printed (if not None) for diagnostic messages
41def setLogSourcefile(filename):
42    global logSourcefile
43    logSourcefile = filename
44
45def setLogProcname(procname):
46    global logProcname
47    logProcname = procname
48
49def setLogLine(line):
50    global logLine
51    logLine = line
52
53# Generate prefix for a diagnostic line using metadata and severity
54def logHeader(severity):
55    global logSourcefile, logProcname, logLine
56
57    msg = severity + ': '
58    if logProcname:
59        msg = msg + ' in ' + logProcname
60    if logSourcefile:
61        msg = msg + ' for ' + logSourcefile
62    if logLine:
63        msg = msg + ' line ' + str(logLine)
64    return msg + ' '
65
66# Set the file handle to log either or both warnings and diagnostics to.
67# setDiag and setWarn are True if the corresponding handle is to be set.
68# filename is None for no logging, '-' for stdout, or a pathname.
69def setLogFile(setDiag, setWarn, filename):
70    global diagFile, warnFile
71
72    if filename == None:
73        return
74    elif filename == '-':
75        fp = sys.stdout
76    else:
77        fp = open(filename, 'w', encoding='utf-8')
78
79    if setDiag:
80        diagFile = fp
81    if setWarn:
82        warnFile = fp
83
84def logDiag(*args, **kwargs):
85    file = kwargs.pop('file', diagFile)
86    end = kwargs.pop('end','\n')
87    if file != None:
88        file.write(logHeader('DIAG') + ' '.join([str(arg) for arg in args]))
89        file.write(end)
90
91def logWarn(*args, **kwargs):
92    file = kwargs.pop('file', warnFile)
93    end = kwargs.pop('end','\n')
94    if file != None:
95        file.write(logHeader('WARN') + ' '.join([str(arg) for arg in args]))
96        file.write(end)
97
98def logErr(*args, **kwargs):
99    file = kwargs.pop('file', errFile)
100    end = kwargs.pop('end','\n')
101
102    strfile = io.StringIO()
103    strfile.write(logHeader('ERROR') + ' '.join([str(arg) for arg in args]))
104    strfile.write(end)
105
106    if file != None:
107        file.write(strfile.getvalue())
108    raise UserWarning(strfile.getvalue())
109
110# Return True if s is nothing but white space, False otherwise
111def isempty(s):
112    return len(''.join(s.split())) == 0
113
114# pageInfo - information about a ref page relative to the file it's
115# extracted from.
116#
117#   extractPage - True if page should be extracted
118#   Warning - string warning if page is suboptimal or can't be generated
119#   embed - False or the name of the ref page this include is imbedded within
120#
121#   type - 'structs', 'protos', 'funcpointers', 'flags', 'enums'
122#   name - struct/proto/enumerant/etc. name
123#   desc - short description of ref page
124#   begin - index of first line of the page (heuristic or // refBegin)
125#   include - index of include:: line defining the page
126#   param - index of first line of parameter/member definitions
127#   body - index of first line of body text
128#   validity - index of validity include
129#   end - index of last line of the page (heuristic validity include, or // refEnd)
130#   refs - cross-references on // refEnd line, if supplied
131class pageInfo:
132    def __init__(self):
133        self.extractPage = True
134        self.Warning  = None
135        self.embed    = False
136
137        self.type     = None
138        self.name     = None
139        self.desc     = None
140        self.begin    = None
141        self.include  = None
142        self.param    = None
143        self.body     = None
144        self.validity = None
145        self.end      = None
146        self.refs     = ''
147
148# Print a single field of a pageInfo struct, possibly None
149#   desc - string description of field
150#   line - field value or None
151#   file - indexed by line
152def printPageInfoField(desc, line, file):
153    if line != None:
154        logDiag(desc + ':', line + 1, '\t-> ', file[line], end='')
155    else:
156        logDiag(desc + ':', line)
157
158# Print out fields of a pageInfo struct
159#   pi - pageInfo
160#   file - indexed by pageInfo
161def printPageInfo(pi, file):
162    logDiag('TYPE:   ', pi.type)
163    logDiag('NAME:   ', pi.name)
164    logDiag('WARNING:', pi.Warning)
165    logDiag('EXTRACT:', pi.extractPage)
166    logDiag('EMBED:  ', pi.embed)
167    logDiag('DESC:   ', pi.desc)
168    printPageInfoField('BEGIN   ', pi.begin,    file)
169    printPageInfoField('INCLUDE ', pi.include,  file)
170    printPageInfoField('PARAM   ', pi.param,    file)
171    printPageInfoField('BODY    ', pi.body,     file)
172    printPageInfoField('VALIDITY', pi.validity, file)
173    printPageInfoField('END     ', pi.end,      file)
174    logDiag('REFS: "' + pi.refs + '"')
175
176# Go back one paragraph from the specified line and return the line number
177# of the first line of that paragraph.
178#
179# Paragraphs are delimited by blank lines. It is assumed that the
180# current line is the first line of a paragraph.
181#   file is an array of strings
182#   line is the starting point (zero-based)
183def prevPara(file, line):
184    # Skip over current paragraph
185    while (line >= 0 and not isempty(file[line])):
186        line = line - 1
187    # Skip over white space
188    while (line >= 0 and isempty(file[line])):
189        line = line - 1
190    # Skip to first line of previous paragraph
191    while (line >= 1 and not isempty(file[line-1])):
192        line = line - 1
193    return line
194
195# Go forward one paragraph from the specified line and return the line
196# number of the first line of that paragraph.
197#
198# Paragraphs are delimited by blank lines. It is assumed that the
199# current line is standalone (which is bogus)
200#   file is an array of strings
201#   line is the starting point (zero-based)
202def nextPara(file, line):
203    maxLine = len(file) - 1
204    # Skip over current paragraph
205    while (line != maxLine and not isempty(file[line])):
206        line = line + 1
207    # Skip over white space
208    while (line != maxLine and isempty(file[line])):
209        line = line + 1
210    return line
211
212# Return (creating if needed) the pageInfo entry in pageMap for name
213def lookupPage(pageMap, name):
214    if not name in pageMap.keys():
215        pi = pageInfo()
216        pi.name = name
217        pageMap[name] = pi
218    else:
219        pi = pageMap[name]
220    return pi
221
222# Load a file into a list of strings. Return the list or None on failure
223def loadFile(filename):
224    try:
225        fp = open(filename, 'r', encoding='utf-8')
226    except:
227        logWarn('Cannot open file', filename, ':', sys.exc_info()[0])
228        return None
229
230    file = fp.readlines()
231    fp.close()
232
233    return file
234
235# Clamp a line number to be in the range [minline,maxline].
236# If the line number is None, just return it.
237# If minline is None, don't clamp to that value.
238def clampToBlock(line, minline, maxline):
239    if line == None:
240        return line
241    elif minline and line < minline:
242        return minline
243    elif line > maxline:
244        return maxline
245    else:
246        return line
247
248# Fill in missing fields in pageInfo structures, to the extent they can be
249# inferred.
250#   pageMap - dictionary of pageInfo structures
251#   specFile - filename
252#   file - list of strings making up the file, indexed by pageInfo
253def fixupRefs(pageMap, specFile, file):
254    # All potential ref pages are now in pageMap. Process them to
255    # identify actual page start/end/description boundaries, if
256    # not already determined from the text.
257    for name in sorted(pageMap.keys()):
258        pi = pageMap[name]
259
260        # # If nothing is found but an include line with no begin, validity,
261        # # or end, this is not intended as a ref page (yet). Set the begin
262        # # line to the include line, so autogeneration can at least
263        # # pull the include out, but mark it not to be extracted.
264        # # Examples include the host sync table includes in
265        # # chapters/fundamentals.txt and the table of Vk*Flag types in
266        # # appendices/boilerplate.txt.
267        # if pi.begin == None and pi.validity == None and pi.end == None:
268        #     pi.begin = pi.include
269        #     pi.extractPage = False
270        #     pi.Warning = 'No begin, validity, or end lines identified'
271        #     continue
272
273        # Using open block delimiters, ref pages must *always* have a
274        # defined begin and end. If either is undefined, that's fatal.
275        if pi.begin == None:
276            pi.extractPage = False
277            pi.Warning = 'Can\'t identify begin of ref page open block'
278            continue
279
280        if pi.end == None:
281            pi.extractPage = False
282            pi.Warning = 'Can\'t identify end of ref page open block'
283            continue
284
285        # If there's no description of the page, infer one from the type
286        if pi.desc == None:
287            if pi.type != None:
288                # pi.desc = pi.type[0:len(pi.type)-1] + ' (no short description available)'
289                pi.Warning = 'No short description available; could infer from the type and name'
290                True
291            else:
292                pi.extractPage = False
293                pi.Warning = 'No short description available, cannot infer from the type'
294                continue
295
296        # Try to determine where the parameter and body sections of the page
297        # begin. funcpointer, proto, and struct pages infer the location of
298        # the parameter and body sections. Other pages infer the location of
299        # the body, but have no parameter sections.
300        if pi.include != None:
301            if pi.type in ['funcpointers', 'protos', 'structs']:
302                pi.param = nextPara(file, pi.include)
303                if pi.body == None:
304                    pi.body = nextPara(file, pi.param)
305            else:
306                if pi.body == None:
307                    pi.body = nextPara(file, pi.include)
308        else:
309            pi.Warning = 'Page does not have an API definition include::'
310
311        # It's possible for the inferred param and body lines to run past
312        # the end of block, if, for example, there is no parameter section.
313        pi.param = clampToBlock(pi.param, pi.include, pi.end)
314        pi.body = clampToBlock(pi.body, pi.param, pi.end)
315
316        # We can get to this point with .include, .param, and .validity
317        # all being None, indicating those sections weren't found.
318
319        logDiag('fixupRefs: after processing,', pi.name, 'looks like:')
320        printPageInfo(pi, file)
321
322    # Now that all the valid pages have been found, try to make some
323    # inferences about invalid pages.
324    #
325    # If a reference without a .end is entirely inside a valid reference,
326    # then it's intentionally embedded - may want to create an indirect
327    # page that links into the embedding page. This is done by a very
328    # inefficient double loop, but the loop depth is small.
329    for name in sorted(pageMap.keys()):
330        pi = pageMap[name]
331
332        if pi.end == None:
333            for embedName in sorted(pageMap.keys()):
334                logDiag('fixupRefs: comparing', pi.name, 'to', embedName)
335                embed = pageMap[embedName]
336                # Don't check embeddings which are themselves invalid
337                if not embed.extractPage:
338                    logDiag('Skipping check for embedding in:', embed.name)
339                    continue
340                if embed.begin == None or embed.end == None:
341                    logDiag('fixupRefs:', name + ':',
342                            'can\'t compare to unanchored ref:', embed.name,
343                            'in', specFile, 'at line', pi.include )
344                    printPageInfo(pi, file)
345                    printPageInfo(embed, file)
346                # If an embed is found, change the error to a warning
347                elif (pi.include != None and pi.include >= embed.begin and
348                      pi.include <= embed.end):
349                    logDiag('fixupRefs: Found embed for:', name,
350                            'inside:', embedName,
351                            'in', specFile, 'at line', pi.include )
352                    pi.embed = embed.name
353                    pi.Warning = 'Embedded in definition for ' + embed.name
354                    break
355                else:
356                    logDiag('fixupRefs: No embed match for:', name,
357                            'inside:', embedName, 'in', specFile,
358                            'at line', pi.include)
359
360
361# Patterns used to recognize interesting lines in an asciidoc source file.
362# These patterns are only compiled once.
363includePat = re.compile('^include::(\.\./)+api/+(?P<type>\w+)/(?P<name>\w+).txt\[\]')
364endifPat   = re.compile('^endif::(?P<condition>[\w_+,]+)\[\]')
365validPat   = re.compile('^include::(\.\./)+validity/(?P<type>\w+)/(?P<name>\w+).txt\[\]')
366beginPat   = re.compile('^\[open,(?P<attribs>refpage=.*)\]')
367# attribute key/value pairs of an open block
368attribStr  = "([a-z]+)='([^'\\\\]*(?:\\\\.[^'\\\\]*)*)'"
369attribPat  = re.compile(attribStr)
370bodyPat    = re.compile('^// *refBody')
371
372# Identify reference pages in a list of strings, returning a dictionary of
373# pageInfo entries for each one found, or None on failure.
374def findRefs(file, filename):
375    setLogSourcefile(filename)
376    setLogProcname('findRefs')
377
378    # To reliably detect the open blocks around reference pages, we must
379    # first detect the '[open,refpage=...]' markup delimiting the block;
380    # skip past the '--' block delimiter on the next line; and identify the
381    # '--' block delimiter closing the page.
382    # This can't be done solely with pattern matching, and requires state to
383    # track 'inside/outside block'.
384    # When looking for open blocks, possible states are:
385    #   'outside' - outside a block
386    #   'start' - have found the '[open...]' line
387    #   'inside' - have found the following '--' line
388    openBlockState = 'outside'
389
390    # This is a dictionary of interesting line numbers and strings related
391    # to a Vulkan API name
392    pageMap = {}
393
394    numLines = len(file)
395    line = 0
396
397    # Track the pageInfo object corresponding to the current open block
398    pi = None
399
400    while (line < numLines):
401        setLogLine(line)
402
403        # Only one of the patterns can possibly match. Add it to
404        # the dictionary for that name.
405
406        # [open,refpage=...] starting a refpage block
407        matches = beginPat.search(file[line])
408        if matches != None:
409            logDiag('Matched open block pattern')
410            attribs = matches.group('attribs')
411
412            openBlockState = 'start'
413
414            # Parse the block attributes
415            matches = attribPat.findall(attribs)
416
417            # Extract each attribute
418            name = None
419            desc = None
420            type = None
421            xrefs = None
422
423            for (key,value) in matches:
424                logDiag('got attribute', key, '=', value)
425                if key == 'refpage':
426                    name = value
427                elif key == 'desc':
428                    desc = unescapeQuotes(value)
429                elif key == 'type':
430                    type = value
431                elif key == 'xrefs':
432                    xrefs = value
433                else:
434                    logWarn('unknown open block attribute:', key)
435
436            if name == None or desc == None or type == None:
437                logWarn('missing one or more required open block attributes:'
438                        'refpage, desc, or type')
439                # Leave pi == None so open block delimiters are ignored
440            else:
441                pi = lookupPage(pageMap, name)
442                pi.desc = desc
443                # Must match later type definitions in interface/validity includes
444                pi.type = type
445                if xrefs:
446                    pi.refs = xrefs
447                logDiag('open block for', name, 'added DESC =', desc,
448                        'TYPE =', type, 'XREFS =', xrefs)
449
450            line = line + 1
451            continue
452
453        # '--' starting or ending and open block
454        if file[line].rstrip() == '--':
455            if openBlockState == 'outside':
456                # Only refpage open blocks should use -- delimiters
457                logWarn('Unexpected double-dash block delimiters')
458            elif openBlockState == 'start':
459                # -- delimiter following [open,refpage=...]
460                openBlockState = 'inside'
461
462                if pi == None:
463                    logWarn('no pageInfo available for opening -- delimiter')
464                else:
465                    pi.begin = line + 1
466                    logDiag('opening -- delimiter: added BEGIN =', pi.begin)
467            elif openBlockState == 'inside':
468                # -- delimiter ending an open block
469                if pi == None:
470                    logWarn('no pageInfo available for closing -- delimiter')
471                else:
472                    pi.end = line - 1
473                    logDiag('closing -- delimiter: added END =', pi.end)
474
475                openBlockState = 'outside'
476                pi = None
477            else:
478                logWarn('unknown openBlockState:', openBlockState)
479
480            line = line + 1
481            continue
482
483        matches = validPat.search(file[line])
484        if matches != None:
485            logDiag('Matched validity pattern')
486            type = matches.group('type')
487            name = matches.group('name')
488
489            if pi != None:
490                if pi.type and type != pi.type:
491                    logWarn('ERROR: pageMap[' + name + '] type:',
492                            pi.type, 'does not match type:', type)
493                pi.type = type
494                pi.validity = line
495                logDiag('added TYPE =', pi.type, 'VALIDITY =', pi.validity)
496            else:
497                logWarn('validity include:: line NOT inside block')
498
499            line = line + 1
500            continue
501
502        matches = includePat.search(file[line])
503        if matches != None:
504            logDiag('Matched include pattern')
505            type = matches.group('type')
506            name = matches.group('name')
507            if pi != None:
508                if pi.include != None:
509                    logDiag('found multiple includes for this block')
510                if pi.type and type != pi.type:
511                    logWarn('ERROR: pageMap[' + name + '] type:',
512                            pi.type, 'does not match type:', type)
513                pi.type = type
514                pi.include = line
515                logDiag('added TYPE =', pi.type, 'INCLUDE =', pi.include)
516            else:
517                logWarn('interface include:: line NOT inside block')
518
519            line = line + 1
520            continue
521
522        # Vulkan 1.1 markup allows the last API include construct to be
523        # followed by an asciidoctor endif:: construct (and also preceded,
524        # at some distance).
525        # This looks for endif:: immediately following an include:: line
526        # and, if found, moves the include boundary to this line.
527        matches = endifPat.search(file[line])
528        if matches != None and pi != None:
529            if pi.include == line - 1:
530                logDiag('Matched endif pattern following include; moving include')
531                pi.include = line
532            else:
533                logDiag('Matched endif pattern (not following include)')
534
535            line = line + 1
536            continue
537
538        matches = bodyPat.search(file[line])
539        if matches != None:
540            logDiag('Matched // refBody pattern')
541            if pi != None:
542                pi.body = line
543                logDiag('added BODY =', pi.body)
544            else:
545                logWarn('// refBody line NOT inside block')
546
547            line = line + 1
548            continue
549
550        line = line + 1
551        continue
552
553    if pi != None:
554        logErr('Unclosed open block at EOF!')
555
556    setLogSourcefile(None)
557    setLogProcname(None)
558    setLogLine(None)
559
560    return pageMap
561
562