From 17c7e1f896cd16fccc006929b6b983f08bc5d459 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Wed, 4 Nov 2026 17:36:15 +0800 Subject: [PATCH] feat(obs): isolate __obs__ library imports in includes_obs Each configured include root also maps to an includes_obs sibling: OBS files may only import modules that exist there, and non-OBS files may not import includes_obs-only modules. Scanning and stub resolution use the combined list, while the Gen layer enforces the direction. --- App/lib/Projectrans/Config.vp | 68 +++++++++++++++++++++++++++++ App/lib/core/Gen/ImportsGenMixin.vp | 55 +++++++++++++++++++++++ App/main.vp | 18 ++++---- 3 files changed, 132 insertions(+), 9 deletions(-) diff --git a/App/lib/Projectrans/Config.vp b/App/lib/Projectrans/Config.vp index 2158ca8..85e9485 100644 --- a/App/lib/Projectrans/Config.vp +++ b/App/lib/Projectrans/Config.vp @@ -35,6 +35,12 @@ Sha1SliceLevel: int = 0 # SHA1 slice level (directory dispersion depth) StrictMode: int = 0 IncludesDir: str = None # includes dir path (first element of includes array, single-dir compat) IncludesDirs: str = None # all includes dirs (space-separated, multi-dir support) +# __obs__ include roots: the same list with each `includes` component replaced by `includes_obs`. +# OBS files may only import library modules that exist here; non-OBS files may not import them. +IncludesDirObs: str = None +IncludesDirsObs: str = None +# Scan/resolve search path = IncludesDirs + IncludesDirsObs (import direction is enforced by the Gen layer). +IncludesDirsAll: str = None Qt6Path: str = None # runtime.qt6_path: Qt6 install root (optional) Qt6Dlls: str = None # runtime.qt6_dlls joined (space-separated DLL names; optional) Qt6Plugins: str = None # runtime.qt6_plugins joined (space-separated paths relative to /plugins; optional) @@ -351,9 +357,33 @@ def _normalize_path(path: str) -> str: return out +def _obs_include_one(dir: str) -> str: + """`.../includes` -> `.../includes_obs`; other directories get `/includes_obs` appended.""" + if dir is None: return None + n: sizet = string.strlen(dir) + while n > 0 and (dir[n - 1] == '/' or dir[n - 1] == '\\'): n -= 1 + if n >= 8: + tail: str = dir + (n - 8) + if string.strncmp(tail, "includes", 8) == 0: + out_obs: str = _mbuddy.alloc(n + 4) + if out_obs is not None: + string.strncpy(out_obs, dir, n) + out_obs[n] = '\0' + string.strcat(out_obs, "_obs") + return out_obs + out_add: str = _mbuddy.alloc(n + 16) + if out_add is not None: + string.strncpy(out_add, dir, n) + out_add[n] = '\0' + string.strcat(out_add, "/includes_obs") + return out_add + return None + + def resolve_paths(project_dir: str) -> int: """Resolve and normalize project paths (src/build/includes); 0=success.""" global SourceDir, BuildDir, TempDir, OutputDir, IncludesDir, IncludesDirs + global IncludesDirObs, IncludesDirsObs, IncludesDirsAll global ProjectDir, LinkerStaticLibs, LinkerDynamicLibs if project_dir is None: return 1 if _mbuddy is None: return 1 @@ -407,6 +437,44 @@ def resolve_paths(project_dir: str) -> int: id_out[id_pos] = '\0' IncludesDirs = id_out if IncludesDir is not None: IncludesDir = _join_path(IncludesDir, project_dir) + # OBS include roots: `.../includes` -> `.../includes_obs` for every entry. + if IncludesDirs is not None: + id_all: sizet = string.strlen(IncludesDirs) + obs_out: str = _mbuddy.alloc(id_all * 3 + 96) + if obs_out is not None: + obs_out[0] = '\0' + obs_pos: sizet = 0 + oi: sizet = 0 + while oi < id_all: + while oi < id_all and IncludesDirs[oi] == ' ': oi += 1 + if oi >= id_all: break + o_start: sizet = oi + while oi < id_all and IncludesDirs[oi] != ' ': oi += 1 + seg_l: sizet = oi - o_start + if seg_l > 0: + seg_b: str = _mbuddy.alloc(seg_l + 1) + if seg_b is not None: + string.strncpy(seg_b, IncludesDirs + o_start, seg_l) + seg_b[seg_l] = '\0' + mapped: str = _obs_include_one(seg_b) + if mapped is not None: + ml: sizet = string.strlen(mapped) + if obs_pos > 0: + obs_out[obs_pos] = ' ' + obs_pos += 1 + string.strcpy(obs_out + obs_pos, mapped) + obs_pos += ml + obs_out[obs_pos] = '\0' + IncludesDirsObs = obs_out + if IncludesDirObs is None and IncludesDir is not None: IncludesDirObs = _obs_include_one(IncludesDir) + # Combined search path for scanning/resolution (Gen enforces which side may import what). + if IncludesDirsObs is not None and string.strlen(IncludesDirsObs) > 0: + all_len: sizet = string.strlen(IncludesDirs) + string.strlen(IncludesDirsObs) + 2 + all_buf: str = _mbuddy.alloc(all_len) + if all_buf is not None: + viperlib.snprintf(all_buf, all_len, "%s %s", IncludesDirs, IncludesDirsObs) + IncludesDirsAll = all_buf + if IncludesDirsAll is None: IncludesDirsAll = IncludesDirs # linker.static_libs explicit paths resolved segment-by-segment to abs paths against project_dir if LinkerStaticLibs is not None: sl_total: sizet = string.strlen(LinkerStaticLibs) diff --git a/App/lib/core/Gen/ImportsGenMixin.vp b/App/lib/core/Gen/ImportsGenMixin.vp index 077314c..5c2d964 100644 --- a/App/lib/core/Gen/ImportsGenMixin.vp +++ b/App/lib/core/Gen/ImportsGenMixin.vp @@ -10,6 +10,8 @@ import stdio import sys import io import lib.Stub.StubMerger as StubMerger +import lib.core.IncludesScanner as IncludesScanner +import lib.Projectrans.Config as Config import lib.core.CompilerFactory as CompilerFactory import lib.core.Gen.GenApi as GenApi import lib.core.Gen.Translator as HT @@ -641,6 +643,54 @@ novirtual class ImportsGenMixin: else: self._report_import_error(node, "imported module SHA1 not registered (the includes scan did not find this module)") return 1 + # _obs_import_check - enforce the __obs__ include split for an absolute import. + def _obs_import_check(self, trans: HT.Translator*, module_name: str, node: ast.AST*) -> int: + """OBS files may only import includes_obs modules; non-OBS files may not import them.""" + if trans is None or node is None or module_name is None: return 0 + if string.strlen(module_name) == 0 or module_name[0] == '.': return 0 + in_normal: int = self._module_resolves(trans.Pool, Config.IncludesDirs, module_name) + in_obs: int = self._module_resolves(trans.Pool, Config.IncludesDirsObs, module_name) + if in_normal != 0 and in_obs != 0: return 0 + if trans.StrictOBS != 0: + if in_normal != 0: + self.Gen.TypeGenErr.fatal_error(node, f"__obs__ files may only import library modules provided by includes_obs: '{module_name}' is not available there", "NameError") + return 1 + else: + if in_obs != 0: + self.Gen.TypeGenErr.fatal_error(node, f"module '{module_name}' is provided by includes_obs and can only be imported from an __obs__ file; add `from __future__ import __obs__`", "NameError") + return 1 + return 0 + + # _module_resolves - 1 when the module resolves to a file under the given include dirs. + def _module_resolves(self, pool: memhub.MemBuddy*, dirs: str, module_name: str) -> int: + if dirs is None or pool is None: return 0 + oa: rawptr = stdlib.malloc(8) + orl: rawptr = stdlib.malloc(8) + oi: rawptr = stdlib.malloc(8) + if oa is None or orl is None or oi is None: + if oa is not None: stdlib.free(oa) + if orl is not None: stdlib.free(orl) + if oi is not None: stdlib.free(oi) + return 0 + a_s: sizet* = sizet*(oa) + r_s: sizet* = sizet*(orl) + i_s: sizet* = sizet*(oi) + a_s[0] = 0 + r_s[0] = 0 + i_s[0] = 0 + rc: int = IncludesScanner.ResolveModuleToPath(dirs, module_name, oa, orl, oi) + a_path: str = rawptr(a_s[0]) + r_path: str = rawptr(r_s[0]) + i_path: str = rawptr(i_s[0]) + if a_path is not None: stdlib.free(a_path) + if r_path is not None: stdlib.free(r_path) + if i_path is not None: stdlib.free(i_path) + stdlib.free(oa) + stdlib.free(orl) + stdlib.free(oi) + if rc == 0: return 1 + return 0 + # 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.""" @@ -655,6 +705,8 @@ novirtual class ImportsGenMixin: 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: + # OBS include split: enforce which include root may provide this module. + if trans._declare_only == 0: self._obs_import_check(trans, alias.name, node) # 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) @@ -678,6 +730,9 @@ novirtual class ImportsGenMixin: # 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: + # OBS include split (absolute imports only; relative ones belong to the app tree). + if trans._declare_only == 0 and impf.level == 0 and impf.module is not None: + self._obs_import_check(trans, impf.module, node) # 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) diff --git a/App/main.vp b/App/main.vp index 0361697..e214a7c 100644 --- a/App/main.vp +++ b/App/main.vp @@ -38,7 +38,6 @@ import mlirlite import subprocess import lib.Phase.Phase1.Phase1Main as Phase1Main - # Main Pool 1 GiB(不要简单调大:2 GiB 会让 MemBuddy 内部索引/位宽溢出 —— # ASan: access-violation at memhub.vp:858 _fl_push,就在 __init__ 阶段。 # 池耗尽的正解是修泄漏:mlirlite 全局数组 dense 渲染的"每元素 ~5KB 不复用",见 includes e9e1933。) @@ -55,12 +54,12 @@ define SRC_BUF_SIZE = 1048576 # Version (dev/beta/rc) # Fixed-size so the bytes stay inline in .PRVERSN even when this file is compiled by the # previous bootstrap compiler; the new `[]char` value-array rule infers the same 14 bytes. -V_VPC_VERSION: const __attribute__(section(".PRVERSN")) [32]char = "0.0.5-dev.479" -define V_VIPER_VERSION = "1.0.5-dev.970 2026-09-28 Non-LTS" -define V_RUNTIME_VERSION = "0.2.3+19-Non-LTS" -define V_TPC_COMMIT = "e376b949c9" -define V_VPC_COMMIT = "ab0f37e1a8" -define V_RUNTIME_COMMIT = "f087aa88e2" +V_VPC_VERSION: const __attribute__(section(".PRVERSN")) [32]char = "0.1.1-dev.964" +define V_VIPER_VERSION = "1.0.6-dev.194 2026-11-03 Non-LTS" +define V_RUNTIME_VERSION = "0.2.4+297-Non-LTS" +define V_TPC_COMMIT = "077d705609" +define V_VPC_COMMIT = "2c65cea97f" +define V_RUNTIME_COMMIT = "a0a71e7a85" define V_TARGET = "x86_64-w64-windows-gnu" define V_LLVM = "22.1.8" # --repl template-project lookup buffers (repl.vpj path / miss diagnostic text) @@ -597,7 +596,7 @@ Target: %s if do_phase1 != 0 and Config.IncludesDirs is not None: ph1_temp: str = Config.TempDir if ph1_temp is None: ph1_temp = "." - if Phase1Main.RunPhase1(mb, Config.IncludesDirs, ph1_temp, log) != 0: + if Phase1Main.RunPhase1(mb, Config.IncludesDirsAll, ph1_temp, log) != 0: VLogger.error("Phase1 failed, aborting compilation", "Phase1") return 1 # Phase1-only (no Phase2): exit right away @@ -628,7 +627,8 @@ Target: %s mf_dynamic_libs: str = Config.LinkerDynamicLibs mf_includes_bin: str = Config.get_includes_binary_dir() - mf_ret: int = Phase2.RunMultiFileProject(mb, Config.SourceDir, mf_temp, mf_output, mf_cc, mf_cc_flags, mf_linker, mf_linker_flags, mf_linker_out, mf_includes_bin, Config.IncludesDirs, do_phase1, do_phase2, log, args, mf_static_libs, mf_dynamic_libs) + mf_inc_dirs: str = Config.IncludesDirsAll if Config.IncludesDirsAll is not None else Config.IncludesDirs + mf_ret: int = Phase2.RunMultiFileProject(mb, Config.SourceDir, mf_temp, mf_output, mf_cc, mf_cc_flags, mf_linker, mf_linker_flags, mf_linker_out, mf_includes_bin, mf_inc_dirs, do_phase1, do_phase2, log, args, mf_static_libs, mf_dynamic_libs) argparse.release(args) log.success("ViperC complete") return mf_ret