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