feat(lsp): cross-file signatures, returns, augassign and richer types

Adds LspSigCache: per-file signature cache under <project>/.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.
This commit is contained in:
TermiNexus committed 2026-10-28 22:36:07 +08:00
1 parent 79d53e691c
commit 1968748ea5
7 files changed
+958 -23

No files matched your search

+205 -6
View File
@@ -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
+14
View File
@@ -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))
+7
View File
@@ -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)
+488
View File
@@ -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 `<project>/.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
+102 -4
View File
@@ -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)
+120 -1
View File
@@ -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"
+22 -12
View File
@@ -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}})