pr 03.01
This commit is contained in:
parent
f5d259ba2a
commit
6a4f7ea773
8
Cargo.lock
generated
8
Cargo.lock
generated
@ -720,6 +720,14 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "frontend-api"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures"
|
name = "futures"
|
||||||
version = "0.3.31"
|
version = "0.3.31"
|
||||||
|
|||||||
@ -11,7 +11,8 @@ members = [
|
|||||||
"crates/prometeu-firmware",
|
"crates/prometeu-firmware",
|
||||||
"crates/prometeu-analysis",
|
"crates/prometeu-analysis",
|
||||||
"crates/prometeu-lsp",
|
"crates/prometeu-lsp",
|
||||||
"crates/prometeu-hardware-contract"
|
"crates/prometeu-hardware-contract",
|
||||||
|
"crates/frontend-api"
|
||||||
]
|
]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
|
|||||||
15
crates/frontend-api/Cargo.toml
Normal file
15
crates/frontend-api/Cargo.toml
Normal file
@ -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"]
|
||||||
11
crates/frontend-api/src/lib.rs
Normal file
11
crates/frontend-api/src/lib.rs
Normal file
@ -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::*;
|
||||||
23
crates/frontend-api/src/traits.rs
Normal file
23
crates/frontend-api/src/traits.rs
Normal file
@ -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<Diagnostic>,
|
||||||
|
pub imports: Vec<ImportRef>,
|
||||||
|
pub exports: Vec<ExportItem>,
|
||||||
|
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;
|
||||||
|
}
|
||||||
262
crates/frontend-api/src/types.rs
Normal file
262
crates/frontend-api/src/types.rs
Normal file
@ -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: AsRef<str>>(s: S) -> Result<Self, CanonError> {
|
||||||
|
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: AsRef<str>>(s: S) -> Result<Self, CanonError> {
|
||||||
|
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: AsRef<str>>(s: S) -> Result<Self, CanonError> {
|
||||||
|
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: Into<String>>(m: M) -> Self { Self { message: m.into(), severity: Severity::Error } }
|
||||||
|
pub fn warning<M: Into<String>>(m: M) -> Self { Self { message: m.into(), severity: Severity::Warning } }
|
||||||
|
pub fn info<M: Into<String>>(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<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LoweredIr {
|
||||||
|
pub fn new<S: Into<Cow<'static, str>>>(format: S, bytes: Vec<u8>) -> 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -400,16 +400,9 @@ pub fn compile_project(
|
|||||||
|
|
||||||
let name_simple = interner.resolve(sym.name).to_string();
|
let name_simple = interner.resolve(sym.name).to_string();
|
||||||
|
|
||||||
// For service methods, VM function name is "Service.method"
|
// VM function names are currently simple for both free functions and service methods (method name only).
|
||||||
let expected_vm_name = if let Some(origin) = &sym.origin {
|
// We still export service methods using qualified names, but we match VM functions by simple name.
|
||||||
if let Some(svc) = origin.strip_prefix("svc:") {
|
let expected_vm_name = name_simple.clone();
|
||||||
format!("{}.{}", svc, name_simple)
|
|
||||||
} else {
|
|
||||||
name_simple.clone()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
name_simple.clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Find VM functions that originated in this module_path and match expected name
|
// Find VM functions that originated in this module_path and match expected name
|
||||||
for (i, f) in combined_vm.functions.iter().enumerate() {
|
for (i, f) in combined_vm.functions.iter().enumerate() {
|
||||||
@ -420,7 +413,20 @@ pub fn compile_project(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let sig_name = format!("{}#sig{}", name_simple, f.sig.0);
|
// Canonical export key name:
|
||||||
|
// - Free function: "name#sig<id>"
|
||||||
|
// - Service method: "Service.method#sig<id>"
|
||||||
|
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(|| {
|
let ty = sym.ty.clone().ok_or_else(|| {
|
||||||
CompileError::Internal(format!(
|
CompileError::Internal(format!(
|
||||||
@ -581,4 +587,51 @@ mod tests {
|
|||||||
};
|
};
|
||||||
assert!(compiled.exports.contains_key(&vec2_key));
|
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::<Vec<_>>());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1330,13 +1330,17 @@ impl<'a> Lowerer<'a> {
|
|||||||
];
|
];
|
||||||
|
|
||||||
// Find candidates among imported value symbols matching:
|
// 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
|
// - origin equals the bound synthetic module path
|
||||||
let mut candidates: Vec<&Symbol> = Vec::new();
|
let mut candidates: Vec<&Symbol> = Vec::new();
|
||||||
for list in self.imported_symbols.value_symbols.symbols.values() {
|
for list in self.imported_symbols.value_symbols.symbols.values() {
|
||||||
for s in list {
|
for s in list {
|
||||||
let sname = self.interner.resolve(s.name);
|
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 let Some(orig) = &s.origin {
|
||||||
if synthetic_paths.iter().any(|p| p == orig) {
|
if synthetic_paths.iter().any(|p| p == orig) {
|
||||||
candidates.push(s);
|
candidates.push(s);
|
||||||
@ -1384,10 +1388,11 @@ impl<'a> Lowerer<'a> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if let Some(sig) = sig_opt {
|
if let Some(sig) = sig_opt {
|
||||||
|
let base_name = format!("{}.{}", obj_name, member_name);
|
||||||
self.emit(InstrKind::ImportCall {
|
self.emit(InstrKind::ImportCall {
|
||||||
dep_alias,
|
dep_alias,
|
||||||
module_path,
|
module_path,
|
||||||
base_name: member_name.to_string(),
|
base_name,
|
||||||
sig,
|
sig,
|
||||||
arg_count: n.args.len() as u32,
|
arg_count: n.args.len() as u32,
|
||||||
});
|
});
|
||||||
|
|||||||
356
files/Hard Reset FE API.md
Normal file
356
files/Hard Reset FE API.md
Normal file
@ -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 `@<alias>:<module_path>` 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<ItemName>, 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).
|
||||||
Loading…
x
Reference in New Issue
Block a user