This document describes the actual in-memory data model the compiler uses —
PackageProvider, PackageDescriptor/PackageSource, ModuleDescriptor,
PackageItem — as found in fp-core/src/package/ and fp-core/src/module/.
A package is one crate/project — the embedded std, a Cargo workspace
member, a .fp-native project, etc. It's identified by a PackageId (a
plain string wrapper) and described by two structs:
pub struct PackageDescriptor {
pub id: PackageId,
pub name: String,
pub version: Option<Version>,
pub manifest_path: VirtualPath,
pub root: VirtualPath,
pub metadata: PackageMetadata, // dependencies, features, ...
pub modules: Vec<ModuleId>,
}
/// What a `PackageProvider` actually returns for `load_package_source`.
pub struct PackageSource {
pub package_id: PackageId,
pub name: String,
pub graph: PackageGraph,
pub module_paths: HashSet<QualifiedPath>,
pub items: Vec<PackageItem>, // the real payload
}Packages are discovered and parsed by implementations of PackageProvider
(fp-core/src/package/provider.rs):
pub trait PackageProvider {
fn list_packages(&self) -> ProviderResult<Vec<PackageId>>;
fn load_package_metadata(&self, id: &PackageId) -> ProviderResult<Arc<PackageDescriptor>>;
fn refresh(&self) -> ProviderResult<()>;
fn load_package_source(&self, id: &PackageId) -> ProviderResult<PackageSource>;
}Each source language/layout has its own provider:
fp_lang::cargo_provider::CargoWorkspaceProvider— discovers a Cargo/Magnet workspace's member crates and parses every source file in each withFerroFrontend(the.fp-and-Rust-superset parser). This is whatmagnet transpile/fp compile <dir>actually uses today for directory inputs.fp_lang::provider::FerroPhaseProvider— serves the embeddedstd/libcpackages (baked into thefp-langbinary from.fpsource at build time; seefp-lang/build.rs/embedded_std.rs).fp_rust::RustPackageProvider— planned/in-progress provider specifically for real.rsCargo projects, with its ownstdbacked by real rustc source (seedocs/RustStd.md). Not wired into language detection yet.
WorkspaceContext::provider_for(package_id) picks whichever registered
provider's list_packages() includes the requested ID — there's no separate
routing table; providers self-report what they own.
This is the part worth being explicit about, since "module" strongly suggests
Rust's own mod/pub mod system. It's a different, smaller thing here:
pub struct ModuleDescriptor {
pub id: ModuleId, // stable string key, e.g. a path key
pub package: PackageId, // owning package
pub language: ModuleLanguage, // Ferro, Rust, TypeScript, Python, Other(_)
pub module_path: Vec<String>, // e.g. ["repo_backend"] for repo_backend.rs
pub source: VirtualPath, // the file it came from
pub exports: Vec<SymbolDescriptor>,
pub requires_features: Vec<FeatureRef>,
}A ModuleDescriptor exists mainly to answer "which source file (and which
language) did this group of items come from" for bookkeeping/diagnostics
purposes (ModuleLanguage is used, e.g., to treat Rust crates and embedded
.fp std uniformly). It is not a nested namespace with its own
declaration syntax at the compiler-infrastructure level — providers construct
one ModuleDescriptor per source file (see CargoWorkspaceProvider::load_package_source,
which maps each file's relative path to a QualifiedPath via
module_path_from_relative), not per pub mod block.
The actual fine-grained unit the compiler operates on below the module level
is PackageItem:
pub struct PackageItem {
pub path: QualifiedPath, // which module (source file) this came from
pub item: Item, // one top-level AST item: a fn/struct/enum/impl/...
}PackageSource::items: Vec<PackageItem> is a flat list of every top-level
item across every file in the package, each tagged with its originating
module path. Normalization, (optional) typechecking, and serialization all
operate over this flat list — grouped back by path where a pass needs
per-file context (e.g. the Kotlin serializer's serialize_package groups by
path.segments.join("/") to emit one .kt file per source module).
.fp source files do have pub mod foo; / use foo::bar; syntax (parsed by
FerroFrontend), and that's a genuine nested-namespace language feature —
but it's resolved during parsing/normalization into flat items with qualified
names, not preserved as a ModuleDescriptor tree. Don't conflate the two: a
.fp file with three pub mod blocks inside it is still exactly one
ModuleDescriptor (one source file) containing however many PackageItems
its parsed items flatten into.