From 1968748ea59a1aa9e96feea77e5b1849c8fe9e93 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Wed, 28 Oct 2026 22:36:07 +0800 Subject: [PATCH] feat(lsp): cross-file signatures, returns, augassign and richer types Adds LspSigCache: per-file signature cache under /.viperc-lsp-cache, keyed by content hash+size, invalidated on didChange/didSave; resolves from-import, module-attribute and wildcard calls without building the workspace index. Extends LspTypes with list/array/pointer structural checks, array/string initializer lengths and list-literal element checks; adds return-type consistency, augmented-assignment narrowing, constant folding for unary/binop and string/list subscripts. Diagnostics use a per-line occurrence bucket, a dedicated cache work pool and statement/expression budgets so large include files stay responsive. gate13 covers the new rules. --- App/lib/lsp/LspChecks.vp | 211 ++++++++++++++- App/lib/lsp/LspNavigation.vp | 14 + App/lib/lsp/LspServer.vp | 7 + App/lib/lsp/LspSigCache.vp | 488 +++++++++++++++++++++++++++++++++++ App/lib/lsp/LspSymbols.vp | 106 +++++++- App/lib/lsp/LspTypes.vp | 121 ++++++++- Tools/lsp/probe.py | 34 ++- 7 files changed, 958 insertions(+), 23 deletions(-) create mode 100644 App/lib/lsp/LspSigCache.vp diff --git a/App/lib/lsp/LspChecks.vp b/App/lib/lsp/LspChecks.vp index 0232bcd..db0fa4b 100644 --- a/App/lib/lsp/LspChecks.vp +++ b/App/lib/lsp/LspChecks.vp @@ -10,6 +10,7 @@ import lib.lsp.LspIndex as LspIndex import lib.lsp.LspWorkspace as LspWorkspace import lib.lsp.LspNavigation as LspNavigation import lib.lsp.LspTypes as LspTypes +import lib.lsp.LspSigCache as LspSigCache # Semantic checks that are cheap enough to run on every diagnostics request. @@ -19,6 +20,11 @@ import lib.lsp.LspTypes as LspTypes define LSP_CHECK_MAX_HEADER_LINES = 400 define LSP_CHECK_DIAG_CAP = 256 +define LSP_CHECK_EXPR_BUDGET = 5000 +define LSP_CHECK_STMT_BUDGET = 2000 + +_walk_count: int = 0 +_expr_count: int = 0 def _ByteAt(doc: LspDocuments.LspDocument*, line0: int, col: int) -> char: @@ -372,7 +378,16 @@ def _TargetAtNode(ws: LspWorkspace.LspWorkspace*, pool: memhub.MemManager*, occ: LspSymbols.LspOccurrence* = LspNavigation._OccAt(a, line0, ch) if occ is not None: if occ.sym is not None: + if occ.sym.import_kind != LspSymbols.LSP_IMPORT_NONE: + t_imp: LspIndex.LspTarget* = LspSigCache.ResolveOccurrence( + pool, table_pool, ws, a, occ) + if t_imp is not None: + return t_imp return LspIndex.TargetFor(pool, occ.sym) + t_cross: LspIndex.LspTarget* = LspSigCache.ResolveOccurrence( + pool, table_pool, ws, a, occ) + if t_cross is not None: + return t_cross return None s: LspSymbols.LspSymbol* = LspNavigation._SymAt(a, line0, ch) if s is not None: @@ -526,6 +541,50 @@ def _DeclAnnotationText(pool: memhub.MemManager*, doc: LspDocuments.LspDocument* return _Trim(pool, LspSource.SliceBytes(pool, doc, line0, start, line0, j)) +def _FoldInt(op: int, a: int64, b: int64, ok: int*) -> int64: + """Fold simple integer constant arithmetic; ok=0 for unsupported/undefined operations.""" + ok[0] = 0 + if op == ast.OpKind.Add: + ok[0] = 1 + return a + b + if op == ast.OpKind.Sub: + ok[0] = 1 + return a - b + if op == ast.OpKind.Mult: + ok[0] = 1 + return a * b + if op == ast.OpKind.BitAnd: + ok[0] = 1 + return a & b + if op == ast.OpKind.BitOr: + ok[0] = 1 + return a | b + if op == ast.OpKind.BitXor: + ok[0] = 1 + return a ^ b + if op == ast.OpKind.Div or op == ast.OpKind.FloorDiv: + if b == 0: + return 0 + ok[0] = 1 + return a / b + if op == ast.OpKind.Mod: + if b == 0: + return 0 + ok[0] = 1 + return a % b + if op == ast.OpKind.LShift: + if b < 0 or b > 63: + return 0 + ok[0] = 1 + return a << b + if op == ast.OpKind.RShift: + if b < 0 or b > 63: + return 0 + ok[0] = 1 + return a >> b + return 0 + + def _ExprInfo(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*, a: LspSymbols.LspAnalysis*, ws: LspWorkspace.LspWorkspace*, table_pool: memhub.MemManager*, e: ast.AST*) -> LspExprInfo*: @@ -573,6 +632,15 @@ def _ExprInfo(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*, inner.cval = -inner.cval return inner return inner + if uo.op == ast.OpKind.Invert: + inner2: LspExprInfo* = _ExprInfo(pool, doc, a, ws, table_pool, uo.operand) + if inner2 is not None and inner2.is_const != 0 and inner2.ty is not None \ + and inner2.ty.kind == LspTypes.LT_INT: + inner2.cval = ~inner2.cval + return inner2 + return inner2 + if uo.op == ast.OpKind.Not: + return _Info(pool, LspTypes.ParseType(pool, "bool")) return _Info(pool, _NewUnknown(pool)) if k == ast.ASTKind.BinOp: bo: ast.BinOp* = ast.BinOp*(e) @@ -583,9 +651,23 @@ def _ExprInfo(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*, out_i: LspTypes.LspType* = LspTypes.ParseType(pool, "int64") if out_i is not None: out_i.bits = li.ty.bits - if ri.ty.bits > out_i.bits: - out_i.bits = ri.ty.bits - return _Info(pool, out_i) + if bo.op == ast.OpKind.LShift or bo.op == ast.OpKind.RShift: + out_i.bits = li.ty.bits + else: + if ri.ty.bits > out_i.bits: + out_i.bits = ri.ty.bits + out_i.unsigned = li.ty.unsigned & ri.ty.unsigned + info_i: LspExprInfo* = _Info(pool, out_i) + if li.is_const != 0 and ri.is_const != 0: + fok: int = 0 + cv: int64 = _FoldInt(bo.op, li.cval, ri.cval, &fok) + if fok != 0: + info_i.is_const = 1 + info_i.cval = cv + if cv < -2147483647 - 1 or cv > 4294967295: + if out_i is not None: + out_i.bits = 64 + return info_i if li.ty.kind == LspTypes.LT_FLOAT and ri.ty.kind == LspTypes.LT_FLOAT: out_f: LspTypes.LspType* = LspTypes.ParseType(pool, "double") if out_f is not None: @@ -608,8 +690,11 @@ def _ExprInfo(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*, sb: ast.Subscript* = ast.Subscript*(e) vi: LspExprInfo* = _ExprInfo(pool, doc, a, ws, table_pool, sb.value) if vi is not None and vi.ty is not None: - if vi.ty.kind == LspTypes.LT_ARRAY or vi.ty.kind == LspTypes.LT_PTR: + if vi.ty.kind == LspTypes.LT_ARRAY or vi.ty.kind == LspTypes.LT_PTR \ + or vi.ty.kind == LspTypes.LT_LIST: return _Info(pool, vi.ty.pointee) + if vi.ty.kind == LspTypes.LT_STR: + return _Info(pool, LspTypes.ParseType(pool, "char")) return _Info(pool, _NewUnknown(pool)) if k == ast.ASTKind.Starred: st: ast.Starred* = ast.Starred*(e) @@ -638,8 +723,39 @@ def _ExprInfo(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*, or t2.kind == LspSymbols.LSP_KIND_METHOD): sig: LspTypes.LspSig* = LspTypes.ParseSignature(pool, t2.detail) if sig is not None and sig.valid != 0: + if sig.ret is not None and sig.ret.kind == LspTypes.LT_VOID \ + and (t2.is_macro != 0 or (t2.detail is not None + and string.strncmp(t2.detail, "define ", 7) == 0)): + return _Info(pool, _NewUnknown(pool)) return _Info(pool, sig.ret) return _Info(pool, _NewUnknown(pool)) + if k == ast.ASTKind.List or k == ast.ASTKind.Tuple: + elts_i: list[ast.AST*]* = None + if k == ast.ASTKind.List: + elts_i = ast.List*(e).elts + else: + elts_i = ast.Tuple*(e).elts + if elts_i is not None and int(len(elts_i)) > 0: + first: LspExprInfo* = _ExprInfo(pool, doc, a, ws, table_pool, elts_i.get(sizet(0))) + if first is not None and first.ty is not None: + same: int = 1 + i_l: int = 1 + while i_l < int(len(elts_i)): + ei: LspExprInfo* = _ExprInfo(pool, doc, a, ws, table_pool, elts_i.get(sizet(i_l))) + i_l += 1 + if ei is None or ei.ty is None or ei.ty.kind != first.ty.kind: + same = 0 + break + if first.ty.kind == LspTypes.LT_INT and ei.ty.bits != first.ty.bits: + same = 0 + break + if first.ty.kind == LspTypes.LT_FLOAT and ei.ty.fbits != first.ty.fbits: + same = 0 + break + if same != 0: + arr: LspTypes.LspType* = LspTypes.NewArray(pool, first.ty, int(len(elts_i))) + return _Info(pool, arr) + return _Info(pool, _NewUnknown(pool)) return _Info(pool, _NewUnknown(pool)) @@ -653,6 +769,44 @@ def _CheckAssignValue(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*, value: ast.AST*, context: str, errors: list[ast.ParseError*]*): if dst is None or value is None or dst.kind == LspTypes.LT_UNKNOWN: return + if (dst.kind == LspTypes.LT_ARRAY or dst.kind == LspTypes.LT_LIST) \ + and value.kind() == ast.ASTKind.List: + lst: ast.List* = ast.List*(value) + elem_t: LspTypes.LspType* = dst.pointee + if lst.elts is not None: + n_el: int = int(len(lst.elts)) + if dst.kind == LspTypes.LT_ARRAY and dst.count > 0 and n_el > dst.count: + msg_buf: bytes = pool.alloc(sizet(256)) + if msg_buf is not None: + stdio.snprintf(msg_buf, sizet(256), + "array initializer element count %d exceeds the declared array length %d;" + " out-of-bounds writes are forbidden", n_el, dst.count) + _EmitTypeError(pool, doc, value, str(msg_buf), errors) + i_el: int = 0 + while i_el < n_el: + el: ast.AST* = lst.elts.get(sizet(i_el)) + ctx_buf: bytes = pool.alloc(sizet(96)) + ctx: str = None + if ctx_buf is not None: + stdio.snprintf(ctx_buf, sizet(96), "element %d: ", i_el + 1) + ctx = str(ctx_buf) + _CheckAssignValue(pool, doc, a, ws, table_pool, elem_t, el, ctx, errors) + i_el += 1 + return + if dst.kind == LspTypes.LT_ARRAY and dst.count > 0 and value.kind() == ast.ASTKind.Constant: + cn: ast.Constant* = ast.Constant*(value) + if cn.const_kind == ast.CONST_STR and cn.str_val is not None \ + and dst.pointee is not None and dst.pointee.kind == LspTypes.LT_INT \ + and dst.pointee.bits == 8: + n_str: int = int(string.strlen(cn.str_val)) + 1 + if n_str > dst.count: + msg_s: bytes = pool.alloc(sizet(256)) + if msg_s is not None: + stdio.snprintf(msg_s, sizet(256), + "string literal length %d exceeds the declared char array length %d;" + " out-of-bounds writes are forbidden", n_str, dst.count) + _EmitTypeError(pool, doc, value, str(msg_s), errors) + return info: LspExprInfo* = _ExprInfo(pool, doc, a, ws, table_pool, value) if info is None or info.ty is None: return @@ -679,6 +833,10 @@ def _WalkChecksExprs(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*, errors: list[ast.ParseError*]*): if e is None: return + global _expr_count + _expr_count += 1 + if _expr_count > LSP_CHECK_EXPR_BUDGET: + return k: int = e.kind() if k == ast.ASTKind.Call: call: ast.Call* = ast.Call*(e) @@ -860,19 +1018,31 @@ def _CheckCall(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*, def _WalkChecks(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*, a: LspSymbols.LspAnalysis*, ws: LspWorkspace.LspWorkspace*, table_pool: memhub.MemManager*, stmts: list[ast.AST*]*, - errors: list[ast.ParseError*]*): + errors: list[ast.ParseError*]*, ret_t: LspTypes.LspType* = None): if stmts is None: return i: int = 0 while i < int(len(stmts)): s: ast.AST* = stmts.get(sizet(i)) i += 1 + global _walk_count + _walk_count += 1 + if _walk_count > LSP_CHECK_STMT_BUDGET: + return if s is None: continue k: int = s.kind() if k == ast.ASTKind.FunctionDef: fd: ast.FunctionDef* = ast.FunctionDef*(s) - _WalkChecks(pool, doc, a, ws, table_pool, fd.children, errors) + fn_ret: LspTypes.LspType* = None + rsl: int = 0 + rsc: int = 0 + rel: int = 0 + rec: int = 0 + if _AnnotationSpan(doc, fd, &rsl, &rsc, &rel, &rec) != 0: + fn_ret = LspTypes.ParseType( + pool, _Trim(pool, LspSource.SliceBytes(pool, doc, rsl, rsc, rel, rec))) + _WalkChecks(pool, doc, a, ws, table_pool, fd.children, errors, fn_ret) continue if k == ast.ASTKind.ClassDef: _WalkChecks(pool, doc, a, ws, table_pool, s.children, errors) @@ -903,10 +1073,36 @@ def _WalkChecks(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*, continue if k == ast.ASTKind.AugAssign: ag: ast.AugAssign* = ast.AugAssign*(s) + if ag.target is not None and ag.value is not None: + tt_a: LspIndex.LspTarget* = _TargetAtNode(ws, pool, table_pool, a, doc, ag.target) + if tt_a is not None and tt_a.detail is not None: + dt_a: LspTypes.LspType* = _TypeFromDetail(pool, tt_a.detail) + if dt_a is not None and dt_a.kind == LspTypes.LT_INT: + vi_a: LspExprInfo* = _ExprInfo(pool, doc, a, ws, table_pool, ag.value) + if vi_a is not None and vi_a.ty is not None \ + and vi_a.ty.kind == LspTypes.LT_INT: + code_a: int = LspTypes.CheckAssign( + vi_a.ty, dt_a, vi_a.is_const, vi_a.cval) + if code_a == LspTypes.LT_NARROW \ + or code_a == LspTypes.LT_NARROW_OVERFLOW \ + or code_a == LspTypes.LT_BOOL_NARROW: + msg_a: bytes = pool.alloc(sizet(320)) + if msg_a is not None: + stdio.snprintf(msg_a, sizet(320), + "augmented assignment operand too wide: the right-hand side" + " needs %d bits, but the target provides only %d bits;" + " an implicit narrowing is forbidden", + vi_a.ty.bits, dt_a.bits) + _EmitTypeError(pool, doc, ag.value, str(msg_a), errors) _WalkChecksExprs(pool, doc, a, ws, table_pool, ag.value, errors) continue if k == ast.ASTKind.Return: rt: ast.Return* = ast.Return*(s) + if rt.value is not None and ret_t is not None \ + and ret_t.kind != LspTypes.LT_VOID and ret_t.kind != LspTypes.LT_NONE \ + and ret_t.kind != LspTypes.LT_UNKNOWN: + _CheckAssignValue(pool, doc, a, ws, table_pool, ret_t, rt.value, + "return value: ", errors) _WalkChecksExprs(pool, doc, a, ws, table_pool, rt.value, errors) continue if k == ast.ASTKind.Expr: @@ -960,6 +1156,9 @@ def Check(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*, a: LspSymbols.LspAnalysis*, ws: LspWorkspace.LspWorkspace*, table_pool: memhub.MemManager*, tree: ast.AST*) -> list[ast.ParseError*]*: """Run semantic checks over a successfully parsed document.""" + global _walk_count, _expr_count + _walk_count = 0 + _expr_count = 0 errors: list[ast.ParseError*]* = list[ast.ParseError*](pool, LSP_CHECK_DIAG_CAP) if errors is None or tree is None: return None diff --git a/App/lib/lsp/LspNavigation.vp b/App/lib/lsp/LspNavigation.vp index 805f758..ee80823 100644 --- a/App/lib/lsp/LspNavigation.vp +++ b/App/lib/lsp/LspNavigation.vp @@ -68,6 +68,20 @@ def _OccAt(a: LspSymbols.LspAnalysis*, line0: int, ch: int) -> LspSymbols.LspOcc if a is None or a.occs is None: return None best: LspSymbols.LspOccurrence* = None + if a.occ_line_head is not None and line0 >= 0 and line0 < a.occ_line_count: + j: int = a.occ_line_head[line0] + while j >= 0: + o: LspSymbols.LspOccurrence* = a.occs.get(sizet(j)) + if a.occ_next is None: + break + j = a.occ_next[j] + if o is None: + continue + if ch >= o.sc and ch < o.ec: + return o + if ch == o.ec and best is None: + best = o + return best i: int = 0 while i < int(len(a.occs)): o: LspSymbols.LspOccurrence* = a.occs.get(sizet(i)) diff --git a/App/lib/lsp/LspServer.vp b/App/lib/lsp/LspServer.vp index c103a29..b8969ec 100644 --- a/App/lib/lsp/LspServer.vp +++ b/App/lib/lsp/LspServer.vp @@ -21,6 +21,7 @@ import lib.lsp.LspSignature as LspSignature import lib.lsp.LspRename as LspRename import lib.lsp.LspSemantic as LspSemantic import lib.lsp.LspCodeAction as LspCodeAction +import lib.lsp.LspSigCache as LspSigCache define LSP_SERVER_NAME = "viperc-lsp" @@ -260,6 +261,9 @@ def _HandleDidChange(pool: memhub.MemManager*, tr: LspTransport.LspTransport*, LspTransport.StderrWrite("viperc-lsp: didChange failed to apply a content change\n") return -1 i += 1 + file_chg: str = LspWorkspace.DecodeUri(pool, uri) + if file_chg is not None: + LspSigCache.Invalidate(pool, docs.pool, file_chg) if _pull_diagnostics != 0: return 0 return LspDiagnostics.Publish(pool, tr, docs, ws, uri) @@ -288,6 +292,9 @@ def _HandleDidSave(pool: memhub.MemManager*, tr: LspTransport.LspTransport*, uri: str = LspRpc.GetStringField(td, "uri") if uri is None: return -1 + file_save: str = LspWorkspace.DecodeUri(pool, uri) + if file_save is not None: + LspSigCache.Invalidate(pool, docs.pool, file_save) if _pull_diagnostics != 0: return 0 return LspDiagnostics.Publish(pool, tr, docs, ws, uri) diff --git a/App/lib/lsp/LspSigCache.vp b/App/lib/lsp/LspSigCache.vp new file mode 100644 index 0000000..f352302 --- /dev/null +++ b/App/lib/lsp/LspSigCache.vp @@ -0,0 +1,488 @@ +from stdint import * +import string +import stdio +import os +import io +import memhub +import lib.lsp.LspFiles as LspFiles +import lib.lsp.LspSource as LspSource +import lib.lsp.LspDocuments as LspDocuments +import lib.lsp.LspSymbols as LspSymbols +import lib.lsp.LspIndex as LspIndex +import lib.lsp.LspWorkspace as LspWorkspace +import lib.lsp.LspPools as LspPools + + +# On-demand cross-file signature cache. +# +# Diagnostics must not build the whole workspace symbol index on every publish. This module keeps +# one small text file per referenced source under `/.viperc-lsp-cache/`, keyed by the +# file's FNV hash and validated by its size. A cache miss parses only that file with +# LspSymbols.Analyze, stores its module/class signatures, and returns the requested entry. + +define LSP_SIG_CACHE_DIR = ".viperc-lsp-cache" +define LSP_SIG_CACHE_LINE_CAP = 4096 + + +class LspSigEntry: + file: str + container: str + name: str + kind: int + detail: str + + def __new__(self, pool: memhub.MemManager*) -> rawptr: + p: LspSigEntry* = (LspSigEntry*)(pool.alloc(LspSigEntry.__sizeof__())) + if p: + string.memset(p, 0, LspSigEntry.__sizeof__()) + return rawptr(p) + + +_cache_dir: str = None +_entries: LspSigEntry** = None +_entry_count: int = 0 +_work: LspPools.LspPool* = None +_ready: int = 0 +_map_vals: LspSigEntry** = None + +define LSP_SIG_MAP_CAP = 262144 +define LSP_SIG_MAP_MASK = 262143 + + +def _HashPath(path: str) -> uint32: + h: uint32 = uint32(2166136261) + if path is None: + return h + i: sizet = 0 + while path[i] != 0: + h = (h ^ uint32(path[i])) * uint32(16777619) + i += 1 + return h + + +def _HashText(text: str) -> uint32: + h: uint32 = uint32(2166136261) + if text is None: + return h + i: sizet = 0 + while text[i] != 0: + h = (h ^ uint32(text[i])) * uint32(16777619) + i += 1 + return h + + +def _HexVal(ch: char) -> int: + if ch >= '0' and ch <= '9': + return int(ch) - 48 + if ch >= 'a' and ch <= 'f': + return int(ch) - 87 + if ch >= 'A' and ch <= 'F': + return int(ch) - 55 + return -1 + + +def _Hex32(pool: memhub.MemManager*, v: uint32) -> str: + buf: bytes = pool.alloc(sizet(16)) + if buf is None: + return None + stdio.snprintf(buf, sizet(16), "%08X", v) + return str(buf) + + +def _Sanitize(pool: memhub.MemManager*, s: str) -> str: + """Replace line breaks/tabs so one cache line holds one signature.""" + if s is None: + return "" + n: sizet = string.strlen(s) + buf: bytes = pool.alloc(n + 1) + if buf is None: + return "" + i: sizet = 0 + while i < n: + ch: char = s[i] + if ch == '\t' or ch == '\n' or ch == '\r': + buf[i] = ' ' + else: + buf[i] = ch + i += 1 + buf[n] = 0 + return str(buf) + + +def _Dup(pool: memhub.MemManager*, s: str) -> str: + if s is None: + return None + n: sizet = string.strlen(s) + buf: bytes = pool.alloc(n + 1) + if buf is None: + return None + string.memcpy(buf, s, n + 1) + return str(buf) + + +def Ensure(ws: LspWorkspace.LspWorkspace*, table_pool: memhub.MemManager*) -> int: + global _cache_dir, _entries, _entry_count, _work, _ready, _map_vals + if _ready != 0: + return 0 + base: str = None + if ws is not None: + project: str = ws.ProjectPath() + if project is not None: + base = LspFiles.DirName(table_pool, project) + if base is None: + base = ws.SourceDir() + if base is None: + base = LspFiles.Normalize(table_pool, os.getcwd()) + if base is None: + return -1 + cache: str = LspFiles.Join(table_pool, base, LSP_SIG_CACHE_DIR) + if cache is None: + return -1 + if os.isdir(cache) == 0: + os.mkdir(cache) + _cache_dir = cache + _entries = (LspSigEntry**)(table_pool.alloc(sizet(65536) * sizet(8))) + if _entries is None: + _ready = 0 + return -1 + string.memset(rawptr(_entries), 0, sizet(65536) * sizet(8)) + _entry_count = 0 + _work = LspPools.Create(table_pool, sizet(33554432)) + if _work is None: + _ready = 0 + return -1 + _ready = 1 + return 0 + + +def _CachePath(pool: memhub.MemManager*, file: str) -> str: + if _cache_dir is None or file is None: + return None + h: str = _Hex32(pool, _HashPath(file)) + if h is None: + return None + return LspFiles.Join(pool, _cache_dir, _Cat(pool, h, ".sig")) + + +def _MapHash(file: str, name: str) -> int: + return int(_HashPath(file) ^ _HashPath(name)) & LSP_SIG_MAP_MASK + + +def _InsertMap(e: LspSigEntry*): + # Linear entry lookup is used; this hook stays for call-site symmetry. + return + + +def _RebuildMap(): + return + + +def _FindEntry(file: str, name: str) -> LspSigEntry*: + if _entries is None or file is None or name is None: + return None + i: int = 0 + while i < _entry_count: + e: LspSigEntry* = _entries[i] + i += 1 + if e is None or e.container is not None: + continue + if LspFiles.PathEqual(e.file, file) != 0 and string.strcmp(e.name, name) == 0: + return e + return None + + +def _AddEntry(pool: memhub.MemManager*, file: str, container: str, name: str, + kind: int, detail: str) -> LspSigEntry*: + global _entry_count + if _entries is None or name is None: + return None + if _entry_count >= 65536: + return None + e: LspSigEntry* = LspSigEntry(pool) + if e is None: + return None + e.file = _Dup(pool, file) + e.container = _Dup(pool, container) + e.name = _Dup(pool, name) + e.kind = kind + e.detail = _Dup(pool, detail) + _entries[_entry_count] = e + _entry_count += 1 + _InsertMap(e) + return e + + +def _LoadCache(pool: memhub.MemManager*, table_pool: memhub.MemManager*, + file: str, cache_path: str, + file_size: int64, file_hash: uint32) -> int: + if not os.isfile(cache_path): + return 0 + text: str = LspFiles.ReadSource(pool, cache_path) + if text is None: + return 0 + n: sizet = string.strlen(text) + if n < 12 or string.strncmp(text, "hash=", 5) != 0: + return 0 + i: sizet = 5 + hv: uint32 = uint32(0) + while i < n: + d: int = _HexVal(text[i]) + if d < 0: + break + hv = hv * uint32(16) + uint32(d) + i += 1 + if i >= n or text[i] != ' ': + return 0 + i += 1 + if i + 5 >= n or string.strncmp(text + i, "size=", 5) != 0: + return 0 + i += 5 + sz: int64 = 0 + while i < n and text[i] >= '0' and text[i] <= '9': + sz = sz * 10 + int64(int(text[i]) - 48) + i += 1 + if hv != file_hash or sz != file_size: + return 0 + while i < n and text[i] != '\n': + i += 1 + if i < n: + i += 1 + while i < n: + start: sizet = i + while i < n and text[i] != '\n': + i += 1 + line_end: sizet = i + if i < n: + i += 1 + if line_end <= start: + continue + # kind \t container \t name \t detail + f1: sizet = start + f2: sizet = f1 + while f1 < line_end and text[f1] != '\t': + f1 += 1 + f2 = f1 + 1 + c1: sizet = f2 + c2: sizet = c1 + while c2 < line_end and text[c2] != '\t': + c2 += 1 + n1: sizet = c2 + 1 + n2: sizet = n1 + while n2 < line_end and text[n2] != '\t': + n2 += 1 + d1: sizet = n2 + 1 + if f1 >= line_end or c2 > line_end or n2 > line_end: + continue + kind_txt: str = _CopyRange(table_pool, text, start, f1) + cont_txt: str = _CopyRange(table_pool, text, f2, c2) + name_txt: str = _CopyRange(table_pool, text, n1, n2) + det_txt: str = _CopyRange(table_pool, text, d1, line_end) + kind: int = string.atoi(kind_txt) + _AddEntry(table_pool, file, cont_txt if string.strlen(cont_txt) > 0 else None, + name_txt, kind, det_txt) + return 1 + + +def _CopyRange(pool: memhub.MemManager*, s: str, a: sizet, b: sizet) -> str: + if s is None or b < a: + return "" + n: sizet = b - a + buf: bytes = pool.alloc(n + 1) + if buf is None: + return "" + string.memcpy(buf, s + a, n) + buf[n] = 0 + return str(buf) + + +def _WriteCache(pool: memhub.MemManager*, file: str, cache_path: str, + file_size: int64, file_hash: uint32): + f: io.File* = io.File(cache_path, io.MODE.W) + if f is None or f.closed: + return + hdr: bytes = pool.alloc(sizet(64)) + if hdr is not None: + stdio.snprintf(hdr, sizet(64), "hash=%08X size=%lld\n", file_hash, file_size) + hstr: str = str(hdr) + f.write(hstr, sizet(string.strlen(hstr))) + i: int = 0 + while i < _entry_count: + e: LspSigEntry* = _entries[i] + i += 1 + if e is None or LspFiles.PathEqual(e.file, file) == 0: + continue + line: str = _Itoa(pool, int64(e.kind)) + line = _Cat(pool, line, "\t") + line = _Cat(pool, line, e.container if e.container is not None else "") + line = _Cat(pool, line, "\t") + line = _Cat(pool, line, e.name) + line = _Cat(pool, line, "\t") + line = _Cat(pool, line, _Sanitize(pool, e.detail)) + if line is not None: + f.write(line, sizet(string.strlen(line))) + f.write("\n", sizet(1)) + f.close() + + +def _Itoa(pool: memhub.MemManager*, v: int64) -> str: + b: bytes = pool.alloc(sizet(32)) + if b is None: + return "0" + stdio.snprintf(b, sizet(32), "%lld", v) + return str(b) + + +def _Cat(pool: memhub.MemManager*, a: str, b: str) -> str: + if a is None: + return b + if b is None: + return a + n1: sizet = string.strlen(a) + n2: sizet = string.strlen(b) + buf: bytes = pool.alloc(n1 + n2 + 1) + if buf is None: + return None + string.memcpy(buf, a, n1) + string.memcpy(buf + n1, b, n2) + buf[n1 + n2] = 0 + return str(buf) + + +def _ParseFile(pool: memhub.MemManager*, table_pool: memhub.MemManager*, + file: str, cache_path: str, text: str, file_hash: uint32): + if text is None: + return + doc: LspDocuments.LspDocument* = LspSource.NewTempDoc(pool, text) + if doc is None: + return + a: LspSymbols.LspAnalysis* = LspSymbols.Analyze(pool, table_pool, doc, file) + if a is None: + return + i: int = 0 + while i < int(len(a.symbols)): + s: LspSymbols.LspSymbol* = a.symbols.get(sizet(i)) + i += 1 + if s is None or s.name is None: + continue + if s.scope != a.module_scope and (s.parent_sym is None + or s.parent_sym.kind != LspSymbols.LSP_KIND_CLASS): + continue + if s.kind == LspSymbols.LSP_KIND_MODULE: + continue + _AddEntry(table_pool, file, s.container, s.name, s.kind, s.detail) + _WriteCache(pool, file, cache_path, os.getsize(file), file_hash) + + +def Invalidate(pool: memhub.MemManager*, table_pool: memhub.MemManager*, file: str): + """Drop cached signatures for a file (open/edited/saved).""" + global _entries, _entry_count + if _entries is not None and file is not None: + j: int = 0 + i: int = 0 + while i < _entry_count: + e: LspSigEntry* = _entries[i] + i += 1 + if e is None or LspFiles.PathEqual(e.file, file) == 0: + _entries[j] = e + j += 1 + _entry_count = j + _RebuildMap() + if _cache_dir is not None and file is not None: + cp: str = _CachePath(pool, file) + if cp is not None and os.isfile(cp): + os.remove(cp) + + +def Lookup(pool: memhub.MemManager*, table_pool: memhub.MemManager*, + ws: LspWorkspace.LspWorkspace*, file: str, name: str) -> LspSigEntry*: + if file is None or name is None: + return None + if Ensure(ws, table_pool) != 0: + return None + norm: str = LspFiles.Normalize(table_pool, file) + e: LspSigEntry* = _FindEntry(norm, name) + if e is not None: + return e + cp: str = _CachePath(pool, norm) + if cp is None: + return None + size: int64 = os.getsize(norm) + if _work is None: + return None + _work.Reset() + text: str = LspFiles.ReadSource(_work.mb, norm) + h: uint32 = _HashText(text) + if _LoadCache(_work.mb, table_pool, norm, cp, size, h) == 0: + _ParseFile(_work.mb, table_pool, norm, cp, text, h) + return _FindEntry(norm, name) + + +def TargetFor(pool: memhub.MemManager*, e: LspSigEntry*) -> LspIndex.LspTarget*: + if e is None: + return None + t: LspIndex.LspTarget* = LspIndex.LspTarget(pool) + if t is None: + return None + t.file = e.file + t.name = e.name + t.kind = e.kind + t.detail = e.detail + t.container = e.container + t.has_sel = 0 + return t + + +def ResolveOccurrence(pool: memhub.MemManager*, table_pool: memhub.MemManager*, + ws: LspWorkspace.LspWorkspace*, a: LspSymbols.LspAnalysis*, + occ: LspSymbols.LspOccurrence*) -> LspIndex.LspTarget*: + """Resolve an imported function/variable through the signature cache.""" + if a is None or occ is None: + return None + if occ.rest is not None and occ.sym is None and occ.head is not None: + h: LspSymbols.LspSymbol* = occ.head + rest: str = occ.rest + dot: int = -1 + i: int = 0 + while i < int(string.strlen(rest)): + if rest[sizet(i)] == '.': + dot = i + break + i += 1 + if dot >= 0: + return None # member chains need class layout knowledge + file: str = None + if h.import_kind == LspSymbols.LSP_IMPORT_MODULE: + file = LspIndex._ModuleFile(ws, pool, a.file, h) + elif h.import_kind == LspSymbols.LSP_IMPORT_FROM: + file = LspIndex._ResolveFromFile(ws, pool, a.file, h) + if file is None: + return None + e: LspSigEntry* = Lookup(pool, table_pool, ws, file, rest) + return TargetFor(pool, e) + if occ.sym is not None: + s: LspSymbols.LspSymbol* = occ.sym + if s.import_kind == LspSymbols.LSP_IMPORT_FROM and s.import_attr is not None: + file: str = LspIndex._ResolveFromFile(ws, pool, a.file, s) + if file is None: + return None + e: LspSigEntry* = Lookup(pool, table_pool, ws, file, s.import_attr) + return TargetFor(pool, e) + if s.import_kind == LspSymbols.LSP_IMPORT_MODULE and occ.rest is not None: + file: str = LspIndex._ModuleFile(ws, pool, a.file, s) + if file is None: + return None + e: LspSigEntry* = Lookup(pool, table_pool, ws, file, occ.rest) + return TargetFor(pool, e) + return None + if occ.name is not None and a.wildcards is not None: + i: int = 0 + while i < int(len(a.wildcards)): + w: LspSymbols.LspSymbol* = a.wildcards.get(sizet(i)) + i += 1 + file: str = LspIndex._ResolveFromFile(ws, pool, a.file, w) + if file is None: + continue + e: LspSigEntry* = Lookup(pool, table_pool, ws, file, occ.name) + if e is not None: + return TargetFor(pool, e) + return None diff --git a/App/lib/lsp/LspSymbols.vp b/App/lib/lsp/LspSymbols.vp index 58565b6..521d0d1 100644 --- a/App/lib/lsp/LspSymbols.vp +++ b/App/lib/lsp/LspSymbols.vp @@ -114,6 +114,11 @@ class LspAnalysis: occs: list[LspOccurrence*]* wildcards: list[LspSymbol*]* root: list[LspSymbol*]* + # Per-line occurrence buckets (built once after binding) so navigation/checks do not scan + # every occurrence for every cursor/expression on large files. + occ_next: int* + occ_line_head: int* + occ_line_count: int def __new__(self, pool: memhub.MemManager*) -> rawptr: p: LspAnalysis* = (LspAnalysis*)(pool.alloc(LspAnalysis.__sizeof__())) @@ -325,11 +330,71 @@ def _Docstring(pool: memhub.MemManager*, a: LspAnalysis*, node: ast.AST*) -> str def _AnnotationText(pool: memhub.MemManager*, a: LspAnalysis*, ann: ast.AST*) -> str: - if ann is None or ann.lineno <= 0: + if ann is None or ann.lineno <= 0 or a is None or a.doc is None: return None - txt: str = LspSource.SliceBytes(pool, a.doc, ann.lineno - 1, ann.col_offset, - ann.lineno - 1, - ann.col_offset + _ExprTextLength(ann)) + line0: int = ann.lineno - 1 + if line0 < 0 or line0 >= a.doc.index.Count(): + return None + limit: int = LspSource.LineByteLength(a.doc, line0) + start: int = ann.col_offset + if start < 0: + start = 0 + if start > limit: + start = limit + # The AST position of a wrapped annotation (`PtrT[T]`, `ArrayT[...]`) is the inner type. + # Recover the source span: scan left to the introducing `:` / `->`, then right to the + # declaration delimiter, so pointers/arrays are preserved. + b: int = start + while b > 0: + ch: char = a.doc.text[a.doc.index.LineStart(line0) + sizet(b - 1)] + if ch == ':': + start = b + break + if ch == '>': + if b >= 2 and a.doc.text[a.doc.index.LineStart(line0) + sizet(b - 2)] == '-': + start = b + break + if ch == '=' or ch == '(' or ch == ',': + break + b -= 1 + while start < limit: + ch0: char = a.doc.text[a.doc.index.LineStart(line0) + sizet(start)] + if ch0 == ' ' or ch0 == '\t': + start += 1 + else: + break + depth: int = 0 + quote: char = char(0) + end: int = start + while end < limit: + ch: char = a.doc.text[a.doc.index.LineStart(line0) + sizet(end)] + if quote != char(0): + if ch == '\\': + end += 2 + continue + if ch == quote: + quote = char(0) + end += 1 + continue + if ch == '\'' or ch == '"': + quote = ch + elif ch == '(' or ch == '[' or ch == '{': + depth += 1 + elif ch == ')' or ch == ']' or ch == '}': + if depth > 0: + depth -= 1 + else: + break + elif (ch == ',' or ch == '=') and depth == 0: + break + end += 1 + while end > start: + ch2: char = a.doc.text[a.doc.index.LineStart(line0) + sizet(end - 1)] + if ch2 == ' ' or ch2 == '\t' or ch2 == '\r': + end -= 1 + else: + break + txt: str = LspSource.SliceBytes(pool, a.doc, line0, start, line0, end) return _CopyStr(pool, txt, 256) @@ -1624,6 +1689,38 @@ def _BuildOutline(pool: memhub.MemManager*, a: LspAnalysis*, scope: LspScope*, # Entry point # ============================================================ +def _BuildOccIndex(pool: memhub.MemManager*, a: LspAnalysis*): + """Bucket occurrences by line (head + next links) for O(line) lookups.""" + if a is None or a.doc is None or a.doc.index is None or a.occs is None: + return + count: int = a.doc.index.Count() + n_occ: int = int(len(a.occs)) + if count <= 0 or n_occ <= 0: + return + a.occ_line_count = count + a.occ_line_head = (int*)(pool.alloc(sizet(count) * sizet(8))) + a.occ_next = (int*)(pool.alloc(sizet(n_occ) * sizet(8))) + if a.occ_line_head is None or a.occ_next is None: + return + i: int = 0 + while i < count: + a.occ_line_head[i] = -1 + i += 1 + i = 0 + while i < n_occ: + a.occ_next[i] = -1 + i += 1 + i = 0 + while i < n_occ: + o: LspOccurrence* = a.occs.get(sizet(i)) + if o is not None: + ln: int = o.line + if ln >= 0 and ln < count: + a.occ_next[i] = a.occ_line_head[ln] + a.occ_line_head[ln] = i + i += 1 + + def Analyze(pool: memhub.MemManager*, table_pool: memhub.MemManager*, doc: LspDocuments.LspDocument*, file: str, tree_override: ast.AST* = None) -> LspAnalysis*: @@ -1654,6 +1751,7 @@ def Analyze(pool: memhub.MemManager*, table_pool: memhub.MemManager*, _WalkBody(pool, a, a.module_scope, a.tree.children) _BindDeclaredTypes(pool, a) _ResolveOccurrences(a) + _BuildOccIndex(pool, a) end_rng: LspSource.LspRange* = LspSource.DocEndRange(pool, doc) a.root = _BuildOutline(pool, a, a.module_scope, a.tree.children, end_rng, 1, None) diff --git a/App/lib/lsp/LspTypes.vp b/App/lib/lsp/LspTypes.vp index 681a4b4..36c0bd0 100644 --- a/App/lib/lsp/LspTypes.vp +++ b/App/lib/lsp/LspTypes.vp @@ -19,6 +19,7 @@ define LT_ARRAY = 6 define LT_STR = 7 define LT_AGG = 8 define LT_NONE = 9 +define LT_LIST = 10 # CheckAssign reason codes. define LT_OK = 1 @@ -32,6 +33,8 @@ define LT_FLOAT_PTR = 8 define LT_SCALAR_TO_AGG = 9 define LT_MISMATCH = 10 define LT_VOID_VALUE = 11 +define LT_PTR_MISMATCH = 12 +define LT_ARRAY_MISMATCH = 13 class LspType: @@ -123,6 +126,14 @@ def NewNone(pool: memhub.MemManager*) -> LspType*: return _NewType(pool, LT_NONE) +def NewArray(pool: memhub.MemManager*, elem: LspType*, count: int) -> LspType*: + a: LspType* = _NewType(pool, LT_ARRAY) + if a is not None: + a.pointee = elem + a.count = count + return a + + def _WidthName(name: str, out_bits: int*, out_unsigned: int*) -> int: """Recognize `int`, `uint`, `intN`, `uintN`, `iN`, `uN`; returns 1 on success.""" if name is None: @@ -290,7 +301,23 @@ def ParseType(pool: memhub.MemManager*, text: str) -> LspType*: return _NewType(pool, LT_STR) if _Starts(name, "rawptr") != 0: return _NewType(pool, LT_PTR) - if _Starts(name, "list[") != 0 or _Starts(name, "list [") != 0: + if _Starts(name, "list[") != 0: + close_l: int = -1 + i_l: int = 5 + n_l: sizet = string.strlen(name) + while i_l < int(n_l): + if name[sizet(i_l)] == ']': + close_l = i_l + break + i_l += 1 + if close_l > 0: + elem_txt: str = _Trim(pool, _Slice(pool, name, 5, close_l)) + elem_t: LspType* = ParseType(pool, elem_txt) + lt: LspType* = _NewType(pool, LT_LIST) + if lt is not None: + lt.pointee = elem_t + lt.name = "list" + return lt t2b: LspType* = _NewType(pool, LT_AGG) if t2b is not None: t2b.name = "list" @@ -430,6 +457,13 @@ def NameOf(pool: memhub.MemManager*, t: LspType*) -> str: return inner2 if t.kind == LT_AGG: return t.name + if t.kind == LT_LIST: + inner3: str = NameOf(pool, t.pointee) + buf5: bytes = pool.alloc(sizet(string.strlen(inner3) + 16)) + if buf5 is not None: + stdio.snprintf(buf5, sizet(string.strlen(inner3) + 16), "list[%s]", inner3) + return str(buf5) + return "list" return "?" @@ -444,6 +478,59 @@ def _IntFitsBits(bits: int, value: int64) -> int: return 1 if value >= lo and value <= hi else 0 +def _SameType(a: LspType*, b: LspType*) -> int: + """Structural compatibility for pointee/element checks (unknown/void are wildcards).""" + if a is None or b is None: + return 1 + if a.kind == LT_UNKNOWN or b.kind == LT_UNKNOWN: + return 1 + if a.kind == LT_VOID or b.kind == LT_VOID: + return 1 + if a.kind != b.kind: + return 0 + if a.kind == LT_INT: + return 1 if a.bits == b.bits else 0 + if a.kind == LT_FLOAT: + return 1 if a.fbits == b.fbits else 0 + if a.kind == LT_BOOL or a.kind == LT_STR or a.kind == LT_NONE: + return 1 + if a.kind == LT_PTR: + return _SameType(a.pointee, b.pointee) + if a.kind == LT_ARRAY: + if a.count != b.count and a.count > 0 and b.count > 0: + return 0 + return _SameType(a.pointee, b.pointee) + if a.kind == LT_LIST: + return _SameType(a.pointee, b.pointee) + if a.kind == LT_AGG: + if a.name is None or b.name is None: + return 1 + na: str = a.name + nb: str = b.name + ia: int = int(string.strlen(na)) - 1 + while ia >= 0: + if na[sizet(ia)] == '.': + ia += 1 + break + ia -= 1 + if ia < 0: + ia = 0 + ib: int = int(string.strlen(nb)) - 1 + while ib >= 0: + if nb[sizet(ib)] == '.': + ib += 1 + break + ib -= 1 + if ib < 0: + ib = 0 + if string.strcmp(na + ia, nb + ib) == 0: + return 1 + # Different class names: the compiler may accept typedef/alias spellings we cannot + # resolve here, so stay silent instead of producing a false pointer mismatch. + return 1 + return 1 + + def CheckAssign(src: LspType*, dst: LspType*, is_const: int, cval: int64) -> int: """Return LT_OK or a reason code; both types unknown means 'no opinion'.""" if src is None or dst is None: @@ -493,6 +580,14 @@ def CheckAssign(src: LspType*, dst: LspType*, is_const: int, cval: int64) -> int return LT_INT_TO_PTR if src.kind == LT_FLOAT: return LT_FLOAT_PTR + if src.kind == LT_PTR: + if _SameType(src.pointee, dst.pointee) == 0: + return LT_PTR_MISMATCH + return LT_OK + if src.kind == LT_ARRAY: + if _SameType(src.pointee, dst.pointee) == 0: + return LT_PTR_MISMATCH + return LT_OK return LT_OK if dst.kind == LT_STR: if src.kind == LT_INT or src.kind == LT_FLOAT or src.kind == LT_BOOL \ @@ -502,6 +597,20 @@ def CheckAssign(src: LspType*, dst: LspType*, is_const: int, cval: int64) -> int if dst.kind == LT_ARRAY: if src.kind == LT_INT or src.kind == LT_FLOAT or src.kind == LT_BOOL: return LT_SCALAR_TO_AGG + if src.kind == LT_ARRAY: + if _SameType(src, dst) == 0: + return LT_ARRAY_MISMATCH + return LT_OK + if src.kind == LT_PTR: + if _SameType(src.pointee, dst.pointee) == 0: + return LT_PTR_MISMATCH + return LT_OK + return LT_OK + if dst.kind == LT_LIST: + if src.kind == LT_LIST: + if _SameType(src.pointee, dst.pointee) == 0: + return LT_ARRAY_MISMATCH + return LT_OK return LT_OK if dst.kind == LT_AGG: if src.kind == LT_INT or src.kind == LT_FLOAT or src.kind == LT_BOOL: @@ -564,6 +673,16 @@ def ReasonText(pool: memhub.MemManager*, code: int, src: LspType*, dst: LspType* " only the first element's bytes would be written; write = None for zero-initialization" " or use a list/string initializer", st, dt) return str(buf) + if code == LT_PTR_MISMATCH: + stdio.snprintf(buf, sizet(512), + "incompatible pointer types: %s cannot be implicitly assigned to %s; use an explicit" + " cast if the reinterpretation is intended", st, dt) + return str(buf) + if code == LT_ARRAY_MISMATCH: + stdio.snprintf(buf, sizet(512), + "incompatible aggregate types: %s cannot be implicitly assigned to %s" + " (element type or array length differs)", st, dt) + return str(buf) if code == LT_VOID_VALUE: stdio.snprintf(buf, sizet(512), "a void value cannot take part in any operation (void is only allowed for void* and" diff --git a/Tools/lsp/probe.py b/Tools/lsp/probe.py index 5032537..d1bf719 100644 --- a/Tools/lsp/probe.py +++ b/Tools/lsp/probe.py @@ -2359,6 +2359,12 @@ def gate13(exe: str) -> int: ("call missing args", "def h(a: int, b: int) -> int:\n return 0\n\ndef f() -> int:\n return h(1)\n", 1, "h(1)", 0, "at least"), ("call extra args", "def h(a: int) -> int:\n return 0\n\ndef f() -> int:\n return h(1, 2)\n", 1, "h(1, 2)", 0, "at most"), ("double to float arg", "def g(p: float) -> int:\n return 0\n\ndef f() -> int:\n d: double = 1.0\n return g(d)\n", 1, "g(d)", 2, "double to float"), + ("return type mismatch", "def f() -> int:\n return \"s\"\n", 1, "\"s\"", 0, "pointer-to-integer"), + ("aug assign", "def f() -> int:\n x: u8 = 1\n x += 300\n return 0\n", 1, "300", 0, "augmented"), + ("pointer pointee", "def g(p: int*) -> int:\n return 0\n\ndef f(q: u8*) -> int:\n return g(q)\n", 1, "g(q)", 2, "incompatible pointer"), + ("array length", "def f() -> int:\n x: [4]int = [1, 2, 3, 4, 5]\n return 0\n", 1, None, 0, "array initializer"), + ("string length", "def f() -> int:\n x: [4]char = \"hello\"\n return 0\n", 1, "\"hello\"", 0, "string literal length"), + ("list element", "def f() -> int:\n x: [4]int = [1, \"s\"]\n return 0\n", 1, "\"s\"", 0, "pointer-to-integer"), ] version = 1 for label, text, expected, needle, delta, message_sub in cases: @@ -2375,18 +2381,22 @@ def gate13(exe: str) -> int: raise AssertionError(f"gate13 {label}: expected {expected} diagnostics, got {publish}") if expected == 1: diagnostic = diagnostics[0] - start = _position(text, needle) - start["character"] += delta - line_text = text.split("\n")[start["line"]] - end_len = 0 - i = start["character"] - while i + end_len < len(line_text) and line_text[i + end_len] not in " \t,():;=+-/%<>&|^": - end_len += 1 - if end_len <= 0: - end_len = 1 - end = {"line": start["line"], "character": start["character"] + end_len} - if diagnostic["range"]["start"] != start or diagnostic["range"]["end"] != end: - raise AssertionError(f"gate13 {label}: range misses {needle!r}: {diagnostic}") + if needle is not None: + start = _position(text, needle) + start["character"] += delta + line_text = text.split("\n")[start["line"]] + end_len = 0 + i = start["character"] + stop_chars = " \t,():;=+-/%<>&|^]}" + if start["character"] < len(line_text) and line_text[start["character"]] == "[": + stop_chars = " \t,():;=+-/%<>&|^" + while i + end_len < len(line_text) and line_text[i + end_len] not in stop_chars: + end_len += 1 + if end_len <= 0: + end_len = 1 + end = {"line": start["line"], "character": start["character"] + end_len} + if diagnostic["range"]["start"] != start or diagnostic["range"]["end"] != end: + raise AssertionError(f"gate13 {label}: range misses {needle!r}: {diagnostic}") if message_sub not in diagnostic["message"]: raise AssertionError(f"gate13 {label}: message is wrong: {diagnostic}") client.notify("textDocument/didClose", {"textDocument": {"uri": uri}})