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.
This commit is contained in:
TermiNexus committed 2026-11-04 17:36:15 +08:00
1 parent 2e5218abc6
commit 17c7e1f896
3 files changed
+132 -9

No files matched your search

+68
View File
@@ -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 <qt6_path>/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)
+55
View File
@@ -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)
+9 -9
View File
@@ -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