diff options
author | Luke T. Shumaker <lukeshu@lukeshu.com> | 2025-02-03 14:59:24 -0700 |
---|---|---|
committer | Luke T. Shumaker <lukeshu@lukeshu.com> | 2025-02-03 20:36:41 -0700 |
commit | ae610c5796cb1624f03b79dfae372c757674ea76 (patch) | |
tree | d2d55c8788b42c84410687117e2dec68744bc50c | |
parent | ff451382aa77966565822918967f77dc9b7ea68d (diff) |
lib9p: idl.gen: Have pretty and informative error messages
"Ignore space change" will be useful when viewing this diff.
-rwxr-xr-x | lib9p/idl.gen | 25 | ||||
-rw-r--r-- | lib9p/idl/__init__.py | 259 |
2 files changed, 159 insertions, 125 deletions
diff --git a/lib9p/idl.gen b/lib9p/idl.gen index b23b5b8..558592f 100755 --- a/lib9p/idl.gen +++ b/lib9p/idl.gen @@ -855,11 +855,34 @@ LM_FLATTEN bool _{idprefix}stat_marshal(struct _marshal_ctx *ctx, struct lib9p_s if __name__ == "__main__": import sys + if typing.TYPE_CHECKING: + + class ANSIColors: + MAGENTA = "\x1b[35m" + RED = "\x1b[31m" + RESET = "\x1b[0m" + + else: + from _colorize import ANSIColors # Present in Python 3.13+ + if len(sys.argv) < 2: raise ValueError("requires at least 1 .9p filename") parser = idl.Parser() for txtname in sys.argv[1:]: - parser.parse_file(txtname) + try: + parser.parse_file(txtname) + except SyntaxError as e: + print( + f"{ANSIColors.RED}{e.filename}{ANSIColors.RESET}:{ANSIColors.MAGENTA}{e.lineno}{ANSIColors.RESET}: {e.msg}", + file=sys.stderr, + ) + assert e.text + print(f"\t{e.text}", file=sys.stderr) + print( + f"\t{ANSIColors.RED}{'~'*len(e.text)}{ANSIColors.RESET}", + file=sys.stderr, + ) + sys.exit(2) versions, typs = parser.all() outdir = os.path.normpath(os.path.join(sys.argv[0], "..")) with open(os.path.join(outdir, "include/lib9p/9p.generated.h"), "w") as fh: diff --git a/lib9p/idl/__init__.py b/lib9p/idl/__init__.py index 82fa69c..a01c38f 100644 --- a/lib9p/idl/__init__.py +++ b/lib9p/idl/__init__.py @@ -433,133 +433,144 @@ def parse_file( with open(filename, "r") as fh: prev: Type | None = None - for line in fh: - line = line.split("#", 1)[0].rstrip() - if not line: - continue - if m := re.fullmatch(re_line_version, line): - if version: + for lineno, line in enumerate(fh): + try: + line = line.split("#", 1)[0].rstrip() + if not line: + continue + if m := re.fullmatch(re_line_version, line): + if version: + raise SyntaxError("must have exactly 1 version line") + version = m.group("version") + continue + if not version: raise SyntaxError("must have exactly 1 version line") - version = m.group("version") - continue - if not version: - raise SyntaxError("must have exactly 1 version line") - - if m := re.fullmatch(re_line_import, line): - other_version, other_typs = get_include(m.group("file")) - for symname in m.group("syms").split(sep=","): - symname = symname.strip() - found = False - for typ in other_typs: - if typ.name == symname or symname == "*": - found = True - match typ: - case Primitive(): - pass - case Number(): - typ.in_versions.add(version) - case Bitfield(): - typ.in_versions.add(version) - for val in typ.names.values(): - if other_version in val.in_versions: - val.in_versions.add(version) - case Struct(): # and Message() - typ.in_versions.add(version) - for member in typ.members: - if other_version in member.in_versions: - member.in_versions.add(version) - if typ.name in env and env[typ.name] != typ: + + if m := re.fullmatch(re_line_import, line): + other_version, other_typs = get_include(m.group("file")) + for symname in m.group("syms").split(sep=","): + symname = symname.strip() + found = False + for typ in other_typs: + if typ.name == symname or symname == "*": + found = True + match typ: + case Primitive(): + pass + case Number(): + typ.in_versions.add(version) + case Bitfield(): + typ.in_versions.add(version) + for val in typ.names.values(): + if other_version in val.in_versions: + val.in_versions.add(version) + case Struct(): # and Message() + typ.in_versions.add(version) + for member in typ.members: + if other_version in member.in_versions: + member.in_versions.add(version) + if typ.name in env and env[typ.name] != typ: + raise ValueError( + f"duplicate type name {repr(typ.name)}" + ) + env[typ.name] = typ + if symname != "*" and not found: + raise ValueError( + f"import: {m.group('file')}: no symbol {repr(symname)}" + ) + elif m := re.fullmatch(re_line_num, line): + num = Number() + num.name = m.group("name") + num.in_versions.add(version) + + prim = env[m.group("prim")] + assert isinstance(prim, Primitive) + num.prim = prim + + if num.name in env: + raise ValueError(f"duplicate type name {repr(num.name)}") + env[num.name] = num + prev = num + elif m := re.fullmatch(re_line_bitfield, line): + bf = Bitfield() + bf.name = m.group("name") + bf.in_versions.add(version) + + prim = env[m.group("prim")] + assert isinstance(prim, Primitive) + bf.prim = prim + + bf.bits = (prim.static_size * 8) * [""] + + if bf.name in env: + raise ValueError(f"duplicate type name {repr(bf.name)}") + env[bf.name] = bf + prev = bf + elif m := re.fullmatch(re_line_bitfield_, line): + bf = get_type(m.group("name"), Bitfield) + parse_bitspec(version, bf, m.group("member")) + + prev = bf + elif m := re.fullmatch(re_line_struct, line): + match m.group("op"): + case "=": + struct = Struct() + struct.name = m.group("name") + struct.in_versions.add(version) + struct.members = [] + parse_members(version, env, struct, m.group("members")) + + if struct.name in env: + raise ValueError( + f"duplicate type name {repr(struct.name)}" + ) + env[struct.name] = struct + prev = struct + case "+=": + struct = get_type(m.group("name"), Struct) + parse_members(version, env, struct, m.group("members")) + + prev = struct + elif m := re.fullmatch(re_line_msg, line): + match m.group("op"): + case "=": + msg = Message() + msg.name = m.group("name") + msg.in_versions.add(version) + msg.members = [] + parse_members(version, env, msg, m.group("members")) + + if msg.name in env: raise ValueError( - f"duplicate type name {repr(typ.name)}" + f"duplicate type name {repr(msg.name)}" ) - env[typ.name] = typ - if symname != "*" and not found: - raise ValueError( - f"import: {m.group('file')}: no symbol {repr(symname)}" - ) - elif m := re.fullmatch(re_line_num, line): - num = Number() - num.name = m.group("name") - num.in_versions.add(version) - - prim = env[m.group("prim")] - assert isinstance(prim, Primitive) - num.prim = prim - - if num.name in env: - raise ValueError(f"duplicate type name {repr(num.name)}") - env[num.name] = num - prev = num - elif m := re.fullmatch(re_line_bitfield, line): - bf = Bitfield() - bf.name = m.group("name") - bf.in_versions.add(version) - - prim = env[m.group("prim")] - assert isinstance(prim, Primitive) - bf.prim = prim - - bf.bits = (prim.static_size * 8) * [""] - - if bf.name in env: - raise ValueError(f"duplicate type name {repr(bf.name)}") - env[bf.name] = bf - prev = bf - elif m := re.fullmatch(re_line_bitfield_, line): - bf = get_type(m.group("name"), Bitfield) - parse_bitspec(version, bf, m.group("member")) - - prev = bf - elif m := re.fullmatch(re_line_struct, line): - match m.group("op"): - case "=": - struct = Struct() - struct.name = m.group("name") - struct.in_versions.add(version) - struct.members = [] - parse_members(version, env, struct, m.group("members")) - - if struct.name in env: - raise ValueError(f"duplicate type name {repr(struct.name)}") - env[struct.name] = struct - prev = struct - case "+=": - struct = get_type(m.group("name"), Struct) - parse_members(version, env, struct, m.group("members")) - - prev = struct - elif m := re.fullmatch(re_line_msg, line): - match m.group("op"): - case "=": - msg = Message() - msg.name = m.group("name") - msg.in_versions.add(version) - msg.members = [] - parse_members(version, env, msg, m.group("members")) - - if msg.name in env: - raise ValueError(f"duplicate type name {repr(msg.name)}") - env[msg.name] = msg - prev = msg - case "+=": - msg = get_type(m.group("name"), Message) - parse_members(version, env, msg, m.group("members")) - - prev = msg - elif m := re.fullmatch(re_line_cont, line): - match prev: - case Bitfield(): - parse_bitspec(version, prev, m.group("specs")) - case Number(): - parse_numspec(version, prev, m.group("specs")) - case Struct(): # and Message() - parse_members(version, env, prev, m.group("specs")) - case _: - raise SyntaxError( - "continuation line must come after a bitfield, struct, or msg line" - ) - else: - raise SyntaxError(f"invalid line {repr(line)}") + env[msg.name] = msg + prev = msg + case "+=": + msg = get_type(m.group("name"), Message) + parse_members(version, env, msg, m.group("members")) + + prev = msg + elif m := re.fullmatch(re_line_cont, line): + match prev: + case Bitfield(): + parse_bitspec(version, prev, m.group("specs")) + case Number(): + parse_numspec(version, prev, m.group("specs")) + case Struct(): # and Message() + parse_members(version, env, prev, m.group("specs")) + case _: + raise SyntaxError( + "continuation line must come after a bitfield, struct, or msg line" + ) + else: + raise SyntaxError("invalid line") + except (SyntaxError, NameError, ValueError) as e: + e2 = SyntaxError(str(e)) + e2.filename = filename + e2.lineno = lineno + 1 + e2.text = line + raise e2 from e if not version: raise SyntaxError("must have exactly 1 version line") |