This commit is contained in:
bQUARKz 2026-02-10 19:37:07 +00:00
parent df8c0db6eb
commit 2ba8507284
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
5 changed files with 88 additions and 56 deletions

View File

@ -125,6 +125,33 @@ impl ImportRef {
pub fn new(project: ProjectAlias, module: ModulePath, item: ItemName) -> Self {
Self { project, module, item }
}
/// Parse a PBS-style import `from` string and an `item` into an `ImportRef`.
///
/// Expected `from_str` format: "@<alias>:<module_path>", e.g., "@sdk:input/testing".
/// `item_str` must be a single symbol name like "Test" or "ServiceName".
pub fn parse_pbs_import<S1: AsRef<str>, S2: AsRef<str>>(from_str: S1, item_str: S2) -> Result<Self, CanonError> {
let (project, module) = parse_pbs_from_string(from_str)?;
let item = ItemName::new(item_str)?;
Ok(ImportRef { project, module, item })
}
}
/// Parse the PBS `from` string in the canonical format `@<alias>:<module_path>`.
/// Returns `(ProjectAlias, ModulePath)` or a `CanonError` if invalid.
pub fn parse_pbs_from_string<S: AsRef<str>>(from_str: S) -> Result<(ProjectAlias, ModulePath), CanonError> {
let s = from_str.as_ref().trim();
// Must start with '@' and contain a ':' separating alias and module path
let rest = s.strip_prefix('@').ok_or(CanonError::InvalidStart("ImportFrom"))?;
let mut parts = rest.splitn(2, ':');
let alias = parts.next().unwrap_or("");
let module = parts.next().unwrap_or("");
if alias.is_empty() || module.is_empty() {
return Err(CanonError::Empty("ImportFrom"));
}
let project = ProjectAlias::new(alias)?;
let module = ModulePath::parse(module)?;
Ok((project, module))
}
/// Export kind — generic, FE-agnostic.
@ -270,4 +297,28 @@ mod tests {
assert!(ItemName::new("_Test").is_err());
assert!(ItemName::new("Te-st").is_err());
}
#[test]
fn parse_pbs_from_string_valid_and_normalization() {
let (proj, module) = parse_pbs_from_string("@sdk:input/testing").unwrap();
assert_eq!(proj.as_str(), "sdk");
assert_eq!(module.as_str(), "input/testing");
// Normalization of module path
let (_, module2) = parse_pbs_from_string("@sdk:/Input/./Testing/").unwrap();
assert_eq!(module2.as_str(), "input/testing");
// Windows-style separators normalize
let (_, module3) = parse_pbs_from_string("@abc:foo\\bar").unwrap();
assert_eq!(module3.as_str(), "foo/bar");
}
#[test]
fn parse_pbs_from_string_invalid_forms() {
assert!(parse_pbs_from_string("").is_err());
assert!(parse_pbs_from_string("sdk:input/testing").is_err()); // missing '@'
assert!(parse_pbs_from_string("@Sdk:input/testing").is_err()); // invalid alias
assert!(parse_pbs_from_string("@sdk:").is_err()); // empty module
assert!(parse_pbs_from_string("@sdk:..").is_err()); // parent segments forbidden
}
}

View File

