Phase 6: P-Code-Bibliotheken und Linker abschließen, Cross-Buildplan festhalten

This commit is contained in:
2026-09-07 16:06:23 +02:00
parent 60d37ec79d
commit 25176947ba
33 changed files with 2416 additions and 203 deletions

View File

@@ -2,6 +2,7 @@
use crate::{
bytecode::{CompiledModule, Instr},
codegen,
library::{Library, LibraryModule, ModuleMetadata},
};
use std::collections::{HashMap, HashSet};
use tb_frontend::{
@@ -31,6 +32,7 @@ pub struct ProjectCompiler {
/// Vollständige Imports im Cache halten, damit spätere Debugkommandos sie binden können.
pub debug_symbols: bool,
debug_maps: Vec<DebugMap>,
debug_names: Vec<String>,
}
#[derive(Debug, Default, Clone, Copy)]
pub struct CompileStats {
@@ -91,14 +93,95 @@ impl ProjectCompiler {
catalog: &FormCatalog,
forms: &[FormFile],
) -> Result<CompiledModule, Vec<Diagnostic>> {
let library = self.compile_library(units, catalog, forms, &[])?;
self.link_library(name, library, forms.first().map(|f| f.root.name.as_str()))
}
pub fn link_library(
&mut self,
name: &str,
library: Library,
startup: Option<&str>,
) -> Result<CompiledModule, Vec<Diagnostic>> {
self.link_product(name, library, startup, false)
}
fn link_product(
&mut self,
name: &str,
library: Library,
startup: Option<&str>,
allow_open: bool,
) -> Result<CompiledModule, Vec<Diagnostic>> {
library.validate().map_err(|e| vec![diagnostic(e)])?;
let source_count = library
.modules
.iter()
.try_fold(0usize, |sum, m| sum.checked_add(m.code.sources.len()));
if source_count.is_none_or(|n| n > u32::MAX as usize) {
return Err(vec![diagnostic("Zu viele Quelldateien")]);
}
let mut sources = Vec::new();
let metadata: Vec<_> =
library
.modules
.iter()
.enumerate()
.map(|(id, m)| {
let offset = sources.len() as u32;
sources.extend(m.code.sources.iter().map(|s| {
tb_frontend::source::SourceFile {
module: id as u16,
path: s.path.clone(),
}
}));
let mut meta = m.metadata.clone();
meta.pos.source += offset;
for (_, p) in &mut meta.declarations {
p.source += offset;
}
for c in &mut meta.commons {
c.pos.source += offset;
}
meta
})
.collect();
let parts = library.modules.into_iter().map(|m| m.code).collect();
let (mut result, maps) = link(name, parts, &metadata, allow_open).map_err(|e| {
let mut errors = vec![e];
locate_diagnostics(&mut errors, &sources);
errors
})?;
self.debug_maps = maps;
self.debug_names = metadata.iter().map(|m| m.name.clone()).collect();
result.startup_form = startup.and_then(|name| {
FormCatalog {
objects: result.objects.clone(),
}
.find(name)
.map(|(id, _)| id)
});
result
.validate()
.map_err(|e| vec![diagnostic(e.to_string())])?;
Ok(result)
}
pub fn compile_library(
&mut self,
units: &[SourceUnit],
catalog: &FormCatalog,
forms: &[FormFile],
libraries: &[Library],
) -> Result<Library, Vec<Diagnostic>> {
self.stats = CompileStats::default();
if units.is_empty() || units.len() > u16::MAX as usize {
if (units.is_empty() && libraries.is_empty())
|| units.len() + libraries.iter().map(|l| l.modules.len()).sum::<usize>()
> u16::MAX as usize
{
return Err(vec![diagnostic("Projekt ohne Module oder zu viele Module")]);
}
let mut sources = Vec::new();
let mut diagnostics = Vec::new();
let mut names = HashSet::new();
let parsed: Vec<_> = units
let mut parsed: Vec<_> = units
.iter()
.enumerate()
.map(|(id, unit)| {
@@ -151,7 +234,24 @@ impl ProjectCompiler {
if !diagnostics.is_empty() {
return Err(diagnostics);
}
for library in libraries {
library.validate().map_err(|e| vec![diagnostic(e)])?;
for m in &library.modules {
if !names.insert(m.metadata.name.to_uppercase()) {
return Err(vec![diagnostic(format!(
"Duplicate definition: module {}",
m.metadata.name
))]);
}
parsed.push(m.metadata.declaration_view());
}
}
let mut catalog = catalog.clone();
for library in libraries {
for m in &library.modules {
merge_objects(&mut catalog.objects, &m.code.objects).map_err(|e| vec![e])?;
}
}
if catalog.find("SCREEN").is_none() {
catalog.add(
"SCREEN",
@@ -210,7 +310,7 @@ impl ProjectCompiler {
let mut parts = Vec::new();
let mut commons = Vec::new();
let mut products = Vec::new();
for (index, module) in parsed.iter().enumerate() {
for (index, module) in parsed.iter().take(units.len()).enumerate() {
let mut key = module.clone();
import_declarations(&mut key, &parsed, &exports, Some(&self.parsed[index].names));
let mut module = module.clone();
@@ -229,7 +329,48 @@ impl ProjectCompiler {
continue;
}
self.stats.compiled += 1;
let (hir, errors) = tb_frontend::sema::lower_with_forms(&module, &catalog);
let (hir, mut errors) = tb_frontend::sema::lower_with_forms(&module, &catalog);
for error in &mut errors {
if error.message != "Subprogram not defined" {
continue;
}
// Sema kennt absichtlich nur eindeutige Imports. Ergänze den Konflikt am tatsächlichen Aufruftoken.
let file = sources
.get(error.pos.source as usize)
.map(|s| s.path.as_str());
let called = units[index]
.segments
.iter()
.filter(|s| Some(s.file.as_str()) == file)
.find_map(|s| {
let line = error.pos.line.checked_sub(s.first_line)? as usize;
let text = s.text.lines().nth(line)?;
tb_frontend::lexer::lex(text)
.tokens
.into_iter()
.filter(|t| t.pos.column >= error.pos.column)
.find_map(|t| match t.kind {
tb_frontend::lexer::TokenKind::Ident { name, .. } => Some(name),
_ => None,
})
});
if let Some(called) = called {
let origins: Vec<_> = exports
.iter()
.enumerate()
.filter(|(_, declarations)| {
declarations
.iter()
.any(|s| matches!(s,Stmt::Declare{sig,..} if sig.name==called))
})
.map(|(id, _)| parsed[id].name.as_str())
.collect();
if origins.len() > 1 {
error.message =
format!("Ambiguous subprogram: {called} ({})", origins.join(", "));
}
}
}
diagnostics.extend(errors);
if let Some(hir) = hir {
let code = codegen::compile(&hir);
@@ -254,33 +395,81 @@ impl ProjectCompiler {
&& !products.iter().any(|n| n.module.name == p.module.name)
});
self.products.extend(products);
let (mut result, maps) = link(name, parts, &parsed, &commons).map_err(|error| {
let mut errors = vec![error];
locate_diagnostics(&mut errors, &sources);
errors
})?;
self.debug_maps = maps;
result.sources = sources;
let objects = FormCatalog {
objects: result.objects.clone(),
};
for form in forms {
result
.form_initial
.extend(form.initial_values(&objects).map_err(|e| {
vec![diagnostic(format!(
"{}: ungültige Forms-Anfangsdaten ({e})",
form.root.name
))]
})?);
let mut library = Library::default();
for (index, mut code) in parts.into_iter().enumerate() {
let mut metadata = ModuleMetadata::from_source(
&parsed[index],
exports[index].clone(),
commons[index].clone(),
);
// Die Quell-IDs jedes Produkts werden lokal: ein TBL bleibt unabhängig vom Verbraucher.
let local: Vec<_> = sources
.iter()
.enumerate()
.filter(|(_, s)| s.module as usize == index)
.collect();
let source_id = |id: u32| {
local
.iter()
.position(|(old, _)| *old == id as usize)
.unwrap_or(0) as u32
};
code.sources = local
.iter()
.map(|(_, s)| tb_frontend::source::SourceFile {
module: 0,
path: s.path.clone(),
})
.collect();
if code.sources.is_empty() {
code.sources.push(tb_frontend::source::SourceFile {
module: 0,
path: code.name.clone(),
});
}
for proc in &mut code.procs {
for instruction in &mut proc.code {
if let Instr::Source(id, _) = instruction {
*id = source_id(*id);
}
}
}
metadata.pos.source = source_id(metadata.pos.source);
for (_, p) in &mut metadata.declarations {
p.source = source_id(p.source);
}
for c in &mut metadata.commons {
c.pos.source = source_id(c.pos.source);
}
for stmt in &mut metadata.exports {
match stmt {
Stmt::Declare { pos, .. }
| Stmt::TypeDecl { pos, .. }
| Stmt::ConstDecl { pos, .. } => pos.source = source_id(pos.source),
_ => {}
}
}
let objects = FormCatalog {
objects: code.objects.clone(),
};
for form in forms
.iter()
.filter(|f| f.root.name.eq_ignore_ascii_case(&code.name))
{
code.form_initial.extend(
form.initial_values(&objects)
.map_err(|e| vec![diagnostic(e.to_string())])?,
);
}
library.modules.push(LibraryModule { metadata, code });
}
result.startup_form = forms
.first()
.and_then(|form| objects.find(&form.root.name).map(|(id, _)| id));
result
.validate()
.map_err(|e| vec![diagnostic(e.to_string())])?;
Ok(result)
for dependency in libraries {
library.modules.extend(dependency.modules.clone());
}
library.validate().map_err(|e| vec![diagnostic(e)])?;
// Auch offene Produkte müssen vorhandene Definitionen und COMMON-Verträge konsistent binden.
self.link_product("LIBRARY", library.clone(), None, true)?;
Ok(library)
}
}
@@ -512,23 +701,6 @@ fn import_declarations(
module.body = ordered;
}
fn proc_pos(module: &Module, name: &str) -> SourcePos {
module
.body
.iter()
.find_map(|stmt| match stmt {
Stmt::Declare { sig, pos } if sig.name == name => Some(*pos),
_ => None,
})
.or_else(|| {
module
.procs
.iter()
.find(|p| p.sig.name == name)
.map(|p| p.pos)
})
.unwrap_or_else(|| module_pos(module))
}
fn module_pos(module: &Module) -> SourcePos {
module
.body
@@ -550,11 +722,39 @@ fn remap_signature(ty: &mut HTy, ids: &[u16]) {
}
}
fn merge_objects(
target: &mut Vec<tb_frontend::forms::FormObject>,
objects: &[tb_frontend::forms::FormObject],
) -> Result<Vec<u16>, Diagnostic> {
let mut ids = Vec::new();
for object in objects {
let mut object = object.clone();
object.parent = object.parent.map(|id| ids[id as usize]);
let id = if let Some(id) = target
.iter()
.position(|o| o.name == object.name && o.parent == object.parent)
{
if target[id] != object {
return Err(diagnostic(format!(
"Incompatible Forms object: {}",
object.name
)));
}
id
} else {
target.push(object);
target.len() - 1
};
ids.push(u16::try_from(id).map_err(|_| diagnostic("Zu viele Forms-Objekte"))?);
}
Ok(ids)
}
fn link(
name: &str,
mut parts: Vec<CompiledModule>,
ast: &[Module],
module_commons: &[Vec<tb_frontend::hir::HCommon>],
ast: &[ModuleMetadata],
allow_open: bool,
) -> Result<(CompiledModule, Vec<DebugMap>), Diagnostic> {
let at = |pos, message| Diagnostic {
file: None,
@@ -566,7 +766,8 @@ fn link(
.iter()
.map(|p| (p.name.clone(), p.option_base))
.collect();
result.objects = parts[0].objects.clone();
result.objects.clear();
result.sources.clear();
result.strings.clear();
result.procs.clear();
result.option_base = parts[0].option_base;
@@ -574,11 +775,14 @@ fn link(
let mut count = 1usize;
let mut definitions: HashMap<String, Vec<u16>> = HashMap::new();
for (part, module) in parts.iter().zip(ast) {
let defined: HashSet<_> = module.procs.iter().map(|p| p.sig.name.as_str()).collect();
let defined: HashSet<_> = module.defined.iter().map(String::as_str).collect();
let mut map = vec![0];
for p in part.procs.iter().skip(1) {
if count >= u16::MAX as usize {
return Err(at(module.proc_pos(&p.name), "Zu viele Prozeduren".into()));
}
let id = u16::try_from(count)
.map_err(|_| at(proc_pos(module, &p.name), "Zu viele Prozeduren".into()))?;
.map_err(|_| at(module.proc_pos(&p.name), "Zu viele Prozeduren".into()))?;
count += 1;
map.push(id);
if defined.contains(p.name.as_str()) || p.kind == HProcKind::DefFn {
@@ -587,23 +791,44 @@ fn link(
}
proc_maps.push(map);
}
let mut unused_imports = HashSet::new();
// DECLARE-Platzhalter auf die tatsächliche, eindeutig bestimmte Definition binden.
for (module_id, part) in parts.iter().enumerate() {
let defined: HashSet<_> = ast[module_id]
.procs
.iter()
.map(|p| p.sig.name.as_str())
.collect();
let defined: HashSet<_> = ast[module_id].defined.iter().map(String::as_str).collect();
for (id, proc) in part.procs.iter().enumerate().skip(1) {
if !defined.contains(proc.name.as_str()) && proc.kind != HProcKind::DefFn {
if let Some(candidates) = definitions.get(&proc.name) {
if candidates.len() != 1 {
return Err(at(
proc_pos(&ast[module_id], &proc.name),
format!("Ambiguous subprogram: {}", proc.name),
ast[module_id].proc_pos(&proc.name),
format!(
"Ambiguous subprogram: {} ({})",
proc.name,
ast.iter()
.filter(|m| m.defined.contains(&proc.name))
.map(|m| m.name.as_str())
.collect::<Vec<_>>()
.join(", ")
),
));
}
proc_maps[module_id][id] = candidates[0];
} else if !allow_open && !crate::interp::is_runtime_external(&proc.name) {
let referenced = part
.procs
.iter()
.flat_map(|p| &p.code)
.any(|i| matches!(i,Instr::Call(target,_) if *target as usize==id))
|| part.event_procs.iter().any(|e| e.proc as usize == id);
if referenced {
return Err(at(
ast[module_id].proc_pos(&proc.name),
format!("Subprogram not defined: {} ({})", proc.name, part.name),
));
}
// Ein bloßer Prototyp ohne Relokation ist kein offener Import des Endprodukts.
unused_imports.insert(proc_maps[module_id][id]);
proc_maps[module_id][id] = u16::MAX;
}
}
}
@@ -629,6 +854,9 @@ fn link(
body_starts.push(pc);
pc += part.procs[0].code.len() - split - 1;
}
if pc >= u32::MAX as usize {
return Err(diagnostic("Zu viele Hauptprogramminstruktionen"));
}
let mut main = parts[0].procs[0].clone();
main.code.clear();
main.name = name.into();
@@ -637,6 +865,35 @@ fn link(
let mut debug_maps = Vec::new();
let mut common: HashMap<_, (u16, tb_frontend::hir::HCommon)> = HashMap::new();
for (module_id, part) in parts.iter_mut().enumerate() {
let objects = merge_objects(&mut result.objects, &part.objects)?;
let source_offset =
u32::try_from(result.sources.len()).map_err(|_| diagnostic("Zu viele Quelldateien"))?;
if result
.sources
.len()
.checked_add(part.sources.len())
.is_none_or(|n| n > u32::MAX as usize)
{
return Err(diagnostic("Zu viele Quelldateien"));
}
result.sources.extend(
part.sources
.iter()
.map(|s| tb_frontend::source::SourceFile {
module: module_id as u16,
path: s.path.clone(),
}),
);
for mut initial in part.form_initial.drain(..) {
initial.object = objects[initial.object as usize];
for value in initial.properties.values_mut() {
if let tb_ui::forms::PropertyValue::Object(Some((id, _))) = value {
*id = objects[*id as usize];
}
}
result.form_initial.push(initial);
}
let mut types = Vec::new();
for udt in &part.udts {
let mut udt = udt.clone();
@@ -652,15 +909,14 @@ fn link(
result.udts.push(udt);
result.udts.len() - 1
});
if result.udts.len() > u16::MAX as usize {
return Err(at(ast[module_id].pos, "Zu viele TYPEs".into()));
}
types.push(
u16::try_from(id)
.map_err(|_| at(module_pos(&ast[module_id]), "Zu viele TYPEs".into()))?,
u16::try_from(id).map_err(|_| at(ast[module_id].pos, "Zu viele TYPEs".into()))?,
);
}
let commons: HashMap<_, _> = module_commons[module_id]
.iter()
.map(|c| (c.slot, c))
.collect();
let commons: HashMap<_, _> = ast[module_id].commons.iter().map(|c| (c.slot, c)).collect();
let mut globals = Vec::new();
for (slot, (ty, name)) in part.globals_init.iter().zip(&part.global_names).enumerate() {
let mut ty = ty.clone();
@@ -704,12 +960,8 @@ fn link(
}
*id
} else {
let id = u16::try_from(result.globals_init.len()).map_err(|_| {
at(
module_pos(&ast[module_id]),
"Zu viele globale Variablen".into(),
)
})?;
let id = u16::try_from(result.globals_init.len())
.map_err(|_| at(ast[module_id].pos, "Zu viele globale Variablen".into()))?;
result.globals_init.push(ty);
result.global_names.push(if ast.len() == 1 {
name.clone()
@@ -732,20 +984,23 @@ fn link(
});
let string_offset = result.strings.len();
if string_offset + part.strings.len() >= u16::MAX as usize {
return Err(at(
module_pos(&ast[module_id]),
"Zu viele Stringkonstanten".into(),
));
return Err(at(ast[module_id].pos, "Zu viele Stringkonstanten".into()));
}
result.strings.append(&mut part.strings);
let data_offset = result.data.len() as u32;
let data_offset =
u32::try_from(result.data.len()).map_err(|_| diagnostic("Zu viele DATA-Werte"))?;
if result
.data
.len()
.checked_add(part.data.len())
.is_none_or(|n| n > u32::MAX as usize)
{
return Err(diagnostic("Zu viele DATA-Werte"));
}
result.data.append(&mut part.data);
let jump_offset = result.jump_tables.len();
if jump_offset + part.jump_tables.len() > u16::MAX as usize {
return Err(at(
module_pos(&ast[module_id]),
"Zu viele Sprungtabellen".into(),
));
return Err(at(ast[module_id].pos, "Zu viele Sprungtabellen".into()));
}
let main_pc = |pc: u32| if (pc as usize) < splits[module_id] { init_starts[module_id] + pc as usize } else { body_starts[module_id] + pc as usize - splits[module_id] } as u32;
for (proc_id, proc) in part.procs.iter_mut().enumerate() {
@@ -765,6 +1020,15 @@ fn link(
for instruction in &mut proc.code {
use Instr::*;
match instruction {
Source(id, _) => *id += source_offset,
LoadObjectProperty(id, _, _)
| StoreObjectProperty(id, _, _)
| PushObject(id, _)
| ObjectMethod(id, _, _)
| ObjectLoad(id, _, _)
| LoadObjectIndexedProperty(id, _)
| ObjectMethodFn(id, _, _)
| StoreObjectIndexedProperty(id, _) => *id = objects[*id as usize],
PushStr(id)
| Unsupported(id)
| LoadDynamicObjectProperty(id)
@@ -818,6 +1082,7 @@ fn link(
result.procs.extend(part.procs.iter().skip(1).cloned());
for mut e in part.event_procs.drain(..) {
e.proc = proc_maps[module_id][e.proc as usize];
e.object = objects[e.object as usize];
result.event_procs.push(e);
}
result.jump_tables.append(&mut part.jump_tables);
@@ -829,6 +1094,9 @@ fn link(
// Vollständige Signaturen zwischen DECLARE-Platzhalter und Ziel vergleichen.
for (module, part) in parts.iter().enumerate() {
for (id, proc) in part.procs.iter().enumerate().skip(1) {
if proc_maps[module][id] == u16::MAX {
continue;
}
let target = &result.procs[proc_maps[module][id] as usize];
if proc.ret_ty != target.ret_ty
|| proc.kind != target.kind
@@ -840,12 +1108,47 @@ fn link(
.any(|(a, b)| a.ty != b.ty || a.array != b.array || a.by_ref != b.by_ref)
{
return Err(at(
proc_pos(&ast[module], proc.name.rsplit('!').next().unwrap()),
ast[module].proc_pos(proc.name.rsplit('!').next().unwrap()),
format!("Parameter type mismatch: {}", proc.name),
));
}
}
}
if !unused_imports.is_empty() {
let mut compact = vec![u16::MAX; result.procs.len()];
let mut next = 0u16;
for (old, id) in compact.iter_mut().enumerate() {
if !unused_imports.contains(&(old as u16)) {
*id = next;
next += 1;
}
}
result.procs = std::mem::take(&mut result.procs)
.into_iter()
.enumerate()
.filter_map(|(id, mut proc)| {
if compact[id] == u16::MAX {
return None;
}
for instruction in &mut proc.code {
if let Instr::Call(id, _) = instruction {
*id = compact[*id as usize];
}
}
Some(proc)
})
.collect();
for e in &mut result.event_procs {
e.proc = compact[e.proc as usize];
}
for map in &mut debug_maps {
for id in &mut map.procs {
if *id != u16::MAX {
*id = compact[*id as usize];
}
}
}
}
Ok((result, debug_maps))
}
@@ -857,7 +1160,7 @@ type DebugCompileFn = fn(&DebugCompiler, u16, &str, &str, bool) -> Result<DebugC
#[derive(Clone)]
pub struct DebugCompiler {
compile_fn: DebugCompileFn,
modules: Vec<(Module, FormCatalog, DebugMap)>,
modules: Vec<(u16, Module, FormCatalog, DebugMap)>,
symbols: tb_frontend::sema::DebugSymbols,
slots: Vec<u16>,
error: Option<String>,
@@ -877,20 +1180,28 @@ pub struct DebugCode {
impl ProjectCompiler {
pub fn debug_compiler(&self) -> DebugCompiler {
let modules: Vec<_> = self
.parsed
.debug_names
.iter()
.zip(&self.debug_maps)
.filter_map(|(parsed, map)| {
.enumerate()
.filter_map(|(id, (name, map))| {
self.products
.iter()
.find(|p| p.module.name == parsed.module.name)
.map(|p| (p.debug_module.clone(), p.catalog.clone(), map.clone()))
.find(|p| p.module.name == *name)
.map(|p| {
(
id as u16,
p.debug_module.clone(),
p.catalog.clone(),
map.clone(),
)
})
})
.collect();
let mut symbols = tb_frontend::sema::DebugSymbols::default();
let mut slots = Vec::new();
let mut error = None;
for (ast, catalog, map) in &modules {
for (_, ast, catalog, map) in &modules {
if let Some(hir) = tb_frontend::sema::lower_with_forms(ast, catalog).0 {
if symbols.udts.len() + hir.udts.len() >= u16::MAX as usize
|| symbols.globals.len() + hir.globals.len() >= u16::MAX as usize
@@ -947,9 +1258,10 @@ impl DebugCompiler {
if let Some(error) = &self.error {
return Err(error.clone());
}
let (ast, catalog, map) = self
let (_, ast, catalog, map) = self
.modules
.get(module as usize)
.iter()
.find(|(id, _, _, _)| *id == module)
.ok_or("Kein Debug-Quellkontext für dieses Kompilat")?;
let name = if procedure == "<main>" {
&ast.name
@@ -976,7 +1288,7 @@ impl DebugCompiler {
let mut globals = map.globals.clone();
globals.extend(&self.slots);
let mut types = map.types.clone();
for (_, _, mapping) in &self.modules {
for (_, _, _, mapping) in &self.modules {
types.extend(&mapping.types);
}
let mut compiled = codegen::compile(&hir);
@@ -1002,7 +1314,14 @@ impl DebugCompiler {
}
remap_type(ty, &types);
}
Call(id, _) => *id = map.procs[*id as usize],
Call(id, _) => {
*id = map.procs[*id as usize];
if *id == u16::MAX {
return Err(
"Subprogram not defined: ungebundene DECLARE-Deklaration".into()
);
}
}
PushUdtId(id) => *id = types[*id as usize],
_ => {}
}
@@ -1023,7 +1342,7 @@ impl DebugCompiler {
from: SourcePos,
to: SourcePos,
) -> bool {
let Some((ast, _, _)) = self.modules.get(module as usize) else {
let Some((_, ast, _, _)) = self.modules.iter().find(|(id, _, _, _)| *id == module) else {
return false;
};
let body = if procedure == "<main>" {