1412 lines
52 KiB
Rust
1412 lines
52 KiB
Rust
//! 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<String>) -> 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<Parsed>,
|
|
products: Vec<Product>,
|
|
pub stats: CompileStats,
|
|
/// 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 {
|
|
pub parsed: usize,
|
|
pub compiled: usize,
|
|
pub reused: usize,
|
|
}
|
|
struct Parsed {
|
|
unit: SourceUnit,
|
|
prefix: Vec<tb_frontend::source::SourceFile>,
|
|
sources: Vec<tb_frontend::source::SourceFile>,
|
|
module: Module,
|
|
diagnostics: Vec<Diagnostic>,
|
|
names: HashSet<String>,
|
|
}
|
|
struct Product {
|
|
module: Module,
|
|
debug_module: Module,
|
|
catalog: FormCatalog,
|
|
code: CompiledModule,
|
|
commons: Vec<tb_frontend::hir::HCommon>,
|
|
}
|
|
|
|
pub fn compile_project(
|
|
name: &str,
|
|
units: &[SourceUnit],
|
|
catalog: &FormCatalog,
|
|
forms: &[FormFile],
|
|
) -> Result<CompiledModule, Vec<Diagnostic>> {
|
|
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<tb_frontend::sema::BoundFormReferences, Vec<Diagnostic>> {
|
|
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<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() && 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 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<Stmt>,
|
|
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<Stmt>],
|
|
signature: Option<&HashSet<String>>,
|
|
) {
|
|
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<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: &[ModuleMetadata],
|
|
allow_open: bool,
|
|
) -> Result<(CompiledModule, Vec<DebugMap>), 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<String, Vec<u16>> = 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::<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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
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<DebugCode, String>;
|
|
|
|
/// 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<u16>,
|
|
error: Option<String>,
|
|
}
|
|
#[derive(Clone)]
|
|
struct DebugMap {
|
|
globals: Vec<u16>,
|
|
types: Vec<u16>,
|
|
procs: Vec<u16>,
|
|
}
|
|
#[derive(Clone)]
|
|
pub struct DebugCode {
|
|
pub procedure: crate::bytecode::ProcCode,
|
|
pub strings: Vec<std::rc::Rc<str>>,
|
|
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!("<Debug:{}!{}>", 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<DebugCode, String> {
|
|
(self.compile_fn)(self, module, procedure, text, expression)
|
|
}
|
|
fn compile_impl(
|
|
&self,
|
|
module: u16,
|
|
procedure: &str,
|
|
text: &str,
|
|
expression: bool,
|
|
) -> Result<DebugCode, String> {
|
|
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 == "<main>" {
|
|
&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::<Vec<_>>()
|
|
.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 == "<main>" {
|
|
&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<Vec<(SourcePos, usize)>> {
|
|
for stmt in body {
|
|
let pos = tb_frontend::sema::stmt_pos(stmt);
|
|
if pos == target {
|
|
return Some(vec![]);
|
|
}
|
|
let mut children: Vec<(&[Stmt], Option<SourcePos>)> = 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
|
|
}
|