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

@@ -21,11 +21,12 @@ fn main() -> ExitCode {
match args.first().map(String::as_str) {
Some("run") => cmd_run(&args[1..]),
Some("build") => cmd_build(&args[1..]),
Some("link") => cmd_link(&args[1..]),
Some("check") => cmd_check(&args[1..]),
Some("convert-frm") => cmd_convert_frm(&args[1..]),
_ => {
eprintln!(
"Aufruf: tbc run|build|check <datei.bas|datei.frm|projekt.mak|datei.tbc> | tbc convert-frm <quelle.frm> <ziel.frm>"
"Aufruf: tbc run|build|link|check <datei.bas|datei.frm|projekt.mak|datei.tbc> | tbc convert-frm <quelle.frm> <ziel.frm>"
);
ExitCode::from(1)
}
@@ -79,7 +80,7 @@ fn compile(
path_arg: Option<&String>,
) -> Result<(PathBuf, tb_vm::bytecode::CompiledModule), ExitCode> {
let Some(path) = path_arg else {
eprintln!("Aufruf: tbc run|build|check <datei.bas|datei.frm|projekt.mak|datei.tbc>");
eprintln!("Aufruf: tbc run|build|link|check <datei.bas|datei.frm|projekt.mak|datei.tbc>");
return Err(ExitCode::from(1));
};
let path = PathBuf::from(path);
@@ -118,6 +119,10 @@ fn compile(
}
fn cmd_check(args: &[String]) -> ExitCode {
if args.len() != 1 {
eprintln!("Aufruf: tbc check <Projekt>");
return ExitCode::from(1);
}
match compile(args.first()) {
Ok(_) => ExitCode::SUCCESS,
Err(code) => code,
@@ -125,7 +130,13 @@ fn cmd_check(args: &[String]) -> ExitCode {
}
fn cmd_build(args: &[String]) -> ExitCode {
match build(args) {
cmd_output(args, false)
}
fn cmd_link(args: &[String]) -> ExitCode {
cmd_output(args, true)
}
fn cmd_output(args: &[String], link: bool) -> ExitCode {
match build(args, link) {
Ok(path) => {
println!("{}", path.display());
ExitCode::SUCCESS
@@ -137,9 +148,10 @@ fn cmd_build(args: &[String]) -> ExitCode {
}
}
fn build(args: &[String]) -> anyhow::Result<PathBuf> {
fn build(args: &[String], link: bool) -> anyhow::Result<PathBuf> {
use anyhow::{bail, ensure, Context};
let mut source = None;
let mut inputs = Vec::new();
let mut library = false;
let mut exe = false;
let mut force = false;
let mut output = None;
@@ -148,6 +160,13 @@ fn build(args: &[String]) -> anyhow::Result<PathBuf> {
let mut args = args.iter();
while let Some(arg) = args.next() {
match arg.as_str() {
"--library" => {
ensure!(
!library && !link,
"--library ist nur bei build zulässig und darf nicht doppelt sein"
);
library = true;
}
"--exe" => {
ensure!(!exe, "Doppeltes --exe");
exe = true;
@@ -171,30 +190,74 @@ fn build(args: &[String]) -> anyhow::Result<PathBuf> {
"Doppelte Option {arg}"
);
}
"--" => {
for value in args.by_ref() {
ensure!(
source.replace(value.clone()).is_none(),
"Mehrere Quelldateien angegeben"
);
}
}
"--" => inputs.extend(args.by_ref().cloned()),
value if value.starts_with('-') => bail!("Unbekannte Build-Option: {value}"),
_ => {
ensure!(
source.replace(arg.clone()).is_none(),
"Mehrere Quelldateien angegeben"
);
}
_ => inputs.push(arg.clone()),
}
}
let source = source.context(
"Aufruf: tbc build <Quelle> [--exe --target <Triple> --template <tbrt> -o <Ziel> --force]",
)?;
ensure!(!inputs.is_empty(), "Aufruf: tbc build|link <Quelle> [Bibliothek.tbl ...] [--library | --exe] [-o Ziel] [--force]");
ensure!(!(exe && library), "--library und --exe schließen sich aus");
ensure!(
exe || (target.is_none() && template.is_none() && output.is_none() && !force),
"Native Ausgabeoptionen benötigen --exe"
exe || (target.is_none() && template.is_none()),
"--target/--template benötigen --exe"
);
ensure!(
exe || library || link || output.is_none(),
"Ausgabeoptionen benötigen --exe, --library oder link"
);
let source = &inputs[0];
let path = PathBuf::from(source);
let tbc = tb_vm::project_io::has_extension(&path, "tbc");
ensure!(!tbc || (!library && inputs.len()==1 && (!link || exe)), "Fertiges TBC kann nur ohne zusätzliche TBL mit link --exe verpackt werden; es ist kein Library-Produkt");
let mut protected = vec![path.clone()];
let mut compiler = tb_vm::project::ProjectCompiler::default();
let loaded = if tbc {
None
} else {
let mut project = SourceLoader::default()
.load(&path)
.map_err(anyhow::Error::msg)?;
for input in &inputs[1..] {
project
.add_library(Path::new(input))
.map_err(anyhow::Error::msg)?;
}
protected.extend(project.protected_inputs());
Some(project)
};
let has_libraries = loaded.as_ref().is_some_and(|p| !p.libraries.is_empty());
let diags = |errors: Vec<tb_frontend::Diagnostic>| {
anyhow::anyhow!(errors
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join("\n"))
};
let cancel = export_cancel::ExportCancel::new()?;
if library {
let product = loaded
.as_ref()
.unwrap()
.compile_library(&mut compiler)
.map_err(diags)?;
let bytes = product.to_tbl().map_err(anyhow::Error::msg)?;
let out = output
.map(PathBuf::from)
.unwrap_or_else(|| path.with_extension("tbl"));
tb_export::publish(
&out,
force,
&protected,
&|| cancel.cancelled(),
|temporary| {
std::fs::write(temporary, &bytes)?;
tb_vm::library::Library::from_tbl(&std::fs::read(temporary)?)
.map_err(anyhow::Error::msg)?;
Ok(())
},
)?;
return Ok(out);
}
let selected = if exe {
Some(match target {
Some(s) => tb_export::Target::parse(&s)?,
@@ -203,8 +266,14 @@ fn build(args: &[String]) -> anyhow::Result<PathBuf> {
} else {
None
};
let (path, module) =
compile(Some(&source)).map_err(|_| anyhow::anyhow!("Build fehlgeschlagen"))?;
let module = if let Some(project) = loaded {
project
.compile(&mut compiler, &module_name(&path))
.map_err(diags)?
} else {
tb_vm::bytecode::CompiledModule::from_tbc(&std::fs::read(&path)?)
.map_err(|e| anyhow::anyhow!("{e}"))?
};
if let Some(target) = selected {
let out = output.map(PathBuf::from).unwrap_or_else(|| {
path.with_extension(if target == tb_export::Target::WindowsAmd64 {
@@ -222,7 +291,6 @@ fn build(args: &[String]) -> anyhow::Result<PathBuf> {
.join(target.triple())
.join(target.runtime_name()),
};
let cancel = export_cancel::ExportCancel::new()?;
tb_export::export(
tb_export::Export {
module: &module,
@@ -230,14 +298,37 @@ fn build(args: &[String]) -> anyhow::Result<PathBuf> {
template: &template,
output: &out,
overwrite: force,
protected: &[path],
protected: &protected,
},
&|| cancel.cancelled(),
)?;
Ok(out)
} else {
let out = path.with_extension("tbc");
std::fs::write(&out, module.to_tbc())?;
let legacy = !link && !has_libraries && output.is_none() && !force && inputs.len() == 1;
let out = output
.map(PathBuf::from)
.unwrap_or_else(|| path.with_extension("tbc"));
if legacy
&& !tbc
&& protected.iter().all(|p| {
tb_vm::project_io::identity(p).ok() != tb_vm::project_io::identity(&out).ok()
})
{
std::fs::write(&out, module.to_tbc())?;
} else {
tb_export::publish(
&out,
force,
&protected,
&|| cancel.cancelled(),
|temporary| {
std::fs::write(temporary, module.to_tbc())?;
tb_vm::bytecode::CompiledModule::from_tbc(&std::fs::read(temporary)?)
.map_err(|e| anyhow::anyhow!("{e}"))?;
Ok(())
},
)?;
}
Ok(out)
}
}

View File

@@ -384,3 +384,92 @@ fn native_build_optionen_sind_explizit_und_tbc_bleibt_standard() {
assert!(!dir.join("program").exists());
}
}
#[test]
fn library_cli_links_without_sources_and_protects_all_inputs_and_outputs() {
let dir = std::env::temp_dir().join(format!("tb-library-cli-{}", std::process::id()));
std::fs::create_dir(&dir).unwrap();
struct Cleanup(std::path::PathBuf);
impl Drop for Cleanup {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
let _cleanup = Cleanup(dir.clone());
let call = |args: &[&str]| {
Command::new(env!("CARGO_BIN_EXE_tbc"))
.current_dir(&dir)
.env("PATH", "")
.args(args)
.output()
.unwrap()
};
let success = |args: &[&str]| {
let out = call(args);
assert!(out.status.success(), "{args:?}: {out:?}");
out
};
std::fs::write(
dir.join("a.bas"),
"DECLARE SUB Second(n%)\nSUB First(n%)\nSecond n%\nEND SUB\n",
)
.unwrap();
std::fs::write(dir.join("b.bas"), "SUB Second(n%)\nn%=n%+7\nEND SUB\n").unwrap();
success(&["build", "a.bas", "--library"]);
success(&["build", "b.bas", "--library", "--output", "b.tbl"]);
std::fs::remove_file(dir.join("a.bas")).unwrap();
std::fs::remove_file(dir.join("b.bas")).unwrap();
std::fs::write(dir.join("main.bas"), "CALL First(n%)\nPRINT n%\nEND\n").unwrap();
assert!(!call(&["link", "main.bas", "a.tbl"]).status.success());
success(&["link", "main.bas", "a.tbl", "b.tbl", "-o", "linked.tbc"]);
assert_eq!(success(&["run", "linked.tbc"]).stdout, b" 7 \n");
std::fs::write(dir.join("app.mak"), "main.bas\na.tbl\nb.tbl\n").unwrap();
success(&["check", "app.mak"]);
success(&["build", "app.mak"]);
assert_eq!(success(&["run", "app.mak"]).stdout, b" 7 \n");
let before = std::fs::read(dir.join("a.tbl")).unwrap();
let old = std::fs::read(dir.join("linked.tbc")).unwrap();
for args in [
vec![
"link", "main.bas", "a.tbl", "b.tbl", "-o", "a.tbl", "--force",
],
vec!["link", "main.bas", "a.tbl", "b.tbl", "-o", "linked.tbc"],
vec!["build", "main.bas", "--library", "--exe"],
vec![
"build",
"main.bas",
"--library",
"--target",
"aarch64-apple-darwin",
],
vec!["link", "linked.tbc", "a.tbl", "--exe"],
vec!["link", "linked.tbc"],
vec!["run", "a.tbl"],
vec!["check", "a.tbl"],
vec!["link", "a.tbl"],
vec!["link", "main.bas", "main.bas"],
vec!["link", "main.bas", "a.tbl", "a.tbl"],
vec!["link", "app.mak", "-o", "app.mak", "--force"],
vec!["link", "app.mak", "-o", "missing/output.tbc"],
] {
assert!(!call(&args).status.success(), "accepted {args:?}");
assert_eq!(std::fs::read(dir.join("a.tbl")).unwrap(), before);
assert_eq!(std::fs::read(dir.join("linked.tbc")).unwrap(), old);
}
success(&["link", "app.mak", "-o", "linked.tbc", "--force"]);
let old = std::fs::read(dir.join("linked.tbc")).unwrap();
std::fs::write(dir.join("a.tbl"), &before[..before.len() - 1]).unwrap();
assert!(!call(&["link", "app.mak", "-o", "linked.tbc", "--force"])
.status
.success());
assert_eq!(std::fs::read(dir.join("linked.tbc")).unwrap(), old);
for p in ["a.tbl", "b.tbl", "main.bas", "app.mak"] {
std::fs::remove_file(dir.join(p)).unwrap();
}
assert_eq!(success(&["run", "linked.tbc"]).stdout, b" 7 \n");
assert!(!std::fs::read_dir(&dir).unwrap().any(|e| e
.unwrap()
.file_name()
.to_string_lossy()
.contains(".tmp")));
}

View File

@@ -7,7 +7,7 @@ use std::{
};
use tb_vm::bytecode::{CompiledModule, TBC_VERSION};
pub const RUNTIME_VERSION: u32 = 1;
pub use tb_vm::bytecode::RUNTIME_VERSION;
const CONTAINER_VERSION: u32 = 1;
const MAGIC: &[u8; 8] = b"TBPCODE!";
const FOOTER: usize = 48;
@@ -78,12 +78,7 @@ fn put64(b: &mut [u8], at: usize, n: usize) {
b[at..at + 8].copy_from_slice(&(n as u64).to_le_bytes());
}
/// FNV-1a-64 gegen Übertragungs-/Dateibeschädigung; keine Herkunftsauthentisierung.
pub fn checksum(b: &[u8]) -> u64 {
b.iter().fold(0xcbf29ce484222325, |h, v| {
(h ^ u64::from(*v)).wrapping_mul(0x100000001b3)
})
}
pub use tb_vm::bytecode::checksum;
fn macho_commands(b: &[u8]) -> Result<Vec<(u32, usize)>> {
let end = 32usize

View File

@@ -17,6 +17,14 @@ use tb_ui::frm::FormInitial;
pub const TBC_MAGIC: &[u8; 4] = b"TBC\0";
pub const TBC_VERSION: u16 = 4;
pub const RUNTIME_VERSION: u32 = 1;
/// FNV-1a-64 gegen Dateibeschädigung, keine Herkunftsauthentisierung.
pub fn checksum(bytes: &[u8]) -> u64 {
bytes.iter().fold(0xcbf29ce484222325, |h, v| {
(h ^ u64::from(*v)).wrapping_mul(0x100000001b3)
})
}
/// Vergleichsoperator (Operand der `Cmp*`-Instruktionen).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -78,14 +86,14 @@ impl<'a> Reader<'a> {
pub fn new(buf: &'a [u8]) -> Self {
Reader { buf, pos: 0 }
}
fn finish(&self) -> Result<(), LoadError> {
pub(crate) fn finish(&self) -> Result<(), LoadError> {
if self.pos != self.buf.len() {
Err(LoadError::Corrupt("überzählige Abschnittsdaten"))
} else {
Ok(())
}
}
fn take(&mut self, n: usize) -> Result<&'a [u8], LoadError> {
pub(crate) fn take(&mut self, n: usize) -> Result<&'a [u8], LoadError> {
if n > self.buf.len().saturating_sub(self.pos) {
return Err(LoadError::Corrupt("unerwartetes Dateiende"));
}
@@ -93,23 +101,30 @@ impl<'a> Reader<'a> {
self.pos += n;
Ok(s)
}
fn u8(&mut self) -> Result<u8, LoadError> {
pub(crate) fn u8(&mut self) -> Result<u8, LoadError> {
Ok(self.take(1)?[0])
}
fn u16(&mut self) -> Result<u16, LoadError> {
pub(crate) fn u16(&mut self) -> Result<u16, LoadError> {
Ok(u16::from_le_bytes(self.take(2)?.try_into().unwrap()))
}
fn u32(&mut self) -> Result<u32, LoadError> {
pub(crate) fn u32(&mut self) -> Result<u32, LoadError> {
Ok(u32::from_le_bytes(self.take(4)?.try_into().unwrap()))
}
fn string(&mut self) -> Result<String, LoadError> {
pub(crate) fn count(&mut self) -> Result<usize, LoadError> {
let n = self.u32()? as usize;
if n > self.buf.len().saturating_sub(self.pos) {
return Err(LoadError::Corrupt("Tabellenlänge"));
}
Ok(n)
}
pub(crate) fn string(&mut self) -> Result<String, LoadError> {
let n = self.u32()? as usize;
let b = self.take(n)?;
String::from_utf8(b.to_vec()).map_err(|_| LoadError::Corrupt("UTF-8"))
}
}
trait Enc: Sized {
pub(crate) trait Enc: Sized {
fn enc(&self, out: &mut Vec<u8>);
fn dec(r: &mut Reader) -> Result<Self, LoadError>;
}
@@ -534,7 +549,7 @@ pub struct CompiledModule {
pub event_procs: Vec<HEventProc>,
}
fn w_string(out: &mut Vec<u8>, s: &str) {
pub(crate) fn w_string(out: &mut Vec<u8>, s: &str) {
out.extend_from_slice(&(s.len() as u32).to_le_bytes());
out.extend_from_slice(s.as_bytes());
}

View File

@@ -406,18 +406,7 @@ impl Vm {
.next()
.unwrap()
.to_ascii_uppercase();
if !matches!(
name.as_str(),
"CMNDLGREGISTER"
| "CMNDLGCLOSE"
| "ABOUT"
| "FILEOPEN"
| "FILESAVE"
| "FILEPRINT"
| "FINDTEXT"
| "CHANGETEXT"
| "COLORPALETTE"
) {
if !is_runtime_external(&name) {
return Ok(false);
}
let args = self.stack[self.stack.len().saturating_sub(argc)..].to_vec();
@@ -3290,3 +3279,18 @@ fn strict_number(t: &str) -> Option<f64> {
let cleaned = t.replace(['d', 'D'], "E").replace('e', "E");
cleaned.parse::<f64>().ok()
}
pub(crate) fn is_runtime_external(name: &str) -> bool {
matches!(
name,
"CMNDLGREGISTER"
| "CMNDLGCLOSE"
| "ABOUT"
| "FILEOPEN"
| "FILESAVE"
| "FILEPRINT"
| "FINDTEXT"
| "CHANGETEXT"
| "COLORPALETTE"
)
}

View File

@@ -12,6 +12,7 @@ pub mod bytecode;
pub mod codegen;
pub mod interp;
pub mod library;
pub mod project;
pub mod project_io;
pub use project::compile_project;

618
crates/tb-vm/src/library.rs Normal file
View File

@@ -0,0 +1,618 @@
//! Portable, unverknüpfte Modulprodukte. Kein Quelltext und kein ausführbares TBC.
use crate::bytecode::{w_string, CompiledModule, Enc, LoadError, Reader, TBC_VERSION};
use tb_frontend::{
ast::{Expr, Module, Param, Proc, ProcKind, ProcSig, Stmt, TypeName},
hir::HCommon,
lexer::Suffix,
SourcePos,
};
pub const TBL_VERSION: u16 = 1;
pub use crate::bytecode::RUNTIME_VERSION as TBL_RUNTIME_VERSION;
#[derive(Debug, Clone)]
pub struct ModuleMetadata {
pub name: String,
pub pos: SourcePos,
pub exports: Vec<Stmt>,
pub declarations: Vec<(String, SourcePos)>,
pub defined: Vec<String>,
pub commons: Vec<HCommon>,
}
impl ModuleMetadata {
pub(crate) fn from_source(module: &Module, exports: Vec<Stmt>, commons: Vec<HCommon>) -> Self {
Self {
name: module.name.clone(),
pos: 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(),
exports,
declarations: module
.body
.iter()
.filter_map(|s| match s {
Stmt::Declare { sig, pos } => Some((sig.name.clone(), *pos)),
_ => None,
})
.chain(module.procs.iter().map(|p| (p.sig.name.clone(), p.pos)))
.collect(),
defined: module.procs.iter().map(|p| p.sig.name.clone()).collect(),
commons,
}
}
pub(crate) fn proc_pos(&self, name: &str) -> SourcePos {
self.declarations
.iter()
.find(|(n, _)| n == name)
.map_or(self.pos, |(_, p)| *p)
}
/// Eine Deklarationssicht für den vorhandenen Namens-/Typresolver, ohne Programmrümpfe.
pub(crate) fn declaration_view(&self) -> Module {
Module {
name: self.name.clone(),
body: self
.exports
.iter()
.filter(|s| !matches!(s, Stmt::Declare { .. }))
.cloned()
.collect(),
procs: self
.exports
.iter()
.filter_map(|s| match s {
Stmt::Declare { sig, pos } => Some(Proc {
sig: sig.clone(),
is_static: false,
body: vec![],
pos: *pos,
end_pos: *pos,
}),
_ => None,
})
.collect(),
}
}
}
#[derive(Debug, Clone)]
pub struct LibraryModule {
pub metadata: ModuleMetadata,
pub code: CompiledModule,
}
#[derive(Debug, Clone, Default)]
pub struct Library {
pub modules: Vec<LibraryModule>,
}
fn count(out: &mut Vec<u8>, n: usize) -> Result<(), String> {
u32::try_from(n)
.map_err(|_| "TBL-Tabelle zu groß")?
.enc(out);
Ok(())
}
fn pos(out: &mut Vec<u8>, p: SourcePos) {
p.source.enc(out);
p.line.enc(out);
p.column.enc(out);
}
fn read_pos(r: &mut Reader) -> Result<SourcePos, LoadError> {
Ok(SourcePos {
source: r.u32()?,
line: r.u32()?,
column: r.u32()?,
})
}
fn suffix(out: &mut Vec<u8>, s: Option<Suffix>) {
out.push(s.map_or(0, |s| s.as_char() as u8));
}
fn read_suffix(r: &mut Reader) -> Result<Option<Suffix>, LoadError> {
let b = r.u8()?;
if b == 0 {
Ok(None)
} else {
Suffix::from_char(b as char)
.map(Some)
.ok_or(LoadError::Corrupt("TBL-Suffix"))
}
}
fn ty(out: &mut Vec<u8>, t: &TypeName) {
use TypeName::*;
out.push(match t {
Integer => 0,
Long => 1,
Single => 2,
Double => 3,
Currency => 4,
Str => 5,
FixedStr(_) => 6,
Form => 7,
Control => 8,
Udt(_) => 9,
});
match t {
FixedStr(n) => n.enc(out),
Udt(n) => w_string(out, n),
_ => {}
}
}
fn read_ty(r: &mut Reader) -> Result<TypeName, LoadError> {
use TypeName::*;
Ok(match r.u8()? {
0 => Integer,
1 => Long,
2 => Single,
3 => Double,
4 => Currency,
5 => Str,
6 => FixedStr(i64::dec(r)?),
7 => Form,
8 => Control,
9 => Udt(r.string()?),
_ => return Err(LoadError::Corrupt("TBL-Typ")),
})
}
fn declaration(out: &mut Vec<u8>, s: &Stmt) -> Result<(), String> {
match s {
Stmt::Declare { sig, pos: p } => {
out.push(0);
pos(out, *p);
out.push(if sig.kind == ProcKind::Sub { 0 } else { 1 });
w_string(out, &sig.name);
suffix(out, sig.suffix);
count(out, sig.params.len())?;
for p in &sig.params {
w_string(out, &p.name);
suffix(out, p.suffix);
p.array.enc(out);
p.as_type.is_some().enc(out);
if let Some(t) = &p.as_type {
ty(out, t);
}
}
}
Stmt::TypeDecl {
name,
fields,
pos: p,
} => {
out.push(1);
pos(out, *p);
w_string(out, name);
count(out, fields.len())?;
for (n, t) in fields {
w_string(out, n);
ty(out, t);
}
}
Stmt::ConstDecl { items, pos: p } => {
out.push(2);
pos(out, *p);
count(out, items.len())?;
for (n, s, e) in items {
w_string(out, n);
suffix(out, *s);
match e {
Expr::DoubleLit(v, _) => {
out.push(0);
v.enc(out);
}
Expr::StrLit(v, _) => {
out.push(1);
w_string(out, v);
}
_ => return Err(format!("Nicht aufgelöste TBL-Konstante: {n}")),
}
}
}
_ => return Err("Ungültige TBL-Exportdeklaration".into()),
}
Ok(())
}
fn read_declaration(r: &mut Reader) -> Result<Stmt, LoadError> {
let tag = r.u8()?;
let p = read_pos(r)?;
Ok(match tag {
0 => {
let kind = match r.u8()? {
0 => ProcKind::Sub,
1 => ProcKind::Function,
_ => return Err(LoadError::Corrupt("TBL-Prozedurart")),
};
let name = r.string()?;
let suffix = read_suffix(r)?;
let mut params = vec![];
for _ in 0..r.count()? {
params.push(Param {
name: r.string()?,
suffix: read_suffix(r)?,
array: bool::dec(r)?,
as_type: if bool::dec(r)? {
Some(read_ty(r)?)
} else {
None
},
});
}
Stmt::Declare {
sig: ProcSig {
kind,
name,
suffix,
params,
},
pos: p,
}
}
1 => {
let name = r.string()?;
let mut fields = vec![];
for _ in 0..r.count()? {
fields.push((r.string()?, read_ty(r)?));
}
Stmt::TypeDecl {
name,
fields,
pos: p,
}
}
2 => {
let mut items = vec![];
for _ in 0..r.count()? {
let name = r.string()?;
let suffix = read_suffix(r)?;
let expr = match r.u8()? {
0 => Expr::DoubleLit(f64::dec(r)?, p),
1 => Expr::StrLit(r.string()?, p),
_ => return Err(LoadError::Corrupt("TBL-Konstante")),
};
items.push((name, suffix, expr));
}
Stmt::ConstDecl { items, pos: p }
}
_ => return Err(LoadError::Corrupt("TBL-Deklaration")),
})
}
impl Library {
pub fn to_tbl(&self) -> Result<Vec<u8>, String> {
self.validate()?;
let mut out = b"TBL\0".to_vec();
TBL_VERSION.enc(&mut out);
TBC_VERSION.enc(&mut out);
TBL_RUNTIME_VERSION.enc(&mut out);
count(&mut out, self.modules.len())?;
for m in &self.modules {
let mut section = vec![];
let meta = &m.metadata;
w_string(&mut section, &meta.name);
pos(&mut section, meta.pos);
count(&mut section, meta.exports.len())?;
for s in &meta.exports {
declaration(&mut section, s)?;
}
count(&mut section, meta.declarations.len())?;
for (n, p) in &meta.declarations {
w_string(&mut section, n);
pos(&mut section, *p);
}
count(&mut section, meta.defined.len())?;
for n in &meta.defined {
w_string(&mut section, n);
}
count(&mut section, meta.commons.len())?;
for c in &meta.commons {
c.slot.enc(&mut section);
c.block.is_some().enc(&mut section);
if let Some(b) = &c.block {
w_string(&mut section, b);
}
w_string(&mut section, &c.key);
c.ty.enc(&mut section);
pos(&mut section, c.pos);
c.dims.is_some().enc(&mut section);
if let Some(dims) = &c.dims {
count(&mut section, dims.len())?;
for (lo, hi) in dims {
for v in [lo, hi] {
v.is_some().enc(&mut section);
if let Some(v) = v {
v.enc(&mut section);
}
}
}
}
}
let code = m.code.to_tbc();
count(&mut section, code.len())?;
section.extend(code);
count(&mut out, section.len())?;
out.extend(section);
}
out.extend_from_slice(&crate::bytecode::checksum(&out).to_le_bytes());
Ok(out)
}
pub fn from_tbl(bytes: &[u8]) -> Result<Self, String> {
let end = bytes.len().checked_sub(8).ok_or("TBL-Prüfsumme fehlt")?;
if crate::bytecode::checksum(&bytes[..end])
!= u64::from_le_bytes(bytes[end..].try_into().unwrap())
{
return Err("TBL-Prüfsumme stimmt nicht".into());
}
let bytes = &bytes[..end];
let decode = || -> Result<Self, LoadError> {
let mut r = Reader::new(bytes);
if r.take(4)? != b"TBL\0" {
return Err(LoadError::Corrupt("TBL-Magic"));
}
if r.u16()? != TBL_VERSION || r.u16()? != TBC_VERSION || r.u32()? != TBL_RUNTIME_VERSION
{
return Err(LoadError::Corrupt("TBL-/P-Code-/Runtime-Version"));
}
let mut modules = vec![];
for _ in 0..r.count()? {
let size = r.u32()? as usize;
let mut s = Reader::new(r.take(size)?);
let name = s.string()?;
let pos = read_pos(&mut s)?;
let mut exports = vec![];
for _ in 0..s.count()? {
exports.push(read_declaration(&mut s)?);
}
let mut declarations = vec![];
for _ in 0..s.count()? {
declarations.push((s.string()?, read_pos(&mut s)?));
}
let mut defined = vec![];
for _ in 0..s.count()? {
defined.push(s.string()?);
}
let mut commons = vec![];
for _ in 0..s.count()? {
let slot = s.u16()?;
let block = if bool::dec(&mut s)? {
Some(s.string()?)
} else {
None
};
let key = s.string()?;
let ty = tb_frontend::hir::HTy::dec(&mut s)?;
let pos = read_pos(&mut s)?;
let dims = if bool::dec(&mut s)? {
let mut dims = vec![];
for _ in 0..s.count()? {
let mut bound = || {
if bool::dec(&mut s)? {
i32::dec(&mut s).map(Some)
} else {
Ok(None)
}
};
dims.push((bound()?, bound()?));
}
Some(dims)
} else {
None
};
commons.push(HCommon {
slot,
block,
key,
ty,
dims,
pos,
});
}
let size = s.u32()? as usize;
let code = CompiledModule::from_tbc(s.take(size)?)?;
s.finish()?;
modules.push(LibraryModule {
metadata: ModuleMetadata {
name,
pos,
exports,
declarations,
defined,
commons,
},
code,
});
}
r.finish()?;
Ok(Self { modules })
};
let library = decode().map_err(|e| format!("Ungültige TBL-Datei: {e}"))?;
library.validate()?;
Ok(library)
}
pub fn validate(&self) -> Result<(), String> {
if self.modules.is_empty() || self.modules.len() > u16::MAX as usize {
return Err("TBL ohne Module oder zu viele Module".into());
}
let mut names = std::collections::HashSet::new();
for m in &self.modules {
let meta = &m.metadata;
let code = &m.code;
if !names.insert(meta.name.to_uppercase()) {
return Err(format!("Duplicate definition: module {}", meta.name));
}
code.validate().map_err(|e| e.to_string())?;
if code.name != meta.name
|| code.modules.len() != 1
|| code.modules[0].0 != meta.name
|| code.startup_form.is_some()
{
return Err("TBL-Modulidentität/Startformular".into());
}
let valid_pos = |p: SourcePos| (p.source as usize) < code.sources.len();
if !valid_pos(meta.pos)
|| meta.declarations.iter().any(|(_, p)| !valid_pos(*p))
|| meta
.exports
.iter()
.any(|s| !valid_pos(tb_frontend::sema::stmt_pos(s)))
{
return Err("TBL-Quellposition".into());
}
if code.procs[0].kind != tb_frontend::hir::HProcKind::Main
|| code.udts.len() > u16::MAX as usize
|| code.jump_tables.len() > u16::MAX as usize
|| code.data.len() > u32::MAX as usize
|| code.sources.len() > u32::MAX as usize
|| !matches!(code.procs[0].code.last(), Some(crate::bytecode::Instr::End))
|| code.procs.iter().any(|p| p.code.len() > u32::MAX as usize)
{
return Err("TBL-Modulrumpf/Grenzwert".into());
}
let mut procedure_names = std::collections::HashSet::new();
for proc in code.procs.iter().skip(1) {
if proc.kind == tb_frontend::hir::HProcKind::Main
|| !procedure_names.insert(&proc.name)
|| proc.params.len() > u8::MAX as usize
{
return Err("TBL-Prozedurtabelle".into());
}
}
if meta
.declarations
.iter()
.any(|(n, _)| !procedure_names.contains(n))
{
return Err("TBL-Deklarationsreferenz".into());
}
let mut exported = std::collections::HashSet::new();
let resolve_ty = |t: &TypeName| -> Option<tb_frontend::hir::HTy> {
use tb_frontend::hir::{HTy, NumTy};
Some(match t {
TypeName::Integer => HTy::Num(NumTy::Int),
TypeName::Long => HTy::Num(NumTy::Lng),
TypeName::Single => HTy::Num(NumTy::Sng),
TypeName::Double => HTy::Num(NumTy::Dbl),
TypeName::Currency => HTy::Num(NumTy::Cur),
TypeName::Str => HTy::Str,
TypeName::FixedStr(n) => HTy::FixedStr(u32::try_from(*n).ok()?),
TypeName::Form => HTy::Form,
TypeName::Control => HTy::Control,
TypeName::Udt(n) => {
HTy::Udt(u16::try_from(code.udts.iter().position(|u| u.name == *n)?).ok()?)
}
})
};
let suffix_ty = |s: Option<Suffix>| {
use tb_frontend::hir::{HTy, NumTy};
match s {
Some(Suffix::Integer) => HTy::Num(NumTy::Int),
Some(Suffix::Long) => HTy::Num(NumTy::Lng),
None | Some(Suffix::Single) => HTy::Num(NumTy::Sng),
Some(Suffix::Double) => HTy::Num(NumTy::Dbl),
Some(Suffix::Currency) => HTy::Num(NumTy::Cur),
Some(Suffix::Str) => HTy::Str,
}
};
for declaration in &meta.exports {
let invalid = || format!("TBL-Exportvertrag in {}", meta.name);
match declaration {
Stmt::Declare { sig, .. } => {
if !exported.insert((0, sig.name.clone()))
|| !meta.defined.contains(&sig.name)
{
return Err(invalid());
}
let proc = code
.procs
.iter()
.skip(1)
.find(|p| p.name == sig.name)
.ok_or_else(invalid)?;
if (sig.kind == ProcKind::Sub)
!= (proc.kind == tb_frontend::hir::HProcKind::Sub)
|| proc.ret_ty
!= (sig.kind == ProcKind::Function).then(|| suffix_ty(sig.suffix))
|| sig.params.len() != proc.params.len()
{
return Err(invalid());
}
for (a, b) in sig.params.iter().zip(&proc.params) {
let ty = if let Some(t) = &a.as_type {
resolve_ty(t).ok_or_else(invalid)?
} else {
suffix_ty(a.suffix)
};
if ty != b.ty || a.array != b.array || b.by_ref == a.array {
return Err(invalid());
}
}
}
Stmt::TypeDecl { name, fields, .. } => {
if !exported.insert((1, name.clone())) {
return Err(invalid());
}
let layout = code
.udts
.iter()
.find(|u| u.name == *name)
.ok_or_else(invalid)?;
if fields.len() != layout.fields.len() {
return Err(invalid());
}
let mut names = std::collections::HashSet::new();
for ((n, t), actual) in fields.iter().zip(&layout.fields) {
if !names.insert(n)
|| crate::codegen::type_init(&resolve_ty(t).ok_or_else(invalid)?)
!= *actual
{
return Err(invalid());
}
}
}
Stmt::ConstDecl { items, .. } => {
if items.len() != 1 {
return Err(invalid());
}
for (n, _, e) in items {
if !exported.insert((2, n.clone()))
|| !matches!(e,Expr::DoubleLit(v,_) if v.is_finite())
&& !matches!(e, Expr::StrLit(..))
{
return Err(invalid());
}
}
}
_ => return Err(invalid()),
}
}
let mut defs = std::collections::HashSet::new();
for n in &meta.defined {
if !exported.contains(&(0, n.clone()))
|| !defs.insert(n)
|| !code.procs.iter().skip(1).any(|p| p.name == *n)
{
return Err(format!("TBL-Prozedurreferenz: {n}"));
}
}
let mut common_slots = std::collections::HashSet::new();
for c in &meta.commons {
if c.slot as usize >= code.globals_init.len()
|| !common_slots.insert(c.slot)
|| code.globals_init[c.slot as usize]
!= if c.dims.is_some() {
tb_runtime::value::TypeInit::Empty
} else {
crate::codegen::type_init(&c.ty)
}
|| !valid_pos(c.pos)
|| matches!(c.ty,tb_frontend::hir::HTy::Udt(id) if id as usize>=code.udts.len())
|| c.dims.as_ref().is_some_and(|d| {
d.len() > 255
|| d.iter()
.any(|(lo, hi)| lo.zip(*hi).is_some_and(|(lo, hi)| lo > hi))
})
{
return Err("TBL-COMMON-Referenz/Bounds".into());
}
}
}
Ok(())
}
}

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>" {

View File

@@ -71,6 +71,9 @@ pub struct ReadDocument {
}
pub fn read_document(path: &Path) -> Result<ReadDocument, String> {
if has_extension(path, "tbl") {
return Err(format!("{}: TBL ist kein Textdokument", path.display()));
}
let bytes = std::fs::read(path).map_err(|e| format!("{}: {e}", path.display()))?;
let binary = has_extension(path, "frm") && bytes.starts_with(&[0xfc, 0x08, 1, 0]);
let mut warnings = Vec::new();
@@ -146,8 +149,11 @@ impl Manifest {
}
} else {
let member = loader.resolve(base, s).map_err(|e| fail(&e))?;
if !has_extension(&member, "bas") && !has_extension(&member, "frm") {
return Err(fail("Projektmitglied muss BAS oder FRM sein"));
if !has_extension(&member, "bas")
&& !has_extension(&member, "frm")
&& !has_extension(&member, "tbl")
{
return Err(fail("Projektmitglied muss BAS, FRM oder TBL sein"));
}
let key = identity(&member).map_err(|e| fail(&e.to_string()))?;
if out
@@ -172,6 +178,9 @@ impl Manifest {
start.display()
)
})?;
if has_extension(&chosen, "tbl") {
return Err(format!("{}: TBL kann kein $STARTUP sein", path.display()));
}
out.startup = Some(chosen);
}
Ok(out)
@@ -252,10 +261,18 @@ pub struct SourceLoader {
pub include_paths: Vec<PathBuf>,
}
#[derive(Debug, Clone, Copy)]
pub enum BuildMember {
Source(usize),
Library(usize),
}
#[derive(Debug)]
pub struct ProjectSources {
pub manifest: Manifest,
pub units: Vec<SourceUnit>,
pub libraries: Vec<PathBuf>,
pub order: Vec<BuildMember>,
pub forms: Vec<FormFile>,
}
impl ProjectSources {
@@ -265,21 +282,108 @@ impl ProjectSources {
compiler: &mut crate::project::ProjectCompiler,
name: &str,
) -> Result<crate::bytecode::CompiledModule, Vec<tb_frontend::Diagnostic>> {
let mut catalog = tb_frontend::forms::FormCatalog::default();
for form in &self.forms {
catalog.append(&form.catalog());
if self.units.is_empty() {
return Err(vec![tb_frontend::Diagnostic {file:None,pos:Default::default(),message:"Ausführbares Projekt benötigt ein BASIC-/Form-Startmodul; TBL ist keine Startdatei".into()}]);
}
let mut module = compiler.compile(name, &self.units, &catalog, &self.forms)?;
if self
let library = self.compile_library(compiler)?;
let startup = if self
.manifest
.startup
.as_ref()
.is_some_and(|p| !has_extension(p, "frm"))
{
module.startup_form = None;
}
Ok(module)
None
} else {
self.forms.first().map(|f| f.root.name.as_str())
};
compiler.link_library(name, library, startup)
}
/// Der Inhalt jeder expliziten TBL wird bei jedem Build neu gelesen; Dateigröße/mtime sind kein Cache-Schlüssel.
pub fn compile_library(
&self,
compiler: &mut crate::project::ProjectCompiler,
) -> Result<crate::library::Library, Vec<tb_frontend::Diagnostic>> {
let fail = |message| {
vec![tb_frontend::Diagnostic {
file: None,
pos: Default::default(),
message,
}]
};
let libraries: Vec<_> = self
.libraries
.iter()
.map(|path| read_library(path).map_err(&fail))
.collect::<Result<_, _>>()?;
let mut catalog = tb_frontend::forms::FormCatalog::default();
for form in &self.forms {
catalog.append(&form.catalog());
}
let compiled = compiler.compile_library(&self.units, &catalog, &self.forms, &libraries)?;
let mut modules: std::collections::HashMap<_, _> = compiled
.modules
.into_iter()
.map(|m| (m.metadata.name.to_uppercase(), m))
.collect();
let mut result = crate::library::Library::default();
for member in &self.order {
let names = match *member {
BuildMember::Source(id) => vec![self.units[id].name.as_str()],
BuildMember::Library(id) => libraries[id]
.modules
.iter()
.map(|m| m.metadata.name.as_str())
.collect(),
};
for name in names {
result.modules.push(
modules
.remove(&name.to_uppercase())
.ok_or_else(|| fail(format!("Doppeltes Modul: {name}")))?,
);
}
}
result.validate().map_err(fail)?;
Ok(result)
}
pub fn add_library(&mut self, path: &Path) -> Result<(), String> {
let path = SourceLoader::default().resolve(Path::new("."), &path.to_string_lossy())?;
if !has_extension(&path, "tbl") {
return Err(format!(
"{}: zusätzliche Eingabe muss TBL sein",
path.display()
));
}
let key = identity(&path).map_err(|e| e.to_string())?;
if self
.libraries
.iter()
.any(|p| identity(p).ok().as_ref() == Some(&key))
{
return Err(format!("{}: doppelte Bibliothek", path.display()));
}
read_library(&path)?;
self.order.push(BuildMember::Library(self.libraries.len()));
self.libraries.push(path);
Ok(())
}
pub fn protected_inputs(&self) -> Vec<PathBuf> {
self.manifest
.members()
.cloned()
.chain(self.libraries.iter().cloned())
.chain(
self.units
.iter()
.flat_map(|u| u.segments.iter().map(|s| PathBuf::from(&s.file))),
)
.collect()
}
}
fn read_library(path: &Path) -> Result<crate::library::Library, String> {
let bytes = std::fs::read(path).map_err(|e| format!("{}: {e}", path.display()))?;
crate::library::Library::from_tbl(&bytes).map_err(|e| format!("{}: {e}", path.display()))
}
/// BASIC RUN resolves relative to the execution target, never to process cwd.
@@ -430,15 +534,18 @@ impl SourceLoader {
Manifest::parse(&path, &self.read(&path)?.text(), self)?
} else {
Manifest {
lines: vec![ProjectLine::File(path)],
lines: vec![ProjectLine::File(path.clone())],
startup: None,
}
};
self.load_manifest(manifest)
.map_err(|e| format!("{}: {e}", path.display()))
}
pub fn load_manifest(&self, manifest: Manifest) -> Result<ProjectSources, String> {
let mut units = Vec::new();
let mut forms = Vec::new();
let mut libraries = Vec::new();
let mut order = Vec::new();
let mut members: Vec<_> = manifest.members().collect();
if let Some(startup) = &manifest.startup {
let index = members
@@ -448,7 +555,23 @@ impl SourceLoader {
let selected = members.remove(index);
members.insert(0, selected);
}
if manifest.startup.is_none() {
if let Some(index) = members.iter().position(|p| !has_extension(p, "tbl")) {
let source = members.remove(index);
members.insert(0, source);
}
}
for path in members {
if has_extension(path, "tbl") {
if manifest.startup.as_ref() == Some(path) {
return Err("TBL kann kein $STARTUP sein".into());
}
read_library(path)?;
order.push(BuildMember::Library(libraries.len()));
libraries.push(path.clone());
continue;
}
order.push(BuildMember::Source(units.len()));
let content = self.read(path)?;
let (name, code, first_line) = match content {
Content::Text(text) => (module_name(path), text, 1),
@@ -469,6 +592,8 @@ impl SourceLoader {
}
Ok(ProjectSources {
manifest,
libraries,
order,
units,
forms,
})

View File

@@ -0,0 +1,375 @@
use tb_frontend::{forms::FormCatalog, source::SourceUnit};
use tb_runtime::host::CaptureHost;
use tb_vm::{
bytecode::CompiledModule,
interp::{RunEvent, Vm},
library::Library,
project::ProjectCompiler,
};
fn unit(name: &str, text: &str) -> SourceUnit {
SourceUnit::new(name, &format!("{name}.bas"), text)
}
fn library(units: &[SourceUnit], libs: &[Library]) -> Library {
let l = ProjectCompiler::default()
.compile_library(units, &FormCatalog::default(), &[], libs)
.unwrap();
let bytes = l.to_tbl().unwrap();
let l = Library::from_tbl(&bytes).unwrap();
assert_eq!(bytes, l.to_tbl().unwrap());
l
}
fn link(main: &str, libs: &[Library]) -> Result<CompiledModule, String> {
let mut c = ProjectCompiler::default();
c.compile_library(&[unit("APP", main)], &FormCatalog::default(), &[], libs)
.and_then(|l| c.link_library("APP", l, None))
.map_err(|ds| {
ds.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join("\n")
})
}
fn output(code: CompiledModule) -> String {
let mut vm = Vm::new(code);
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
tb_runtime::snapshot::text(&vm.rt.screen)
}
#[test]
fn two_source_free_libraries_with_typed_open_imports_and_diagnostics() {
let a = library(
&[unit(
"A",
"DECLARE SUB Second(n AS INTEGER)\nSUB First(n AS INTEGER)\nSecond n\nEND SUB",
)],
&[],
);
assert!(link("CALL First(n%)\nEND", std::slice::from_ref(&a))
.unwrap_err()
.contains("SECOND"));
let b = library(
&[unit("B", "SUB Second(n AS INTEGER)\nn=n+3\nEND SUB")],
&[],
);
assert_eq!(
output(
link(
"n%=2\nCALL First(n%)\nPRINT n%\nEND",
&[a.clone(), b.clone()]
)
.unwrap()
),
" 5 \n"
);
let incompatible = library(&[unit("B", "SUB Second(n AS LONG)\nEND SUB")], &[]);
assert!(link("CALL First(n%)\nEND", &[a.clone(), incompatible])
.unwrap_err()
.contains("Parameter type mismatch"));
let c = library(&[unit("C", "SUB Second(n AS INTEGER)\nEND SUB")], &[]);
let direct = link("CALL Second(n%)\nEND", &[b.clone(), c.clone()]).unwrap_err();
assert!(
direct.contains("Ambiguous subprogram: SECOND (B, C)"),
"{direct}"
);
let error = link("CALL First(n%)\nEND", &[a.clone(), b.clone(), c]).unwrap_err();
assert!(
error.contains("Ambiguous") && error.contains("B") && error.contains("C"),
"{error}"
);
assert!(link("END", &[b.clone(), b.clone()])
.unwrap_err()
.contains("Duplicate definition: module B"));
let combined = library(&[], &[a, b]);
assert_eq!(combined.modules.len(), 2);
assert_eq!(
output(link("CALL First(n%)\nPRINT n%\nEND", &[combined]).unwrap()),
" 3 \n"
);
}
#[test]
fn source_and_library_have_identical_types_constants_common_data_byref_and_init_order() {
let a=unit("A","CONST N=3\nTYPE Record\nx AS INTEGER\ns AS STRING * 4\nEND TYPE\nCOMMON SHARED shared%\nDIM SHARED ready%(N)\nDATA 7\nPRINT \"A\"\nDATA 7\nSUB Work(a%(), r AS Record, n%)\nready%(N)=1\nshared%=shared%+1\na%(1)=N\nr.x=r.x+n%\nn%=9\nRESTORE\nREAD d%\nPRINT d%\nEND SUB\n");
let b = unit("B", "PRINT \"B\"\n");
let main="COMMON SHARED shared%\nDIM a%(N)\nDIM r AS Record\nn%=2\nCALL Work(a%(),r,n%)\nPRINT a%(1);r.x;n%;shared%\nCALL Work(a%(),r,(n%))\nPRINT r.x;n%;shared%\n";
let source = ProjectCompiler::default()
.compile(
"APP",
&[unit("APP", main), a.clone(), b.clone()],
&FormCatalog::default(),
&[],
)
.unwrap();
let binary = link(main, &[library(&[a, b], &[])]).unwrap();
assert_eq!(output(source), output(binary));
}
#[test]
fn library_source_locations_and_run_restart_are_preserved() {
let l = library(&[unit("LIB", "SUB Fail\n200 ERROR 6\nEND SUB")], &[]);
let mut vm = Vm::new(link("CALL Fail\nEND", &[l]).unwrap());
assert!(matches!(
vm.run(&mut CaptureHost::default()),
RunEvent::Error {
code: 6,
line: 2,
..
}
));
assert_eq!(vm.current_file(), "LIB.bas");
let l = library(
&[unit(
"LIB",
"DIM SHARED n%\nSUB Increment\nn%=n%+1\nPRINT n%\nEND SUB",
)],
&[],
);
let code = link("CALL Increment\nEND", &[l]).unwrap();
for _ in 0..2 {
assert_eq!(output(code.clone()), " 1 \n");
}
}
#[test]
fn library_container_rejects_truncation_versions_lengths_and_references() {
let l = library(
&[unit("LIB", "CONST N=3\nSUB Work(n%)\nn%=N\nEND SUB")],
&[],
);
let bytes = l.to_tbl().unwrap();
let seal = |mut payload: Vec<u8>| {
payload.extend_from_slice(&tb_vm::bytecode::checksum(&payload).to_le_bytes());
payload
};
for end in 0..bytes.len() - 8 {
assert!(
Library::from_tbl(&bytes[..end]).is_err(),
"accepted prefix {end}"
);
assert!(
Library::from_tbl(&seal(bytes[..end].to_vec())).is_err(),
"accepted incomplete payload {end}"
);
}
for offset in [0, 4, 6, 8, 12, 16] {
let mut bad = bytes[..bytes.len() - 8].to_vec();
bad[offset] = 255;
assert!(
Library::from_tbl(&seal(bad)).is_err(),
"accepted offset {offset}"
);
}
let mut bad = bytes[..bytes.len() - 8].to_vec();
bad.push(0);
assert!(Library::from_tbl(&seal(bad)).is_err());
for offset in (0..bytes.len() - 8).step_by(7) {
let mut bad = bytes[..bytes.len() - 8].to_vec();
bad[offset] ^= 255;
let bad = seal(bad);
std::panic::catch_unwind(|| {
if let Ok(l) = Library::from_tbl(&bad) {
let _ = link("END", &[l]);
}
})
.expect("beschädigte TBL darf nicht paniken");
}
let mut bad = l.clone();
if let tb_frontend::ast::Stmt::ConstDecl { items, .. } = &mut bad.modules[0].metadata.exports[0]
{
items.clear();
}
assert!(bad.to_tbl().is_err());
let mut bad = l.clone();
bad.modules[0].metadata.defined.push("ABSENT".into());
assert!(bad.to_tbl().is_err());
let mut bad = l;
bad.modules[0].code.procs[0]
.code
.push(tb_vm::bytecode::Instr::Call(u16::MAX, 0));
assert!(bad.to_tbl().is_err());
}
#[test]
fn separate_form_libraries_remap_nested_objects_initials_arrays_and_events() {
let form = |name: &str, text: &str| {
tb_ui::frm::read_text(&format!("{name}.frm"),&format!("VERSION 1.00\nBEGIN Form {name}\n Width = 30\n Height = 10\n BEGIN Frame Frame1\n BEGIN TextBox Text1\n Text = \"{text}\"\n END\n END\n BEGIN TextBox Feld\n Index = 0\n Text = \"null\"\n END\n BEGIN TextBox Feld\n Index = 2\n Text = \"zwei\"\n END\nEND\nSUB Form_Load\nText1.Text=Text1.Text+\"!\"\nEND SUB\n")).unwrap()
};
let forms = [form("Form1", "a"), form("Form2", "b")];
let mut catalog = FormCatalog::default();
for f in &forms {
catalog.append(&f.catalog());
}
let units: Vec<_> = forms.iter().map(|f| unit(&f.root.name, &f.code)).collect();
let libs: Vec<_> = forms
.iter()
.zip(&units)
.map(|(f, u)| {
let l = ProjectCompiler::default()
.compile_library(
std::slice::from_ref(u),
&f.catalog(),
std::slice::from_ref(f),
&[],
)
.unwrap();
Library::from_tbl(&l.to_tbl().unwrap()).unwrap()
})
.collect();
let main=unit("APP","Form1.Show\nForm2.Show\na$=Form1!Text1.Text\nb$=Form2!Text1.Text\nc$=Form2!Feld(2).Text\nForm1.Hide\nForm2.Hide\nCLS\nPRINT a$\nPRINT b$\nPRINT c$\nEND");
let mut all = vec![main.clone()];
all.extend(units);
let mut c = ProjectCompiler::default();
let mut source = c.compile("APP", &all, &catalog, &forms).unwrap();
source.startup_form = None;
let l = c
.compile_library(&[main], &FormCatalog::default(), &[], &libs)
.unwrap();
let linked = c.link_library("APP", l, None).unwrap();
assert_eq!(linked.event_procs.len(), 2);
assert_eq!(output(source), output(linked));
}
#[test]
fn contracts_reject_result_common_bounds_and_invalid_declarations() {
let a = library(
&[unit(
"A",
"DECLARE FUNCTION Value%()\nSUB Work\nPRINT Value%\nEND SUB",
)],
&[],
);
let b = library(&[unit("B", "FUNCTION Value&\nValue&=4\nEND FUNCTION")], &[]);
assert!(link("CALL Work\nEND", &[a, b])
.unwrap_err()
.contains("Parameter type mismatch"));
let a = library(&[unit("A", "COMMON SHARED n%(1 TO 3)\n")], &[]);
let b = library(&[unit("B", "COMMON SHARED n%(1 TO 4)\n")], &[]);
assert!(link("END", &[a, b])
.unwrap_err()
.contains("COMMON type or bounds mismatch"));
for source in [
"DECLARE SUB Bad(x AS Missing)",
"DECLARE SUB Bad(x%)\nDECLARE SUB Bad(x&)",
"CONST N=Missing\n",
"TYPE X\nn AS Missing\nEND TYPE",
] {
assert!(
ProjectCompiler::default()
.compile_library(&[unit("BAD", source)], &FormCatalog::default(), &[], &[])
.is_err(),
"{source}"
);
}
}
#[test]
fn source_debug_context_keeps_module_ids_when_library_is_between_sources() {
let l = library(&[unit("LIB", "DIM n%\nSUB LibraryCall\nEND SUB")], &[]);
let mut compiler = ProjectCompiler::default();
compiler.debug_symbols = true;
let mut product = compiler
.compile_library(
&[unit("APP", "PRINT 0"), unit("LAST", "x%=7\nSTOP")],
&FormCatalog::default(),
&[],
&[l],
)
.unwrap();
product.modules.swap(1, 2);
let code = compiler.link_library("APP", product, None).unwrap();
let debug = compiler.debug_compiler();
assert!(debug.compile(1, "<main>", "x%", true).is_err());
let mut vm = Vm::new(code);
vm.debug.enabled = true;
vm.debug.compiler = Some(debug);
assert!(matches!(
vm.run(&mut CaptureHost::default()),
RunEvent::Stopped { .. }
));
let frame = vm.debug_location().unwrap().frame;
assert!(matches!(
vm.evaluate_watch(frame, "x%").unwrap(),
tb_runtime::value::Value::Int(7)
));
}
#[test]
fn unused_declare_is_not_an_import_but_references_in_any_body_must_resolve() {
let l = library(
&[unit(
"LIB",
"DECLARE FUNCTION Missing%(s$)\nSUB Work\nPRINT 7\nEND SUB",
)],
&[],
);
let code = link("CALL Work\nEND", &[l]).unwrap();
assert!(!code.procs.iter().any(|p| p.name.ends_with("MISSING")));
assert_eq!(output(code), " 7 \n");
let l = library(
&[unit(
"LIB",
"DECLARE SUB Missing\nSUB Unused\nCALL Missing\nEND SUB",
)],
&[],
);
assert!(link("END", &[l])
.unwrap_err()
.contains("Subprogram not defined: MISSING"));
let mut compiler = ProjectCompiler::default();
compiler.debug_symbols = true;
compiler
.compile(
"APP",
&[unit("APP", "DECLARE SUB Missing\nSTOP")],
&FormCatalog::default(),
&[],
)
.unwrap();
assert!(compiler
.debug_compiler()
.compile(0, "<main>", "CALL Missing", false)
.err()
.unwrap()
.contains("Subprogram not defined"));
}
#[test]
fn library_include_locations_keep_physical_lines_and_basic_erl() {
use tb_frontend::source::SourceSegment;
let source = SourceUnit {
name: "LIB".into(),
segments: vec![
SourceSegment {
file: "lib.bas".into(),
first_line: 1,
text: "SUB Fail\n".into(),
},
SourceSegment {
file: "nested.bi".into(),
first_line: 9,
text: "200 ERROR 6\n".into(),
},
SourceSegment {
file: "lib.bas".into(),
first_line: 3,
text: "END SUB\n".into(),
},
],
};
let l = library(&[source], &[]);
let mut vm = Vm::new(link("CALL Fail\nEND", std::slice::from_ref(&l)).unwrap());
assert!(matches!(
vm.run(&mut CaptureHost::default()),
RunEvent::Error {
code: 6,
line: 9,
..
}
));
assert_eq!(vm.current_file(), "nested.bi");
assert_eq!(
output(
link(
"ON ERROR GOTO Handler\nCALL Fail\nEND\nHandler: PRINT ERR;ERL\nEND",
&[l]
)
.unwrap()
),
" 6 200 \n"
);
}

View File

@@ -191,3 +191,107 @@ fn case_insensitive_parent_lookup_works_from_relative_base_without_changing_cwd(
let found = tb_vm::project_io::relative_case_insensitive(&base, "../SIBLING.BAS").unwrap();
assert_eq!(identity(&found).unwrap(), identity(&sibling).unwrap());
}
#[test]
fn tbl_members_move_save_as_order_startup_and_same_size_cache_replacement() {
use tb_frontend::{forms::FormCatalog, source::SourceUnit};
use tb_vm::{
interp::{RunEvent, Vm},
project::ProjectCompiler,
};
let t = Temp::new();
let library = |n, value| {
ProjectCompiler::default()
.compile_library(
&[SourceUnit::new(
"LIB",
"removed.bas",
&format!("CONST N={n}\nSUB Work\nPRINT {value}\nEND SUB\nPRINT \"LIB\""),
)],
&FormCatalog::default(),
&[],
&[],
)
.unwrap()
.to_tbl()
.unwrap()
};
let first = library(1, 1);
let second = library(2, 2);
assert_eq!(first.len(), second.len());
t.write("old/main.bas", "PRINT N\nCALL Work\n");
t.write("old/last.bas", "PRINT \"LAST\"\n");
let lib = t.0.join("old/Mixed.tbl");
fs::write(&lib, &first).unwrap();
let mak = t.write(
"old/app.mak",
"mixed.TBL\nlast.bas\nmain.bas\n' $STARTUP: \"main.bas\"\n",
);
let loader = SourceLoader::default();
let input = loader.load(&mak).unwrap();
assert_eq!(input.units.len(), 2);
assert_eq!(input.libraries.len(), 1);
let run = |code| {
let mut vm = Vm::new(code);
assert_eq!(
vm.run(&mut tb_runtime::host::CaptureHost::default()),
RunEvent::Ended
);
tb_runtime::snapshot::text(&vm.rt.screen)
};
let mut c = ProjectCompiler::default();
assert_eq!(
run(input.compile(&mut c, "APP").unwrap()),
" 1 \n 1 \nLIB\nLAST\n"
);
fs::write(&lib, &second).unwrap();
assert_eq!(
run(input.compile(&mut c, "APP").unwrap()),
" 2 \n 2 \nLIB\nLAST\n"
);
let body_only = library(2, 3);
assert_eq!(second.len(), body_only.len());
fs::write(&lib, &body_only).unwrap();
assert_eq!(
run(input.compile(&mut c, "APP").unwrap()),
" 2 \n 3 \nLIB\nLAST\n"
);
assert_eq!(
c.stats.compiled, 0,
"Unveränderte Deklarationen dürfen Quellprodukte wiederverwenden"
);
fs::create_dir_all(t.0.join("save")).unwrap();
let saved = t.0.join("save/app.mak");
fs::write(&saved, input.manifest.text(&saved).unwrap()).unwrap();
assert_eq!(
identity(&loader.load(&saved).unwrap().libraries[0]).unwrap(),
identity(&input.libraries[0]).unwrap()
);
fs::rename(t.0.join("old"), t.0.join("moved")).unwrap();
assert_eq!(
run(loader
.load(&t.0.join("moved/app.mak"))
.unwrap()
.compile(&mut c, "APP")
.unwrap()),
" 2 \n 3 \nLIB\nLAST\n"
);
let moved = t.0.join("moved/app.mak");
for text in [
"Mixed.tbl\n' $STARTUP: \"Mixed.tbl\"",
"Mixed.tbl\nmixed.TBL\n",
"absent.tbl\n",
] {
assert!(Manifest::parse(&moved, text, &loader).is_err(), "{text}");
}
assert!(
tb_vm::project_io::read_document(&t.0.join("moved/Mixed.tbl"))
.unwrap_err()
.contains("kein Textdokument")
);
fs::remove_file(t.0.join("moved/Mixed.tbl")).unwrap();
let error = loader.load(&moved).unwrap_err();
assert!(
error.contains("app.mak") && error.contains("mixed.TBL"),
"{error}"
);
}