Phase 6: P-Code-Bibliotheken und Linker abschließen, Cross-Buildplan festhalten
This commit is contained in:
14
PLAN.md
14
PLAN.md
@@ -492,8 +492,18 @@ sind bestanden. Die vollständige Terminal-/OS-Matrix bleibt Phase 6.
|
|||||||
|
|
||||||
Verbindliche Build- und Release-Ziele (Terminalanwendungen): Windows amd64,
|
Verbindliche Build- und Release-Ziele (Terminalanwendungen): Windows amd64,
|
||||||
macOS arm64 sowie Linux amd64 und arm64. Gitea Actions am bestehenden
|
macOS arm64 sowie Linux amd64 und arm64. Gitea Actions am bestehenden
|
||||||
Origin erstellt und prüft die Artefakte für alle vier Ziele. Die
|
Origin erstellt die Artefakte für alle vier Ziele auf dem vorhandenen
|
||||||
Plattformtests umfassen unter Linux weiterhin mindestens zwei Emulatoren.
|
Linux-arm64-Runner: Linux arm64 nativ beziehungsweise mit festgelegter
|
||||||
|
Zielbaseline, Linux amd64 und macOS arm64 über cargo-zigbuild, Windows amd64
|
||||||
|
über cargo-xwin. Cross-Toolchains einschließlich SDK/Sysroot werden in Change
|
||||||
|
06 reproduzierbar festgelegt; vier native Buildrunner sind nicht erforderlich.
|
||||||
|
Echte Zielsystem- und Terminalprüfungen gehören in Change 05; Change 06 bindet
|
||||||
|
diese Prüfungen und versions-/prüfsummengebundene Nachweise für die fertigen
|
||||||
|
Pakete in die Releasefreigabe ein. Cross-Builds ersetzen keine Zielausführung.
|
||||||
|
Unter Linux werden weiterhin mindestens zwei Emulatoren geprüft.
|
||||||
|
Change 03 liefert Backend/CLI, das gemeinsame TBL-Testartefakt und die lokal
|
||||||
|
verifizierte Verbraucherprobe; die vollständige Vierzielabnahme folgt in
|
||||||
|
05/06 und die abschließende Phasenabnahme in 07.
|
||||||
Release-Pakete: Windows als `.7z`, macOS und beide Linux-Architekturen
|
Release-Pakete: Windows als `.7z`, macOS und beide Linux-Architekturen
|
||||||
als `.tar.gz`, jeweils mit `tb`, `tbc` und der passenden `tbrt`-Vorlage
|
als `.tar.gz`, jeweils mit `tb`, `tbc` und der passenden `tbrt`-Vorlage
|
||||||
(Windows jeweils `.exe`) samt benötigten Metadaten.
|
(Windows jeweils `.exe`) samt benötigten Metadaten.
|
||||||
|
|||||||
10
README.md
10
README.md
@@ -77,9 +77,13 @@ Die [IDE-Bedienung](docs/ide-bedienung.md) beschreibt Menüs, Fenster,
|
|||||||
Speichern, Optionen und die Exportdialoge einschließlich ihres aktuellen
|
Speichern, Optionen und die Exportdialoge einschließlich ihres aktuellen
|
||||||
Umsetzungsstands.
|
Umsetzungsstands.
|
||||||
|
|
||||||
|
Native Programme lassen sich mit `tbc build --exe` aus vorgebauten Runtime-Vorlagen
|
||||||
|
erzeugen. Aufrufe, Zielvoraussetzungen und Abnahme: [Native Executables](docs/native-executables.md).
|
||||||
|
|
||||||
|
Mit `tbc build --library` entstehen portable `.tbl`-Bibliotheken; `tbc link`
|
||||||
|
verbindet sie mit einem Hauptprojekt zu TBC oder einem Executable.
|
||||||
|
[Bibliotheksformat, Befehle und Abnahme](docs/pcode-bibliotheken.md).
|
||||||
|
|
||||||
## Lizenz
|
## Lizenz
|
||||||
|
|
||||||
MIT — siehe [LICENSE](LICENSE).
|
MIT — siehe [LICENSE](LICENSE).
|
||||||
|
|
||||||
Native Programme lassen sich mit `tbc build --exe` aus vorgebauten Runtime-Vorlagen
|
|
||||||
erzeugen. Aufrufe, Zielvoraussetzungen und Abnahme: [Native Executables](docs/native-executables.md).
|
|
||||||
|
|||||||
@@ -21,11 +21,12 @@ fn main() -> ExitCode {
|
|||||||
match args.first().map(String::as_str) {
|
match args.first().map(String::as_str) {
|
||||||
Some("run") => cmd_run(&args[1..]),
|
Some("run") => cmd_run(&args[1..]),
|
||||||
Some("build") => cmd_build(&args[1..]),
|
Some("build") => cmd_build(&args[1..]),
|
||||||
|
Some("link") => cmd_link(&args[1..]),
|
||||||
Some("check") => cmd_check(&args[1..]),
|
Some("check") => cmd_check(&args[1..]),
|
||||||
Some("convert-frm") => cmd_convert_frm(&args[1..]),
|
Some("convert-frm") => cmd_convert_frm(&args[1..]),
|
||||||
_ => {
|
_ => {
|
||||||
eprintln!(
|
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)
|
ExitCode::from(1)
|
||||||
}
|
}
|
||||||
@@ -79,7 +80,7 @@ fn compile(
|
|||||||
path_arg: Option<&String>,
|
path_arg: Option<&String>,
|
||||||
) -> Result<(PathBuf, tb_vm::bytecode::CompiledModule), ExitCode> {
|
) -> Result<(PathBuf, tb_vm::bytecode::CompiledModule), ExitCode> {
|
||||||
let Some(path) = path_arg else {
|
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));
|
return Err(ExitCode::from(1));
|
||||||
};
|
};
|
||||||
let path = PathBuf::from(path);
|
let path = PathBuf::from(path);
|
||||||
@@ -118,6 +119,10 @@ fn compile(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn cmd_check(args: &[String]) -> ExitCode {
|
fn cmd_check(args: &[String]) -> ExitCode {
|
||||||
|
if args.len() != 1 {
|
||||||
|
eprintln!("Aufruf: tbc check <Projekt>");
|
||||||
|
return ExitCode::from(1);
|
||||||
|
}
|
||||||
match compile(args.first()) {
|
match compile(args.first()) {
|
||||||
Ok(_) => ExitCode::SUCCESS,
|
Ok(_) => ExitCode::SUCCESS,
|
||||||
Err(code) => code,
|
Err(code) => code,
|
||||||
@@ -125,7 +130,13 @@ fn cmd_check(args: &[String]) -> ExitCode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn cmd_build(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) => {
|
Ok(path) => {
|
||||||
println!("{}", path.display());
|
println!("{}", path.display());
|
||||||
ExitCode::SUCCESS
|
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};
|
use anyhow::{bail, ensure, Context};
|
||||||
let mut source = None;
|
let mut inputs = Vec::new();
|
||||||
|
let mut library = false;
|
||||||
let mut exe = false;
|
let mut exe = false;
|
||||||
let mut force = false;
|
let mut force = false;
|
||||||
let mut output = None;
|
let mut output = None;
|
||||||
@@ -148,6 +160,13 @@ fn build(args: &[String]) -> anyhow::Result<PathBuf> {
|
|||||||
let mut args = args.iter();
|
let mut args = args.iter();
|
||||||
while let Some(arg) = args.next() {
|
while let Some(arg) = args.next() {
|
||||||
match arg.as_str() {
|
match arg.as_str() {
|
||||||
|
"--library" => {
|
||||||
|
ensure!(
|
||||||
|
!library && !link,
|
||||||
|
"--library ist nur bei build zulässig und darf nicht doppelt sein"
|
||||||
|
);
|
||||||
|
library = true;
|
||||||
|
}
|
||||||
"--exe" => {
|
"--exe" => {
|
||||||
ensure!(!exe, "Doppeltes --exe");
|
ensure!(!exe, "Doppeltes --exe");
|
||||||
exe = true;
|
exe = true;
|
||||||
@@ -171,30 +190,74 @@ fn build(args: &[String]) -> anyhow::Result<PathBuf> {
|
|||||||
"Doppelte Option {arg}"
|
"Doppelte Option {arg}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
"--" => {
|
"--" => inputs.extend(args.by_ref().cloned()),
|
||||||
for value in args.by_ref() {
|
|
||||||
ensure!(
|
|
||||||
source.replace(value.clone()).is_none(),
|
|
||||||
"Mehrere Quelldateien angegeben"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
value if value.starts_with('-') => bail!("Unbekannte Build-Option: {value}"),
|
value if value.starts_with('-') => bail!("Unbekannte Build-Option: {value}"),
|
||||||
_ => {
|
_ => inputs.push(arg.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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!(
|
ensure!(
|
||||||
source.replace(arg.clone()).is_none(),
|
exe || (target.is_none() && template.is_none()),
|
||||||
"Mehrere Quelldateien angegeben"
|
"--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 source = source.context(
|
};
|
||||||
"Aufruf: tbc build <Quelle> [--exe --target <Triple> --template <tbrt> -o <Ziel> --force]",
|
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(())
|
||||||
|
},
|
||||||
)?;
|
)?;
|
||||||
ensure!(
|
return Ok(out);
|
||||||
exe || (target.is_none() && template.is_none() && output.is_none() && !force),
|
}
|
||||||
"Native Ausgabeoptionen benötigen --exe"
|
|
||||||
);
|
|
||||||
let selected = if exe {
|
let selected = if exe {
|
||||||
Some(match target {
|
Some(match target {
|
||||||
Some(s) => tb_export::Target::parse(&s)?,
|
Some(s) => tb_export::Target::parse(&s)?,
|
||||||
@@ -203,8 +266,14 @@ fn build(args: &[String]) -> anyhow::Result<PathBuf> {
|
|||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
let (path, module) =
|
let module = if let Some(project) = loaded {
|
||||||
compile(Some(&source)).map_err(|_| anyhow::anyhow!("Build fehlgeschlagen"))?;
|
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 {
|
if let Some(target) = selected {
|
||||||
let out = output.map(PathBuf::from).unwrap_or_else(|| {
|
let out = output.map(PathBuf::from).unwrap_or_else(|| {
|
||||||
path.with_extension(if target == tb_export::Target::WindowsAmd64 {
|
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.triple())
|
||||||
.join(target.runtime_name()),
|
.join(target.runtime_name()),
|
||||||
};
|
};
|
||||||
let cancel = export_cancel::ExportCancel::new()?;
|
|
||||||
tb_export::export(
|
tb_export::export(
|
||||||
tb_export::Export {
|
tb_export::Export {
|
||||||
module: &module,
|
module: &module,
|
||||||
@@ -230,14 +298,37 @@ fn build(args: &[String]) -> anyhow::Result<PathBuf> {
|
|||||||
template: &template,
|
template: &template,
|
||||||
output: &out,
|
output: &out,
|
||||||
overwrite: force,
|
overwrite: force,
|
||||||
protected: &[path],
|
protected: &protected,
|
||||||
},
|
},
|
||||||
&|| cancel.cancelled(),
|
&|| cancel.cancelled(),
|
||||||
)?;
|
)?;
|
||||||
Ok(out)
|
Ok(out)
|
||||||
} else {
|
} else {
|
||||||
let out = path.with_extension("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())?;
|
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)
|
Ok(out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -384,3 +384,92 @@ fn native_build_optionen_sind_explizit_und_tbc_bleibt_standard() {
|
|||||||
assert!(!dir.join("program").exists());
|
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};
|
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 CONTAINER_VERSION: u32 = 1;
|
||||||
const MAGIC: &[u8; 8] = b"TBPCODE!";
|
const MAGIC: &[u8; 8] = b"TBPCODE!";
|
||||||
const FOOTER: usize = 48;
|
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());
|
b[at..at + 8].copy_from_slice(&(n as u64).to_le_bytes());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// FNV-1a-64 gegen Übertragungs-/Dateibeschädigung; keine Herkunftsauthentisierung.
|
pub use tb_vm::bytecode::checksum;
|
||||||
pub fn checksum(b: &[u8]) -> u64 {
|
|
||||||
b.iter().fold(0xcbf29ce484222325, |h, v| {
|
|
||||||
(h ^ u64::from(*v)).wrapping_mul(0x100000001b3)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn macho_commands(b: &[u8]) -> Result<Vec<(u32, usize)>> {
|
fn macho_commands(b: &[u8]) -> Result<Vec<(u32, usize)>> {
|
||||||
let end = 32usize
|
let end = 32usize
|
||||||
|
|||||||
@@ -17,6 +17,14 @@ use tb_ui::frm::FormInitial;
|
|||||||
|
|
||||||
pub const TBC_MAGIC: &[u8; 4] = b"TBC\0";
|
pub const TBC_MAGIC: &[u8; 4] = b"TBC\0";
|
||||||
pub const TBC_VERSION: u16 = 4;
|
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).
|
/// Vergleichsoperator (Operand der `Cmp*`-Instruktionen).
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
@@ -78,14 +86,14 @@ impl<'a> Reader<'a> {
|
|||||||
pub fn new(buf: &'a [u8]) -> Self {
|
pub fn new(buf: &'a [u8]) -> Self {
|
||||||
Reader { buf, pos: 0 }
|
Reader { buf, pos: 0 }
|
||||||
}
|
}
|
||||||
fn finish(&self) -> Result<(), LoadError> {
|
pub(crate) fn finish(&self) -> Result<(), LoadError> {
|
||||||
if self.pos != self.buf.len() {
|
if self.pos != self.buf.len() {
|
||||||
Err(LoadError::Corrupt("überzählige Abschnittsdaten"))
|
Err(LoadError::Corrupt("überzählige Abschnittsdaten"))
|
||||||
} else {
|
} else {
|
||||||
Ok(())
|
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) {
|
if n > self.buf.len().saturating_sub(self.pos) {
|
||||||
return Err(LoadError::Corrupt("unerwartetes Dateiende"));
|
return Err(LoadError::Corrupt("unerwartetes Dateiende"));
|
||||||
}
|
}
|
||||||
@@ -93,23 +101,30 @@ impl<'a> Reader<'a> {
|
|||||||
self.pos += n;
|
self.pos += n;
|
||||||
Ok(s)
|
Ok(s)
|
||||||
}
|
}
|
||||||
fn u8(&mut self) -> Result<u8, LoadError> {
|
pub(crate) fn u8(&mut self) -> Result<u8, LoadError> {
|
||||||
Ok(self.take(1)?[0])
|
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()))
|
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()))
|
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 n = self.u32()? as usize;
|
||||||
let b = self.take(n)?;
|
let b = self.take(n)?;
|
||||||
String::from_utf8(b.to_vec()).map_err(|_| LoadError::Corrupt("UTF-8"))
|
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 enc(&self, out: &mut Vec<u8>);
|
||||||
fn dec(r: &mut Reader) -> Result<Self, LoadError>;
|
fn dec(r: &mut Reader) -> Result<Self, LoadError>;
|
||||||
}
|
}
|
||||||
@@ -534,7 +549,7 @@ pub struct CompiledModule {
|
|||||||
pub event_procs: Vec<HEventProc>,
|
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.len() as u32).to_le_bytes());
|
||||||
out.extend_from_slice(s.as_bytes());
|
out.extend_from_slice(s.as_bytes());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -406,18 +406,7 @@ impl Vm {
|
|||||||
.next()
|
.next()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.to_ascii_uppercase();
|
.to_ascii_uppercase();
|
||||||
if !matches!(
|
if !is_runtime_external(&name) {
|
||||||
name.as_str(),
|
|
||||||
"CMNDLGREGISTER"
|
|
||||||
| "CMNDLGCLOSE"
|
|
||||||
| "ABOUT"
|
|
||||||
| "FILEOPEN"
|
|
||||||
| "FILESAVE"
|
|
||||||
| "FILEPRINT"
|
|
||||||
| "FINDTEXT"
|
|
||||||
| "CHANGETEXT"
|
|
||||||
| "COLORPALETTE"
|
|
||||||
) {
|
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
let args = self.stack[self.stack.len().saturating_sub(argc)..].to_vec();
|
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");
|
let cleaned = t.replace(['d', 'D'], "E").replace('e', "E");
|
||||||
cleaned.parse::<f64>().ok()
|
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 codegen;
|
||||||
pub mod interp;
|
pub mod interp;
|
||||||
|
|
||||||
|
pub mod library;
|
||||||
pub mod project;
|
pub mod project;
|
||||||
pub mod project_io;
|
pub mod project_io;
|
||||||
pub use project::compile_project;
|
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::{
|
use crate::{
|
||||||
bytecode::{CompiledModule, Instr},
|
bytecode::{CompiledModule, Instr},
|
||||||
codegen,
|
codegen,
|
||||||
|
library::{Library, LibraryModule, ModuleMetadata},
|
||||||
};
|
};
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use tb_frontend::{
|
use tb_frontend::{
|
||||||
@@ -31,6 +32,7 @@ pub struct ProjectCompiler {
|
|||||||
/// Vollständige Imports im Cache halten, damit spätere Debugkommandos sie binden können.
|
/// Vollständige Imports im Cache halten, damit spätere Debugkommandos sie binden können.
|
||||||
pub debug_symbols: bool,
|
pub debug_symbols: bool,
|
||||||
debug_maps: Vec<DebugMap>,
|
debug_maps: Vec<DebugMap>,
|
||||||
|
debug_names: Vec<String>,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Default, Clone, Copy)]
|
#[derive(Debug, Default, Clone, Copy)]
|
||||||
pub struct CompileStats {
|
pub struct CompileStats {
|
||||||
@@ -91,14 +93,95 @@ impl ProjectCompiler {
|
|||||||
catalog: &FormCatalog,
|
catalog: &FormCatalog,
|
||||||
forms: &[FormFile],
|
forms: &[FormFile],
|
||||||
) -> Result<CompiledModule, Vec<Diagnostic>> {
|
) -> 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();
|
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")]);
|
return Err(vec![diagnostic("Projekt ohne Module oder zu viele Module")]);
|
||||||
}
|
}
|
||||||
let mut sources = Vec::new();
|
let mut sources = Vec::new();
|
||||||
let mut diagnostics = Vec::new();
|
let mut diagnostics = Vec::new();
|
||||||
let mut names = HashSet::new();
|
let mut names = HashSet::new();
|
||||||
let parsed: Vec<_> = units
|
let mut parsed: Vec<_> = units
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(id, unit)| {
|
.map(|(id, unit)| {
|
||||||
@@ -151,7 +234,24 @@ impl ProjectCompiler {
|
|||||||
if !diagnostics.is_empty() {
|
if !diagnostics.is_empty() {
|
||||||
return Err(diagnostics);
|
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();
|
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() {
|
if catalog.find("SCREEN").is_none() {
|
||||||
catalog.add(
|
catalog.add(
|
||||||
"SCREEN",
|
"SCREEN",
|
||||||
@@ -210,7 +310,7 @@ impl ProjectCompiler {
|
|||||||
let mut parts = Vec::new();
|
let mut parts = Vec::new();
|
||||||
let mut commons = Vec::new();
|
let mut commons = Vec::new();
|
||||||
let mut products = 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();
|
let mut key = module.clone();
|
||||||
import_declarations(&mut key, &parsed, &exports, Some(&self.parsed[index].names));
|
import_declarations(&mut key, &parsed, &exports, Some(&self.parsed[index].names));
|
||||||
let mut module = module.clone();
|
let mut module = module.clone();
|
||||||
@@ -229,7 +329,48 @@ impl ProjectCompiler {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
self.stats.compiled += 1;
|
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);
|
diagnostics.extend(errors);
|
||||||
if let Some(hir) = hir {
|
if let Some(hir) = hir {
|
||||||
let code = codegen::compile(&hir);
|
let code = codegen::compile(&hir);
|
||||||
@@ -254,33 +395,81 @@ impl ProjectCompiler {
|
|||||||
&& !products.iter().any(|n| n.module.name == p.module.name)
|
&& !products.iter().any(|n| n.module.name == p.module.name)
|
||||||
});
|
});
|
||||||
self.products.extend(products);
|
self.products.extend(products);
|
||||||
let (mut result, maps) = link(name, parts, &parsed, &commons).map_err(|error| {
|
let mut library = Library::default();
|
||||||
let mut errors = vec![error];
|
for (index, mut code) in parts.into_iter().enumerate() {
|
||||||
locate_diagnostics(&mut errors, &sources);
|
let mut metadata = ModuleMetadata::from_source(
|
||||||
errors
|
&parsed[index],
|
||||||
})?;
|
exports[index].clone(),
|
||||||
self.debug_maps = maps;
|
commons[index].clone(),
|
||||||
result.sources = sources;
|
);
|
||||||
let objects = FormCatalog {
|
// Die Quell-IDs jedes Produkts werden lokal: ein TBL bleibt unabhängig vom Verbraucher.
|
||||||
objects: result.objects.clone(),
|
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
|
||||||
};
|
};
|
||||||
for form in forms {
|
code.sources = local
|
||||||
result
|
.iter()
|
||||||
.form_initial
|
.map(|(_, s)| tb_frontend::source::SourceFile {
|
||||||
.extend(form.initial_values(&objects).map_err(|e| {
|
module: 0,
|
||||||
vec![diagnostic(format!(
|
path: s.path.clone(),
|
||||||
"{}: ungültige Forms-Anfangsdaten ({e})",
|
})
|
||||||
form.root.name
|
.collect();
|
||||||
))]
|
if code.sources.is_empty() {
|
||||||
})?);
|
code.sources.push(tb_frontend::source::SourceFile {
|
||||||
|
module: 0,
|
||||||
|
path: code.name.clone(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
result.startup_form = forms
|
for proc in &mut code.procs {
|
||||||
.first()
|
for instruction in &mut proc.code {
|
||||||
.and_then(|form| objects.find(&form.root.name).map(|(id, _)| id));
|
if let Instr::Source(id, _) = instruction {
|
||||||
result
|
*id = source_id(*id);
|
||||||
.validate()
|
}
|
||||||
.map_err(|e| vec![diagnostic(e.to_string())])?;
|
}
|
||||||
Ok(result)
|
}
|
||||||
|
metadata.pos.source = source_id(metadata.pos.source);
|
||||||
|
for (_, p) in &mut metadata.declarations {
|
||||||
|
p.source = source_id(p.source);
|
||||||
|
}
|
||||||
|
for c in &mut metadata.commons {
|
||||||
|
c.pos.source = source_id(c.pos.source);
|
||||||
|
}
|
||||||
|
for stmt in &mut metadata.exports {
|
||||||
|
match stmt {
|
||||||
|
Stmt::Declare { pos, .. }
|
||||||
|
| Stmt::TypeDecl { pos, .. }
|
||||||
|
| Stmt::ConstDecl { pos, .. } => pos.source = source_id(pos.source),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let objects = FormCatalog {
|
||||||
|
objects: code.objects.clone(),
|
||||||
|
};
|
||||||
|
for form in forms
|
||||||
|
.iter()
|
||||||
|
.filter(|f| f.root.name.eq_ignore_ascii_case(&code.name))
|
||||||
|
{
|
||||||
|
code.form_initial.extend(
|
||||||
|
form.initial_values(&objects)
|
||||||
|
.map_err(|e| vec![diagnostic(e.to_string())])?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
library.modules.push(LibraryModule { metadata, code });
|
||||||
|
}
|
||||||
|
for dependency in libraries {
|
||||||
|
library.modules.extend(dependency.modules.clone());
|
||||||
|
}
|
||||||
|
library.validate().map_err(|e| vec![diagnostic(e)])?;
|
||||||
|
// Auch offene Produkte müssen vorhandene Definitionen und COMMON-Verträge konsistent binden.
|
||||||
|
self.link_product("LIBRARY", library.clone(), None, true)?;
|
||||||
|
Ok(library)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -512,23 +701,6 @@ fn import_declarations(
|
|||||||
module.body = ordered;
|
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 {
|
fn module_pos(module: &Module) -> SourcePos {
|
||||||
module
|
module
|
||||||
.body
|
.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(
|
fn link(
|
||||||
name: &str,
|
name: &str,
|
||||||
mut parts: Vec<CompiledModule>,
|
mut parts: Vec<CompiledModule>,
|
||||||
ast: &[Module],
|
ast: &[ModuleMetadata],
|
||||||
module_commons: &[Vec<tb_frontend::hir::HCommon>],
|
allow_open: bool,
|
||||||
) -> Result<(CompiledModule, Vec<DebugMap>), Diagnostic> {
|
) -> Result<(CompiledModule, Vec<DebugMap>), Diagnostic> {
|
||||||
let at = |pos, message| Diagnostic {
|
let at = |pos, message| Diagnostic {
|
||||||
file: None,
|
file: None,
|
||||||
@@ -566,7 +766,8 @@ fn link(
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|p| (p.name.clone(), p.option_base))
|
.map(|p| (p.name.clone(), p.option_base))
|
||||||
.collect();
|
.collect();
|
||||||
result.objects = parts[0].objects.clone();
|
result.objects.clear();
|
||||||
|
result.sources.clear();
|
||||||
result.strings.clear();
|
result.strings.clear();
|
||||||
result.procs.clear();
|
result.procs.clear();
|
||||||
result.option_base = parts[0].option_base;
|
result.option_base = parts[0].option_base;
|
||||||
@@ -574,11 +775,14 @@ fn link(
|
|||||||
let mut count = 1usize;
|
let mut count = 1usize;
|
||||||
let mut definitions: HashMap<String, Vec<u16>> = HashMap::new();
|
let mut definitions: HashMap<String, Vec<u16>> = HashMap::new();
|
||||||
for (part, module) in parts.iter().zip(ast) {
|
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];
|
let mut map = vec![0];
|
||||||
for p in part.procs.iter().skip(1) {
|
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)
|
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;
|
count += 1;
|
||||||
map.push(id);
|
map.push(id);
|
||||||
if defined.contains(p.name.as_str()) || p.kind == HProcKind::DefFn {
|
if defined.contains(p.name.as_str()) || p.kind == HProcKind::DefFn {
|
||||||
@@ -587,23 +791,44 @@ fn link(
|
|||||||
}
|
}
|
||||||
proc_maps.push(map);
|
proc_maps.push(map);
|
||||||
}
|
}
|
||||||
|
let mut unused_imports = HashSet::new();
|
||||||
// DECLARE-Platzhalter auf die tatsächliche, eindeutig bestimmte Definition binden.
|
// DECLARE-Platzhalter auf die tatsächliche, eindeutig bestimmte Definition binden.
|
||||||
for (module_id, part) in parts.iter().enumerate() {
|
for (module_id, part) in parts.iter().enumerate() {
|
||||||
let defined: HashSet<_> = ast[module_id]
|
let defined: HashSet<_> = ast[module_id].defined.iter().map(String::as_str).collect();
|
||||||
.procs
|
|
||||||
.iter()
|
|
||||||
.map(|p| p.sig.name.as_str())
|
|
||||||
.collect();
|
|
||||||
for (id, proc) in part.procs.iter().enumerate().skip(1) {
|
for (id, proc) in part.procs.iter().enumerate().skip(1) {
|
||||||
if !defined.contains(proc.name.as_str()) && proc.kind != HProcKind::DefFn {
|
if !defined.contains(proc.name.as_str()) && proc.kind != HProcKind::DefFn {
|
||||||
if let Some(candidates) = definitions.get(&proc.name) {
|
if let Some(candidates) = definitions.get(&proc.name) {
|
||||||
if candidates.len() != 1 {
|
if candidates.len() != 1 {
|
||||||
return Err(at(
|
return Err(at(
|
||||||
proc_pos(&ast[module_id], &proc.name),
|
ast[module_id].proc_pos(&proc.name),
|
||||||
format!("Ambiguous subprogram: {}", 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];
|
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);
|
body_starts.push(pc);
|
||||||
pc += part.procs[0].code.len() - split - 1;
|
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();
|
let mut main = parts[0].procs[0].clone();
|
||||||
main.code.clear();
|
main.code.clear();
|
||||||
main.name = name.into();
|
main.name = name.into();
|
||||||
@@ -637,6 +865,35 @@ fn link(
|
|||||||
let mut debug_maps = Vec::new();
|
let mut debug_maps = Vec::new();
|
||||||
let mut common: HashMap<_, (u16, tb_frontend::hir::HCommon)> = HashMap::new();
|
let mut common: HashMap<_, (u16, tb_frontend::hir::HCommon)> = HashMap::new();
|
||||||
for (module_id, part) in parts.iter_mut().enumerate() {
|
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();
|
let mut types = Vec::new();
|
||||||
for udt in &part.udts {
|
for udt in &part.udts {
|
||||||
let mut udt = udt.clone();
|
let mut udt = udt.clone();
|
||||||
@@ -652,15 +909,14 @@ fn link(
|
|||||||
result.udts.push(udt);
|
result.udts.push(udt);
|
||||||
result.udts.len() - 1
|
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(
|
types.push(
|
||||||
u16::try_from(id)
|
u16::try_from(id).map_err(|_| at(ast[module_id].pos, "Zu viele TYPEs".into()))?,
|
||||||
.map_err(|_| at(module_pos(&ast[module_id]), "Zu viele TYPEs".into()))?,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let commons: HashMap<_, _> = module_commons[module_id]
|
let commons: HashMap<_, _> = ast[module_id].commons.iter().map(|c| (c.slot, c)).collect();
|
||||||
.iter()
|
|
||||||
.map(|c| (c.slot, c))
|
|
||||||
.collect();
|
|
||||||
let mut globals = Vec::new();
|
let mut globals = Vec::new();
|
||||||
for (slot, (ty, name)) in part.globals_init.iter().zip(&part.global_names).enumerate() {
|
for (slot, (ty, name)) in part.globals_init.iter().zip(&part.global_names).enumerate() {
|
||||||
let mut ty = ty.clone();
|
let mut ty = ty.clone();
|
||||||
@@ -704,12 +960,8 @@ fn link(
|
|||||||
}
|
}
|
||||||
*id
|
*id
|
||||||
} else {
|
} else {
|
||||||
let id = u16::try_from(result.globals_init.len()).map_err(|_| {
|
let id = u16::try_from(result.globals_init.len())
|
||||||
at(
|
.map_err(|_| at(ast[module_id].pos, "Zu viele globale Variablen".into()))?;
|
||||||
module_pos(&ast[module_id]),
|
|
||||||
"Zu viele globale Variablen".into(),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
result.globals_init.push(ty);
|
result.globals_init.push(ty);
|
||||||
result.global_names.push(if ast.len() == 1 {
|
result.global_names.push(if ast.len() == 1 {
|
||||||
name.clone()
|
name.clone()
|
||||||
@@ -732,20 +984,23 @@ fn link(
|
|||||||
});
|
});
|
||||||
let string_offset = result.strings.len();
|
let string_offset = result.strings.len();
|
||||||
if string_offset + part.strings.len() >= u16::MAX as usize {
|
if string_offset + part.strings.len() >= u16::MAX as usize {
|
||||||
return Err(at(
|
return Err(at(ast[module_id].pos, "Zu viele Stringkonstanten".into()));
|
||||||
module_pos(&ast[module_id]),
|
|
||||||
"Zu viele Stringkonstanten".into(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
result.strings.append(&mut part.strings);
|
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);
|
result.data.append(&mut part.data);
|
||||||
let jump_offset = result.jump_tables.len();
|
let jump_offset = result.jump_tables.len();
|
||||||
if jump_offset + part.jump_tables.len() > u16::MAX as usize {
|
if jump_offset + part.jump_tables.len() > u16::MAX as usize {
|
||||||
return Err(at(
|
return Err(at(ast[module_id].pos, "Zu viele Sprungtabellen".into()));
|
||||||
module_pos(&ast[module_id]),
|
|
||||||
"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;
|
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() {
|
for (proc_id, proc) in part.procs.iter_mut().enumerate() {
|
||||||
@@ -765,6 +1020,15 @@ fn link(
|
|||||||
for instruction in &mut proc.code {
|
for instruction in &mut proc.code {
|
||||||
use Instr::*;
|
use Instr::*;
|
||||||
match instruction {
|
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)
|
PushStr(id)
|
||||||
| Unsupported(id)
|
| Unsupported(id)
|
||||||
| LoadDynamicObjectProperty(id)
|
| LoadDynamicObjectProperty(id)
|
||||||
@@ -818,6 +1082,7 @@ fn link(
|
|||||||
result.procs.extend(part.procs.iter().skip(1).cloned());
|
result.procs.extend(part.procs.iter().skip(1).cloned());
|
||||||
for mut e in part.event_procs.drain(..) {
|
for mut e in part.event_procs.drain(..) {
|
||||||
e.proc = proc_maps[module_id][e.proc as usize];
|
e.proc = proc_maps[module_id][e.proc as usize];
|
||||||
|
e.object = objects[e.object as usize];
|
||||||
result.event_procs.push(e);
|
result.event_procs.push(e);
|
||||||
}
|
}
|
||||||
result.jump_tables.append(&mut part.jump_tables);
|
result.jump_tables.append(&mut part.jump_tables);
|
||||||
@@ -829,6 +1094,9 @@ fn link(
|
|||||||
// Vollständige Signaturen zwischen DECLARE-Platzhalter und Ziel vergleichen.
|
// Vollständige Signaturen zwischen DECLARE-Platzhalter und Ziel vergleichen.
|
||||||
for (module, part) in parts.iter().enumerate() {
|
for (module, part) in parts.iter().enumerate() {
|
||||||
for (id, proc) in part.procs.iter().enumerate().skip(1) {
|
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];
|
let target = &result.procs[proc_maps[module][id] as usize];
|
||||||
if proc.ret_ty != target.ret_ty
|
if proc.ret_ty != target.ret_ty
|
||||||
|| proc.kind != target.kind
|
|| 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)
|
.any(|(a, b)| a.ty != b.ty || a.array != b.array || a.by_ref != b.by_ref)
|
||||||
{
|
{
|
||||||
return Err(at(
|
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),
|
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))
|
Ok((result, debug_maps))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -857,7 +1160,7 @@ type DebugCompileFn = fn(&DebugCompiler, u16, &str, &str, bool) -> Result<DebugC
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct DebugCompiler {
|
pub struct DebugCompiler {
|
||||||
compile_fn: DebugCompileFn,
|
compile_fn: DebugCompileFn,
|
||||||
modules: Vec<(Module, FormCatalog, DebugMap)>,
|
modules: Vec<(u16, Module, FormCatalog, DebugMap)>,
|
||||||
symbols: tb_frontend::sema::DebugSymbols,
|
symbols: tb_frontend::sema::DebugSymbols,
|
||||||
slots: Vec<u16>,
|
slots: Vec<u16>,
|
||||||
error: Option<String>,
|
error: Option<String>,
|
||||||
@@ -877,20 +1180,28 @@ pub struct DebugCode {
|
|||||||
impl ProjectCompiler {
|
impl ProjectCompiler {
|
||||||
pub fn debug_compiler(&self) -> DebugCompiler {
|
pub fn debug_compiler(&self) -> DebugCompiler {
|
||||||
let modules: Vec<_> = self
|
let modules: Vec<_> = self
|
||||||
.parsed
|
.debug_names
|
||||||
.iter()
|
.iter()
|
||||||
.zip(&self.debug_maps)
|
.zip(&self.debug_maps)
|
||||||
.filter_map(|(parsed, map)| {
|
.enumerate()
|
||||||
|
.filter_map(|(id, (name, map))| {
|
||||||
self.products
|
self.products
|
||||||
.iter()
|
.iter()
|
||||||
.find(|p| p.module.name == parsed.module.name)
|
.find(|p| p.module.name == *name)
|
||||||
.map(|p| (p.debug_module.clone(), p.catalog.clone(), map.clone()))
|
.map(|p| {
|
||||||
|
(
|
||||||
|
id as u16,
|
||||||
|
p.debug_module.clone(),
|
||||||
|
p.catalog.clone(),
|
||||||
|
map.clone(),
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let mut symbols = tb_frontend::sema::DebugSymbols::default();
|
let mut symbols = tb_frontend::sema::DebugSymbols::default();
|
||||||
let mut slots = Vec::new();
|
let mut slots = Vec::new();
|
||||||
let mut error = None;
|
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 let Some(hir) = tb_frontend::sema::lower_with_forms(ast, catalog).0 {
|
||||||
if symbols.udts.len() + hir.udts.len() >= u16::MAX as usize
|
if symbols.udts.len() + hir.udts.len() >= u16::MAX as usize
|
||||||
|| symbols.globals.len() + hir.globals.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 {
|
if let Some(error) = &self.error {
|
||||||
return Err(error.clone());
|
return Err(error.clone());
|
||||||
}
|
}
|
||||||
let (ast, catalog, map) = self
|
let (_, ast, catalog, map) = self
|
||||||
.modules
|
.modules
|
||||||
.get(module as usize)
|
.iter()
|
||||||
|
.find(|(id, _, _, _)| *id == module)
|
||||||
.ok_or("Kein Debug-Quellkontext für dieses Kompilat")?;
|
.ok_or("Kein Debug-Quellkontext für dieses Kompilat")?;
|
||||||
let name = if procedure == "<main>" {
|
let name = if procedure == "<main>" {
|
||||||
&ast.name
|
&ast.name
|
||||||
@@ -976,7 +1288,7 @@ impl DebugCompiler {
|
|||||||
let mut globals = map.globals.clone();
|
let mut globals = map.globals.clone();
|
||||||
globals.extend(&self.slots);
|
globals.extend(&self.slots);
|
||||||
let mut types = map.types.clone();
|
let mut types = map.types.clone();
|
||||||
for (_, _, mapping) in &self.modules {
|
for (_, _, _, mapping) in &self.modules {
|
||||||
types.extend(&mapping.types);
|
types.extend(&mapping.types);
|
||||||
}
|
}
|
||||||
let mut compiled = codegen::compile(&hir);
|
let mut compiled = codegen::compile(&hir);
|
||||||
@@ -1002,7 +1314,14 @@ impl DebugCompiler {
|
|||||||
}
|
}
|
||||||
remap_type(ty, &types);
|
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],
|
PushUdtId(id) => *id = types[*id as usize],
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -1023,7 +1342,7 @@ impl DebugCompiler {
|
|||||||
from: SourcePos,
|
from: SourcePos,
|
||||||
to: SourcePos,
|
to: SourcePos,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
let Some((ast, _, _)) = self.modules.get(module as usize) else {
|
let Some((_, ast, _, _)) = self.modules.iter().find(|(id, _, _, _)| *id == module) else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
let body = if procedure == "<main>" {
|
let body = if procedure == "<main>" {
|
||||||
|
|||||||
@@ -71,6 +71,9 @@ pub struct ReadDocument {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_document(path: &Path) -> Result<ReadDocument, String> {
|
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 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 binary = has_extension(path, "frm") && bytes.starts_with(&[0xfc, 0x08, 1, 0]);
|
||||||
let mut warnings = Vec::new();
|
let mut warnings = Vec::new();
|
||||||
@@ -146,8 +149,11 @@ impl Manifest {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let member = loader.resolve(base, s).map_err(|e| fail(&e))?;
|
let member = loader.resolve(base, s).map_err(|e| fail(&e))?;
|
||||||
if !has_extension(&member, "bas") && !has_extension(&member, "frm") {
|
if !has_extension(&member, "bas")
|
||||||
return Err(fail("Projektmitglied muss BAS oder FRM sein"));
|
&& !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()))?;
|
let key = identity(&member).map_err(|e| fail(&e.to_string()))?;
|
||||||
if out
|
if out
|
||||||
@@ -172,6 +178,9 @@ impl Manifest {
|
|||||||
start.display()
|
start.display()
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
if has_extension(&chosen, "tbl") {
|
||||||
|
return Err(format!("{}: TBL kann kein $STARTUP sein", path.display()));
|
||||||
|
}
|
||||||
out.startup = Some(chosen);
|
out.startup = Some(chosen);
|
||||||
}
|
}
|
||||||
Ok(out)
|
Ok(out)
|
||||||
@@ -252,10 +261,18 @@ pub struct SourceLoader {
|
|||||||
pub include_paths: Vec<PathBuf>,
|
pub include_paths: Vec<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum BuildMember {
|
||||||
|
Source(usize),
|
||||||
|
Library(usize),
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ProjectSources {
|
pub struct ProjectSources {
|
||||||
pub manifest: Manifest,
|
pub manifest: Manifest,
|
||||||
pub units: Vec<SourceUnit>,
|
pub units: Vec<SourceUnit>,
|
||||||
|
pub libraries: Vec<PathBuf>,
|
||||||
|
pub order: Vec<BuildMember>,
|
||||||
pub forms: Vec<FormFile>,
|
pub forms: Vec<FormFile>,
|
||||||
}
|
}
|
||||||
impl ProjectSources {
|
impl ProjectSources {
|
||||||
@@ -265,21 +282,108 @@ impl ProjectSources {
|
|||||||
compiler: &mut crate::project::ProjectCompiler,
|
compiler: &mut crate::project::ProjectCompiler,
|
||||||
name: &str,
|
name: &str,
|
||||||
) -> Result<crate::bytecode::CompiledModule, Vec<tb_frontend::Diagnostic>> {
|
) -> Result<crate::bytecode::CompiledModule, Vec<tb_frontend::Diagnostic>> {
|
||||||
let mut catalog = tb_frontend::forms::FormCatalog::default();
|
if self.units.is_empty() {
|
||||||
for form in &self.forms {
|
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()}]);
|
||||||
catalog.append(&form.catalog());
|
|
||||||
}
|
}
|
||||||
let mut module = compiler.compile(name, &self.units, &catalog, &self.forms)?;
|
let library = self.compile_library(compiler)?;
|
||||||
if self
|
let startup = if self
|
||||||
.manifest
|
.manifest
|
||||||
.startup
|
.startup
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.is_some_and(|p| !has_extension(p, "frm"))
|
.is_some_and(|p| !has_extension(p, "frm"))
|
||||||
{
|
{
|
||||||
module.startup_form = None;
|
None
|
||||||
|
} else {
|
||||||
|
self.forms.first().map(|f| f.root.name.as_str())
|
||||||
|
};
|
||||||
|
compiler.link_library(name, library, startup)
|
||||||
}
|
}
|
||||||
Ok(module)
|
/// 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.
|
/// 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)?
|
Manifest::parse(&path, &self.read(&path)?.text(), self)?
|
||||||
} else {
|
} else {
|
||||||
Manifest {
|
Manifest {
|
||||||
lines: vec![ProjectLine::File(path)],
|
lines: vec![ProjectLine::File(path.clone())],
|
||||||
startup: None,
|
startup: None,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
self.load_manifest(manifest)
|
self.load_manifest(manifest)
|
||||||
|
.map_err(|e| format!("{}: {e}", path.display()))
|
||||||
}
|
}
|
||||||
pub fn load_manifest(&self, manifest: Manifest) -> Result<ProjectSources, String> {
|
pub fn load_manifest(&self, manifest: Manifest) -> Result<ProjectSources, String> {
|
||||||
let mut units = Vec::new();
|
let mut units = Vec::new();
|
||||||
let mut forms = Vec::new();
|
let mut forms = Vec::new();
|
||||||
|
let mut libraries = Vec::new();
|
||||||
|
let mut order = Vec::new();
|
||||||
let mut members: Vec<_> = manifest.members().collect();
|
let mut members: Vec<_> = manifest.members().collect();
|
||||||
if let Some(startup) = &manifest.startup {
|
if let Some(startup) = &manifest.startup {
|
||||||
let index = members
|
let index = members
|
||||||
@@ -448,7 +555,23 @@ impl SourceLoader {
|
|||||||
let selected = members.remove(index);
|
let selected = members.remove(index);
|
||||||
members.insert(0, selected);
|
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 {
|
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 content = self.read(path)?;
|
||||||
let (name, code, first_line) = match content {
|
let (name, code, first_line) = match content {
|
||||||
Content::Text(text) => (module_name(path), text, 1),
|
Content::Text(text) => (module_name(path), text, 1),
|
||||||
@@ -469,6 +592,8 @@ impl SourceLoader {
|
|||||||
}
|
}
|
||||||
Ok(ProjectSources {
|
Ok(ProjectSources {
|
||||||
manifest,
|
manifest,
|
||||||
|
libraries,
|
||||||
|
order,
|
||||||
units,
|
units,
|
||||||
forms,
|
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();
|
let found = tb_vm::project_io::relative_case_insensitive(&base, "../SIBLING.BAS").unwrap();
|
||||||
assert_eq!(identity(&found).unwrap(), identity(&sibling).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}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
159
docs/pcode-bibliotheken.md
Normal file
159
docs/pcode-bibliotheken.md
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
# P-Code-Bibliotheken und Linker
|
||||||
|
|
||||||
|
Eine `.tbl` enthält übersetzte BASIC-Module mit Deklarationen, typisierten
|
||||||
|
Imports, COMMON-Verträgen, DATA, Forms-Anfangsdaten und Quellpositionen. Sie
|
||||||
|
enthält weder BASIC-Programmrümpfe als Quelltext noch native Runtime oder
|
||||||
|
Maschinencode. Dieselben Bytes sind für Windows amd64, macOS arm64 und Linux
|
||||||
|
amd64/arm64 vorgesehen. Der Verbraucher braucht die Bibliotheksquellen nicht.
|
||||||
|
|
||||||
|
## Befehle
|
||||||
|
|
||||||
|
```text
|
||||||
|
tbc build library.mak --library --output library.tbl
|
||||||
|
tbc link app.mak library.tbl --output app.tbc
|
||||||
|
tbc link app.mak library.tbl --exe --target aarch64-apple-darwin --output app
|
||||||
|
```
|
||||||
|
|
||||||
|
`build --library` akzeptiert BAS, FRM oder MAK sowie zusätzliche TBL-Eingaben.
|
||||||
|
Ohne `--output` entsteht eine gleichnamige `.tbl`. Alle angegebenen Module,
|
||||||
|
einschließlich eingebundener Bibliotheken, werden in Reihenfolge übernommen;
|
||||||
|
Abhängigkeiten werden flach mitgegeben, ohne automatische Paketsuche.
|
||||||
|
|
||||||
|
`link` benötigt ein BASIC-/Form-Hauptmodul oder ein entsprechendes MAK-Projekt.
|
||||||
|
Zusätzliche Argumente müssen TBL-Dateien sein. Ohne `--exe` entsteht ein
|
||||||
|
vollständig verknüpftes `.tbc`, standardmäßig neben dem Hauptprojekt. Ein
|
||||||
|
fertiges TBC kann ausschließlich mit `tbc link app.tbc --exe` verpackt werden;
|
||||||
|
weitere TBL-Dateien sind dabei unzulässig, da das TBC bereits aufgelöst ist.
|
||||||
|
|
||||||
|
`--library` und `--exe` schließen sich aus. `--target` und `--template` sind
|
||||||
|
EXE-Optionen. TBL-/Link-/EXE-Ausgaben überschreiben vorhandene Ziele nur mit
|
||||||
|
`--force`. Eingaben, Includes, MAK- und TBL-Dateien bleiben auch dann geschützt.
|
||||||
|
`build app.mak`, `run app.mak` und `check app.mak` verwenden denselben Resolver.
|
||||||
|
Beim erneuten `build` eines MAK mit TBL-Mitgliedern ist ebenfalls `--force`
|
||||||
|
zum Ersetzen nötig. Der bisherige Aufruf `build source.bas` bleibt eine
|
||||||
|
TBC-Übersetzung; reine Quellprojekte behalten ihre Standardausgabe.
|
||||||
|
|
||||||
|
Für TBL und TBC genügen die ausgelieferten TB-Werkzeuge. Ein C-/Rust-Compiler
|
||||||
|
oder nativer Linker wird beim Anwender nicht aufgerufen. Für EXE gilt der
|
||||||
|
[Runtime-Vorlagenvertrag](native-executables.md): passende vorgebaute
|
||||||
|
`runtimes/<Triple>/tbrt[.exe]` mit Metadaten oder ausdrücklich `--template`;
|
||||||
|
auf macOS die dort beschriebene System-Signierung. Eine fehlende, falsche oder
|
||||||
|
inkompatible Vorlage liefert eine Diagnose und kein fertiges Executable.
|
||||||
|
|
||||||
|
## Projektmitglieder, Auflösung und Initialisierung
|
||||||
|
|
||||||
|
Beispiel `app.mak`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
' $STARTUP: "main.bas"
|
||||||
|
main.bas
|
||||||
|
lib/common.tbl
|
||||||
|
last.bas
|
||||||
|
```
|
||||||
|
|
||||||
|
MAK-Pfade sind relativ zur MAK-Datei und werden wie bisher ohne Beachtung der
|
||||||
|
Groß-/Kleinschreibung aufgelöst. Eine TBL wird an ihrer Position in ihre
|
||||||
|
Modulreihenfolge aufgefächert. Zusätzliche CLI-Bibliotheken folgen danach.
|
||||||
|
Das gewählte Quell-Startmodul steht zuerst; ohne explizite Wahl ist es das
|
||||||
|
erste Quellmitglied. Eine TBL ist weder Textdokument noch Startdatei. Die
|
||||||
|
Form-Startauswahl gehört dem Verbraucher; eine Bibliothek speichert keine
|
||||||
|
aktive Startform. Reine Forms-Projekte behalten ihren bisherigen Startvertrag.
|
||||||
|
|
||||||
|
Alle globalen Anfangswerte und Array-Initialisierungen stehen vor den
|
||||||
|
Modulrümpfen bereit. Die Rümpfe folgen der erweiterten Projektreihenfolge mit
|
||||||
|
dem normalen BASIC-Kontrollfluss: ein `END` im Hauptmodul beendet auch das
|
||||||
|
Gesamtprogramm. Ein Prozeduraufruf führt keinen zusätzlichen Modulrumpf aus.
|
||||||
|
`RUN` ohne Ziel erzeugt die gesamte Runtime einschließlich Bibliotheksvariablen,
|
||||||
|
DATA-Zeiger, Forms und Textbildschirm erneut. Gespeicherte physische Quellorte
|
||||||
|
und BASIC-Zeilenlabels bleiben für Laufzeitfehler verfügbar.
|
||||||
|
|
||||||
|
Die vorhandene Sichtbarkeit gilt: lokale Definitionen gewinnen, fremde
|
||||||
|
Prozeduren müssen eindeutig sein, Konstanten und TYPE-Deklarationen werden
|
||||||
|
vor der Verbraucher-Semantikanalyse importiert. Prozedurparameter und
|
||||||
|
Ergebnisse werden nach dem UDT-Remapping geprüft; COMMON wird nur bei
|
||||||
|
kompatiblen Typen und Array-Grenzen geteilt. Offene `DECLARE`-Imports dürfen in
|
||||||
|
einer TBL verbleiben, müssen aber im ausführbaren Endprodukt aufgelöst sein.
|
||||||
|
Nicht aufgerufene reine DECLARE-Prototypen sind keine Imports und werden aus
|
||||||
|
dem Endprodukt entfernt; Aufrufe in mitgelieferten Prozedurrümpfen müssen auch
|
||||||
|
dann auflösbar sein, wenn diese Prozedur noch nicht aufgerufen wird. Die bereits
|
||||||
|
vorhandenen nativen Runtime-Dialogfunktionen bleiben Runtime-Imports.
|
||||||
|
Doppelte Modulidentitäten, etwa durch überlappende flache Bibliotheken, sind
|
||||||
|
Fehler. Bibliotheksdateien werden bei jedem Build nach ihrem tatsächlichen
|
||||||
|
Inhalt gelesen; gleiche Dateigröße oder Zeitstempel können alte Deklarationen
|
||||||
|
oder alten Code nicht im Ergebnis festhalten.
|
||||||
|
|
||||||
|
## TBL-Version 1
|
||||||
|
|
||||||
|
Alle Ganzzahlen sind Little Endian und haben feste Breiten; Zeichenketten sind
|
||||||
|
UTF-8 mit `u32`-Byteanzahl. Es werden keine Rust-Structlayouts serialisiert.
|
||||||
|
|
||||||
|
| Feld | Darstellung |
|
||||||
|
|---|---|
|
||||||
|
| Magic | vier Bytes `TBL\0` |
|
||||||
|
| TBL-Version | `u16`, aktuell 1 |
|
||||||
|
| P-Code-Version | `u16`, aktuell TBC 4 |
|
||||||
|
| Runtime-Vertragsversion | `u32`, aktuell 1; gemeinsam mit EXE-Vorlagen |
|
||||||
|
| Modulanzahl | `u32`, 1 bis 65535 |
|
||||||
|
| Module | je `u32`-Abschnittslänge und Abschnittsbytes |
|
||||||
|
| Prüfsumme | `u64` FNV-1a über alle vorherigen Bytes; erkennt Beschädigung, authentisiert keine Herkunft |
|
||||||
|
|
||||||
|
Ein Modulabschnitt enthält in dieser Reihenfolge:
|
||||||
|
|
||||||
|
1. Modulname und Quellposition (`source`, `line`, `column`, jeweils `u32`).
|
||||||
|
2. Exportdeklarationen mit `u32`-Anzahl: Prozedur (Tag 0), TYPE (Tag 1),
|
||||||
|
aufgelöste Konstante (Tag 2). Jede Deklaration hat eine Quellposition.
|
||||||
|
Prozeduren speichern Art, Name, Suffix und Parameter mit Name, Suffix,
|
||||||
|
Array-Markierung und optionalem Typ. TYPE speichert Namen und geordnete
|
||||||
|
Felder. Konstanten speichern Name, Suffix und gefaltetes `f64` oder String.
|
||||||
|
3. Deklarationsorte (Name/Position) und Namen tatsächlich definierter
|
||||||
|
Prozeduren, jeweils mit `u32`-Anzahl. Die übrigen typisierten Prozedurzeilen
|
||||||
|
im Modulprodukt sind Imports. Es werden keine vollständigen ASTs gespeichert.
|
||||||
|
4. COMMON-Zeilen mit Slot (`u16`), optionalem Blocknamen, Schlüssel,
|
||||||
|
HIR-Typ, Position und optionalen Array-Grenzen (`i32`, einzeln optional).
|
||||||
|
5. `u32`-Länge und Bytecode-Produkt mit dem bestehenden TBC-Codec. Dieses
|
||||||
|
Produkt ist **unverknüpft** und auf genau ein Modul begrenzt: eigene
|
||||||
|
Quell-IDs, Typen, Globals, Prozeduren, Strings, DATA, Sprungtabellen,
|
||||||
|
Objektkatalog, Ereignisse und eigene Forms-Anfangswerte. Es ist kein
|
||||||
|
umbenanntes fertiges Projekt-TBC.
|
||||||
|
|
||||||
|
Optionen und Booleans sind strikt 0/1; Suffixe sind 0 oder das ASCII-Zeichen
|
||||||
|
`%`, `&`, `!`, `#`, `$`, `@`. Typ-Tags 0–9 stehen für INTEGER, LONG, SINGLE,
|
||||||
|
DOUBLE, CURRENCY, STRING, festen STRING (`i64` Länge), FORM, CONTROL und TYPE
|
||||||
|
(UTF-8-Name). Konstantenwert-Tags sind 0 (`f64`) und 1 (String).
|
||||||
|
|
||||||
|
Der Leser prüft Prüfsumme, Versionen, Abschnittsgrenzen, Restbytes, Tags,
|
||||||
|
Tabellen- und Symbolreferenzen, Export-/Bytecode-Signaturen, COMMON sowie
|
||||||
|
Forms-Anfangsdaten vor der Weitergabe. Der Linker prüft auch die vereinigten
|
||||||
|
Tabellengrenzen und remappt alle Typ-, Global-, Prozedur-, String-, DATA-,
|
||||||
|
Sprung-, Objekt-, Ereignis- und Quellreferenzen. Veröffentlichungen verwenden
|
||||||
|
die gemeinsame temporäre Datei mit erneuter Validierung, Abbruchprüfung und
|
||||||
|
atomarer Freigabe. Fehler erhalten vorhandene Ziele und Eingaben.
|
||||||
|
|
||||||
|
## Reproduzierbare Abnahme und Übergabe
|
||||||
|
|
||||||
|
`tests/libraries/portable.tbl` ist das gemeinsame unveränderte TBL-Artefakt.
|
||||||
|
Seine SHA-256 lautet
|
||||||
|
`5d25c2f8c95ae535e55a6c84c1ddd0d964358863f4f3c780c50ac4a2eb3594df`.
|
||||||
|
`portable.bas` dokumentiert den einmaligen Ursprung; die Verbraucherprobe
|
||||||
|
liest oder übersetzt diese Quelle nicht.
|
||||||
|
|
||||||
|
```text
|
||||||
|
python3 tests/support/library-abnahme.py --bin-dir target/release
|
||||||
|
```
|
||||||
|
|
||||||
|
Auf jedem echten Ziel werden dieselben TBL-Bytes geprüft, ein separater
|
||||||
|
Verbraucher zu TBC und EXE verknüpft und nach Entfernen von Quellen, TBL, TBC
|
||||||
|
und Runtime-Vorlage das Executable mit leerem PATH ausgeführt. Resultate,
|
||||||
|
Dateieffekt und Neustart durch `RUN` werden geprüft. Die JSON-Ausgabe nennt
|
||||||
|
Host-Target, Prüfsumme und Ergebnis. Ein Cross-Compile ersetzt diesen Lauf
|
||||||
|
nicht. Die vier JSON-Nachweise gehören in die Plattform-/Gitea-Abnahme.
|
||||||
|
|
||||||
|
IDE-Change 04 kann `ProjectSources::compile_library`, `compile` und
|
||||||
|
`protected_inputs` sowie `ProjectCompiler::link_library` nutzen. Er ergänzt
|
||||||
|
das IDE-Dokumentmodell für nicht editierbare TBL-Mitglieder und verbindet
|
||||||
|
Make Library/Make EXE mit derselben Veröffentlichung; die UI ist Teil von 04.
|
||||||
|
Change 05 führt den identischen Verbrauchercheck auf allen vier echten
|
||||||
|
Zielsystemen aus. Change 06 baut die vier nativen Compiler-/Runtime-Pakete
|
||||||
|
zentral auf dem Linux-arm64-Gitea-Runner per Cross-Kompilierung und bindet
|
||||||
|
die echten Verbraucherprüfungen für die entpackten Pakete an Revision und
|
||||||
|
Paketprüfsumme. Vier native Buildrunner sind dafür nicht erforderlich.
|
||||||
@@ -29,4 +29,4 @@
|
|||||||
|
|
||||||
## Migration Plan
|
## Migration Plan
|
||||||
|
|
||||||
Additive Dateiendung, MAK-Mitgliedsart und CLI-Befehle; reine Quellprojekte und vorhandenes vollständiges TBC behalten ihr Verhalten. Versionen bei inkompatiblen Änderungen explizit prüfen und Neubau der betroffenen Bibliothek verlangen. Keine Migration historischer DOS-LIB/QLB. IDE-Anbindung in 04, identische TBL auf allen Zielrunnern in 06, Benutzerweg in 07. Der verworfene native Bibliotheksentwurf war nicht implementiert und erfordert keine Produktdatenmigration.
|
Additive Dateiendung, MAK-Mitgliedsart und CLI-Befehle; reine Quellprojekte und vorhandenes vollständiges TBC behalten ihr Verhalten. Versionen bei inkompatiblen Änderungen explizit prüfen und Neubau der betroffenen Bibliothek verlangen. Keine Migration historischer DOS-LIB/QLB. 03 wird mit Backend-/CLI-Regressionen, festem TBL-Artefakt samt Prüfsumme und Verbraucherprobe auf dem verfügbaren Host abgenommen. IDE-Anbindung in 04, echte Ausführung derselben TBL-Bytes auf allen vier Zielsystemen in 05, zentraler Cross-Paketbau und paketgebundene Zielnachweise in 06, Benutzerweg in 07. Diese Aufteilung ändert den Portabilitätsvertrag nicht; fehlende Zielausführungen bleiben dort offen. Der verworfene native Bibliotheksentwurf war nicht implementiert und erfordert keine Produktdatenmigration.
|
||||||
@@ -8,6 +8,7 @@ BASIC wird als P-Code ausgeführt; wiederverwendbare BASIC-Bibliotheken benötig
|
|||||||
- `tbc build --library` erzeugt `.tbl`; `tbc link` verbindet ein Hauptprojekt und `.tbl`-Bibliotheken zu einem vollständigen `.tbc` beziehungsweise über `--exe` mit der passenden `tbrt`-Vorlage zu einem eigenständigen Executable.
|
- `tbc build --library` erzeugt `.tbl`; `tbc link` verbindet ein Hauptprojekt und `.tbl`-Bibliotheken zu einem vollständigen `.tbc` beziehungsweise über `--exe` mit der passenden `tbrt`-Vorlage zu einem eigenständigen Executable.
|
||||||
- Den vorhandenen Projektlinker erweitern: Bibliotheken sind ohne ihre BAS-/FRM-/Include-Quellen nutzbar, Auflösung und BASIC-Semantik bleiben gemeinsam.
|
- Den vorhandenen Projektlinker erweitern: Bibliotheken sind ohne ihre BAS-/FRM-/Include-Quellen nutzbar, Auflösung und BASIC-Semantik bleiben gemeinsam.
|
||||||
- `.tbl`-Verweise als geordnete `.MAK`-Einträge sowie explizite CLI-Eingaben unterstützen. Fehlende oder inkompatible Symbole, Typen, Formate und Abhängigkeiten vor Ausgabe diagnostizieren.
|
- `.tbl`-Verweise als geordnete `.MAK`-Einträge sowie explizite CLI-Eingaben unterstützen. Fehlende oder inkompatible Symbole, Typen, Formate und Abhängigkeiten vor Ausgabe diagnostizieren.
|
||||||
|
- Die Abnahme dieses Changes umfasst Backend/CLI, ein festes gemeinsames TBL-Testartefakt und die lokal ausgeführte Verbraucherprobe. Echte Vierzielnachweise werden verbindlich in 05 und für Releasepakete in 06 erbracht.
|
||||||
- Erzeugung und Linken benötigen weder C-/Rust-Compiler noch nativen Linker beim Anwender. Die IDE bindet denselben Dienst in 04 an.
|
- Erzeugung und Linken benötigen weder C-/Rust-Compiler noch nativen Linker beim Anwender. Die IDE bindet denselben Dienst in 04 an.
|
||||||
|
|
||||||
## Capabilities
|
## Capabilities
|
||||||
@@ -22,4 +23,4 @@ Keine. TBL ist eine additive Projekt-/Buildfähigkeit; die Anpassung der bisheri
|
|||||||
|
|
||||||
## Impact
|
## Impact
|
||||||
|
|
||||||
`tb-vm/src/project.rs`, `bytecode.rs`, `project_io.rs`, Frontend-Deklarationsimport und `tb-cli/src/main.rs`; Wiederverwendung der sicheren Veröffentlichung und `tbrt`-Verpackung aus [02](../archive/2026-09-07-phase-6-02-native-executables/proposal.md). Abhängigkeit: 02. IDE-Dokumentmodell, Ausführung, Debugger und Make-Anbindung folgen in 04; plattformübergreifende Verbraucherprüfungen in 05/06, Dokumentation und Gesamtabnahme in 07. Dieser Entwurf ersetzt den noch nicht implementierten Entwurf `phase-6-03-native-bibliotheken` vollständig.
|
`tb-vm/src/project.rs`, `bytecode.rs`, `project_io.rs`, Frontend-Deklarationsimport und `tb-cli/src/main.rs`; Wiederverwendung der sicheren Veröffentlichung und `tbrt`-Verpackung aus [02](../2026-09-07-phase-6-02-native-executables/proposal.md). Abhängigkeit: 02. IDE-Dokumentmodell, Ausführung, Debugger und Make-Anbindung folgen in 04; plattformübergreifende Verbraucherprüfungen in 05/06, Dokumentation und Gesamtabnahme in 07. Dieser Entwurf ersetzt den noch nicht implementierten Entwurf `phase-6-03-native-bibliotheken` vollständig.
|
||||||
@@ -1,24 +1,33 @@
|
|||||||
## 1. Bibliotheksprodukt und Format
|
## 1. Bibliotheksprodukt und Format
|
||||||
|
|
||||||
- [ ] 1.1 AST-abhängige Linkinformationen in kompakte gemeinsame Modulmetadaten überführen; bestehende reine Quellprojekt-/Cachetests müssen Verhalten, Diagnosen und Wiederverwendung erhalten.
|
- [x] 1.1 AST-abhängige Linkinformationen in kompakte gemeinsame Modulmetadaten überführen; bestehende reine Quellprojekt-/Cachetests müssen Verhalten, Diagnosen und Wiederverwendung erhalten.
|
||||||
- [ ] 1.2 Versionierten TBL-Leser/-Schreiber für unverknüpfte Produkte, Deklarationen, COMMON, Forms und Quellorte implementieren; Roundtrip sowie beschädigte Längen, Referenzen, Versionen und Grenzwerte prüfen.
|
- [x] 1.2 Versionierten TBL-Leser/-Schreiber für unverknüpfte Produkte, Deklarationen, COMMON, Forms und Quellorte implementieren; Roundtrip sowie beschädigte Längen, Referenzen, Versionen und Grenzwerte prüfen.
|
||||||
- [ ] 1.3 Library-Kompilation mit typisierten offenen Imports und bestehender Exportsichtbarkeit anbinden; eine Bibliothek mit DECLARE-Import muss ohne dessen Implementierung baubar sein, ungültige Deklarationen müssen scheitern.
|
- [x] 1.3 Library-Kompilation mit typisierten offenen Imports und bestehender Exportsichtbarkeit anbinden; eine Bibliothek mit DECLARE-Import muss ohne dessen Implementierung baubar sein, ungültige Deklarationen müssen scheitern.
|
||||||
|
|
||||||
## 2. Gemeinsames Linken
|
## 2. Gemeinsames Linken
|
||||||
|
|
||||||
- [ ] 2.1 TBL-Deklarationen vor der Verbraucher-Semantikanalyse importieren und Quell-/Libraryprodukte gemeinsam verknüpfen; zwei Bibliotheken mit Abhängigkeit müssen ohne Bibliotheksquellen funktionieren, fehlende/mehrdeutige/inkompatible Symbole und doppelte Module müssen konkrete Diagnosen liefern.
|
- [x] 2.1 TBL-Deklarationen vor der Verbraucher-Semantikanalyse importieren und Quell-/Libraryprodukte gemeinsam verknüpfen; zwei Bibliotheken mit Abhängigkeit müssen ohne Bibliotheksquellen funktionieren, fehlende/mehrdeutige/inkompatible Symbole und doppelte Module müssen konkrete Diagnosen liefern.
|
||||||
- [ ] 2.2 Vollständiges Remapping und Initialisierung prüfen; Vergleichsprojekte müssen BYREF/BYVAL, Arrays, UDTs, COMMON, Konstanten, DATA/RESTORE, Forms-Ereignisse, Fehlerorte und RUN mit äquivalenten Quellmodulen abgleichen.
|
- [x] 2.2 Vollständiges Remapping und Initialisierung prüfen; Vergleichsprojekte müssen BYREF/BYVAL, Arrays, UDTs, COMMON, Konstanten, DATA/RESTORE, Forms-Ereignisse, Fehlerorte und RUN mit äquivalenten Quellmodulen abgleichen.
|
||||||
- [ ] 2.3 MAK-Laden/-Schreiben um geordnete TBL-Einträge erweitern; Verschieben/Save-As, Fallunabhängigkeit, Duplikate, fehlende Dateien, TBL als unzulässige Startdatei und unveränderte reine Quellprojekte testen.
|
- [x] 2.3 MAK-Laden/-Schreiben um geordnete TBL-Einträge erweitern; Verschieben/Save-As, Fallunabhängigkeit, Duplikate, fehlende Dateien, TBL als unzulässige Startdatei und unveränderte reine Quellprojekte testen.
|
||||||
- [ ] 2.4 Bibliotheksinhalt in die Compiler-Cacheidentität aufnehmen; Austausch einer TBL einschließlich gleicher Dateigröße muss Linkresultat und betroffene Deklarationen aktualisieren.
|
- [x] 2.4 Bibliotheksinhalt in die Compiler-Cacheidentität aufnehmen; Austausch einer TBL einschließlich gleicher Dateigröße muss Linkresultat und betroffene Deklarationen aktualisieren.
|
||||||
|
|
||||||
## 3. CLI und Ausgabe
|
## 3. CLI und Ausgabe
|
||||||
|
|
||||||
- [ ] 3.1 `tbc build --library` und `tbc link` mit expliziten TBL-Eingaben implementieren sowie run/build/check für MAK-Bibliotheken anbinden; CLI-Tests müssen gültige Beispiele, unveränderten TBC-Standard und unzulässige Flag-/Eingabekombinationen prüfen.
|
- [x] 3.1 `tbc build --library` und `tbc link` mit expliziten TBL-Eingaben implementieren sowie run/build/check für MAK-Bibliotheken anbinden; CLI-Tests müssen gültige Beispiele, unveränderten TBC-Standard und unzulässige Flag-/Eingabekombinationen prüfen.
|
||||||
- [ ] 3.2 `tbc link --exe` mit der tbrt-Verpackung aus 02 verbinden; ein ohne Bibliotheksquellen gelinktes Programm muss anschließend ohne TBL/TBC/tbrt/tb/tbc-Installation starten und dieselben Resultate liefern.
|
- [x] 3.2 `tbc link --exe` mit der tbrt-Verpackung aus 02 verbinden; ein ohne Bibliotheksquellen gelinktes Programm muss anschließend ohne TBL/TBC/tbrt/tb/tbc-Installation starten und dieselben Resultate liefern.
|
||||||
- [ ] 3.3 TBL-/Linkausgabe über die geschützte Veröffentlichung aus 02 führen; Abbruch, Schreibfehler, No-clobber-Konflikt und Ausgabe auf eine Eingabebibliothek dürfen keine Eingabedaten oder alten Ziele beschädigen.
|
- [x] 3.3 TBL-/Linkausgabe über die geschützte Veröffentlichung aus 02 führen; Abbruch, Schreibfehler, No-clobber-Konflikt und Ausgabe auf eine Eingabebibliothek dürfen keine Eingabedaten oder alten Ziele beschädigen.
|
||||||
|
|
||||||
## 4. Abnahme und Übergabe
|
## 4. Abnahme und Übergabe
|
||||||
|
|
||||||
- [ ] 4.1 Ein identisches TBL-Artefakt auf Windows amd64, macOS arm64 und Linux amd64/arm64 mit separaten BASIC-Verbrauchern verknüpfen; Prüfsumme und echte Zielausführung müssen Portabilität ohne Neubau der Library beweisen.
|
- [x] 4.1 Ein festes gemeinsames TBL-Artefakt mit Prüfsumme und reproduzierbarer quellfreier TBC-/EXE-Verbraucherprobe bereitstellen und auf dem verfügbaren Host ausführen; echte Läufe derselben Bytes auf Windows amd64, macOS arm64 und Linux amd64/arm64 mit Befehlen und Erwartungen verbindlich an 05/06 übergeben.
|
||||||
- [ ] 4.2 Format-/Befehls-/Initialisierungsvertrag und Werkzeugfreiheit dokumentieren; Beispiele müssen mit ausgelieferten TB-Tools ohne C-/Rust-Compiler oder nativen Linker laufen und fehlende EXE-Voraussetzungen korrekt benennen.
|
- [x] 4.2 Format-/Befehls-/Initialisierungsvertrag und Werkzeugfreiheit dokumentieren; Beispiele müssen mit ausgelieferten TB-Tools ohne C-/Rust-Compiler oder nativen Linker laufen und fehlende EXE-Voraussetzungen korrekt benennen.
|
||||||
- [ ] 4.3 Sämtliche Spec-Szenarien mit Ergebnissen zuordnen und Übergabe an IDE 04/Actions 06 vorbereiten; fokussierte Link-/CLI-Tests, Workspace-Regressionen, Clippy, Format- und OpenSpec-Prüfung müssen ohne offenen Befund bestehen.
|
- [x] 4.3 Sämtliche Spec-Szenarien mit lokalen Ergebnissen beziehungsweise ausdrücklich in 05/06 ausstehenden Zielnachweisen zuordnen und Übergabe an IDE 04/Plattformabnahme 05/Actions 06 vorbereiten; fokussierte Link-/CLI-Tests, Workspace-Regressionen, Clippy, Format- und OpenSpec-Prüfung müssen ohne offenen Befund bestehen.
|
||||||
|
|
||||||
|
## Abnahmestand am 2026-09-07
|
||||||
|
|
||||||
|
Die Implementierung und lokalen Prüfungen sind abgeschlossen (13/13).
|
||||||
|
Nach der am 2026-09-07 vom Benutzer bestätigten Aufteilung gehören die echten
|
||||||
|
Vierziel-Verbraucherläufe in 05/06. macOS arm64 wurde mit dem gemeinsamen
|
||||||
|
TBL-Artefakt geprüft; Windows amd64 und Linux amd64/arm64 sind noch nicht
|
||||||
|
ausgeführt. Diese Nachweise bleiben in den Folgechanges offen und werden
|
||||||
|
nicht als bestanden gezählt. Ergebnisse und Aufruf: [verification.md](verification.md).
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
# Verifikation: phase-6-03-pcode-bibliotheken-und-linker
|
||||||
|
|
||||||
|
Stand: 2026-09-07. Verifiziert wurde die lokale Implementierung auf Basis von
|
||||||
|
`60d37ec79ddfdebdfa71b893bf6b3a141d20ac3c` auf `main`. Der vorangehende Change
|
||||||
|
02 wurde synchronisiert, als `2026-09-07-phase-6-02-native-executables`
|
||||||
|
archiviert und mit diesem Commit auf `origin/main` bestätigt. Change 03 wurde am 2026-09-07 nach Spec-Synchronisierung archiviert;
|
||||||
|
dieser Bericht wird gemeinsam mit der Implementierung committed.
|
||||||
|
|
||||||
|
## Ergebnis
|
||||||
|
|
||||||
|
| Dimension | Ergebnis |
|
||||||
|
|---|---|
|
||||||
|
| Vollständigkeit | 13/13 Aufgaben gemäß bestätigter Abnahmeaufteilung abgeschlossen |
|
||||||
|
| Implementierung | Alle sechs Requirements im Backend-/CLI-Umfang von 03 umgesetzt; IDE-Anbindung gemäß Proposal in 04 |
|
||||||
|
| Szenarien | Sechs vollständig lokal nachgewiesen; Vierziel-Portabilität auf macOS arm64 nachgewiesen, drei Zielausführungen fehlen |
|
||||||
|
| Korrektheit | Keine offenen Codebefunde nach Korrektur und erneuten Prüfungen |
|
||||||
|
| Kohärenz | Gemeinsamer Projektcompiler/Linker, kompakte Metadaten, bestehender TBC-Codec und sichere Veröffentlichung wiederverwendet |
|
||||||
|
| Gesamtabnahme | Change 03 ohne offene Befunde; Vierziel-/Releaseabnahme in 05/06 noch offen |
|
||||||
|
|
||||||
|
## Bestätigte Abnahmeaufteilung
|
||||||
|
|
||||||
|
Am 2026-09-07 bestätigte der Benutzer den zentralen Cross-Paketbau auf dem
|
||||||
|
vorhandenen Linux-arm64-Gitea-Runner und die getrennte echte Plattformabnahme.
|
||||||
|
PLAN, Proposal, Design und Tasks ordnen die vollständigen Zielausführungen
|
||||||
|
jetzt konsistent 05/06 zu. Der bisherige Befund zu Aufgabe 4.1 ist damit durch
|
||||||
|
eine ausdrücklich bestätigte Änderung der Abnahmezuständigkeit geschlossen,
|
||||||
|
nicht durch zusätzliche behauptete Testläufe. Der Portabilitätsvertrag bleibt
|
||||||
|
unverändert. Die lokale Implementierungsverifikation enthält keine Codebefunde.
|
||||||
|
|
||||||
|
Dieselben TBL-Bytes wurden auf macOS arm64 ohne Bibliotheksquellen verknüpft
|
||||||
|
und als eigenständiges Executable ausgeführt. Windows amd64 sowie Linux amd64
|
||||||
|
und arm64 wurden noch nicht ausgeführt; Linux-arm64-`cargo check` beweist keine
|
||||||
|
Ausführung. Der vorhandene entfernte Gitea-Runner wurde als Linux aarch64
|
||||||
|
bestätigt; TerminalBasic wurde darauf noch nicht über die Vierzielmatrix gebaut.
|
||||||
|
|
||||||
|
05 muss `python3 tests/support/library-abnahme.py --bin-dir <native-TB-Werkzeuge>`
|
||||||
|
mit unveränderter `tests/libraries/portable.tbl` auf allen vier echten Zielen
|
||||||
|
nachweisen. 06 übernimmt dies für die entpackten Releasepakete und bindet die
|
||||||
|
Nachweise an Revision und Paketprüfsumme; fehlende Nachweise sperren die
|
||||||
|
Releasefreigabe. Die bisherigen lokalen Testergebnisse bleiben unverändert.
|
||||||
|
|
||||||
|
## Requirement- und Szenarioabgleich
|
||||||
|
|
||||||
|
| Requirement / Szenario | Umsetzung und Nachweis |
|
||||||
|
|---|---|
|
||||||
|
| Portables TBL-Format / Bibliothek auf anderem Ziel | [library.rs](../../../../crates/tb-vm/src/library.rs): TBL 1, P-Code 4, gemeinsamer Runtime-Vertrag 1, feste Breiten, Länge/Prüfsumme, unverknüpfte Modulprodukte, Export-/Importverträge. Gemeinsames [TBL-Artefakt](../../../../tests/libraries/portable.tbl) und [Zielprobe](../../../../tests/support/library-abnahme.py). macOS bestanden; drei Zielausführungen offen. |
|
||||||
|
| Gemeinsamer Linkbefehl / quellfreie Nutzung bis EXE | [CLI](../../../../crates/tb-cli/src/main.rs), `ProjectSources::compile`/`compile_library`, `ProjectCompiler::link_library`. Der Verbrauchercheck löscht Quelle, TBL, TBC und Vorlage vor dem EXE-Lauf mit leerem PATH; Ausgabe und Dateieffekte stimmen einschließlich RUN. |
|
||||||
|
| Explizite Bibliothekszuordnung / verschobenes Projekt | [project_io.rs](../../../../crates/tb-vm/src/project_io.rs): geordnete TBL-Mitglieder, Startmodul zuerst, Zusatzbibliotheken danach, fallunabhängige Pfade. [Projekt-I/O-Tests](../../../../crates/tb-vm/tests/project_io.rs) prüfen Verschieben, Save As, fehlende Datei mit MAK-Bezug, Duplikate, TBL nicht als Text/Startdatei. |
|
||||||
|
| Typgeprüfte Verknüpfung / A benötigt B | [Projektlinker](../../../../crates/tb-vm/src/project.rs) bindet vor der Verbraucher-Semantik Deklarationen und danach vollständige Signaturen. [Library-Tests](../../../../crates/tb-vm/tests/library.rs) prüfen offene Imports, zwei quellfreie Bibliotheken, fehlendes B, Parameter-/Ergebnistypen, COMMON-Grenzen, ungültige Deklarationen und flache Abhängigkeiten. |
|
||||||
|
| Typgeprüfte Verknüpfung / mehrdeutiges Symbol | Library-Tests mit zwei Definitionen und explizitem sowie implizitem Verbraucher-DECLARE: Konflikt nennt beide Herkunftsmodule; keine zufällige Auswahl. Doppelte Modulidentitäten werden separat abgewiesen. |
|
||||||
|
| BASIC-Semantik / Quellmodule durch TBL ersetzen | Vergleichsprojekte prüfen BYREF/BYVAL, Arrays, UDTs/feste Strings, Konstanten, COMMON, DATA/RESTORE und Modulreihenfolge. Getrennte Forms-Libraries prüfen verschachtelte gleiche Controlnamen, Designarrays, Anfangsdaten und Form_Load-Ereignisse. Include-Fehler behalten physischen Ort und ERL=200. Der native Verbraucher erzeugt nach RUN nachweislich erneut frische Bibliotheksvariablen, DATA und Bildschirmzustand. |
|
||||||
|
| Validierung/geschützte Ausgabe / beschädigte Datei oder Ausgabe auf Eingabe | TBL-Leser prüft Header, Versionen, Prüfsumme, Abschnitte, Tags, Referenzen, Export-/Code-Verträge und Grenzen. Tests für alle abgeschnittenen Präfixe, nachberechnete Prüfsummen auf ungültigen Strukturen und kontrollierte Mutationen. [CLI-Tests](../../../../crates/tb-cli/tests/project.rs) prüfen Ausgabe auf TBL/MAK, No-clobber, ungültige Optionen und defekte Eingaben ohne Änderung alter Ziele. Der gemeinsame [Publikationstest](../../../../crates/tb-export/src/tests.rs) prüft Abbruch vor/nach Vorbereitung, Schreibfehler, konkurrierende Änderungen und Bereinigung. |
|
||||||
|
|
||||||
|
## Behobene Befunde aus den Prüfläufen
|
||||||
|
|
||||||
|
- Die Quelltabellen-Normalisierung erzeugte zunächst zusätzliche Einträge und
|
||||||
|
verschob IDE-Bindungsorte. Module behalten jetzt ihre vollständige lokale
|
||||||
|
Quelltabellenreihenfolge; die vorhandenen vier betroffenen Designerfälle,
|
||||||
|
Projekt-, Cache- und Debuggertests bestehen. Bei eingeschobenen TBL-Modulen
|
||||||
|
bleiben außerdem die tatsächlichen Modul-IDs der Quell-Debugkontexte erhalten.
|
||||||
|
- Objektzuordnung nur über Namen und übergeordneten Namen kollidierte bei
|
||||||
|
gleich benannten verschachtelten Controls in zwei Forms. Der Linker verwendet
|
||||||
|
den remappten Eltern-Slot; getrennte Form-Libraries ergeben dieselben Ereignisse
|
||||||
|
und Anfangswerte wie die äquivalenten Quellen.
|
||||||
|
- Die strengere Importprüfung behandelte den unbenutzten NotReserved-Prototyp
|
||||||
|
in QLBVIEW als Linkabhängigkeit. Nicht referenzierte ungebundene Prototypen
|
||||||
|
werden aus dem Endprodukt entfernt; tatsächliche Aufrufe müssen auch in
|
||||||
|
unaufgerufenen mitgelieferten Prozeduren gebunden sein. Eine spätere
|
||||||
|
Debug-Ausführung eines entfernten Prototyps liefert ebenfalls eine Diagnose.
|
||||||
|
Der Fremdprogramm-Abnahmesatz besteht wieder vollständig.
|
||||||
|
- Zusätzliche Grenz-/Vertragsprüfungen verhindern unter anderem leere
|
||||||
|
Konstantenexporte, inkonsistente COMMON-Slots und Überlauf remappter Tabellen.
|
||||||
|
Bytecode-Mutationen mit erneut berechneter Prüfsumme werden kontrolliert
|
||||||
|
behandelt; es trat keine Panic im Reader-/Linkpfad auf.
|
||||||
|
|
||||||
|
## Ausgeführte Prüfungen
|
||||||
|
|
||||||
|
| Prüfung | Ergebnis |
|
||||||
|
|---|---|
|
||||||
|
| `cargo test --locked --workspace` auf endgültigem Stand | 625 bestanden, 0 fehlgeschlagen, 2 vorgesehen ignoriert |
|
||||||
|
| `release-abnahme.py --vbdos-repo /tmp/terminalbasic-vbdos-evidence` | Vollständig bestanden: Fremdbestand, Workspace, Inventar, Format, Clippy, Benchmarks |
|
||||||
|
| Explizite Fremdabnahme in fixierter Revision | Drei Tests einschließlich 14 Programm-Ausführungen bestanden; kein Emulator/GUI gestartet |
|
||||||
|
| Nachträglich ergänzte gezielte Library-/Projekt-/CLI-Prüfungen | Bestanden, einschließlich Include-ERL, identischer Dateigröße bei reinem Codeaustausch und TBL als unzulässigem Startprodukt |
|
||||||
|
| `cargo fmt --all -- --check` | Bestanden |
|
||||||
|
| `cargo clippy --locked --workspace --all-targets -- -D warnings` | Bestanden auf endgültigem Stand |
|
||||||
|
| `openspec validate --all --strict` | 30 Elemente bestanden, 0 fehlgeschlagen; rein informative Längenhinweise in bestehenden Requirements |
|
||||||
|
| Release-Build von `tb-cli`, `tb-runner`, `tb-export` | Bestanden |
|
||||||
|
| `library-abnahme.py` auf macOS arm64 | Echter quellfreier TBL→TBC/EXE-Verbraucher und RUN bestanden |
|
||||||
|
| `native-abnahme.py --target aarch64-apple-darwin` | Alle EXE-, PTY-, Abbruch-, Signierungs- und Beschädigungsprüfungen bestanden |
|
||||||
|
| Linux arm64: `cargo check --locked --target aarch64-unknown-linux-gnu -p tb-cli -p tb-runner -p tb-export` | Bestanden; keine Zielausführung |
|
||||||
|
| Runtime-Prüfung | `tbrt` 3.022.416 Bytes; keine BASIC-Parser-/Quellcompiler-Symbole; native Abhängigkeiten nur libiconv und libSystem |
|
||||||
|
|
||||||
|
Die beiden regulär ignorierten Workspace-Tests sind bewusste Golden-Erzeugung
|
||||||
|
und der externe Fremdbestand; letzterer wurde im gesonderten Abnahmesatz explizit
|
||||||
|
ausgeführt. Das Inventar meldet 795 implementiert, 50 Non-Features, 0 offen.
|
||||||
|
|
||||||
|
Leistung: Einzelmodul 0,79 ms bei 50-ms-Budget, Projekt mit 49.760 Zeilen
|
||||||
|
100 ms bei 1000-ms-Budget; Cacheänderung 0,77 ms/28,37 ms für 1/20 Module.
|
||||||
|
VM: INTEGER 1289 ms, DOUBLE 589 ms, BYREF-Aufrufe 148 ms,
|
||||||
|
String-Funktionen 114 ms. Alle geltenden Budgets bestanden.
|
||||||
|
|
||||||
|
Reproduzierbares gemeinsames Artefakt:
|
||||||
|
`5d25c2f8c95ae535e55a6c84c1ddd0d964358863f4f3c780c50ac4a2eb3594df`
|
||||||
|
(SHA-256 von `tests/libraries/portable.tbl`). Der aktuelle macOS-Nachweis meldet
|
||||||
|
`native_execution=true`, `source_free=true`, `run_reset=true`.
|
||||||
|
|
||||||
|
Lokale Rohprotokolle: `/tmp/tb-phase6-03-release-abnahme.log`,
|
||||||
|
`/tmp/tb-phase6-03-workspace-final.log`, `/tmp/tb-phase6-03-focused.log`,
|
||||||
|
`/tmp/tb-phase6-03-clippy-final.log`, `/tmp/tb-phase6-03-openspec.log`,
|
||||||
|
`/tmp/tb-phase6-03-library-native.log`, `/tmp/tb-phase6-03-native-regression.log`,
|
||||||
|
`/tmp/tb-phase6-03-linux-check.log`, `/tmp/tb-phase6-03-runtime-audit.log`.
|
||||||
|
|
||||||
|
## Übergabe
|
||||||
|
|
||||||
|
[Format und Befehle](../../../../docs/pcode-bibliotheken.md) beschreiben das
|
||||||
|
Binärformat, Auflösung, Initialisierung, geschützte Ausgabe und Systemvoraussetzungen.
|
||||||
|
04 erhält den gemeinsamen Dienst für IDE-Dokumentmodell und Make-Aktionen.
|
||||||
|
05/06 erhalten ein einziges gespeichertes TBL-Artefakt und denselben
|
||||||
|
plattformprüfenden Verbraucheraufruf für echte Zielsysteme; der Paketbau erfolgt
|
||||||
|
zentral auf dem Linux-arm64-Gitea-Runner. Die fehlenden
|
||||||
|
Zielnachweise sind noch offen und werden nicht durch das geplante spätere
|
||||||
|
CI-Setup ersetzt.
|
||||||
@@ -12,9 +12,10 @@ Phase 5 verwendete crossterm-Ereignisse, ratatui-TestBackend und `tests/support/
|
|||||||
|
|
||||||
1. Eine Matrix mit Windows Terminal auf Windows amd64, Terminal.app auf macOS arm64 sowie xterm und einem VTE-Terminal auf beiden Linux-Architekturen verwenden. Konkrete installierte Versionsnummern sind Messmetadaten. Diese Kombinationen bilden den geplanten Mindestsatz, weitere Emulatoren sind keine Voraussetzung.
|
1. Eine Matrix mit Windows Terminal auf Windows amd64, Terminal.app auf macOS arm64 sowie xterm und einem VTE-Terminal auf beiden Linux-Architekturen verwenden. Konkrete installierte Versionsnummern sind Messmetadaten. Diese Kombinationen bilden den geplanten Mindestsatz, weitere Emulatoren sind keine Voraussetzung.
|
||||||
2. Die vorhandenen Phase-5-Befehls-/Fokus-/Help-/Debuggerfälle zu kurzen tatsächlichen Eingabesequenzen verdichten. Screenshots/Transkripte, Eventdiagnosen und sichtbare Resultate werden je Matrixzelle festgehalten; ein Log „Prozess gestartet“ reicht nicht. F1–F12 und konfliktreiche Modifikatoren vollständig aufführen.
|
2. Die vorhandenen Phase-5-Befehls-/Fokus-/Help-/Debuggerfälle zu kurzen tatsächlichen Eingabesequenzen verdichten. Screenshots/Transkripte, Eventdiagnosen und sichtbare Resultate werden je Matrixzelle festgehalten; ein Log „Prozess gestartet“ reicht nicht. F1–F12 und konfliktreiche Modifikatoren vollständig aufführen.
|
||||||
3. Unix-PTY-Prüfung wiederverwenden; Windows-ConPTY-/Konsolenprüfung für automatisierbare Terminalübergänge ergänzen. Dieselben nativen Releaseprogramme und eine repräsentative exportierte Forms-Anwendung verwenden. Gitea-Jobs führen automatisierbare Prüfungen aus; echte Emulatorfolgen dürfen dokumentiert manuell erfolgen.
|
3. Buildhost und Prüfsystem unterscheiden: Pakete entstehen in 06 zentral auf Linux arm64, echte Zielprüfungen laufen auf passenden Systemen; vier native Buildrunner sind keine Voraussetzung. Für 05 können Kandidaten aus lokalen Repository-Buildaufrufen verwendet werden, sodass kein Vorababschluss von 06 nötig ist. 06 prüft später die tatsächlich gepackte Revision. Die unveränderte `tests/libraries/portable.tbl` aus 03 mit `tests/support/library-abnahme.py` auf allen vier Zielen prüfen; TBL-Prüfsumme, Toolrevision, tatsächliches Host-Target und Ergebnisse einschließlich quellfreier TBC-/EXE-Nutzung und RUN festhalten.
|
||||||
4. Windows-/Unix-spezifische Tests und Hilfsprozesse mit passenden Plattformzweigen führen. Keine Unix-Shell-Kommandos unverändert auf Windows als vermeintliche Produktfehler werten. Produktsemantik und Testhost-Portabilität getrennt prüfen.
|
4. Unix-PTY-Prüfung wiederverwenden; Windows-ConPTY-/Konsolenprüfung für automatisierbare Terminalübergänge ergänzen. Dieselben nativen Releaseprogramme und eine repräsentative exportierte Forms-Anwendung verwenden. Gitea-Jobs führen automatisierbare Prüfungen aus; echte Emulatorfolgen dürfen dokumentiert manuell erfolgen.
|
||||||
5. Fehler im gemeinsamen Terminaladapter beheben und einen fokussierten Regressionstest ergänzen. Für vom Emulator reservierte Shortcuts einen bereits vorhandenen Menü-/Mausweg prüfen. Fehlende Pflichtfunktionen werden nicht als Terminalgrenze umetikettiert.
|
5. Windows-/Unix-spezifische Tests und Hilfsprozesse mit passenden Plattformzweigen führen. Keine Unix-Shell-Kommandos unverändert auf Windows als vermeintliche Produktfehler werten. Produktsemantik und Testhost-Portabilität getrennt prüfen.
|
||||||
|
6. Fehler im gemeinsamen Terminaladapter beheben und einen fokussierten Regressionstest ergänzen. Für vom Emulator reservierte Shortcuts einen bereits vorhandenen Menü-/Mausweg prüfen. Fehlende Pflichtfunktionen werden nicht als Terminalgrenze umetikettiert.
|
||||||
|
|
||||||
## Risks / Trade-offs
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ Die bisherige headless Abnahme und der lokale Unix-PTY-Test beweisen nicht die e
|
|||||||
- Windows amd64 mit Windows Terminal, macOS arm64 und Linux amd64/arm64 mit mindestens zwei Terminalemulatoren systematisch abnehmen.
|
- Windows amd64 mit Windows Terminal, macOS arm64 und Linux amd64/arm64 mit mindestens zwei Terminalemulatoren systematisch abnehmen.
|
||||||
- IDE und native Terminalprogramme mit F1–F12, Modifikatoren, Maus, Unicode, Resize, INPUT/Break und Shell-Rückkehr prüfen.
|
- IDE und native Terminalprogramme mit F1–F12, Modifikatoren, Maus, Unicode, Resize, INPUT/Break und Shell-Rückkehr prüfen.
|
||||||
- Ziel-/Terminalversionen, Eingabeprotokolle, Einschränkungen und funktionierende Ersatzwege dokumentieren.
|
- Ziel-/Terminalversionen, Eingabeprotokolle, Einschränkungen und funktionierende Ersatzwege dokumentieren.
|
||||||
|
- Dieselben TBL-Bytes aus 03 auf allen vier echten Zielen mit separaten quellfreien TBC-/EXE-Verbrauchern prüfen und Prüfsumme/Ergebnis protokollieren.
|
||||||
- Automatisierbare Zieltests von manuellen Emulatornachweisen unterscheiden; fehlende Systeme bleiben fehlende Nachweise.
|
- Automatisierbare Zieltests von manuellen Emulatornachweisen unterscheiden; fehlende Systeme bleiben fehlende Nachweise.
|
||||||
|
|
||||||
## Capabilities
|
## Capabilities
|
||||||
|
|||||||
@@ -31,3 +31,10 @@ Einschränkungen SHALL reproduzierbar, plattformspezifisch und mit Auswirkung so
|
|||||||
#### Scenario: Fehlender Prüfrechner
|
#### Scenario: Fehlender Prüfrechner
|
||||||
- **WHEN** eine erforderliche System-/Emulatorkombination nicht verfügbar ist
|
- **WHEN** eine erforderliche System-/Emulatorkombination nicht verfügbar ist
|
||||||
- **THEN** nennt der Bericht die fehlende Kombination und führt sie bis zum tatsächlichen Nachweis offen
|
- **THEN** nennt der Bericht die fehlende Kombination und führt sie bis zum tatsächlichen Nachweis offen
|
||||||
|
|
||||||
|
### Requirement: Identische Bibliothek auf vier Zielsystemen
|
||||||
|
Die Plattformabnahme SHALL dieselben unveränderten TBL-Bytes aus Change 03 auf Windows amd64, macOS arm64 und Linux amd64/arm64 mit separaten BASIC-Verbrauchern zu TBC und eigenständigem Executable verknüpfen und tatsächlich ausführen. Library-Quellen SHALL nicht benötigt werden; Resultate und RUN-Neustart SHALL dem gemeinsamen Soll entsprechen. Jeder Nachweis SHALL TBL-Prüfsumme, Toolrevision, tatsächliches Host-Target und Ergebnis enthalten. Der zentrale Cross-Build in Change 06 MUST NOT diese Zielausführung ersetzen; separate Prüfsysteme und dokumentierte manuelle Läufe sind zulässig.
|
||||||
|
|
||||||
|
#### Scenario: Cross-Bau erfolgreich, Bibliothekslauf fehlt
|
||||||
|
- **WHEN** alle vier Toolpakete gebaut sind, aber die gemeinsame TBL-Probe auf Windows noch nicht ausgeführt wurde
|
||||||
|
- **THEN** bleibt der Windows-Verbrauchernachweis offen und die Plattformabnahme ist nicht abgeschlossen
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
- [ ] 1.1 Verbindliche Matrix und kurze Bediensequenzen für die vier Targets anlegen; Windows Terminal, Terminal.app sowie xterm/VTE auf beiden Linux-Architekturen müssen konkrete Versions-/Revisionsfelder und erwartete Resultate besitzen.
|
- [ ] 1.1 Verbindliche Matrix und kurze Bediensequenzen für die vier Targets anlegen; Windows Terminal, Terminal.app sowie xterm/VTE auf beiden Linux-Architekturen müssen konkrete Versions-/Revisionsfelder und erwartete Resultate besitzen.
|
||||||
- [ ] 1.2 Bestehenden Unix-PTY-Test und passenden Windows-Konsolen-/ConPTY-Test für Shell, Abbruch und Wiederherstellung bereitstellen; positive Läufe und erzwungene Fehler dürfen keinen beschädigten Terminalmodus hinterlassen.
|
- [ ] 1.2 Bestehenden Unix-PTY-Test und passenden Windows-Konsolen-/ConPTY-Test für Shell, Abbruch und Wiederherstellung bereitstellen; positive Läufe und erzwungene Fehler dürfen keinen beschädigten Terminalmodus hinterlassen.
|
||||||
- [ ] 1.3 IDE-/Programmprüfungen für F1–F12, Modifikatoren, Maus, Unicode/Attribute und Resize nachvollziehbar auflisten; jeder Fall muss sichtbaren Effekt und Ausschluss doppelter BASIC-Zustellung prüfen.
|
- [ ] 1.3 Die gemeinsame TBL-Verbraucherprobe aus 03 samt fester Prüfsumme in jede der vier Zielabnahmen aufnehmen (quellfreies TBC/EXE, RUN, tatsächliches Host-Target); IDE-/Programmprüfungen für F1–F12, Modifikatoren, Maus, Unicode/Attribute und Resize nachvollziehbar auflisten; jeder Fall muss sichtbaren Effekt und Ausschluss doppelter BASIC-Zustellung prüfen.
|
||||||
|
|
||||||
## 2. Tatsächliche Zielabnahme
|
## 2. Tatsächliche Zielabnahme
|
||||||
|
|
||||||
@@ -15,4 +15,4 @@
|
|||||||
## 3. Übergabe
|
## 3. Übergabe
|
||||||
|
|
||||||
- [ ] 3.1 Terminalgrenzen und verifizierte Ersatzwege dokumentieren; kein Produktfehler oder fehlender Prüfrechner darf als bestandene Matrixzelle erscheinen.
|
- [ ] 3.1 Terminalgrenzen und verifizierte Ersatzwege dokumentieren; kein Produktfehler oder fehlender Prüfrechner darf als bestandene Matrixzelle erscheinen.
|
||||||
- [ ] 3.2 Automatisierbare Zielprüfungen an 06 übergeben und Matrixbericht fertigstellen; alle Pflichtzellen, Revisionen und tatsächlichen Ergebnisse müssen vor Abschluss vorliegen.
|
- [ ] 3.2 Automatisierbare Zielprüfungen an 06 übergeben und Matrixbericht fertigstellen; alle Pflichtzellen, Revisionen, vier Ergebnisse derselben TBL-Verbraucherprobe und tatsächlichen Ergebnisse müssen vor Abschluss vorliegen. Native Prüfsysteme dürfen separat vom zentralen Buildrunner betrieben und Nachweise dokumentiert manuell erhoben werden.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
## Context
|
## Context
|
||||||
|
|
||||||
Origin ist `https://git.rfc1437.de/hugo/TerminalBasic.git`; die read-only Versionsabfrage liefert Gitea 1.25.4. Im Repository existieren noch keine Actions-Workflows. Runner-Verfügbarkeit, Labels und Release-Zugang wurden nicht behauptet oder verändert. Der Benutzer verlangt ausschließlich Gitea Actions am Origin und vier native Terminalziele.
|
Origin ist `https://git.rfc1437.de/hugo/TerminalBasic.git`; die read-only Versionsabfrage liefert Gitea 1.25.4. Im Repository existieren noch keine Actions-Workflows. Der bestehende Runner wurde am 2026-09-07 per read-only SSH als Linux aarch64 mit laufendem Gitea-Runner-Container bestätigt. Andere Projekte am Origin verwenden bereits `linux-arm64` mit cargo-zigbuild und cargo-xwin. Das tatsächliche Label, Toolchain-/SDK-Versionen und der Release-Zugang werden bei der Anbindung dieses Projekts geprüft; eine erfolgreiche TerminalBasic-Cross-Matrix wurde noch nicht nachgewiesen. Der Benutzer verlangt ausschließlich Gitea Actions am Origin und vier native Terminalziele.
|
||||||
|
|
||||||
## Goals / Non-Goals
|
## Goals / Non-Goals
|
||||||
|
|
||||||
@@ -10,19 +10,19 @@ Origin ist `https://git.rfc1437.de/hugo/TerminalBasic.git`; die read-only Versio
|
|||||||
|
|
||||||
## Decisions
|
## Decisions
|
||||||
|
|
||||||
1. `.gitea/workflows/` verwendet explizite Einzel-Labels für die vier nativen Runner: Windows-amd64, macOS-arm64, Linux-amd64, Linux-arm64. Tatsächliche Labels bei Anbindung inventarisieren und dokumentieren; nicht davon ausgehen, dass mehrere Labels logisch UND bedeuten. Runner werden passend zum Ziel betrieben. Grundlage: [Gitea-1.25-Runnerdokumentation](https://docs.gitea.com/1.25/usage/actions/act-runner/).
|
1. `.gitea/workflows/` nutzt den vorhandenen Linux-arm64-Runner für alle vier Build-/Paketziele. Vorhandene Workflowmuster der anderen Origin-Projekte wiederverwenden: cargo-zigbuild für Linux amd64 und macOS arm64, cargo-xwin für Windows amd64/MSVC; Linux arm64 nativ oder mit derselben festgelegten Linux-Zielbaseline. Container/Tools und benötigte SDKs/Sysroots reproduzierbar pinnen; konkrete Rust-/OS-/glibc-Versionen mit TerminalBasic prüfen. Das Runnerlabel `linux-arm64` bei Anbindung bestätigen. Vier native Buildrunner sind nicht erforderlich. `build-runtime.py` erzeugt Vorlagenmetadaten mit dem Hostwerkzeug für das jeweilige Target; fremde Executables werden auf dem Buildhost nicht nativ ausgeführt.
|
||||||
2. Wiederverwendbare Repository-Skripte erledigen Build, Tests, Paketierung und Prüfung auch lokal. Source-Checkout vom Gitea-Origin, alle zusätzlichen Actions nur über explizite Gitea-URLs und feste Revisionen, soweit überhaupt nötig; keine implizite fremde Action-Auflösung. Rust und benötigte Systemwerkzeuge versioniert bereitstellen. Server-/Runner-Kompatibilität praktisch prüfen, statt lediglich YAML zu validieren.
|
2. Wiederverwendbare Repository-Skripte erledigen Build, Tests, Paketierung und Prüfung auch lokal. Source-Checkout vom Gitea-Origin, alle zusätzlichen Actions nur über explizite Gitea-URLs und feste Revisionen, soweit überhaupt nötig; keine implizite fremde Action-Auflösung. Rust und benötigte Systemwerkzeuge versioniert bereitstellen. Server-/Runner-Kompatibilität praktisch prüfen, statt lediglich YAML zu validieren.
|
||||||
3. Matrixprodukte: `terminalbasic-<version>-windows-amd64.7z`, `terminalbasic-<version>-macos-arm64.tar.gz`, `terminalbasic-<version>-linux-amd64.tar.gz`, `terminalbasic-<version>-linux-arm64.tar.gz`. Windows enthält `tb.exe`/`tbc.exe`/`tbrt.exe`, übrige `tb`/`tbc`/`tbrt`; tbrt ist die native Exportvorlage, dazu deren Metadaten, Lizenz, Kurzinstallation. Archive dürfen keine Buildverzeichnis-/Quellbaumabhängigkeiten haben. Exportierte Anwenderprogramme bleiben normale native Einzelartefakte; die Archivformate betreffen die Distribution.
|
3. Matrixprodukte: `terminalbasic-<version>-windows-amd64.7z`, `terminalbasic-<version>-macos-arm64.tar.gz`, `terminalbasic-<version>-linux-amd64.tar.gz`, `terminalbasic-<version>-linux-arm64.tar.gz`. Windows enthält `tb.exe`/`tbc.exe`/`tbrt.exe`, übrige `tb`/`tbc`/`tbrt`; tbrt ist die native Exportvorlage, dazu deren Metadaten, Lizenz, Kurzinstallation. Archive dürfen keine Buildverzeichnis-/Quellbaumabhängigkeiten haben. Exportierte Anwenderprogramme bleiben normale native Einzelartefakte; die Archivformate betreffen die Distribution.
|
||||||
4. Windows nutzt 7-Zip zum Erzeugen/Prüfen, Unix tar+gzip unter Erhalt der Executable-Bits. Jobs entpacken das fertige Archiv in ein neues Verzeichnis und führen die darin liegenden Tools aus. Ein gemeinsames TBL-Testartefakt wird einmal erzeugt und mit nachgewiesen identischen Bytes auf allen vier Zieljobs verwendet. Separate BASIC-Verbraucher werden mit den entpackten TB-Tools verknüpft, als TBC ausgeführt und mit tbrt als EXE exportiert. Library-Quellen sind dabei nicht verfügbar. Erzeugung/Linken werden in einer Umgebung ohne erreichbaren C-/Rust-Compiler oder nativen Linker geprüft; Systemhilfen zur EXE-Finalisierung dürfen verfügbar bleiben. Fertige EXEs laufen anschließend ohne externe TBL/TBC oder TB-Installation.
|
4. Der Linux-Buildhost erzeugt/prüft Windows-Archive mit 7-Zip und Unix-Archive mit tar+gzip unter Erhalt der Executable-Bits. Inhalts-/Formatprüfungen laufen dort; tatsächliche Start-/Exportprüfungen verwenden neu entpackte Pakete auf passenden Zielsystemen aus 05, automatisiert oder dokumentiert manuell. Externe Nachweise müssen an Revision und exakte Paketprüfsumme gebunden und vor Freigabe geprüft sein. Ein gemeinsames TBL-Testartefakt wird einmal erzeugt und mit nachgewiesen identischen Bytes auf allen vier Zielsystemen verwendet. Separate BASIC-Verbraucher werden mit den entpackten TB-Tools verknüpft, als TBC ausgeführt und mit tbrt als EXE exportiert. Library-Quellen sind dabei nicht verfügbar. Erzeugung/Linken werden in einer Umgebung ohne erreichbaren C-/Rust-Compiler oder nativen Linker geprüft; Systemhilfen zur EXE-Finalisierung dürfen verfügbar bleiben. Fertige EXEs laufen anschließend ohne externe TBL/TBC oder TB-Installation.
|
||||||
5. In jedem Zieljob `cargo test --workspace`; Clippy/Formatprüfung zentral mit passender Toolchain, native Tests aus 02/03/04 und automatisierbare Plattformtests aus 05 auf ihren tatsächlichen Zielen. Den fixierten Fremdprogrammbestand ausdrücklich im erforderlichen Abnahmelauf bereitstellen. Compile-/VM-Messungen auf dokumentiertem Referenzrunner statt instabiler Debug-Zeitlimits in allen Jobs.
|
5. `cargo test --locked --workspace`, Clippy und Formatprüfung zentral auf dem Linux-arm64-Host; zielabhängige Rust-Testprogramme bei Bedarf cross-kompilieren und auf passenden Prüfsystemen ausführen. Native Tests aus 02/03/04 und automatisierbare Plattformtests aus 05 auf ihren tatsächlichen Zielen ausführen. Buildstatus und Ausführungsnachweise getrennt ausweisen; macOS-EXE-Finalisierung mit codesign erfolgt auf macOS. Den fixierten Fremdprogrammbestand ausdrücklich im erforderlichen Abnahmelauf bereitstellen. Compile-/VM-Messungen auf dokumentiertem Referenzrunner statt instabiler Debug-Zeitlimits in allen Jobs.
|
||||||
6. Gitea-Release erst nach allen Pflichtjobs und Vollständigkeits-/Prüfsummenprüfung freigeben. Upload zunächst zu einem als Entwurf erkennbaren Release oder gleichwertigem nicht freigegebenen Staging; anschließend atomare Freigabe im Rahmen der verfügbaren API. PR-Jobs ohne Veröffentlichungsrechte und ohne Release-Secrets; fremder PR-Code darf nicht mit solchen Rechten auf persönlichen Runnern laufen. Zugangswerte werden als Gitea-Secrets konfiguriert, nicht committed. Tests erzeugen keine öffentlichen Testreleases.
|
6. Gitea-Release erst nach allen Pflichtjobs, zugeordneten echten Zielnachweisen und Vollständigkeits-/Prüfsummenprüfung freigeben. Upload zunächst zu einem als Entwurf erkennbaren Release oder gleichwertigem nicht freigegebenen Staging; anschließend atomare Freigabe im Rahmen der verfügbaren API. PR-Jobs ohne Veröffentlichungsrechte und ohne Release-Secrets; fremder PR-Code darf nicht mit solchen Rechten auf persönlichen Runnern laufen. Zugangswerte werden als Gitea-Secrets konfiguriert, nicht committed. Tests erzeugen keine öffentlichen Testreleases.
|
||||||
7. README-/PLAN-/Releasehinweise nennen die tatsächlich gemessenen minimalen OS-/Systembibliotheksversionen. Developer-ID-/öffentliche Vertrauenssignatur ist kein Ersatz für lokale Ausführbarkeit; wenn entsprechende Identitäten nicht konfiguriert sind, deren Download-/Vertrauensgrenzen ehrlich dokumentieren. Mac-Artefakte müssen trotzdem lokal korrekt finalisiert und startbar sein.
|
7. README-/PLAN-/Releasehinweise nennen die tatsächlich gemessenen minimalen OS-/Systembibliotheksversionen. Developer-ID-/öffentliche Vertrauenssignatur ist kein Ersatz für lokale Ausführbarkeit; wenn entsprechende Identitäten nicht konfiguriert sind, deren Download-/Vertrauensgrenzen ehrlich dokumentieren. Mac-Artefakte müssen trotzdem lokal korrekt finalisiert und startbar sein.
|
||||||
|
|
||||||
## Risks / Trade-offs
|
## Risks / Trade-offs
|
||||||
|
|
||||||
- Native Runner fehlen → explizite Bereitstellungsaufgabe und fehlender Zieljob verhindern Abschluss; Workflowdateien allein sind kein CI-Nachweis.
|
- Cross-Toolchain/SDK fehlt oder ist inkompatibel → Buildbootstrap korrigieren und echte Vierzielpakete nachweisen. Fehlende Zielausführung auf Prüfsystemen sperrt weiterhin Abschluss/Release; erfolgreiche Cross-Builds allein reichen nicht.
|
||||||
- Vorlagen passen nicht zum Tool → Manifest-/Prüfsummen-/Versionscheck und Export aus entpacktem Paket.
|
- Vorlagen passen nicht zum Tool → Manifest-/Prüfsummen-/Versionscheck und Export aus entpacktem Paket.
|
||||||
- Plattformwerkzeuge variieren → Zieljob prüft tatsächliche 7z-/tar-/Rust-Build-/Signierwerkzeuge; keine stillen Ersatzformate.
|
- Plattformwerkzeuge variieren → Buildhost prüft Archiv-/Cross-Werkzeuge, Prüfsysteme prüfen benötigte Signierwerkzeuge; keine stillen Ersatzformate.
|
||||||
- Fehler beim Upload → vollständige Pflichtasset-Liste vor Freigabe prüfen; bestehende vollständige Releases nicht beschädigen.
|
- Fehler beim Upload → vollständige Pflichtasset-Liste vor Freigabe prüfen; bestehende vollständige Releases nicht beschädigen.
|
||||||
|
|
||||||
## Migration Plan
|
## Migration Plan
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ Das Repository am bestehenden Gitea-Origin enthält noch keine CI-/Release-Workf
|
|||||||
## What Changes
|
## What Changes
|
||||||
|
|
||||||
- Gitea Actions am bestehenden Origin für Push-/PR-Prüfungen und taggebundene Releases einrichten.
|
- Gitea Actions am bestehenden Origin für Push-/PR-Prüfungen und taggebundene Releases einrichten.
|
||||||
- Terminalanwendungen für Windows amd64, macOS arm64, Linux amd64 und Linux arm64 bauen; keine zusätzlichen Zielplattformen und kein Mirror.
|
- Auf dem vorhandenen Linux-arm64-Gitea-Runner Terminalanwendungen für Windows amd64 (cargo-xwin), macOS arm64 und Linux amd64 (cargo-zigbuild) sowie Linux arm64 bauen; keine zusätzlichen Zielplattformen und kein Mirror.
|
||||||
- Windows als `.7z`, macOS und beide Linux-Ziele als `.tar.gz` mit `tb`, `tbc` und der nativen `tbrt`-Vorlage samt Metadaten paketieren.
|
- Windows als `.7z`, macOS und beide Linux-Ziele als `.tar.gz` mit `tb`, `tbc` und der nativen `tbrt`-Vorlage samt Metadaten paketieren.
|
||||||
- Entpackte Pakete einschließlich `tbc link`, nativer Exporte und separater BASIC-Verbraucher von `.tbl` prüfen, Prüfsummen und nachvollziehbare Buildmetadaten liefern.
|
- Entpackte Pakete einschließlich `tbc link`, nativer Exporte und separater BASIC-Verbraucher von `.tbl` prüfen, Prüfsummen und nachvollziehbare Buildmetadaten liefern.
|
||||||
- Releases erst nach vollständigen erforderlichen Prüfungen veröffentlichen; fehlende Runner oder Assets dürfen keinen grünen vollständigen Release ergeben.
|
- Echte Zielausführung aus 05 getrennt vom Cross-Bau einbinden, automatisiert oder als revisions-/paketprüfsummengebundenen Nachweis von separaten Prüfsystemen. Vier native Buildrunner werden nicht vorausgesetzt.
|
||||||
|
- Releases erst nach vollständigen erforderlichen Prüfungen veröffentlichen; fehlende Zielnachweise oder Assets dürfen keinen grünen vollständigen Release ergeben.
|
||||||
|
|
||||||
## Capabilities
|
## Capabilities
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Liefert geprüfte Terminal-Basic-Programme und Exportvorlagen für die vier fest
|
|||||||
## ADDED Requirements
|
## ADDED Requirements
|
||||||
|
|
||||||
### Requirement: Vier verbindliche Buildziele
|
### Requirement: Vier verbindliche Buildziele
|
||||||
Gitea Actions am bestehenden Origin SHALL Terminalanwendungen für Windows amd64, macOS arm64, Linux amd64 und Linux arm64 aus derselben Revision bauen und prüfen. Fehlende Runner, fehlgeschlagene Tests oder fehlende Artefakte eines Pflichtziels MUST einen vollständigen erfolgreichen Release verhindern. Für normale Änderungen SHALL ein reproduzierbarer Build-/Teststatus vorliegen.
|
Gitea Actions am bestehenden Origin SHALL Terminalanwendungen für Windows amd64, macOS arm64, Linux amd64 und Linux arm64 aus derselben Revision auf dem vorhandenen Linux-arm64-Runner bauen und paketieren. Cross-Kompilierung SHALL für fremde Ziele verwendet werden; vier native Buildrunner MUST NOT vorausgesetzt werden. Buildprüfungen und echte Zielausführung SHALL getrennt nachvollziehbar sein. Fehlende Build- oder Ausführungsnachweise, fehlgeschlagene Tests oder fehlende Artefakte eines Pflichtziels MUST einen vollständigen erfolgreichen Release verhindern. Für normale Änderungen SHALL ein reproduzierbarer Build-/Teststatus vorliegen.
|
||||||
|
|
||||||
#### Scenario: Ein Ziel fehlt
|
#### Scenario: Ein Ziel fehlt
|
||||||
- **WHEN** drei Zieljobs erfolgreich sind und der vierte fehlt oder fehlschlägt
|
- **WHEN** drei Zieljobs erfolgreich sind und der vierte fehlt oder fehlschlägt
|
||||||
@@ -19,14 +19,14 @@ Windows SHALL als `.7z` ausgeliefert werden; macOS und beide Linux-Architekturen
|
|||||||
- **THEN** starten IDE und Compiler als native Terminalanwendungen und ein Beispielprojekt kann mit den mitgelieferten Vorlagen exportiert und nativ verwendet werden
|
- **THEN** starten IDE und Compiler als native Terminalanwendungen und ein Beispielprojekt kann mit den mitgelieferten Vorlagen exportiert und nativ verwendet werden
|
||||||
|
|
||||||
### Requirement: Tatsächliche Artefaktprüfung
|
### Requirement: Tatsächliche Artefaktprüfung
|
||||||
Die Actions SHALL die fertig gepackten und erneut entpackten Artefakte auf dem jeweiligen Ziel testen. Native Executables SHALL ohne TB-Installation und Quellen laufen, Library-Ausgaben SHALL mit separaten BASIC-Verbrauchern über `tbc link` ohne Library-Quellen funktionieren. Dieselbe TBL-Probe SHALL mit identischen Bytes auf allen vier Zielen verknüpft und ausgeführt werden. Library-Bau und Link-/EXE-Export SHALL mit den entpackten Tools ohne C-/Rust-Compiler oder nativen Linker beim Anwender geprüft werden; dokumentierte Systemhilfen zur EXE-Finalisierung dürfen vorhanden sein. Ein erfolgreiches Cargo-Kompilieren oder korrekt benannter Archivname MUST NOT diese Prüfung ersetzen.
|
Der Actions-Releasepfad SHALL tatsächliche Prüfungen der fertig gepackten und erneut entpackten Artefakte auf dem jeweiligen Ziel verlangen. Diese SHALL als automatisierte Zielprüfungen oder dokumentierte manuelle Nachweise von separaten Prüfsystemen aus Change 05 eingebunden und vor Freigabe an die geprüfte Revision und exakte Paketprüfsumme gebunden werden. Fehlende oder nicht passende Nachweise MUST die Freigabe verhindern. Native Executables SHALL ohne TB-Installation und Quellen laufen, Library-Ausgaben SHALL mit separaten BASIC-Verbrauchern über `tbc link` ohne Library-Quellen funktionieren. Dieselbe TBL-Probe SHALL mit identischen Bytes auf allen vier Zielen verknüpft und ausgeführt werden. Library-Bau und Link-/EXE-Export SHALL mit den entpackten Tools ohne C-/Rust-Compiler oder nativen Linker beim Anwender geprüft werden; dokumentierte Systemhilfen zur EXE-Finalisierung dürfen vorhanden sein. Ein erfolgreiches Cargo-Kompilieren oder korrekt benannter Archivname MUST NOT diese Prüfung ersetzen.
|
||||||
|
|
||||||
#### Scenario: Falsches Binary im richtigen Archiv
|
#### Scenario: Falsches Binary im richtigen Archiv
|
||||||
- **WHEN** ein Windows-amd64-Archiv versehentlich ein Binary eines anderen Ziels enthält
|
- **WHEN** ein Windows-amd64-Archiv versehentlich ein Binary eines anderen Ziels enthält
|
||||||
- **THEN** scheitert die Format-/Startprüfung vor der Veröffentlichung
|
- **THEN** scheitert die Format-/Startprüfung vor der Veröffentlichung
|
||||||
|
|
||||||
### Requirement: Nachvollziehbare Veröffentlichung
|
### Requirement: Nachvollziehbare Veröffentlichung
|
||||||
Releaseartefakte SHALL Version/Tag, Commit, Toolchain, Ziel, Integritätsprüfsumme und zugehörige Prüfergebnisse nachvollziehbar machen. Nur der freigegebene Tag-/Releasepfad SHALL Veröffentlichungsrechte verwenden; PR-Prüfungen MUST keine Release-Secrets benötigen. Wiederholte fehlgeschlagene Veröffentlichungen SHALL keinen bestehenden vollständigen Release stillschweigend durch unvollständige Assets ersetzen.
|
Releaseartefakte SHALL Version/Tag, Commit, Toolchain, Buildhost, Ziel, Integritätsprüfsumme und zugehörige Prüfergebnisse nachvollziehbar machen. Nur der freigegebene Tag-/Releasepfad SHALL Veröffentlichungsrechte verwenden; PR-Prüfungen MUST keine Release-Secrets benötigen. Wiederholte fehlgeschlagene Veröffentlichungen SHALL keinen bestehenden vollständigen Release stillschweigend durch unvollständige Assets ersetzen.
|
||||||
|
|
||||||
#### Scenario: Paket beschädigt oder Upload unvollständig
|
#### Scenario: Paket beschädigt oder Upload unvollständig
|
||||||
- **WHEN** ein Archiv nach dem Bau verändert wird oder ein Pflichtasset nicht hochgeladen werden kann
|
- **WHEN** ein Archiv nach dem Bau verändert wird oder ein Pflichtasset nicht hochgeladen werden kann
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
## 1. Gitea und Runner
|
## 1. Gitea und Runner
|
||||||
|
|
||||||
- [ ] 1.1 Verfügbare Gitea-1.25-kompatible Runner und Labels für Windows amd64, macOS arm64, Linux amd64/arm64 inventarisieren und fehlende Zielrunner anbinden; ein kleiner Gitea-Prüfjob muss tatsächliches OS/Architektur je Ziel nachweisen.
|
- [ ] 1.1 Vorhandenen Linux-arm64-Gitea-Runner und tatsächliches Label bestätigen; Cross-Buildumgebungen mit cargo-zigbuild, cargo-xwin und benötigten SDKs/Sysroots reproduzierbar festlegen. Ein Gitea-Prüfjob muss den Buildhost nachweisen; separate echte Prüfsysteme aus 05 zuordnen, ohne vier native Buildrunner vorauszusetzen.
|
||||||
- [ ] 1.2 Repositorylokale Build-/Testaufrufe und feste Toolchain-/Systemvoraussetzungen bereitstellen; jeder Aufruf muss lokal auf seinem Ziel mit --locked bauen und erforderliche Regressionen starten können.
|
- [ ] 1.2 Repositorylokale Build-/Testaufrufe und feste Toolchain-/Systemvoraussetzungen bereitstellen; alle vier Ziele müssen auf Linux arm64 mit --locked baubar sein. Zentrale Workspace-Regressionen und getrennte Aufrufe für echte Zielprüfungen bereitstellen; fremde Executables nicht als nativ auf dem Buildhost getestet ausweisen.
|
||||||
- [ ] 1.3 Push-/PR-Workflow unter .gitea/workflows anbinden; ein tatsächlicher Origin-Lauf muss vier Zielergebnisse und Fehlerstatus liefern, ohne Release-Secrets im PR-Pfad.
|
- [ ] 1.3 Push-/PR-Workflow unter .gitea/workflows anbinden; ein tatsächlicher Origin-Lauf muss vier Zielergebnisse und Fehlerstatus liefern, ohne Release-Secrets im PR-Pfad.
|
||||||
|
|
||||||
## 2. Artefakte und Pakete
|
## 2. Artefakte und Pakete
|
||||||
|
|
||||||
- [ ] 2.1 IDE, CLI und native `tbrt`-Exportvorlage je Ziel in Gitea Actions bauen; Format-/Architektur-/Versionsprüfungen müssen Windows amd64, macOS arm64 und beide Linux-Architekturen eindeutig bestätigen.
|
- [ ] 2.1 IDE, CLI und native `tbrt`-Exportvorlage je Ziel in Gitea Actions bauen; Format-/Architektur-/Versionsprüfungen müssen Windows amd64, macOS arm64 und beide Linux-Architekturen eindeutig bestätigen.
|
||||||
- [ ] 2.2 Windows-Paket als .7z und macOS-/Linux-Pakete als .tar.gz erzeugen; Archivtests müssen Dateinamen, Paketinhalt, Executable-Rechte und notwendige Begleitdateien prüfen.
|
- [ ] 2.2 Windows-Paket als .7z und macOS-/Linux-Pakete als .tar.gz erzeugen; Archivtests müssen Dateinamen, Paketinhalt, Executable-Rechte und notwendige Begleitdateien prüfen.
|
||||||
- [ ] 2.3 Jedes fertige Paket neu entpacken und die enthaltenen Programme starten; `tbc link` und EXE-/TBL-Beispiele müssen mit separaten BASIC-Verbrauchern ohne Library-Quellen/Entwicklungscheckout sowie ohne erreichbaren C-/Rust-Compiler oder nativen Linker funktionieren.
|
- [ ] 2.3 Jedes fertige Paket auf dem tatsächlichen Ziel neu entpacken und die enthaltenen Programme starten; automatisierte oder dokumentiert manuelle Nachweise aus separaten Prüfsystemen an Revision und Paketprüfsumme binden; `tbc link` und EXE-/TBL-Beispiele müssen mit separaten BASIC-Verbrauchern ohne Library-Quellen/Entwicklungscheckout sowie ohne erreichbaren C-/Rust-Compiler oder nativen Linker funktionieren.
|
||||||
- [ ] 2.4 Einmal erzeugte TBL-Probe mit identischen Bytes an alle vier Zielprüfungen verteilen sowie Artefaktmanifest und Prüfsummen erzeugen; absichtlich beschädigte oder vertauschte Zielartefakte müssen die Prüfung scheitern lassen.
|
- [ ] 2.4 Einmal erzeugte TBL-Probe mit identischen Bytes an alle vier Zielprüfungen verteilen sowie Artefaktmanifest und Prüfsummen erzeugen; absichtlich beschädigte oder vertauschte Zielartefakte müssen die Prüfung scheitern lassen.
|
||||||
|
|
||||||
## 3. Releasepfad
|
## 3. Releasepfad
|
||||||
|
|
||||||
- [ ] 3.1 Taggebundenen Gitea-Releasepfad mit vollständiger Pflichtasset-Liste und getrennten Rechten einrichten; fehlgeschlagene/fehlende Zieljobs müssen die Freigabe verhindern.
|
- [ ] 3.1 Taggebundenen Gitea-Releasepfad mit vollständiger Pflichtasset-Liste und getrennten Rechten einrichten; fehlgeschlagene/fehlende Zielbuilds oder zugeordnete echte Paket-/Zielprüfungen müssen die Freigabe verhindern.
|
||||||
- [ ] 3.2 Upload zunächst als nicht freigegebenen Stand prüfen und erst vollständig freigeben; ein simulierter fehlender Upload darf keinen vollständigen bestehenden Release überschreiben.
|
- [ ] 3.2 Upload zunächst als nicht freigegebenen Stand prüfen und erst vollständig freigeben; ein simulierter fehlender Upload darf keinen vollständigen bestehenden Release überschreiben.
|
||||||
- [ ] 3.3 Fremdprogramm-, Inventar-, native und automatisierte Plattformnachweise sowie Referenzbenchmarks in den erforderlichen Abnahmelauf integrieren; ignorierte Pflichtfälle müssen als fehlend sichtbar bleiben.
|
- [ ] 3.3 Fremdprogramm-, Inventar-, native und automatisierte Plattformnachweise sowie Referenzbenchmarks in den erforderlichen Abnahmelauf integrieren; ignorierte Pflichtfälle müssen als fehlend sichtbar bleiben.
|
||||||
- [ ] 3.4 Einen tatsächlichen vollständigen Gitea-Lauf mit vier Paketartefakten nachweisen; Run-/Commit-/Targetdaten und entpackte Prüfergebnisse an 07 übergeben, bevor der Change als fertig gilt.
|
- [ ] 3.4 Einen tatsächlichen vollständigen Gitea-Lauf mit vier Paketartefakten nachweisen; Run-/Commit-/Buildhost-/Targetdaten und paketprüfsummengebundene entpackte Prüfergebnisse an 07 übergeben, bevor der Change als fertig gilt.
|
||||||
|
|||||||
@@ -14,5 +14,5 @@
|
|||||||
|
|
||||||
## 3. Phasenabschluss
|
## 3. Phasenabschluss
|
||||||
|
|
||||||
- [ ] 3.1 Alle acht PLAN-Punkte einer bestandenen Evidenz zuordnen und erst dann abhaken; Bericht muss Gitea-Run-/Releasebezug, vier Targets und verbleibende Stufe-2-Abgrenzung zeigen.
|
- [ ] 3.1 Alle acht PLAN-Punkte einer bestandenen Evidenz zuordnen und erst dann abhaken; Bericht muss Gitea-Run-/Releasebezug, zentralen Cross-Paketbau und davon getrennte echte Zielausführung, vier Targets und verbleibende Stufe-2-Abgrenzung zeigen.
|
||||||
- [ ] 3.2 Abschließende Doku-/Help-/OpenSpec-Konsistenz prüfen; keine unaufgelösten Links, widersprüchlichen Exporthinweise oder offenen Phase-6-Befunde dürfen verbleiben.
|
- [ ] 3.2 Abschließende Doku-/Help-/OpenSpec-Konsistenz prüfen; keine unaufgelösten Links, widersprüchlichen Exporthinweise oder offenen Phase-6-Befunde dürfen verbleiben.
|
||||||
|
|||||||
53
openspec/specs/pcode-bibliotheken/spec.md
Normal file
53
openspec/specs/pcode-bibliotheken/spec.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# pcode-bibliotheken Specification
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Ermöglicht die quellfreie Wiederverwendung übersetzter BASIC-Module als portable TBL-Bibliotheken und deren Verknüpfung zu vollständigen Programmen für dieselbe TBVM.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Requirement: Portables TBL-Bibliotheksformat
|
||||||
|
`tbc build --library` und Make Library SHALL eine `.tbl` mit übersetzten BASIC-Modulen, exportierten Deklarationen, typisierten Imports sowie den benötigten Daten und Forms-Ressourcen erzeugen. Sie SHALL ohne ihre ursprünglichen BAS-/FRM-/Include-Quellen weiterverwendbar sein. Das Format SHALL versioniert und unabhängig von Windows/Linux/macOS sowie amd64/arm64 sein. Native Runtime und plattformspezifischer Maschinencode MUST NOT Bestandteil der TBL sein; die native Runtime SHALL im Ziel-`tbrt` liegen.
|
||||||
|
|
||||||
|
#### Scenario: Bibliothek auf einem anderen Ziel verwenden
|
||||||
|
- **WHEN** dieselben TBL-Bytes mit kompatiblen Terminal-Basic-Versionen für die vier Releaseziele verknüpft werden
|
||||||
|
- **THEN** liefern die Programme bei gleichen Eingaben dieselben BASIC-Ergebnisse ohne Neubau der Bibliothek aus Quellen
|
||||||
|
|
||||||
|
### Requirement: Gemeinsamer Linkbefehl
|
||||||
|
`tbc link` SHALL ein Hauptprojekt und angegebene TBL-Bibliotheken zu einem vollständig aufgelösten TBC-Programm verbinden. Mit `--exe` SHALL derselbe Befehl dieses Programm in die passende vorgebaute `tbrt`-Vorlage einbetten und ein natives eigenständiges Executable erzeugen. `tbc build`, `run` und `check` SHALL bei Projekten mit TBL-Verweisen dieselbe Auflösung verwenden. Erzeugen und Linken SHALL ohne C-/Rust-Compiler oder nativen Linker beim Anwender möglich sein; dokumentierte Systemvoraussetzungen zur EXE-Finalisierung bleiben anwendbar.
|
||||||
|
|
||||||
|
#### Scenario: Quellfreie Bibliotheksnutzung bis zur EXE
|
||||||
|
- **WHEN** nach dem TBL-Bau deren Quellen entfernt werden und ein Verbraucherprojekt über `tbc link --exe` verknüpft wird
|
||||||
|
- **THEN** läuft das Executable anschließend ohne separate TBL/TBC, separat installiertes tbrt/tb/tbc oder Buildtoolchain mit dem korrekten Bibliotheksverhalten
|
||||||
|
|
||||||
|
### Requirement: Explizite Bibliothekszuordnung
|
||||||
|
TBL-Dateien SHALL als geordnete MAK-Einträge relativ zur Projektdatei mit der bestehenden DOS-großschreibungsunabhängigen Pfadauflösung verwendbar sein. `tbc link` SHALL zusätzliche TBL-Eingaben ausdrücklich annehmen. Bibliotheksmodule SHALL ihre Identität und Deklarationen behalten; ein TBL-Eintrag MUST NOT als editierbare Quelltextdatei oder als Startdatei interpretiert werden. Externe Bibliotheksabhängigkeiten SHALL aus den angegebenen Eingaben aufgelöst werden; fehlende Abhängigkeiten MUST vor Programmausgabe diagnostiziert werden.
|
||||||
|
|
||||||
|
#### Scenario: Projekt mit verschobener Bibliothek
|
||||||
|
- **WHEN** ein Projekt und seine relativ referenzierte TBL gemeinsam in ein anderes Verzeichnis verschoben werden
|
||||||
|
- **THEN** bleibt das Projekt ohne Bibliotheksquellen verknüpfbar; fehlt stattdessen die TBL, nennt die Diagnose die fehlende Datei und ihren Projektbezug
|
||||||
|
|
||||||
|
### Requirement: Eindeutige und typgeprüfte Verknüpfung
|
||||||
|
Der Linkpfad SHALL Imports eindeutig mit kompatiblen Exporten verbinden und die bestehenden Regeln für Namen, Konstanten, UDTs, COMMON sowie Prozedursignaturen erhalten. Fehlende Definitionen, mehrdeutige Exporte, inkompatible Parameter-/Ergebnistypen, doppelte Modulidentitäten und überschrittene Formatgrenzen SHALL vor Programmausführung mit konkretem Bezug abgewiesen werden. Bibliotheken SHALL typisierte offene Imports speichern können; ein ausführbares Endprodukt MUST NOT unaufgelöste Imports enthalten.
|
||||||
|
|
||||||
|
#### Scenario: Bibliothek benötigt eine weitere Bibliothek
|
||||||
|
- **WHEN** Bibliothek A eine typisierte Prozedur aus B importiert und nur A zum Verbraucher angegeben wird
|
||||||
|
- **THEN** scheitert das Linken mit dem fehlenden Symbol; nach Hinzufügen eines signaturkompatiblen B funktioniert der Aufruf, während ein inkompatibles B eine Typdiagnose auslöst
|
||||||
|
|
||||||
|
#### Scenario: Mehrdeutiges Symbol
|
||||||
|
- **WHEN** zwei eingebundene Module einen nach bestehenden Sprachregeln mehrdeutigen Export anbieten
|
||||||
|
- **THEN** meldet der Linker den Namenskonflikt mit beiden Herkunftsmodulen und wählt keine zufällige Definition
|
||||||
|
|
||||||
|
### Requirement: Erhaltene BASIC- und Laufzeitsemantik
|
||||||
|
Verknüpfte Bibliotheksmodule SHALL dieselben Wert-, BYREF-/BYVAL-, Array-, UDT-, COMMON-, DATA-/RESTORE-, Forms- und Fehlerregeln wie entsprechende Quellmodule erfüllen. Globale Anfangswerte SHALL vor Ausführung bereitstehen; Modulrümpfe SHALL in der dokumentierten erweiterten Projektreihenfolge mit demselben Kontrollfluss wie die entsprechenden Quellmodule ausgeführt werden; bloße Symbolverweise SHALL keine zusätzliche Modulinitialisierung auslösen. Die Startauswahl SHALL dem Hauptprojekt gehören. RUN ohne Ziel SHALL auch den Bibliothekszustand neu initialisieren. Physische Fehlerorte und BASIC-Zeilenlabels SHALL trotz fehlender Bibliotheksquellen erhalten bleiben.
|
||||||
|
|
||||||
|
#### Scenario: Quellmodule durch TBL ersetzen
|
||||||
|
- **WHEN** Bibliotheksmodule eines Projekts durch ihre TBL in derselben Modulreihenfolge ersetzt und direkte sowie verknüpfte Variante mit gleichen Ereignissen gestartet werden
|
||||||
|
- **THEN** stimmen Initialisierung, BYREF-Änderungen, strukturierte Werte, Forms-Ereignisse und Fehlerorte überein; nach RUN besitzen beide denselben frischen Anfangszustand
|
||||||
|
|
||||||
|
### Requirement: Validierung und geschützte Ausgabe
|
||||||
|
TBL-Lader und Linker SHALL Format-/P-Code-/Runtime-Kompatibilität, Grenzen und alle Tabellen-/Symbolverweise prüfen. Beschädigte oder inkompatible Bibliotheken SHALL kontrolliert abgewiesen werden. Fehler, Abbruch oder Ausgabekonflikte SHALL bestehende Ausgaben und sämtliche Eingaben einschließlich Bibliotheken erhalten. Erfolg SHALL erst nach vollständiger validierter Veröffentlichung gemeldet werden.
|
||||||
|
|
||||||
|
#### Scenario: Beschädigter Container oder Ausgabe auf Eingabe
|
||||||
|
- **WHEN** eine abgeschnittene TBL geladen wird oder das Linkziel eine der Eingabebibliotheken überschreiben würde
|
||||||
|
- **THEN** scheitert der Vorgang mit konkreter Diagnose, führt keinen BASIC-Code aus und erhält bestehende Eingaben und Ausgaben
|
||||||
17
tests/libraries/portable.bas
Normal file
17
tests/libraries/portable.bas
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
CONST WIDTH=2
|
||||||
|
TYPE Record
|
||||||
|
value AS LONG
|
||||||
|
text AS STRING * 4
|
||||||
|
END TYPE
|
||||||
|
COMMON SHARED shared&
|
||||||
|
DIM SHARED state&
|
||||||
|
DATA 7
|
||||||
|
SUB Work(a&(), r AS Record, n&)
|
||||||
|
state&=state&+1
|
||||||
|
shared&=shared&+1
|
||||||
|
READ a&(1)
|
||||||
|
r.value=r.value+n&
|
||||||
|
r.text="TB"
|
||||||
|
n&=9
|
||||||
|
PRINT state&
|
||||||
|
END SUB
|
||||||
BIN
tests/libraries/portable.tbl
Normal file
BIN
tests/libraries/portable.tbl
Normal file
Binary file not shown.
92
tests/support/library-abnahme.py
Normal file
92
tests/support/library-abnahme.py
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Dieselben TBL-Bytes auf jedem Releaseziel: Linken und echte Standalone-Ausführung."""
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import platform
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
DIGEST = '5d25c2f8c95ae535e55a6c84c1ddd0d964358863f4f3c780c50ac4a2eb3594df'
|
||||||
|
TARGETS = {
|
||||||
|
('Windows', 'AMD64'): 'x86_64-pc-windows-msvc',
|
||||||
|
('Darwin', 'arm64'): 'aarch64-apple-darwin',
|
||||||
|
('Linux', 'x86_64'): 'x86_64-unknown-linux-gnu',
|
||||||
|
('Linux', 'aarch64'): 'aarch64-unknown-linux-gnu',
|
||||||
|
}
|
||||||
|
SOURCE = '''COMMON SHARED shared&
|
||||||
|
OPEN "round.txt" FOR APPEND AS #1
|
||||||
|
old&=LOF(1)
|
||||||
|
PRINT #1, "x"
|
||||||
|
CLOSE #1
|
||||||
|
DIM a&(WIDTH)
|
||||||
|
DIM r AS Record
|
||||||
|
n&=3
|
||||||
|
CALL Work(a&(),r,n&)
|
||||||
|
PRINT a&(1);r.value;n&;shared&
|
||||||
|
PRINT r.text
|
||||||
|
IF old&=0 THEN RUN
|
||||||
|
END
|
||||||
|
'''
|
||||||
|
EXPECTED = b' 1 \n 7 3 9 1 \nTB \n' # RUN setzt auch den Textbildschirm zurück.
|
||||||
|
|
||||||
|
|
||||||
|
def run(args, cwd, env):
|
||||||
|
result = subprocess.run([str(a) for a in args], cwd=cwd, env=env,
|
||||||
|
input=b'', capture_output=True, timeout=30)
|
||||||
|
assert result.returncode == 0, (result.args, result.returncode, result.stdout, result.stderr)
|
||||||
|
return result.stdout.replace(b'\r\n', b'\n')
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument('--bin-dir', type=Path, default=Path('target/release'))
|
||||||
|
parser.add_argument('--library', type=Path, default=Path(__file__).resolve().parents[1] / 'libraries/portable.tbl')
|
||||||
|
args = parser.parse_args()
|
||||||
|
target = TARGETS.get((platform.system(), platform.machine()))
|
||||||
|
assert target, f'Kein Releaseziel: {platform.system()} {platform.machine()}'
|
||||||
|
digest = hashlib.sha256(args.library.read_bytes()).hexdigest()
|
||||||
|
assert digest == DIGEST, f'Gemeinsames TBL-Artefakt verändert: {digest}'
|
||||||
|
binaries = args.bin_dir.resolve()
|
||||||
|
suffix = '.exe' if platform.system() == 'Windows' else ''
|
||||||
|
compiler = binaries / ('tbc' + suffix)
|
||||||
|
template_tool = binaries / ('tb-template' + suffix)
|
||||||
|
env = dict(os.environ, PATH='')
|
||||||
|
with tempfile.TemporaryDirectory(prefix='tb-library-') as work:
|
||||||
|
root = Path(work)
|
||||||
|
reference, clean = root / 'reference', root / 'clean'
|
||||||
|
reference.mkdir()
|
||||||
|
clean.mkdir()
|
||||||
|
library = root / 'library.tbl'
|
||||||
|
shutil.copyfile(args.library, library)
|
||||||
|
template = root / ('runtime' + suffix)
|
||||||
|
shutil.copy2(binaries / ('tbrt' + suffix), template)
|
||||||
|
run([template_tool, template, target], root, env)
|
||||||
|
source = root / 'consumer.bas'
|
||||||
|
source.write_text(SOURCE, encoding='utf-8')
|
||||||
|
linked = root / 'consumer.tbc'
|
||||||
|
run([compiler, 'link', source, library, '-o', linked], root, env)
|
||||||
|
expected = run([compiler, 'run', linked], reference, env)
|
||||||
|
assert expected == EXPECTED, expected
|
||||||
|
executable = clean / ('consumer' + suffix)
|
||||||
|
run([compiler, 'link', source, library, '--exe', '--target', target,
|
||||||
|
'--template', template, '-o', executable], root, env)
|
||||||
|
source.unlink()
|
||||||
|
library.unlink()
|
||||||
|
linked.unlink()
|
||||||
|
template.unlink()
|
||||||
|
Path(str(template) + '.meta').unlink()
|
||||||
|
actual = run([executable], clean, env)
|
||||||
|
assert actual == EXPECTED, actual
|
||||||
|
assert (reference / 'round.txt').read_bytes() == (clean / 'round.txt').read_bytes()
|
||||||
|
assert (clean / 'round.txt').read_text().splitlines() == ['x', 'x']
|
||||||
|
assert sorted(p.name for p in clean.iterdir()) == ['consumer' + suffix, 'round.txt']
|
||||||
|
print(json.dumps({'target': target, 'tbl_sha256': digest, 'native_execution': True,
|
||||||
|
'source_free': True, 'run_reset': True, 'output': actual.decode()}, ensure_ascii=False))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user