This commit is contained in:
Nilton Constantino 2026-02-02 21:37:40 +00:00
parent 3af2aa1328
commit 6cf1e71b99
No known key found for this signature in database
14 changed files with 135 additions and 67 deletions

View File

@ -237,14 +237,19 @@ impl Linker {
if let Some(local_func_idx) = meta.func_idx { if let Some(local_func_idx) = meta.func_idx {
let global_func_idx = module_function_offsets.last().unwrap() + local_func_idx; let global_func_idx = module_function_offsets.last().unwrap() + local_func_idx;
final_exports.insert(format!("{}:{}", key.module_path, key.symbol_name), global_func_idx); final_exports.insert(format!("{}:{}", key.module_path, key.symbol_name), global_func_idx);
// Also provide short name for root module exports to facilitate entrypoint resolution
if !final_exports.contains_key(&key.symbol_name) {
final_exports.insert(key.symbol_name.clone(), global_func_idx);
}
} }
} }
} }
// v0: Fallback export for entrypoint `src/main/modules:frame` (root module) // v0: Fallback export for entrypoint `frame` (root module)
if !final_exports.iter().any(|(name, _)| name.ends_with(":frame")) { if !final_exports.iter().any(|(name, _)| name.ends_with(":frame") || name == "frame") {
if let Some(&root_offset) = module_function_offsets.last() { if let Some(&root_offset) = module_function_offsets.last() {
if let Some((idx, _)) = combined_function_names.iter().find(|(i, name)| *i >= root_offset && name == "frame") { if let Some((idx, _)) = combined_function_names.iter().find(|(i, name)| *i >= root_offset && name == "frame") {
final_exports.insert("frame".to_string(), *idx);
final_exports.insert("src/main/modules:frame".to_string(), *idx); final_exports.insert("src/main/modules:frame".to_string(), *idx);
} }
} }
@ -274,9 +279,9 @@ mod tests {
use std::collections::BTreeMap; use std::collections::BTreeMap;
use super::*; use super::*;
use crate::building::output::{ExportKey, ExportMetadata, ImportKey, ImportMetadata}; use crate::building::output::{ExportKey, ExportMetadata, ImportKey, ImportMetadata};
use crate::semantics::export_surface::ExportSurfaceKind;
use crate::building::plan::BuildTarget; use crate::building::plan::BuildTarget;
use crate::deps::resolver::ProjectId; use crate::deps::resolver::ProjectId;
use crate::frontends::pbs::symbols::SymbolKind;
use prometeu_bytecode::opcode::OpCode; use prometeu_bytecode::opcode::OpCode;
use prometeu_bytecode::FunctionMeta; use prometeu_bytecode::FunctionMeta;
@ -294,7 +299,7 @@ mod tests {
lib_exports.insert(ExportKey { lib_exports.insert(ExportKey {
module_path: "math".into(), module_path: "math".into(),
symbol_name: "add".into(), symbol_name: "add".into(),
kind: SymbolKind::Function, kind: ExportSurfaceKind::Service,
}, ExportMetadata { func_idx: Some(0), is_host: false, ty: None }); }, ExportMetadata { func_idx: Some(0), is_host: false, ty: None });
let lib_module = CompiledModule { let lib_module = CompiledModule {

View File

@ -12,6 +12,7 @@ use crate::frontends::pbs::resolver::{ModuleProvider, Resolver};
use crate::frontends::pbs::symbols::{ModuleSymbols, Namespace, Symbol, SymbolKind, Visibility}; use crate::frontends::pbs::symbols::{ModuleSymbols, Namespace, Symbol, SymbolKind, Visibility};
use crate::frontends::pbs::typecheck::TypeChecker; use crate::frontends::pbs::typecheck::TypeChecker;
use crate::frontends::pbs::types::PbsType; use crate::frontends::pbs::types::PbsType;
use crate::semantics::export_surface::ExportSurfaceKind;
use crate::lowering::core_to_vm; use crate::lowering::core_to_vm;
use prometeu_bytecode::{ConstantPoolEntry, DebugInfo, FunctionMeta}; use prometeu_bytecode::{ConstantPoolEntry, DebugInfo, FunctionMeta};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -22,7 +23,7 @@ use std::path::Path;
pub struct ExportKey { pub struct ExportKey {
pub module_path: String, pub module_path: String,
pub symbol_name: String, pub symbol_name: String,
pub kind: SymbolKind, pub kind: ExportSurfaceKind,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@ -165,12 +166,17 @@ pub fn compile_project(
let sym = Symbol { let sym = Symbol {
name: key.symbol_name.clone(), name: key.symbol_name.clone(),
kind: key.kind.clone(), kind: match key.kind {
namespace: match key.kind { ExportSurfaceKind::Service => SymbolKind::Service,
SymbolKind::Function | SymbolKind::Service => Namespace::Value, ExportSurfaceKind::DeclareType => {
SymbolKind::Struct | SymbolKind::Contract | SymbolKind::ErrorType => Namespace::Type, match &meta.ty {
_ => Namespace::Value, Some(PbsType::Contract(_)) => SymbolKind::Contract,
Some(PbsType::ErrorType(_)) => SymbolKind::ErrorType,
_ => SymbolKind::Struct,
}
}
}, },
namespace: key.kind.namespace(),
visibility: Visibility::Pub, visibility: Visibility::Pub,
ty: meta.ty.clone(), ty: meta.ty.clone(),
is_host: meta.is_host, is_host: meta.is_host,
@ -245,31 +251,35 @@ pub fn compile_project(
for (module_path, ms) in &module_symbols_map { for (module_path, ms) in &module_symbols_map {
for sym in ms.type_symbols.symbols.values() { for sym in ms.type_symbols.symbols.values() {
if sym.visibility == Visibility::Pub { if sym.visibility == Visibility::Pub {
exports.insert(ExportKey { if let Some(surface_kind) = ExportSurfaceKind::from_symbol_kind(sym.kind) {
module_path: module_path.clone(), exports.insert(ExportKey {
symbol_name: sym.name.clone(), module_path: module_path.clone(),
kind: sym.kind.clone(), symbol_name: sym.name.clone(),
}, ExportMetadata { kind: surface_kind,
func_idx: None, }, ExportMetadata {
is_host: sym.is_host, func_idx: None,
ty: sym.ty.clone(), is_host: sym.is_host,
}); ty: sym.ty.clone(),
});
}
} }
} }
for sym in ms.value_symbols.symbols.values() { for sym in ms.value_symbols.symbols.values() {
if sym.visibility == Visibility::Pub { if sym.visibility == Visibility::Pub {
// Find func_idx if it's a function or service if let Some(surface_kind) = ExportSurfaceKind::from_symbol_kind(sym.kind) {
let func_idx = vm_module.functions.iter().position(|f| f.name == sym.name).map(|i| i as u32); // Find func_idx if it's a function or service
let func_idx = vm_module.functions.iter().position(|f| f.name == sym.name).map(|i| i as u32);
exports.insert(ExportKey { exports.insert(ExportKey {
module_path: module_path.clone(), module_path: module_path.clone(),
symbol_name: sym.name.clone(), symbol_name: sym.name.clone(),
kind: sym.kind.clone(), kind: surface_kind,
}, ExportMetadata { }, ExportMetadata {
func_idx, func_idx,
is_host: sym.is_host, is_host: sym.is_host,
ty: sym.ty.clone(), ty: sym.ty.clone(),
}); });
}
} }
} }
} }
@ -359,7 +369,7 @@ mod tests {
let vec2_key = ExportKey { let vec2_key = ExportKey {
module_path: "src/main/modules".to_string(), module_path: "src/main/modules".to_string(),
symbol_name: "Vec2".to_string(), symbol_name: "Vec2".to_string(),
kind: SymbolKind::Struct, kind: ExportSurfaceKind::DeclareType,
}; };
assert!(compiled.exports.contains_key(&vec2_key)); assert!(compiled.exports.contains_key(&vec2_key));

View File

@ -62,7 +62,7 @@ pub struct ImportSpecNode {
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ServiceDeclNode { pub struct ServiceDeclNode {
pub span: Span, pub span: Span,
pub vis: String, // "pub" | "mod" pub vis: Option<String>, // "pub" | "mod"
pub name: String, pub name: String,
pub extends: Option<String>, pub extends: Option<String>,
pub members: Vec<Node>, // ServiceFnSig pub members: Vec<Node>, // ServiceFnSig
@ -86,7 +86,7 @@ pub struct ParamNode {
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct FnDeclNode { pub struct FnDeclNode {
pub span: Span, pub span: Span,
pub vis: String, pub vis: Option<String>,
pub name: String, pub name: String,
pub params: Vec<ParamNode>, pub params: Vec<ParamNode>,
pub ret: Option<Box<Node>>, pub ret: Option<Box<Node>>,

View File

@ -1,6 +1,7 @@
use crate::common::diagnostics::{Diagnostic, DiagnosticBundle, DiagnosticLevel}; use crate::common::diagnostics::{Diagnostic, DiagnosticBundle, DiagnosticLevel};
use crate::frontends::pbs::ast::*; use crate::frontends::pbs::ast::*;
use crate::frontends::pbs::symbols::*; use crate::frontends::pbs::symbols::*;
use crate::semantics::export_surface::ExportSurfaceKind;
pub struct SymbolCollector { pub struct SymbolCollector {
type_symbols: SymbolTable, type_symbols: SymbolTable,
@ -40,11 +41,14 @@ impl SymbolCollector {
} }
fn collect_fn(&mut self, decl: &FnDeclNode) { fn collect_fn(&mut self, decl: &FnDeclNode) {
let vis = match decl.vis.as_str() { let vis = match decl.vis.as_deref() {
"pub" => Visibility::Pub, Some("pub") => Visibility::Pub,
"mod" => Visibility::Mod, Some("mod") => Visibility::Mod,
_ => Visibility::FilePrivate, _ => Visibility::FilePrivate,
}; };
self.check_export_eligibility(SymbolKind::Function, vis, decl.span);
let symbol = Symbol { let symbol = Symbol {
name: decl.name.clone(), name: decl.name.clone(),
kind: SymbolKind::Function, kind: SymbolKind::Function,
@ -59,11 +63,13 @@ impl SymbolCollector {
} }
fn collect_service(&mut self, decl: &ServiceDeclNode) { fn collect_service(&mut self, decl: &ServiceDeclNode) {
let vis = match decl.vis.as_str() { let vis = match decl.vis.as_deref() {
"pub" => Visibility::Pub, Some("pub") => Visibility::Pub,
"mod" => Visibility::Mod, _ => Visibility::Mod, // Defaults to Mod
_ => Visibility::FilePrivate, // Should not happen with valid parser
}; };
self.check_export_eligibility(SymbolKind::Service, vis, decl.span);
let symbol = Symbol { let symbol = Symbol {
name: decl.name.clone(), name: decl.name.clone(),
kind: SymbolKind::Service, kind: SymbolKind::Service,
@ -89,6 +95,9 @@ impl SymbolCollector {
"error" => SymbolKind::ErrorType, "error" => SymbolKind::ErrorType,
_ => SymbolKind::Struct, // Default _ => SymbolKind::Struct, // Default
}; };
self.check_export_eligibility(kind.clone(), vis, decl.span);
let symbol = Symbol { let symbol = Symbol {
name: decl.name.clone(), name: decl.name.clone(),
kind, kind,
@ -148,4 +157,15 @@ impl SymbolCollector {
span: Some(symbol.span), span: Some(symbol.span),
}); });
} }
fn check_export_eligibility(&mut self, kind: SymbolKind, vis: Visibility, span: crate::common::spans::Span) {
if let Err(msg) = ExportSurfaceKind::validate_visibility(kind, vis) {
self.diagnostics.push(Diagnostic {
level: DiagnosticLevel::Error,
code: Some("E_SEMANTIC_EXPORT_RESTRICTION".to_string()),
message: msg,
span: Some(span),
});
}
}
} }

View File

@ -154,7 +154,7 @@ impl Parser {
fn parse_top_level_decl(&mut self) -> Result<Node, DiagnosticBundle> { fn parse_top_level_decl(&mut self) -> Result<Node, DiagnosticBundle> {
match self.peek().kind { match self.peek().kind {
TokenKind::Fn => self.parse_fn_decl("file".to_string()), TokenKind::Fn => self.parse_fn_decl(None),
TokenKind::Pub | TokenKind::Mod | TokenKind::Declare | TokenKind::Service => self.parse_decl(), TokenKind::Pub | TokenKind::Mod | TokenKind::Declare | TokenKind::Service => self.parse_decl(),
TokenKind::Invalid(ref msg) => { TokenKind::Invalid(ref msg) => {
let code = if msg.contains("Unterminated string") { let code = if msg.contains("Unterminated string") {
@ -181,23 +181,14 @@ impl Parser {
}; };
match self.peek().kind { match self.peek().kind {
TokenKind::Service => self.parse_service_decl(vis.unwrap_or_else(|| "pub".to_string())), TokenKind::Service => self.parse_service_decl(vis),
TokenKind::Declare => self.parse_type_decl(vis), TokenKind::Declare => self.parse_type_decl(vis),
TokenKind::Fn => { TokenKind::Fn => self.parse_fn_decl(vis),
let vis_str = vis.unwrap_or_else(|| "file".to_string());
if vis_str == "pub" {
return Err(self.error_with_code(
"Functions cannot be public. They are always mod or file-private.",
Some("E_RESOLVE_VISIBILITY"),
));
}
self.parse_fn_decl(vis_str)
}
_ => Err(self.error("Expected 'service', 'declare', or 'fn'")), _ => Err(self.error("Expected 'service', 'declare', or 'fn'")),
} }
} }
fn parse_service_decl(&mut self, vis: String) -> Result<Node, DiagnosticBundle> { fn parse_service_decl(&mut self, vis: Option<String>) -> Result<Node, DiagnosticBundle> {
let start_span = self.consume(TokenKind::Service)?.span; let start_span = self.consume(TokenKind::Service)?.span;
let name = self.expect_identifier()?; let name = self.expect_identifier()?;
let mut extends = None; let mut extends = None;
@ -362,7 +353,7 @@ impl Parser {
})) }))
} }
fn parse_fn_decl(&mut self, vis: String) -> Result<Node, DiagnosticBundle> { fn parse_fn_decl(&mut self, vis: Option<String>) -> Result<Node, DiagnosticBundle> {
let start_span = self.consume(TokenKind::Fn)?.span; let start_span = self.consume(TokenKind::Fn)?.span;
let name = self.expect_identifier()?; let name = self.expect_identifier()?;
let params = self.parse_param_list()?; let params = self.parse_param_list()?;
@ -1181,7 +1172,7 @@ fn good() {}
let mut parser = Parser::new(source, 0); let mut parser = Parser::new(source, 0);
let result = parser.parse_file().expect("mod fn should be allowed"); let result = parser.parse_file().expect("mod fn should be allowed");
if let Node::FnDecl(fn_decl) = &result.decls[0] { if let Node::FnDecl(fn_decl) = &result.decls[0] {
assert_eq!(fn_decl.vis, "mod"); assert_eq!(fn_decl.vis, Some("mod".to_string()));
} else { } else {
panic!("Expected FnDecl"); panic!("Expected FnDecl");
} }
@ -1191,10 +1182,12 @@ fn good() {}
fn test_parse_pub_fn() { fn test_parse_pub_fn() {
let source = "pub fn test() {}"; let source = "pub fn test() {}";
let mut parser = Parser::new(source, 0); let mut parser = Parser::new(source, 0);
let result = parser.parse_file(); let result = parser.parse_file().expect("pub fn should be allowed in parser");
assert!(result.is_err(), "pub fn should be disallowed"); if let Node::FnDecl(fn_decl) = &result.decls[0] {
let err = result.unwrap_err(); assert_eq!(fn_decl.vis, Some("pub".to_string()));
assert!(err.diagnostics[0].message.contains("Functions cannot be public")); } else {
panic!("Expected FnDecl");
}
} }
#[test] #[test]

View File

@ -3,14 +3,14 @@ use crate::frontends::pbs::types::PbsType;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Visibility { pub enum Visibility {
FilePrivate, FilePrivate,
Mod, Mod,
Pub, Pub,
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord)]
pub enum SymbolKind { pub enum SymbolKind {
Function, Function,
Service, Service,

View File

@ -48,6 +48,7 @@ pub mod manifest;
pub mod deps; pub mod deps;
pub mod sources; pub mod sources;
pub mod building; pub mod building;
pub mod semantics;
use anyhow::Result; use anyhow::Result;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};

View File

@ -0,0 +1,40 @@
use crate::frontends::pbs::symbols::{SymbolKind, Visibility};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum ExportSurfaceKind {
Service,
DeclareType, // struct, storage struct, type alias
}
impl ExportSurfaceKind {
pub fn from_symbol_kind(kind: SymbolKind) -> Option<Self> {
match kind {
SymbolKind::Service => Some(ExportSurfaceKind::Service),
SymbolKind::Struct | SymbolKind::Contract | SymbolKind::ErrorType => {
Some(ExportSurfaceKind::DeclareType)
}
SymbolKind::Function | SymbolKind::Local => None,
}
}
pub fn validate_visibility(kind: SymbolKind, vis: Visibility) -> Result<(), String> {
if vis == Visibility::Pub {
if Self::from_symbol_kind(kind).is_none() {
let kind_str = match kind {
SymbolKind::Function => "Functions",
_ => "This declaration",
};
return Err(format!("{} are not exportable in this version.", kind_str));
}
}
Ok(())
}
pub fn namespace(&self) -> crate::frontends::pbs::symbols::Namespace {
match self {
ExportSurfaceKind::Service => crate::frontends::pbs::symbols::Namespace::Type,
ExportSurfaceKind::DeclareType => crate::frontends::pbs::symbols::Namespace::Type,
}
}
}

View File

@ -0,0 +1 @@
pub mod export_surface;

View File

@ -67,7 +67,7 @@ fn test_integration_test01_link() {
let mut vm = VirtualMachine::default(); let mut vm = VirtualMachine::default();
// Use initialize to load the ROM; entrypoint must be numeric or empty (defaults to 0) // Use initialize to load the ROM; entrypoint must be numeric or empty (defaults to 0)
vm.initialize(unit.rom, "").expect("Failed to initialize VM"); vm.initialize(unit.rom, "frame").expect("Failed to initialize VM");
let mut native = SimpleNative; let mut native = SimpleNative;
let mut hw = SimpleHardware::new(); let mut hw = SimpleHardware::new();

View File

@ -6,7 +6,6 @@ use prometeu_core::virtual_machine::{LogicalFrameEndingReason, VirtualMachine};
use prometeu_core::Hardware; use prometeu_core::Hardware;
use std::fs; use std::fs;
use std::path::Path; use std::path::Path;
use prometeu_bytecode::BytecodeLoader;
struct MockNative; struct MockNative;
impl NativeInterface for MockNative { impl NativeInterface for MockNative {
@ -40,7 +39,6 @@ fn test_canonical_cartridge_heartbeat() {
let pbc_bytes = fs::read(pbc_path).expect("Failed to read canonical PBC. Did you run the generation test?"); let pbc_bytes = fs::read(pbc_path).expect("Failed to read canonical PBC. Did you run the generation test?");
// Determine entrypoint from the compiled module exports // Determine entrypoint from the compiled module exports
let module = BytecodeLoader::load(&pbc_bytes).expect("Failed to parse PBC");
let entry_symbol = "src/main/modules:frame"; let entry_symbol = "src/main/modules:frame";
let mut vm = VirtualMachine::new(vec![], vec![]); let mut vm = VirtualMachine::new(vec![], vec![]);

View File

@ -641,7 +641,7 @@
"start": 739, "start": 739,
"end": 788 "end": 788
}, },
"vis": "file", "vis": null,
"name": "add", "name": "add",
"params": [ "params": [
{ {
@ -743,7 +743,7 @@
"start": 790, "start": 790,
"end": 1180 "end": 1180
}, },
"vis": "file", "vis": null,
"name": "frame", "name": "frame",
"params": [], "params": [],
"ret": { "ret": {