diff --git a/App/lib/lsp/LspDocuments.vp b/App/lib/lsp/LspDocuments.vp index b2b17b2..5d9eb94 100644 --- a/App/lib/lsp/LspDocuments.vp +++ b/App/lib/lsp/LspDocuments.vp @@ -85,6 +85,10 @@ class LspDocuments: if doc.spare is not None: doc.spare.Destroy() doc.spare = None + if doc.uri is not None: + self.pool.free(rawptr(doc.uri)) + doc.uri = None + self.pool.free(rawptr(doc)) return 0 def Open(self, uri: str, text: str, version: int) -> LspDocument*: diff --git a/App/lib/lsp/LspIndex.vp b/App/lib/lsp/LspIndex.vp index 46900b8..4108504 100644 --- a/App/lib/lsp/LspIndex.vp +++ b/App/lib/lsp/LspIndex.vp @@ -62,6 +62,7 @@ class LspTarget: sel: LspSource.LspRange* has_sel: int is_macro: int + is_alias: int def __new__(self, pool: memhub.MemManager*) -> rawptr: p: LspTarget* = (LspTarget*)(pool.alloc(LspTarget.__sizeof__())) @@ -250,6 +251,7 @@ def _CopySymbol(pool: memhub.MemManager*, s: LspSymbols.LspSymbol*, c.name = LspPools.DupStr(pool, s.name) c.kind = s.kind c.is_macro = s.is_macro + c.is_alias = s.is_alias c.detail = LspSymbols._CopyStr(pool, s.detail, LspSymbols.LSP_SIGNATURE_CAP) c.doc = LspSymbols._CopyStr(pool, s.doc, LspSymbols.LSP_DOC_CAP) c.file = file @@ -742,6 +744,7 @@ def _SymbolTarget(pool: memhub.MemManager*, s: LspSymbols.LspSymbol*) -> LspTarg t.name = s.name t.kind = s.kind t.is_macro = s.is_macro + t.is_alias = s.is_alias t.detail = s.detail t.doc = s.doc t.container = s.container @@ -954,6 +957,34 @@ def ResolveSymbol(ws: LspWorkspace.LspWorkspace*, pool: memhub.MemManager*, return _SymbolTarget(pool, s) +def ResolveWildcard(ws: LspWorkspace.LspWorkspace*, pool: memhub.MemManager*, + table_pool: memhub.MemManager*, from_file: str, + wildcard: LspSymbols.LspSymbol*, + name: str) -> LspTarget*: + """Resolve a name (optionally `Name.member...`) supplied by `from module import *`.""" + if wildcard is None or name is None: + return None + file: str = _ResolveFromFile(ws, pool, from_file, wildcard) + if file is None or not os.isfile(file): + return None + h: str = None + r: str = None + LspSymbols._SplitHead(pool, name, &h, &r) + if h is None: + return None + member: LspSymbols.LspSymbol* = ResolveInFile(pool, table_pool, file, None, h) + if member is None: + return None + base: LspTarget* = None + if member.import_kind != LspSymbols.LSP_IMPORT_NONE: + base = _ResolveFromTarget(ws, pool, table_pool, member.file, member, 0) + else: + base = _SymbolTarget(pool, member) + if r is None: + return base + return ResolveMembers(pool, table_pool, base, r) + + def ResolveMembers(pool: memhub.MemManager*, table_pool: memhub.MemManager*, base: LspTarget*, rest: str) -> LspTarget*: """Walk class members from an already-resolved base target (no import re-resolution).""" @@ -986,6 +1017,8 @@ def ResolveChain(ws: LspWorkspace.LspWorkspace*, pool: memhub.MemManager*, rest: str) -> LspTarget*: if head is None: return None + if head.import_attr is not None and string.strcmp(head.import_attr, "*") == 0: + return ResolveWildcard(ws, pool, table_pool, from_file, head, rest) if head.import_kind == LspSymbols.LSP_IMPORT_FROM: base: LspTarget* = _ResolveFromTarget(ws, pool, table_pool, from_file, head, 0) if base is None or rest is None: diff --git a/App/lib/lsp/LspNavigation.vp b/App/lib/lsp/LspNavigation.vp index 86e016a..aeb392d 100644 --- a/App/lib/lsp/LspNavigation.vp +++ b/App/lib/lsp/LspNavigation.vp @@ -161,6 +161,32 @@ def _LiveSymbol(a: LspSymbols.LspAnalysis*, container: str, return None +def _WildcardTarget(ws: LspWorkspace.LspWorkspace*, pool: memhub.MemManager*, + table_pool: memhub.MemManager*, + a: LspSymbols.LspAnalysis*, + o: LspSymbols.LspOccurrence*) -> LspIndex.LspTarget*: + if a is None or a.wildcards is None or o is None or o.name is None: + return None + i: int = 0 + while i < int(len(a.wildcards)): + w: LspSymbols.LspSymbol* = a.wildcards.get(sizet(i)) + i += 1 + t: LspIndex.LspTarget* = LspIndex.ResolveWildcard( + ws, pool, table_pool, a.file, w, o.name) + if t is not None: + return t + return None + + +def WildcardNameMismatch(o: LspSymbols.LspOccurrence*, target: str) -> int: + """Skip wildcard resolution for occurrences whose name cannot match the target.""" + if o is None or target is None or o.name is None: + return 0 + if o.sym is not None or o.head is not None: + return 0 + return 1 if string.strcmp(o.name, target) != 0 else 0 + + def _TargetAt(ws: LspWorkspace.LspWorkspace*, pool: memhub.MemManager*, table_pool: memhub.MemManager*, a: LspSymbols.LspAnalysis*, doc: LspDocuments.LspDocument*, @@ -173,7 +199,7 @@ def _TargetAt(ws: LspWorkspace.LspWorkspace*, pool: memhub.MemManager*, return LspIndex.ResolveSymbol(ws, pool, table_pool, a.file, occ.sym, 0) if occ.head is not None and occ.rest is not None: return LspIndex.ResolveChain(ws, pool, table_pool, a.file, occ.head, occ.rest) - return None + return _WildcardTarget(ws, pool, table_pool, a, occ) s: LspSymbols.LspSymbol* = _SymAt(a, line0, ch) if s is not None: return LspIndex.ResolveSymbol(ws, pool, table_pool, a.file, s, 0) @@ -400,6 +426,13 @@ def _IdentityForOcc(pool: memhub.MemManager*, a: LspSymbols.LspAnalysis*, out_container[0] = t.container out_name[0] = t.name return 1 + return 0 + t2: LspIndex.LspTarget* = _WildcardTarget(ws, pool, table_pool, a, o) + if t2 is not None and t2.name is not None and t2.file is not None: + out_file[0] = LspFiles.Normalize(pool, t2.file) + out_container[0] = t2.container + out_name[0] = t2.name + return 1 return 0 @@ -531,6 +564,8 @@ def References(pool: memhub.MemManager*, tr: LspTransport.LspTransport*, while j < int(len(oa.occs)): o: LspSymbols.LspOccurrence* = oa.occs.get(sizet(j)) j += 1 + if WildcardNameMismatch(o, tn) != 0: + continue of: str = None oc: str = None on: str = None @@ -636,6 +671,8 @@ def DocumentHighlight(pool: memhub.MemManager*, tr: LspTransport.LspTransport*, while i2 < int(len(a.occs)): o2: LspSymbols.LspOccurrence* = a.occs.get(sizet(i2)) i2 += 1 + if WildcardNameMismatch(o2, tn) != 0: + continue of: str = None oc: str = None on: str = None diff --git a/App/lib/lsp/LspRename.vp b/App/lib/lsp/LspRename.vp index d080e30..90f54da 100644 --- a/App/lib/lsp/LspRename.vp +++ b/App/lib/lsp/LspRename.vp @@ -278,6 +278,8 @@ def Rename(pool: memhub.MemManager*, tr: LspTransport.LspTransport*, while j < int(len(oa.occs)): o: LspSymbols.LspOccurrence* = oa.occs.get(sizet(j)) j += 1 + if LspNavigation.WildcardNameMismatch(o, tn) != 0: + continue of: str = None oc: str = None on: str = None diff --git a/App/lib/lsp/LspSemantic.vp b/App/lib/lsp/LspSemantic.vp index b9c1926..53596ec 100644 --- a/App/lib/lsp/LspSemantic.vp +++ b/App/lib/lsp/LspSemantic.vp @@ -31,6 +31,7 @@ define ST_NUMBER = 10 define ST_OPERATOR = 11 define ST_DECORATOR = 12 define ST_MACRO = 13 +define ST_TYPE = 14 class LspSemToken: @@ -83,6 +84,7 @@ def Provider(pool: memhub.MemManager*) -> json.JsonValue*: json.array_append(pool, types, json.string_val(pool, "operator")) json.array_append(pool, types, json.string_val(pool, "decorator")) json.array_append(pool, types, json.string_val(pool, "macro")) + json.array_append(pool, types, json.string_val(pool, "type")) json.array_append(pool, mods, json.string_val(pool, "declaration")) json.array_append(pool, mods, json.string_val(pool, "readonly")) json.object_set(pool, legend, "tokenTypes", types) @@ -105,6 +107,8 @@ def _IsDefineMacro(s: LspSymbols.LspSymbol*) -> int: def _TypeFor(s: LspSymbols.LspSymbol*) -> int: if s is None: return ST_VARIABLE + if s.is_alias != 0: + return ST_TYPE if s.kind == LspSymbols.LSP_KIND_MODULE: return ST_NAMESPACE if s.kind == LspSymbols.LSP_KIND_CLASS: @@ -128,6 +132,8 @@ def _ModsFor(s: LspSymbols.LspSymbol*) -> int: """readonly = bit 1 in the legend; constants use the theme's constant color (as Vipeye does).""" if s is None: return 0 + if s.is_alias != 0: + return 0 if s.kind == LspSymbols.LSP_KIND_CONSTANT: return 2 return 0 @@ -137,6 +143,7 @@ class LspSemCtx: ws: LspWorkspace.LspWorkspace* table_pool: memhub.MemManager* file: str + wildcards: list[LspSymbols.LspSymbol*]* sym_keys: list[LspSymbols.LspSymbol*]* sym_tgts: list[LspIndex.LspTarget*]* chain_heads: list[LspSymbols.LspSymbol*]* @@ -153,6 +160,8 @@ class LspSemCtx: def _TypeForTarget(t: LspIndex.LspTarget*) -> int: if t is None: return -1 + if t.is_alias != 0: + return ST_TYPE if t.kind == LspSymbols.LSP_KIND_MODULE: return ST_NAMESPACE if t.kind == LspSymbols.LSP_KIND_CLASS: @@ -167,7 +176,7 @@ def _TypeForTarget(t: LspIndex.LspTarget*) -> int: def _ModsForTarget(t: LspIndex.LspTarget*) -> int: - if t is not None and t.kind == LspSymbols.LSP_KIND_CONSTANT: + if t is not None and t.is_alias == 0 and t.kind == LspSymbols.LSP_KIND_CONSTANT: return 2 return 0 @@ -209,12 +218,16 @@ def _CachedChain(ctx: LspSemCtx*, pool: memhub.MemManager*, if ctx is None or head is None or rest is None: return None slot: sizet = (_SemHash(rest) ^ _SemHash(head.name)) & sizet(LSP_SEM_CACHE_MASK) + if head.import_mod is not None: + slot = (slot ^ _SemHash(head.import_mod)) & sizet(LSP_SEM_CACHE_MASK) if ctx.chain_heads.get(slot) == head: cached: str = ctx.chain_rests.get(slot) if cached is not None and string.strcmp(cached, rest) == 0: return ctx.chain_tgts.get(slot) t: LspIndex.LspTarget* = None - if head.import_kind == LspSymbols.LSP_IMPORT_FROM: + if head.import_attr is not None and string.strcmp(head.import_attr, "*") == 0: + t = LspIndex.ResolveWildcard(ctx.ws, pool, ctx.table_pool, ctx.file, head, rest) + elif head.import_kind == LspSymbols.LSP_IMPORT_FROM: base: LspIndex.LspTarget* = _CachedSymbol(ctx, pool, head) if base is not None: t = LspIndex.ResolveMembers(pool, ctx.table_pool, base, rest) @@ -263,6 +276,16 @@ def _TokenForOcc(ctx: LspSemCtx*, pool: memhub.MemManager*, if t is not None: typ = _TypeForTarget(t) mods = _ModsForTarget(t) + if typ < 0 and o.head is None and o.rest is None and o.name is not None: + if ctx is not None and ctx.wildcards is not None: + wi: int = 0 + while wi < int(len(ctx.wildcards)) and typ < 0: + w: LspSymbols.LspSymbol* = ctx.wildcards.get(sizet(wi)) + wi += 1 + tw: LspIndex.LspTarget* = _CachedChain(ctx, pool, w, o.name) + if tw is not None: + typ = _TypeForTarget(tw) + mods = _ModsForTarget(tw) if typ < 0: # Builtin type names have no declaration; color them like class types. if o.builtin_type == 0: @@ -568,6 +591,7 @@ def Tokens(pool: memhub.MemManager*, tr: LspTransport.LspTransport*, ctx.ws = ws ctx.table_pool = docs.pool ctx.file = a.file + ctx.wildcards = a.wildcards ctx.sym_keys = list[LspSymbols.LspSymbol*](pool, 8) ctx.sym_tgts = list[LspIndex.LspTarget*](pool, 8) ctx.chain_heads = list[LspSymbols.LspSymbol*](pool, 8) diff --git a/App/lib/lsp/LspServer.vp b/App/lib/lsp/LspServer.vp index 3da785e..7375aea 100644 --- a/App/lib/lsp/LspServer.vp +++ b/App/lib/lsp/LspServer.vp @@ -474,7 +474,6 @@ def Run(pool: memhub.MemBuddy*, perm_pool: memhub.MemBuddy*) -> int: pass elif string.strcmp(method, "textDocument/didOpen") == 0: _HandleDidOpen(pool, tr, docs, ws, params) - LspIndex.Invalidate() elif string.strcmp(method, "textDocument/didChange") == 0: _HandleDidChange(pool, tr, docs, params) elif string.strcmp(method, "textDocument/didClose") == 0: diff --git a/App/lib/lsp/LspSymbols.vp b/App/lib/lsp/LspSymbols.vp index f625b82..9dc1b51 100644 --- a/App/lib/lsp/LspSymbols.vp +++ b/App/lib/lsp/LspSymbols.vp @@ -48,6 +48,8 @@ class LspSymbol: kind: int # 1 for `define` macro functions (colored as macro), 0 otherwise. is_macro: int + # 1 for typedef/type aliases (colored as type), 0 otherwise. + is_alias: int detail: str doc: str file: str @@ -108,6 +110,7 @@ class LspAnalysis: module_scope: LspScope* symbols: list[LspSymbol*]* occs: list[LspOccurrence*]* + wildcards: list[LspSymbol*]* root: list[LspSymbol*]* def __new__(self, pool: memhub.MemManager*) -> rawptr: @@ -464,6 +467,12 @@ def _BindType(pool: memhub.MemManager*, a: LspAnalysis*, scope: LspScope*, return head: LspSymbol* = _Lookup(a, scope, nm.id, 0) if head is None: + # A type imported by `from module import *` has no local binding; bind it to the + # wildcard module so member access can resolve through the index. + if _IsBuiltinTypeName(nm.id) == 0 and a.wildcards is not None: + if int(len(a.wildcards)) > 0: + sym.type_sym = a.wildcards.get(0) + sym.type_rest = nm.id return sym.type_sym = head sym.type_rest = None @@ -619,6 +628,22 @@ def _NameOnLine(pool: memhub.MemManager*, a: LspAnalysis*, line1: int, return None +def _IsTypeAliasDecl(decl: ast.AST*) -> int: + """1 when decl is `name: t.TypedefT = type` (the canonical typedef form).""" + if decl is None or decl.kind() != ast.ASTKind.AnnAssign: + return 0 + ann: ast.AST* = ((ast.AnnAssign*)(decl)).annotation + if ann is None: + return 0 + if ann.kind() == ast.ASTKind.Name: + nm: ast.Name* = (ast.Name*)(ann) + return 1 if nm.id is not None and string.strcmp(nm.id, "TypedefT") == 0 else 0 + if ann.kind() == ast.ASTKind.Attribute: + at: ast.Attribute* = (ast.Attribute*)(ann) + return 1 if at.attr is not None and string.strcmp(at.attr, "CTypedef") == 0 else 0 + return 0 + + def _Declare(pool: memhub.MemManager*, a: LspAnalysis*, scope: LspScope*, name: str, kind: int, decl: ast.AST*, line: int, col: int, detail: str) -> LspSymbol*: @@ -636,6 +661,8 @@ def _Declare(pool: memhub.MemManager*, a: LspAnalysis*, scope: LspScope*, fd: ast.FunctionDef* = (ast.FunctionDef*)(decl) if (fd.flags & ast.FLAG_IS_DEFINE) != 0: s.is_macro = 1 + if _IsTypeAliasDecl(decl) != 0: + s.is_alias = 1 s.sel = _NameSpan(pool, a.doc, line, col, name) if s.sel is None or _RangeTextIsName(pool, a, s.sel, name) == 0: if decl is None or (decl.kind() != ast.ASTKind.Import and decl.kind() != ast.ASTKind.ImportFrom): @@ -1188,6 +1215,17 @@ def _WalkStmt(pool: memhub.MemManager*, a: LspAnalysis*, scope: LspScope*, sym.import_attr = al.name sym.import_level = ifr.level _DeclImportOcc(pool, a, scope, al, bound, sym) + elif ifr.module is not None and a.wildcards is not None: + # Wildcard imports bind no local names; keep the module for on-demand + # resolution of unresolved names (highlighting/navigation). + w: LspSymbol* = _NewSymbol(pool, "*", LSP_KIND_MODULE, node, scope) + if w is not None: + w.file = a.file + w.import_kind = LSP_IMPORT_FROM + w.import_mod = ifr.module + w.import_attr = "*" + w.import_level = ifr.level + a.wildcards.append(w) i += 1 return # Pass/Break/Continue/FallThrough and unknown statements carry no names. @@ -1484,6 +1522,7 @@ def Analyze(pool: memhub.MemManager*, table_pool: memhub.MemManager*, a.file = file a.symbols = list[LspSymbol*](pool, 64) a.occs = list[LspOccurrence*](pool, 128) + a.wildcards = list[LspSymbol*](pool, 8) a.module_scope = _NewScope(pool, LSP_SCOPE_MODULE, None, None, None) a.tree = ast.parse_ex(doc.text, pool, table_pool) a.errors = ast.get_parse_errors() diff --git a/App/lib/lsp/LspWorkspace.vp b/App/lib/lsp/LspWorkspace.vp index d30f3a9..f51e734 100644 --- a/App/lib/lsp/LspWorkspace.vp +++ b/App/lib/lsp/LspWorkspace.vp @@ -10,6 +10,68 @@ import lib.lsp.LspTransport as LspTransport import lib.Projectrans.Config as Config +def _AbsJoin(pool: memhub.MemManager*, base: str, path: str) -> str: + """Resolve a project-relative path against the project directory (any relative form).""" + if path is None or path[0] == 0: + return path + plen: sizet = string.strlen(path) + if path[0] == '/' or path[0] == '\\': + return LspPools.DupStr(pool, path) + if plen >= 2 and path[1] == ':': + return LspPools.DupStr(pool, path) + if base is None or base[0] == 0: + return LspPools.DupStr(pool, path) + joined: str = os.path.join(base, path) + if joined is None: + return LspPools.DupStr(pool, path) + norm: str = os.path.normpath(joined) + if norm is None: + norm = joined + return LspPools.DupStr(pool, norm) + + +def _AbsList(pool: memhub.MemManager*, base: str, dirs: str) -> str: + """Resolve a space-separated directory list against base.""" + if dirs is None or dirs[0] == 0: + return dirs + n: sizet = string.strlen(dirs) + out: str = pool.alloc(n * 3 + 256) + if out is None: + return dirs + out[0] = 0 + pos: sizet = 0 + first: int = 1 + i: sizet = 0 + while i < n: + while i < n and dirs[i] == ' ': + i += 1 + if i >= n: + break + start: sizet = i + while i < n and dirs[i] != ' ': + i += 1 + seg_len: sizet = i - start + if seg_len == 0: + continue + seg: str = pool.alloc(seg_len + 1) + if seg is None: + continue + string.memcpy(seg, dirs + start, seg_len) + seg[seg_len] = 0 + resolved: str = _AbsJoin(pool, base, str(seg)) + if resolved is None: + continue + alen: sizet = string.strlen(resolved) + if first == 0: + out[pos] = ' ' + pos += 1 + string.strcpy(out + pos, resolved) + pos += alen + first = 0 + out[pos] = 0 + return str(out) + + def _HexVal(ch: char) -> int: if ch >= '0' and ch <= '9': return int(ch) - int('0') @@ -145,8 +207,9 @@ class LspWorkspace: rc: int = Config.Load_project_config(project) if rc == 0: Config.resolve_paths(os.path.dirname(project)) - self.source_dir = Config.SourceDir - self.includes = Config.IncludesDirs + base: str = os.path.dirname(project) + self.source_dir = _AbsJoin(self.pool, base, Config.SourceDir) + self.includes = _AbsList(self.pool, base, Config.IncludesDirs) Config._mbuddy = old_pool return rc diff --git a/Tools/lsp/fixtures/gate10_extra.vp b/Tools/lsp/fixtures/gate10_extra.vp new file mode 100644 index 0000000..471f714 --- /dev/null +++ b/Tools/lsp/fixtures/gate10_extra.vp @@ -0,0 +1,12 @@ +"""Gate 10 fixture: exports consumed through `from module import *`.""" + + +class Extra: + amount: int + + +define EXTRA_CONST = 5 + + +def extra_fn(x: int) -> int: + return x diff --git a/Tools/lsp/fixtures/gate10_use.vp b/Tools/lsp/fixtures/gate10_use.vp index f00faed..6290eb4 100644 --- a/Tools/lsp/fixtures/gate10_use.vp +++ b/Tools/lsp/fixtures/gate10_use.vp @@ -2,9 +2,14 @@ from .gate10_types import Widget, DOUBLE from .gate10_stub import StubThing +from .gate10_extra import * import gate10_types +def star_use(e: Extra) -> int: + return extra_fn(EXTRA_CONST) + e.amount + + def macro_use(n: int) -> int: return DOUBLE(n) diff --git a/Tools/lsp/probe.py b/Tools/lsp/probe.py index f1a756b..fc8a4d4 100644 --- a/Tools/lsp/probe.py +++ b/Tools/lsp/probe.py @@ -1420,7 +1420,7 @@ def gate10(exe: str) -> int: # The fixtures live outside the project's source roots; copy them into an isolated workspace # so the cross-file index contains the module, the stub and the consumer. workspace = Path(tempfile.mkdtemp(prefix="transpyc_lsp_gate10_")) - names = ("gate10_types.vp", "gate10_stub.vpi", "gate10_use.vp") + names = ("gate10_types.vp", "gate10_stub.vpi", "gate10_extra.vp", "gate10_use.vp") for name in names: shutil.copy(fixtures / name, workspace / name) type_path = workspace / "gate10_types.vp" @@ -1449,6 +1449,7 @@ def gate10(exe: str) -> int: legend = response.get("result", {}).get("capabilities", {}).get("semanticTokensProvider", {}).get("legend", {}) sem_types = legend.get("tokenTypes", []) + sem_mods = legend.get("tokenModifiers", []) semantic = client.request( "textDocument/semanticTokens/full", {"textDocument": {"uri": uri}}, timeout=120.0 ).get("result") @@ -1461,11 +1462,30 @@ def gate10(exe: str) -> int: ("macro call", "return DOUBLE", 7, "macro"), ("member field", "s.value", 2, "property"), ("member method", "s.resize", 2, "method"), + ("star class", "e: Extra", 3, "class"), + ("star function", "return extra_fn", 7, "function"), + ("star constant", "EXTRA_CONST", 0, "variable"), + ("star member", "e.amount", 2, "property"), ): pos = _position(text, needle) pos["character"] += delta if not _find_token(sem_tokens, pos["line"], pos["character"], sem_types.index(token_name)): raise AssertionError(f"missing cross-module semantic token for {label}: {pos}") + const_pos = _position(text, "EXTRA_CONST") + if not _find_token_mods(sem_tokens, const_pos["line"], const_pos["character"], + sem_types.index("variable"), + 1 << sem_mods.index("readonly")): + raise AssertionError(f"wildcard constant is not readonly: {const_pos}") + star_def = client.request( + "textDocument/definition", + {"textDocument": {"uri": uri}, "position": {"line": _position(text, "e: Extra")["line"], + "character": _position(text, "e: Extra")["character"] + 3}}, + timeout=120.0, + ).get("result") + if not isinstance(star_def, list) or not star_def: + raise AssertionError(f"wildcard definition failed: {star_def}") + if not star_def[0]["uri"].endswith("gate10_extra.vp"): + raise AssertionError(f"wildcard definition target is wrong: {star_def}") def at(needle: str, delta: int = 0, occurrence: int = 0) -> dict[str, int]: pos = _position(text, needle, occurrence)