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:
1 parent
79d53e691c
commit
1968748ea5
7 files changed
+958
-23
No files matched your search
+205
-6
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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}})
|
||||
|
||||
Reference in new issue
Block a user