Files
ViperC/App/lib/core/Gen/ImportsGenMixin.vp

696 lines
38 KiB
Plaintext

import lib.core.Gen.Generator as Generator
from stdint import *
import ast
import memhub
import hashtable
import string
import stdlib
import viperlib
import stdio
import sys
import win32.fileio as fileio
import lib.Stub.StubMerger as StubMerger
import lib.core.CompilerFactory as CompilerFactory
import lib.core.Gen.Translator as HT
import lib.Stub.StubMerger.StubMergerStore as StubMergerStore
import lib.core.VLogger as VLogger
# ImportsGenMixin - import handling mixin. Maintains the imported_modules/from_imports
# tables, re-export registration, relative import resolution and module SHA1 checks.
# Constants (kept consistent with HandlesImports)
define MAX_REEXPORT = 512
# ImportsGenMixin - import handling
novirtual class ImportsGenMixin:
"""Import handling mixin."""
# --- Global re-export map tables (_g_reexport_sha1s/funcs/srcs/count) ---
Gen: Generator.Generator*
ReexportSha1s: bytes
ReexportFuncs: bytes
ReexportSrcs: bytes
ReexportCount: int
# --- Import lookup index (turns O(string) scanning into O(name) hashing) ---
# Bound to the current string pointer; a mismatch falls back to scanning. PermPool-allocated.
CurImportedModulesPtr: str
CurImportedModulesNames: hashtable.HashTable*
CurFromImportsPtr: str
CurFromImportsMods: hashtable.HashTable*
CurFromImportsOrigs: hashtable.HashTable*
# Note: CurrentFile is not declared here (TypeGenMixin owns it; a duplicate would
# misalign fields). Access goes through HandlesType to avoid a circular import.
# _index_imported_module - incrementally maintain the imported_modules index.
# Reuse the bound index when old_ptr matches, else unbind and fall back to scanning.
def _index_imported_module(self, old_ptr: str, new_ptr: str, name_ptr: str) -> None:
"""Incrementally update the imported_modules index, reusing it when possible."""
if new_ptr is None or name_ptr is None: return
perm: memhub.MemBuddy* = CompilerFactory.factory.PermPool
if perm is None: return
idx: hashtable.HashTable* = None
if old_ptr is None:
# New file / chain head: build a fresh index (the old one is discarded with its string)
idx = hashtable.HashTable(perm)
elif old_ptr == self.CurImportedModulesPtr and self.CurImportedModulesNames is not None:
# Normal path: reuse the current string's index and append (no rebuild, O(imports) memory)
idx = self.CurImportedModulesNames
else:
# Old string is not the bound one (unexpected source): unbind and fall back to scanning
self.CurImportedModulesPtr = None
self.CurImportedModulesNames = None
return
if idx is None:
self.CurImportedModulesPtr = None
self.CurImportedModulesNames = None
return
# Skip if the name already exists (duplicate import); otherwise write (key/value point inside new_ptr)
if idx.__contains__(name_ptr) == 0: idx.set_str(name_ptr, name_ptr)
self.CurImportedModulesPtr = new_ptr
self.CurImportedModulesNames = idx
# _index_from_import - incrementally maintain the from_imports index. entry_off is
# the new entry's offset; module/orig tables key by local_name (first hit wins).
def _index_from_import(self, old_ptr: str, new_ptr: str, entry_off: sizet, local_name: str, module_name: str, original_name: str) -> None:
"""Incrementally update the from_imports index, reusing it when possible."""
if new_ptr is None or local_name is None or module_name is None: return
perm: memhub.MemBuddy* = CompilerFactory.factory.PermPool
if perm is None: return
idx_m: hashtable.HashTable* = None
idx_o: hashtable.HashTable* = None
if old_ptr is None:
idx_m = hashtable.HashTable(perm)
idx_o = hashtable.HashTable(perm)
elif old_ptr == self.CurFromImportsPtr and self.CurFromImportsMods is not None and self.CurFromImportsOrigs is not None:
idx_m = self.CurFromImportsMods
idx_o = self.CurFromImportsOrigs
else:
self.CurFromImportsPtr = None
self.CurFromImportsMods = None
self.CurFromImportsOrigs = None
return
if idx_m is None or idx_o is None:
self.CurFromImportsPtr = None
self.CurFromImportsMods = None
self.CurFromImportsOrigs = None
return
local_len: sizet = string.strlen(local_name)
mod_len: sizet = string.strlen(module_name)
# The hash key must be NUL-terminated: the flat string's local segment ends with ':',
# so hashing it directly never matches. Allocate a stable NUL-terminated copy.
key_ptr: str = perm.alloc(local_len + 1)
if key_ptr is None:
self.CurFromImportsPtr = None
self.CurFromImportsMods = None
self.CurFromImportsOrigs = None
return
string.strncpy(key_ptr, local_name, local_len)
key_ptr[local_len] = '\0'
mod_ptr: str = new_ptr + entry_off + local_len + 1
if local_len == 1 and local_name[0] == '*':
# star import: the last entry's module name wins (overwrite, matching star_mod)
idx_m.set_str(key_ptr, mod_ptr)
else:
if idx_m.__contains__(key_ptr) == 0: idx_m.set_str(key_ptr, mod_ptr)
if idx_o.__contains__(key_ptr) == 0:
if original_name is not None and string.strcmp(original_name, local_name) != 0 and string.strlen(original_name) > 0: idx_o.set_str(key_ptr, mod_ptr + mod_len + 1)
else:
# First occurrence has no alias (or empty alias) -> query returns None, matching scanning
idx_o.set_str(key_ptr, None)
self.CurFromImportsPtr = new_ptr
self.CurFromImportsMods = idx_m
self.CurFromImportsOrigs = idx_o
# Add an imported module name
def add_imported_module(self, pool: memhub.MemBuddy*, imported_modules: str, name: str) -> str:
"""Record an imported module name and return the new imported_modules string."""
if name is None: return imported_modules
old_len: sizet = 0
if imported_modules is not None: old_len = string.strlen(imported_modules)
buf: str = StubMergerStore.AppendImportedModule(pool, imported_modules, name)
if buf is None: return imported_modules
if imported_modules is None:
# Index: first module name (key points inside buf)
self._index_imported_module(None, buf, buf)
else:
# Index: reuse the old string's index and append the new name (key points inside buf)
self._index_imported_module(imported_modules, buf, buf + old_len + 1)
return buf
# Check whether a module has been imported
def is_module_imported(self, imported_modules: str, name: str) -> int:
"""Check whether a module is imported, matching on word boundaries."""
if name is None or imported_modules is None: return 0
# [Index fast path] When the input is the bound current string, do an O(name) hash
# lookup; otherwise fall back to the scanning below, semantics unchanged.
if imported_modules == self.CurImportedModulesPtr and self.CurImportedModulesNames is not None:
if self.CurImportedModulesNames.__contains__(name) != 0: return 1
# Index miss never returns 0 (an incomplete index would misreport "not imported");
# fall through to scanning on the real string content (correctness first).
name_len: sizet = string.strlen(name)
cur: str = imported_modules
total_len: sizet = string.strlen(imported_modules)
ci: sizet = 0
while ci < total_len:
while ci < total_len and cur[ci] == ' ': ci += 1
if ci >= total_len: break
word_start: sizet = ci
while ci < total_len and cur[ci] != ' ': ci += 1
word_len: sizet = ci - word_start
if word_len == name_len:
Matched: int = 1
for ei in range(int(name_len)):
if cur[word_start + ei] != name[ei]:
Matched = 0
break
if Matched == 1: return 1
return 0
# Check whether a module is imported or globally registered (package submodule resolution)
def is_module_imported_or_known(self, imported_modules: str, name: str) -> int:
"""Return 1 when the module is imported or reachable through an imported package."""
if self.is_module_imported(imported_modules, name) != 0: return 1
if name is None: return 0
if StubMergerStore.LookupSha1ByModName(name) is None: return 0
nl: sizet = string.strlen(name)
for dot_pos in range(int(nl)):
if name[dot_pos] == '.':
pfx: str = stdlib.malloc(dot_pos + 1)
if pfx is not None:
string.strncpy(pfx, name, dot_pos)
pfx[dot_pos] = '\0'
hit: int = self.is_module_imported(imported_modules, pfx)
stdlib.free(pfx)
if hit != 0: return 1
return 0
# Add a from-import name mapping
def add_from_import(self, pool: memhub.MemBuddy*, from_imports: str, local_name: str, module_name: str, original_name: str = None) -> str:
"""Add a from-import mapping and return the new from_imports string."""
if local_name is None or module_name is None: return from_imports
old_len: sizet = 0
if from_imports is not None: old_len = string.strlen(from_imports)
buf: str = StubMergerStore.AppendFromImport(pool, from_imports, local_name, module_name, original_name)
if buf is None: return from_imports
# Index: the first entry has entry_off=0; an appended entry starts at buf[old_len+1]
entry_off: sizet = 0
if from_imports is not None: entry_off = old_len + 1
self._index_from_import(from_imports, buf, entry_off, local_name, module_name, original_name)
return buf
# Look up a from-import name and return the original function name (alias case):
# for `from X import Y as Z` return Y; return None when there is no alias.
def lookup_from_import_original(self, from_imports: str, local_name: str) -> str:
"""Return the original function name for a from-import alias, or None."""
if local_name is None or from_imports is None: return None
# [Index fast path] When the input is the bound current string, do an O(name) hash
# lookup; else fall back to scanning. orig None means a first occurrence with no alias.
if from_imports == self.CurFromImportsPtr and self.CurFromImportsOrigs is not None:
if self.CurFromImportsOrigs.__contains__(local_name) != 0:
v_o: rawptr = self.CurFromImportsOrigs.__getitem__(local_name)
if v_o is not None: return rawptr(v_o)
# Index miss or None value never returns None directly (the real string may still
# contain the name); fall through to scanning (correctness first).
name_len: sizet = string.strlen(local_name)
cur: str = from_imports
ci: sizet = 0
total_len: sizet = string.strlen(from_imports)
while ci < total_len:
# Skip leading spaces
while ci < total_len and cur[ci] == ' ': ci += 1
if ci >= total_len: break
# Find the first ':'
colon1: sizet = ci
while colon1 < total_len and cur[colon1] != ':' and cur[colon1] != ' ': colon1 += 1
if colon1 >= total_len or cur[colon1] != ':': break
entry_name_len: sizet = colon1 - ci
# Skip star imports
if entry_name_len == 1 and cur[ci] == '*':
ci = colon1
while ci < total_len and cur[ci] != ' ': ci += 1
continue
# Compare the name
if entry_name_len == name_len:
Matched: int = 1
for ei in range(int(name_len)):
if cur[ci + ei] != local_name[ei]:
Matched = 0
break
if Matched == 1:
# Match found; check for a third field (the original name)
pos: sizet = colon1 + 1
# Skip the module name
while pos < total_len and cur[pos] != ':' and cur[pos] != ' ' and cur[pos] != '\0': pos += 1
if pos < total_len and cur[pos] == ':':
# Third field present: original_name
orig_start: sizet = pos + 1
orig_end: sizet = orig_start
while orig_end < total_len and cur[orig_end] != ' ' and cur[orig_end] != '\0': orig_end += 1
orig_len: sizet = orig_end - orig_start
if orig_len > 0:
orig_buf: str = cur + orig_start
# Return an interior pointer (caller keeps from_imports alive during use)
return orig_buf
return None
return None
# Advance to the next entry
ci = colon1
while ci < total_len and cur[ci] != ' ': ci += 1
return None
# Look up a from-import name and return the module name, or None.
# allow_star_fallback=1 permits a star-import fallback; pass 0 to disable it.
def lookup_from_import(self, from_imports: str, name: str, allow_star_fallback: int = 1) -> str:
"""Return the module name for a from-import, with optional star fallback."""
if name is None or from_imports is None: return None
# [Index fast path] When the input is the bound current string, do an O(name) hash
# lookup; else fall back to scanning. name=="*" skips exact match and goes to star.
if from_imports == self.CurFromImportsPtr and self.CurFromImportsMods is not None:
name_is_star: int = 0
if string.strlen(name) == 1 and name[0] == '*': name_is_star = 1
if name_is_star == 0:
v_f: rawptr = self.CurFromImportsMods.__getitem__(name)
if v_f is not None: return rawptr(v_f)
if allow_star_fallback != 0:
sv_f: rawptr = self.CurFromImportsMods.__getitem__("*")
if sv_f is not None: return rawptr(sv_f)
# Neither exact nor star hit in the index never returns None (an incomplete index
# misses imported names, causing _op_head NameError); fall through to scanning.
name_len: sizet = string.strlen(name)
cur: str = from_imports
ci: sizet = 0
total_len: sizet = string.strlen(from_imports)
star_mod: str = None
while ci < total_len:
# Skip leading spaces
while ci < total_len and cur[ci] == ' ': ci += 1
if ci >= total_len: break
# Find the ':' position
colon_pos: sizet = ci
while colon_pos < total_len and cur[colon_pos] != ':' and cur[colon_pos] != ' ': colon_pos += 1
if colon_pos >= total_len or cur[colon_pos] != ':': break
entry_name_len: sizet = colon_pos - ci
# Detect star import ("*:module")
if entry_name_len == 1 and cur[ci] == '*':
mod_start: sizet = colon_pos + 1
mod_end: sizet = mod_start
while mod_end < total_len and cur[mod_end] != ' ' and cur[mod_end] != '\0' and cur[mod_end] != ':': mod_end += 1
star_mod = cur + mod_start
# Compare the name
elif entry_name_len == name_len:
Matched: int = 1
for ei in range(int(name_len)):
if cur[ci + ei] != name[ei]:
Matched = 0
break
if Matched == 1:
mod_start2: sizet = colon_pos + 1
mod_end2: sizet = mod_start2
# The module name ends at a space, null, or a second ':' (alias separator)
while mod_end2 < total_len and cur[mod_end2] != ' ' and cur[mod_end2] != '\0' and cur[mod_end2] != ':': mod_end2 += 1
return cur + mod_start2
# Advance to the next entry
ci = colon_pos
while ci < total_len and cur[ci] != ' ': ci += 1
# No exact match; fall back to star import only when allowed
if allow_star_fallback == 0: return None
return star_mod
# register_reexport - register a re-export mapping (called from HandleImportFromNames
# for `from .X import Y`). Idempotent; memory is stdlib.malloc'd to persist across phases.
def register_reexport(self, mod_sha1: str, func_name: str, source_module: str) -> int:
"""Register a re-export mapping; returns 0 on success, -1 on failure."""
if mod_sha1 is None or func_name is None or source_module is None: return -1
# fail-fast: the table is a flat slab with fixed slots (17 bytes for the SHA1, 64 for the
# name/module). strncpy used to clamp silently, after which lookup_reexport's full-name
# strcmp never matched (or matched another module sharing the 63-byte prefix).
if string.strlen(mod_sha1) > 16:
self.Gen.TypeGenErr.fatal_error(None, "re-export module SHA1 is longer than its 16-byte table slot", "OverflowError")
if string.strlen(func_name) > 62:
self.Gen.TypeGenErr.fatal_error(None, f"re-exported name '{func_name}' exceeds the 62-byte table slot; shorten the identifier", "OverflowError")
if string.strlen(source_module) > 62:
self.Gen.TypeGenErr.fatal_error(None, f"re-export source module '{source_module}' exceeds the 62-byte table slot; shorten the module name", "OverflowError")
# Lazy initialization
if self.ReexportSha1s is None:
self.ReexportSha1s = CompilerFactory.factory.PermPool.alloc(MAX_REEXPORT * 17)
self.ReexportFuncs = CompilerFactory.factory.PermPool.alloc(MAX_REEXPORT * 64)
self.ReexportSrcs = CompilerFactory.factory.PermPool.alloc(MAX_REEXPORT * 64)
if self.ReexportSha1s is None or self.ReexportFuncs is None or self.ReexportSrcs is None: return -1
string.memset(self.ReexportSha1s, 0, MAX_REEXPORT * 17)
string.memset(self.ReexportFuncs, 0, MAX_REEXPORT * 64)
string.memset(self.ReexportSrcs, 0, MAX_REEXPORT * 64)
if self.ReexportCount >= MAX_REEXPORT: return -1
# Idempotency check: look for an existing identical mapping
for i in range(int(self.ReexportCount)):
sidx: sizet = sizet(i) * 17
fidx: sizet = sizet(i) * 64
if string.strcmp(self.ReexportSha1s + sidx, mod_sha1) == 0:
if string.strcmp(self.ReexportFuncs + fidx, func_name) == 0:
# Already exists; update source_module (64-byte slot, strncpy-clamped)
string.strncpy(self.ReexportSrcs + fidx, source_module, 63)
self.ReexportSrcs[fidx + 63] = '\0'
return 0
# Add a new entry (fixed 17/64-byte slots; unbounded strcpy would overrun, so all strncpy)
idx2: sizet = sizet(self.ReexportCount) * 17
fidx2: sizet = sizet(self.ReexportCount) * 64
string.strncpy(self.ReexportSha1s + idx2, mod_sha1, 16)
self.ReexportSha1s[idx2 + 16] = '\0'
string.strncpy(self.ReexportFuncs + fidx2, func_name, 63)
self.ReexportFuncs[fidx2 + 63] = '\0'
string.strncpy(self.ReexportSrcs + fidx2, source_module, 63)
self.ReexportSrcs[fidx2 + 63] = '\0'
self.ReexportCount += 1
return 0
# lookup_reexport - given a module SHA1 and function name, return the source module
# name (e.g. "ast.lexer") or None; the caller resolves its SHA1 separately.
def lookup_reexport(self, mod_sha1: str, func_name: str) -> str:
"""Return the source module name for a re-export mapping, or None."""
if mod_sha1 is None or func_name is None: return None
if self.ReexportSha1s is None or self.ReexportCount <= 0: return None
for i in range(int(self.ReexportCount)):
sidx: sizet = sizet(i) * 17
fidx: sizet = sizet(i) * 64
if string.strcmp(self.ReexportSha1s + sidx, mod_sha1) == 0:
if string.strcmp(self.ReexportFuncs + fidx, func_name) == 0: return self.ReexportSrcs + fidx
return None
# _resolve_relative_module - resolve a relative import into a full module name:
# `from .X import Y` -> "pkg.X", `from . import Y` -> "pkg", `from ..X` -> "pkg.X".
def _resolve_relative_module(self, pool: memhub.MemBuddy*, current_package: str, level: int, module: str) -> str:
"""Resolve a relative import into a full module name."""
if level <= 0: return module
if current_package is None:
# No package context (top-level includes/App files): `from .X import Y` means
# "the X module in the same/root directory", so returning module is correct.
return module
# Climb (level-1) levels up from current_package
pkg: str = current_package
up: int = level - 1
while up > 0:
pkg_len: sizet = string.strlen(pkg)
last_dot: sizet = 0
found: int = 0
for i in range(int(pkg_len)):
if pkg[i] == '.':
last_dot = i
found = 1
if found == 0:
# No more parents (the relative level exceeds the package depth): fall back to
# the original module; a later import SHA1 check reports "module not found".
pkg = None
break
# Truncate at the last '.'
new_pkg: str = pool.alloc(last_dot + 1)
if new_pkg is None:
self.Gen.TypeGenErr.fatal_error(None, f"relative import resolution failed: level={level} module='{module}'", "ImportError")
return module
string.strncpy(new_pkg, pkg, last_dot)
new_pkg[last_dot] = '\0'
pkg = new_pkg
up -= 1
if module is None:
# from . import Y -> the module is the package itself
return pkg
if pkg is None:
# The package climbed to empty (relative import out of range): fall back to module
return module
# Concatenate pkg + "." + module
pkg_len2: sizet = string.strlen(pkg)
mod_len: sizet = string.strlen(module)
buf: str = pool.alloc(pkg_len2 + 1 + mod_len + 1)
if buf is None:
self.Gen.TypeGenErr.fatal_error(None, f"relative import resolution failed: level={level} module='{module}'", "ImportError")
return module
string.strcpy(buf, pkg)
buf[pkg_len2] = '.'
string.strcpy(buf + pkg_len2 + 1, module)
return buf
# compute_package_from_relpath - derive the package name from a relative path by taking
# the directory and replacing / and \ with '.'. Top-level files (no separator) -> None.
def compute_package_from_relpath(self, pool: memhub.MemBuddy*, rel_path: str) -> str:
"""Derive the package name from a relative path by replacing separators with dots."""
if rel_path is None: return None
rlen: sizet = string.strlen(rel_path)
# Find the last / or \
last_sep: sizet = 0
found: int = 0
for i in range(int(rlen)):
ch: char = rel_path[i]
if ch == '/' or ch == '\\':
last_sep = i
found = 1
if found == 0:
# No directory separator -> top-level file, no package
return None
# Copy the directory part, replacing / and \ with .
dir_len: sizet = last_sep
buf: str = pool.alloc(dir_len + 1)
if buf is None: return None
for j in range(int(dir_len)):
ch2: char = rel_path[j]
if ch2 == '/' or ch2 == '\\': buf[j] = '.'
else: buf[j] = ch2
buf[dir_len] = '\0'
return buf
# set_current_file - set the file being compiled, matching TypeGenMixin's behavior;
# delegated through HandlesType (lazy import avoids a circular dependency).
def set_current_file(self, file_path: str) -> None:
"""Set the file currently being compiled, synced with HandlesType."""
# File-level invalidation: the index binds a string pointer from the previous file's
# arena that may be reused after reset, so unbind; add_* rebuilds it for this file.
self.CurImportedModulesPtr = None
self.CurImportedModulesNames = None
self.CurFromImportsPtr = None
self.CurFromImportsMods = None
self.CurFromImportsOrigs = None
self.Gen.TypeGenResolve.set_current_file(file_path)
# _report_import_error - local Python-style error reporter to avoid a circular import
# with HandlesType. Emits a Traceback/File/ImportError block and aborts.
def _report_import_error(self, node: ast.AST*, msg: str) -> None:
"""Report a Python-style import error and abort."""
stdio.printf("Traceback (most recent call last):\n")
lineno: int = 0
if node is not None: lineno = node.lineno
file_name: str = self.Gen.TypeGenResolve.get_current_file()
if file_name is None: file_name = "<unknown>"
stdio.printf(' File "%s", line %d\n', file_name, lineno)
# Show the source line's content (read line lineno for quick location)
if lineno > 0 and file_name is not None:
f_err: fileio.File* = fileio.File(file_name, fileio.MODE.R)
if not f_err.closed:
line_buf_err: bytes = stdlib.malloc(512)
if line_buf_err is not None:
cur_ln: int = 0
while cur_ln < lineno:
r_err: LONG = f_err.readline(line_buf_err, 511)
if r_err <= 0: break
cur_ln += 1
if cur_ln == lineno:
ll_err: sizet = string.strlen(line_buf_err)
while ll_err > 0 and (line_buf_err[ll_err - 1] == '\n' or line_buf_err[ll_err - 1] == '\r'):
ll_err -= 1
line_buf_err[ll_err] = '\0'
stdio.printf(" %s\n", line_buf_err)
break
stdlib.free(line_buf_err)
f_err.close()
stdio.printf("ImportError: %s\n", msg)
# Termination convergence: longjmp back to the REPL loop in a session, else abort
VLogger.repl_abort()
# _is_local_module_file - a sibling {mod_name}.vp/.py exists next to the current file
# (e.g. caller1.vp importing g4node); such modules are exempt from the SHA1 check.
def _is_local_module_file(self, mod_name: str) -> int:
"""Return 1 when a sibling {mod_name}.vp/.py exists next to the current file."""
cur: str = self.Gen.TypeGenResolve.get_current_file()
if cur is None or mod_name is None: return 0
cur_len: sizet = string.strlen(cur)
if cur_len == 0: return 0
sep_pos: sizet = cur_len
while sep_pos > 0:
sep_pos -= 1
ch: char = cur[sep_pos]
if ch == '/' or ch == '\\':
sep_pos += 1
break
mod_len: sizet = string.strlen(mod_name)
path_len: sizet = sep_pos + mod_len + 5
path_buf: bytes = stdlib.malloc(path_len + 1)
if path_buf is None: return 0
# .vp file (Viper sources are .vp only, no .py fallback)
string.strncpy(path_buf, cur, sep_pos)
path_buf[sep_pos] = '\0'
string.strcat(path_buf, mod_name)
string.strcat(path_buf, ".vp")
fex: fileio.File* = fileio.File(path_buf, fileio.MODE.R)
if not fex.closed:
fex.close()
stdlib.free(path_buf)
return 1
stdlib.free(path_buf)
return 0
# _check_import_sha1 - validate at import time that the module SHA1 is registered
# (fast-fail). Reports immediately instead of misleading the user later.
def _check_import_sha1(self, node: ast.AST*, mod_name: str) -> int:
"""Fail-fast when an imported module's SHA1 is not registered."""
if mod_name is None: return 1
# t / c were retired: their types are builtin and their C helpers now have language syntax.
if string.strcmp(mod_name, "t") == 0 or string.strcmp(mod_name, "c") == 0:
self._report_import_error(node, "`import t` / `import c` is retired; builtin types and C helpers no longer live in modules")
return 0
# ctraits (a .vpi compile-time macro lib) and viperbuild (a compiler builtin macro lib) stay exempt.
if string.strcmp(mod_name, "ctraits") == 0: return 1
if string.strcmp(mod_name, "viperbuild") == 0: return 1
mod_sha1: str = StubMergerStore.LookupSha1ByModName(mod_name)
if mod_sha1 is None:
# App sibling exemption: a {mod_name}.vp/.py exists next to the current file
# (SHA1 lives in the project file list, not the includes store).
if self._is_local_module_file(mod_name) != 0: return 1
err_buf: str = stdlib.malloc(256)
if err_buf is not None:
viperlib.snprintf(err_buf, 256, "module '%s' has no registered SHA1 (the includes scan did not find this module; check the includes path configuration in project.json)", mod_name)
self._report_import_error(node, err_buf)
else: self._report_import_error(node, "imported module SHA1 not registered (the includes scan did not find this module)")
return 1
# translate_import - handle import statements and update trans._imported_modules
def translate_import(self, trans: HT.Translator*, node: ast.AST*) -> int:
"""Translate an import statement and update trans._imported_modules."""
imp: ast.Import* = ast.Import*(node)
if imp is None: return 0
names: list[ast.AST*]* = imp.names
if names is None: return 0
nn: sizet = len(names)
pool_val: memhub.MemBuddy* = trans.Pool
ctx: StubMergerStore.ImportCtx* = StubMergerStore.AllocImportCtx(pool_val, trans._imported_modules, trans._from_imports)
if ctx is None: return 0
for ni in range(int(nn)):
alias: ast.Alias* = ast.Alias*(names.get(ni))
if alias is not None and alias.name is not None:
# Validate the module SHA1 at import time (fast-fail, full translation only):
# declare_only=1/2 is the pre-registration scan and would false-positive.
if trans._declare_only == 0: self._check_import_sha1(node, alias.name)
StubMergerStore.NotePlainImport(pool_val, ctx, alias.name, alias.asname)
trans._imported_modules = ctx.ImportedModules
trans._from_imports = ctx.FromImports
return 0
# translate_import_from_module - handle the module part of `from X import Y`.
# Relative imports (level>0) resolve via trans.CurrentPackage to a full module name.
def translate_import_from_module(self, trans: HT.Translator*, node: ast.AST*) -> int:
"""Translate the module part of a from-import statement."""
impf: ast.ImportFrom* = ast.ImportFrom*(node)
if impf is None: return 0
# Resolve the module name: relative imports need the package prefix
resolved: str = self._resolve_relative_module(trans.Pool, trans.CurrentPackage, impf.level, impf.module)
if resolved is not None:
# Validate the module SHA1 at import time (fast-fail, full translation only). For
# `from . import Y` resolved is a package directory, not a file, so skip the check.
if trans._declare_only == 0 and impf.module is not None: self._check_import_sha1(node, resolved)
trans._imported_modules = self.add_imported_module(trans.Pool, trans._imported_modules, resolved)
# For `from . import Y` resolved is the package name; each name is a submodule.
# Add the full submodule names or cross-module resolution of syscall.* fails.
if impf.module is None:
names_m: list[ast.AST*]* = impf.names
if names_m is not None:
nn_m: sizet = len(names_m)
for ni_m in range(int(nn_m)):
alias_m: ast.Alias* = ast.Alias*(names_m.get(ni_m))
if alias_m is not None and alias_m.name is not None:
rl_m: sizet = string.strlen(resolved)
rn_m: sizet = string.strlen(alias_m.name)
jb_m: str = trans.Pool.alloc(rl_m + rn_m + 2)
if jb_m is not None:
string.strcpy(jb_m, resolved)
jb_m[rl_m] = '.'
string.strcpy(jb_m + rl_m + 1, alias_m.name)
# Y may be a package __init__.vp symbol (class/constant) with no
# file or SHA1; validate/register only for real submodules or local files.
if (StubMergerStore.LookupSha1ByModName(jb_m) is not None or self._is_local_module_file(alias_m.name) != 0):
if trans._declare_only == 0: self._check_import_sha1(node, jb_m)
trans._imported_modules = self.add_imported_module(trans.Pool, trans._imported_modules, jb_m)
return 0
# translate_import_from_names handles the name part of `from X import Y` below.
# _CheckStaticFromImport fail-fasts on `from mod import name` hitting a static symbol.
def _CheckStaticFromImport(self, trans: HT.Translator*, fi_target: str, orig_name: str, node: ast.AST*) -> int:
if trans is None or fi_target is None or orig_name is None: return 0
if trans.ModuleSha1 is None: return 0
# Fast short-circuit: zero overhead when the project has no static symbols
if self.Gen.CallGenBase.StaticCount <= 0: return 0
sf_sha1: str = self.Gen.StructGen._resolve_module_sha1(fi_target, trans._from_imports)
if sf_sha1 is None: return 0
if string.strncmp(sf_sha1, trans.ModuleSha1, 16) == 0: return 0
if self.Gen.CallGenBase.IsStaticSymbol(sf_sha1, orig_name) == 0: return 0
self.Gen.TypeGenErr.fatal_error(node, f"'{fi_target}.{orig_name}' is a static symbol; cross-module use is forbidden (static is visible only within the defining module)", "NameError")
return 0
def translate_import_from_names(self, trans: HT.Translator*, node: ast.AST*) -> int:
"""Translate the name part of a from-import statement."""
impf: ast.ImportFrom* = ast.ImportFrom*(node)
if impf is None: return 0
# Resolve the module name: relative imports need the package prefix
resolved: str = self._resolve_relative_module(trans.Pool, trans.CurrentPackage, impf.level, impf.module)
if resolved is not None:
names: list[ast.AST*]* = impf.names
if names is not None:
nn: sizet = len(names)
for ni in range(int(nn)):
alias: ast.Alias* = ast.Alias*(names.get(ni))
if alias is not None and alias.name is not None:
local_name: str = alias.name
orig_name: str = None
if alias.asname is not None:
local_name = alias.asname
orig_name = alias.name
# For `from . import Y`, _resolve_relative_module returns the package name
# but Y is a submodule, so map local_name -> the full submodule name.
fi_target: str = resolved
if impf.module is None and resolved is not None:
fl_t: sizet = string.strlen(resolved)
fn_t: sizet = string.strlen(alias.name)
jb_t: str = trans.Pool.alloc(fl_t + fn_t + 2)
if jb_t is not None:
string.strcpy(jb_t, resolved)
jb_t[fl_t] = '.'
string.strcpy(jb_t + fl_t + 1, alias.name)
fi_target = jb_t
trans._from_imports = self.add_from_import(trans.Pool, trans._from_imports, local_name, fi_target, orig_name)
# Static symbols (forbidden cross-module): `from mod import name` hitting one
# fail-fasts. Logic stays in _CheckStaticFromImport (inlining breaks inference).
self._CheckStaticFromImport(trans, fi_target, alias.name, node)
# Namespace isolation: mark the from-imported name as a visible struct
self.Gen.StructGen.add_visible_struct(trans.Pool, local_name)
# Register the re-export mapping (current module re-exports local_name from
# fi_target) so cross-module module.func() resolves to the right SHA1.
if trans.ModuleSha1 is not None: self.register_reexport(trans.ModuleSha1, local_name, fi_target)
return 0
def InitImportsGen(self) -> int:
"""Initialize the ImportsGen fields, called by Generator at construction."""
if self is None: return 1
self.ReexportSha1s = None
self.ReexportFuncs = None
self.ReexportSrcs = None
self.ReexportCount = 0
# Import lookup index fields (created incrementally by add_*, PermPool-allocated)
self.CurImportedModulesPtr = None
self.CurImportedModulesNames = None
self.CurFromImportsPtr = None
self.CurFromImportsMods = None
self.CurFromImportsOrigs = None
return 0