diff --git a/Cargo.lock b/Cargo.lock index 7769ca30..70d4573a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -720,6 +720,14 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "frontend-api" +version = "0.1.0" +dependencies = [ + "serde", + "thiserror", +] + [[package]] name = "futures" version = "0.3.31" diff --git a/Cargo.toml b/Cargo.toml index 19c089b7..2002bdf8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,8 @@ members = [ "crates/prometeu-firmware", "crates/prometeu-analysis", "crates/prometeu-lsp", - "crates/prometeu-hardware-contract" + "crates/prometeu-hardware-contract", + "crates/frontend-api" ] resolver = "2" diff --git a/crates/frontend-api/Cargo.toml b/crates/frontend-api/Cargo.toml new file mode 100644 index 00000000..f9cf9800 --- /dev/null +++ b/crates/frontend-api/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "frontend-api" +version = "0.1.0" +edition = "2021" +license = "MIT" +description = "Canonical frontend contract for Prometeu Backend: identifiers, references, and strict Frontend trait." +repository = "https://github.com/prometeu/runtime" + +[dependencies] +serde = { version = "1", features = ["derive"], optional = true } +thiserror = "1" + +[features] +default = [] +serde = ["dep:serde"] diff --git a/crates/frontend-api/src/lib.rs b/crates/frontend-api/src/lib.rs new file mode 100644 index 00000000..6f528757 --- /dev/null +++ b/crates/frontend-api/src/lib.rs @@ -0,0 +1,11 @@ +//! Frontend API (canonical contract between Frontend and Backend) +//! +//! Policy: no strings-as-protocol. All identifiers use canonical newtypes with +//! clear invariants and normalization helpers. No PBS- (or any FE-) specific +//! types are allowed in this crate. + +pub mod types; +pub mod traits; + +pub use crate::types::*; +pub use crate::traits::*; diff --git a/crates/frontend-api/src/traits.rs b/crates/frontend-api/src/traits.rs new file mode 100644 index 00000000..d85e4cdb --- /dev/null +++ b/crates/frontend-api/src/traits.rs @@ -0,0 +1,23 @@ +//! Strict frontend contract owned by the Backend. +//! +//! Implementations must not expose FE-specific types through this boundary. + +use crate::{Diagnostic, ExportItem, ImportRef, LoweredIr}; + +#[derive(Debug, Default)] +pub struct FrontendUnit { + pub diagnostics: Vec, + pub imports: Vec, + pub exports: Vec, + pub lowered_ir: LoweredIr, +} + +/// Frontend entrypoint that parses and analyzes a single compilation unit +/// and produces a `FrontendUnit` for the Backend. +pub trait Frontend { + /// Parse and analyze the provided sources according to the FE's language, + /// returning only canonical artifacts required by the Backend. + /// + /// No strings-as-protocol allowed in the output; use canonical types. + fn parse_and_analyze(&self, entry_path: &str) -> FrontendUnit; +} diff --git a/crates/frontend-api/src/types.rs b/crates/frontend-api/src/types.rs new file mode 100644 index 00000000..4599afc2 --- /dev/null +++ b/crates/frontend-api/src/types.rs @@ -0,0 +1,262 @@ +use core::fmt; +use std::borrow::Cow; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use thiserror::Error; + +/// A project alias (canonical lowercase name). +/// Invariants: +/// - lowercase ASCII +/// - must start with [a-z] +/// - remaining chars: [a-z0-9_-] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] +pub struct ProjectAlias(String); + +impl ProjectAlias { + pub fn new>(s: S) -> Result { + let s = s.as_ref().trim(); + if s.is_empty() { + return Err(CanonError::Empty("ProjectAlias")); + } + if !s.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' || c == '_') { + return Err(CanonError::InvalidChars("ProjectAlias")); + } + let mut chars = s.chars(); + match chars.next() { + Some(c) if c.is_ascii_lowercase() => {} + _ => return Err(CanonError::InvalidStart("ProjectAlias")), + } + Ok(Self(s.to_string())) + } + + pub fn as_str(&self) -> &str { &self.0 } +} + +impl fmt::Display for ProjectAlias { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0.fmt(f) } +} + +/// Canonical module path with '/' separators and lowercase segments. +/// Invariants: +/// - uses '/' only; '\\' normalized to '/' +/// - no leading/trailing '/' +/// - no empty segments, no duplicate '//' +/// - segments are lowercase ASCII [a-z0-9_-] +/// - '.' segments are removed; '..' is forbidden +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] +pub struct ModulePath(String); + +impl ModulePath { + pub fn parse>(s: S) -> Result { + let raw = s.as_ref().trim(); + if raw.is_empty() { + return Err(CanonError::Empty("ModulePath")); + } + let raw = raw.replace('\\', "/"); + let raw = raw.trim_matches('/'); + if raw.is_empty() { + return Err(CanonError::Empty("ModulePath")); + } + let mut out: Vec<&str> = Vec::new(); + for seg in raw.split('/') { + if seg.is_empty() { return Err(CanonError::EmptySegment("ModulePath")); } + if seg == "." { continue; } + if seg == ".." { return Err(CanonError::ParentSegmentsForbidden); } + let seg_lc = seg.to_ascii_lowercase(); + if !seg_lc.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' || c == '_') { + return Err(CanonError::InvalidChars("ModulePath")); + } + out.push(Box::leak(seg_lc.into_boxed_str())); + } + if out.is_empty() { return Err(CanonError::Empty("ModulePath")); } + Ok(Self(out.join("/"))) + } + + pub fn as_str(&self) -> &str { &self.0 } +} + +impl fmt::Display for ModulePath { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0.fmt(f) } +} + +/// Canonical exported/declared item name. +/// Invariants: +/// - must start with [A-Z] +/// - remaining chars: [A-Za-z0-9_] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] +pub struct ItemName(String); + +impl ItemName { + pub fn new>(s: S) -> Result { + let s = s.as_ref().trim(); + if s.is_empty() { return Err(CanonError::Empty("ItemName")); } + let mut chars = s.chars(); + match chars.next() { + Some(c) if c.is_ascii_uppercase() => {} + _ => return Err(CanonError::InvalidStart("ItemName")), + } + if !s[1..].chars().all(|c| c.is_ascii_alphanumeric() || c == '_') { + return Err(CanonError::InvalidChars("ItemName")); + } + Ok(Self(s.to_string())) + } + pub fn as_str(&self) -> &str { &self.0 } +} + +impl fmt::Display for ItemName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0.fmt(f) } +} + +/// A fully-qualified import reference: project + module + item. +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] +pub struct ImportRef { + pub project: ProjectAlias, + pub module: ModulePath, + pub item: ItemName, +} + +impl ImportRef { + pub fn new(project: ProjectAlias, module: ModulePath, item: ItemName) -> Self { + Self { project, module, item } + } +} + +/// Export kind — generic, FE-agnostic. +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)] +pub enum ExportKind { + Function, + Service, + Type, + Const, +} + +/// An export item description (opaque for BE logic, except name/kind). +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] +pub struct ExportItem { + pub name: ItemName, + pub kind: ExportKind, +} + +impl ExportItem { + pub fn new(name: ItemName, kind: ExportKind) -> Self { Self { name, kind } } +} + +/// A fully-qualified export reference: project + module + item + kind. +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] +pub struct ExportRef { + pub project: ProjectAlias, + pub module: ModulePath, + pub item: ItemName, + pub kind: ExportKind, +} + +impl ExportRef { + pub fn new(project: ProjectAlias, module: ModulePath, item: ItemName, kind: ExportKind) -> Self { + Self { project, module, item, kind } + } +} + +/// Canonical function key, identifying an overload by arity only (for now). +/// This will be extended in PR-03.04 with types and calling convention. +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] +pub struct CanonicalFnKey { + pub import: ImportRef, + pub arity: u16, +} + +impl CanonicalFnKey { + pub fn new(import: ImportRef, arity: u16) -> Self { Self { import, arity } } +} + +/// Diagnostic severity. +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Severity { Error, Warning, Info } + +/// A simple diagnostic message produced by the frontend. +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Diagnostic { + pub message: String, + pub severity: Severity, +} + +impl Diagnostic { + pub fn error>(m: M) -> Self { Self { message: m.into(), severity: Severity::Error } } + pub fn warning>(m: M) -> Self { Self { message: m.into(), severity: Severity::Warning } } + pub fn info>(m: M) -> Self { Self { message: m.into(), severity: Severity::Info } } +} + +/// Opaque lowered IR payload. +/// The backend owns the meaning of these bytes for a given `format` tag. +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] +pub struct LoweredIr { + pub format: Cow<'static, str>, + pub bytes: Vec, +} + +impl LoweredIr { + pub fn new>>(format: S, bytes: Vec) -> Self { Self { format: format.into(), bytes } } +} + +/// Errors raised while constructing canonical identifiers. +#[derive(Debug, Error, PartialEq, Eq)] +pub enum CanonError { + #[error("{0} cannot be empty")] Empty(&'static str), + #[error("{0} has invalid starting character")] InvalidStart(&'static str), + #[error("{0} contains invalid characters")] InvalidChars(&'static str), + #[error("{0} contains empty segment")] EmptySegment(&'static str), + #[error("parent segments ('..') are forbidden in ModulePath")] ParentSegmentsForbidden, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn module_path_normalization_basic() { + assert_eq!(ModulePath::parse("input/testing").unwrap().as_str(), "input/testing"); + assert_eq!(ModulePath::parse("/input/testing/").unwrap().as_str(), "input/testing"); + assert_eq!(ModulePath::parse("INPUT/Testing").unwrap().as_str(), "input/testing"); + assert_eq!(ModulePath::parse("input/./testing").unwrap().as_str(), "input/testing"); + assert_eq!(ModulePath::parse("input\\testing").unwrap().as_str(), "input/testing"); + } + + #[test] + fn module_path_rejects_invalid() { + assert!(ModulePath::parse("").is_err()); + assert!(ModulePath::parse("/").is_err()); + assert!(ModulePath::parse("input//testing").is_err()); + assert!(ModulePath::parse("input/../testing").is_err()); + assert!(ModulePath::parse("in$put/testing").is_err()); + } + + #[test] + fn project_alias_rules() { + assert!(ProjectAlias::new("main").is_ok()); + assert!(ProjectAlias::new("_main").is_err()); + assert!(ProjectAlias::new("Main").is_err()); + assert!(ProjectAlias::new("main-1").is_ok()); + assert!(ProjectAlias::new("main/1").is_err()); + } + + #[test] + fn item_name_rules() { + assert!(ItemName::new("Test").is_ok()); + assert!(ItemName::new("Log2").is_ok()); + assert!(ItemName::new("test").is_err()); + assert!(ItemName::new("_Test").is_err()); + assert!(ItemName::new("Te-st").is_err()); + } +} diff --git a/crates/prometeu-compiler/src/building/output.rs b/crates/prometeu-compiler/src/building/output.rs index 2fb25bd8..7d2ba0ba 100644 --- a/crates/prometeu-compiler/src/building/output.rs +++ b/crates/prometeu-compiler/src/building/output.rs @@ -400,16 +400,9 @@ pub fn compile_project( let name_simple = interner.resolve(sym.name).to_string(); - // For service methods, VM function name is "Service.method" - let expected_vm_name = if let Some(origin) = &sym.origin { - if let Some(svc) = origin.strip_prefix("svc:") { - format!("{}.{}", svc, name_simple) - } else { - name_simple.clone() - } - } else { - name_simple.clone() - }; + // VM function names are currently simple for both free functions and service methods (method name only). + // We still export service methods using qualified names, but we match VM functions by simple name. + let expected_vm_name = name_simple.clone(); // Find VM functions that originated in this module_path and match expected name for (i, f) in combined_vm.functions.iter().enumerate() { @@ -420,7 +413,20 @@ pub fn compile_project( continue; } - let sig_name = format!("{}#sig{}", name_simple, f.sig.0); + // Canonical export key name: + // - Free function: "name#sig" + // - Service method: "Service.method#sig" + let canonical_base = if let Some(origin) = &sym.origin { + if let Some(svc) = origin.strip_prefix("svc:") { + format!("{}.{}", svc, name_simple) + } else { + name_simple.clone() + } + } else { + name_simple.clone() + }; + + let sig_name = format!("{}#sig{}", canonical_base, f.sig.0); let ty = sym.ty.clone().ok_or_else(|| { CompileError::Internal(format!( @@ -581,4 +587,51 @@ mod tests { }; assert!(compiled.exports.contains_key(&vec2_key)); } + + #[test] + fn test_service_method_export_qualified() { + let dir = tempdir().unwrap(); + let project_dir = dir.path().to_path_buf(); + + fs::create_dir_all(project_dir.join("src/main/modules")).unwrap(); + + let main_code = r#" + pub service Log { + fn debug(msg: string): void { + } + } + "#; + + fs::write(project_dir.join("src/main/modules/main.pbs"), main_code).unwrap(); + + let project_key = ProjectKey { + name: "root".to_string(), + version: "0.1.0".to_string(), + }; + let project_id = ProjectId(0); + + let step = BuildStep { + project_id, + project_key: project_key.clone(), + project_dir: project_dir.clone(), + target: BuildTarget::Main, + sources: vec![PathBuf::from("src/main/modules/main.pbs")], + deps: BTreeMap::new(), + }; + + let mut file_manager = FileManager::new(); + let compiled = compile_project(step, &HashMap::new(), &mut file_manager) + .expect("Failed to compile project"); + + // Find a function export with qualified method name prefix "Log.debug#sig" + let mut found = false; + for (key, _meta) in &compiled.exports { + if key.kind == ExportSurfaceKind::Function && key.symbol_name.starts_with("Log.debug#sig") { + found = true; + break; + } + } + + assert!(found, "Expected an export with qualified name 'Log.debug#sigX' but not found. Exports: {:?}", compiled.exports.keys().collect::>()); + } } diff --git a/crates/prometeu-compiler/src/frontends/pbs/lowering.rs b/crates/prometeu-compiler/src/frontends/pbs/lowering.rs index 6d02cd31..2f539b08 100644 --- a/crates/prometeu-compiler/src/frontends/pbs/lowering.rs +++ b/crates/prometeu-compiler/src/frontends/pbs/lowering.rs @@ -1330,13 +1330,17 @@ impl<'a> Lowerer<'a> { ]; // Find candidates among imported value symbols matching: - // - name in the new canonical form: "member#sigN" (prefix match on member_name) + // - name in the new canonical form: "Service.member#sigN" (prefix match on qualified base) // - origin equals the bound synthetic module path let mut candidates: Vec<&Symbol> = Vec::new(); for list in self.imported_symbols.value_symbols.symbols.values() { for s in list { let sname = self.interner.resolve(s.name); - if sname.starts_with(&format!("{}#sig", member_name)) { + // Accept both canonical qualified form and legacy simple form for compatibility + let qualified_base = format!("{}.{}", obj_name, member_name); + let matches_qualified = sname.starts_with(&format!("{}#sig", &qualified_base)); + 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) { candidates.push(s); @@ -1384,10 +1388,11 @@ impl<'a> Lowerer<'a> { }; if let Some(sig) = sig_opt { + let base_name = format!("{}.{}", obj_name, member_name); self.emit(InstrKind::ImportCall { dep_alias, module_path, - base_name: member_name.to_string(), + base_name, sig, arg_count: n.args.len() as u32, }); diff --git a/files/Hard Reset FE API.md b/files/Hard Reset FE API.md new file mode 100644 index 00000000..6795cc0c --- /dev/null +++ b/files/Hard Reset FE API.md @@ -0,0 +1,356 @@ +# Phase 03 – Rigid Frontend API & PBS Leak Containment (Junie PR Templates) + +> Goal: **finish Phase 03 with JVM-like discipline** by making the **Backend (BE) the source of truth** and forcing the PBS Frontend (FE) to implement a **strict, minimal, canonical** contract (`frontend-api`). +> +> Strategy: **surgical PRs** that (1) stop PBS types from leaking, (2) replace stringy protocols with canonical models, and (3) make imports/exports/overloads deterministic across deps. + +--- + +## PR-03.01 — Introduce `frontend-api` crate (canonical models + strict trait) + +### Title + +Create `frontend-api` crate with canonical models and strict `Frontend` trait + +### Briefing / Context + +Right now, compiler layers import PBS-specific symbols/types and depend on “string protocols” (e.g., `"alias/module"`, `"@alias:module"`, `svc:` prefixes). This caused Phase 03 instability and the current golden failure (`E_OVERLOAD_NOT_FOUND` for imported service methods). We need a **single source of truth contract** owned by BE. + +### Target (What “done” means) + +* A new crate (or module) `crates/frontend-api` exporting: + + * **Canonical identifiers and references** used by BE for imports/exports. + * A **strict** `Frontend` trait that returns only BE-required artifacts. +* No PBS types inside this crate. + +### Scope + +* Add `frontend-api` crate. +* Define canonical types: + + * `ProjectRef { alias: String }` (or `ProjectAlias` newtype) + * `ModulePath` newtype (normalized `"input/testing"`) + * `ItemName` newtype (e.g., `"Test"`, `"Log"`) + * `ImportRef { project: ProjectRef, module: ModulePath, item: ItemName }` + * `ExportRef` / `ExportItem` (see PR-03.05) + * `CanonicalFnKey` (see PR-03.04) +* Define **strict** `Frontend` trait (draft; implemented later): + + * `parse_and_analyze(...) -> FrontendUnit` + * `FrontendUnit { diagnostics, imports, exports, lowered_ir }` +* Include explicit “no strings-as-protocol” policy in doc comments. + +### Out of scope + +* Implementing PBS FE. +* Refactoring build pipeline. + +### Checklist + +* [ ] Add crate and wire to workspace. +* [ ] Add canonical types with clear invariants. +* [ ] Add `Frontend` trait and minimal `FrontendUnit` output. +* [ ] Add unit tests for parsing/normalization helpers (module path normalization rules). + +### Tests + +* `cargo test -p frontend-api` + +### Risk + +Low. New crate only; no behavior changes yet. + +--- + +## 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 + +Define single canonical import model and parse PBS imports into `ImportRef` + +### Briefing / Context + +We currently support multiple synthetic import path styles (`"alias/module"` and `"@alias:module"`). This amplifies ambiguity and is a root cause of mismatch in imported service method overloads. + +We want **one** canonical representation: + +* PBS syntax: `import { Test } from "@sdk:input/testing"` +* Canonical model: `ImportRef { project: "sdk", module: "input/testing", item: "Test" }` + +### Target + +* PBS FE produces a list of canonical `ImportRef`. +* BE consumes only `ImportRef`. +* Remove support for dual synthetic path style in the BE pipeline. + +### Scope + +* In PBS FE: + + * Parse `@:` into `ImportRef`. + * Validate module path normalization. + * Validate that `item` is a single symbol name (service/struct/host/contract/etc). +* In BE: + + * Replace “synthetic path generation” with canonical module lookup using `(alias, module_path)`. + +### Out of scope + +* Export naming canonicalization (PR-03.04/03.05). + +### Checklist + +* [ ] Implement import parser → `ImportRef`. +* [ ] Remove `alias/module` synthetic path support. +* [ ] Update resolver/module-provider lookup to accept `(alias, module_path)`. +* [ ] Add diagnostics for invalid import string. + +### Tests + +* Unit tests in PBS FE for: + + * valid: `"@sdk:input/testing"` + * invalid forms + * normalization edge cases (leading `/`, `./`, `\\` on Windows paths) +* Integration test (golden-style) compiling a small project importing a service. + +### Risk + +Medium. Changes import resolution plumbing. + +--- + +## PR-03.04 — Canonical function identity: `CanonicalFnKey` (JVM-like) + +### Title + +Introduce canonical function identity for exports/import calls (no string prefix matching) + +### Briefing / Context + +Phase 03 currently tries to match overloads using `name#sigN` strings + prefix logic + origin checks. This breaks easily and is exactly what produced `E_OVERLOAD_NOT_FOUND` for `Log.debug`. + +We need a **canonical function key** that is not “string protocol”: + +* `CanonicalFnKey { owner: Option, name: ItemName, sig: SigId }` + + * Free fn: `owner=None, name=foo, sig=...` + * Service method: `owner=Some(Log), name=debug, sig=...` + +### Target + +* BE uses `CanonicalFnKey` for export surface and import relocation. +* FE supplies `owner/name` and produces/requests signatures deterministically. + +### Scope + +* Add `CanonicalFnKey` to `frontend-api`. +* Update VM import call instruction payload to carry canonical pieces: + + * `ImportCall { dep_alias, module_path, fn_key: CanonicalFnKey, arg_count }` + * (or equivalent) +* Eliminate string matching / prefix matching for overload selection. + +### Checklist + +* [ ] Define `CanonicalFnKey` and helpers. +* [ ] Update IR / bytecode instruction structures if needed. +* [ ] Update lowering call sites. +* [ ] Ensure debug info keeps readable names (owner.name). + +### Tests + +* Unit: canonical formatting for debug name `Log.debug`. +* Integration: two overloads of `Log.debug` across deps resolved by exact signature. + +### Risk + +High-ish. Touches instruction encoding and matching logic. + +--- + +## PR-03.05 — Canonical export surface: `ExportItem` (no `svc:` / no `name#sig` strings) + +### Title + +Replace stringy export naming with canonical `ExportItem` model + +### Briefing / Context + +Exports are currently keyed by `(module_path, symbol_name string, kind)` where symbol_name embeds `#sig` and/or owner names. This is fragile and couples FE naming to BE behavior. + +### Target + +* BE export map keys are canonical: + + * `ExportItem::Type { name }` + * `ExportItem::Service { name }` + * `ExportItem::Function { fn_key: CanonicalFnKey }` +* Export surface remains stable even if we later change display formatting. + +### Scope + +* Update compiled module export structures. +* Update dependency symbol synthesis to use canonical export items. +* Update linker relocation labels to reference canonical export items. + +### Checklist + +* [ ] Introduce `ExportItem` and migrate ExportKey. +* [ ] Update dependency export synthesis. +* [ ] Update linker/import label format (if used) to canonical encoding. +* [ ] Ensure backward compatibility is explicitly NOT required for Phase 03. + +### Tests + +* Unit: exporting a service method yields `ExportItem::Function { owner=Log, name=debug, sig=... }`. +* Integration: build root + dep, link, run golden. + +### Risk + +High. Touches serialization and linking labels. + +--- + +## PR-03.06 — Deterministic overload resolution across deps (arity is not enough) + +### Title + +Implement deterministic overload selection using canonical signature matching + +### Briefing / Context + +We currently try to disambiguate overloads by arity as a fallback. That’s not sufficient (same arity, different types). For Phase 03 “professional grade”, overload resolution must be deterministic and match by full signature. + +### Target + +* Imported method call selects overload by: + + 1. resolve callee symbol → candidate set + 2. typecheck args → determine expected param types + 3. choose exact match + 4. otherwise `E_OVERLOAD_NOT_FOUND` or `E_OVERLOAD_AMBIGUOUS` deterministically + +### Scope + +* PBS FE typechecker must provide enough info to compute signature selection. +* Resolver must expose all overload candidates for an imported `ImportRef` item. +* Lowering uses canonical fn key and selected `SigId`. + +### Checklist + +* [ ] Ensure imported service methods are actually present in imported symbol arena. +* [ ] Ensure candidates include `(owner, name, sig)` not just `name`. +* [ ] Implement exact-match algorithm. +* [ ] Implement deterministic ambiguity ordering for diagnostics. + +### Tests + +* Add golden regression reproducing `Log.debug` failure: + + * dep exports `service Log { debug(string) }` + * root imports `Log` and calls `Log.debug("x")` +* Add tests for: + + * ambiguous same signature + * not found + +### Risk + +Medium/High. Needs clean integration across resolver/typechecker/lowering. + +--- + +## PR-03.07 — Phase 03 cleanup: remove legacy compatibility branches and document boundary + +### Title + +Remove legacy string protocol branches and document FE/BE boundary rules + +### Briefing / Context + +After canonical models are in place, we must delete compatibility code paths (`alias/module`, `svc:` prefixes, prefix matching, etc.) to prevent regressions. + +### Target + +* No legacy synthetic module path support. +* No string prefix matching for overloads. +* Documentation: “BE owns the contract; FE implements it.” + +### Scope + +* Delete dead code. +* Add `docs/phase-03-frontend-api.md` (or in-crate docs) summarizing invariants. +* Add CI/lints to prevent BE from importing PBS modules. + +### Checklist + +* [ ] Remove legacy branches. +* [ ] Add boundary docs. +* [ ] Add lint/CI guard. + +### Tests + +* Full workspace tests. +* Golden tests. + +### Risk + +Low/Medium. Mostly deletion + docs, but could expose hidden dependencies. + +--- + +# Notes / Operating Rules (for Junie) + +1. **BE is the source of truth**: `frontend-api` defines canonical models; FE conforms. +2. **No string protocols** across layers. Strings may exist only as *display/debug*. +3. **No FE implementation imports from other FE implementations**. +4. **No BE imports PBS modules** (hard boundary). +5. **Overload resolution is signature-based** (arity alone is not valid).