Source code for ocebuild.parsers.asl

## @file
# Copyright (c) 2023, The OCE Build Authors. All rights reserved.
# SPDX-License-Identifier: BSD-3-Clause
##
"""Helper functions for parsing ASL source code."""

from collections import OrderedDict
from io import TextIOWrapper

from typing import List, Union

from .dict import nested_set
from .regex import re_search


def _normalize_line(string: str):
  """Normalizes a line of ASL source code."""
  return string \
    .replace('_', '') \
    .replace('"', '') \
    .replace("'", '')

def _normalize_scope(stmt: str,
                     name: str,
                     cursor: dict
                     ) -> str:
  """Normalizes the scope of a symbol's device path tree."""
  scope = cursor['scope']
  if any(c.startswith(ASL_PREFIX_MODIFIERS) for c in name):
    # Leave root-prefixed name unmodified
    if name[0] == ('\\'): return name[1:]
    # Handle upleveling cursor scope
    elif (uplevel := name.count('^')):
      name = name[uplevel:]
      scope = '.'.join(scope.split('.')[:-uplevel])
      # Modify cursor if statement is a scope
      if stmt == 'Scope': cursor['scope'] = scope
  # Resolve name scope
  delimiter = '.' if scope != '\\' else ''
  return delimiter.join([scope, name]) if scope else name

################################################################################
#                            ASL Directives and Opcodes                        #
################################################################################

