Source code for pyibisami.ibis.parser

"""Parse an IBIS model file.

Original Author: David Banas <capn.freako@gmail.com>

Original Date:   November 1, 2019

For information regarding the IBIS modeling standard, visit:
https://ibis.org/

Copyright (c) 2019 by David Banas; All rights reserved World wide.
"""

import re

from parsec import (
    ParseError,
    Parser,
    count,
    eof,
    fail_with,
    generate,
    letter,
    many,
    many1,
    none_of,
    one_of,
    optional,
    regex,
    separated,
    sepBy1,
    string,
    times,
)

from pyibisami.ibis.model import Component, Model

DBG = False

# Parser Definitions

whitespace = regex(r"\s+", re.MULTILINE)
comment = regex(r"\|.*")
ignore = many(whitespace | comment)


[docs] def logf(p, preStr=""): """Logs failure at point of occurence. Args: p (Parser): The original parser. Keyword Args: preStr (str): A prefix string to use in failure message. (Default = <empty string>) """ @Parser def fn(txt, ix): res = p(txt, ix) if not res.status: print( f"{preStr}: Expected {res.expected} in '{txt[res.index: res.index + 5]}' at {ParseError.loc_info(txt, res.index)}." ) return res return fn
[docs] def lexeme(p): """Lexer for words. Skips all ignored characters after word, including newlines. """ return p << ignore
[docs] def word(p): """Line limited word lexer. Only skips space after words; dosen't skip comments or newlines. Requires, at least, one white space character after word. """ return p << regex(r"\s+")
@generate("remainder of line") def rest_line(): "Parse remainder of line." chars = yield many(none_of("\n\r")) << ignore # So that we still function as a lexeme. return "".join(chars) skip_line = lexeme(rest_line).result("(Skipped.)") name_only = regex(r"[_a-zA-Z0-9/\.()#-]+") name = word(name_only) symbol = lexeme(regex(r"[a-zA-Z_][^\s()\[\]]*")) true = lexeme(string("True")).result(True) false = lexeme(string("False")).result(False) quoted_string = lexeme(regex(r'"[^"]*"')) skip_keyword = (skip_line >> many(none_of("[") >> skip_line)).result( "(Skipped.)" ) # Skip over everything until the next keyword begins. IBIS_num_suf = { "T": "e12", "k": "e3", "n": "e-9", "G": "e9", "m": "e-3", "p": "e-12", "M": "e6", "u": "e-6", "f": "e-15", } @generate("number") def number(): "Parse an IBIS numerical value." s = yield (regex(r"[-+]?[0-9]*\.?[0-9]+(([eE][-+]?[0-9]+)|([TknGmpMuf][a-zA-Z]*))?") << many(letter()) << ignore) m = re.search(r"[^\d]+$", s) if m: ix = m.start() c = s[ix] if c in IBIS_num_suf: res = float(s[:ix] + IBIS_num_suf[c]) else: raise ParseError("IBIS numerical suffix", s[ix:], ix) else: res = float(s) return res na = word(string("NA") | string("na")).result(None) @generate("typminmax") def typminmax(): "Parse Typ/Min/Max values." typ = yield number minmax = yield optional(count(number, 2) | count(na, 2).result([]), []) yield ignore # So that ``typminmax`` behaves as a lexeme. res = [typ] res.extend(minmax) return res vi_line = (number + typminmax) << ignore @generate("ratio") def ratio(): "Parse ratio." [num, den] = yield (separated(number, string("/"), 2, maxt=2, end=False) | na.result([0, 0])) if den: return num / den return None ramp_line = string("dV/dt_") >> ((string("r").result("rising") | string("f").result("falling")) << ignore) + times( ratio, 1, 3 ) ex_line = ( word(string("Executable")) >> ( # noqa: W503 ( ((string("L") | string("l")) >> string("inux")).result("linux") | ((string("W") | string("w")) >> string("indows")).result("windows") # noqa: W503 ) << string("_") # noqa: W503 << many(none_of("_")) # noqa: W503 << string("_") # noqa: W503 ) + lexeme(string("32") | string("64")) # noqa: W503 + count(name, 2) # noqa: W503 << ignore # noqa: W503 )
[docs] def manyTrue(p): "Run a parser multiple times, filtering ``False`` results." @generate("manyTrue") def fn(): "many(p) >> filter(True)" nodes = yield many(p) res = list(filter(None, nodes)) return res return fn
[docs] def many1True(p): "Run a parser at least once, filtering ``False`` results." @generate("many1True") def fn(): "many1(p) >> filter(True)" nodes = yield many1(p) res = list(filter(None, nodes)) return res return fn
# IBIS file parser:
[docs] def keyword(kywrd=""): """Parse an IBIS keyword. Keyword Args: kywrd (str): The particular keyword to match; null for any keyword. If provided, *must* be in canonicalized form (i.e. - underscores, no spaces)! Returns: Parser: A keyword parser. """ @generate("IBIS keyword") def fn(): "Parse IBIS keyword." yield regex(r"^\[", re.MULTILINE) wordlets = yield sepBy1(name_only, one_of(" _")) # ``name`` gobbles up trailing space, which we don't want. yield string("]") yield ignore # So that ``keyword`` functions as a lexeme. res = "_".join(wordlets) # Canonicalize to: "<wordlet1>_<wordlet2>_...". if kywrd: if res.lower() == kywrd.lower(): return res return fail_with(f"Expecting: {kywrd}; got: {res}.") return res return fn
@generate("IBIS parameter") def param(): "Parse IBIS parameter." # Parameters must begin with a letter in column 1. pname = yield word(regex(r"^[a-zA-Z]\w*", re.MULTILINE)) if DBG: print(f"Parsing parameter {pname}...", end="") res = yield ((word(string("=")) >> (number | rest_line)) | typminmax | name | rest_line) if DBG: print(res) yield ignore # So that ``param`` functions as a lexeme. return (pname.lower(), res)
[docs] def node(valid_keywords, stop_keywords, debug=False): """Build a node-specific parser. Args: valid_keywords (dict): A dictionary with keys matching those keywords we want parsed. The values are the parsers for those keywords. stop_keywords: Any iterable with primary values (i.e. - those tested by the ``in`` function) matching those keywords we want to stop the parsing of this node and pop us back up the parsing stack. Returns: Parser: A parser for this node. Notes: 1: Any keywords encountered that are _not_ found (via ``in``) in either ``valid_keywords`` or ``stop_keywords`` are ignored. """ @generate("kywrd") def kywrd(): "Parse keyword syntax." nm = yield keyword() nmL = nm.lower() if debug: print(f"Parsing keyword: [{nm}]...") if nmL in valid_keywords: if nmL == "end": # Because ``ibis_file`` expects this to be the last thing it sees, return fail_with("") # we can't consume it here. res = yield logf(valid_keywords[nmL], f"[{nm}]") # Parse the sub-keyword. elif nmL in stop_keywords: return fail_with("") # Stop parsing. else: res = yield skip_keyword yield ignore # So that ``kywrd`` behaves as a lexeme. if debug: print(f"Finished parsing keyword: [{nm}].") return (nmL, res) return kywrd | param
# Individual IBIS keyword (i.e. - "node") parsers: # [End] @generate("[End]") def end(): "Parse [End]." yield keyword("End") return eof # [Model] @generate("[Ramp]") def ramp(): "Parse [Ramp]." # params = yield many(exclude(param, ramp_line)) lines = yield count(ramp_line, 2).desc("Two ramp_lines") return dict(lines) # .update(dict(params)) Model_keywords = { "pulldown": many1(vi_line), "pullup": many1(vi_line), "ramp": ramp, "algorithmic_model": many1(ex_line) << keyword("end_algorithmic_model"), "voltage_range": typminmax, "temperature_range": typminmax, "gnd_clamp": many1(vi_line), "power_clamp": many1(vi_line), } @generate("[Model]") def model(): "Parse [Model]." nm = yield name << ignore if DBG: print(f"Parsing model: {nm}...") res = yield many1(node(Model_keywords, IBIS_keywords, debug=DBG)) if DBG: print(f"[Model] {nm} contains: {dict(res).keys()}") try: theModel = Model(dict(res)) except LookupError as le: return fail_with(f"[Model] {nm}: {str(le)}") except Exception as err: # pylint: disable=broad-exception-caught return fail_with(f"[Model] {nm}: {str(err)}") return {nm: theModel} # [Component] rlc = lexeme(string("R_pin") | string("L_pin") | string("C_pin")) @generate("[Package]") def package(): "Parse package RLC values." rlcs = yield many1(param) if DBG: print(f"rlcs: {rlcs}") return dict(rlcs)
[docs] def pin(rlcs): "Parse indiviual component pin." @generate("Component Pin") def fn(): "Parse an individual component pin." [nm, sig] = yield count(name, 2) mod = yield name_only rem_line = yield rest_line rlc_vals = optional(count(number, 3), []).parse(rem_line) rlc_dict = {} if rlcs: rlc_dict.update(dict(zip(rlcs, rlc_vals))) return ((nm + "(" + sig + ")"), (mod, rlc_dict)) return fn
@generate("[Component].[Pin]") def pins(): "Parse [Component].[Pin]." def filt(x): (_, (mod, _)) = x m = mod.upper() return m not in ("POWER", "GND", "NC") yield (lexeme(string("signal_name")) << lexeme(string("model_name"))) rlcs = yield optional(count(rlc, 3), []) prs = yield many1(pin(rlcs)) prs_filt = list(filter(filt, prs)) return dict(prs_filt) Component_keywords = { "manufacturer": rest_line, "package": package, "pin": pins, "diff_pin": skip_keyword, } @generate("[Component]") def comp(): "Parse [Component]." nm = yield lexeme(name) if DBG: print(f"Parsing component: {nm}") res = yield many1(node(Component_keywords, IBIS_keywords, debug=DBG)) try: Component(dict(res)) except LookupError as le: return fail_with(f"[Component] {nm}: {str(le)}") except Exception as err: # pylint: disable=broad-exception-caught return fail_with(f"[Component] {nm}: {str(err)}") return {nm: Component(dict(res))} @generate("[Model Selector]") def modsel(): "Parse [Model Selector]." nm = yield name res = yield ignore >> many1(name + rest_line) return {nm: res} # Note: The following list MUST have a complete set of keys, # in order for the parsing logic to work correctly! IBIS_keywords = [ "model", "end", "ibis_ver", "comment_char", "file_name", "file_rev", "date", "source", "notes", "disclaimer", "copyright", "component", "model_selector", "submodel", "external_circuit", "test_data", "test_load", "define_package_model", "interconnect_model_set", ] IBIS_kywrd_parsers = dict(zip(IBIS_keywords, [skip_keyword] * len(IBIS_keywords))) IBIS_kywrd_parsers.update( { "model": model, "end": end, "ibis_ver": lexeme(number), "file_name": lexeme(name), "file_rev": lexeme(name), "date": rest_line, "component": comp, "model_selector": modsel, } ) @generate("IBIS File") def ibis_file(): "Parse IBIS file." res = yield ignore >> many1True(node(IBIS_kywrd_parsers, {}, debug=DBG)) << end return res
[docs] def parse_ibis_file(ibis_file_contents_str, debug=False): """Parse the contents of an IBIS file. Args: ibis_file_contents_str (str): The contents of the IBIS file, as a single string. Keyword Args: debug (bool): Output debugging info to console when true. Default = False Example: :: with open(<ibis_file_name>) as ibis_file: ibis_file_contents_str = ibis_file.read() (err_str, model_dict) = parse_ibis_file(ibis_file_contents_str) Returns: (str, dict): A pair containing: err_str: A message describing the nature of any parse failure that occured. model_dict: Dictionary containing keyword definitions (empty upon failure). """ try: nodes = ibis_file.parse_strict(ibis_file_contents_str) # Parse must consume the entire file. if debug: print("Parsed nodes:\n", nodes) except ParseError as pe: return str(pe), {} kw_dict = {} components = {} models = {} model_selectors = {} for kw, val in nodes: if kw == "model": models.update(val) elif kw == "component": components.update(val) elif kw == "model_selector": model_selectors.update(val) else: kw_dict.update({kw: val}) kw_dict.update( { "components": components, "models": models, "model_selectors": model_selectors, } ) return "Success!", kw_dict