@ -141,6 +141,8 @@ pub fn compile_project(
for (alias, project_id) in &step.deps {
if let Some(compiled) = dep_modules.get(project_id) {
for (key, _meta) in &compiled.exports {
// Track both legacy and canonical forms ONLY for collision detection between deps and local modules.
// Import resolution uses only '@alias:module' elsewhere.
let synthetic_paths = [
format!("{}/{}", alias, key.module_path),
format!("@{}:{}", alias, key.module_path),

View File

@ -2,7 +2,7 @@ use crate::frontends::pbs::{parser::Parser, SymbolCollector, ModuleSymbols, Reso
use crate::lowering::core_to_vm;
use crate::common::spans::FileId;
use frontend_api::traits::{Frontend as CanonFrontend, FrontendUnit};
use frontend_api::types::{Diagnostic as CanonDiagnostic, Severity as CanonSeverity, ExportItem, ExportKind, ItemName, LoweredIr};
use frontend_api::types::{Diagnostic as CanonDiagnostic, Severity as CanonSeverity, ExportItem, ExportKind, ItemName, LoweredIr, ImportRef, parse_pbs_from_string};
use prometeu_analysis::NameInterner;
use std::path::Path;
@ -59,8 +59,36 @@ impl CanonFrontend for PbsFrontendAdapter {
return FrontendUnit { diagnostics: diags, imports: vec![], exports: vec![], lowered_ir: LoweredIr::default() };
}
// TODO: Collect canonical imports from resolver.imported_symbols if/when available
let imports = Vec::new();
// Collect canonical imports from AST imports: `from` must be "@alias:module" and items single identifiers
let mut imports: Vec<ImportRef> = Vec::new();
if let crate::frontends::pbs::ast::NodeKind::File(file_node) = parsed.arena.kind(parsed.root) {
for imp_id in &file_node.imports {
if let crate::frontends::pbs::ast::NodeKind::Import(imp) = parsed.arena.kind(*imp_id) {
// Parse project/module from `from` string
match parse_pbs_from_string(&imp.from) {
Ok((project, module)) => {
// Resolve item names from spec node
if let crate::frontends::pbs::ast::NodeKind::ImportSpec(spec) = parsed.arena.kind(imp.spec) {
for &name_id in &spec.path {
let name_str = interner.resolve(name_id);
match ItemName::new(name_str) {
Ok(item) => imports.push(ImportRef::new(project.clone(), module.clone(), item)),
Err(e) => diags.push(CanonDiagnostic::error(format!(
"Invalid import item '{}': {}",
name_str, e
))),
}
}
}
}
Err(e) => diags.push(CanonDiagnostic::error(format!(
"Invalid import path '{}': {}",
&imp.from, e
))),
}
}
}
}
// Prepare canonical exports from symbol tables (names only, kind best-effort)
let mut exports: Vec<ExportItem> = Vec::new();

View File

@ -1322,12 +1322,9 @@ impl<'a> Lowerer<'a> {
// Usar o binding real do import para este Service (ex.: Log -> (sdk, log))
let obj_name_str = obj_name.to_string();
if let Some((dep_alias, module_path)) = self.import_bindings.get(&obj_name_str).cloned() {
// Determine the synthetic module path used when we synthesized dependency symbols
// We support both styles: "alias/module" and "@alias:module"
let synthetic_paths = [
format!("{}/{}", dep_alias, module_path),
format!("@{}:{}", dep_alias, module_path),
];
// Determine the canonical module origin used when we synthesized dependency symbols
// Only supported style: "@alias:module"
let canonical_origin = format!("@{}:{}", dep_alias, module_path);
// Find candidates among imported value symbols matching:
// - name in the new canonical form: "Service.member#sigN" (prefix match on qualified base)
@ -1342,7 +1339,7 @@ impl<'a> Lowerer<'a> {
let matches_legacy = sname.starts_with(&format!("{}#sig", member_name));
if matches_qualified || matches_legacy {
if let Some(orig) = &s.origin {
if synthetic_paths.iter().any(|p| p == orig) {
if *orig == canonical_origin {
candidates.push(s);
}
}

View File

@ -6,52 +6,6 @@
---
## PR-03.02 — Ban PBS leakage at the BE boundary (dependency & import hygiene)
### Title
Remove PBS imports from BE layers and enforce `frontend-api` boundary
### Briefing / Context
BE code currently imports PBS modules (symbols, typed builder, etc.) from `prometeu-compiler`. This is the leak that makes the system unmaintainable and creates accidental coupling. We must ensure BE only depends on `frontend-api` outputs.
### Target
* BE layers (`building/*`, `sources.rs`, orchestrator/linker paths) **must not import PBS modules**.
* Any FE-specific logic is moved behind the `Frontend` implementation.
### Scope
* Replace `use crate::frontends::pbs::*` imports in BE files with `frontend-api` types.
* Add a simple compile-time guard:
* Option A: a `deny`/lint via `mod` separation + no re-exports.
* Option B: create `crates/prometeu-compiler-backend` module that does not depend on `pbs` module.
* Identify and remove PBS-specific helper calls inside BE (e.g., `build_typed_module_symbols` from BE).
### Out of scope
* Fixing overload resolution itself (handled in later PRs).
### Checklist
* [ ] Update imports in BE files.
* [ ] Remove PBS type references from BE data structures.
* [ ] Ensure build compiles without BE → PBS direct dependency.
* [ ] Add a “boundary test”: a module that `use`s backend and fails to compile if PBS is required (or a CI check script).
### Tests
* `cargo test -p prometeu-compiler`
* `cargo test --workspace`
### Risk
Medium. Refactor touches build/orchestrator wiring.
---
## PR-03.03 — Canonical import syntax → `ImportRef` (no dual styles)
### Title