# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 """Trace annotated with debugging information.""" import html import logging import os import re import voluptuous import voluptuous.humanize from cbmc_viewer import markup_link from cbmc_viewer import srcloct from cbmc_viewer import templates from cbmc_viewer import util # Name of directory holding annotated traces TRACES = "traces" ################################################################ # Data passed to jinja to generate annotated traces from trace.jinja.html VALID_STEP = voluptuous.Schema({ 'kind': voluptuous.Any( 'function-call', 'function-return', 'variable-assignment', 'parameter-assignment', 'assumption', 'failure' ), 'num': int, 'srcloc': str, 'code': voluptuous.Any(str, None), 'cbmc': str }, required=True) VALID_TRACE = voluptuous.Schema({ 'prop_name': str, 'prop_desc': str, 'prop_srcloc': str, 'steps': [VALID_STEP], 'outdir': str }, required=True) ################################################################ # Source code fragments used to annotate traces. class CodeSnippet: """Source code fragments.""" def __init__(self, root): self.root = root # source root self.source = {} # cache mapping file name -> lines of source code def lookup(self, path, line): """A line of source code.""" if line <= 0: # line numbers are 1-based logging.info("CodeSnippet lookup: line number not positive: %s", line) return None line -= 1 # list indices are 0-based try: if path not in self.source: with open(os.path.join(self.root, path), encoding='utf-8') as code: self.source[path] = code.read().splitlines() except FileNotFoundError as error: if srcloct.is_builtin(path): # , etc. return None raise UserWarning(f"CodeSnippet lookup: file not found: {path}") from error # return the whole statement which may be broken over several lines snippet = ' '.join(self.source[path][line:line+5]) snippet = re.sub(r'\s+', ' ', snippet).strip() idx = snippet.find(';') # end of statement if idx >= 0: return html.escape(snippet[:idx+1]) idx = snippet.find('}') # end of block if idx >= 0: return html.escape(snippet[:idx+1]) return html.escape(snippet) # statement extends over more that 5 lines def lookup_srcloc(self, srcloc): """A line of source code (at a source location).""" return self.lookup(srcloc['file'], srcloc['line']) ################################################################ class Trace: """Trace annotated with debugging information.""" def __init__(self, name, trace, symbols, properties, loops, snippets, outdir='.'): self.prop_name = name self.prop_desc = properties.get_description(name) self.prop_srcloc = format_srcloc(properties.get_srcloc(name) or loops.lookup_assertion(name), symbols) self.steps = [{ 'kind': step['kind'], 'num': num+1, # convert 0-based index to 1-based line number 'srcloc': format_srcloc(step['location'], symbols), 'code': snippets.lookup_srcloc(step['location']), 'cbmc': format_step(step) } for num, step in enumerate(trace)] self.outdir = outdir self.validate() def __str__(self): """Render annotated trace as html.""" return templates.render_trace(self.prop_name, self.prop_desc, self.prop_srcloc, self.steps) def validate(self): """Validate members of an annotated trace object.""" return voluptuous.humanize.validate_with_humanized_errors( self.__dict__, VALID_TRACE ) def dump(self, filename=None, outdir=None): """Write annotated trace to a file rendered as html.""" util.dump(self, filename or self.prop_name + ".html", outdir or self.outdir) ################################################################ # Format a source location def format_srcloc(srcloc, symbols): """Format a source location for a trace step.""" if srcloc is None: return 'Function none, File none, Line none' fyle, func, line = srcloc['file'], srcloc['function'], srcloc['line'] func_srcloc = symbols.lookup(func) # Warning: next line assumes trace root is subdirectory of code root from_file = os.path.join(TRACES, 'foo.html') # any name foo.html will do # pylint: disable=consider-using-f-string return 'Function {}, File {}, Line {}'.format( markup_link.link_text_to_srcloc(func, func_srcloc, from_file), markup_link.link_text_to_file(fyle, fyle, from_file), markup_link.link_text_to_line(line, fyle, line, from_file) ) ################################################################ # Format a trace step def format_step(step): """Format a trace step.""" markup = { "function-call": format_function_call, "function-return": format_function_return, "variable-assignment": format_variable_assignment, "parameter-assignment": format_parameter_assignment, "assumption": format_assumption, "failure": format_failure }[step['kind']] return markup(step) def format_function_call(step): """Format a function call.""" name, srcloc = step['detail']['name'], step['detail']['location'] line = f'-> {markup_link.link_text_to_srcloc(name, srcloc, "./trace/trace.html")}' return line def format_function_return(step): """Format a function return.""" name, srcloc = step['detail']['name'], step['detail']['location'] line = f'<- {markup_link.link_text_to_srcloc(name, srcloc, "./trace/trace.html")}' return line def format_variable_assignment(step): """Format an assignment statement.""" asn = step['detail'] lhs, rhs, binary = asn['lhs'], asn['rhs-value'], asn['rhs-binary'] binary = f'({binary})' if binary else '' return f'{lhs} = {rhs} {binary}' def format_parameter_assignment(step): """Format an assignment of an actual to formal function argument.""" asn = step['detail'] lhs, rhs, binary = asn['lhs'], asn['rhs-value'], asn['rhs-binary'] binary = f'({binary})' if binary else '' return f'{lhs} = {rhs} {binary}' def format_assumption(step): """Format a proof assumption.""" pred = step['detail']['predicate'] return f'assumption: {pred}' def format_failure(step): """Format a proof failure.""" prop = step['detail']['property'] or "Unnamed" reason = step['detail']['reason'] or "Not given" return f'failure: {prop}: {reason}' ################################################################