pyibis-ami 7.2.3__py3-none-any.whl → 7.2.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pyibisami/ibis/parser.py CHANGED
@@ -1,525 +1,525 @@
1
- """Parse an IBIS model file.
2
-
3
- Original Author: David Banas <capn.freako@gmail.com>
4
-
5
- Original Date: November 1, 2019
6
-
7
- For information regarding the IBIS modeling standard, visit:
8
- https://ibis.org/
9
-
10
- Copyright (c) 2019 by David Banas; All rights reserved World wide.
11
- """
12
-
13
- import re
14
-
15
- from parsec import (
16
- ParseError,
17
- Parser,
18
- count,
19
- eof,
20
- fail_with,
21
- generate,
22
- letter,
23
- many,
24
- many1,
25
- none_of,
26
- one_of,
27
- optional,
28
- regex,
29
- separated,
30
- sepBy1,
31
- string,
32
- times,
33
- )
34
-
35
- from pyibisami.ibis.model import Component, Model
36
-
37
- DEBUG = False
38
-
39
- # Parser Definitions
40
-
41
- whitespace = regex(r"\s+", re.MULTILINE)
42
- comment = regex(r"\|.*") # To end of line only.
43
- ignore = many(whitespace | comment) # None is okay; so, can be used completely safely.
44
-
45
-
46
- def logf(p: Parser, preStr: str = "") -> Parser:
47
- """
48
- Returns parser ``p`` wrapped in a thin shell, which logs any failure at the point of occurence.
49
-
50
- Args:
51
- p: The original parser.
52
-
53
- Keyword Args:
54
- preStr: A prefix string to use in failure message.
55
- (Default = <empty string>)
56
-
57
- Returns:
58
- p': The original parser wrapped in a thin failure location shell.
59
- """
60
-
61
- @Parser
62
- def fn(txt, ix):
63
- res = p(txt, ix)
64
- if not res.status:
65
- print(
66
- f"{preStr}: Expected `{res.expected}` in `{txt[res.index: res.index + 5]}` at {ParseError.loc_info(txt, res.index)}.",
67
- flush=True
68
- )
69
- return res
70
-
71
- return fn
72
-
73
-
74
- def lexeme(p):
75
- """Lexer for words.
76
-
77
- Skips all ignored characters after word, including newlines.
78
- """
79
- return p << ignore
80
-
81
-
82
- def word(p):
83
- """Line limited word lexer.
84
-
85
- Only skips space after words; dosen't skip comments or newlines.
86
- Requires, at least, one white space character after word.
87
- """
88
- return p << regex(r"\s+")
89
-
90
-
91
- @generate("remainder of line")
92
- def rest_line():
93
- "Parse remainder of line."
94
- chars = yield many(none_of("\n\r")) << ignore # So that we still function as a lexeme.
95
- return "".join(chars)
96
-
97
-
98
- skip_line = lexeme(rest_line).result("(Skipped.)")
99
- name_only = regex(r"[_a-zA-Z0-9/\.()#-]+")
100
- name = word(name_only)
101
- symbol = lexeme(regex(r"[a-zA-Z_][^\s()\[\]]*"))
102
- true = lexeme(string("True")).result(True)
103
- false = lexeme(string("False")).result(False)
104
- quoted_string = lexeme(regex(r'"[^"]*"'))
105
- skip_keyword = (skip_line >> many(none_of("[") >> skip_line)).result(
106
- "(Skipped.)"
107
- ) # Skip over everything until the next keyword begins.
108
-
109
- IBIS_num_suf = {
110
- "T": "e12",
111
- "k": "e3",
112
- "n": "e-9",
113
- "G": "e9",
114
- "m": "e-3",
115
- "p": "e-12",
116
- "M": "e6",
117
- "u": "e-6",
118
- "f": "e-15",
119
- }
120
-
121
-
122
- @generate("number")
123
- def number():
124
- "Parse an IBIS numerical value."
125
- s = yield (regex(r"[-+]?[0-9]*\.?[0-9]+(([eE][-+]?[0-9]+)|([TknGmpMuf][a-zA-Z]*))?") << many(letter()) << ignore)
126
- m = re.search(r"[^\d]+$", s)
127
- if m:
128
- ix = m.start()
129
- c = s[ix]
130
- if c in IBIS_num_suf:
131
- res = float(s[:ix] + IBIS_num_suf[c])
132
- else:
133
- raise ParseError("IBIS numerical suffix", s[ix:], ix)
134
- else:
135
- res = float(s)
136
- return res
137
-
138
-
139
- na = word(string("NA") | string("na")).result(None)
140
-
141
-
142
- @generate("typminmax")
143
- def typminmax():
144
- "Parse Typ/Min/Max values."
145
- typ = yield number
146
- minmax = yield optional(count(number, 2) | count(na, 2).result([]), [])
147
- yield ignore # So that ``typminmax`` behaves as a lexeme.
148
- res = [typ]
149
- res.extend(minmax)
150
- return res
151
-
152
-
153
- vi_line = (number + typminmax) << ignore
154
-
155
-
156
- @generate("ratio")
157
- def ratio():
158
- "Parse ratio."
159
- [num, den] = yield (separated(number, string("/"), 2, maxt=2, end=False) | na.result([0, 0]))
160
- if den:
161
- return num / den
162
- return None
163
-
164
-
165
- ramp_line = string("dV/dt_") >> ((string("r").result("rising") | string("f").result("falling")) << ignore) + times(
166
- ratio, 1, 3
167
- )
168
- ex_line = (
169
- word(string("Executable"))
170
- >> ( # noqa: W503
171
- (
172
- ((string("L") | string("l")) >> string("inux")).result("linux")
173
- | ((string("W") | string("w")) >> string("indows")).result("windows") # noqa: W503
174
- )
175
- << string("_") # noqa: W503
176
- << many(none_of("_")) # noqa: W503
177
- << string("_") # noqa: W503
178
- )
179
- + lexeme(string("32") | string("64")) # noqa: W503
180
- + count(name, 2) # noqa: W503
181
- << ignore # noqa: W503
182
- )
183
-
184
-
185
- def manyTrue(p):
186
- "Run a parser multiple times, filtering ``False`` results."
187
-
188
- @generate("manyTrue")
189
- def fn():
190
- "many(p) >> filter(True)"
191
- nodes = yield many(p)
192
- res = list(filter(None, nodes))
193
- return res
194
-
195
- return fn
196
-
197
-
198
- def many1True(p):
199
- "Run a parser at least once, filtering ``False`` results."
200
-
201
- @generate("many1True")
202
- def fn():
203
- "many1(p) >> filter(True)"
204
- nodes = yield many1(p)
205
- res = list(filter(None, nodes))
206
- return res
207
-
208
- return fn
209
-
210
-
211
- # IBIS file parser:
212
-
213
-
214
- def keyword(kywrd=""):
215
- """Parse an IBIS keyword.
216
-
217
- Keyword Args:
218
- kywrd (str): The particular keyword to match; null for any keyword.
219
- If provided, *must* be in canonicalized form (i.e. - underscores,
220
- no spaces)!
221
-
222
- Returns:
223
- Parser: A keyword parser.
224
- """
225
-
226
- @generate("IBIS keyword")
227
- def fn():
228
- "Parse IBIS keyword."
229
- yield regex(r"^\[", re.MULTILINE)
230
- wordlets = yield sepBy1(name_only, one_of(" _")) # ``name`` gobbles up trailing space, which we don't want.
231
- yield string("]")
232
- yield ignore # So that ``keyword`` functions as a lexeme.
233
- res = "_".join(wordlets) # Canonicalize to: "<wordlet1>_<wordlet2>_...".
234
- if kywrd:
235
- if res.lower() == kywrd.lower():
236
- return res
237
- return fail_with(f"Expecting: {kywrd}; got: {res}.")
238
- return res
239
-
240
- return fn
241
-
242
-
243
- @generate("IBIS parameter")
244
- def param():
245
- "Parse IBIS parameter."
246
- # Parameters must begin with a letter in column 1.
247
- pname = yield word(regex(r"^[a-zA-Z]\w*", re.MULTILINE))
248
- if DEBUG:
249
- print(f"Parsing parameter {pname}...", end="", flush=True)
250
- res = yield ((word(string("=")) >> (number | rest_line)) | typminmax | name | rest_line)
251
- if DEBUG:
252
- print(res, flush=True)
253
- yield ignore # So that ``param`` functions as a lexeme.
254
- return (pname.lower(), res)
255
-
256
-
257
- def node(valid_keywords, stop_keywords, debug=False):
258
- """Build a node-specific parser.
259
-
260
- Args:
261
- valid_keywords (dict): A dictionary with keys matching those
262
- keywords we want parsed. The values are the parsers for
263
- those keywords.
264
- stop_keywords: Any iterable with primary values (i.e. - those
265
- tested by the ``in`` function) matching those keywords we want
266
- to stop the parsing of this node and pop us back up the
267
- parsing stack.
268
-
269
- Returns:
270
- Parser: A parser for this node.
271
-
272
- Notes:
273
- 1: Any keywords encountered that are _not_ found (via ``in``) in
274
- either ``valid_keywords`` or ``stop_keywords`` are ignored.
275
- """
276
-
277
- @generate("kywrd")
278
- def kywrd():
279
- "Parse keyword syntax."
280
- nm = yield keyword()
281
- nmL = nm.lower()
282
- if debug:
283
- print(f"Parsing keyword: [{nm}]...", flush=True)
284
- if nmL in valid_keywords:
285
- if nmL == "end": # Because ``ibis_file`` expects this to be the last thing it sees,
286
- return fail_with("") # we can't consume it here.
287
- res = yield logf(valid_keywords[nmL], f"[{nm}]") # Parse the sub-keyword.
288
- elif nmL in stop_keywords:
289
- return fail_with("") # Stop parsing.
290
- else:
291
- res = yield skip_keyword
292
- yield ignore # So that ``kywrd`` behaves as a lexeme.
293
- if debug:
294
- print(f"Finished parsing keyword: [{nm}].", flush=True)
295
- return (nmL, res)
296
-
297
- return kywrd | param
298
-
299
-
300
- # Individual IBIS keyword (i.e. - "node") parsers:
301
-
302
-
303
- # [End]
304
- @generate("[End]")
305
- def end():
306
- "Parse [End]."
307
- yield keyword("End")
308
- return eof
309
-
310
-
311
- # [Model]
312
- @generate("[Ramp]")
313
- def ramp():
314
- "Parse [Ramp]."
315
- lines = yield count(ramp_line, 2).desc("Two ramp_lines")
316
- return dict(lines) # .update(dict(params))
317
-
318
-
319
- Model_keywords = {
320
- "pulldown": many1(vi_line),
321
- "pullup": many1(vi_line),
322
- "ramp": ramp,
323
- "algorithmic_model": many1(ex_line) << keyword("end_algorithmic_model"),
324
- "voltage_range": typminmax,
325
- "temperature_range": typminmax,
326
- "gnd_clamp": many1(vi_line),
327
- "power_clamp": many1(vi_line),
328
- }
329
-
330
-
331
- @generate("[Model]")
332
- def model():
333
- "Parse [Model]."
334
- nm = yield name << ignore
335
- if DEBUG:
336
- print(f"Parsing model: {nm}...", flush=True)
337
- res = yield many1(node(Model_keywords, IBIS_keywords, debug=DEBUG))
338
- if DEBUG:
339
- print(f"[Model] {nm} contains: {dict(res).keys()}", flush=True)
340
- try:
341
- theModel = Model(dict(res))
342
- except LookupError as le:
343
- return fail_with(f"[Model] {nm}: {str(le)}")
344
- return {nm: theModel}
345
-
346
-
347
- # [Component]
348
- rlc = lexeme(string("R_pin") | string("L_pin") | string("C_pin"))
349
-
350
-
351
- @generate("[Package]")
352
- def package():
353
- "Parse package RLC values."
354
- rlcs = yield many1(param)
355
- if DEBUG:
356
- print(f"rlcs: {rlcs}", flush=True)
357
- return dict(rlcs)
358
-
359
-
360
- def pin(rlcs):
361
- "Parse indiviual component pin."
362
-
363
- @generate("Component Pin")
364
- def fn():
365
- "Parse an individual component pin."
366
- [nm, sig] = yield count(name, 2)
367
- mod = yield name_only
368
- rem_line = yield rest_line
369
- rlc_vals = optional(count(number, 3), []).parse(rem_line)
370
- rlc_dict = {}
371
- if rlcs:
372
- rlc_dict.update(dict(zip(rlcs, rlc_vals)))
373
- return ((nm + "(" + sig + ")"), (mod, rlc_dict))
374
-
375
- return fn
376
-
377
-
378
- @generate("[Component].[Pin]")
379
- def pins():
380
- "Parse [Component].[Pin]."
381
-
382
- def filt(x):
383
- (_, (mod, _)) = x
384
- m = mod.upper()
385
- return m not in ("POWER", "GND", "NC")
386
-
387
- yield (lexeme(string("signal_name")) << lexeme(string("model_name")))
388
- rlcs = yield optional(count(rlc, 3), [])
389
- prs = yield many1(pin(rlcs))
390
- prs_filt = list(filter(filt, prs))
391
- return dict(prs_filt)
392
-
393
-
394
- Component_keywords = {
395
- "manufacturer": rest_line,
396
- "package": package,
397
- "pin": pins,
398
- "diff_pin": skip_keyword,
399
- }
400
-
401
-
402
- @generate("[Component]")
403
- def comp():
404
- "Parse [Component]."
405
- nm = yield lexeme(name)
406
- if DEBUG:
407
- print(f"Parsing component: {nm}", flush=True)
408
- res = yield many1(node(Component_keywords, IBIS_keywords, debug=DEBUG))
409
- try:
410
- Component(dict(res))
411
- except LookupError as le:
412
- return fail_with(f"[Component] {nm}: {str(le)}")
413
- except Exception as err: # pylint: disable=broad-exception-caught
414
- return fail_with(f"[Component] {nm}: {str(err)}")
415
- return {nm: Component(dict(res))}
416
-
417
-
418
- @generate("[Model Selector]")
419
- def modsel():
420
- "Parse [Model Selector]."
421
- nm = yield name
422
- res = yield ignore >> many1(name + rest_line)
423
- return {nm: res}
424
-
425
-
426
- # Note: The following list MUST have a complete set of keys,
427
- # in order for the parsing logic to work correctly!
428
- IBIS_keywords = [
429
- "model",
430
- "end",
431
- "ibis_ver",
432
- "comment_char",
433
- "file_name",
434
- "file_rev",
435
- "date",
436
- "source",
437
- "notes",
438
- "disclaimer",
439
- "copyright",
440
- "component",
441
- "model_selector",
442
- "submodel",
443
- "external_circuit",
444
- "test_data",
445
- "test_load",
446
- "define_package_model",
447
- "interconnect_model_set",
448
- ]
449
-
450
- IBIS_kywrd_parsers = dict(zip(IBIS_keywords, [skip_keyword] * len(IBIS_keywords)))
451
- IBIS_kywrd_parsers.update(
452
- {
453
- "model": model,
454
- "end": end,
455
- "ibis_ver": lexeme(number),
456
- "file_name": lexeme(name),
457
- "file_rev": lexeme(name),
458
- "date": rest_line,
459
- "component": comp,
460
- "model_selector": modsel,
461
- }
462
- )
463
-
464
-
465
- @generate("IBIS File")
466
- def ibis_file():
467
- "Parse IBIS file."
468
- res = yield ignore >> many1True(node(IBIS_kywrd_parsers, {}, debug=DEBUG)) << end
469
- return res
470
-
471
-
472
- def parse_ibis_file(ibis_file_contents_str, debug=False):
473
- """Parse the contents of an IBIS file.
474
-
475
- Args:
476
- ibis_file_contents_str (str): The contents of the IBIS file, as a single string.
477
-
478
- Keyword Args:
479
- debug (bool): Output debugging info to console when true.
480
- Default = False
481
-
482
- Example:
483
- ::
484
-
485
- with open(<ibis_file_name>) as ibis_file:
486
- ibis_file_contents_str = ibis_file.read()
487
- (err_str, model_dict) = parse_ibis_file(ibis_file_contents_str)
488
-
489
- Returns:
490
- (str, dict): A pair containing:
491
-
492
- err_str:
493
- A message describing the nature of any parse failure that occured.
494
- model_dict:
495
- Dictionary containing keyword definitions (empty upon failure).
496
- """
497
-
498
- try:
499
- nodes = ibis_file.parse_strict(ibis_file_contents_str) # Parse must consume the entire file.
500
- if debug:
501
- print("Parsed nodes:\n", nodes, flush=True)
502
- except ParseError as pe:
503
- return str(pe), {}
504
-
505
- kw_dict = {}
506
- components = {}
507
- models = {}
508
- model_selectors = {}
509
- for kw, val in nodes:
510
- if kw == "model":
511
- models.update(val)
512
- elif kw == "component":
513
- components.update(val)
514
- elif kw == "model_selector":
515
- model_selectors.update(val)
516
- else:
517
- kw_dict.update({kw: val})
518
- kw_dict.update(
519
- {
520
- "components": components,
521
- "models": models,
522
- "model_selectors": model_selectors,
523
- }
524
- )
525
- return "Success!", kw_dict
1
+ """Parse an IBIS model file.
2
+
3
+ Original Author: David Banas <capn.freako@gmail.com>
4
+
5
+ Original Date: November 1, 2019
6
+
7
+ For information regarding the IBIS modeling standard, visit:
8
+ https://ibis.org/
9
+
10
+ Copyright (c) 2019 by David Banas; All rights reserved World wide.
11
+ """
12
+
13
+ import re
14
+
15
+ from parsec import (
16
+ ParseError,
17
+ Parser,
18
+ count,
19
+ eof,
20
+ fail_with,
21
+ generate,
22
+ letter,
23
+ many,
24
+ many1,
25
+ none_of,
26
+ one_of,
27
+ optional,
28
+ regex,
29
+ separated,
30
+ sepBy1,
31
+ string,
32
+ times,
33
+ )
34
+
35
+ from pyibisami.ibis.model import Component, Model
36
+
37
+ DEBUG = False
38
+
39
+ # Parser Definitions
40
+
41
+ whitespace = regex(r"\s+", re.MULTILINE)
42
+ comment = regex(r"\|.*") # To end of line only.
43
+ ignore = many(whitespace | comment) # None is okay; so, can be used completely safely.
44
+
45
+
46
+ def logf(p: Parser, preStr: str = "") -> Parser:
47
+ """
48
+ Returns parser ``p`` wrapped in a thin shell, which logs any failure at the point of occurence.
49
+
50
+ Args:
51
+ p: The original parser.
52
+
53
+ Keyword Args:
54
+ preStr: A prefix string to use in failure message.
55
+ (Default = <empty string>)
56
+
57
+ Returns:
58
+ p': The original parser wrapped in a thin failure location shell.
59
+ """
60
+
61
+ @Parser
62
+ def fn(txt, ix):
63
+ res = p(txt, ix)
64
+ if not res.status:
65
+ print(
66
+ f"{preStr}: Expected `{res.expected}` in `{txt[res.index: res.index + 5]}` at {ParseError.loc_info(txt, res.index)}.",
67
+ flush=True
68
+ )
69
+ return res
70
+
71
+ return fn
72
+
73
+
74
+ def lexeme(p):
75
+ """Lexer for words.
76
+
77
+ Skips all ignored characters after word, including newlines.
78
+ """
79
+ return p << ignore
80
+
81
+
82
+ def word(p):
83
+ """Line limited word lexer.
84
+
85
+ Only skips space after words; dosen't skip comments or newlines.
86
+ Requires, at least, one white space character after word.
87
+ """
88
+ return p << regex(r"\s+")
89
+
90
+
91
+ @generate("remainder of line")
92
+ def rest_line():
93
+ "Parse remainder of line."
94
+ chars = yield many(none_of("\n\r")) << ignore # So that we still function as a lexeme.
95
+ return "".join(chars)
96
+
97
+
98
+ skip_line = lexeme(rest_line).result("(Skipped.)")
99
+ name_only = regex(r"[_a-zA-Z0-9/\.()#-]+")
100
+ name = word(name_only)
101
+ symbol = lexeme(regex(r"[a-zA-Z_][^\s()\[\]]*"))
102
+ true = lexeme(string("True")).result(True)
103
+ false = lexeme(string("False")).result(False)
104
+ quoted_string = lexeme(regex(r'"[^"]*"'))
105
+ skip_keyword = (skip_line >> many(none_of("[") >> skip_line)).result(
106
+ "(Skipped.)"
107
+ ) # Skip over everything until the next keyword begins.
108
+
109
+ IBIS_num_suf = {
110
+ "T": "e12",
111
+ "k": "e3",
112
+ "n": "e-9",
113
+ "G": "e9",
114
+ "m": "e-3",
115
+ "p": "e-12",
116
+ "M": "e6",
117
+ "u": "e-6",
118
+ "f": "e-15",
119
+ }
120
+
121
+
122
+ @generate("number")
123
+ def number():
124
+ "Parse an IBIS numerical value."
125
+ s = yield (regex(r"[-+]?[0-9]*\.?[0-9]+(([eE][-+]?[0-9]+)|([TknGmpMuf][a-zA-Z]*))?") << many(letter()) << ignore)
126
+ m = re.search(r"[^\d]+$", s)
127
+ if m:
128
+ ix = m.start()
129
+ c = s[ix]
130
+ if c in IBIS_num_suf:
131
+ res = float(s[:ix] + IBIS_num_suf[c])
132
+ else:
133
+ raise ParseError("IBIS numerical suffix", s[ix:], ix)
134
+ else:
135
+ res = float(s)
136
+ return res
137
+
138
+
139
+ na = word(string("NA") | string("na")).result(None)
140
+
141
+
142
+ @generate("typminmax")
143
+ def typminmax():
144
+ "Parse Typ/Min/Max values."
145
+ typ = yield number
146
+ minmax = yield optional(count(number, 2) | count(na, 2).result([]), [])
147
+ yield ignore # So that ``typminmax`` behaves as a lexeme.
148
+ res = [typ]
149
+ res.extend(minmax)
150
+ return res
151
+
152
+
153
+ vi_line = (number + typminmax) << ignore
154
+
155
+
156
+ @generate("ratio")
157
+ def ratio():
158
+ "Parse ratio."
159
+ [num, den] = yield (separated(number, string("/"), 2, maxt=2, end=False) | na.result([0, 0]))
160
+ if den:
161
+ return num / den
162
+ return None
163
+
164
+
165
+ ramp_line = string("dV/dt_") >> ((string("r").result("rising") | string("f").result("falling")) << ignore) + times(
166
+ ratio, 1, 3
167
+ )
168
+ ex_line = (
169
+ word(string("Executable"))
170
+ >> ( # noqa: W503
171
+ (
172
+ ((string("L") | string("l")) >> string("inux")).result("linux")
173
+ | ((string("W") | string("w")) >> string("indows")).result("windows") # noqa: W503
174
+ )
175
+ << string("_") # noqa: W503
176
+ << many(none_of("_")) # noqa: W503
177
+ << string("_") # noqa: W503
178
+ )
179
+ + lexeme(string("32") | string("64")) # noqa: W503
180
+ + count(name, 2) # noqa: W503
181
+ << ignore # noqa: W503
182
+ )
183
+
184
+
185
+ def manyTrue(p):
186
+ "Run a parser multiple times, filtering ``False`` results."
187
+
188
+ @generate("manyTrue")
189
+ def fn():
190
+ "many(p) >> filter(True)"
191
+ nodes = yield many(p)
192
+ res = list(filter(None, nodes))
193
+ return res
194
+
195
+ return fn
196
+
197
+
198
+ def many1True(p):
199
+ "Run a parser at least once, filtering ``False`` results."
200
+
201
+ @generate("many1True")
202
+ def fn():
203
+ "many1(p) >> filter(True)"
204
+ nodes = yield many1(p)
205
+ res = list(filter(None, nodes))
206
+ return res
207
+
208
+ return fn
209
+
210
+
211
+ # IBIS file parser:
212
+
213
+
214
+ def keyword(kywrd=""):
215
+ """Parse an IBIS keyword.
216
+
217
+ Keyword Args:
218
+ kywrd (str): The particular keyword to match; null for any keyword.
219
+ If provided, *must* be in canonicalized form (i.e. - underscores,
220
+ no spaces)!
221
+
222
+ Returns:
223
+ Parser: A keyword parser.
224
+ """
225
+
226
+ @generate("IBIS keyword")
227
+ def fn():
228
+ "Parse IBIS keyword."
229
+ yield regex(r"^\[", re.MULTILINE)
230
+ wordlets = yield sepBy1(name_only, one_of(" _")) # ``name`` gobbles up trailing space, which we don't want.
231
+ yield string("]")
232
+ yield ignore # So that ``keyword`` functions as a lexeme.
233
+ res = "_".join(wordlets) # Canonicalize to: "<wordlet1>_<wordlet2>_...".
234
+ if kywrd:
235
+ if res.lower() == kywrd.lower():
236
+ return res
237
+ return fail_with(f"Expecting: {kywrd}; got: {res}.")
238
+ return res
239
+
240
+ return fn
241
+
242
+
243
+ @generate("IBIS parameter")
244
+ def param():
245
+ "Parse IBIS parameter."
246
+ # Parameters must begin with a letter in column 1.
247
+ pname = yield word(regex(r"^[a-zA-Z]\w*", re.MULTILINE))
248
+ if DEBUG:
249
+ print(f"Parsing parameter {pname}...", end="", flush=True)
250
+ res = yield ((word(string("=")) >> (number | rest_line)) | typminmax | name | rest_line)
251
+ if DEBUG:
252
+ print(res, flush=True)
253
+ yield ignore # So that ``param`` functions as a lexeme.
254
+ return (pname.lower(), res)
255
+
256
+
257
+ def node(valid_keywords, stop_keywords, debug=False):
258
+ """Build a node-specific parser.
259
+
260
+ Args:
261
+ valid_keywords (dict): A dictionary with keys matching those
262
+ keywords we want parsed. The values are the parsers for
263
+ those keywords.
264
+ stop_keywords: Any iterable with primary values (i.e. - those
265
+ tested by the ``in`` function) matching those keywords we want
266
+ to stop the parsing of this node and pop us back up the
267
+ parsing stack.
268
+
269
+ Returns:
270
+ Parser: A parser for this node.
271
+
272
+ Notes:
273
+ 1: Any keywords encountered that are _not_ found (via ``in``) in
274
+ either ``valid_keywords`` or ``stop_keywords`` are ignored.
275
+ """
276
+
277
+ @generate("kywrd")
278
+ def kywrd():
279
+ "Parse keyword syntax."
280
+ nm = yield keyword()
281
+ nmL = nm.lower()
282
+ if debug:
283
+ print(f"Parsing keyword: [{nm}]...", flush=True)
284
+ if nmL in valid_keywords:
285
+ if nmL == "end": # Because ``ibis_file`` expects this to be the last thing it sees,
286
+ return fail_with("") # we can't consume it here.
287
+ res = yield logf(valid_keywords[nmL], f"[{nm}]") # Parse the sub-keyword.
288
+ elif nmL in stop_keywords:
289
+ return fail_with("") # Stop parsing.
290
+ else:
291
+ res = yield skip_keyword
292
+ yield ignore # So that ``kywrd`` behaves as a lexeme.
293
+ if debug:
294
+ print(f"Finished parsing keyword: [{nm}].", flush=True)
295
+ return (nmL, res)
296
+
297
+ return kywrd | param
298
+
299
+
300
+ # Individual IBIS keyword (i.e. - "node") parsers:
301
+
302
+
303
+ # [End]
304
+ @generate("[End]")
305
+ def end():
306
+ "Parse [End]."
307
+ yield keyword("End")
308
+ return eof
309
+
310
+
311
+ # [Model]
312
+ @generate("[Ramp]")
313
+ def ramp():
314
+ "Parse [Ramp]."
315
+ lines = yield count(ramp_line, 2).desc("Two ramp_lines")
316
+ return dict(lines) # .update(dict(params))
317
+
318
+
319
+ Model_keywords = {
320
+ "pulldown": many1(vi_line),
321
+ "pullup": many1(vi_line),
322
+ "ramp": ramp,
323
+ "algorithmic_model": many1(ex_line) << keyword("end_algorithmic_model"),
324
+ "voltage_range": typminmax,
325
+ "temperature_range": typminmax,
326
+ "gnd_clamp": many1(vi_line),
327
+ "power_clamp": many1(vi_line),
328
+ }
329
+
330
+
331
+ @generate("[Model]")
332
+ def model():
333
+ "Parse [Model]."
334
+ nm = yield name << ignore
335
+ if DEBUG:
336
+ print(f"Parsing model: {nm}...", flush=True)
337
+ res = yield many1(node(Model_keywords, IBIS_keywords, debug=DEBUG))
338
+ if DEBUG:
339
+ print(f"[Model] {nm} contains: {dict(res).keys()}", flush=True)
340
+ try:
341
+ theModel = Model(dict(res))
342
+ except LookupError as le:
343
+ return fail_with(f"[Model] {nm}: {str(le)}")
344
+ return {nm: theModel}
345
+
346
+
347
+ # [Component]
348
+ rlc = lexeme(string("R_pin") | string("L_pin") | string("C_pin"))
349
+
350
+
351
+ @generate("[Package]")
352
+ def package():
353
+ "Parse package RLC values."
354
+ rlcs = yield many1(param)
355
+ if DEBUG:
356
+ print(f"rlcs: {rlcs}", flush=True)
357
+ return dict(rlcs)
358
+
359
+
360
+ def pin(rlcs):
361
+ "Parse indiviual component pin."
362
+
363
+ @generate("Component Pin")
364
+ def fn():
365
+ "Parse an individual component pin."
366
+ [nm, sig] = yield count(name, 2)
367
+ mod = yield name_only
368
+ rem_line = yield rest_line
369
+ rlc_vals = optional(count(number, 3), []).parse(rem_line)
370
+ rlc_dict = {}
371
+ if rlcs:
372
+ rlc_dict.update(dict(zip(rlcs, rlc_vals)))
373
+ return ((nm + "(" + sig + ")"), (mod, rlc_dict))
374
+
375
+ return fn
376
+
377
+
378
+ @generate("[Component].[Pin]")
379
+ def pins():
380
+ "Parse [Component].[Pin]."
381
+
382
+ def filt(x):
383
+ (_, (mod, _)) = x
384
+ m = mod.upper()
385
+ return m not in ("POWER", "GND", "NC")
386
+
387
+ yield (lexeme(string("signal_name")) << lexeme(string("model_name")))
388
+ rlcs = yield optional(count(rlc, 3), [])
389
+ prs = yield many1(pin(rlcs))
390
+ prs_filt = list(filter(filt, prs))
391
+ return dict(prs_filt)
392
+
393
+
394
+ Component_keywords = {
395
+ "manufacturer": rest_line,
396
+ "package": package,
397
+ "pin": pins,
398
+ "diff_pin": skip_keyword,
399
+ }
400
+
401
+
402
+ @generate("[Component]")
403
+ def comp():
404
+ "Parse [Component]."
405
+ nm = yield lexeme(name)
406
+ if DEBUG:
407
+ print(f"Parsing component: {nm}", flush=True)
408
+ res = yield many1(node(Component_keywords, IBIS_keywords, debug=DEBUG))
409
+ try:
410
+ Component(dict(res))
411
+ except LookupError as le:
412
+ return fail_with(f"[Component] {nm}: {str(le)}")
413
+ except Exception as err: # pylint: disable=broad-exception-caught
414
+ return fail_with(f"[Component] {nm}: {str(err)}")
415
+ return {nm: Component(dict(res))}
416
+
417
+
418
+ @generate("[Model Selector]")
419
+ def modsel():
420
+ "Parse [Model Selector]."
421
+ nm = yield name
422
+ res = yield ignore >> many1(name + rest_line)
423
+ return {nm: res}
424
+
425
+
426
+ # Note: The following list MUST have a complete set of keys,
427
+ # in order for the parsing logic to work correctly!
428
+ IBIS_keywords = [
429
+ "model",
430
+ "end",
431
+ "ibis_ver",
432
+ "comment_char",
433
+ "file_name",
434
+ "file_rev",
435
+ "date",
436
+ "source",
437
+ "notes",
438
+ "disclaimer",
439
+ "copyright",
440
+ "component",
441
+ "model_selector",
442
+ "submodel",
443
+ "external_circuit",
444
+ "test_data",
445
+ "test_load",
446
+ "define_package_model",
447
+ "interconnect_model_set",
448
+ ]
449
+
450
+ IBIS_kywrd_parsers = dict(zip(IBIS_keywords, [skip_keyword] * len(IBIS_keywords)))
451
+ IBIS_kywrd_parsers.update(
452
+ {
453
+ "model": model,
454
+ "end": end,
455
+ "ibis_ver": lexeme(number),
456
+ "file_name": lexeme(name),
457
+ "file_rev": lexeme(name),
458
+ "date": rest_line,
459
+ "component": comp,
460
+ "model_selector": modsel,
461
+ }
462
+ )
463
+
464
+
465
+ @generate("IBIS File")
466
+ def ibis_file():
467
+ "Parse IBIS file."
468
+ res = yield ignore >> many1True(node(IBIS_kywrd_parsers, {}, debug=DEBUG)) << end
469
+ return res
470
+
471
+
472
+ def parse_ibis_file(ibis_file_contents_str, debug=False):
473
+ """Parse the contents of an IBIS file.
474
+
475
+ Args:
476
+ ibis_file_contents_str (str): The contents of the IBIS file, as a single string.
477
+
478
+ Keyword Args:
479
+ debug (bool): Output debugging info to console when true.
480
+ Default = False
481
+
482
+ Example:
483
+ ::
484
+
485
+ with open(<ibis_file_name>) as ibis_file:
486
+ ibis_file_contents_str = ibis_file.read()
487
+ (err_str, model_dict) = parse_ibis_file(ibis_file_contents_str)
488
+
489
+ Returns:
490
+ (str, dict): A pair containing:
491
+
492
+ err_str:
493
+ A message describing the nature of any parse failure that occured.
494
+ model_dict:
495
+ Dictionary containing keyword definitions (empty upon failure).
496
+ """
497
+
498
+ try:
499
+ nodes = ibis_file.parse_strict(ibis_file_contents_str) # Parse must consume the entire file.
500
+ if debug:
501
+ print("Parsed nodes:\n", nodes, flush=True)
502
+ except ParseError as pe:
503
+ return str(pe), {}
504
+
505
+ kw_dict = {}
506
+ components = {}
507
+ models = {}
508
+ model_selectors = {}
509
+ for kw, val in nodes:
510
+ if kw == "model":
511
+ models.update(val)
512
+ elif kw == "component":
513
+ components.update(val)
514
+ elif kw == "model_selector":
515
+ model_selectors.update(val)
516
+ else:
517
+ kw_dict.update({kw: val})
518
+ kw_dict.update(
519
+ {
520
+ "components": components,
521
+ "models": models,
522
+ "model_selectors": model_selectors,
523
+ }
524
+ )
525
+ return "Success!", kw_dict