//! Gemeinsame Tabellenauflösung getrennter Modulübersetzungen. use crate::{ bytecode::{CompiledModule, Instr}, codegen, library::{Library, LibraryModule, ModuleMetadata}, }; use std::collections::{HashMap, HashSet}; use tb_frontend::{ ast::{Module, Stmt, TypeName}, forms::FormCatalog, hir::{HProcKind, HTy}, source::{locate_diagnostics, SourceUnit}, Diagnostic, SourcePos, }; use tb_runtime::value::TypeInit; use tb_ui::frm::FormFile; fn diagnostic(message: impl Into) -> Diagnostic { Diagnostic { file: None, pos: SourcePos::default(), message: message.into(), } } /// Prozesslokaler Cache: unverknüpfte Produkte, niemals ein bereits gelinktes Programm. #[derive(Default)] pub struct ProjectCompiler { parsed: Vec, products: Vec, pub stats: CompileStats, /// Vollständige Imports im Cache halten, damit spätere Debugkommandos sie binden können. pub debug_symbols: bool, debug_maps: Vec, debug_names: Vec, } #[derive(Debug, Default, Clone, Copy)] pub struct CompileStats { pub parsed: usize, pub compiled: usize, pub reused: usize, } struct Parsed { unit: SourceUnit, prefix: Vec, sources: Vec, module: Module, diagnostics: Vec, names: HashSet, } struct Product { module: Module, debug_module: Module, catalog: FormCatalog, code: CompiledModule, commons: Vec, } pub fn compile_project( name: &str, units: &[SourceUnit], catalog: &FormCatalog, forms: &[FormFile], ) -> Result> { ProjectCompiler::default().compile(name, units, catalog, forms) } impl ProjectCompiler { /// Bindungen aus den tatsächlich übersetzten Modulen einschließlich ihrer Imports. pub fn bound_form_references( &self, ) -> Result> { let mut result = tb_frontend::sema::BoundFormReferences::default(); for product in &self.products { let bound = tb_frontend::sema::bound_form_references(&product.module, &product.catalog); result.objects.extend(bound.objects); // Importierte Prototypen im Cache haben keine physische Quellposition. result .procedures .extend(bound.procedures.into_iter().filter(|(pos, _)| pos.line > 0)); result.diagnostics.extend(bound.diagnostics); } if result.diagnostics.is_empty() { Ok(result) } else { Err(result.diagnostics) } } pub fn compile( &mut self, name: &str, units: &[SourceUnit], catalog: &FormCatalog, forms: &[FormFile], ) -> Result> { 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> { self.link_product(name, library, startup, false) } fn link_product( &mut self, name: &str, library: Library, startup: Option<&str>, allow_open: bool, ) -> Result> { 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> { self.stats = CompileStats::default(); if (units.is_empty() && libraries.is_empty()) || units.len() + libraries.iter().map(|l| l.modules.len()).sum::() > 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 mut parsed: Vec<_> = units .iter() .enumerate() .map(|(id, unit)| { let cached = self .parsed .get(id) .filter(|p| p.unit == *unit && p.prefix == sources); let (module, errors) = if let Some(p) = cached { sources = p.sources.clone(); (p.module.clone(), p.diagnostics.clone()) } else { let prefix = sources.clone(); let (module, errors) = unit.parse(id as u16, &mut sources); let entry = Parsed { unit: unit.clone(), prefix, sources: sources.clone(), module: module.clone(), diagnostics: errors.clone(), names: unit .segments .iter() .flat_map(|s| tb_frontend::lexer::lex(&s.text).tokens) .filter_map(|t| match t.kind { tb_frontend::lexer::TokenKind::Ident { name, .. } => Some(name), _ => None, }) .collect(), }; if id < self.parsed.len() { self.parsed[id] = entry; } else { self.parsed.push(entry); } self.stats.parsed += 1; (module, errors) }; if !names.insert(unit.name.to_uppercase()) { let mut error = diagnostic(format!("Duplicate definition: module {}", unit.name)); error.pos = module_pos(&module); error.file = unit.segments.first().map(|s| s.file.clone()); diagnostics.push(error); } diagnostics.extend(errors); module }) .collect(); locate_diagnostics(&mut diagnostics, &sources); 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", tb_frontend::forms::ObjectClass::Screen, None, false, ); } for module in &parsed { if module .body .iter() .any(|s| matches!(s, Stmt::MetaForm { .. })) && catalog.find(&module.name).is_none() { catalog.add( &module.name, tb_frontend::forms::ObjectClass::Form, None, false, ); } } let mut exports: Vec<_> = parsed .iter() .map(|m| tb_frontend::sema::export_declarations(m, &[])) .collect(); // Konstantenabhängigkeiten können beliebig über Module verteilt sein. // Jeder erfolgreiche Durchlauf löst mindestens eine weitere Deklaration auf. let constant_count: usize = exports .iter() .flatten() .filter(|s| matches!(s, Stmt::ConstDecl { .. })) .count(); for _ in 0..constant_count { let mut constants: HashMap<_, Vec<_>> = HashMap::new(); for stmt in exports.iter().flatten() { if let Stmt::ConstDecl { items, .. } = stmt { constants.entry(&items[0].0).or_default().push(stmt.clone()); } } let constants: Vec<_> = constants .into_values() .filter(|s| s.len() == 1) .flatten() .collect(); let next: Vec<_> = parsed .iter() .map(|m| tb_frontend::sema::export_declarations(m, &constants)) .collect(); if next == exports { break; } exports = next; } let mut parts = Vec::new(); let mut commons = Vec::new(); let mut products = Vec::new(); 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(); import_declarations(&mut module, &parsed, &exports, None); if self.debug_symbols { key = module.clone(); } if let Some(product) = self .products .iter() .find(|p| p.module == key && p.catalog == catalog) { parts.push(product.code.clone()); commons.push(product.commons.clone()); self.stats.reused += 1; continue; } self.stats.compiled += 1; 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); parts.push(code.clone()); commons.push(hir.commons.clone()); products.push(Product { module: key, debug_module: module, catalog: catalog.clone(), code, commons: hir.commons, }); } } locate_diagnostics(&mut diagnostics, &sources); if !diagnostics.is_empty() { return Err(diagnostics); } self.parsed.truncate(units.len()); self.products.retain(|p| { parsed.iter().any(|m| m.name == p.module.name) && !products.iter().any(|n| n.module.name == p.module.name) }); self.products.extend(products); 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 }); } 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) } } /// Lokale Definition gewinnt; außerhalb ihres Moduls muss ein Name eindeutig sein. fn type_definition<'a>(name: &str, origin: usize, all: &'a [Module]) -> Option<(usize, &'a Stmt)> { let mut candidates = all.iter().enumerate().flat_map(|(id, module)| { module.body.iter().filter_map(move |stmt| { matches!(stmt, Stmt::TypeDecl { name: n, .. } if n == name).then_some((id, stmt)) }) }); if let Some(local) = candidates.clone().find(|(id, _)| *id == origin) { return Some(local); } let first = candidates.next()?; candidates.next().is_none().then_some(first) } fn same_type( name: &str, a: usize, b: usize, all: &[Module], visiting: &mut HashSet<(usize, usize, String)>, ) -> bool { let ( Some((a, Stmt::TypeDecl { fields: af, .. })), Some((b, Stmt::TypeDecl { fields: bf, .. })), ) = (type_definition(name, a, all), type_definition(name, b, all)) else { return false; }; if a == b { return true; } if !visiting.insert((a, b, name.into())) { return false; } let equal = af.len() == bf.len() && af.iter().zip(bf).all(|((an, at), (bn, bt))| { an == bn && match (at, bt) { (TypeName::Udt(an), TypeName::Udt(bn)) => { an == bn && same_type(an, a, b, all, visiting) } _ => at == bt, } }); visiting.remove(&(a, b, name.into())); equal } fn import_type( name: &str, origin: usize, target: usize, all: &[Module], imported: &mut Vec, seen: &mut HashMap<(usize, String), String>, ) -> String { let Some((origin, Stmt::TypeDecl { fields, pos, .. })) = type_definition(name, origin, all) else { return name.into(); }; if origin == target || (type_definition(name, target, all).is_some_and(|(id, _)| id == target) && same_type(name, origin, target, all, &mut HashSet::new())) { return name.into(); } let key = (origin, name.to_string()); if let Some(alias) = seen.get(&key) { return alias.clone(); } // Öffentliche eindeutige Namen bleiben erhalten. Konfliktbehaftete Abhängigkeiten // bekommen einen internen Modulnamen, damit das lokale Layout unangetastet bleibt. let alias = if type_definition(name, target, all).is_some_and(|(id, _)| id == origin) { name.to_string() } else { format!("{}!{name}", all[origin].name) }; seen.insert(key, alias.clone()); let fields = fields .iter() .map(|(field, ty)| { let ty = match ty { TypeName::Udt(name) => { TypeName::Udt(import_type(name, origin, target, all, imported, seen)) } _ => ty.clone(), }; (field.clone(), ty) }) .collect(); imported.push(Stmt::TypeDecl { name: alias.clone(), fields, pos: *pos, }); alias } fn import_declarations( module: &mut Module, all: &[Module], exports: &[Vec], signature: Option<&HashSet>, ) { let target = all.iter().position(|m| m.name == module.name).unwrap(); let mut procedures: HashSet<_> = module.procs.iter().map(|p| p.sig.name.clone()).collect(); let mut local_constants = HashSet::new(); let mut local_types = HashSet::new(); for stmt in &module.body { match stmt { Stmt::Declare { sig, .. } => { procedures.insert(sig.name.clone()); } Stmt::ConstDecl { items, .. } => { local_constants.extend(items.iter().map(|i| i.0.clone())) } Stmt::TypeDecl { name, .. } => { local_types.insert(name.clone()); } _ => {} } } let mut foreign: HashMap<_, Vec<_>> = HashMap::new(); for (origin, declarations) in exports.iter().enumerate().filter(|(id, _)| *id != target) { for stmt in declarations { let key = match stmt { Stmt::Declare { sig, .. } => (0, sig.name.clone()), Stmt::ConstDecl { items, .. } => (1, items[0].0.clone()), Stmt::TypeDecl { name, .. } => (2, name.clone()), _ => continue, }; foreign.entry(key).or_default().push((origin, stmt)); } } let mut names: Vec<_> = foreign.keys().cloned().collect(); names.sort(); let mut imported = Vec::new(); let mut seen_types = HashMap::new(); for key in names { let candidates = &foreign[&key]; if candidates.len() != 1 { continue; } let (origin, stmt) = candidates[0]; match stmt { Stmt::Declare { sig, pos } if !procedures.contains(&sig.name) => { let mut sig = sig.clone(); for param in &mut sig.params { if let Some(TypeName::Udt(name)) = ¶m.as_type { param.as_type = Some(TypeName::Udt(import_type( name, origin, target, all, &mut imported, &mut seen_types, ))); } } imported.push(Stmt::Declare { sig, pos: *pos }); } Stmt::ConstDecl { items, .. } if !local_constants.contains(&items[0].0) => { imported.push(stmt.clone()) } Stmt::TypeDecl { name, .. } if !local_types.contains(name) => { import_type(name, origin, target, all, &mut imported, &mut seen_types); } _ => {} } } if let Some(names) = signature { // Nicht verwendete importierte Konstanten erzeugen weder Slots noch Tabellen. // TYPEs und Prozeduren dagegen gehören zu den unverketteten Tabellen. imported .retain(|s| !matches!(s, Stmt::ConstDecl {items,..} if !names.contains(&items[0].0))); // Importverträge enthalten Namen/Typen/Werte, keine verschobenen Rumpfzeilen // ihres Ursprungs. Eigene Quellorte bleiben Teil des Modulschlüssels. for stmt in &mut imported { match stmt { Stmt::Declare { pos, .. } | Stmt::TypeDecl { pos, .. } => { *pos = SourcePos::default() } Stmt::ConstDecl { items, pos } => { *pos = SourcePos::default(); for (_, _, expr) in items { match expr { tb_frontend::ast::Expr::DoubleLit(_, p) | tb_frontend::ast::Expr::StrLit(_, p) => *p = SourcePos::default(), _ => {} } } } _ => {} } } } imported.append(&mut module.body); let mut types = Vec::new(); imported.retain(|stmt| { if matches!(stmt, Stmt::TypeDecl { .. }) { types.push(stmt.clone()); false } else { true } }); let mut known = HashSet::new(); let mut ordered = Vec::new(); while !types.is_empty() { let next = types.iter().position(|stmt| match stmt { Stmt::TypeDecl { fields, .. } => fields.iter().all(|(_, ty)| match ty { TypeName::Udt(name) => known.contains(name), _ => true, }), _ => unreachable!(), }); let Some(index) = next else { break }; let stmt = types.remove(index); if let Stmt::TypeDecl { name, .. } = &stmt { known.insert(name.clone()); } ordered.push(stmt); } ordered.extend(types); // Sema diagnostiziert fehlende oder zyklische Typen. ordered.extend(imported); module.body = ordered; } fn module_pos(module: &Module) -> SourcePos { module .body .iter() .map(tb_frontend::sema::stmt_pos) .find(|p| p.line > 0) .or_else(|| module.procs.first().map(|p| p.pos)) .unwrap_or_default() } fn remap_type(ty: &mut TypeInit, ids: &[u16]) { if let TypeInit::Udt(id) = ty { *id = ids[*id as usize]; } } fn remap_signature(ty: &mut HTy, ids: &[u16]) { if let HTy::Udt(id) = ty { *id = ids[*id as usize]; } } fn merge_objects( target: &mut Vec, objects: &[tb_frontend::forms::FormObject], ) -> Result, 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, ast: &[ModuleMetadata], allow_open: bool, ) -> Result<(CompiledModule, Vec), Diagnostic> { let at = |pos, message| Diagnostic { file: None, pos, message, }; let mut result = codegen::compile(&tb_frontend::analyze_source(name, "").hir.unwrap()); result.modules = parts .iter() .map(|p| (p.name.clone(), p.option_base)) .collect(); result.objects.clear(); result.sources.clear(); result.strings.clear(); result.procs.clear(); result.option_base = parts[0].option_base; let mut proc_maps = Vec::new(); let mut count = 1usize; let mut definitions: HashMap> = HashMap::new(); for (part, module) in parts.iter().zip(ast) { 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(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 { definitions.entry(p.name.clone()).or_default().push(id); } } 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].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( 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::>() .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; } } } } let splits: Vec<_> = parts .iter() .map(|p| { p.procs[0] .code .iter() .position(|i| matches!(i, Instr::Stmt(0))) .map_or(0, |i| i.saturating_sub(1)) }) .collect(); let mut init_starts = Vec::new(); let mut body_starts = Vec::new(); let mut pc = 0; for split in &splits { init_starts.push(pc); pc += split; } for (part, split) in parts.iter().zip(&splits) { 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(); let mut initializers = Vec::new(); let mut bodies = Vec::new(); 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(); udt.name = udt.name.rsplit('!').next().unwrap().to_string(); for ty in &mut udt.fields { remap_type(ty, &types); } let id = result .udts .iter() .position(|u| u.name == udt.name && u.fields == udt.fields) .unwrap_or_else(|| { 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(ast[module_id].pos, "Zu viele TYPEs".into()))?, ); } 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(); remap_type(&mut ty, &types); let declaration = commons.get(&(slot as u16)); let key = declaration.map(|c| (c.block.clone(), c.key.clone())); let mut common_ty = declaration.map(|c| c.ty.clone()); if let Some(ty) = &mut common_ty { remap_signature(ty, &types); } let id = if let Some((id, previous)) = key.as_ref().and_then(|key| common.get_mut(key)) { let declaration = declaration.unwrap(); let compatible_dims = match (&previous.dims, &declaration.dims) { (None, None) => true, (Some(a), Some(b)) if a.is_empty() || b.is_empty() => true, (Some(a), Some(b)) => { a.len() == b.len() && a.iter().zip(b).all(|((al, ah), (bl, bh))| { al.zip(*bl).is_none_or(|(a, b)| a == b) && ah.zip(*bh).is_none_or(|(a, b)| a == b) }) } _ => false, }; if Some(&previous.ty) != common_ty.as_ref() || !compatible_dims { return Err(at( declaration.pos, format!("COMMON type or bounds mismatch: {name}"), )); } if let (Some(previous), Some(current)) = (&mut previous.dims, &declaration.dims) { if previous.is_empty() { *previous = current.clone(); } else { for ((lo, hi), (new_lo, new_hi)) in previous.iter_mut().zip(current) { *lo = lo.or(*new_lo); *hi = hi.or(*new_hi); } } } *id } else { 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() } else { format!("{}!{name}", part.name) }); if let Some(key) = key { let mut declaration = (*declaration.unwrap()).clone(); declaration.ty = common_ty.unwrap(); common.insert(key, (id, declaration)); } id }; globals.push(id); } debug_maps.push(DebugMap { globals: globals.clone(), types: types.clone(), procs: proc_maps[module_id].clone(), }); let string_offset = result.strings.len(); if string_offset + part.strings.len() >= u16::MAX as usize { return Err(at(ast[module_id].pos, "Zu viele Stringkonstanten".into())); } result.strings.append(&mut part.strings); 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(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() { proc.module = module_id as u16; for ty in &mut proc.locals_init { remap_type(ty, &types); } for param in &mut proc.params { remap_signature(&mut param.ty, &types); } if let Some(ty) = &mut proc.ret_ty { remap_signature(ty, &types); } if ast.len() > 1 { proc.name = format!("{}!{}", part.name, proc.name); } 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) | StoreDynamicObjectProperty(id) => *id += string_offset as u16, Input(_, _, id, _) if *id != u16::MAX => *id += string_offset as u16, LoadGlobal(id) | StoreGlobal(id) | MakeRefGlobal(id) => { *id = globals[*id as usize] } LoadArr(global, id, _, ty) | DimArr(global, id, _, ty) | CommonArr(global, id, _, ty) | RedimArr(global, id, _, ty) => { if *global { *id = globals[*id as usize]; } remap_type(ty, &types); } EraseSlot(true, id) => *id = globals[*id as usize], GetPut(_, _, 7, id) | PushUdtId(id) => *id = types[*id as usize], Call(id, _) => *id = proc_maps[module_id][*id as usize], Restore(id) => *id += data_offset, OnErrorGoto(pc) => *pc = main_pc(*pc), Jump(pc) | JumpIfFalse(pc) | JumpIfTrue(pc) | Gosub(pc) | RetGosubTo(pc) | OnErrorLocal(pc) | ResumeLabel(pc) | TrapDefine(_, pc) if proc_id == 0 => { *pc = main_pc(*pc) } OnJump(id, _) => { if proc_id == 0 { for target in &mut part.jump_tables[*id as usize] { *target = main_pc(*target); } } *id += jump_offset as u16; } _ => {} } } } initializers.extend_from_slice(&part.procs[0].code[..splits[module_id]]); bodies.extend_from_slice( &part.procs[0].code[splits[module_id]..part.procs[0].code.len() - 1], ); 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); } initializers.extend(bodies); initializers.push(Instr::End); main.code = initializers; result.procs.insert(0, main); // 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 || proc.params.len() != target.params.len() || proc .params .iter() .zip(&target.params) .any(|(a, b)| a.ty != b.ty || a.array != b.array || a.by_ref != b.by_ref) { return Err(at( 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)) } // Erst ein ausdrücklich erzeugter DebugCompiler bindet den Quellcompiler ein. // Die reine Runtime kann dadurch den gleichen VM-Pfad ohne Parser/Codegenerator linken. type DebugCompileFn = fn(&DebugCompiler, u16, &str, &str, bool) -> Result; /// Ephemeral symbol/link context, deliberately not part of TBC serialization. #[derive(Clone)] pub struct DebugCompiler { compile_fn: DebugCompileFn, modules: Vec<(u16, Module, FormCatalog, DebugMap)>, symbols: tb_frontend::sema::DebugSymbols, slots: Vec, error: Option, } #[derive(Clone)] struct DebugMap { globals: Vec, types: Vec, procs: Vec, } #[derive(Clone)] pub struct DebugCode { pub procedure: crate::bytecode::ProcCode, pub strings: Vec>, pub expression: bool, } impl ProjectCompiler { pub fn debug_compiler(&self) -> DebugCompiler { let modules: Vec<_> = self .debug_names .iter() .zip(&self.debug_maps) .enumerate() .filter_map(|(id, (name, map))| { self.products .iter() .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 { 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 { error = Some("Debug-Symboltabelle überschreitet die 16-Bit-Slotgrenze".into()); break; } let offset = symbols.udts.len() as u16; for mut udt in hir.udts { udt.name = format!("", ast.name, udt.name); for (_, ty) in &mut udt.fields { if let HTy::Udt(id) = ty { *id += offset; } } symbols.udts.push(udt); } for (mut var, slot) in hir.globals.into_iter().zip(&map.globals) { var.name = format!("{}!{}", ast.name, var.name); if let HTy::Udt(id) = &mut var.ty { *id += offset; } symbols.globals.push(var); slots.push(*slot); } } } DebugCompiler { compile_fn: DebugCompiler::compile_impl, modules, symbols, slots, error, } } } impl DebugCompiler { pub fn compile( &self, module: u16, procedure: &str, text: &str, expression: bool, ) -> Result { (self.compile_fn)(self, module, procedure, text, expression) } fn compile_impl( &self, module: u16, procedure: &str, text: &str, expression: bool, ) -> Result { if let Some(error) = &self.error { return Err(error.clone()); } let (_, ast, catalog, map) = self .modules .iter() .find(|(id, _, _, _)| *id == module) .ok_or("Kein Debug-Quellkontext für dieses Kompilat")?; let name = if procedure == "
" { &ast.name } else { procedure.rsplit('!').next().unwrap_or(procedure) }; if map.globals.len() + self.symbols.globals.len() >= u16::MAX as usize || map.types.len() + self.symbols.udts.len() >= u16::MAX as usize { return Err("Debug-Kontext überschreitet die 16-Bit-Slotgrenze".into()); } let mut symbols = self.symbols.clone(); symbols.original_globals = map.globals.len(); let (mut hir, debug) = tb_frontend::sema::lower_debug( ast, catalog, name, text, expression, symbols, ) .map_err(|e| { e.iter() .map(ToString::to_string) .collect::>() .join("\n") })?; hir.procs.push(debug); let mut globals = map.globals.clone(); globals.extend(&self.slots); let mut types = map.types.clone(); for (_, _, _, mapping) in &self.modules { types.extend(&mapping.types); } let mut compiled = codegen::compile(&hir); let mut proc = compiled.procs.pop().unwrap(); proc.module = module; for ty in &mut proc.locals_init { remap_type(ty, &types); } if let Some(ty) = &mut proc.ret_ty { remap_signature(ty, &types); } for i in &mut proc.code { use Instr::*; match i { LoadGlobal(id) | StoreGlobal(id) | MakeRefGlobal(id) => { *id = *globals .get(*id as usize) .ok_or("Unbekannter globaler Slot")? } LoadArr(global, id, _, ty) => { if *global { *id = globals[*id as usize]; } remap_type(ty, &types); } 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], _ => {} } } Ok(DebugCode { procedure: proc, strings: compiled.strings, expression, }) } } impl DebugCompiler { pub(crate) fn same_control_context( &self, module: u16, procedure: &str, from: SourcePos, to: SourcePos, ) -> bool { let Some((_, ast, _, _)) = self.modules.iter().find(|(id, _, _, _)| *id == module) else { return false; }; let body = if procedure == "
" { &ast.body } else { let Some(proc) = ast.procs.iter().find(|p| { p.sig .name .eq_ignore_ascii_case(procedure.rsplit('!').next().unwrap_or(procedure)) }) else { return false; }; &proc.body }; let a = control_path(body, from); let b = control_path(body, to); a.is_some() && a == b } } /// Structured arms supplement bytecode loop ranges: ELSE and CASE are distinct /// even when their forward jump would otherwise look like a top-level GOTO. fn control_path(body: &[Stmt], target: SourcePos) -> Option> { for stmt in body { let pos = tb_frontend::sema::stmt_pos(stmt); if pos == target { return Some(vec![]); } let mut children: Vec<(&[Stmt], Option)> = Vec::new(); match stmt { Stmt::If { then_body, elseifs, else_body, .. } => { children.push((then_body, None)); for (_, body, pos) in elseifs { children.push((body, Some(*pos))); } if let Some(body) = else_body { children.push((body, None)); } } Stmt::Select { arms, .. } => { for arm in arms { children.push((&arm.body, Some(arm.pos))); } } Stmt::For { body, end_pos, .. } | Stmt::DoLoop { body, end_pos, .. } | Stmt::While { body, end_pos, .. } => children.push((body, Some(*end_pos))), _ => {} } for (index, (body, entry)) in children.into_iter().enumerate() { if let Some(mut path) = if entry == Some(target) { Some(vec![]) } else { control_path(body, target) } { path.insert(0, (pos, index)); return Some(path); } } } None }