files: '''Sphinx extension that provides directives for automatically generating
  documentation for the three types of dynamically loaded modules used by SC:
  flows, foundries, and tools.
  '''
  
  from docutils import nodes
  from sphinx.util.nodes import nested_parse_with_titles
  from docutils.statemachine import ViewList
  from sphinx.util.docutils import SphinxDirective
  import docutils
  from docutils.parsers.rst import directives
  
  import copy
  import importlib
  import pkgutil
  import os
  import sys
  import subprocess
  
  from common import *
  
  from siliconcompiler import utils
  
  #############
  # Helpers
  #############
  
  # We need this in a few places, so just make it global
  SC_ROOT = os.path.abspath(f'{__file__}/../../../')
  
  def build_schema_value_table(schema, keypath_prefix=[], skip_zero_weight=False):
      '''Helper function for displaying values set in schema as a docutils table.'''
      table = [[strong('Keypath'), strong('Value')]]
      flat_cfg = flatten(schema)
      for keys, val in flat_cfg.items():
          full_keypath = list(keypath_prefix) + list(keys)
  
          if (skip_zero_weight and
              len(full_keypath) == 6 and full_keypath[0] == 'flowgraph' and full_keypath[-2] == 'weight' and
              'value' in val and val['value'] == '0'):
              continue
  
          keypath = ', '.join(full_keypath)
  
          if 'value' in val and val['value']:
              # Don't display false booleans
              if val['type'] == 'bool' and val['value'] == 'false':
                  continue
              if val['type'].startswith('['):
                  if len(val['value']) > 1:
                      val_node = build_list([code(v) for v in val['value']])
                  elif len(val['value']) > 0:
                      val_node = code(val['value'][0])
                  else:
                      val_node = para('')
              else:
                  val_node = code(val['value'])
  
              table.append([code(keypath), val_node])
  
      if len(table) > 1:
          return build_table(table)
      else:
          return None
  
  def build_config_recursive(schema, keypath_prefix=[], sec_key_prefix=[]):
      '''Helper function for displaying schema at each level as tables unde nested
      sections.
  
      For each item:
      - If it's a leaf, collect it into a table we will display at this
          level
      - Otherwise, recurse and collect sections of lower levels
      '''
      leaves = {}
      child_sections = []
      for key, val in schema.items():
          if key == 'default': continue
          if 'help' in val:
              if 'value' in val and val['value']:
                  leaves.update({key: val})
          else:
              children = build_config_recursive(val, keypath_prefix=keypath_prefix+[key], sec_key_prefix=sec_key_prefix)
              child_sections.extend(children)
  
      # If we've found leaves, create a new section where we'll display a
      # table plus all child sections.
      if len(leaves) > 0:
          keypath = ', '.join(keypath_prefix)
          section_key = '-'.join(sec_key_prefix + keypath_prefix)
          top = build_section(keypath, section_key)
          top += build_schema_value_table(leaves, keypath_prefix=keypath_prefix)
          top += child_sections
          return [top]
      else:
          # Otherwise, just pass on the child sections -- we don't want to
          # create an extra level of section hierarchy for levels of the
          # schema without leaves.
          return child_sections
  
  #############
  # Base class
  #############
  
  def flag_opt(argument):
      if argument is not None:
          raise ValueError('Flag should not have content')
      return True
  
  class DynamicGen(SphinxDirective):
      '''Base class for all three directives provided by this extension.
  
      Each child class implements a directive by overriding the display_config()
      method and setting a PATH member variable.
      '''
  
      option_spec = {'nobuiltins': flag_opt}
  
      def document_module(self, module, modname, path):
          '''Build section documenting given module and name.'''
          s = build_section_with_target(modname, f'{modname}-ref', self.state.document)
  
          if not hasattr(module, 'make_docs'):
              return None
  
          make_docs = getattr(module, 'make_docs')
  
          # raw docstrings have funky indentation (basically, each line is already
          # indented as much as the function), so we call trim() helper function
          # to clean it up
          docstr = utils.trim(make_docs.__doc__)
  
          if docstr:
              self.parse_rst(docstr, s)
  
          builtin = os.path.abspath(path).startswith(SC_ROOT)
  
          if builtin:
              relpath = path[len(SC_ROOT)+1:]
              gh_root = 'https://github.com/siliconcompiler/siliconcompiler/blob/main'
              gh_link = f'{gh_root}/{relpath}'
              filename = os.path.basename(relpath)
              p = para('Setup file: ')
              p += link(gh_link, text=filename)
              s += p
  
          chip = make_docs()
  
          extra_content = self.extra_content(chip, modname)
          if extra_content is not None:
              s += extra_content
  
          s += self.display_config(chip, modname)
  
          return s
  
      def run(self):
          '''Main entry point of directive.'''
          sections = []
  
          for module, modname in self.get_modules():
              path = module.__file__
              self.env.note_dependency(path)
              docs = self.document_module(module, modname, path)
              if docs is not None:
                  sections.append((docs, modname))
  
          if len(sections) > 0:
              # Sort sections by module name
              sections = sorted(sections, key=lambda t: t[1])
              # Strip off modname so we just return list of docutils sections
              sections, _ = zip(*sections)
  
          return list(sections)
  
      def get_modules(self):
          '''Gets dynamic modules under `self.PATH`.
  
          This function explicitly searches builtins as well as SCPATH
          directories. Although the directory for builtin tools gets added to
          SCPATH after a chip object has been initialized, we can't rely on this
          since we can't be sure that's happened yet. Therefore, we have to check
          each one explicitly.
  
          However, this could result in duplicate modules being detected once the
          SCPATH does get updated. Therefore, we check to ensure that SCPATH
          directories are not equal to the builtins directory before searching it.
  
          TODO: we want better duplicate resolution (in case the user explicitly
          declares a duplicate tool), where SCPATH takes priority.
          '''
          builtins_dir = f'{SC_ROOT}/siliconcompiler/{self.PATH}'
          if 'nobuiltins' not in self.options:
              modules = self.get_modules_in_dir(builtins_dir)
          else:
              modules = []
  
          if 'SCPATH' in os.environ:
              scpaths = os.environ['SCPATH'].split(':')
              for scpath in scpaths:
                  user_dir = f'{scpath}/{self.PATH}'
                  if not os.path.isdir(user_dir) or builtins_dir == user_dir:
                      continue
                  modules.extend(self.get_modules_in_dir(user_dir))
  
          return modules
  
      def get_modules_in_dir(self, module_dir):
          '''Routine for getting modules and their names from a certain
          directory.'''
          modules = []
          for importer, modname, _ in pkgutil.iter_modules([module_dir]):
              if modname in ('sc_floorplan'):
                  continue
              module = importer.find_module(modname).load_module(modname)
              modules.append((module, modname))
  
          return modules
  
      def parse_rst(self, content, s):
          '''Helper for parsing reStructuredText content, adding it directly to
          section `s`.'''
          rst = ViewList()
          # use fake filename 'inline' for error # reporting
          for i, line in enumerate(content.split('\n')):
              rst.append(line, 'inline', i)
          nested_parse_with_titles(self.state, rst, s)
  
      def extra_content(self, chip, modname):
          '''Adds extra content to documentation.
  
          May return a list of docutils nodes that will be added to the
          documentation in between a module's docstring and configuration table.
          Otherwise, if return value is None, don't add anything.
          '''
          return None
  
  #########################
  # Specialized extensions
  #########################
  
  class FlowGen(DynamicGen):
      PATH = 'flows'
  
      def extra_content(self, chip, modname):
          flow_path = os.path.join(self.env.app.outdir, f'_images/gen/{modname}.svg')
          #chip.write_flowgraph(flow_path, fillcolor='#1c4587', fontcolor='#f1c232', border=False)
          chip.write_flowgraph(flow_path)
          return [image(flow_path, center=True)]
  
      def display_config(self, chip, modname):
          '''Display parameters under `flowgraph, <step>`, `metric, <step>` and
          `showtool`. Parameters are grouped into sections by step, with an
          additional table for non-step items.
          '''
          section_key = '-'.join(['flows', modname, 'configuration'])
          settings = build_section('Configuration', section_key)
  
          steps = chip.getkeys('flowgraph')
          # TODO: should try to order?
  
          # Build section + table for each step (combining entires under flowgraph
          # and metric)
          for step in steps:
              section_key = '-'.join(['flows', modname, step])
              section = build_section(step, section_key)
              step_cfg = {}
              for prefix in ['flowgraph', 'metric']:
                  cfg = chip.getdict(prefix, step)
                  if cfg is None:
                      continue
                  pruned = chip._prune(cfg)
                  if prefix not in step_cfg:
                      step_cfg[prefix] = {}
                  step_cfg[prefix][step] = pruned
  
              section += build_schema_value_table(step_cfg, skip_zero_weight=True)
              settings += section
  
          # Build table for non-step items (just showtool for now)
          section_key = '-'.join(['flows', modname, 'showtool'])
          section = build_section('showtool', section_key)
          cfg = chip.getdict('showtool')
          pruned = chip._prune(cfg)
          table = build_schema_value_table(pruned)
          if table is not None:
              section += table
              settings += section
  
          return settings
  
  class PDKGen(DynamicGen):
      PATH = 'pdks'
  
      def display_config(self, chip, modname):
          '''Display parameters under `pdk`, `asic`, and `library` in nested form.'''
  
          section_key = '-'.join(['pdks', modname, 'configuration'])
          settings = build_section('Configuration', section_key)
  
          cfg = chip.getdict('pdk')
          settings += build_config_recursive(cfg, keypath_prefix=['pdk'], sec_key_prefix=['pdks', modname])
  
          return settings
  
  class LibGen(DynamicGen):
      PATH = 'libs'
  
      def extra_content(self, chip, modname):
          # assume same pdk for all libraries configured by this module
          libname = chip.getkeys('library')[0]
          pdks = chip.get('library', libname, 'pdk')
  
          if len(pdks) > 0:
              pdk = pdks[0]
              p = docutils.nodes.inline('')
              self.parse_rst(f'Associated PDK: :ref:`{pdk}<{pdk}-ref>`', p)
              return [p]
  
          return None
  
      def display_config(self, chip, modname):
          '''Display parameters under in nested form.'''
  
          sections = []
  
          for libname in chip.getkeys('library'):
              section_key = '-'.join(['libs', modname, libname, 'configuration'])
              settings = build_section(libname, section_key)
              cfg = chip.getdict('library', libname)
              settings += build_config_recursive(cfg, keypath_prefix=['library', libname], sec_key_prefix=['libs', modname])
              sections.append(settings)
  
          return sections
  
  class ToolGen(DynamicGen):
      PATH = 'tools'
  
      def display_config(self, chip, modname):
          '''Display config under `eda, <modname>` in a single table.'''
          cfg = chip.getdict('eda', modname)
          pruned = chip._prune(cfg)
          table = build_schema_value_table(pruned, keypath_prefix=['eda', modname])
          if table is not None:
              return table
          else:
              return []
  
      def get_modules_in_dir(self, module_dir):
          '''Custom implementation for ToolGen since the tool setup modules are
          under an extra directory, and this way we don't have to force users to
          add an __init__.py to make the directory a module itself.
          '''
          modules = []
          for toolname in os.listdir(module_dir):
              # skip over directories/files that don't match the structure of tool
              # directories (otherwise we'll get confused by Python metadata like
              # __init__.py or __pycache__/)
              if not os.path.isdir(f'{module_dir}/{toolname}'):
                  continue
              path = f'{module_dir}/{toolname}/{toolname}.py'
              if not os.path.exists(path):
                  continue
  
              spec = importlib.util.spec_from_file_location(toolname, path)
              module = importlib.util.module_from_spec(spec)
              spec.loader.exec_module(module)
  
              modules.append((module, toolname))
  
          return modules
  
  class TargetGen(DynamicGen):
      PATH = 'targets'
  
      def build_module_list(self, chip, header, modtype, targetname):
          modules = chip._loaded_modules[modtype]
          if len(modules) > 0:
              section = build_section(header, f'{targetname}-{modtype}')
              modlist = nodes.bullet_list()
              for module in modules:
                  list_item = nodes.list_item()
                  # TODO: replace with proper docutils nodes: sphinx.addnodes.pending_xref
                  modkey = nodes.make_id(module)
                  self.parse_rst(f':ref:`{module}<{modkey}-ref>`', list_item)
                  modlist += list_item
  
              section += modlist
              return section
          return None
  
      def display_config(self, chip, modname):
          sections = []
  
          flow_section = self.build_module_list(chip, 'Flows', 'flows', modname)
          if flow_section is not None:
              sections.append(flow_section)
  
          pdk_section = self.build_module_list(chip, 'PDK', 'pdks', modname)
          if pdk_section is not None:
              sections.append(pdk_section)
  
          libs_section = self.build_module_list(chip, 'Libraries', 'libs', modname)
          if libs_section is not None:
              sections.append(libs_section)
  
          filtered_cfg = {}
          for key in ('asic', 'mcmm'):
              filtered_cfg[key] = chip.getdict(key)
          pruned_cfg = chip._prune(filtered_cfg)
  
          if len(pruned_cfg) > 0:
              schema_section = build_section('Configuration', key=f'{modname}-config')
              schema_section += build_schema_value_table(pruned_cfg)
              sections.append(schema_section)
  
          return sections
  
  class AppGen(DynamicGen):
      PATH = 'apps'
  
      def document_module(self, module, modname, path):
          # TODO: Auto-documentation does not work with apps that use 'input(...)'
          if modname in ('sc_configure'):
              return
  
          cmd_name = modname.replace('_', '-')
          cmd = [cmd_name, '--help']
  
          output = subprocess.check_output(cmd).decode('ascii')
  
          section = build_section(cmd_name, cmd_name)
          section += literalblock(output)
  
          return section
  
  class ExampleGen(DynamicGen):
  
      def get_modules(self):
          examples_dir = f'{SC_ROOT}/examples'
  
          modules = []
          for example in os.listdir(examples_dir):
              if not os.path.isdir(f'{examples_dir}/{example}'):
                  continue
              path = f'{examples_dir}/{example}/{example}.py'
              if not os.path.exists(path):
                  continue
  
              spec = importlib.util.spec_from_file_location(example, path)
              module = importlib.util.module_from_spec(spec)
              spec.loader.exec_module(module)
  
              modules.append((module, example))
  
          return modules
  
      def document_module(self, module, modname, path):
          section = build_section(modname, modname)
  
          if not hasattr(module, 'main'):
              return None
  
          main = getattr(module, 'main')
  
          # raw docstrings have funky indentation (basically, each line is already
          # indented as much as the function), so we call trim() helper function
          # to clean it up
          docstr = trim(main.__doc__)
  
          if docstr:
              self.parse_rst(docstr, section)
  
          return section
  
  def setup(app):
      app.add_directive('flowgen', FlowGen)
      app.add_directive('pdkgen', PDKGen)
      app.add_directive('libgen', LibGen)
      app.add_directive('toolgen', ToolGen)
      app.add_directive('appgen', AppGen)
      app.add_directive('examplegen', ExampleGen)
      app.add_directive('targetgen', TargetGen)
  
      return {
          'version': '0.1',
          'parallel_read_safe': True,
          'parallel_write_safe': True,
      }
  
  
  [... the rest is omitted ...]
