Phase 6: P-Code-Bibliotheken und Linker abschließen, Cross-Buildplan festhalten
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
618
crates/tb-vm/src/library.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
@@ -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>" {
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
375
crates/tb-vm/tests/library.rs
Normal file
375
crates/tb-vm/tests/library.rs
Normal 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"
|
||||
);
|
||||
}
|
||||
@@ -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}"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user