[docs]ASL_COMPILER_CONTROLS = ( 'External', 'Include' )
"""Types of AST nodes that represent compiler controls. For ASL Compiler Controls (19.16; Table), see: https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/19_ASL_Reference/ACPI_Source_Language_Reference.html#asl-compiler-controls """
[docs]ASL_TYPES_SCOPES = ( 'DefinitionBlock', 'Scope' )
"""Types of AST nodes that represent scopes."""
[docs]ASL_TYPES_CONDITIONALS = ( 'If', 'ElseIf', 'Else' )
"""Types of AST nodes that represent conditionals."""
[docs]ASL_PREFIX_MODIFIERS = ( '\\', '^' )
"""ASL name modifier prefixes. For Definition Block Name Modifier Encodings (19.3; Table), see: https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/19_ASL_Reference/ACPI_Source_Language_Reference.html#definition-block-name-modifier-encodings Example: ```cpp Scope (\\PCI0) { Name (X, 3) Scope (\\) { Method (RQ) {Return (0)} } Name (^Y, 4) } ``` # ACPI Namespace -> PCI0.X, RQ, Y """ ################################################################################ # ASL Block Arguments # ################################################################################
[docs]DEFINITION_BLOCK_ARGS = ( 'AMLFileName', 'TableSignature', 'ComplianceRevision', 'OEMID', 'TableID', 'OEMRevision' )
"""The arguments of a definition block. For DefinitionBlock (19.6.29), see: https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/19_ASL_Reference/ACPI_Source_Language_Reference.html#definitionblock-declare-definition-block """ ################################################################################ # Regular Expressions # ################################################################################
[docs]RE_BLOCK_ARGS = r'\(((?:.*?),?\s?)\)'
"""Regular expression for matching block arguments. Groups: 0: The matched line 1: The block arguments """
[docs]RE_IMPORT_TYPE = r'\(.*?,\s?([a-zA-Z]+),?\)?'
"""Regular expression for matching import types. For External (Declare External Objects, 19.6.45), see: https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/19_ASL_Reference/ACPI_Source_Language_Reference.html#external-declare-external-objects Groups: 0: The import type """
[docs]RE_LOCAL_VAR = r'^(Arg|Local)(\d+)$'
"""Regular expression for matching local-scoped object names. For ArgX Objects (19.3.5.8.1), see: https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/19_ASL_Reference/ACPI_Source_Language_Reference.html#argx-objects For LocalX Objects (19.3.5.8.2), see: https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/19_ASL_Reference/ACPI_Source_Language_Reference.html#localx-objects Groups: 0: The object type 1: The variable index """
[docs]RE_STATEMENT = r'^([a-zA-Z]+)\s?\((.*?)[,\)]+?'
"""Regular expression for matching statements. Groups: 0: The statement type. 1: The object name. """
[docs]RE_NAME = r'^[A-Z\^\_\\][a-zA-Z0-9\^\_\.\\]+$'
"""Regular expression for matching object names. Groups: 0: The object name. """ ################################################################################ # ASL Parsing Methods # ################################################################################
[docs]def parse_definition_block(string: str) -> OrderedDict: """Parses arguments from a definition block line. Args: string: A definition block line. Returns: A dictionary of definition block arguments. Raises: ValueError: If the line is not a valid definition block. Example: >>> parse_definition_block('DefinitionBlock ("", "DSDT", 2, "_ASUS_", ...)') OrderedDict([('AMLFileName', ''), ('TableSignature', 'DSDT'), ('ComplianceRevision', 2), ('OEMID', '_ASUS_'), ('TableID', 'Notebook'), ('OEMRevision', '0x01072009')]) """ definition_block = OrderedDict() # Validate definition block line args = re_search(RE_BLOCK_ARGS, string, group=1, multiline=True) if not args: raise ValueError(f'Invalid definition block: {string}') # Extract definition block arguments args = map(_normalize_line, args.split(', ')) for arg, v in zip(DEFINITION_BLOCK_ARGS, args): # Parse `ComplianceVersion` arg as integer # [0-1]: All integers are 32 bit # [2+]: All integers are 64 bit if arg == 'ComplianceRevision': v = int(v) else: v = _normalize_line(v) # Update definition block definition_block[arg] = v return definition_block
[docs]def parse_ssdt_namespace(lines: Union[List[str], TextIOWrapper]) -> dict: """Parses an SSDT's namespace for imports and statement exports. Args: lines: A list of SSDT lines. Returns: A dictionary of extracted SSDT information. Example: >>> parse_ssdt_namespace(open('path/to/ssdt.dsl', encoding='UTF-8')) >>> with open('path/to/ssdt.dsl', encoding='UTF-8') as ssdt_file: ... ssdt_lines = [f.strip() for f in ssdt_file.readlines()] ... parse_ssdt_namespace(ssdt_lines) """ extracted = { 'definition_block': OrderedDict(), 'imports': OrderedDict(), 'statements': OrderedDict(), } # Enumerate SSDT lines cursor = { 'scope': '', 'blocks': [] } for line in lines: # Skip empty lines if not (lnorm := line.lstrip()): continue # Skip comments if any(lnorm.startswith(c) for c in ('/*','*','*/', '//')): continue level = len(line[:-len(lnorm)]) # Handle nested block and statement scopes if cursor['blocks'] and cursor['blocks'][-1][0] == level: if lnorm.startswith('{'): continue cursor['blocks'] = cursor['blocks'][:-1] # Handle downleveling cursor scope elif cursor['blocks'] and (downlevel := cursor['blocks'][-1][0] - level) >0: cursor['blocks'] = cursor['blocks'][:-downlevel] # Update cursor scope for subsequent entries else: cursor['scope'] = cursor['blocks'][-1][1] if cursor['blocks'] else '' # Extract tokens from line. ln_match = re_search(RE_STATEMENT, lnorm, group=None, multiline=True) if not ln_match: continue stmt, name = map(_normalize_line, ln_match.groups()) # Skip local variables if re_search(RE_LOCAL_VAR, name): continue # Handle compiler control operators if stmt in ASL_COMPILER_CONTROLS: # Extract SSDT imports if stmt == 'External': import_type = re_search(RE_IMPORT_TYPE, lnorm, group=1, multiline=True) normalized_name = _normalize_scope(stmt, name, cursor={ 'scope': '', 'blocks': [] }) nested_set(extracted, ['imports', normalized_name], import_type) # Include another SSDT file if stmt == 'Include': pass #pragma: no cover continue # Normalize object scope normalized_name = _normalize_scope(stmt, name, cursor) # Handle scopes types if stmt in ASL_TYPES_SCOPES: # Extract definition block if stmt == 'DefinitionBlock': extracted['definition_block'] = parse_definition_block(lnorm) # Extract scope block elif stmt == 'Scope': cursor['blocks'].append((level, normalized_name)) continue # Check if line is a statement if stmt in ASL_TYPES_CONDITIONALS: continue if name != re_search(RE_NAME, name): continue # Extract statement block nested_set(extracted, ['statements', normalized_name], stmt) cursor['blocks'].append((level, normalized_name)) return extracted
__all__ = [ # Constants (10) "ASL_COMPILER_CONTROLS", "ASL_TYPES_SCOPES", "ASL_TYPES_CONDITIONALS", "ASL_PREFIX_MODIFIERS", "DEFINITION_BLOCK_ARGS", "RE_BLOCK_ARGS", "RE_IMPORT_TYPE", "RE_LOCAL_VAR", "RE_STATEMENT", "RE_NAME", # Functions (2) "parse_definition_block", "parse_ssdt_namespace" ]