files: docs/_ext/dynamicgen.py
files: '''Sphinx extension that provides directives for automatically generating
  documentation for the three types of dynamically loaded modules used by SC:
  flows, foundries, and tools.
  '''
  
  from docutils import nodes
  from sphinx.util.nodes import nested_parse_with_titles
  from docutils.statemachine import ViewList
  from sphinx.util.docutils import SphinxDirective
  import docutils
  from docutils.parsers.rst import directives
  
  import copy
  import importlib
  import pkgutil
  import os
  import sys
  import subprocess
  
  from common import *
  
  import siliconcompiler
  
  #############
  # Helpers
  #############
  
  # We need this in a few places, so just make it global
  SC_ROOT = os.path.abspath(f'{__file__}/../../../')
  
  def build_schema_value_table(schema, keypath_prefix=[], skip_zero_weight=False):
      '''Helper function for displaying values set in schema as a docutils table.'''
      table = [[strong('Keypath'), strong('Value')]]
      flat_cfg = flatten(schema)
      for keys, val in flat_cfg.items():
          full_keypath = list(keypath_prefix) + list(keys)
  
          if (skip_zero_weight and
              len(full_keypath) == 6 and full_keypath[0] == 'flowgraph' and full_keypath[-2] == 'weight' and
              'value' in val and val['value'] == '0'):
              continue
  
          keypath = ', '.join(full_keypath)
  
          if 'value' in val and val['value']:
              # Don't display false booleans
              if val['type'] == 'bool' and val['value'] == 'false':
                  continue
              if val['type'].startswith('['):
                  if len(val['value']) > 1:
                      val_node = build_list([code(v) for v in val['value']])
                  elif len(val['value']) > 0:
                      val_node = code(val['value'][0])
                  else:
                      val_node = para('')
              else:
                  val_node = code(val['value'])
  
              table.append([code(keypath), val_node])
  
      if len(table) > 1:
          return build_table(table)
      else:
          return None
  
  def build_config_recursive(schema, keypath_prefix=[], sec_key_prefix=[]):
      '''Helper function for displaying schema at each level as tables unde nested
      sections.
  
      For each item:
      - If it's a leaf, collect it into a table we will display at this
          level
      - Otherwise, recurse and collect sections of lower levels
      '''
      leaves = {}
      child_sections = []
      for key, val in schema.items():
          if key == 'default': continue
          if 'help' in val:
              if 'value' in val and val['value']:
                  leaves.update({key: val})
          else:
              children = build_config_recursive(val, keypath_prefix=keypath_prefix+[key], sec_key_prefix=sec_key_prefix)
              child_sections.extend(children)
  
      # If we've found leaves, create a new section where we'll display a
      # table plus all child sections.
      if len(leaves) > 0:
          keypath = ', '.join(keypath_prefix)
          section_key = '-'.join(sec_key_prefix + keypath_prefix)
          top = build_section(keypath, section_key)
          top += build_schema_value_table(leaves, keypath_prefix=keypath_prefix)
          top += child_sections
          return [top]
      else:
          # Otherwise, just pass on the child sections -- we don't want to
          # create an extra level of section hierarchy for levels of the
          # schema without leaves.
          return child_sections
  
  def trim(docstring):
      '''Helper function for cleaning up indentation of docstring.
  
      This is important for properly parsing complex RST in our docs.
  
      Source:
      https://www.python.org/dev/peps/pep-0257/#handling-docstring-indentation'''
      if not docstring:
          return ''
      # Convert tabs to spaces (following the normal Python rules)
      # and split into a list of lines:
      lines = docstring.expandtabs().splitlines()
      # Determine minimum indentation (first line doesn't count):
      indent = sys.maxsize
      for line in lines[1:]:
          stripped = line.lstrip()
          if stripped:
              indent = min(indent, len(line) - len(stripped))
      # Remove indentation (first line is special):
      trimmed = [lines[0].strip()]
      if indent < sys.maxsize:
          for line in lines[1:]:
              trimmed.append(line[indent:].rstrip())
      # Strip off trailing and leading blank lines:
      while trimmed and not trimmed[-1]:
          trimmed.pop()
      while trimmed and not trimmed[0]:
          trimmed.pop(0)
      # Return a single string:
      return '\n'.join(trimmed)
  
  #############
  # Base class
  #############
  
  def flag_opt(argument):
      if argument is not None:
          raise ValueError('Flag should not have content')
      return True
  
  class DynamicGen(SphinxDirective):
      '''Base class for all three directives provided by this extension.
  
      Each child class implements a directive by overriding the display_config()
      method and setting a PATH member variable.
      '''
  
      option_spec = {'nobuiltins': flag_opt}
  
      def document_module(self, module, modname, path):
          '''Build section documenting given module and name.'''
          s = build_section_with_target(modname, f'{modname}-ref', self.state.document)
  
          if not hasattr(module, 'make_docs'):
              return None
  
          make_docs = getattr(module, 'make_docs')
  
          # raw docstrings have funky indentation (basically, each line is already
          # indented as much as the function), so we call trim() helper function
          # to clean it up
          docstr = trim(make_docs.__doc__)
  
          if docstr:
              self.parse_rst(docstr, s)
  
          builtin = os.path.abspath(path).startswith(SC_ROOT)
  
          if builtin:
              relpath = path[len(SC_ROOT)+1:]
              gh_root = 'https://github.com/siliconcompiler/siliconcompiler/blob/main'
              gh_link = f'{gh_root}/{relpath}'
              filename = os.path.basename(relpath)
              p = para('Setup file: ')
              p += link(gh_link, text=filename)
              s += p
  
          chip = make_docs()
  
          extra_content = self.extra_content(chip, modname)
          if extra_content is not None:
              s += extra_content
  
          s += self.display_config(chip, modname)
  
          return s
  
      def run(self):
          '''Main entry point of directive.'''
          sections = []
  
          for module, modname in self.get_modules():
              path = module.__file__
              self.env.note_dependency(path)
              docs = self.document_module(module, modname, path)
              if docs is not None:
                  sections.append((docs, modname))
  
          if len(sections) > 0:
              # Sort sections by module name
              sections = sorted(sections, key=lambda t: t[1])
              # Strip off modname so we just return list of docutils sections
              sections, _ = zip(*sections)
  
          return list(sections)
  
      def get_modules(self):
          '''Gets dynamic modules under `self.PATH`.
  
          This function explicitly searches builtins as well as SCPATH
          directories. Although the directory for builtin tools gets added to
          SCPATH after a chip object has been initialized, we can't rely on this
          since we can't be sure that's happened yet. Therefore, we have to check
          each one explicitly.
  
          However, this could result in duplicate modules being detected once the
          SCPATH does get updated. Therefore, we check to ensure that SCPATH
          directories are not equal to the builtins directory before searching it.
  
          TODO: we want better duplicate resolution (in case the user explicitly
          declares a duplicate tool), where SCPATH takes priority.
          '''
          builtins_dir = f'{SC_ROOT}/siliconcompiler/{self.PATH}'
          if 'nobuiltins' not in self.options:
              modules = self.get_modules_in_dir(builtins_dir)
          else:
              modules = []
  
          if 'SCPATH' in os.environ:
              scpaths = os.environ['SCPATH'].split(':')
              for scpath in scpaths:
                  user_dir = f'{scpath}/{self.PATH}'
                  if not os.path.isdir(user_dir) or builtins_dir == user_dir:
                      continue
                  modules.extend(self.get_modules_in_dir(user_dir))
  
          return modules
  
      def get_modules_in_dir(self, module_dir):
          '''Routine for getting modules and their names from a certain
          directory.'''
          modules = []
          for importer, modname, _ in pkgutil.iter_modules([module_dir]):
              if modname in ('sc_floorplan'):
                  continue
              module = importer.find_module(modname).load_module(modname)
              modules.append((module, modname))
  
          return modules
  
      def parse_rst(self, content, s):
          '''Helper for parsing reStructuredText content, adding it directly to
          section `s`.'''
          rst = ViewList()
          # use fake filename 'inline' for error # reporting
          for i, line in enumerate(content.split('\n')):
              rst.append(line, 'inline', i)
          nested_parse_with_titles(self.state, rst, s)
  
      def extra_content(self, chip, modname):
          '''Adds extra content to documentation.
  
          May return a list of docutils nodes that will be added to the
          documentation in between a module's docstring and configuration table.
          Otherwise, if return value is None, don't add anything.
          '''
          return None
  
  #########################
  # Specialized extensions
  #########################
  
  class FlowGen(DynamicGen):
      PATH = 'flows'
  
      def extra_content(self, chip, modname):
          flow_path = os.path.join(self.env.app.outdir, f'_images/gen/{modname}.svg')
          #chip.write_flowgraph(flow_path, fillcolor='#1c4587', fontcolor='#f1c232', border=False)
          chip.write_flowgraph(flow_path)
          return [image(flow_path, center=True)]
  
      def display_config(self, chip, modname):
          '''Display parameters under `flowgraph, <step>`, `metric, <step>` and
          `showtool`. Parameters are grouped into sections by step, with an
          additional table for non-step items.
          '''
          section_key = '-'.join(['flows', modname, 'configuration'])
          settings = build_section('Configuration', section_key)
  
          steps = chip.getkeys('flowgraph')
          # TODO: should try to order?
  
          # Build section + table for each step (combining entires under flowgraph
          # and metric)
          for step in steps:
              section_key = '-'.join(['flows', modname, step])
              section = build_section(step, section_key)
              step_cfg = {}
              for prefix in ['flowgraph', 'metric']:
                  cfg = chip.getdict(prefix, step)
                  if cfg is None:
                      continue
                  pruned = chip._prune(cfg)
                  if prefix not in step_cfg:
                      step_cfg[prefix] = {}
                  step_cfg[prefix][step] = pruned
  
              section += build_schema_value_table(step_cfg, skip_zero_weight=True)
              settings += section
  
          # Build table for non-step items (just showtool for now)
          section_key = '-'.join(['flows', modname, 'showtool'])
          section = build_section('showtool', section_key)
          cfg = chip.getdict('showtool')
          pruned = chip._prune(cfg)
          table = build_schema_value_table(pruned)
          if table is not None:
              section += table
              settings += section
  
          return settings
  
  class PDKGen(DynamicGen):
      PATH = 'pdks'
  
      def display_config(self, chip, modname):
          '''Display parameters under `pdk`, `asic`, and `library` in nested form.'''
  
          section_key = '-'.join(['pdks', modname, 'configuration'])
          settings = build_section('Configuration', section_key)
  
          cfg = chip.getdict('pdk')
          settings += build_config_recursive(cfg, keypath_prefix=['pdk'], sec_key_prefix=['pdks', modname])
  
          return settings
  
  class LibGen(DynamicGen):
      PATH = 'libs'
  
      def extra_content(self, chip, modname):
          # assume same pdk for all libraries configured by this module
          libname = chip.getkeys('library')[0]
          pdks = chip.get('library', libname, 'pdk')
  
          if len(pdks) > 0:
              pdk = pdks[0]
              p = docutils.nodes.inline('')
              self.parse_rst(f'Associated PDK: :ref:`{pdk}<{pdk}-ref>`', p)
              return [p]
  
          return None
  
      def display_config(self, chip, modname):
          '''Display parameters under in nested form.'''
  
          sections = []
  
          for libname in chip.getkeys('library'):
              section_key = '-'.join(['libs', modname, libname, 'configuration'])
              settings = build_section(libname, section_key)
              cfg = chip.getdict('library', libname)
              settings += build_config_recursive(cfg, keypath_prefix=['library', libname], sec_key_prefix=['libs', modname])
              sections.append(settings)
  
          return sections
  
  class ToolGen(DynamicGen):
      PATH = 'tools'
  
      def display_config(self, chip, modname):
          '''Display config under `eda, <modname>` in a single table.'''
          cfg = chip.getdict('eda', modname)
          pruned = chip._prune(cfg)
          table = build_schema_value_table(pruned, keypath_prefix=['eda', modname])
          if table is not None:
              return table
          else:
              return []
  
      def get_modules_in_dir(self, module_dir):
          '''Custom implementation for ToolGen since the tool setup modules are
          under an extra directory, and this way we don't have to force users to
          add an __init__.py to make the directory a module itself.
          '''
          modules = []
          for toolname in os.listdir(module_dir):
              # skip over directories/files that don't match the structure of tool
              # directories (otherwise we'll get confused by Python metadata like
              # __init__.py or __pycache__/)
              if not os.path.isdir(f'{module_dir}/{toolname}'):
                  continue
              path = f'{module_dir}/{toolname}/{toolname}.py'
              if not os.path.exists(path):
                  continue
  
              spec = importlib.util.spec_from_file_location(toolname, path)
              module = importlib.util.module_from_spec(spec)
              spec.loader.exec_module(module)
  
              modules.append((module, toolname))
  
          return modules
  
  class TargetGen(DynamicGen):
      PATH = 'targets'
  
      def build_module_list(self, chip, header, modtype, targetname):
          modules = chip._loaded_modules[modtype]
          if len(modules) > 0:
              section = build_section(header, f'{targetname}-{modtype}')
              modlist = nodes.bullet_list()
              for module in modules:
                  list_item = nodes.list_item()
                  # TODO: replace with proper docutils nodes: sphinx.addnodes.pending_xref
                  modkey = nodes.make_id(module)
                  self.parse_rst(f':ref:`{module}<{modkey}-ref>`', list_item)
                  modlist += list_item
  
              section += modlist
              return section
          return None
  
      def display_config(self, chip, modname):
          sections = []
  
          flow_section = self.build_module_list(chip, 'Flows', 'flows', modname)
          if flow_section is not None:
              sections.append(flow_section)
  
          pdk_section = self.build_module_list(chip, 'PDK', 'pdks', modname)
          if pdk_section is not None:
              sections.append(pdk_section)
  
          libs_section = self.build_module_list(chip, 'Libraries', 'libs', modname)
          if libs_section is not None:
              sections.append(libs_section)
  
          filtered_cfg = {}
          for key in ('asic', 'mcmm'):
              filtered_cfg[key] = chip.getdict(key)
          pruned_cfg = chip._prune(filtered_cfg)
  
          if len(pruned_cfg) > 0:
              schema_section = build_section('Configuration', key=f'{modname}-config')
              schema_section += build_schema_value_table(pruned_cfg)
              sections.append(schema_section)
  
          return sections
  
  class AppGen(DynamicGen):
      PATH = 'apps'
  
      def document_module(self, module, modname, path):
          # TODO: Auto-documentation does not work with apps that use 'input(...)'
          if modname in ('sc_configure'):
              return
  
          cmd_name = modname.replace('_', '-')
          cmd = [cmd_name, '--help']
  
          output = subprocess.check_output(cmd).decode('ascii')
  
          section = build_section(cmd_name, cmd_name)
          section += literalblock(output)
  
          return section
  
  class ExampleGen(DynamicGen):
  
      def get_modules(self):
          examples_dir = f'{SC_ROOT}/examples'
  
          modules = []
          for example in os.listdir(examples_dir):
              if not os.path.isdir(f'{examples_dir}/{example}'):
                  continue
              path = f'{examples_dir}/{example}/{example}.py'
              if not os.path.exists(path):
                  continue
  
              spec = importlib.util.spec_from_file_location(example, path)
              module = importlib.util.module_from_spec(spec)
              spec.loader.exec_module(module)
  
              modules.append((
  
  [... the rest is omitted ...]
files: docs/_ext/dynamicgen.py
files: from docutils import nodes
  from sphinx.util.nodes import nested_parse_with_titles
  from docutils.statemachine import ViewList
  from sphinx.util.docutils import SphinxDirective
  from siliconcompiler.schema import schema_cfg
  
  from common import *
  
  # Main Sphinx plugin
  class SchemaGen(SphinxDirective):
  
      def run(self):
          self.env.note_dependency('../siliconcompiler/schema.py')
  
          schema = schema_cfg()
  
          # Split up schema into root and nested keys so that we can add a custom
          # header to the root entries.
          basic_schema = {}
          nested_schema = {}
          for key, val in schema.items():
              if is_leaf(val):
                  basic_schema[key] = val
              else:
                  nested_schema[key] = val
  
          basic_section = build_section('root', 'root')
          for n in self.process_schema(basic_schema):
              basic_section += n
  
          nested_sections = self.process_schema(nested_schema)
  
          return [basic_section] + nested_sections
  
      def process_schema(self, schema, parents=[]):
          if 'help' in schema:
              entries = [[strong('Description'),   para(schema['shorthelp'])],
                         [strong('Type'),          para(schema['type'])],
                         [strong('Default Value'), para(schema['defvalue'])],
                         [strong('CLI Switch'),    code(schema['switch'])]]
              for example in schema['example']:
                  name, ex = example.split(':', 1)
                  entries.append([strong(f'Example ({name.upper()})'), code(ex.strip())])
  
              table = build_table(entries)
              body = self.parse_rst(schema['help'])
  
              return [table, body]
          else:
              sections = []
              for key in schema.keys():
                  if key == 'default':
                      for n in self.process_schema(schema['default'], parents=parents):
                          sections.append(n)
                  else:
                      section_key = '-'.join(parents) + '-' + key
                      section = build_section(key, section_key)
                      for n in self.process_schema(schema[key], parents=parents+[key]):
                          section += n
                      sections.append(section)
  
              # Sort all sections alphabetically by title. We may also have nodes
              # in this list that aren't sections if  `schema` has a 'default'
              # entry that's a leaf. In this case, we sort this as an empty string
              # in order to put this node at the beginning of the list.
              return sorted(sections, key=lambda s: s[0][0] if isinstance(s, nodes.section) else '')
  
      def parse_rst(self, content):
          rst = ViewList()
          # use fake filename 'inline' for error # reporting
          for i, line in enumerate(content.split('\n')):
              rst.append(line, 'inline', i)
          body = nodes.paragraph()
          nested_parse_with_titles(self.state, rst, body)
  
          return body
  
  
  def setup(app):
      app.add_directive('schemagen', SchemaGen)
  
      return {
          'version': '0.1',
          'parallel_read_safe': True,
          'parallel_write_safe': True,
      }
  
  
  [... the rest is omitted ...]
files: docs/_ext/schemagen.py
files: from docutils import nodes
  from sphinx.util.nodes import nested_parse_with_titles
  from docutils.statemachine import ViewList
  from sphinx.util.docutils import SphinxDirective
  from siliconcompiler.schema import schema_cfg
  
  from common import *
  
  # Main Sphinx plugin
  class SchemaGen(SphinxDirective):
  
      def run(self):
          self.env.note_dependency('../siliconcompiler/schema.py')
  
          schema = schema_cfg()
  
          # Split up schema into root and nested keys so that we can add a custom
          # header to the root entries.
          basic_schema = {}
          nested_schema = {}
          for key, val in schema.items():
              if is_leaf(val):
                  basic_schema[key] = val
              else:
                  nested_schema[key] = val
  
          basic_section = build_section('root', 'root')
          for n in self.process_schema(basic_schema):
              basic_section += n
  
          nested_sections = self.process_schema(nested_schema)
  
          return [basic_section] + nested_sections
  
      def process_schema(self, schema, parents=[]):
          if 'help' in schema:
              entries = [[strong('Description'),   para(schema['shorthelp'])],
                         [strong('Type'),          para(schema['type'])],
                         [strong('Default Value'), para(schema['defvalue'])],
                         [strong('CLI Switch'),    code(schema['switch'])]]
              for example in schema['example']:
                  name, ex = example.split(':', 1)
                  entries.append([strong(f'Example ({name.upper()})'), code(ex.strip())])
  
              table = build_table(entries)
              body = self.parse_rst(schema['help'])
  
              return [table, body]
          else:
              sections = []
              for key in schema.keys():
                  if key == 'default':
                      for n in self.process_schema(schema['default'], parents=parents):
                          sections.append(n)
                  else:
                      section_key = '-'.join(parents) + '-' + key
                      section = build_section(key, section_key)
                      for n in self.process_schema(schema[key], parents=parents+[key]):
                          section += n
                      sections.append(section)
  
              # Sort all sections alphabetically by title. We may also have nodes
              # in this list that aren't sections if  `schema` has a 'default'
              # entry that's a leaf. In this case, we sort this as an empty string
              # in order to put this node at the beginning of the list.
              return sorted(sections, key=lambda s: s[0][0] if isinstance(s, nodes.section) else '')
  
      def parse_rst(self, content):
          rst = ViewList()
          # use fake filename 'inline' and fake line number '1' for error reporting
          rst.append(content, 'inline', 1)
          body = nodes.paragraph()
          nested_parse_with_titles(self.state, rst, body)
  
          return body
  
  
  def setup(app):
      app.add_directive('schemagen', SchemaGen)
  
      return {
          'version': '0.1',
          'parallel_read_safe': True,
          'parallel_write_safe': True,
      }
  
  
  [... the rest is omitted ...]
files: docs/_ext/schemagen.py
files: # Copyright 2020 Silicon Compiler Authors. All Rights Reserved.
  
  from siliconcompiler import utils
  
  import re
  import os
  import sys
  import copy
  import json
  
  #############################################################################
  # PARAM DEFINITION
  #############################################################################
  
  def scparam(cfg,
              keypath,
              sctype=None,
              require=None,
              defvalue=None,
              scope='global',
              copy='false',
              lock='false',
              hashalgo='sha256',
              signature=None,
              shorthelp=None,
              switch=None,
              example=None,
              schelp=None):
  
      # 1. decend keypath until done
      # 2. create key if missing
      # 3. populate leaf cell when keypath empty
      if keypath:
          key = keypath[0]
          keypath.pop(0)
          if key not in cfg.keys():
              cfg[key] = {}
          scparam(cfg[key],
                  keypath,
                  sctype=sctype,
                  scope=scope,
                  require=require,
                  defvalue=defvalue,
                  copy=copy,
                  lock=lock,
                  signature=signature,
                  hashalgo=hashalgo,
                  shorthelp=shorthelp,
                  switch=switch,
                  example=example,
                  schelp=schelp)
      else:
  
          # removing leading spaces as if schelp were a docstring
          schelp = utils.trim(schelp)
  
          # setting valus based on types
          # note (bools are never lists)
          if re.match(r'bool',sctype):
              require = 'all'
              defvalue = 'false'
          if re.match(r'\[',sctype) and signature is None:
              signature = []
          if re.match(r'\[',sctype) and defvalue is None:
              defvalue = []
  
          # mandatory for all
          cfg['type'] = sctype
          cfg['scope'] = scope
          cfg['require'] = require
          cfg['lock'] = lock
          cfg['switch'] = switch
          cfg['shorthelp'] = shorthelp
          cfg['example'] = example
          cfg['help'] = schelp
          cfg['defvalue'] = defvalue
          cfg['value'] = defvalue
          cfg['signature'] = signature
  
          # file only values
          if re.search(r'file',sctype):
              cfg['hashalgo'] = hashalgo
              cfg['copy'] = copy
              cfg['filehash'] = []
              cfg['date'] = []
              cfg['author'] = []
  
  
  #############################################################################
  # CHIP CONFIGURATION
  #############################################################################
  
  def schema_cfg():
      '''Method for defining Chip configuration schema
      All the keys defined in this dictionary are reserved words.
      '''
  
      # SC version number (bump on every non trivial change)
      # Version number following semver standard.
      # Software version syncs with SC releases (from _metadata)
  
      SCHEMA_VERSION = '0.8.0'
  
      # Basic schema setup
      cfg = {}
  
      # Version handling
      cfg = schema_version(cfg, SCHEMA_VERSION)
  
      # Runtime options
      cfg = schema_options(cfg)
      cfg = schema_arg(cfg)
  
      # Primary sources
      cfg = schema_design(cfg)
  
      # Constraints
      #cfg = schema_constraints(cfg)
  
      # Project configuration
      cfg = schema_package(cfg, 'package')
      cfg = schema_checklist(cfg)
      cfg = schema_design(cfg)
      cfg = schema_read(cfg)
      cfg = schema_fpga(cfg)
      cfg = schema_asic(cfg)
      cfg = schema_mcmm(cfg)
  
      # Flow Information
      cfg = schema_flowgraph(cfg)
      cfg = schema_flowstatus(cfg)
      cfg = schema_eda(cfg)
  
      # PDK
      cfg = schema_pdk(cfg)
  
      # Package management
      cfg = schema_libs(cfg)
      cfg = schema_package(cfg, 'library')
      cfg = schema_checklist(cfg, 'library')
  
      # Compilation records
      cfg = schema_metric(cfg)
      cfg = schema_record(cfg)
  
      return cfg
  
  ###############################################################################
  # Minimal setup FPGA
  ###############################################################################
  
  def schema_version(cfg, version):
  
      scparam(cfg,['version', 'schema'],
              sctype='str',
              defvalue=version,
              shorthelp="Schema version number",
              switch="-version_schema <str>",
              example=["cli: -version_schema",
                       "api: chip.get('version', 'schema')"],
              schelp="""SiliconCompiler schema version number.""")
  
      scparam(cfg,['version', 'software'],
              sctype='str',
              shorthelp="Software version number",
              switch="-version_software <str>",
              example=["cli: -version_software",
                       "api: chip.get('version', 'software')"],
              schelp="""SiliconCompiler software version number.""")
  
      scparam(cfg,['version', 'print'],
              sctype='bool',
              shorthelp="Prints version number",
              switch="-version <bool>",
              example=["cli: -version",
                      "api: chip.get('version', 'print')"],
              schelp="""Command line switch to print the schema and software
              version numbers in an 'sc' command line app.""")
  
      return cfg
  
  ###############################################################################
  # FPGA
  ###############################################################################
  
  def schema_fpga(cfg):
      ''' FPGA configuration
      '''
  
      scparam(cfg,['fpga', 'arch'],
              sctype='[file]',
              copy='true',
              shorthelp="FPGA architecture file",
              switch="-fpga_arch <file>",
              example=["cli: -fpga_arch myfpga.xml",
                       "api:  chip.set('fpga', 'arch', 'myfpga.xml')"],
              schelp=""" Architecture definition file for FPGA place and route
              tool. For the VPR tool, the file is a required XML based description,
              allowing targeting a large number of virtual and commercial
              architectures. For most commercial tools, the fpga part name provides
              enough information to enable compilation and the 'arch' parameter is
              optional.""")
  
      scparam(cfg,['fpga', 'vendor'],
              sctype='str',
              shorthelp="FPGA vendor name",
              switch="-fpga_vendor <str>",
              example=["cli: -fpga_vendor acme",
                      "api:  chip.set('fpga', 'vendor', 'acme')"],
              schelp="""
              Name of the FPGA vendor. The parameter is used to check part
              name and to select the eda tool flow in case 'edaflow' is
              unspecified.""")
  
      scparam(cfg,['fpga', 'partname'],
              sctype='str',
              require='fpga',
              shorthelp="FPGA part name",
              switch="-fpga_partname <str>",
              example=["cli: -fpga_partname fpga64k",
                       "api:  chip.set('fpga', 'partname', 'fpga64k')"],
              schelp="""
              Complete part name used as a device target by the FPGA compilation
              tool. The part name must be an exact string match to the partname
              hard coded within the FPGA eda tool.""")
  
      scparam(cfg,['fpga', 'board'],
              sctype='str',
              shorthelp="FPGA board name",
              switch="-fpga_board <str>",
              example=["cli: -fpga_board parallella",
                       "api:  chip.set('fpga', 'board', 'parallella')"],
              schelp="""
              Complete board name used as a device target by the FPGA compilation
              tool. The board name must be an exact string match to the partname
              hard coded within the FPGA eda tool. The parameter is optional and can
              be used in place of a partname and pin constraints for some tools.""")
  
      scparam(cfg,['fpga', 'program'],
              sctype='bool',
              shorthelp="FPGA program enable",
              switch="-fpga_program <bool>",
              example=["cli: -fpga_program",
                       "api:  chip.set('fpga', 'program', True)"],
              schelp="""Specifies that the bitstream should be loaded into an FPGA.""")
  
      scparam(cfg,['fpga', 'flash'],
              sctype='bool',
              shorthelp="FPGA flash enable",
              switch="-fpga_flash <bool>",
              example=["cli: -fpga_flash",
                       "api:  chip.set('fpga', 'flash', True)"],
              schelp="""Specifies that the bitstream should be flashed in the board/device.
              The default is to load the bitstream into volatile memory (SRAM).""")
  
  
      return cfg
  
  ###############################################################################
  # PDK
  ###############################################################################
  
  def schema_pdk(cfg, stackup='default'):
      ''' Process design kit configuration
      '''
  
      tool = 'default'
      filetype = 'default'
  
      scparam(cfg, ['pdk', 'foundry'],
              sctype='str',
              require="asic",
              shorthelp="PDK foundry name",
              switch="-pdk_foundry <str>",
              example=["cli: -pdk_foundry virtual",
                      "api:  chip.set('pdk', 'foundry', 'virtual')"],
              schelp="""
              Name of foundry corporation. Examples include intel, gf, tsmc,
              samsung, skywater, virtual. The \'virtual\' keyword is reserved for
              simulated non-manufacturable processes.""")
  
      scparam(cfg, ['pdk', 'process'],
              sctype='str',
              require="asic",
              shorthelp="PDK process name",
              switch="-pdk_process <str>",
              example=["cli: -pdk_process asap7",
                       "api:  chip.set('pdk', 'process', 'asap7')"],
              schelp="""
              Public name of the foundry process. The string is case insensitive and
              must match the public process name exactly. Examples of virtual
              processes include freepdk45 and asap7.""")
  
      scparam(cfg, ['pdk', 'version'],
              sctype='str',
              shorthelp="PDK version number",
              switch="-pdk_version <str>",
              example=["cli: -pdk_version 1.0",
                      "api:  chip.set('pdk', 'version', '1.0')"],
              schelp="""
              Alphanumeric string specifying the version of the PDK. Verification of
              correct PDK and IP versions is a hard ASIC tapeout require in all
              commercial foundries. The version number can be used for design manifest
              tracking and tapeout checklists.""")
  
      scparam(cfg, ['pdk', 'stackup'],
              sctype='[str]',
              require='asic',
              shorthelp="PDK metal stackups",
              switch="-pdk_stackup <str>",
              example=["cli: -pdk_stackup 2MA4MB2MC",
                       "api: chip.add('pdk','stackup','2MA4MB2MC')"],
              schelp="""
              List of all metal stackups offered in the process node. Older process
              nodes may only offer a single metal stackup, while advanced nodes
              offer a large but finite list of metal stacks with varying combinations
              of metal line pitches and thicknesses. Stackup naming is unique to a
              foundry, but is generally a long string or code. For example, a 10
              metal stackup with two 1x wide, four 2x wide, and 4x wide metals,
              might be identified as 2MA4MB2MC, where MA, MB, and MC denote wiring
              layers with different properties (thickness, width, space). Each
              stackup will come with its own set of routing technology files and
              parasitic models specified in the pdk_pexmodel and pdk_aprtech
              parameters.""")
  
      scparam(cfg, ['pdk', 'node'],
              sctype='float',
              require="asic",
              shorthelp="PDK process node",
              switch="-pdk_node <float>",
              example=["cli: -pdk_node 130",
                      "api:  chip.set('pdk', 'node', 130)"],
              schelp="""
              Approximate relative minimum dimension of the process target specified
              in nanometers. The parameter is required for flows and tools that
              leverage the value to drive technology dependent synthesis and APR
              optimization. Node examples include 180, 130, 90, 65, 45, 32, 22 14,
              10, 7, 5, 3.""")
  
      scparam(cfg, ['pdk', 'wafersize'],
              sctype='float',
              require="asic",
              shorthelp="PDK process node",
              switch="-pdk_wafersize <float>",
              example=["cli: -pdk_wafersize 300",
                      "api:  chip.set('pdk', 'wafersize', 300)"],
              schelp="""
              Wafer diameter used in manufacturing process specified in mm. The
              standard diameter for leading edge manufacturing is 300mm. For older
              process technologies and specialty fabs, smaller diameters such as
              200, 100, 125, 100 are common. The value is used to calculate dies per
              wafer and full factory chip costs.""")
  
      scparam(cfg, ['pdk', 'wafercost'],
              sctype='float',
              shorthelp="PDK wafer cost",
              switch="-pdk_wafercost <float>",
              example=["cli: -pdk_wafercost 10000",
                       "api:  chip.set('pdk', 'wafercost', 10000)"],
              schelp="""
              Raw cost per wafer purchased specified in USD, not accounting for
              yield loss. The values is used to calculate chip full factory costs.""")
  
      scparam(cfg, ['pdk', 'd0'],
              sctype='float',
              shorthelp="PDK process defect density",
              switch="-pdk_d0 <float>",
              example=["cli: -pdk_d0 0.1",
                       "api:  chip.set('pdk', 'd0', 0.1)"],
              schelp="""
              Process defect density (d0) expressed as random defects per cm^2. The
              value is used to calculate yield losses as a function of area, which in
              turn affects the chip full factory costs. Two yield models are
              supported: Poisson (default), and Murphy. The Poisson based yield is
              calculated as dy = exp(-area * d0/100). The Murphy based yield is
              calculated as dy = ((1-exp(-area * d0/100))/(area * d0/100))^2.""")
  
      scparam(cfg, ['pdk', 'hscribe'],
              sctype='float',
              shorthelp="PDK horizontal scribe line width",
              switch="-pdk_hscribe <float>",
              example=["cli: -pdk_hscribe 0.1",
                       "api:  chip.set('pdk', 'hscribe', 0.1)"],
              schelp="""
               Width of the horizontal scribe line (in mm) used during die separation.
              The process is generally completed using a mechanical saw, but can be
              done through combinations of mechanical saws, lasers, wafer thinning,
              and chemical etching in more advanced technologies. The value is used
              to calculate effective dies per wafer and full factory cost.""")
  
      scparam(cfg, ['pdk', 'vscribe'],
              sctype='float',
              shorthelp="PDK vertical scribe line width",
              switch="-pdk_vscribe <float>",
              example=["cli: -pdk_vscribe 0.1",
                       "api:  chip.set('pdk', 'vscribe', 0.1)"],
              schelp="""
               Width of the vertical scribe line (in mm) used during die separation.
              The process is generally completed using a mechanical saw, but can be
              done through combinations of mechanical saws, lasers, wafer thinning,
              and chemical etching in more advanced technologies. The value is used
              to calculate effective dies per wafer and full factory cost.""")
  
      scparam(cfg, ['pdk', 'edgemargin'],
              sctype='float',
              shorthelp="PDK wafer edge keep-out margin",
              switch="-pdk_edgemargin <float>",
              example=["cli: -pdk_edgemargin 1",
                       "api:  chip.set('pdk', 'edgemargin', 1)"],
              schelp="""
              Keep-out distance/margin from the wafer edge inwards specified in mm.
              The wafer edge is prone to chipping and need special treatment that
              preclude placement of designs in this area. The edge value is used to
              calculate effective dies per wafer and full factory cost.""")
  
      scparam(cfg, ['pdk', 'density'],
              sctype='float',
              shorthelp="PDK transistor density",
              switch="-pdk_density <float>",
              example=["cli: -pdk_density 100e6",
                      "api:  chip.set('pdk', 'density', 10e6)"],
              schelp="""
              Approximate logic density expressed as # transistors / mm^2
              calculated as:
              0.6 * (Nand2 Transistor Count) / (Nand2 Cell Area) +
              0.4 * (Register Transistor Count) / (Register Cell Area)
              The value is specified for a fixed standard cell library within a node
              and will differ depending on the library vendor, library track height
              and library type. The value can be used to to normalize the effective
              density reported for the design across different process nodes. The
              value can be derived from a variety of sources, including the PDK DRM,
              library LEFs, conference presentations, and public analysis.""")
  
      scparam(cfg, ['pdk', 'sramsize'],
              sctype='float',
              shorthelp="PDK SRAM bitcell size",
              switch
  
  [... the rest is omitted ...]
files: siliconcompiler/schema.py
files: # Copyright 2020 Silicon Compiler Authors. All Rights Reserved.
  
  import re
  import os
  import sys
  import copy
  import json
  
  #############################################################################
  # PARAM DEFINITION
  #############################################################################
  
  def scparam(cfg,
              keypath,
              sctype=None,
              require=None,
              defvalue=None,
              scope='global',
              copy='false',
              lock='false',
              hashalgo='sha256',
              signature=None,
              shorthelp=None,
              switch=None,
              example=None,
              schelp=None):
  
      # 1. decend keypath until done
      # 2. create key if missing
      # 3. populate leaf cell when keypath empty
      if keypath:
          key = keypath[0]
          keypath.pop(0)
          if key not in cfg.keys():
              cfg[key] = {}
          scparam(cfg[key],
                  keypath,
                  sctype=sctype,
                  scope=scope,
                  require=require,
                  defvalue=defvalue,
                  copy=copy,
                  lock=lock,
                  signature=signature,
                  hashalgo=hashalgo,
                  shorthelp=shorthelp,
                  switch=switch,
                  example=example,
                  schelp=schelp)
      else:
  
          # removing leading newline and space
          schelp = re.sub(r'\n\s*', " ", schelp)
          schelp = schelp.strip()
  
          # setting valus based on types
          # note (bools are never lists)
          if re.match(r'bool',sctype):
              require = 'all'
              defvalue = 'false'
          if re.match(r'\[',sctype) and signature is None:
              signature = []
          if re.match(r'\[',sctype) and defvalue is None:
              defvalue = []
  
          # mandatory for all
          cfg['type'] = sctype
          cfg['scope'] = scope
          cfg['require'] = require
          cfg['lock'] = lock
          cfg['switch'] = switch
          cfg['shorthelp'] = shorthelp
          cfg['example'] = example
          cfg['help'] = schelp
          cfg['defvalue'] = defvalue
          cfg['value'] = defvalue
          cfg['signature'] = signature
  
          # file only values
          if re.search(r'file',sctype):
              cfg['hashalgo'] = hashalgo
              cfg['copy'] = copy
              cfg['filehash'] = []
              cfg['date'] = []
              cfg['author'] = []
  
  
  #############################################################################
  # CHIP CONFIGURATION
  #############################################################################
  
  def schema_cfg():
      '''Method for defining Chip configuration schema
      All the keys defined in this dictionary are reserved words.
      '''
  
      # SC version number (bump on every non trivial change)
      # Version number following semver standard.
      # Software version syncs with SC releases (from _metadata)
  
      SCHEMA_VERSION = '0.8.0'
  
      # Basic schema setup
      cfg = {}
  
      # Version handling
      cfg = schema_version(cfg, SCHEMA_VERSION)
  
      # Runtime options
      cfg = schema_options(cfg)
      cfg = schema_arg(cfg)
  
      # Primary sources
      cfg = schema_design(cfg)
  
      # Constraints
      #cfg = schema_constraints(cfg)
  
      # Project configuration
      cfg = schema_package(cfg, 'package')
      cfg = schema_checklist(cfg)
      cfg = schema_design(cfg)
      cfg = schema_read(cfg)
      cfg = schema_fpga(cfg)
      cfg = schema_asic(cfg)
      cfg = schema_mcmm(cfg)
  
      # Flow Information
      cfg = schema_flowgraph(cfg)
      cfg = schema_flowstatus(cfg)
      cfg = schema_eda(cfg)
  
      # PDK
      cfg = schema_pdk(cfg)
  
      # Package management
      cfg = schema_libs(cfg)
      cfg = schema_package(cfg, 'library')
      cfg = schema_checklist(cfg, 'library')
  
      # Compilation records
      cfg = schema_metric(cfg)
      cfg = schema_record(cfg)
  
      return cfg
  
  ###############################################################################
  # Minimal setup FPGA
  ###############################################################################
  
  def schema_version(cfg, version):
  
      scparam(cfg,['version', 'schema'],
              sctype='str',
              defvalue=version,
              shorthelp="Schema version number",
              switch="-version_schema <str>",
              example=["cli: -version_schema",
                       "api: chip.get('version', 'schema')"],
              schelp="""SiliconCompiler schema version number.""")
  
      scparam(cfg,['version', 'software'],
              sctype='str',
              shorthelp="Software version number",
              switch="-version_software <str>",
              example=["cli: -version_software",
                       "api: chip.get('version', 'software')"],
              schelp="""SiliconCompiler software version number.""")
  
      scparam(cfg,['version', 'print'],
              sctype='bool',
              shorthelp="Prints version number",
              switch="-version <bool>",
              example=["cli: -version",
                      "api: chip.get('version', 'print')"],
              schelp="""Command line switch to print the schema and software
              version numbers in an 'sc' command line app.""")
  
      return cfg
  
  ###############################################################################
  # FPGA
  ###############################################################################
  
  def schema_fpga(cfg):
      ''' FPGA configuration
      '''
  
      scparam(cfg,['fpga', 'arch'],
              sctype='[file]',
              copy='true',
              shorthelp="FPGA architecture file",
              switch="-fpga_arch <file>",
              example=["cli: -fpga_arch myfpga.xml",
                       "api:  chip.set('fpga', 'arch', 'myfpga.xml')"],
              schelp=""" Architecture definition file for FPGA place and route
              tool. For the VPR tool, the file is a required XML based description,
              allowing targeting a large number of virtual and commercial
              architectures. For most commercial tools, the fpga part name provides
              enough information to enable compilation and the 'arch' parameter is
              optional.""")
  
      scparam(cfg,['fpga', 'vendor'],
              sctype='str',
              shorthelp="FPGA vendor name",
              switch="-fpga_vendor <str>",
              example=["cli: -fpga_vendor acme",
                      "api:  chip.set('fpga', 'vendor', 'acme')"],
              schelp="""
              Name of the FPGA vendor. The parameter is used to check part
              name and to select the eda tool flow in case 'edaflow' is
              unspecified.""")
  
      scparam(cfg,['fpga', 'partname'],
              sctype='str',
              require='fpga',
              shorthelp="FPGA part name",
              switch="-fpga_partname <str>",
              example=["cli: -fpga_partname fpga64k",
                       "api:  chip.set('fpga', 'partname', 'fpga64k')"],
              schelp="""
              Complete part name used as a device target by the FPGA compilation
              tool. The part name must be an exact string match to the partname
              hard coded within the FPGA eda tool.""")
  
      scparam(cfg,['fpga', 'board'],
              sctype='str',
              shorthelp="FPGA board name",
              switch="-fpga_board <str>",
              example=["cli: -fpga_board parallella",
                       "api:  chip.set('fpga', 'board', 'parallella')"],
              schelp="""
              Complete board name used as a device target by the FPGA compilation
              tool. The board name must be an exact string match to the partname
              hard coded within the FPGA eda tool. The parameter is optional and can
              be used in place of a partname and pin constraints for some tools.""")
  
      scparam(cfg,['fpga', 'program'],
              sctype='bool',
              shorthelp="FPGA program enable",
              switch="-fpga_program <bool>",
              example=["cli: -fpga_program",
                       "api:  chip.set('fpga', 'program', True)"],
              schelp="""Specifies that the bitstream should be loaded into an FPGA.""")
  
      scparam(cfg,['fpga', 'flash'],
              sctype='bool',
              shorthelp="FPGA flash enable",
              switch="-fpga_flash <bool>",
              example=["cli: -fpga_flash",
                       "api:  chip.set('fpga', 'flash', True)"],
              schelp="""Specifies that the bitstream should be flashed in the board/device.
              The default is to load the bitstream into volatile memory (SRAM).""")
  
  
      return cfg
  
  ###############################################################################
  # PDK
  ###############################################################################
  
  def schema_pdk(cfg, stackup='default'):
      ''' Process design kit configuration
      '''
  
      tool = 'default'
      filetype = 'default'
  
      scparam(cfg, ['pdk', 'foundry'],
              sctype='str',
              require="asic",
              shorthelp="PDK foundry name",
              switch="-pdk_foundry <str>",
              example=["cli: -pdk_foundry virtual",
                      "api:  chip.set('pdk', 'foundry', 'virtual')"],
              schelp="""
              Name of foundry corporation. Examples include intel, gf, tsmc,
              samsung, skywater, virtual. The \'virtual\' keyword is reserved for
              simulated non-manufacturable processes.""")
  
      scparam(cfg, ['pdk', 'process'],
              sctype='str',
              require="asic",
              shorthelp="PDK process name",
              switch="-pdk_process <str>",
              example=["cli: -pdk_process asap7",
                       "api:  chip.set('pdk', 'process', 'asap7')"],
              schelp="""
              Public name of the foundry process. The string is case insensitive and
              must match the public process name exactly. Examples of virtual
              processes include freepdk45 and asap7.""")
  
      scparam(cfg, ['pdk', 'version'],
              sctype='str',
              shorthelp="PDK version number",
              switch="-pdk_version <str>",
              example=["cli: -pdk_version 1.0",
                      "api:  chip.set('pdk', 'version', '1.0')"],
              schelp="""
              Alphanumeric string specifying the version of the PDK. Verification of
              correct PDK and IP versions is a hard ASIC tapeout require in all
              commercial foundries. The version number can be used for design manifest
              tracking and tapeout checklists.""")
  
      scparam(cfg, ['pdk', 'stackup'],
              sctype='[str]',
              require='asic',
              shorthelp="PDK metal stackups",
              switch="-pdk_stackup <str>",
              example=["cli: -pdk_stackup 2MA4MB2MC",
                       "api: chip.add('pdk','stackup','2MA4MB2MC')"],
              schelp="""
              List of all metal stackups offered in the process node. Older process
              nodes may only offer a single metal stackup, while advanced nodes
              offer a large but finite list of metal stacks with varying combinations
              of metal line pitches and thicknesses. Stackup naming is unique to a
              foundry, but is generally a long string or code. For example, a 10
              metal stackup with two 1x wide, four 2x wide, and 4x wide metals,
              might be identified as 2MA4MB2MC, where MA, MB, and MC denote wiring
              layers with different properties (thickness, width, space). Each
              stackup will come with its own set of routing technology files and
              parasitic models specified in the pdk_pexmodel and pdk_aprtech
              parameters.""")
  
      scparam(cfg, ['pdk', 'node'],
              sctype='float',
              require="asic",
              shorthelp="PDK process node",
              switch="-pdk_node <float>",
              example=["cli: -pdk_node 130",
                      "api:  chip.set('pdk', 'node', 130)"],
              schelp="""
              Approximate relative minimum dimension of the process target specified
              in nanometers. The parameter is required for flows and tools that
              leverage the value to drive technology dependent synthesis and APR
              optimization. Node examples include 180, 130, 90, 65, 45, 32, 22 14,
              10, 7, 5, 3.""")
  
      scparam(cfg, ['pdk', 'wafersize'],
              sctype='float',
              require="asic",
              shorthelp="PDK process node",
              switch="-pdk_wafersize <float>",
              example=["cli: -pdk_wafersize 300",
                      "api:  chip.set('pdk', 'wafersize', 300)"],
              schelp="""
              Wafer diameter used in manufacturing process specified in mm. The
              standard diameter for leading edge manufacturing is 300mm. For older
              process technologies and specialty fabs, smaller diameters such as
              200, 100, 125, 100 are common. The value is used to calculate dies per
              wafer and full factory chip costs.""")
  
      scparam(cfg, ['pdk', 'wafercost'],
              sctype='float',
              shorthelp="PDK wafer cost",
              switch="-pdk_wafercost <float>",
              example=["cli: -pdk_wafercost 10000",
                       "api:  chip.set('pdk', 'wafercost', 10000)"],
              schelp="""
              Raw cost per wafer purchased specified in USD, not accounting for
              yield loss. The values is used to calculate chip full factory costs.""")
  
      scparam(cfg, ['pdk', 'd0'],
              sctype='float',
              shorthelp="PDK process defect density",
              switch="-pdk_d0 <float>",
              example=["cli: -pdk_d0 0.1",
                       "api:  chip.set('pdk', 'd0', 0.1)"],
              schelp="""
              Process defect density (d0) expressed as random defects per cm^2. The
              value is used to calculate yield losses as a function of area, which in
              turn affects the chip full factory costs. Two yield models are
              supported: Poisson (default), and Murphy. The Poisson based yield is
              calculated as dy = exp(-area * d0/100). The Murphy based yield is
              calculated as dy = ((1-exp(-area * d0/100))/(area * d0/100))^2.""")
  
      scparam(cfg, ['pdk', 'hscribe'],
              sctype='float',
              shorthelp="PDK horizontal scribe line width",
              switch="-pdk_hscribe <float>",
              example=["cli: -pdk_hscribe 0.1",
                       "api:  chip.set('pdk', 'hscribe', 0.1)"],
              schelp="""
               Width of the horizontal scribe line (in mm) used during die separation.
              The process is generally completed using a mechanical saw, but can be
              done through combinations of mechanical saws, lasers, wafer thinning,
              and chemical etching in more advanced technologies. The value is used
              to calculate effective dies per wafer and full factory cost.""")
  
      scparam(cfg, ['pdk', 'vscribe'],
              sctype='float',
              shorthelp="PDK vertical scribe line width",
              switch="-pdk_vscribe <float>",
              example=["cli: -pdk_vscribe 0.1",
                       "api:  chip.set('pdk', 'vscribe', 0.1)"],
              schelp="""
               Width of the vertical scribe line (in mm) used during die separation.
              The process is generally completed using a mechanical saw, but can be
              done through combinations of mechanical saws, lasers, wafer thinning,
              and chemical etching in more advanced technologies. The value is used
              to calculate effective dies per wafer and full factory cost.""")
  
      scparam(cfg, ['pdk', 'edgemargin'],
              sctype='float',
              shorthelp="PDK wafer edge keep-out margin",
              switch="-pdk_edgemargin <float>",
              example=["cli: -pdk_edgemargin 1",
                       "api:  chip.set('pdk', 'edgemargin', 1)"],
              schelp="""
              Keep-out distance/margin from the wafer edge inwards specified in mm.
              The wafer edge is prone to chipping and need special treatment that
              preclude placement of designs in this area. The edge value is used to
              calculate effective dies per wafer and full factory cost.""")
  
      scparam(cfg, ['pdk', 'density'],
              sctype='float',
              shorthelp="PDK transistor density",
              switch="-pdk_density <float>",
              example=["cli: -pdk_density 100e6",
                      "api:  chip.set('pdk', 'density', 10e6)"],
              schelp="""
              Approximate logic density expressed as # transistors / mm^2
              calculated as:
              0.6 * (Nand2 Transistor Count) / (Nand2 Cell Area) +
              0.4 * (Register Transistor Count) / (Register Cell Area)
              The value is specified for a fixed standard cell library within a node
              and will differ depending on the library vendor, library track height
              and library type. The value can be used to to normalize the effective
              density reported for the design across different process nodes. The
              value can be derived from a variety of sources, including the PDK DRM,
              library LEFs, conference presentations, and public analysis.""")
  
      scparam(cfg, ['pdk', 'sramsize'],
              sctype='float',
              shorthelp="PDK SRAM bitcell
  
  [... the rest is omitted ...]
files: siliconcompiler/schema.py
files: import os
  import shutil
  import sys
  
  def copytree(src, dst, ignore=[], dirs_exist_ok=False, link=False):
      '''Simple implementation of shutil.copytree to give us a dirs_exist_ok
      option in Python < 3.8.
  
      If link is True, create hard links in dst pointing to files in src
      instead of copying them.
      '''
      os.makedirs(dst, exist_ok=dirs_exist_ok)
  
      for name in os.listdir(src):
          if name in ignore:
              continue
  
          srcfile = os.path.join(src, name)
          dstfile = os.path.join(dst, name)
  
          if os.path.isdir(srcfile):
              copytree(srcfile, dstfile, ignore=ignore, dirs_exist_ok=dirs_exist_ok)
          elif link:
              os.link(srcfile, dstfile)
          else:
              shutil.copy2(srcfile, dstfile)
  
  def trim(docstring):
      '''Helper function for cleaning up indentation of docstring.
  
      This is important for properly parsing complex RST in our docs.
  
      Source:
      https://www.python.org/dev/peps/pep-0257/#handling-docstring-indentation'''
      if not docstring:
          return ''
      # Convert tabs to spaces (following the normal Python rules)
      # and split into a list of lines:
      lines = docstring.expandtabs().splitlines()
      # Determine minimum indentation (first line doesn't count):
      indent = sys.maxsize
      for line in lines[1:]:
          stripped = line.lstrip()
          if stripped:
              indent = min(indent, len(line) - len(stripped))
      # Remove indentation (first line is special):
      trimmed = [lines[0].strip()]
      if indent < sys.maxsize:
          for line in lines[1:]:
              trimmed.append(line[indent:].rstrip())
      # Strip off trailing and leading blank lines:
      while trimmed and not trimmed[-1]:
          trimmed.pop()
      while trimmed and not trimmed[0]:
          trimmed.pop(0)
      # Return a single string:
      return '\n'.join(trimmed)
  
  
  [... the rest is omitted ...]
files: siliconcompiler/utils.py
files: import os
  import shutil
  
  def copytree(src, dst, ignore=[], dirs_exist_ok=False, link=False):
      '''Simple implementation of shutil.copytree to give us a dirs_exist_ok
      option in Python < 3.8.
  
      If link is True, create hard links in dst pointing to files in src
      instead of copying them.
      '''
      os.makedirs(dst, exist_ok=dirs_exist_ok)
  
      for name in os.listdir(src):
          if name in ignore:
              continue
  
          srcfile = os.path.join(src, name)
          dstfile = os.path.join(dst, name)
  
          if os.path.isdir(srcfile):
              copytree(srcfile, dstfile, ignore=ignore, dirs_exist_ok=dirs_exist_ok)
          elif link:
              os.link(srcfile, dstfile)
          else:
              shutil.copy2(srcfile, dstfile)
  
  
  [... the rest is omitted ...]
files: siliconcompiler/utils.py