telemetry-cli 1.0.0__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.
- telemetry_cli/__init__.py +8 -0
- telemetry_cli/_cli.py +18 -0
- telemetry_cli/_getopt.py +114 -0
- telemetry_cli/_msg.py +39 -0
- telemetry_cli/_pack.py +183 -0
- telemetry_cli/_perl.py +183 -0
- telemetry_cli/pick/__init__.py +1 -0
- telemetry_cli/pick/__main__.py +7 -0
- telemetry_cli/pick/cli.py +157 -0
- telemetry_cli/pick/commandline.py +219 -0
- telemetry_cli/pick/fields.py +166 -0
- telemetry_cli/pick/formats.py +191 -0
- telemetry_cli/pick/help.py +536 -0
- telemetry_cli/pick/symbols/airmoc +24 -0
- telemetry_cli/pick/symbols/aux +28 -0
- telemetry_cli/pick/symbols/corr +31 -0
- telemetry_cli/pick/symbols/corrOld +29 -0
- telemetry_cli/pick/symbols/hw +16 -0
- telemetry_cli/pick/symbols/moc +42 -0
- telemetry_cli/pick/symbols/subcom +83 -0
- telemetry_cli/pick/symbols/subcom_2002 +111 -0
- telemetry_cli/recl/__init__.py +1 -0
- telemetry_cli/recl/__main__.py +7 -0
- telemetry_cli/recl/cli.py +214 -0
- telemetry_cli/recl/corr.py +83 -0
- telemetry_cli/recs/__init__.py +1 -0
- telemetry_cli/recs/__main__.py +7 -0
- telemetry_cli/recs/cli.py +182 -0
- telemetry_cli/recs/extract.py +399 -0
- telemetry_cli/tgen/__init__.py +1 -0
- telemetry_cli/tgen/__main__.py +7 -0
- telemetry_cli/tgen/cli.py +128 -0
- telemetry_cli/tgen/cmdfile.py +68 -0
- telemetry_cli/tgen/convert.py +278 -0
- telemetry_cli/tgen/examples/arb_data.tgen +15 -0
- telemetry_cli/tgen/examples/calsweep.tgen +32 -0
- telemetry_cli/tgen/examples/cond_fill.tgen +17 -0
- telemetry_cli/tgen/examples/counter.tgen +11 -0
- telemetry_cli/tgen/examples/null_data.tgen +11 -0
- telemetry_cli/tgen/examples/random_size.tgen +20 -0
- telemetry_cli/tgen/examples/subcom.tgen +14 -0
- telemetry_cli/tgen/examples/valueV_value.tgen +20 -0
- telemetry_cli/tgen/runtime.py +390 -0
- telemetry_cli-1.0.0.dist-info/METADATA +106 -0
- telemetry_cli-1.0.0.dist-info/RECORD +48 -0
- telemetry_cli-1.0.0.dist-info/WHEEL +4 -0
- telemetry_cli-1.0.0.dist-info/entry_points.txt +5 -0
- telemetry_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""The tgen command: generate telemetry records from a command file.
|
|
2
|
+
|
|
3
|
+
tgen [ options ] command_file [ nframes [ firstframe ] ] > telemetry.dat
|
|
4
|
+
tgen --convert old_perl_command_file > new_command_file
|
|
5
|
+
|
|
6
|
+
Run ``tgen -help`` for help.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
from typing import BinaryIO, TextIO
|
|
13
|
+
|
|
14
|
+
from .. import __version__
|
|
15
|
+
from .._cli import broken_pipe
|
|
16
|
+
from .._getopt import OptionError, getoptions
|
|
17
|
+
from .._perl import numify, truthy
|
|
18
|
+
from . import cmdfile
|
|
19
|
+
from .convert import convert
|
|
20
|
+
from .runtime import Generator, TgenError
|
|
21
|
+
|
|
22
|
+
SPECS = ["h|help|man", "r|random|random_fill", "f|fill=i", "n|notify=i", "q|quiet",
|
|
23
|
+
"v|version", "seed=i", "convert"] # fmt: skip
|
|
24
|
+
|
|
25
|
+
USAGE = """\
|
|
26
|
+
Usage:
|
|
27
|
+
tgen [ options ] command_file [ nframes [ firstframe ] ] > telemetry.dat
|
|
28
|
+
tgen --convert perl_command_file > python_command_file
|
|
29
|
+
tgen -help
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
HELP = (
|
|
33
|
+
USAGE
|
|
34
|
+
+ """
|
|
35
|
+
tgen reads a command file and writes records (frames) laid out as it says:
|
|
36
|
+
nframes of them (default 1), numbering them from firstframe (default 0).
|
|
37
|
+
|
|
38
|
+
Options:
|
|
39
|
+
-r, -random fill unspecified bytes with random values
|
|
40
|
+
-f, -fill=n fill unspecified bytes with the byte n (default 0)
|
|
41
|
+
-n, -notify=n report progress on stderr every n frames (default 100)
|
|
42
|
+
-q, -quiet no progress reports
|
|
43
|
+
-seed=n seed the random numbers (rand and -r), for repeatable output
|
|
44
|
+
-convert translate a Perl tgen command file to the Python form
|
|
45
|
+
-v, -version show the version
|
|
46
|
+
-h, -help show this help
|
|
47
|
+
|
|
48
|
+
A command file holds item specifications, one per line:
|
|
49
|
+
|
|
50
|
+
name # offset # trigger # type # value
|
|
51
|
+
|
|
52
|
+
Each field is Python code, evaluated for every frame. The value is packed
|
|
53
|
+
with the type (a Perl pack template such as 'C', 'L', 'a10' or f"C{n}") and
|
|
54
|
+
written at the byte offset, if the trigger is true. A negative offset means
|
|
55
|
+
"evaluate the value but don't write it". Other lines are Python statements,
|
|
56
|
+
run once at the start; lines starting with #, ! or ; are comments.
|
|
57
|
+
|
|
58
|
+
In the code, I is the frame number, F the fill byte, and P.name.value an
|
|
59
|
+
item's value. The full manual, with examples, is at
|
|
60
|
+
https://github.com/donalgrant/telemetry-cli/blob/main/docs/tgen.md
|
|
61
|
+
"""
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def main(
|
|
66
|
+
argv: list[str] | None = None,
|
|
67
|
+
*,
|
|
68
|
+
stdin: BinaryIO | None = None,
|
|
69
|
+
stdout: BinaryIO | None = None,
|
|
70
|
+
stderr: TextIO | None = None,
|
|
71
|
+
) -> int:
|
|
72
|
+
argv = sys.argv[1:] if argv is None else list(argv)
|
|
73
|
+
stdout = stdout if stdout is not None else sys.stdout.buffer
|
|
74
|
+
stderr = stderr if stderr is not None else sys.stderr
|
|
75
|
+
|
|
76
|
+
def error(message: str, status: int = 1) -> int:
|
|
77
|
+
print(f"tgen: {message}", file=stderr)
|
|
78
|
+
return status
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
o, args = getoptions(argv, SPECS)
|
|
82
|
+
except OptionError as e:
|
|
83
|
+
return error(f"{e} (see 'tgen -help')", 2)
|
|
84
|
+
if o.get("v"):
|
|
85
|
+
print(f"tgen (Python), from telemetry-cli {__version__}", file=stderr)
|
|
86
|
+
return 0
|
|
87
|
+
if o.get("h"):
|
|
88
|
+
stdout.write(HELP.encode())
|
|
89
|
+
return 0
|
|
90
|
+
if not args:
|
|
91
|
+
stderr.write(USAGE)
|
|
92
|
+
return 2
|
|
93
|
+
try:
|
|
94
|
+
with open(args[0], encoding="latin-1") as f:
|
|
95
|
+
text = f.read()
|
|
96
|
+
except OSError as e:
|
|
97
|
+
return error(f"Can't read {args[0]}: {e.strerror}")
|
|
98
|
+
if o.get("convert"):
|
|
99
|
+
stdout.write(convert(text).encode("latin-1"))
|
|
100
|
+
return 0
|
|
101
|
+
|
|
102
|
+
def count(i, default):
|
|
103
|
+
if len(args) <= i:
|
|
104
|
+
return default
|
|
105
|
+
v = numify(args[i])
|
|
106
|
+
return int(v) if truthy(args[i]) and v else default # Perl: shift || default
|
|
107
|
+
|
|
108
|
+
nframes, first = count(1, 1), count(2, 0)
|
|
109
|
+
fill = "random" if o.get("r") else o.get("f", 0)
|
|
110
|
+
try:
|
|
111
|
+
items, statements = cmdfile.parse(text)
|
|
112
|
+
gen = Generator(
|
|
113
|
+
items, statements, fill=fill, seed=o.get("seed"), stderr=stderr, source=args[0]
|
|
114
|
+
)
|
|
115
|
+
gen.run(
|
|
116
|
+
stdout,
|
|
117
|
+
nframes,
|
|
118
|
+
first,
|
|
119
|
+
notify=o.get("n") or 100,
|
|
120
|
+
quiet=bool(o.get("q")),
|
|
121
|
+
progress=lambda m: print(m, file=stderr),
|
|
122
|
+
)
|
|
123
|
+
except TgenError as e:
|
|
124
|
+
return error(str(e))
|
|
125
|
+
except BrokenPipeError:
|
|
126
|
+
return broken_pipe(stdout)
|
|
127
|
+
stdout.flush()
|
|
128
|
+
return 0
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Reading tgen command files.
|
|
2
|
+
|
|
3
|
+
A command file has three kinds of lines:
|
|
4
|
+
|
|
5
|
+
- comments: lines starting with ``#``, ``!`` or ``;`` in the first column;
|
|
6
|
+
- item specifications, with at least four `` # `` separators (a ``#`` with
|
|
7
|
+
white space on both sides)::
|
|
8
|
+
|
|
9
|
+
name # offset # trigger # type # value
|
|
10
|
+
|
|
11
|
+
- statements, run once before any record is generated.
|
|
12
|
+
|
|
13
|
+
A line ending in a backslash continues on the next line.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import re
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
|
|
21
|
+
SEPARATOR = re.compile(r"\s+[#]\s+")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class Item:
|
|
26
|
+
name: str
|
|
27
|
+
offset: str
|
|
28
|
+
trigger: str
|
|
29
|
+
type: str
|
|
30
|
+
value: str
|
|
31
|
+
line: int # where the specification starts in the file
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class Statement:
|
|
36
|
+
code: str
|
|
37
|
+
line: int
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def logical_lines(text: str):
|
|
41
|
+
"""(line number, text) of each logical line, with continuations joined."""
|
|
42
|
+
lines = text.split("\n")
|
|
43
|
+
if lines and lines[-1] == "":
|
|
44
|
+
lines.pop()
|
|
45
|
+
i = 0
|
|
46
|
+
while i < len(lines):
|
|
47
|
+
start = i + 1
|
|
48
|
+
line = lines[i]
|
|
49
|
+
i += 1
|
|
50
|
+
while line.endswith("\\"):
|
|
51
|
+
line = line[:-1]
|
|
52
|
+
if i < len(lines):
|
|
53
|
+
line += lines[i]
|
|
54
|
+
i += 1
|
|
55
|
+
yield start, line
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def parse(text: str) -> tuple[list[Item], list[Statement]]:
|
|
59
|
+
items, statements = [], []
|
|
60
|
+
for n, line in logical_lines(text):
|
|
61
|
+
if line[:1] in ("#", "!", ";"):
|
|
62
|
+
continue
|
|
63
|
+
if len(SEPARATOR.findall(line)) >= 4:
|
|
64
|
+
fields = [f.strip() for f in SEPARATOR.split(line)]
|
|
65
|
+
items.append(Item(*fields[:5], line=n)) # further fields are ignored
|
|
66
|
+
elif line.strip():
|
|
67
|
+
statements.append(Statement(line.strip(), n))
|
|
68
|
+
return items, statements
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""Translate a Perl tgen command file into the Python form (``tgen --convert``).
|
|
2
|
+
|
|
3
|
+
The translation is pattern-based. It handles what tgen command files usually
|
|
4
|
+
contain: ``$::name`` variables, ``$P{item}{field}``, arithmetic, string
|
|
5
|
+
concatenation, interpolated strings, ``map { ... } (a..b)``, ``++``/``--``,
|
|
6
|
+
and statement modifiers (``x if y``). Anything else that looks like Perl is
|
|
7
|
+
left as it is, with a ``# TODO(convert)`` comment line above it, so that
|
|
8
|
+
running the result fails loudly rather than doing something different.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import ast
|
|
14
|
+
import re
|
|
15
|
+
|
|
16
|
+
from .cmdfile import SEPARATOR, logical_lines
|
|
17
|
+
|
|
18
|
+
_PERLISH = re.compile(
|
|
19
|
+
r"\$|@|->|\bsub\b|=~|\bmy\b|\bprint\b(?!\()|\?|\blast\b|\bnext\b|\beq\b|\bne\b"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def convert(text: str) -> str:
|
|
24
|
+
out = []
|
|
25
|
+
for _, line in logical_lines(text):
|
|
26
|
+
if line[:1] in ("#", "!", ";") or not line.strip():
|
|
27
|
+
out.append(line)
|
|
28
|
+
continue
|
|
29
|
+
if len(SEPARATOR.findall(line)) >= 4:
|
|
30
|
+
parts = SEPARATOR.split(line)
|
|
31
|
+
seps = SEPARATOR.findall(line)
|
|
32
|
+
fields = [parts[0].strip()] + [expr(p.strip()) for p in parts[1:5]]
|
|
33
|
+
todo = [f for f in fields[1:] if _PERLISH.search(_strip_strings(f))]
|
|
34
|
+
rest = "".join(s + p for s, p in zip(seps[4:], parts[5:], strict=False))
|
|
35
|
+
new = fields[0] + "".join(s + f for s, f in zip(seps, fields[1:], strict=False)) + rest
|
|
36
|
+
else:
|
|
37
|
+
new = statement(line.strip())
|
|
38
|
+
todo = [new] if _PERLISH.search(_strip_strings(new)) else []
|
|
39
|
+
if todo:
|
|
40
|
+
out.append("# TODO(convert): this line still contains Perl")
|
|
41
|
+
out.append(new)
|
|
42
|
+
return "\n".join(out) + "\n"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# --- pieces --------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
_STRING = re.compile(r"""("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')""")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _strip_strings(s: str) -> str:
|
|
51
|
+
return _STRING.sub('""', s)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _split_code(s: str):
|
|
55
|
+
"""Alternating (code, string literal) pieces of s."""
|
|
56
|
+
pos = 0
|
|
57
|
+
for m in _STRING.finditer(s):
|
|
58
|
+
yield s[pos : m.start()], False
|
|
59
|
+
yield m.group(0), True
|
|
60
|
+
pos = m.end()
|
|
61
|
+
yield s[pos:], False
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _variables(code: str) -> str:
|
|
65
|
+
code = re.sub(r"\$P\{\s*(\w+)\s*\}\s*\{\s*(\w+)\s*\}", r"P.\1.\2", code)
|
|
66
|
+
code = re.sub(r"[$@]::(\w+)", r"\1", code)
|
|
67
|
+
code = re.sub(r"\bmy\s+[$@](\w+)", r"\1", code)
|
|
68
|
+
code = re.sub(r"\$_\b", "_", code)
|
|
69
|
+
code = re.sub(r"[$@](\w+)", r"\1", code)
|
|
70
|
+
return code
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _string(lit: str) -> str:
|
|
74
|
+
"""A Perl string literal as Python: double-quoted strings interpolate."""
|
|
75
|
+
if lit.startswith("'"):
|
|
76
|
+
if "$" in lit: # code in a string, for seek_file's offset and condition
|
|
77
|
+
return "'" + _code(lit[1:-1]) + "'"
|
|
78
|
+
return lit
|
|
79
|
+
body = lit[1:-1]
|
|
80
|
+
if not re.search(r"[$@]", body.replace("\\$", "").replace("\\@", "")):
|
|
81
|
+
return lit
|
|
82
|
+
body = body.replace("{", "{{").replace("}", "}}")
|
|
83
|
+
body = re.sub(r"\$(?:::)?(\w+)", r"{\1}", body)
|
|
84
|
+
return 'f"' + body + '"'
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _ranges(code: str) -> str:
|
|
88
|
+
return re.sub(r"\(\s*([^()]+?|\([^()]*\))\s*\.\.\s*(\([^()]*\)|[^()]+?)\s*\)",
|
|
89
|
+
r"range(\1, \2 + 1)", code) # fmt: skip
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _maps(code: str) -> str:
|
|
93
|
+
# [ map { EXPR } (LIST) ] and map { EXPR } (LIST)
|
|
94
|
+
code = re.sub(r"\[\s*map\s*\{(.*)\}\s*(range\(.*\)|\(.*\))\s*\]", r"[\1 for _ in \2]", code)
|
|
95
|
+
code = re.sub(r"\bmap\s*\{(.*)\}\s*(range\(.*\)|\(.*\))", r"[\1 for _ in \2]", code)
|
|
96
|
+
return code
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _operators(code: str) -> str:
|
|
100
|
+
code = re.sub(r"&&", " and ", code)
|
|
101
|
+
code = re.sub(r"\|\|", " or ", code)
|
|
102
|
+
code = re.sub(r"!(?!=)", " not ", code)
|
|
103
|
+
code = re.sub(r"\beq\b", "==", code)
|
|
104
|
+
code = re.sub(r"\bne\b", "!=", code)
|
|
105
|
+
return code
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# Perl operators whose operands are integers, and the helpers that do the same
|
|
109
|
+
_INTEGER_OPS = {ast.Mod: "pmod", ast.BitAnd: "pand", ast.BitOr: "por", ast.BitXor: "pxor",
|
|
110
|
+
ast.LShift: "pshl", ast.RShift: "pshr"} # fmt: skip
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _integer_ops(code: str) -> str:
|
|
114
|
+
"""Rewrite %, &, |, ^, << and >> as calls to helpers with Perl's integer semantics.
|
|
115
|
+
|
|
116
|
+
Only those expressions change; the rest of the text is kept as written.
|
|
117
|
+
"""
|
|
118
|
+
if not re.search(r"%|&|\||\^|<<|>>", _strip_strings(code)) or "\n" in code:
|
|
119
|
+
return code
|
|
120
|
+
try:
|
|
121
|
+
tree = ast.parse(code)
|
|
122
|
+
except SyntaxError:
|
|
123
|
+
return code # still Perl; it will be marked for review
|
|
124
|
+
src = code.encode()
|
|
125
|
+
|
|
126
|
+
def needs(node) -> bool:
|
|
127
|
+
return any(isinstance(n, ast.BinOp) and type(n.op) in _INTEGER_OPS for n in ast.walk(node))
|
|
128
|
+
|
|
129
|
+
def span(node) -> tuple[int, int]:
|
|
130
|
+
return node.col_offset, node.end_col_offset
|
|
131
|
+
|
|
132
|
+
def render(node) -> bytes:
|
|
133
|
+
if isinstance(node, ast.BinOp) and type(node.op) in _INTEGER_OPS:
|
|
134
|
+
fn = _INTEGER_OPS[type(node.op)].encode()
|
|
135
|
+
return fn + b"(" + render(node.left) + b", " + render(node.right) + b")"
|
|
136
|
+
start, end = span(node)
|
|
137
|
+
text = src[start:end]
|
|
138
|
+
for child in sorted(
|
|
139
|
+
(c for c in ast.iter_child_nodes(node) if hasattr(c, "col_offset") and needs(c)),
|
|
140
|
+
key=lambda c: c.col_offset,
|
|
141
|
+
reverse=True,
|
|
142
|
+
):
|
|
143
|
+
cs, ce = span(child)
|
|
144
|
+
text = text[: cs - start] + render(child) + text[ce - start :]
|
|
145
|
+
return text
|
|
146
|
+
|
|
147
|
+
out = src
|
|
148
|
+
for stmt in sorted(tree.body, key=lambda n: n.col_offset, reverse=True):
|
|
149
|
+
if needs(stmt):
|
|
150
|
+
s0, e0 = span(stmt)
|
|
151
|
+
out = out[:s0] + render(stmt) + out[e0:]
|
|
152
|
+
return out.decode()
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _code(code: str) -> str:
|
|
156
|
+
return _operators(_maps(_ranges(_variables(code))))
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _finish(code: str) -> str:
|
|
160
|
+
return _integer_ops(code)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _concat_pieces(s: str) -> list[str]:
|
|
164
|
+
"""s split at Perl's string-concatenation operator (.), outside strings and brackets.
|
|
165
|
+
|
|
166
|
+
A dot next to a digit is part of a number, and ".." is a range.
|
|
167
|
+
"""
|
|
168
|
+
pieces, depth, start, i, quote = [], 0, 0, 0, None
|
|
169
|
+
while i < len(s):
|
|
170
|
+
ch = s[i]
|
|
171
|
+
if quote:
|
|
172
|
+
if ch == "\\":
|
|
173
|
+
i += 1
|
|
174
|
+
elif ch == quote:
|
|
175
|
+
quote = None
|
|
176
|
+
elif ch in "'\"":
|
|
177
|
+
quote = ch
|
|
178
|
+
elif ch in "([{":
|
|
179
|
+
depth += 1
|
|
180
|
+
elif ch in ")]}":
|
|
181
|
+
depth -= 1
|
|
182
|
+
elif ch == "." and depth == 0:
|
|
183
|
+
before, after = s[i - 1 : i], s[i + 1 : i + 2]
|
|
184
|
+
if "." not in (before, after) and not (before.isdigit() or after.isdigit()):
|
|
185
|
+
pieces.append(s[start:i])
|
|
186
|
+
start = i + 1
|
|
187
|
+
i += 1
|
|
188
|
+
pieces.append(s[start:])
|
|
189
|
+
return pieces
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _translate(s: str) -> str:
|
|
193
|
+
pieces = _concat_pieces(s)
|
|
194
|
+
if len(pieces) > 1: # Perl's . joins strings, and numbers as text
|
|
195
|
+
return "cat(" + ", ".join(_translate(p.strip()) for p in pieces) + ")"
|
|
196
|
+
out = []
|
|
197
|
+
for piece, is_string in _split_code(s):
|
|
198
|
+
out.append(_string(piece) if is_string else _code(piece))
|
|
199
|
+
return "".join(out)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _increments(stmt: str) -> str:
|
|
203
|
+
"""x++ / ++x / x-- / --x as assignments."""
|
|
204
|
+
m = re.fullmatch(r"\s*(?:(\+\+|--)\s*(\S+?)|(\S+?)\s*(\+\+|--))\s*", stmt)
|
|
205
|
+
if not m:
|
|
206
|
+
return stmt
|
|
207
|
+
op = m.group(1) or m.group(4)
|
|
208
|
+
target = m.group(2) or m.group(3)
|
|
209
|
+
return f"{target} = {target} {'+' if op == '++' else '-'} 1"
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _statements(s: str) -> list[str]:
|
|
213
|
+
parts, depth, cur, quote = [], 0, "", None
|
|
214
|
+
for ch in s:
|
|
215
|
+
if quote:
|
|
216
|
+
cur += ch
|
|
217
|
+
if ch == quote:
|
|
218
|
+
quote = None
|
|
219
|
+
continue
|
|
220
|
+
if ch in "'\"":
|
|
221
|
+
quote = ch
|
|
222
|
+
elif ch in "([{":
|
|
223
|
+
depth += 1
|
|
224
|
+
elif ch in ")]}":
|
|
225
|
+
depth -= 1
|
|
226
|
+
elif ch == ";" and depth == 0:
|
|
227
|
+
parts.append(cur)
|
|
228
|
+
cur = ""
|
|
229
|
+
continue
|
|
230
|
+
cur += ch
|
|
231
|
+
parts.append(cur)
|
|
232
|
+
return [p.strip() for p in parts if p.strip()]
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _modifier(stmt: str, *, as_expression: bool) -> str:
|
|
236
|
+
"""Perl's `STATEMENT if CONDITION`."""
|
|
237
|
+
m = re.fullmatch(r"(.+?)\s+if\s+(.+)", stmt)
|
|
238
|
+
if not m or " else " in stmt:
|
|
239
|
+
return _increments(stmt)
|
|
240
|
+
body, cond = _increments(m.group(1)), m.group(2)
|
|
241
|
+
if not as_expression:
|
|
242
|
+
return f"if {cond}: {body}"
|
|
243
|
+
a = re.fullmatch(r"(\w+(?:\.\w+)*)\s*=\s*(.+)", body)
|
|
244
|
+
if a:
|
|
245
|
+
return f"{a.group(1)} = {a.group(2)} if {cond} else {a.group(1)}"
|
|
246
|
+
return f"({body}) if ({cond}) else None"
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _print(stmt: str) -> str:
|
|
250
|
+
m = re.fullmatch(r"print\s+STDERR\s+(.+?)((?:\s+if\s+.+)?)", stmt)
|
|
251
|
+
if not m:
|
|
252
|
+
return stmt
|
|
253
|
+
return f'print({m.group(1)}, file=stderr, end=""){m.group(2)}'
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def expr(field: str) -> str:
|
|
257
|
+
"""A Perl item field (an expression, or statements whose last value counts)."""
|
|
258
|
+
stmts = [_translate(_print(s)) for s in _statements(field)]
|
|
259
|
+
return _finish("; ".join(_modifier(s, as_expression=True) for s in stmts))
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def statement(line: str) -> str:
|
|
263
|
+
"""A Perl statement line (run once)."""
|
|
264
|
+
code, comment = _split_comment(line)
|
|
265
|
+
stmts = [_translate(_print(s)) for s in _statements(code)]
|
|
266
|
+
new = _finish("; ".join(_modifier(s, as_expression=False) for s in stmts))
|
|
267
|
+
return new + (f" {comment}" if comment else "")
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _split_comment(line: str) -> tuple[str, str]:
|
|
271
|
+
"""Code and a trailing # comment (outside strings)."""
|
|
272
|
+
pos = 0
|
|
273
|
+
for piece, is_string in _split_code(line):
|
|
274
|
+
if not is_string and "#" in piece:
|
|
275
|
+
i = pos + piece.index("#")
|
|
276
|
+
return line[:i].rstrip(), line[i:]
|
|
277
|
+
pos += len(piece)
|
|
278
|
+
return line, ""
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Arbitrarily-filled data file -- generates 1024-byte records
|
|
3
|
+
#
|
|
4
|
+
|
|
5
|
+
# this following item specification gives only the value of the
|
|
6
|
+
# final byte (byte 1023, with is the 1024th zero-offset byte);
|
|
7
|
+
# the bytes at all earlier byte offsets are padded automatically.
|
|
8
|
+
# $::F is a special variable in tgen which contains the value
|
|
9
|
+
# of the fill byte. The value is zero by default, but can
|
|
10
|
+
# be specified on the command line as any other byte value using
|
|
11
|
+
# the -f flag, or can be made random using the -r flag.
|
|
12
|
+
|
|
13
|
+
data # 1023 # 1 # 'C' # F
|
|
14
|
+
|
|
15
|
+
# end of command file
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Generate a binary data file
|
|
3
|
+
#
|
|
4
|
+
|
|
5
|
+
# some parameters to define a digital system
|
|
6
|
+
|
|
7
|
+
pi=3.141592654 # the (approx) value of pi
|
|
8
|
+
nf=10 # number of caltones to generate
|
|
9
|
+
f0=5.0e6 # minimum caltone frequency
|
|
10
|
+
f_max=35.0e6 # maximum caltone frequency
|
|
11
|
+
df=(f_max-f0)/(nf-1) # frequency step
|
|
12
|
+
|
|
13
|
+
f = [ f0+_*df for _ in range(1, nf + 1)] # array of frequencies
|
|
14
|
+
|
|
15
|
+
fs=90.0e6 # sampling rate
|
|
16
|
+
a=2.0*pi/fs # convenience coefficient
|
|
17
|
+
|
|
18
|
+
ns=256 # number of samples to generate
|
|
19
|
+
|
|
20
|
+
# below we have two item specifications:
|
|
21
|
+
# 'calfreq' calculates which caltone frequency
|
|
22
|
+
# we should use for the current pulse, and
|
|
23
|
+
# data actually generates the sample values
|
|
24
|
+
# to be written for the current pulse, using
|
|
25
|
+
# the caltone frequency calculated previously.
|
|
26
|
+
|
|
27
|
+
calfreq # -1 # 1 # X # pmod(I/32, nf) # 32 pulses per caltone
|
|
28
|
+
|
|
29
|
+
data # 0 # 1 # f"C{ns}" # [ 128+int(128.0*cos(a*_*f[P.calfreq.value])) for _ in range(1, (ns-1) + 1)]
|
|
30
|
+
|
|
31
|
+
# end of command file
|
|
32
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Change fill behavior on every third pulse
|
|
3
|
+
#
|
|
4
|
+
|
|
5
|
+
zero_fill # -1 # (pmod(I, 3))==0 # X # F=0
|
|
6
|
+
rail_fill # -1 # (pmod(I, 3))==1 # X # F=255
|
|
7
|
+
rand_fill # -1 # (pmod(I, 3))==2 # X # F="random"
|
|
8
|
+
|
|
9
|
+
pulse_number # 0 # 1 # 'C' # I
|
|
10
|
+
last_byte # 9 # 1 # 'C' # F
|
|
11
|
+
|
|
12
|
+
# Note that the -1 offset for the first three item specs
|
|
13
|
+
# means that the fields are only evaluated when the trigger field is true,
|
|
14
|
+
# but not written. The pack-type is ignored in this case.
|
|
15
|
+
|
|
16
|
+
# end of command file
|
|
17
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Null data file -- generates 1024-byte null-valued data items.
|
|
3
|
+
#
|
|
4
|
+
|
|
5
|
+
# this following item specification gives only the value of the
|
|
6
|
+
# final byte (byte 1023, with is the 1024th zero-offset byte).
|
|
7
|
+
# the bytes at all earlier byte offsets are null-padded automatically.
|
|
8
|
+
|
|
9
|
+
data # 1023 # 1 # 'C' # 0
|
|
10
|
+
|
|
11
|
+
# end of command file
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#
|
|
2
|
+
# random record lengths
|
|
3
|
+
#
|
|
4
|
+
|
|
5
|
+
# we make use of the fact that the byte-offset is actually
|
|
6
|
+
# a Perl-evaluated statement for each pulse, so that it
|
|
7
|
+
# can vary from one pulse to the next. We'll write random
|
|
8
|
+
# or fixed value data (using the -r or -f flag on the command line)
|
|
9
|
+
# but have a fixed "PN-Sequence" for the start of the records.
|
|
10
|
+
|
|
11
|
+
pn # 0 # 1 # 'L' # 0x03915ed3
|
|
12
|
+
|
|
13
|
+
# the following item gives a random byte offset between 4
|
|
14
|
+
# (so that we don't try to over-write the pn-sequence)
|
|
15
|
+
# and 255 (maximum 256-byte record length). The value
|
|
16
|
+
# is the same as the fill byte value.
|
|
17
|
+
|
|
18
|
+
data # int(rand()*252)+4 # 1 # 'C' # F
|
|
19
|
+
|
|
20
|
+
# end of command file
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Generate a "subcommutated header" -- use this command file
|
|
3
|
+
#
|
|
4
|
+
|
|
5
|
+
SUB_LENGTH=500 # size (bytes) of subcom data record
|
|
6
|
+
SUB_BYTES=10 # number of subcom bytes per record
|
|
7
|
+
FILE='subcom.tgen' # use this file for example subcom data (run from its directory)
|
|
8
|
+
|
|
9
|
+
counter # 0 # 1 # 'L' # I
|
|
10
|
+
sub_index # 4 # 1 # 'S' # pmod(I * SUB_BYTES, SUB_LENGTH)
|
|
11
|
+
sub_data # 6 # 1 # 'a10' # subcom_file(FILE,P.sub_index.value,10)
|
|
12
|
+
|
|
13
|
+
# end of command file
|
|
14
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Contrast the value and _value $P{}{} fields
|
|
3
|
+
#
|
|
4
|
+
|
|
5
|
+
random_number # 0 # 1 # 'f' # rand(1.0)
|
|
6
|
+
|
|
7
|
+
COUNT1=0 # this does a count on the rand(1.0) function
|
|
8
|
+
COUNT2=0 # this one will be what we were probably wanting
|
|
9
|
+
|
|
10
|
+
value_1 # 4 # 1 # 'S' # COUNT1 = COUNT1 + 1 if P.random_number._value>0.5 else COUNT1; COUNT1
|
|
11
|
+
|
|
12
|
+
value_2 # 6 # 1 # 'S' # COUNT2 = COUNT2 + 1 if P.random_number.value>0.5 else COUNT2; COUNT2
|
|
13
|
+
|
|
14
|
+
# The difference between the two is that _value does an 'eval' on the
|
|
15
|
+
# content of the value field (in this case, the rand(1.0) function),
|
|
16
|
+
# while value gives the result of the eval when the item was generated
|
|
17
|
+
# for this record.
|
|
18
|
+
|
|
19
|
+
# end of command file
|
|
20
|
+
|