Projektmodule und vollständiges TBC-Kompilat umsetzen und Change archivieren

This commit is contained in:
2026-09-05 23:06:56 +02:00
parent 58b1f620ea
commit 815825dde7
40 changed files with 4177 additions and 555 deletions

View File

@@ -1,7 +1,7 @@
//! `tbc` — Standalone-Compiler von Terminal Basic. //! `tbc` — Standalone-Compiler von Terminal Basic.
//! //!
//! Unterbefehle (Phase 2): //! Unterbefehle (Phase 2):
//! - `tbc run <datei.bas|datei.frm|projekt.mak>` Kompilieren und ausführen //! - `tbc run <datei.bas|datei.frm|projekt.mak|datei.tbc>` Kompilieren und ausführen
//! - `tbc build <datei.bas|datei.frm|projekt.mak>` Zu `.tbc` kompilieren //! - `tbc build <datei.bas|datei.frm|projekt.mak>` Zu `.tbc` kompilieren
//! - `tbc check <datei.bas|datei.frm|projekt.mak>` Syntax/Semantik prüfen //! - `tbc check <datei.bas|datei.frm|projekt.mak>` Syntax/Semantik prüfen
//! - `tbc convert-frm <quelle.frm> <ziel.frm>` Binärformular in Text wandeln //! - `tbc convert-frm <quelle.frm> <ziel.frm>` Binärformular in Text wandeln
@@ -10,7 +10,6 @@
//! 0 = END/SYSTEM/Programmende · 3 = STOP · 2 = Laufzeitfehler · //! 0 = END/SYSTEM/Programmende · 3 = STOP · 2 = Laufzeitfehler ·
//! 1 = Compile-Fehler/Bedienfehler. //! 1 = Compile-Fehler/Bedienfehler.
use std::collections::HashSet;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::ExitCode; use std::process::ExitCode;
use tb_runtime::host::{Ereignis, Host}; use tb_runtime::host::{Ereignis, Host};
@@ -26,7 +25,7 @@ fn main() -> ExitCode {
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> | tbc convert-frm <quelle.frm> <ziel.frm>" "Aufruf: tbc run|build|check <datei.bas|datei.frm|projekt.mak|datei.tbc> | tbc convert-frm <quelle.frm> <ziel.frm>"
); );
ExitCode::from(1) ExitCode::from(1)
} }
@@ -128,33 +127,47 @@ fn include_name(line: &str) -> Option<String> {
Some(rest.split('\'').next().unwrap_or_default().to_string()) Some(rest.split('\'').next().unwrap_or_default().to_string())
} }
fn expand_includes(path: &Path, source: &str, stack: &mut Vec<PathBuf>) -> Result<String, String> { fn expand_includes(
path: &Path,
source: &str,
first_line: u32,
stack: &mut Vec<PathBuf>,
) -> Result<Vec<tb_frontend::source::SourceSegment>, String> {
let canonical = path.canonicalize().unwrap_or_else(|_| path.to_path_buf()); let canonical = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
if stack.contains(&canonical) { if stack.contains(&canonical) {
return Err(format!("{}: zyklisches $INCLUDE", path.display())); return Err(format!("{}: zyklisches $INCLUDE", path.display()));
} }
stack.push(canonical); stack.push(canonical);
let mut out = String::new(); let mut segments = vec![tb_frontend::source::SourceSegment {
for line in source.split_inclusive('\n') { file: path.display().to_string(),
first_line,
text: String::new(),
}];
for (line_no, line) in source.split_inclusive('\n').enumerate() {
if let Some(name) = include_name(line) { if let Some(name) = include_name(line) {
if name.is_empty() { if name.is_empty() {
return Err(format!("{}: leeres $INCLUDE", path.display())); return Err(format!(
"{}:{}: leeres $INCLUDE",
path.display(),
first_line + line_no as u32
));
} }
let included = let included =
relative_case_insensitive(path.parent().unwrap_or(Path::new(".")), &name) relative_case_insensitive(path.parent().unwrap_or(Path::new(".")), &name)
.map_err(|error| format!("{}: {error}", path.display()))?; .map_err(|e| format!("{}: {e}", path.display()))?;
let text = std::fs::read_to_string(&included) let text = std::fs::read_to_string(&included)
.map_err(|error| format!("{}: {error}", included.display()))?; .map_err(|e| format!("{}: {e}", included.display()))?;
out.push_str(&expand_includes(&included, &text, stack)?); segments.extend(expand_includes(&included, &text, 1, stack)?);
if !out.ends_with('\n') {
out.push('\n');
}
} else { } else {
out.push_str(line); segments.push(tb_frontend::source::SourceSegment {
file: path.display().to_string(),
first_line: first_line + line_no as u32,
text: line.into(),
});
} }
} }
stack.pop(); stack.pop();
Ok(out) Ok(segments)
} }
fn read_form(path: &Path) -> Result<tb_ui::frm::FormFile, String> { fn read_form(path: &Path) -> Result<tb_ui::frm::FormFile, String> {
@@ -162,9 +175,8 @@ fn read_form(path: &Path) -> Result<tb_ui::frm::FormFile, String> {
if bytes.starts_with(b"VERSION ") || bytes.starts_with(b"Version ") { if bytes.starts_with(b"VERSION ") || bytes.starts_with(b"Version ") {
let source = std::str::from_utf8(&bytes) let source = std::str::from_utf8(&bytes)
.map_err(|_| format!("{}: ungültige Textkodierung", path.display()))?; .map_err(|_| format!("{}: ungültige Textkodierung", path.display()))?;
let mut form = tb_ui::frm::read_text(&path.display().to_string(), source) let form = tb_ui::frm::read_text(&path.display().to_string(), source)
.map_err(|error| error.to_string())?; .map_err(|error| error.to_string())?;
form.code = expand_includes(path, &form.code, &mut Vec::new())?;
Ok(form) Ok(form)
} else { } else {
tb_ui::frm::read_binary(&path.display().to_string(), &bytes) tb_ui::frm::read_binary(&path.display().to_string(), &bytes)
@@ -173,65 +185,15 @@ fn read_form(path: &Path) -> Result<tb_ui::frm::FormFile, String> {
} }
} }
#[derive(Default)] fn input_sources(
struct ProjectSymbols { path: &Path,
constants: HashSet<String>, ) -> Result<
types: HashSet<String>, (
} Vec<tb_frontend::source::SourceUnit>,
Vec<tb_ui::frm::FormFile>,
fn append_project_source(target: &mut String, source: &str, symbols: &mut ProjectSymbols) { ),
let mut in_proc = false; String,
let mut skip_type = false; > {
for line in source.split_inclusive('\n') {
let upper = line.trim().to_ascii_uppercase();
if skip_type {
if upper == "END TYPE" {
skip_type = false;
}
continue;
}
if !in_proc {
if let Some(name) = upper
.strip_prefix("TYPE ")
.and_then(|rest| rest.split_whitespace().next())
{
if !symbols.types.insert(name.to_string()) {
skip_type = true;
continue;
}
}
if let Some(rest) = upper.strip_prefix("CONST ") {
let names = rest
.split(',')
.filter_map(|part| part.split('=').next())
.map(str::trim)
.filter(|name| !name.is_empty())
.collect::<Vec<_>>();
if !names.is_empty() && names.iter().all(|name| symbols.constants.contains(*name)) {
continue;
}
symbols
.constants
.extend(names.into_iter().map(str::to_string));
}
}
target.push_str(line);
if upper.starts_with("SUB ")
|| upper.starts_with("FUNCTION ")
|| upper.starts_with("STATIC SUB ")
|| upper.starts_with("STATIC FUNCTION ")
{
in_proc = true;
} else if matches!(upper.as_str(), "END SUB" | "END FUNCTION") {
in_proc = false;
}
}
if !target.ends_with('\n') {
target.push('\n');
}
}
fn input_sources(path: &Path) -> Result<(String, Vec<tb_ui::frm::FormFile>), String> {
let extension = path.extension().and_then(|ext| ext.to_str()).unwrap_or(""); let extension = path.extension().and_then(|ext| ext.to_str()).unwrap_or("");
let paths = if extension.eq_ignore_ascii_case("mak") { let paths = if extension.eq_ignore_ascii_case("mak") {
let project = std::fs::read_to_string(path) let project = std::fs::read_to_string(path)
@@ -240,7 +202,7 @@ fn input_sources(path: &Path) -> Result<(String, Vec<tb_ui::frm::FormFile>), Str
project project
.lines() .lines()
.map(str::trim) .map(str::trim)
.filter(|line| !line.is_empty()) .filter(|line| !line.is_empty() && !line.starts_with('\''))
.map(|line| { .map(|line| {
relative_case_insensitive(base, line) relative_case_insensitive(base, line)
.map_err(|error| format!("{}: {error}", path.display())) .map_err(|error| format!("{}: {error}", path.display()))
@@ -249,9 +211,8 @@ fn input_sources(path: &Path) -> Result<(String, Vec<tb_ui::frm::FormFile>), Str
} else { } else {
vec![path.to_path_buf()] vec![path.to_path_buf()]
}; };
let mut source = String::new(); let mut source = Vec::new();
let mut forms = Vec::new(); let mut forms = Vec::new();
let mut symbols = ProjectSymbols::default();
for member in paths { for member in paths {
if member if member
.extension() .extension()
@@ -259,13 +220,18 @@ fn input_sources(path: &Path) -> Result<(String, Vec<tb_ui::frm::FormFile>), Str
.is_some_and(|ext| ext.eq_ignore_ascii_case("frm")) .is_some_and(|ext| ext.eq_ignore_ascii_case("frm"))
{ {
let form = read_form(&member)?; let form = read_form(&member)?;
append_project_source(&mut source, &form.code, &mut symbols); source.push(tb_frontend::source::SourceUnit {
name: form.root.name.clone(),
segments: expand_includes(&member, &form.code, form.code_line(), &mut Vec::new())?,
});
forms.push(form); forms.push(form);
} else { } else {
let text = std::fs::read_to_string(&member) let text = std::fs::read_to_string(&member)
.map_err(|error| format!("{}: {error}", member.display()))?; .map_err(|error| format!("{}: {error}", member.display()))?;
let expanded = expand_includes(&member, &text, &mut Vec::new())?; source.push(tb_frontend::source::SourceUnit {
append_project_source(&mut source, &expanded, &mut symbols); name: module_name(&member),
segments: expand_includes(&member, &text, 1, &mut Vec::new())?,
});
} }
} }
Ok((source, forms)) Ok((source, forms))
@@ -273,19 +239,26 @@ fn input_sources(path: &Path) -> Result<(String, Vec<tb_ui::frm::FormFile>), Str
fn compile( fn compile(
path_arg: Option<&String>, path_arg: Option<&String>,
) -> Result< ) -> Result<(PathBuf, tb_vm::bytecode::CompiledModule), ExitCode> {
(
PathBuf,
tb_vm::bytecode::CompiledModule,
Vec<tb_ui::frm::FormFile>,
),
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>"); eprintln!("Aufruf: tbc run|build|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);
if path
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("tbc"))
{
let bytes = std::fs::read(&path).map_err(|e| {
eprintln!("{}: {e}", path.display());
ExitCode::from(1)
})?;
let module = tb_vm::bytecode::CompiledModule::from_tbc(&bytes).map_err(|e| {
eprintln!("{}: {e}", path.display());
ExitCode::from(1)
})?;
return Ok((path, module));
}
let (source, forms) = match input_sources(&path) { let (source, forms) = match input_sources(&path) {
Ok(input) => input, Ok(input) => input,
Err(error) => { Err(error) => {
@@ -293,22 +266,16 @@ fn compile(
return Err(ExitCode::from(1)); return Err(ExitCode::from(1));
} }
}; };
let compiled = if forms.is_empty() { let mut catalog = tb_frontend::forms::FormCatalog::default();
tb_vm::compile_source(&module_name(&path), &source) for form in &forms {
} else { catalog.append(&form.catalog());
let mut catalog = tb_frontend::forms::FormCatalog::default(); }
for form in &forms { let compiled = tb_vm::compile_project(&module_name(&path), &source, &catalog, &forms);
for object in form.catalog().objects {
catalog.objects.push(object);
}
}
tb_vm::compile_source_with_forms(&forms[0].root.name, &source, &catalog)
};
match compiled { match compiled {
Ok(m) => Ok((path, m, forms)), Ok(m) => Ok((path, m)),
Err(diags) => { Err(diags) => {
for d in &diags { for d in &diags {
eprintln!("{}:{d}", path.display()); eprintln!("{d}");
} }
eprintln!("{} Fehler.", diags.len()); eprintln!("{} Fehler.", diags.len());
Err(ExitCode::from(1)) Err(ExitCode::from(1))
@@ -324,7 +291,7 @@ fn cmd_check(args: &[String]) -> ExitCode {
} }
fn cmd_build(args: &[String]) -> ExitCode { fn cmd_build(args: &[String]) -> ExitCode {
let (path, module, _) = match compile(args.first()) { let (path, module) = match compile(args.first()) {
Ok(x) => x, Ok(x) => x,
Err(code) => return code, Err(code) => return code,
}; };
@@ -371,7 +338,7 @@ fn cmd_run(args: &[String]) -> ExitCode {
RunEvent::Ended => ExitCode::SUCCESS, RunEvent::Ended => ExitCode::SUCCESS,
RunEvent::Stopped { line } => { RunEvent::Stopped { line } => {
// STOP außerhalb der IDE: Meldung + Exit-Code ≠ 0 (D6). // STOP außerhalb der IDE: Meldung + Exit-Code ≠ 0 (D6).
eprintln!("STOP in line {line}"); eprintln!("{}:{line}: STOP in line {line}", vm.current_file());
ExitCode::from(3) ExitCode::from(3)
} }
RunEvent::Error { RunEvent::Error {
@@ -379,7 +346,11 @@ fn cmd_run(args: &[String]) -> ExitCode {
line, line,
message, message,
} => { } => {
eprintln!("Runtime error {code}: {message} in line {line}"); eprintln!(
"{}:{line}:{}: Runtime error {code}: {message} in line {line}",
vm.current_file(),
vm.current_source_pos().column
);
ExitCode::from(2) ExitCode::from(2)
} }
// Ohne Debugger-Flags treten diese Ereignisse nicht auf. // Ohne Debugger-Flags treten diese Ereignisse nicht auf.
@@ -398,7 +369,7 @@ fn run_target(current: &Path, program: &str) -> Result<PathBuf, String> {
if Path::new(program).extension().is_some() { if Path::new(program).extension().is_some() {
return relative_case_insensitive(base, program).map_err(|error| error.to_string()); return relative_case_insensitive(base, program).map_err(|error| error.to_string());
} }
for extension in ["bas", "frm", "mak"] { for extension in ["bas", "frm", "mak", "tbc"] {
let candidate = format!("{program}.{extension}"); let candidate = format!("{program}.{extension}");
if let Ok(path) = relative_case_insensitive(base, &candidate) { if let Ok(path) = relative_case_insensitive(base, &candidate) {
return Ok(path); return Ok(path);
@@ -413,7 +384,7 @@ fn run_chain(
size: Option<(usize, usize)>, size: Option<(usize, usize)>,
) -> Result<(RunEvent, Vm), ExitCode> { ) -> Result<(RunEvent, Vm), ExitCode> {
let Some(first) = args.first() else { let Some(first) = args.first() else {
eprintln!("Aufruf: tbc run <datei.bas|datei.frm|projekt.mak>"); eprintln!("Aufruf: tbc run <datei.bas|datei.frm|projekt.mak|datei.tbc>");
return Err(ExitCode::from(1)); return Err(ExitCode::from(1));
}; };
let mut current = PathBuf::from(first); let mut current = PathBuf::from(first);
@@ -421,26 +392,8 @@ fn run_chain(
let command = args[1..].join(" "); let command = args[1..].join(" ");
loop { loop {
let current_arg = current.display().to_string(); let current_arg = current.display().to_string();
let (path, module, forms) = compile(Some(&current_arg))?; let (path, module) = compile(Some(&current_arg))?;
let mut vm = Vm::new(module); let mut vm = Vm::new(module);
if !forms.is_empty() {
let applied = forms.iter().try_for_each(|form| form.apply(&mut vm.forms));
let first = vm
.forms
.objects
.iter()
.position(|object| {
object
.description
.name
.eq_ignore_ascii_case(&forms[0].root.name)
})
.unwrap() as u16;
if let Err(error) = applied.and_then(|_| vm.forms.show(first, false).map(|_| ())) {
eprintln!("Runtime error {}: {}", error.0, error);
return Err(ExitCode::from(2));
}
}
if let Some(line) = start_line.take() { if let Some(line) = start_line.take() {
if let Err(error) = vm.start_at_line(line) { if let Err(error) = vm.start_at_line(line) {
eprintln!("Runtime error {}: {}", error.0, error); eprintln!("Runtime error {}: {}", error.0, error);

View File

@@ -0,0 +1,130 @@
use std::{
path::Path,
process::{Command, Output, Stdio},
time::{Duration, Instant},
};
fn tbc(dir: &Path, command: &str, file: &str) -> Output {
let mut child = Command::new(env!("CARGO_BIN_EXE_tbc"))
.current_dir(dir)
.args([command, file])
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
let start = Instant::now();
while child.try_wait().unwrap().is_none() {
if start.elapsed() > Duration::from_secs(10) {
child.kill().unwrap();
child.wait().unwrap();
panic!("tbc {command} {file}: Frist überschritten");
}
std::thread::sleep(Duration::from_millis(10));
}
child.wait_with_output().unwrap()
}
#[test]
fn projekt_und_formular_laufen_nach_entfernen_saemtlicher_quellen() {
let dir = std::env::temp_dir().join(format!("tb-tbc-v4-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("main.bas"), "a$=Form1!Text1.Text\nb$=Form1!Text1(2).Text\nForm1.Hide\nCLS\nPRINT a$\nPRINT b$\nCALL Ausgabe\nEND\n").unwrap();
std::fs::write(
dir.join("lib.bas"),
"'$INCLUDE: 'outer.bi'\nSUB Ausgabe\nPRINT N\nEND SUB\n",
)
.unwrap();
std::fs::write(dir.join("outer.bi"), "'$INCLUDE: 'inner.bi'\n").unwrap();
std::fs::write(dir.join("inner.bi"), "CONST N=7\n").unwrap();
std::fs::write(dir.join("form.frm"), "VERSION 1.00\nBEGIN Form Form1\n BEGIN TextBox Text1\n Index = 0\n Text = \"hello\"\n END\n BEGIN TextBox Text1\n Index = 2\n Text = \"world\"\n END\nEND\n").unwrap();
std::fs::write(
dir.join("app.mak"),
"' Projekt\nMAIN.BAS\nLIB.BAS\nFORM.FRM\n",
)
.unwrap();
let before = tbc(&dir, "run", "app.mak");
assert!(before.status.success(), "{before:?}");
assert_eq!(
String::from_utf8_lossy(&before.stdout),
"hello\nworld\n 7 \n"
);
let build = tbc(&dir, "build", "app.mak");
assert!(build.status.success(), "{build:?}");
let bytes = std::fs::read(dir.join("app.tbc")).unwrap();
assert_eq!(&bytes[..6], b"TBC\0\x04\0");
for file in [
"main.bas", "lib.bas", "outer.bi", "inner.bi", "form.frm", "app.mak",
] {
std::fs::remove_file(dir.join(file)).unwrap();
}
let after = tbc(&dir, "run", "app.tbc");
assert!(after.status.success(), "{after:?}");
assert_eq!(after.stdout, before.stdout);
for version in [1u16, 2, 3, 5, 32767] {
let mut invalid = bytes.clone();
invalid[4..6].copy_from_slice(&version.to_le_bytes());
std::fs::write(dir.join("old.tbc"), invalid).unwrap();
let out = tbc(&dir, "run", "old.tbc");
assert!(!out.status.success());
assert!(
String::from_utf8_lossy(&out.stderr).contains(&format!("Formatversion {version}")),
"{out:?}"
);
}
std::fs::remove_dir_all(dir).unwrap();
}
#[test]
fn cli_meldet_physische_quellorte_auch_nach_verschachtelten_includes() {
let dir = std::env::temp_dir().join(format!("tb-project-origins-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("main.bas"), "CALL Fehler\nEND\n").unwrap();
std::fs::write(dir.join("lib.bas"), "SUB Fehler\n200 ERROR 6\nEND SUB\n").unwrap();
std::fs::write(dir.join("app.mak"), "main.bas\nlib.bas\n").unwrap();
let out = tbc(&dir, "run", "app.mak");
assert_eq!(out.status.code(), Some(2));
assert!(
String::from_utf8_lossy(&out.stderr).contains("lib.bas:2:5:"),
"{out:?}"
);
std::fs::write(dir.join("lib.bas"), "SUB Fehler\ns$=42\nEND SUB\n").unwrap();
let out = tbc(&dir, "check", "app.mak");
assert!(!out.status.success());
assert!(
String::from_utf8_lossy(&out.stderr).contains("lib.bas:2:1:"),
"{out:?}"
);
std::fs::write(dir.join("lib.bas"), "'$INCLUDE: 'outer.bi'\n").unwrap();
std::fs::write(dir.join("outer.bi"), "'$INCLUDE: 'inner.bi'\n").unwrap();
std::fs::write(dir.join("inner.bi"), "SUB Fehler\ns$=42\nEND SUB\n").unwrap();
let out = tbc(&dir, "check", "app.mak");
assert!(!out.status.success());
assert!(
String::from_utf8_lossy(&out.stderr).contains("inner.bi:2:1:"),
"{out:?}"
);
std::fs::write(dir.join("inner.bi"), "SUB Fehler\n200 ERROR 6\nEND SUB\n").unwrap();
let out = tbc(&dir, "run", "app.mak");
assert_eq!(out.status.code(), Some(2));
assert!(
String::from_utf8_lossy(&out.stderr).contains("inner.bi:2:5:"),
"{out:?}"
);
std::fs::write(
dir.join("error.frm"),
"VERSION 1.00\nBEGIN Form F\nEND\n\nSUB Form_Load\nERROR 6\nEND SUB\n",
)
.unwrap();
assert!(tbc(&dir, "build", "error.frm").status.success());
std::fs::remove_file(dir.join("error.frm")).unwrap();
let out = tbc(&dir, "run", "error.tbc");
assert_eq!(out.status.code(), Some(2));
assert!(
String::from_utf8_lossy(&out.stderr).contains("error.frm:6:1:"),
"{out:?}"
);
std::fs::write(dir.join("empty.bas"), "").unwrap();
assert!(tbc(&dir, "run", "empty.bas").status.success());
std::fs::remove_dir_all(dir).unwrap();
}

View File

@@ -49,12 +49,12 @@ pub enum BinOp {
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub enum Expr { pub enum Expr {
IntLit(i16), IntLit(i16, SourcePos),
LongLit(i32), LongLit(i32, SourcePos),
SingleLit(f32), SingleLit(f32, SourcePos),
DoubleLit(f64), DoubleLit(f64, SourcePos),
CurrencyLit(i64), CurrencyLit(i64, SourcePos),
StrLit(String), StrLit(String, SourcePos),
/// Benannter Zugriff: Variable, Arrayelement, Funktionsaufruf oder /// Benannter Zugriff: Variable, Arrayelement, Funktionsaufruf oder
/// Konstante — Auflösung erfolgt in der Semantik. /// Konstante — Auflösung erfolgt in der Semantik.
Name { Name {
@@ -90,7 +90,13 @@ pub enum Expr {
impl Expr { impl Expr {
pub fn pos(&self) -> SourcePos { pub fn pos(&self) -> SourcePos {
match self { match self {
Expr::Name { pos, .. } Expr::IntLit(_, pos)
| Expr::LongLit(_, pos)
| Expr::SingleLit(_, pos)
| Expr::DoubleLit(_, pos)
| Expr::CurrencyLit(_, pos)
| Expr::StrLit(_, pos)
| Expr::Name { pos, .. }
| Expr::Unary { pos, .. } | Expr::Unary { pos, .. }
| Expr::Binary { pos, .. } | Expr::Binary { pos, .. }
| Expr::TypeOf { pos, .. } => *pos, | Expr::TypeOf { pos, .. } => *pos,
@@ -234,7 +240,7 @@ pub enum EventAction {
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub enum Stmt { pub enum Stmt {
Label(String), Label(String),
LineNumber(u32), LineNumber(u32, SourcePos),
Assign { Assign {
target: Expr, target: Expr,
value: Expr, value: Expr,
@@ -259,7 +265,7 @@ pub enum Stmt {
If { If {
cond: Expr, cond: Expr,
then_body: Vec<Stmt>, then_body: Vec<Stmt>,
elseifs: Vec<(Expr, Vec<Stmt>)>, elseifs: Vec<(Expr, Vec<Stmt>, SourcePos)>,
else_body: Option<Vec<Stmt>>, else_body: Option<Vec<Stmt>>,
pos: SourcePos, pos: SourcePos,
}, },
@@ -275,17 +281,20 @@ pub enum Stmt {
step: Option<Expr>, step: Option<Expr>,
body: Vec<Stmt>, body: Vec<Stmt>,
pos: SourcePos, pos: SourcePos,
end_pos: SourcePos,
}, },
DoLoop { DoLoop {
pre: Option<(bool, Expr)>, // (ist UNTIL, Bedingung) pre: Option<(bool, Expr)>, // (ist UNTIL, Bedingung)
post: Option<(bool, Expr)>, post: Option<(bool, Expr)>,
body: Vec<Stmt>, body: Vec<Stmt>,
pos: SourcePos, pos: SourcePos,
end_pos: SourcePos,
}, },
While { While {
cond: Expr, cond: Expr,
body: Vec<Stmt>, body: Vec<Stmt>,
pos: SourcePos, pos: SourcePos,
end_pos: SourcePos,
}, },
Goto { Goto {
target: LabelRef, target: LabelRef,

View File

@@ -678,6 +678,7 @@ pub struct FormObject {
pub name: String, pub name: String,
pub class: ObjectClass, pub class: ObjectClass,
pub parent_form: Option<String>, pub parent_form: Option<String>,
pub parent: Option<u16>,
pub array: bool, pub array: bool,
} }
@@ -698,6 +699,13 @@ impl FormCatalog {
self.objects.push(FormObject { self.objects.push(FormObject {
name: name.into().to_uppercase(), name: name.into().to_uppercase(),
class, class,
parent: parent_form
.and_then(|name| {
self.objects
.iter()
.rposition(|o| o.name.eq_ignore_ascii_case(name))
})
.map(|id| id as u16),
parent_form: parent_form.map(str::to_uppercase), parent_form: parent_form.map(str::to_uppercase),
array, array,
}); });
@@ -712,16 +720,33 @@ impl FormCatalog {
.map(|(i, o)| (i as u16, o)) .map(|(i, o)| (i as u16, o))
} }
pub fn append(&mut self, other: &Self) {
let offset = self.objects.len() as u16;
self.objects
.extend(other.objects.iter().cloned().map(|mut object| {
object.parent = object.parent.map(|id| id + offset);
object
}));
}
pub fn find_in(&self, name: &str, form: &str) -> Option<(u16, &FormObject)> {
self.objects
.iter()
.enumerate()
.find(|(_, o)| o.name.eq_ignore_ascii_case(name) && self.belongs_to(o, form))
.map(|(id, o)| (id as u16, o))
}
pub fn belongs_to(&self, object: &FormObject, form: &str) -> bool { pub fn belongs_to(&self, object: &FormObject, form: &str) -> bool {
let mut parent = object.parent_form.as_deref(); let mut parent = object.parent;
for _ in 0..self.objects.len() { for _ in 0..self.objects.len() {
let Some(name) = parent else { return false }; let Some(object) = parent.and_then(|id| self.objects.get(id as usize)) else {
if name.eq_ignore_ascii_case(form) { return false;
};
if object.name.eq_ignore_ascii_case(form) {
return true; return true;
} }
parent = self parent = object.parent;
.find(name)
.and_then(|(_, object)| object.parent_form.as_deref());
} }
false false
} }

View File

@@ -124,10 +124,22 @@ pub struct DataItem {
pub line: u32, pub line: u32,
} }
/// Aufgelöste COMMON-Deklaration für die projektweite Slotverknüpfung.
#[derive(Debug, Clone)]
pub struct HCommon {
pub slot: u16,
pub block: Option<String>,
pub key: String,
pub ty: HTy,
pub dims: Option<Vec<(Option<i32>, Option<i32>)>>,
pub pos: crate::SourcePos,
}
#[derive(Debug)] #[derive(Debug)]
pub struct HirModule { pub struct HirModule {
pub name: String, pub name: String,
pub globals: Vec<HVar>, pub globals: Vec<HVar>,
pub commons: Vec<HCommon>,
pub udts: Vec<HUdt>, pub udts: Vec<HUdt>,
/// Prozeduren; Index 0 ist das Hauptprogramm. /// Prozeduren; Index 0 ist das Hauptprogramm.
pub procs: Vec<HProc>, pub procs: Vec<HProc>,
@@ -382,6 +394,8 @@ pub enum Builtin {
pub enum HExpr { pub enum HExpr {
Int(i16), Int(i16),
Lng(i32), Lng(i32),
/// TYPE-Tabellenreferenz für ISAM; beim Projektlinken zu versetzen.
UdtId(u16),
Sng(f32), Sng(f32),
Dbl(f64), Dbl(f64),
Cur(i64), Cur(i64),
@@ -513,6 +527,7 @@ pub enum HResume {
#[derive(Debug)] #[derive(Debug)]
pub struct HStmt { pub struct HStmt {
pub pos: crate::SourcePos,
pub line: u32, pub line: u32,
pub kind: HStmtKind, pub kind: HStmtKind,
} }
@@ -596,12 +611,14 @@ pub enum HStmtKind {
}, },
/// DO/LOOP, WHILE/WEND (nur `pre`) — Bedingungen: (ist_until, Ausdruck). /// DO/LOOP, WHILE/WEND (nur `pre`) — Bedingungen: (ist_until, Ausdruck).
Loop { Loop {
end_pos: crate::SourcePos,
pre: Option<(bool, HExpr)>, pre: Option<(bool, HExpr)>,
post: Option<(bool, HExpr)>, post: Option<(bool, HExpr)>,
body: Vec<HStmt>, body: Vec<HStmt>,
exit_label: LabelId, exit_label: LabelId,
}, },
For { For {
end_pos: crate::SourcePos,
var: HPlace, var: HPlace,
ty: NumTy, ty: NumTy,
from: HExpr, from: HExpr,

View File

@@ -342,6 +342,7 @@ pub fn lex(source: &str) -> LexOutput {
} }
let start = i; let start = i;
let pos = SourcePos { let pos = SourcePos {
source: 0,
line: line_no, line: line_no,
column: (start + 1) as u32, column: (start + 1) as u32,
}; };
@@ -413,6 +414,7 @@ pub fn lex(source: &str) -> LexOutput {
TokenKind::Num(NumValue::Int(v as u16 as i16)) TokenKind::Num(NumValue::Int(v as u16 as i16))
} else { } else {
diagnostics.push(Diagnostic { diagnostics.push(Diagnostic {
file: None,
pos, pos,
message: "Overflow".into(), message: "Overflow".into(),
}); });
@@ -421,6 +423,7 @@ pub fn lex(source: &str) -> LexOutput {
tokens.push(Token { kind, pos }); tokens.push(Token { kind, pos });
} }
Err(_) => diagnostics.push(Diagnostic { Err(_) => diagnostics.push(Diagnostic {
file: None,
pos, pos,
message: "Syntax error".into(), message: "Syntax error".into(),
}), }),
@@ -496,6 +499,7 @@ pub fn lex(source: &str) -> LexOutput {
(Some(Suffix::Integer), _) => { (Some(Suffix::Integer), _) => {
if dval > i16::MAX as f64 || dval < i16::MIN as f64 { if dval > i16::MAX as f64 || dval < i16::MIN as f64 {
diagnostics.push(Diagnostic { diagnostics.push(Diagnostic {
file: None,
pos, pos,
message: "Overflow".into(), message: "Overflow".into(),
}); });
@@ -505,6 +509,7 @@ pub fn lex(source: &str) -> LexOutput {
(Some(Suffix::Long), _) => { (Some(Suffix::Long), _) => {
if dval > i32::MAX as f64 || dval < i32::MIN as f64 { if dval > i32::MAX as f64 || dval < i32::MIN as f64 {
diagnostics.push(Diagnostic { diagnostics.push(Diagnostic {
file: None,
pos, pos,
message: "Overflow".into(), message: "Overflow".into(),
}); });
@@ -518,6 +523,7 @@ pub fn lex(source: &str) -> LexOutput {
} }
(Some(Suffix::Str), _) => { (Some(Suffix::Str), _) => {
diagnostics.push(Diagnostic { diagnostics.push(Diagnostic {
file: None,
pos, pos,
message: "Syntax error".into(), message: "Syntax error".into(),
}); });
@@ -674,6 +680,7 @@ pub fn lex(source: &str) -> LexOutput {
} }
other => { other => {
diagnostics.push(Diagnostic { diagnostics.push(Diagnostic {
file: None,
pos, pos,
message: format!("Syntax error ('{other}')"), message: format!("Syntax error ('{other}')"),
}); });
@@ -692,6 +699,7 @@ pub fn lex(source: &str) -> LexOutput {
tokens.push(Token { tokens.push(Token {
kind: TokenKind::Eol, kind: TokenKind::Eol,
pos: SourcePos { pos: SourcePos {
source: 0,
line: line_no, line: line_no,
column: (chars.len() + 1) as u32, column: (chars.len() + 1) as u32,
}, },
@@ -704,6 +712,7 @@ pub fn lex(source: &str) -> LexOutput {
tokens.push(Token { tokens.push(Token {
kind: TokenKind::Eof, kind: TokenKind::Eof,
pos: SourcePos { pos: SourcePos {
source: 0,
line: (source.lines().count() + 1) as u32, line: (source.lines().count() + 1) as u32,
column: 1, column: 1,
}, },

View File

@@ -11,10 +11,12 @@ pub mod hir;
pub mod lexer; pub mod lexer;
pub mod parser; pub mod parser;
pub mod sema; pub mod sema;
pub mod source;
/// Quelltextposition für Diagnostik (1-basiert, wie im IDE-Vorbild). /// Quelltextposition für Diagnostik (1-basiert, wie im IDE-Vorbild).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct SourcePos { pub struct SourcePos {
pub source: u32,
pub line: u32, pub line: u32,
pub column: u32, pub column: u32,
} }
@@ -23,12 +25,16 @@ pub struct SourcePos {
/// Meldungen des Vorbilds, wo es eine Entsprechung gibt. /// Meldungen des Vorbilds, wo es eine Entsprechung gibt.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Diagnostic { pub struct Diagnostic {
pub file: Option<String>,
pub pos: SourcePos, pub pos: SourcePos,
pub message: String, pub message: String,
} }
impl std::fmt::Display for Diagnostic { impl std::fmt::Display for Diagnostic {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(file) = &self.file {
write!(f, "{file}:")?;
}
write!(f, "{}:{}: {}", self.pos.line, self.pos.column, self.message) write!(f, "{}:{}: {}", self.pos.line, self.pos.column, self.message)
} }
} }

View File

@@ -106,6 +106,7 @@ impl<'a> P<'a> {
fn err(&mut self, msg: impl Into<String>) { fn err(&mut self, msg: impl Into<String>) {
let pos = self.pos(); let pos = self.pos();
self.diags.push(Diagnostic { self.diags.push(Diagnostic {
file: None,
pos, pos,
message: msg.into(), message: msg.into(),
}); });
@@ -199,13 +200,13 @@ impl<'a> P<'a> {
if let TokenKind::Num(NumValue::Int(n)) = self.k() { if let TokenKind::Num(NumValue::Int(n)) = self.k() {
if n >= 0 { if n >= 0 {
self.advance(); self.advance();
return Some(Stmt::LineNumber(n as u32)); return Some(Stmt::LineNumber(n as u32, pos));
} }
} }
if let TokenKind::Num(NumValue::Long(n)) = self.k() { if let TokenKind::Num(NumValue::Long(n)) = self.k() {
if n >= 0 { if n >= 0 {
self.advance(); self.advance();
return Some(Stmt::LineNumber(n as u32)); return Some(Stmt::LineNumber(n as u32, pos));
} }
} }
if let TokenKind::Ident { name, suffix: None } = self.k() { if let TokenKind::Ident { name, suffix: None } = self.k() {
@@ -816,7 +817,7 @@ impl<'a> P<'a> {
{ {
if w == "ALL" { if w == "ALL" {
self.advance(); self.advance();
let args = vec![Expr::LongLit(ROLLBACK_ALL)]; let args = vec![Expr::LongLit(ROLLBACK_ALL, pos)];
return Some(Stmt::Call { return Some(Stmt::Call {
name, name,
suffix: None, suffix: None,
@@ -1210,11 +1211,13 @@ impl<'a> P<'a> {
let stop = |p: &P| p.is_kw(Kw::ElseIf) || p.is_kw(Kw::Else) || p.at_end_pair(Kw::If); let stop = |p: &P| p.is_kw(Kw::ElseIf) || p.is_kw(Kw::Else) || p.at_end_pair(Kw::If);
let then_body = self.parse_stmt_list(stop); let then_body = self.parse_stmt_list(stop);
let mut elseifs = Vec::new(); let mut elseifs = Vec::new();
while self.eat_kw(Kw::ElseIf) { while self.is_kw(Kw::ElseIf) {
let elseif_pos = self.pos();
self.advance();
let c = self.parse_expr()?; let c = self.parse_expr()?;
self.expect_kw(Kw::Then, "THEN"); self.expect_kw(Kw::Then, "THEN");
let b = self.parse_stmt_list(stop); let b = self.parse_stmt_list(stop);
elseifs.push((c, b)); elseifs.push((c, b, elseif_pos));
} }
let else_body = if self.eat_kw(Kw::Else) { let else_body = if self.eat_kw(Kw::Else) {
Some(self.parse_stmt_list(|p: &P| p.at_end_pair(Kw::If))) Some(self.parse_stmt_list(|p: &P| p.at_end_pair(Kw::If)))
@@ -1349,6 +1352,7 @@ impl<'a> P<'a> {
None None
}; };
let body = self.parse_stmt_list(|p: &P| p.is_kw(Kw::Next)); let body = self.parse_stmt_list(|p: &P| p.is_kw(Kw::Next));
let end_pos = self.pos();
if self.eat_kw(Kw::Next) { if self.eat_kw(Kw::Next) {
// Optional: Zählvariable(n) hinter NEXT // Optional: Zählvariable(n) hinter NEXT
while matches!(self.k(), TokenKind::Ident { .. }) { while matches!(self.k(), TokenKind::Ident { .. }) {
@@ -1367,6 +1371,7 @@ impl<'a> P<'a> {
step, step,
body, body,
pos, pos,
end_pos,
}) })
} }
@@ -1380,6 +1385,7 @@ impl<'a> P<'a> {
None None
}; };
let body = self.parse_stmt_list(|p: &P| p.is_kw(Kw::Loop)); let body = self.parse_stmt_list(|p: &P| p.is_kw(Kw::Loop));
let end_pos = self.pos();
let mut post = None; let mut post = None;
if self.eat_kw(Kw::Loop) { if self.eat_kw(Kw::Loop) {
if self.eat_kw(Kw::While) { if self.eat_kw(Kw::While) {
@@ -1395,6 +1401,7 @@ impl<'a> P<'a> {
post, post,
body, body,
pos, pos,
end_pos,
}) })
} }
@@ -1402,10 +1409,16 @@ impl<'a> P<'a> {
self.advance(); // WHILE self.advance(); // WHILE
let cond = self.parse_expr()?; let cond = self.parse_expr()?;
let body = self.parse_stmt_list(|p: &P| p.is_kw(Kw::Wend)); let body = self.parse_stmt_list(|p: &P| p.is_kw(Kw::Wend));
let end_pos = self.pos();
if !self.eat_kw(Kw::Wend) { if !self.eat_kw(Kw::Wend) {
self.err("WHILE without WEND"); self.err("WHILE without WEND");
} }
Some(Stmt::While { cond, body, pos }) Some(Stmt::While {
cond,
body,
pos,
end_pos,
})
} }
/// `ON <quelle>[(n)] GOSUB ziel`. Liefert `None` und lässt den Cursor /// `ON <quelle>[(n)] GOSUB ziel`. Liefert `None` und lässt den Cursor
@@ -2214,18 +2227,20 @@ impl<'a> P<'a> {
} }
match self.k() { match self.k() {
TokenKind::Num(n) => { TokenKind::Num(n) => {
let pos = self.pos();
self.advance(); self.advance();
Some(match n { Some(match n {
NumValue::Int(v) => Expr::IntLit(v), NumValue::Int(v) => Expr::IntLit(v, pos),
NumValue::Long(v) => Expr::LongLit(v), NumValue::Long(v) => Expr::LongLit(v, pos),
NumValue::Single(v) => Expr::SingleLit(v), NumValue::Single(v) => Expr::SingleLit(v, pos),
NumValue::Double(v) => Expr::DoubleLit(v), NumValue::Double(v) => Expr::DoubleLit(v, pos),
NumValue::Currency(v) => Expr::CurrencyLit(v), NumValue::Currency(v) => Expr::CurrencyLit(v, pos),
}) })
} }
TokenKind::Str(s) => { TokenKind::Str(s) => {
let pos = self.pos();
self.advance(); self.advance();
Some(Expr::StrLit(s)) Some(Expr::StrLit(s, pos))
} }
TokenKind::LParen => { TokenKind::LParen => {
self.advance(); self.advance();

View File

@@ -150,6 +150,7 @@ struct Scope {
/// Stack der Schleifen-Exit-Labels für `EXIT FOR`/`EXIT DO`. /// Stack der Schleifen-Exit-Labels für `EXIT FOR`/`EXIT DO`.
loop_exits: Vec<(LoopKind, LabelId)>, loop_exits: Vec<(LoopKind, LabelId)>,
current_line: u32, current_line: u32,
current_pos: SourcePos,
} }
#[derive(PartialEq, Clone, Copy)] #[derive(PartialEq, Clone, Copy)]
@@ -412,7 +413,13 @@ pub fn lower_with_forms(
{ {
catalog.add(&module.name, ObjectClass::Form, None, false); catalog.add(&module.name, ObjectClass::Form, None, false);
} }
let mut s = Sema { let mut s = new_sema(module, &catalog);
let hir = s.run(module);
(hir, s.diags)
}
fn new_sema(module: &Module, catalog: &FormCatalog) -> Sema {
Sema {
diags: Vec::new(), diags: Vec::new(),
deftypes: [const { None }; 26], deftypes: [const { None }; 26],
explicit: false, explicit: false,
@@ -427,16 +434,79 @@ pub fn lower_with_forms(
module_labels: HashMap::new(), module_labels: HashMap::new(),
module_line_labels: HashMap::new(), module_line_labels: HashMap::new(),
globals: Vec::new(), globals: Vec::new(),
commons: Vec::new(),
data: Vec::new(), data: Vec::new(),
data_marks_name: HashMap::new(), data_marks_name: HashMap::new(),
data_marks_line: HashMap::new(), data_marks_line: HashMap::new(),
hir_procs: Vec::new(), hir_procs: Vec::new(),
isam_typ: HashMap::new(), isam_typ: HashMap::new(),
forms: catalog, forms: catalog.clone(),
module_name: module.name.clone(),
event_procs: Vec::new(), event_procs: Vec::new(),
}; }
let hir = s.run(module); }
(hir, s.diags)
/// Exportdeklarationen mit den Typen und Konstanten ihres Ursprungsmoduls.
/// Rümpfe werden hier nicht übersetzt; dafür bleibt der reguläre Sema-Durchlauf zuständig.
pub fn export_declarations(module: &Module, constants: &[Stmt]) -> Vec<Stmt> {
let mut s = new_sema(module, &FormCatalog::default());
let mut scope = Scope::default();
let mut declarations = Vec::new();
let local: HashSet<_> = module
.body
.iter()
.flat_map(|stmt| match stmt {
Stmt::ConstDecl { items, .. } => items.iter().map(|i| i.0.as_str()).collect(),
_ => Vec::new(),
})
.collect();
for stmt in constants {
if let Stmt::ConstDecl { items, .. } = stmt {
if !local.contains(items[0].0.as_str()) {
s.lower_stmt(stmt, &mut scope, &mut Vec::new());
}
}
}
for stmt in &module.body {
match stmt {
Stmt::DefType { .. } => s.lower_stmt(stmt, &mut scope, &mut Vec::new()),
Stmt::ConstDecl { items, pos } => {
for item in items {
let declaration = Stmt::ConstDecl {
items: vec![item.clone()],
pos: *pos,
};
s.lower_stmt(&declaration, &mut scope, &mut Vec::new());
let value = match s.consts.get(&item.0).and_then(|(_, value)| value.as_ref()) {
Some(ConstVal::Num(n)) => Expr::DoubleLit(*n, item.2.pos()),
Some(ConstVal::Str(value)) => Expr::StrLit(value.clone(), item.2.pos()),
None => item.2.clone(),
};
declarations.push(Stmt::ConstDecl {
items: vec![(item.0.clone(), item.1, value)],
pos: *pos,
});
}
}
Stmt::TypeDecl { .. } => declarations.push(stmt.clone()),
_ => {}
}
}
for proc in &module.procs {
let mut sig = proc.sig.clone();
if sig.suffix.is_none() && sig.kind == ProcKind::Function {
sig.suffix =
Suffix::from_char(s.var_key(&sig.name, &None, false).chars().last().unwrap());
}
for param in &mut sig.params {
if param.suffix.is_none() && param.as_type.is_none() {
param.suffix =
Suffix::from_char(s.var_key(&param.name, &None, false).chars().last().unwrap());
}
}
declarations.push(Stmt::Declare { sig, pos: proc.pos });
}
declarations
} }
struct Sema { struct Sema {
@@ -467,6 +537,7 @@ struct Sema {
module_line_labels: HashMap<u32, LabelId>, module_line_labels: HashMap<u32, LabelId>,
/// Globale Slots (Modulvariablen, STATICs, versteckte Temps). /// Globale Slots (Modulvariablen, STATICs, versteckte Temps).
globals: Vec<hir::HVar>, globals: Vec<hir::HVar>,
commons: Vec<hir::HCommon>,
/// DATA-Konstanten des Moduls (aus dem Prescan, statisch). /// DATA-Konstanten des Moduls (aus dem Prescan, statisch).
data: Vec<hir::DataItem>, data: Vec<hir::DataItem>,
data_marks_name: HashMap<String, u32>, data_marks_name: HashMap<String, u32>,
@@ -474,12 +545,14 @@ struct Sema {
/// Fertige HIR-Prozeduren nach Id (0 = Hauptprogramm). /// Fertige HIR-Prozeduren nach Id (0 = Hauptprogramm).
hir_procs: Vec<Option<hir::HProc>>, hir_procs: Vec<Option<hir::HProc>>,
forms: FormCatalog, forms: FormCatalog,
module_name: String,
event_procs: Vec<hir::HEventProc>, event_procs: Vec<hir::HEventProc>,
} }
impl Sema { impl Sema {
fn err(&mut self, pos: SourcePos, msg: impl Into<String>) { fn err(&mut self, pos: SourcePos, msg: impl Into<String>) {
self.diags.push(Diagnostic { self.diags.push(Diagnostic {
file: None,
pos, pos,
message: msg.into(), message: msg.into(),
}); });
@@ -489,7 +562,10 @@ impl Sema {
// Pass 1: Prozeduren, DECLAREs und TYPEs registrieren. // Pass 1: Prozeduren, DECLAREs und TYPEs registrieren.
for stmt in &module.body { for stmt in &module.body {
match stmt { match stmt {
Stmt::Declare { sig, .. } => self.register_proc(sig), Stmt::Declare { sig, pos } => self.register_proc(sig, *pos),
Stmt::DefType { .. } => {
self.lower_stmt(stmt, &mut Scope::default(), &mut Vec::new())
}
Stmt::TypeDecl { name, fields, pos } => { Stmt::TypeDecl { name, fields, pos } => {
let mut hfields = Vec::new(); let mut hfields = Vec::new();
for (fname, ftype) in fields { for (fname, ftype) in fields {
@@ -514,8 +590,12 @@ impl Sema {
_ => {} _ => {}
} }
} }
let mut defined = HashSet::new();
for proc in &module.procs { for proc in &module.procs {
self.register_proc(&proc.sig); if !defined.insert(proc.sig.name.clone()) {
self.err(proc.pos, "Duplicate definition");
}
self.register_proc(&proc.sig, proc.pos);
} }
for proc in &module.procs { for proc in &module.procs {
self.register_event_proc(proc); self.register_event_proc(proc);
@@ -524,6 +604,8 @@ impl Sema {
self.hir_procs self.hir_procs
.resize_with(self.next_proc_id as usize, || None); .resize_with(self.next_proc_id as usize, || None);
self.deftypes = [const { None }; 26];
// Pass 2: Modulrumpf. Labels, Zeilennummern und DATA-Positionen // Pass 2: Modulrumpf. Labels, Zeilennummern und DATA-Positionen
// werden vorab eingesammelt (RESTORE nach vorn, statisches DATA). // werden vorab eingesammelt (RESTORE nach vorn, statisches DATA).
let mut scope = Scope { let mut scope = Scope {
@@ -550,7 +632,9 @@ impl Sema {
// Pass 3: Prozedurrümpfe. // Pass 3: Prozedurrümpfe.
for proc in &module.procs { for proc in &module.procs {
let deftypes = self.deftypes.clone();
self.lower_proc(proc); self.lower_proc(proc);
self.deftypes = deftypes;
} }
// Zusammensetzen: fehlende Rümpfe (nur DECLARE) behalten ihre // Zusammensetzen: fehlende Rümpfe (nur DECLARE) behalten ihre
@@ -574,7 +658,7 @@ impl Sema {
name: format!("ARG{index}"), name: format!("ARG{index}"),
ty: self.h_ty(ty), ty: self.h_ty(ty),
array: *array, array: *array,
by_ref: !array && !matches!(ty, Ty::Udt(_)), by_ref: !array,
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let mut locals = params let mut locals = params
@@ -614,6 +698,7 @@ impl Sema {
Some(hir::HirModule { Some(hir::HirModule {
name: module.name.clone(), name: module.name.clone(),
globals: std::mem::take(&mut self.globals), globals: std::mem::take(&mut self.globals),
commons: std::mem::take(&mut self.commons),
udts: std::mem::take(&mut self.udt_defs), udts: std::mem::take(&mut self.udt_defs),
procs, procs,
data: std::mem::take(&mut self.data), data: std::mem::take(&mut self.data),
@@ -638,11 +723,11 @@ impl Sema {
let hty = self.h_ty(&ty); let hty = self.h_ty(&ty);
let slot = scope.locals.len() as u16; let slot = scope.locals.len() as u16;
scope.locals.push(hir::HVar { scope.locals.push(hir::HVar {
name: p.name.clone(), name: key.trim_end_matches("\u{1}AS").to_string(),
ty: hty.clone(), ty: hty.clone(),
array: p.array, array: p.array,
}); });
let by_ref = !p.array && !matches!(ty, Ty::Udt(_)); let by_ref = !p.array;
scope.vars.insert( scope.vars.insert(
key, key,
VarInfo { VarInfo {
@@ -669,7 +754,7 @@ impl Sema {
let key = self.var_key(&proc.sig.name, &proc.sig.suffix, false); let key = self.var_key(&proc.sig.name, &proc.sig.suffix, false);
let slot = scope.locals.len() as u16; let slot = scope.locals.len() as u16;
scope.locals.push(hir::HVar { scope.locals.push(hir::HVar {
name: proc.sig.name.clone(), name: key.clone(),
ty: self.h_ty(&ret), ty: self.h_ty(&ret),
array: false, array: false,
}); });
@@ -721,7 +806,7 @@ impl Sema {
.insert(n.clone(), self.data.len() as u32); .insert(n.clone(), self.data.len() as u32);
} }
} }
Stmt::LineNumber(n) => { Stmt::LineNumber(n, _) => {
let id = scope.new_label(); let id = scope.new_label();
scope.line_labels.insert(*n, id); scope.line_labels.insert(*n, id);
if module_data { if module_data {
@@ -745,7 +830,7 @@ impl Sema {
.. ..
} => { } => {
self.prescan(then_body, scope, module_data); self.prescan(then_body, scope, module_data);
for (_, b) in elseifs { for (_, b, _) in elseifs {
self.prescan(b, scope, module_data); self.prescan(b, scope, module_data);
} }
if let Some(b) = else_body { if let Some(b) = else_body {
@@ -773,18 +858,24 @@ impl Sema {
} }
} }
fn register_proc(&mut self, sig: &ProcSig) { fn register_proc(&mut self, sig: &ProcSig, pos: SourcePos) {
let ret = match sig.kind { let ret = match sig.kind {
ProcKind::Function => self.name_ty(&sig.name, &sig.suffix), ProcKind::Function => self.name_ty(&sig.name, &sig.suffix),
ProcKind::Sub => Ty::Unknown, ProcKind::Sub => Ty::Unknown,
}; };
let params = sig let params: Vec<_> = sig
.params .params
.iter() .iter()
.map(|p| (self.param_ty(p), p.array)) .map(|p| (self.param_ty(p), p.array))
.collect(); .collect();
let id = match self.procs.get(&sig.name) { let id = match self.procs.get(&sig.name) {
Some(p) => p.id, Some(p) => {
let id = p.id;
if p.kind != sig.kind || p.ret != ret || p.params != params {
self.err(pos, "Parameter type mismatch");
}
id
}
None => { None => {
let id = self.next_proc_id; let id = self.next_proc_id;
self.next_proc_id += 1; self.next_proc_id += 1;
@@ -895,14 +986,9 @@ impl Sema {
fn event_binding(&self, name: &str) -> Option<(u16, ObjectClass, String)> { fn event_binding(&self, name: &str) -> Option<(u16, ObjectClass, String)> {
let (base, event) = name.rsplit_once('_')?; let (base, event) = name.rsplit_once('_')?;
let found = if base == "FORM" { let found = if base == "FORM" {
self.forms self.current_form()
.objects
.iter()
.enumerate()
.find(|(_, o)| o.class == ObjectClass::Form)
.map(|(i, o)| (i as u16, o))
} else { } else {
self.forms.find(base) self.find_object(base)
}?; }?;
Some((found.0, found.1.class, event.to_string())) Some((found.0, found.1.class, event.to_string()))
} }
@@ -916,12 +1002,68 @@ impl Sema {
} }
} }
fn current_form(&self) -> Option<(u16, &forms::FormObject)> {
self.forms
.find(&self.module_name)
.filter(|(_, o)| o.class == ObjectClass::Form)
.or_else(|| {
let mut forms = self
.forms
.objects
.iter()
.enumerate()
.filter(|(_, o)| o.class == ObjectClass::Form);
let (id, object) = forms.next()?;
forms.next().is_none().then_some((id as u16, object))
})
}
fn find_object(&self, name: &str) -> Option<(u16, &forms::FormObject)> {
self.forms
.find_in(name, &self.module_name)
.or_else(|| {
self.forms
.find(name)
.filter(|(_, o)| o.class == ObjectClass::Form || o.class == ObjectClass::Screen)
})
.or_else(|| {
let mut matches = self
.forms
.objects
.iter()
.enumerate()
.filter(|(_, o)| o.name.eq_ignore_ascii_case(name));
let (id, object) = matches.next()?;
matches.next().is_none().then_some((id as u16, object))
})
}
fn scoped_object(
&mut self,
name: &str,
parent: Option<&str>,
pos: SourcePos,
) -> Option<(u16, forms::FormObject)> {
if let Some(parent) = parent {
if let Some((id, object)) = self.forms.find_in(name, parent) {
return Some((id, object.clone()));
}
self.err(
pos,
format!("Control '{name}' not found in form '{parent}'"),
);
None
} else {
self.object_by_name(name, pos)
}
}
fn object_by_name( fn object_by_name(
&mut self, &mut self,
name: &str, name: &str,
pos: SourcePos, pos: SourcePos,
) -> Option<(u16, crate::forms::FormObject)> { ) -> Option<(u16, crate::forms::FormObject)> {
if let Some((id, object)) = self.forms.find(name) { if let Some((id, object)) = self.find_object(name) {
return Some((id, object.clone())); return Some((id, object.clone()));
} }
self.err(pos, format!("Unknown object '{name}'")); self.err(pos, format!("Unknown object '{name}'"));
@@ -936,14 +1078,9 @@ impl Sema {
if self.declared_var(scope, name).is_some() || self.consts.contains_key(name) { if self.declared_var(scope, name).is_some() || self.consts.contains_key(name) {
return None; return None;
} }
let (object, form) = self let (object, form) = self.current_form()?;
.forms
.objects
.iter()
.enumerate()
.find(|(_, object)| object.class == ObjectClass::Form)?;
let (property, spec) = forms::property(form.class, name)?; let (property, spec) = forms::property(form.class, name)?;
Some((object as u16, property, spec)) Some((object, property, spec))
} }
fn object_property( fn object_property(
@@ -958,17 +1095,7 @@ impl Sema {
let (object, member) = path.split_once('.')?; let (object, member) = path.split_once('.')?;
(object, member, None) (object, member, None)
}; };
let (id, object) = self.object_by_name(object_name, pos)?; let (id, object) = self.scoped_object(object_name, parent, pos)?;
if let Some(parent) = parent {
let parent_ok = self.forms.belongs_to(&object, parent);
if !parent_ok {
self.err(
pos,
format!("Control '{object_name}' not found in form '{parent}'"),
);
return None;
}
}
let Some((property, spec)) = forms::property(object.class, member) else { let Some((property, spec)) = forms::property(object.class, member) else {
self.err( self.err(
pos, pos,
@@ -991,16 +1118,7 @@ impl Sema {
let (object, member) = path.split_once('.')?; let (object, member) = path.split_once('.')?;
(object, member, None) (object, member, None)
}; };
let (id, object) = self.object_by_name(object_name, pos)?; let (id, object) = self.scoped_object(object_name, parent, pos)?;
if let Some(parent) = parent {
if !self.forms.belongs_to(&object, parent) {
self.err(
pos,
format!("Control '{object_name}' not found in form '{parent}'"),
);
return None;
}
}
Some((id, object, member.to_string())) Some((id, object, member.to_string()))
} }
@@ -1118,7 +1236,7 @@ impl Sema {
fn alloc_global(&mut self, name: &str, ty: &Ty, array: bool) -> u16 { fn alloc_global(&mut self, name: &str, ty: &Ty, array: bool) -> u16 {
let slot = self.globals.len() as u16; let slot = self.globals.len() as u16;
self.globals.push(hir::HVar { self.globals.push(hir::HVar {
name: name.to_string(), name: name.trim_end_matches("\u{1}AS").to_string(),
ty: self.h_ty(ty), ty: self.h_ty(ty),
array, array,
}); });
@@ -1138,7 +1256,7 @@ impl Sema {
} else { } else {
let slot = scope.locals.len() as u16; let slot = scope.locals.len() as u16;
scope.locals.push(hir::HVar { scope.locals.push(hir::HVar {
name: name.to_string(), name: name.trim_end_matches("\u{1}AS").to_string(),
ty: self.h_ty(ty), ty: self.h_ty(ty),
array, array,
}); });
@@ -1255,7 +1373,7 @@ impl Sema {
let ty = self.name_ty(name, suffix); let ty = self.name_ty(name, suffix);
// DEF FN: freie Namen binden an Modulvariablen (Vorbild-Semantik). // DEF FN: freie Namen binden an Modulvariablen (Vorbild-Semantik).
let info = if scope.is_def_fn { let info = if scope.is_def_fn {
let slot = self.alloc_global(name, &ty, array); let slot = self.alloc_global(&key, &ty, array);
let info = VarInfo { let info = VarInfo {
ty, ty,
array, array,
@@ -1267,7 +1385,7 @@ impl Sema {
self.module_vars.insert(key, info.clone()); self.module_vars.insert(key, info.clone());
info info
} else { } else {
let (slot, global) = self.alloc_var(scope, name, &ty, array); let (slot, global) = self.alloc_var(scope, &key, &ty, array);
let info = VarInfo { let info = VarInfo {
ty, ty,
array, array,
@@ -1285,12 +1403,12 @@ impl Sema {
/// Konstantenfaltung für `CONST`-Ausdrücke. /// Konstantenfaltung für `CONST`-Ausdrücke.
fn fold_const(&self, e: &Expr) -> Option<ConstVal> { fn fold_const(&self, e: &Expr) -> Option<ConstVal> {
match e { match e {
Expr::IntLit(v) => Some(ConstVal::Num(*v as f64)), Expr::IntLit(v, _) => Some(ConstVal::Num(*v as f64)),
Expr::LongLit(v) => Some(ConstVal::Num(*v as f64)), Expr::LongLit(v, _) => Some(ConstVal::Num(*v as f64)),
Expr::SingleLit(v) => Some(ConstVal::Num(*v as f64)), Expr::SingleLit(v, _) => Some(ConstVal::Num(*v as f64)),
Expr::DoubleLit(v) => Some(ConstVal::Num(*v)), Expr::DoubleLit(v, _) => Some(ConstVal::Num(*v)),
Expr::CurrencyLit(v) => Some(ConstVal::Num(*v as f64 / 10_000.0)), Expr::CurrencyLit(v, _) => Some(ConstVal::Num(*v as f64 / 10_000.0)),
Expr::StrLit(s) => Some(ConstVal::Str(s.clone())), Expr::StrLit(s, _) => Some(ConstVal::Str(s.clone())),
Expr::Paren(e) => self.fold_const(e), Expr::Paren(e) => self.fold_const(e),
Expr::Name { Expr::Name {
name, args: None, .. name, args: None, ..
@@ -1400,15 +1518,18 @@ impl Sema {
// ---- Anweisungen ------------------------------------------------------- // ---- Anweisungen -------------------------------------------------------
fn lower_body(&mut self, stmts: &[Stmt], scope: &mut Scope) -> Vec<HStmt> { fn lower_body(&mut self, stmts: &[Stmt], scope: &mut Scope) -> Vec<HStmt> {
let enclosing = (scope.current_line, scope.current_pos);
let mut out = Vec::new(); let mut out = Vec::new();
for stmt in stmts { for stmt in stmts {
self.lower_stmt(stmt, scope, &mut out); self.lower_stmt(stmt, scope, &mut out);
} }
(scope.current_line, scope.current_pos) = enclosing;
out out
} }
fn push(&mut self, out: &mut Vec<HStmt>, scope: &Scope, kind: HStmtKind) { fn push(&mut self, out: &mut Vec<HStmt>, scope: &Scope, kind: HStmtKind) {
out.push(HStmt { out.push(HStmt {
pos: scope.current_pos,
line: scope.current_line, line: scope.current_line,
kind, kind,
}); });
@@ -1419,6 +1540,7 @@ impl Sema {
let pos = stmt_pos(stmt); let pos = stmt_pos(stmt);
if pos.line > 0 { if pos.line > 0 {
scope.current_line = pos.line; scope.current_line = pos.line;
scope.current_pos = pos;
} }
match stmt { match stmt {
Stmt::Data { .. } Stmt::Data { .. }
@@ -1430,7 +1552,7 @@ impl Sema {
self.push(out, scope, HStmtKind::Label(id)); self.push(out, scope, HStmtKind::Label(id));
} }
} }
Stmt::LineNumber(n) => { Stmt::LineNumber(n, _) => {
if let Some(id) = scope.line_labels.get(n).copied() { if let Some(id) = scope.line_labels.get(n).copied() {
self.push(out, scope, HStmtKind::Label(id)); self.push(out, scope, HStmtKind::Label(id));
} }
@@ -1637,7 +1759,7 @@ impl Sema {
} }
return; return;
} }
if self.forms.find(name).is_some() { if self.find_object(name).is_some() {
self.err(*pos, format!("Object '{name}' has no default property")); self.err(*pos, format!("Object '{name}' has no default property"));
self.lower_expr(value, scope); self.lower_expr(value, scope);
return; return;
@@ -1895,11 +2017,12 @@ impl Sema {
Some(b) => self.lower_body(b, scope), Some(b) => self.lower_body(b, scope),
None => Vec::new(), None => Vec::new(),
}; };
for (ec, eb) in elseifs.iter().rev() { for (ec, eb, elseif_pos) in elseifs.iter().rev() {
let c2 = self.lower_cond(ec, scope); let c2 = self.lower_cond(ec, scope);
let body2 = self.lower_body(eb, scope); let body2 = self.lower_body(eb, scope);
let stmt = HStmt { let stmt = HStmt {
line: scope.current_line, pos: *elseif_pos,
line: elseif_pos.line,
kind: HStmtKind::If { kind: HStmtKind::If {
cond: c2, cond: c2,
then: body2, then: body2,
@@ -1920,6 +2043,7 @@ impl Sema {
step, step,
body, body,
pos, pos,
end_pos,
} => { } => {
let (place, vt) = self.lower_place(var, scope); let (place, vt) = self.lower_place(var, scope);
if !is_num(&vt) { if !is_num(&vt) {
@@ -1956,6 +2080,7 @@ impl Sema {
ty, ty,
from: from_e, from: from_e,
to: to_e, to: to_e,
end_pos: *end_pos,
step: step_e, step: step_e,
limit_slot, limit_slot,
step_slot, step_slot,
@@ -1966,7 +2091,11 @@ impl Sema {
} }
} }
Stmt::DoLoop { Stmt::DoLoop {
pre, post, body, .. pre,
post,
body,
end_pos,
..
} => { } => {
let pre_c = pre.as_ref().map(|(u, c)| (*u, self.lower_cond(c, scope))); let pre_c = pre.as_ref().map(|(u, c)| (*u, self.lower_cond(c, scope)));
let exit_label = scope.new_label(); let exit_label = scope.new_label();
@@ -1978,6 +2107,7 @@ impl Sema {
out, out,
scope, scope,
HStmtKind::Loop { HStmtKind::Loop {
end_pos: *end_pos,
pre: pre_c, pre: pre_c,
post: post_c, post: post_c,
body: hbody, body: hbody,
@@ -1985,7 +2115,12 @@ impl Sema {
}, },
); );
} }
Stmt::While { cond, body, .. } => { Stmt::While {
cond,
body,
end_pos,
..
} => {
let c = self.lower_cond(cond, scope); let c = self.lower_cond(cond, scope);
let exit_label = scope.new_label(); let exit_label = scope.new_label();
let hbody = self.lower_body(body, scope); let hbody = self.lower_body(body, scope);
@@ -1993,6 +2128,7 @@ impl Sema {
out, out,
scope, scope,
HStmtKind::Loop { HStmtKind::Loop {
end_pos: *end_pos,
pre: Some((false, c)), pre: Some((false, c)),
post: None, post: None,
body: hbody, body: hbody,
@@ -2120,8 +2256,11 @@ impl Sema {
let ty = self.decl_ty(d); let ty = self.decl_ty(d);
let key = self.var_key(&d.name, &d.suffix, d.as_type.is_some()); let key = self.var_key(&d.name, &d.suffix, d.as_type.is_some());
if let Entry::Vacant(entry) = scope.vars.entry(key) { if let Entry::Vacant(entry) = scope.vars.entry(key) {
let slot = let slot = self.alloc_global(
self.alloc_global(&format!("STATIC.{}", d.name), &ty, d.dims.is_some()); &format!("STATIC.{}", entry.key()),
&ty,
d.dims.is_some(),
);
entry.insert(VarInfo { entry.insert(VarInfo {
ty, ty,
array: d.dims.is_some(), array: d.dims.is_some(),
@@ -2133,7 +2272,12 @@ impl Sema {
} }
} }
} }
Stmt::CommonDecl { shared, decls, .. } => { Stmt::CommonDecl {
shared,
decls,
block,
..
} => {
for d in decls { for d in decls {
if *shared && scope.is_module { if *shared && scope.is_module {
self.shared_vars.insert(self.var_key( self.shared_vars.insert(self.var_key(
@@ -2143,6 +2287,30 @@ impl Sema {
)); ));
} }
self.declare(d, false, scope, out); self.declare(d, false, scope, out);
if scope.is_module {
let key = self.var_key(&d.name, &d.suffix, d.as_type.is_some());
let slot = self.module_vars[&key].slot;
let dims = d.dims.as_ref().map(|dims| {
dims.iter()
.map(|(lo, hi)| {
let lower =
lo.as_ref().map_or(Some(self.option_base as i32), |e| {
self.common_bound(e)
});
let upper = self.common_bound(hi);
(lower, upper)
})
.collect()
});
self.commons.push(hir::HCommon {
slot,
block: block.clone(),
key,
ty: self.globals[slot as usize].ty.clone(),
dims,
pos: d.pos,
});
}
} }
} }
Stmt::SharedDecl { decls, .. } => { Stmt::SharedDecl { decls, .. } => {
@@ -2153,7 +2321,7 @@ impl Sema {
Some(v) => v.clone(), Some(v) => v.clone(),
None => { None => {
let ty = self.decl_ty(d); let ty = self.decl_ty(d);
let slot = self.alloc_global(&d.name, &ty, d.dims.is_some()); let slot = self.alloc_global(&key, &ty, d.dims.is_some());
let v = VarInfo { let v = VarInfo {
ty, ty,
array: d.dims.is_some(), array: d.dims.is_some(),
@@ -2255,7 +2423,7 @@ impl Sema {
body, body,
pos, pos,
} => { } => {
self.lower_def_fn(name, suffix, params, None, Some(body), scope, *pos); self.lower_def_fn(name, suffix, params, None, Some(body), *pos);
} }
Stmt::DefFnBlock { Stmt::DefFnBlock {
name, name,
@@ -2264,7 +2432,7 @@ impl Sema {
body, body,
pos, pos,
} => { } => {
self.lower_def_fn(name, suffix, params, Some(body), None, scope, *pos); self.lower_def_fn(name, suffix, params, Some(body), None, *pos);
} }
// ---- Datei-E/A (Grammatik Phase 1, Laufzeit Phase 3) ---- // ---- Datei-E/A (Grammatik Phase 1, Laufzeit Phase 3) ----
Stmt::Open { Stmt::Open {
@@ -2318,7 +2486,7 @@ impl Sema {
ne, ne,
HExpr::Str(table.clone()), HExpr::Str(table.clone()),
HExpr::Str(spalten), HExpr::Str(spalten),
HExpr::Lng(udt as i32), HExpr::UdtId(udt),
], ],
}, },
); );
@@ -2848,7 +3016,7 @@ impl Sema {
); );
// Arme in Bedingungs-/Rumpf-Paare übersetzen; CASE ELSE gesondert. // Arme in Bedingungs-/Rumpf-Paare übersetzen; CASE ELSE gesondert.
let mut cases: Vec<(HExpr, Vec<HStmt>)> = Vec::new(); let mut cases: Vec<(HExpr, Vec<HStmt>, SourcePos)> = Vec::new();
let mut else_body: Vec<HStmt> = Vec::new(); let mut else_body: Vec<HStmt> = Vec::new();
for arm in arms { for arm in arms {
let body = self.lower_body(&arm.body, scope); let body = self.lower_body(&arm.body, scope);
@@ -2869,13 +3037,14 @@ impl Sema {
}, },
}); });
} }
cases.push((cond.unwrap(), body)); cases.push((cond.unwrap(), body, arm.pos));
} }
// Von hinten zu verschachteltem If zusammensetzen. // Von hinten zu verschachteltem If zusammensetzen.
let mut els = else_body; let mut els = else_body;
for (cond, body) in cases.into_iter().rev() { for (cond, body, pos) in cases.into_iter().rev() {
let stmt = HStmt { let stmt = HStmt {
line: scope.current_line, pos,
line: pos.line,
kind: HStmtKind::If { kind: HStmtKind::If {
cond, cond,
then: body, then: body,
@@ -2947,7 +3116,6 @@ impl Sema {
} }
} }
#[allow(clippy::too_many_arguments)]
fn lower_def_fn( fn lower_def_fn(
&mut self, &mut self,
name: &str, name: &str,
@@ -2955,7 +3123,6 @@ impl Sema {
params: &[Param], params: &[Param],
block: Option<&[Stmt]>, block: Option<&[Stmt]>,
single: Option<&Expr>, single: Option<&Expr>,
scope: &mut Scope,
pos: SourcePos, pos: SourcePos,
) { ) {
// Registrierung (mit Prozedur-Id). // Registrierung (mit Prozedur-Id).
@@ -2981,12 +3148,12 @@ impl Sema {
def_fn: true, def_fn: true,
}, },
); );
let _ = pos;
// Rumpf-Scope: Parameter (BYVAL) und Rückgabevariable lokal, // Rumpf-Scope: Parameter (BYVAL) und Rückgabevariable lokal,
// freie Namen binden an Modulvariablen. // freie Namen binden an Modulvariablen.
let mut fscope = Scope { let mut fscope = Scope {
is_def_fn: true, is_def_fn: true,
current_line: pos.line,
current_pos: pos,
..Scope::default() ..Scope::default()
}; };
let mut hparams = Vec::new(); let mut hparams = Vec::new();
@@ -2995,7 +3162,7 @@ impl Sema {
let key = self.var_key(&p.name, &p.suffix, false); let key = self.var_key(&p.name, &p.suffix, false);
let slot = fscope.locals.len() as u16; let slot = fscope.locals.len() as u16;
fscope.locals.push(hir::HVar { fscope.locals.push(hir::HVar {
name: p.name.clone(), name: key.trim_end_matches("\u{1}AS").to_string(),
ty: self.h_ty(&ty), ty: self.h_ty(&ty),
array: false, array: false,
}); });
@@ -3020,7 +3187,7 @@ impl Sema {
let ret_key = self.var_key(name, suffix, false); let ret_key = self.var_key(name, suffix, false);
let ret_slot_idx = fscope.locals.len() as u16; let ret_slot_idx = fscope.locals.len() as u16;
fscope.locals.push(hir::HVar { fscope.locals.push(hir::HVar {
name: name.to_string(), name: name.trim_end_matches("\u{1}AS").to_string(),
ty: self.h_ty(&ret), ty: self.h_ty(&ret),
array: false, array: false,
}); });
@@ -3045,6 +3212,7 @@ impl Sema {
let (e, et) = self.lower_expr(expr, &mut fscope); let (e, et) = self.lower_expr(expr, &mut fscope);
let value = self.coerce(e, &et, &ret, expr.pos()); let value = self.coerce(e, &et, &ret, expr.pos());
vec![HStmt { vec![HStmt {
pos: fscope.current_pos,
line: fscope.current_line, line: fscope.current_line,
kind: HStmtKind::Assign { kind: HStmtKind::Assign {
place: HPlace { place: HPlace {
@@ -3072,14 +3240,24 @@ impl Sema {
body, body,
label_count: fscope.next_label, label_count: fscope.next_label,
}; };
// Modul-Scope-Zeile weiterführen.
scope.current_line = scope.current_line.max(fscope.current_line);
while self.hir_procs.len() <= id as usize { while self.hir_procs.len() <= id as usize {
self.hir_procs.push(None); self.hir_procs.push(None);
} }
self.hir_procs[id as usize] = Some(hproc); self.hir_procs[id as usize] = Some(hproc);
} }
fn common_bound(&self, expr: &Expr) -> Option<i32> {
match self.fold_const(expr)? {
ConstVal::Num(n)
if n.is_finite()
&& (i32::MIN as f64..=i32::MAX as f64).contains(&n.round_ties_even()) =>
{
Some(n.round_ties_even() as i32)
}
_ => None,
}
}
fn declare(&mut self, d: &VarDecl, redim: bool, scope: &mut Scope, out: &mut Vec<HStmt>) { fn declare(&mut self, d: &VarDecl, redim: bool, scope: &mut Scope, out: &mut Vec<HStmt>) {
let ty = self.decl_ty(d); let ty = self.decl_ty(d);
let mut hdims = Vec::new(); let mut hdims = Vec::new();
@@ -3120,7 +3298,7 @@ impl Sema {
return; return;
} }
} }
let (slot, global) = self.alloc_var(scope, &d.name, &ty, is_array); let (slot, global) = self.alloc_var(scope, &key, &ty, is_array);
self.insert_var( self.insert_var(
scope, scope,
key, key,
@@ -3249,13 +3427,7 @@ impl Sema {
return; return;
} }
if matches!(name, "SHOW" | "HIDE") && args.is_empty() { if matches!(name, "SHOW" | "HIDE") && args.is_empty() {
if let Some((object, _)) = self if let Some((object, _)) = self.current_form() {
.forms
.objects
.iter()
.enumerate()
.find(|(_, object)| object.class == ObjectClass::Form)
{
let method = forms::methods(ObjectClass::Form) let method = forms::methods(ObjectClass::Form)
.iter() .iter()
.position(|method| *method == name) .position(|method| *method == name)
@@ -3264,7 +3436,7 @@ impl Sema {
out, out,
scope, scope,
HStmtKind::ObjectMethod { HStmtKind::ObjectMethod {
object: object as u16, object,
index: None, index: None,
method, method,
args: Vec::new(), args: Vec::new(),
@@ -3343,20 +3515,15 @@ impl Sema {
pos: object_pos, pos: object_pos,
} = &args[0] } = &args[0]
{ {
if let Some((object, info)) = let (parent, object_name) = object_name
self.forms.find(object_name).map(|(i, o)| (i, o.clone())) .split_once('!')
{ .map_or((None, object_name.as_str()), |(parent, name)| {
if index.is_some() && !info.array { (Some(parent), name)
self.err( });
*object_pos, if let Some((object, _)) = self.scoped_object(object_name, parent, *object_pos) {
format!("Object '{object_name}' is not an array"), let Ok(index) = self.object_index(object, index, scope, *object_pos) else {
);
return; return;
} };
let index = index
.as_ref()
.and_then(|a| a.first())
.map(|e| self.lower_num_as(e, scope, NumTy::Lng));
self.push( self.push(
out, out,
scope, scope,
@@ -3368,7 +3535,6 @@ impl Sema {
); );
return; return;
} }
self.err(*object_pos, format!("Unknown object '{object_name}'"));
return; return;
} }
} }
@@ -3514,7 +3680,7 @@ impl Sema {
if self.consts.contains_key(name) { if self.consts.contains_key(name) {
return false; return false;
} }
if self.forms.find(name).is_some() if (suffix.is_none() && self.find_object(name).is_some())
|| (name.contains('!') && !self.is_udt_path(scope, name)) || (name.contains('!') && !self.is_udt_path(scope, name))
{ {
return false; return false;
@@ -3978,7 +4144,7 @@ impl Sema {
/// Greift nur bei Stringliteralen; ein zur Laufzeit gebildeter Gerätename /// Greift nur bei Stringliteralen; ein zur Laufzeit gebildeter Gerätename
/// bleibt der Datei-E/A überlassen. /// bleibt der Datei-E/A überlassen.
fn reject_com_device(&mut self, file: &Expr, pos: SourcePos) { fn reject_com_device(&mut self, file: &Expr, pos: SourcePos) {
if let Expr::StrLit(s) = file { if let Expr::StrLit(s, _) = file {
if ist_com_geraet(s) { if ist_com_geraet(s) {
self.err(pos, "Feature unavailable"); self.err(pos, "Feature unavailable");
} }
@@ -4108,7 +4274,7 @@ impl Sema {
return ( return (
Some(HPlace { Some(HPlace {
base: base_slot, base: base_slot,
base_is_ref: false, base_is_ref: info.by_ref,
indices: indices.unwrap_or_default(), indices: indices.unwrap_or_default(),
fields: fpath, fields: fpath,
ty: hty, ty: hty,
@@ -4157,12 +4323,12 @@ impl Sema {
fn lower_expr(&mut self, e: &Expr, scope: &mut Scope) -> (HExpr, Ty) { fn lower_expr(&mut self, e: &Expr, scope: &mut Scope) -> (HExpr, Ty) {
match e { match e {
Expr::IntLit(v) => (HExpr::Int(*v), Ty::Int), Expr::IntLit(v, _) => (HExpr::Int(*v), Ty::Int),
Expr::LongLit(v) => (HExpr::Lng(*v), Ty::Lng), Expr::LongLit(v, _) => (HExpr::Lng(*v), Ty::Lng),
Expr::SingleLit(v) => (HExpr::Sng(*v), Ty::Sng), Expr::SingleLit(v, _) => (HExpr::Sng(*v), Ty::Sng),
Expr::DoubleLit(v) => (HExpr::Dbl(*v), Ty::Dbl), Expr::DoubleLit(v, _) => (HExpr::Dbl(*v), Ty::Dbl),
Expr::CurrencyLit(v) => (HExpr::Cur(*v), Ty::Cur), Expr::CurrencyLit(v, _) => (HExpr::Cur(*v), Ty::Cur),
Expr::StrLit(s) => (HExpr::Str(s.clone()), Ty::Str), Expr::StrLit(s, _) => (HExpr::Str(s.clone()), Ty::Str),
Expr::Paren(inner) => self.lower_expr(inner, scope), Expr::Paren(inner) => self.lower_expr(inner, scope),
Expr::Missing => (HExpr::Int(0), Ty::Unknown), Expr::Missing => (HExpr::Int(0), Ty::Unknown),
Expr::TypeOf { value, class, pos } => { Expr::TypeOf { value, class, pos } => {
@@ -4659,7 +4825,11 @@ impl Sema {
); );
return (HExpr::Int(0), Ty::Unknown); return (HExpr::Int(0), Ty::Unknown);
} }
if let Some((object, info)) = self.forms.find(name).map(|(id, info)| (id, info.clone())) { if let Some((object, info)) = self
.find_object(name)
.filter(|_| suffix.is_none())
.map(|(id, info)| (id, info.clone()))
{
if args.is_none() || info.array { if args.is_none() || info.array {
let Ok(index) = self.object_index(object, args, scope, pos) else { let Ok(index) = self.object_index(object, args, scope, pos) else {
return (HExpr::Int(0), Ty::Unknown); return (HExpr::Int(0), Ty::Unknown);
@@ -5296,7 +5466,7 @@ impl Sema {
} }
/// Position einer Anweisung (für Zeileninfo der Anweisungsgrenzen). /// Position einer Anweisung (für Zeileninfo der Anweisungsgrenzen).
fn stmt_pos(stmt: &Stmt) -> SourcePos { pub fn stmt_pos(stmt: &Stmt) -> SourcePos {
match stmt { match stmt {
Stmt::Assign { pos, .. } Stmt::Assign { pos, .. }
| Stmt::Print { pos, .. } | Stmt::Print { pos, .. }
@@ -5352,7 +5522,8 @@ fn stmt_pos(stmt: &Stmt) -> SourcePos {
| Stmt::End(pos) | Stmt::End(pos)
| Stmt::StopStmt(pos) | Stmt::StopStmt(pos)
| Stmt::System(pos) => *pos, | Stmt::System(pos) => *pos,
Stmt::Label(_) | Stmt::LineNumber(_) => SourcePos::default(), Stmt::LineNumber(_, pos) => *pos,
Stmt::Label(_) => SourcePos::default(),
} }
} }
@@ -5377,10 +5548,10 @@ fn trap_art(device: &str) -> Option<u8> {
/// Konstanter Zahlenwert eines Ausdrucks, soweit direkt ablesbar. /// Konstanter Zahlenwert eines Ausdrucks, soweit direkt ablesbar.
fn const_zahl(e: &Expr) -> Option<i64> { fn const_zahl(e: &Expr) -> Option<i64> {
match e { match e {
Expr::IntLit(n) => Some(*n as i64), Expr::IntLit(n, _) => Some(*n as i64),
Expr::LongLit(n) => Some(*n as i64), Expr::LongLit(n, _) => Some(*n as i64),
Expr::SingleLit(n) => Some(*n as i64), Expr::SingleLit(n, _) => Some(*n as i64),
Expr::DoubleLit(n) => Some(*n as i64), Expr::DoubleLit(n, _) => Some(*n as i64),
Expr::Paren(inner) => const_zahl(inner), Expr::Paren(inner) => const_zahl(inner),
_ => None, _ => None,
} }

View File

@@ -0,0 +1,96 @@
//! Quellen bleiben auch nach Include-Expansion einem Modul und einer Datei zugeordnet.
use crate::{ast, lexer, parser, Diagnostic, SourcePos};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SourceFile {
pub module: u16,
pub path: String,
}
#[derive(Debug, Clone)]
pub struct SourceSegment {
pub file: String,
pub first_line: u32,
pub text: String,
}
#[derive(Debug, Clone)]
pub struct SourceUnit {
pub name: String,
pub segments: Vec<SourceSegment>,
}
impl SourceUnit {
pub fn new(name: &str, file: &str, text: &str) -> Self {
Self {
name: name.into(),
segments: vec![SourceSegment {
file: file.into(),
first_line: 1,
text: text.into(),
}],
}
}
pub fn parse(
&self,
module: u16,
sources: &mut Vec<SourceFile>,
) -> (ast::Module, Vec<Diagnostic>) {
let mut text = String::new();
let mut origins = Vec::new();
for segment in &self.segments {
let file = SourceFile {
module,
path: segment.file.clone(),
};
let source = sources.iter().position(|s| s == &file).unwrap_or_else(|| {
sources.push(file);
sources.len() - 1
}) as u32;
for (line, part) in segment.text.split_inclusive('\n').enumerate() {
origins.push(SourcePos {
source,
line: segment.first_line + line as u32,
column: 1,
});
text.push_str(part);
if !part.ends_with('\n') {
text.push('\n');
}
}
}
let map = |pos: &mut SourcePos| {
let origin = origins
.get(pos.line.saturating_sub(1) as usize)
.copied()
.or_else(|| {
origins.last().map(|p| SourcePos {
line: p.line + 1,
..*p
})
})
.unwrap_or_default();
pos.source = origin.source;
pos.line = origin.line;
};
let mut lexed = lexer::lex(&text);
for token in &mut lexed.tokens {
map(&mut token.pos);
}
for diagnostic in &mut lexed.diagnostics {
map(&mut diagnostic.pos);
}
let parsed = parser::parse(&self.name, &lexed.tokens);
lexed.diagnostics.extend(parsed.diagnostics);
(parsed.module, lexed.diagnostics)
}
}
pub fn locate_diagnostics(diagnostics: &mut [Diagnostic], sources: &[SourceFile]) {
for diagnostic in diagnostics {
diagnostic.file = sources
.get(diagnostic.pos.source as usize)
.map(|s| s.path.clone());
}
}

View File

@@ -252,12 +252,7 @@ impl FormsModel {
return Ok(PropertyValue::Integer(value.saturating_sub(2))); return Ok(PropertyValue::Integer(value.saturating_sub(2)));
} }
if spec.name == "PARENT" { if spec.name == "PARENT" {
let parent = obj.description.parent_form.as_deref().and_then(|name| { let parent = obj.description.parent.map(|id| (id, None));
self.objects
.iter()
.position(|candidate| candidate.description.name.eq_ignore_ascii_case(name))
.map(|id| (id as u16, None))
});
return Ok(PropertyValue::Object(parent)); return Ok(PropertyValue::Object(parent));
} }
Ok(obj.properties[property as usize].clone()) Ok(obj.properties[property as usize].clone())
@@ -834,12 +829,7 @@ impl FormsModel {
if obj.description.class == ObjectClass::Form { if obj.description.class == ObjectClass::Form {
return Some(current); return Some(current);
} }
let parent = obj.description.parent_form.as_deref()?; current = obj.description.parent?;
current = self
.objects
.iter()
.position(|candidate| candidate.description.name.eq_ignore_ascii_case(parent))?
as u16;
} }
None None
} }
@@ -959,7 +949,7 @@ impl FormsModel {
} }
fn select_option(&mut self, key: ObjectKey) -> Result<(), RuntimeError> { fn select_option(&mut self, key: ObjectKey) -> Result<(), RuntimeError> {
let parent = self.instance(key)?.description.parent_form.clone(); let parent = self.instance(key)?.description.parent;
let value_id = forms::property(ObjectClass::OptionButton, "VALUE") let value_id = forms::property(ObjectClass::OptionButton, "VALUE")
.unwrap() .unwrap()
.0 as usize; .0 as usize;
@@ -969,7 +959,7 @@ impl FormsModel {
} }
let same_group = self.instance(other).is_ok_and(|obj| { let same_group = self.instance(other).is_ok_and(|obj| {
obj.description.class == ObjectClass::OptionButton obj.description.class == ObjectClass::OptionButton
&& obj.description.parent_form == parent && obj.description.parent == parent
}); });
if same_group { if same_group {
self.instance_mut(other)?.properties[value_id] = PropertyValue::Integer(0); self.instance_mut(other)?.properties[value_id] = PropertyValue::Integer(0);
@@ -1006,12 +996,8 @@ impl FormsModel {
&& self && self
.instance(key) .instance(key)
.ok() .ok()
.and_then(|obj| obj.description.parent_form.as_deref()) .and_then(|obj| obj.description.parent)
.and_then(|parent| { .and_then(|parent| self.objects.get(parent as usize))
self.objects
.iter()
.find(|obj| obj.description.name.eq_ignore_ascii_case(parent))
})
.is_some_and(|parent| parent.description.class == ObjectClass::Form); .is_some_and(|parent| parent.description.class == ObjectClass::Form);
if invalid || title_shortcut { if invalid || title_shortcut {
Err(RuntimeError::ILLEGAL_FUNCTION_CALL) Err(RuntimeError::ILLEGAL_FUNCTION_CALL)
@@ -1325,19 +1311,12 @@ impl FormsModel {
} }
fn menu_children(&self, parent: ObjectKey) -> Vec<ObjectKey> { fn menu_children(&self, parent: ObjectKey) -> Vec<ObjectKey> {
let Ok(parent) = self.instance(parent) else {
return Vec::new();
};
self.keys() self.keys()
.into_iter() .into_iter()
.filter(|key| { .filter(|key| {
self.instance(*key).is_ok_and(|object| { self.instance(*key).is_ok_and(|object| {
object.description.class == ObjectClass::Menu object.description.class == ObjectClass::Menu
&& object && object.description.parent == Some(parent.0)
.description
.parent_form
.as_deref()
.is_some_and(|name| name.eq_ignore_ascii_case(&parent.description.name))
}) && self.boolean(*key, "VISIBLE") }) && self.boolean(*key, "VISIBLE")
}) })
.collect() .collect()
@@ -1385,23 +1364,15 @@ impl FormsModel {
} }
if shift & umschalt::ALT != 0 { if shift & umschalt::ALT != 0 {
if let Some(ch) = key.chars().next().map(|ch| ch.to_ascii_uppercase()) { if let Some(ch) = key.chars().next().map(|ch| ch.to_ascii_uppercase()) {
if let Some(target) = if let Some(target) = self.form_controls(form).into_iter().find(|candidate| {
self.form_controls(form).into_iter().find(|candidate| { Self::access_key(&self.string(*candidate, "CAPTION")) == Some(ch)
Self::access_key(&self.string(*candidate, "CAPTION")) == Some(ch) && self.boolean(*candidate, "ENABLED")
&& self.boolean(*candidate, "ENABLED") && self.boolean(*candidate, "VISIBLE")
&& self.boolean(*candidate, "VISIBLE") && self.instance(*candidate).is_ok_and(|obj| {
&& self.instance(*candidate).is_ok_and(|obj| { obj.description.class != ObjectClass::Menu
obj.description.class != ObjectClass::Menu || obj.description.parent == Some(form)
|| obj.description.parent_form.as_deref().is_some_and( })
|parent| { }) {
parent.eq_ignore_ascii_case(
&self.objects[form as usize].description.name,
)
},
)
})
})
{
if self if self
.instance(target) .instance(target)
.is_ok_and(|obj| obj.description.class == ObjectClass::Menu) .is_ok_and(|obj| obj.description.class == ObjectClass::Menu)
@@ -1580,21 +1551,14 @@ impl FormsModel {
let mut top = self.integer(key, "TOP").unwrap_or(0).max(0) as usize + 1; let mut top = self.integer(key, "TOP").unwrap_or(0).max(0) as usize + 1;
let width = self.integer(key, "WIDTH").unwrap_or(1).max(1) as usize; let width = self.integer(key, "WIDTH").unwrap_or(1).max(1) as usize;
let height = self.integer(key, "HEIGHT").unwrap_or(1).max(1) as usize; let height = self.integer(key, "HEIGHT").unwrap_or(1).max(1) as usize;
let mut parent = obj.description.parent_form.as_deref(); let mut parent = obj.description.parent;
for _ in 0..self.objects.len() { for _ in 0..self.objects.len() {
let Some(name) = parent else { break }; let Some(id) = parent else { break };
let Some((id, parent_obj)) = self let parent_obj = self.objects.get(id as usize)?;
.objects let parent_key = (id, None);
.iter()
.enumerate()
.find(|(_, candidate)| candidate.description.name.eq_ignore_ascii_case(name))
else {
break;
};
let parent_key = (id as u16, None);
left += self.integer(parent_key, "LEFT").unwrap_or(0).max(0) as usize; left += self.integer(parent_key, "LEFT").unwrap_or(0).max(0) as usize;
top += self.integer(parent_key, "TOP").unwrap_or(0).max(0) as usize; top += self.integer(parent_key, "TOP").unwrap_or(0).max(0) as usize;
parent = parent_obj.description.parent_form.as_deref(); parent = parent_obj.description.parent;
} }
Some((left, top, width, height)) Some((left, top, width, height))
} }
@@ -2357,12 +2321,7 @@ impl FormsModel {
&& self && self
.instance(key) .instance(key)
.ok() .ok()
.and_then(|obj| obj.description.parent_form.as_deref()) .is_some_and(|obj| obj.description.parent == Some(form))
.is_some_and(|parent| {
parent.eq_ignore_ascii_case(
&self.objects[form as usize].description.name,
)
})
{ {
if self.boolean(key, "VISIBLE") { if self.boolean(key, "VISIBLE") {
let caption = format!(" {} ", self.caption(key)); let caption = format!(" {} ", self.caption(key));
@@ -2419,20 +2378,12 @@ impl FormsModel {
.is_some_and(|root| self.root_form(*root) == Some(form)) .is_some_and(|root| self.root_form(*root) == Some(form))
{ {
let mut popup_left = left + 1; let mut popup_left = left + 1;
for root in for root in self.form_controls(form).into_iter().filter(|key| {
self.form_controls(form).into_iter().filter(|key| { self.instance(*key).is_ok_and(|object| {
self.instance(*key).is_ok_and(|object| { object.description.class == ObjectClass::Menu
object.description.class == ObjectClass::Menu && object.description.parent == Some(form)
&& object.description.parent_form.as_deref().is_some_and( }) && self.boolean(*key, "VISIBLE")
|parent| { }) {
parent.eq_ignore_ascii_case(
&self.objects[form as usize].description.name,
)
},
)
}) && self.boolean(*key, "VISIBLE")
})
{
if Some(&root) == self.menu_path.first() { if Some(&root) == self.menu_path.first() {
break; break;
} }

View File

@@ -56,9 +56,7 @@ impl FormFile {
} else { } else {
let array = forms::property(node.class, "INDEX") let array = forms::property(node.class, "INDEX")
.and_then(|(property, _)| node.properties.get(&property)) .and_then(|(property, _)| node.properties.get(&property))
.is_some_and( .is_some();
|value| matches!(value, PropertyValue::Integer(index) if *index != 0),
);
catalog.add(&node.name, node.class, parent, array); catalog.add(&node.name, node.class, parent, array);
} }
for child in &node.children { for child in &node.children {
@@ -70,44 +68,99 @@ impl FormFile {
catalog catalog
} }
pub fn apply(&self, model: &mut FormsModel) -> Result<(), tb_runtime::errors::RuntimeError> { pub fn code_line(&self) -> u32 {
fn apply_node( self.original.as_ref().map_or(1, |original| {
original.bytes[..original.bytes.len() - original.code.len()]
.bytes()
.filter(|b| *b == b'\n')
.count() as u32
+ 1
})
}
pub fn initial_values(
&self,
catalog: &FormCatalog,
) -> Result<Vec<FormInitial>, tb_runtime::errors::RuntimeError> {
fn collect(
node: &FormNode, node: &FormNode,
model: &mut FormsModel, parent: Option<u16>,
menu_depth: usize, catalog: &FormCatalog,
out: &mut Vec<FormInitial>,
depth: usize,
) -> Result<(), tb_runtime::errors::RuntimeError> { ) -> Result<(), tb_runtime::errors::RuntimeError> {
let menu_depth = if node.class == ObjectClass::Menu { let depth = depth + usize::from(node.class == ObjectClass::Menu);
menu_depth + 1 if depth > 6 {
} else { return Err(tb_runtime::errors::RuntimeError(5));
menu_depth
};
if menu_depth > 6 {
return Err(tb_runtime::errors::RuntimeError::ILLEGAL_FUNCTION_CALL);
} }
let object = model let object = catalog
.objects .objects
.iter() .iter()
.position(|candidate| candidate.description.name.eq_ignore_ascii_case(&node.name)) .position(|o| {
o.name.eq_ignore_ascii_case(&node.name)
&& o.parent == parent
&& o.class == node.class
})
.ok_or(tb_runtime::errors::RuntimeError(420))? as u16; .ok_or(tb_runtime::errors::RuntimeError(420))? as u16;
let index = forms::property(node.class, "INDEX") let index = forms::property(node.class, "INDEX")
.and_then(|(property, _)| node.properties.get(&property)) .and_then(|(id, _)| node.properties.get(&id))
.and_then(|value| match value { .and_then(|v| {
PropertyValue::Integer(value) => Some(*value), if let PropertyValue::Integer(n) = v {
_ => None, Some(*n)
} else {
None
}
}) })
.unwrap_or(0); .unwrap_or(0);
if index != 0 && !model.is_loaded_at(object, Some(index)) { if out.iter().any(|v| v.object == object && v.index == index) {
model.load_design_array(object, index)?; return Err(tb_runtime::errors::RuntimeError(5));
}
for (property, value) in &node.properties {
model.set_initial_at(object, Some(index), *property, value.clone())?;
} }
out.push(FormInitial {
object,
index,
properties: node.properties.clone(),
});
for child in &node.children { for child in &node.children {
apply_node(child, model, menu_depth)?; collect(child, Some(object), catalog, out, depth)?;
} }
Ok(()) Ok(())
} }
apply_node(&self.root, model, 0) let mut values = Vec::new();
collect(&self.root, None, catalog, &mut values, 0)?;
Ok(values)
}
pub fn apply(&self, model: &mut FormsModel) -> Result<(), tb_runtime::errors::RuntimeError> {
let catalog = FormCatalog {
objects: model
.objects
.iter()
.map(|o| o.description.clone())
.collect(),
};
for initial in self.initial_values(&catalog)? {
initial.apply(model)?;
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct FormInitial {
pub object: u16,
pub index: i32,
pub properties: BTreeMap<u16, PropertyValue>,
}
impl FormInitial {
pub fn apply(&self, model: &mut FormsModel) -> Result<(), tb_runtime::errors::RuntimeError> {
if self.index != 0 && !model.is_loaded_at(self.object, Some(self.index)) {
model.load_design_array(self.object, self.index)?;
}
for (property, value) in &self.properties {
model.set_initial_at(self.object, Some(self.index), *property, value.clone())?;
}
Ok(())
} }
} }

View File

@@ -35,12 +35,13 @@ fn generate_module(n_blocks: usize, seed: usize) -> String {
} }
fn compile_all(sources: &[(String, String)]) -> usize { fn compile_all(sources: &[(String, String)]) -> usize {
let mut total = 0; let units: Vec<_> = sources
for (name, src) in sources { .iter()
let m = tb_vm::compile_source(name, src).expect("Benchmark-Quelle muss kompilieren"); .map(|(name, src)| tb_frontend::source::SourceUnit::new(name, &format!("{name}.bas"), src))
total += m.procs.iter().map(|p| p.code.len()).sum::<usize>(); .collect();
} let project = tb_vm::compile_project("BENCH", &units, &Default::default(), &[])
total .expect("Benchmark-Projekt muss kompilieren");
project.procs.iter().map(|p| p.code.len()).sum()
} }
fn main() { fn main() {

View File

@@ -9,11 +9,14 @@
use std::fmt; use std::fmt;
use std::rc::Rc; use std::rc::Rc;
use tb_frontend::forms::{FormObject, ObjectClass}; use tb_frontend::forms::{FormObject, ObjectClass};
use tb_frontend::hir::HEventProc; use tb_frontend::hir::{HEventProc, HParam, HProcKind, HTy, NumTy};
use tb_frontend::source::SourceFile;
use tb_runtime::value::{TypeInit, UdtLayout}; use tb_runtime::value::{TypeInit, UdtLayout};
use tb_ui::forms::PropertyValue;
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 = 3; pub const TBC_VERSION: u16 = 4;
/// Vergleichsoperator (Operand der `Cmp*`-Instruktionen). /// Vergleichsoperator (Operand der `Cmp*`-Instruktionen).
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -75,8 +78,15 @@ 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> {
if self.pos != self.buf.len() {
Err(LoadError::Corrupt("überzählige Abschnittsdaten"))
} else {
Ok(())
}
}
fn take(&mut self, n: usize) -> Result<&'a [u8], LoadError> { fn take(&mut self, n: usize) -> Result<&'a [u8], LoadError> {
if self.pos + n > self.buf.len() { if n > self.buf.len().saturating_sub(self.pos) {
return Err(LoadError::Corrupt("unerwartetes Dateiende")); return Err(LoadError::Corrupt("unerwartetes Dateiende"));
} }
let s = &self.buf[self.pos..self.pos + n]; let s = &self.buf[self.pos..self.pos + n];
@@ -138,7 +148,11 @@ impl Enc for bool {
out.push(*self as u8); out.push(*self as u8);
} }
fn dec(r: &mut Reader) -> Result<Self, LoadError> { fn dec(r: &mut Reader) -> Result<Self, LoadError> {
Ok(r.u8()? != 0) match r.u8()? {
0 => Ok(false),
1 => Ok(true),
_ => Err(LoadError::Corrupt("bool")),
}
} }
} }
@@ -170,6 +184,9 @@ impl Enc for TypeInit {
fn dec(r: &mut Reader) -> Result<Self, LoadError> { fn dec(r: &mut Reader) -> Result<Self, LoadError> {
let tag = r.u8()?; let tag = r.u8()?;
let extra = r.u32()?; let extra = r.u32()?;
if (tag != 6 && tag != 7 && extra != 0) || (tag == 7 && extra > u16::MAX as u32) {
return Err(LoadError::Corrupt("TypeInit-Zusatz"));
}
Ok(match tag { Ok(match tag {
0 => TypeInit::Int, 0 => TypeInit::Int,
1 => TypeInit::Lng, 1 => TypeInit::Lng,
@@ -224,6 +241,8 @@ instrs! {
0x03 StopInstr; 0x03 StopInstr;
0x04 SystemInstr; 0x04 SystemInstr;
0x05 Unsupported(a: u16); // Name im Stringpool → Fehler 73 0x05 Unsupported(a: u16); // Name im Stringpool → Fehler 73
0x06 Source(a: u32, b: u32); // Quelldatei-ID, physische Spalte der folgenden Stmt-Grenze
0x07 InitStmt(a: u32); // globale Initialisierung: Quellort/Debugger, noch keine Ereignisse
// 0x10 — Konstanten und Stack // 0x10 — Konstanten und Stack
0x10 PushInt(a: i16); 0x10 PushInt(a: i16);
@@ -234,6 +253,7 @@ instrs! {
0x15 PushStr(a: u16); 0x15 PushStr(a: u16);
0x16 Dup; 0x16 Dup;
0x17 Pop; 0x17 Pop;
0x18 PushUdtId(a: u16); // TYPE-Index als LONG auf dem Stack (ISAM)
// 0x20 — Variablen und Referenzen // 0x20 — Variablen und Referenzen
0x20 LoadGlobal(a: u16); 0x20 LoadGlobal(a: u16);
@@ -260,6 +280,8 @@ instrs! {
0x39 ArrBound(a: bool); // true = LBOUND 0x39 ArrBound(a: bool); // true = LBOUND
0x3A FixStr(a: u32); // auf feste Länge kürzen/padden 0x3A FixStr(a: u32); // auf feste Länge kürzen/padden
0x3B CommonArr(a: bool, b: u16, c: u8, d: TypeInit); // gemeinsame Initialisierung/Layoutprüfung
// 0x40 — Arithmetik (monomorph) // 0x40 — Arithmetik (monomorph)
0x40 AddI2; 0x41 AddI4; 0x42 AddR4; 0x43 AddR8; 0x44 AddCy; 0x40 AddI2; 0x41 AddI4; 0x42 AddR4; 0x43 AddR8; 0x44 AddCy;
0x45 SubI2; 0x46 SubI4; 0x47 SubR4; 0x48 SubR8; 0x49 SubCy; 0x45 SubI2; 0x46 SubI4; 0x47 SubR4; 0x48 SubR8; 0x49 SubCy;
@@ -355,10 +377,122 @@ instrs! {
0xD6 LsetRset(a: bool); // rset?; Stack: Referenz, Wert // argc, line_mode — Dateinummer liegt unter den Referenzen 0xD6 LsetRset(a: bool); // rset?; Stack: Referenz, Wert // argc, line_mode — Dateinummer liegt unter den Referenzen
} }
impl Enc for HTy {
fn enc(&self, out: &mut Vec<u8>) {
let tag = match self {
HTy::Num(NumTy::Int) => 0,
HTy::Num(NumTy::Lng) => 1,
HTy::Num(NumTy::Sng) => 2,
HTy::Num(NumTy::Dbl) => 3,
HTy::Num(NumTy::Cur) => 4,
HTy::Str => 5,
HTy::FixedStr(_) => 6,
HTy::Udt(_) => 7,
HTy::Form => 8,
HTy::Control => 9,
};
out.push(tag);
match self {
HTy::FixedStr(n) => n.enc(out),
HTy::Udt(n) => n.enc(out),
_ => {}
}
}
fn dec(r: &mut Reader) -> Result<Self, LoadError> {
Ok(match r.u8()? {
0 => HTy::Num(NumTy::Int),
1 => HTy::Num(NumTy::Lng),
2 => HTy::Num(NumTy::Sng),
3 => HTy::Num(NumTy::Dbl),
4 => HTy::Num(NumTy::Cur),
5 => HTy::Str,
6 => HTy::FixedStr(r.u32()?),
7 => HTy::Udt(r.u16()?),
8 => HTy::Form,
9 => HTy::Control,
_ => return Err(LoadError::Corrupt("Signaturtyp")),
})
}
}
impl Enc for PropertyValue {
fn enc(&self, out: &mut Vec<u8>) {
match self {
Self::Integer(v) => {
out.push(0);
v.enc(out);
}
Self::Single(v) => {
out.push(1);
v.enc(out);
}
Self::String(v) => {
out.push(2);
w_string(out, v);
}
Self::Boolean(v) => {
out.push(3);
v.enc(out);
}
Self::Object(v) => {
out.push(4);
v.is_some().enc(out);
if let Some((object, index)) = v {
object.enc(out);
index.is_some().enc(out);
if let Some(index) = index {
index.enc(out);
}
}
}
Self::IntegerArray(v) => {
out.push(5);
(v.len() as u32).enc(out);
for v in v {
v.enc(out);
}
}
}
}
fn dec(r: &mut Reader) -> Result<Self, LoadError> {
Ok(match r.u8()? {
0 => Self::Integer(i32::dec(r)?),
1 => Self::Single(f32::dec(r)?),
2 => Self::String(r.string()?),
3 => Self::Boolean(bool::dec(r)?),
4 => Self::Object(if bool::dec(r)? {
Some((
r.u16()?,
if bool::dec(r)? {
Some(i32::dec(r)?)
} else {
None
},
))
} else {
None
}),
5 => {
let n = r.u32()?;
let mut values = Vec::new();
for _ in 0..n {
values.push(i32::dec(r)?);
}
Self::IntegerArray(values)
}
_ => return Err(LoadError::Corrupt("Anfangswerttyp")),
})
}
}
// ---- Modulstruktur ------------------------------------------------------------ // ---- Modulstruktur ------------------------------------------------------------
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ProcCode { pub struct ProcCode {
pub module: u16,
pub kind: HProcKind,
pub params: Vec<HParam>,
pub ret_ty: Option<HTy>,
pub name: String, pub name: String,
pub n_params: u16, pub n_params: u16,
/// Initialisierung aller Frame-Slots (Parameter zuerst; deren Init /// Initialisierung aller Frame-Slots (Parameter zuerst; deren Init
@@ -378,6 +512,10 @@ pub struct DataItem {
/// Übersetztes Modul — Inhalt des `.tbc`-Containers. /// Übersetztes Modul — Inhalt des `.tbc`-Containers.
#[derive(Debug)] #[derive(Debug)]
pub struct CompiledModule { pub struct CompiledModule {
pub modules: Vec<(String, u8)>,
pub sources: Vec<SourceFile>,
pub form_initial: Vec<FormInitial>,
pub startup_form: Option<u16>,
pub name: String, pub name: String,
/// `OPTION BASE` (Untergrenze impliziter Arrays). /// `OPTION BASE` (Untergrenze impliziter Arrays).
pub option_base: u8, pub option_base: u8,
@@ -402,9 +540,204 @@ fn w_string(out: &mut Vec<u8>, s: &str) {
} }
impl CompiledModule { impl CompiledModule {
pub fn validate(&self) -> Result<(), LoadError> {
let bad = || LoadError::Corrupt("ungültige Tabellenreferenz oder Anfangsdaten");
if self.procs.is_empty()
|| self.sources.is_empty()
|| self.modules.is_empty()
|| self.modules.len() > u16::MAX as usize
|| self.procs.len() > u16::MAX as usize
|| self.objects.len() >= u16::MAX as usize
|| self.strings.len() >= u16::MAX as usize
|| self.globals_init.len() > u16::MAX as usize
|| self.global_names.len() != self.globals_init.len()
|| self.modules.iter().any(|(_, base)| *base > 1)
|| self.option_base != self.modules[0].1
|| self
.sources
.iter()
.any(|s| s.module as usize >= self.modules.len())
{
return Err(bad());
}
let ty_ok =
|ty: &TypeInit| !matches!(ty, TypeInit::Udt(id) if *id as usize >= self.udts.len());
let sig_ok = |ty: &HTy| !matches!(ty, HTy::Udt(id) if *id as usize >= self.udts.len());
if self.globals_init.iter().any(|t| !ty_ok(t)) {
return Err(bad());
}
for (id, udt) in self.udts.iter().enumerate() {
if udt
.fields
.iter()
.any(|t| matches!(t, TypeInit::Udt(n) if *n as usize >= id))
{
return Err(bad());
}
}
for (id, o) in self.objects.iter().enumerate() {
if o.parent.is_some_and(|parent| parent as usize >= id) {
return Err(bad());
}
if o.parent_form.as_deref()
!= o.parent
.map(|parent| self.objects[parent as usize].name.as_str())
{
return Err(bad());
}
}
if self.startup_form.is_some_and(|id| {
self.objects
.get(id as usize)
.is_none_or(|o| o.class != ObjectClass::Form)
}) {
return Err(bad());
}
for e in &self.event_procs {
if e.object as usize >= self.objects.len() || e.proc as usize >= self.procs.len() {
return Err(bad());
}
}
let mut initials = std::collections::HashSet::new();
for initial in &self.form_initial {
let object = self.objects.get(initial.object as usize).ok_or_else(bad)?;
if !initials.insert((initial.object, initial.index))
|| (initial.index != 0 && !object.array)
{
return Err(bad());
}
for (id, value) in &initial.properties {
use tb_frontend::forms::PropertyType as T;
let spec = tb_frontend::forms::properties(object.class)
.get(*id as usize)
.copied()
.ok_or_else(bad)?;
let valid = match (value, spec.ty) {
(PropertyValue::Integer(v), T::Integer) => {
!spec.min.is_some_and(|min| *v < min)
&& !spec.max.is_some_and(|max| *v > max)
&& (spec.name != "INDEX" || *v == initial.index)
}
(PropertyValue::Single(v), T::Single) => v.is_finite(),
(PropertyValue::String(_), T::String)
| (PropertyValue::Boolean(_), T::Boolean)
| (PropertyValue::IntegerArray(_), T::IntegerArray) => true,
(PropertyValue::Object(v), T::Object) => {
v.is_none_or(|(id, _)| (id as usize) < self.objects.len())
}
_ => false,
};
if !valid {
return Err(bad());
}
}
}
for p in &self.procs {
if p.module as usize >= self.modules.len()
|| p.n_params as usize != p.params.len()
|| p.params.len() > p.locals_init.len()
|| p.locals_init.len() != p.local_names.len()
|| p.locals_init.iter().any(|t| !ty_ok(t))
|| p.params.iter().any(|p| !sig_ok(&p.ty))
|| p.ret_ty.as_ref().is_some_and(|t| !sig_ok(t))
|| matches!(p.kind, HProcKind::Function | HProcKind::DefFn) != p.ret_ty.is_some()
|| p.params.iter().zip(&p.locals_init).any(|(param, init)| {
*init
!= if param.array {
TypeInit::Empty
} else {
crate::codegen::type_init(&param.ty)
}
})
{
return Err(bad());
}
for instruction in &p.code {
use Instr::*;
let valid = match instruction {
Source(id, _) => (*id as usize) < self.sources.len(),
PushUdtId(id) => (*id as usize) < self.udts.len(),
PushStr(id) | Unsupported(id) => (*id as usize) < self.strings.len(),
LoadGlobal(id) | StoreGlobal(id) | MakeRefGlobal(id) => {
(*id as usize) < self.globals_init.len()
}
LoadLocal(id) | StoreLocal(id) | MakeRefLocal(id) | LoadRef(id)
| StoreRef(id) => (*id as usize) < p.locals_init.len(),
LoadArr(global, id, _, ty)
| DimArr(global, id, _, ty)
| CommonArr(global, id, _, ty)
| RedimArr(global, id, _, ty) => {
ty_ok(ty)
&& (*id as usize)
< if *global {
self.globals_init.len()
} else {
p.locals_init.len()
}
}
EraseSlot(global, id) => {
(*id as usize)
< if *global {
self.globals_init.len()
} else {
p.locals_init.len()
}
}
Call(id, argc) => self
.procs
.get(*id as usize)
.is_some_and(|p| p.n_params == *argc as u16),
Jump(pc) | JumpIfFalse(pc) | JumpIfTrue(pc) | Gosub(pc) | RetGosubTo(pc)
| OnErrorLocal(pc) | ResumeLabel(pc) => (*pc as usize) < p.code.len(),
OnErrorGoto(pc) => self
.procs
.first()
.is_some_and(|p| (*pc as usize) < p.code.len()),
OnJump(id, _) => self
.jump_tables
.get(*id as usize)
.is_some_and(|t| t.iter().all(|pc| (*pc as usize) < p.code.len())),
Restore(id) => (*id as usize) <= self.data.len(),
Input(_, _, id, _) => *id == u16::MAX || (*id as usize) < self.strings.len(),
LoadObjectProperty(id, prop, _)
| StoreObjectProperty(id, prop, _)
| LoadObjectIndexedProperty(id, prop)
| StoreObjectIndexedProperty(id, prop) => {
self.objects.get(*id as usize).is_some_and(|o| {
((*prop & 0x7fff) as usize)
< tb_frontend::forms::properties(o.class).len()
})
}
PushObject(id, _) | ObjectLoad(id, _, _) => (*id as usize) < self.objects.len(),
ObjectMethod(id, method, _) | ObjectMethodFn(id, method, _) => {
self.objects.get(*id as usize).is_some_and(|o| {
(*method as usize) < tb_frontend::forms::methods(o.class).len()
})
}
LoadDynamicObjectProperty(name) | StoreDynamicObjectProperty(name) => {
(*name as usize) < self.strings.len()
}
GetPut(_, _, 7, id) => (*id as usize) < self.udts.len(),
GetPut(_, _, kind, _) => *kind <= 8,
TypeOf(class) => ObjectClass::from_id(*class).is_some(),
TrapDefine(kind, pc) => *kind <= 3 && (*pc as usize) < p.code.len(),
TrapDisable(kind) => *kind <= 3,
TrapSet(kind, state) => *kind <= 3 && *state <= 2,
Run(kind) => *kind <= 2,
ReadData(kind) => *kind <= 1,
_ => true,
};
if !valid {
return Err(bad());
}
}
}
Ok(())
}
/// `.tbc`-Container schreiben: Magic, Version, Flags, Abschnittstabelle /// `.tbc`-Container schreiben: Magic, Version, Flags, Abschnittstabelle
/// (Kennung/Offset/Länge), Abschnitte MODN, CONS, TYPS, GLOB, PROC /// (Kennung/Offset/Länge), Abschnitte MODN, SRCS, CONS, TYPS, GLOB, PROC
/// (mit eingebettetem Code und Zeileninfo), DATA, JMPT. /// (mit eingebettetem Code und Quellorten), DATA, JMPT, OBJS.
pub fn to_tbc(&self) -> Vec<u8> { pub fn to_tbc(&self) -> Vec<u8> {
let mut sections: Vec<([u8; 4], Vec<u8>)> = Vec::new(); let mut sections: Vec<([u8; 4], Vec<u8>)> = Vec::new();
@@ -413,6 +746,19 @@ impl CompiledModule {
modn.push(self.option_base); modn.push(self.option_base);
sections.push((*b"MODN", modn)); sections.push((*b"MODN", modn));
let mut srcs = Vec::new();
(self.modules.len() as u32).enc(&mut srcs);
for (name, base) in &self.modules {
w_string(&mut srcs, name);
base.enc(&mut srcs);
}
(self.sources.len() as u32).enc(&mut srcs);
for source in &self.sources {
source.module.enc(&mut srcs);
w_string(&mut srcs, &source.path);
}
sections.push((*b"SRCS", srcs));
let mut cons = Vec::new(); let mut cons = Vec::new();
cons.extend_from_slice(&(self.strings.len() as u32).to_le_bytes()); cons.extend_from_slice(&(self.strings.len() as u32).to_le_bytes());
for s in &self.strings { for s in &self.strings {
@@ -443,6 +789,19 @@ impl CompiledModule {
proc.extend_from_slice(&(self.procs.len() as u32).to_le_bytes()); proc.extend_from_slice(&(self.procs.len() as u32).to_le_bytes());
for p in &self.procs { for p in &self.procs {
w_string(&mut proc, &p.name); w_string(&mut proc, &p.name);
p.module.enc(&mut proc);
(p.kind as u8).enc(&mut proc);
(p.params.len() as u32).enc(&mut proc);
for param in &p.params {
w_string(&mut proc, &param.name);
param.ty.enc(&mut proc);
param.array.enc(&mut proc);
param.by_ref.enc(&mut proc);
}
p.ret_ty.is_some().enc(&mut proc);
if let Some(ty) = &p.ret_ty {
ty.enc(&mut proc);
}
proc.extend_from_slice(&p.n_params.to_le_bytes()); proc.extend_from_slice(&p.n_params.to_le_bytes());
proc.extend_from_slice(&(p.locals_init.len() as u32).to_le_bytes()); proc.extend_from_slice(&(p.locals_init.len() as u32).to_le_bytes());
for (init, name) in p.locals_init.iter().zip(&p.local_names) { for (init, name) in p.locals_init.iter().zip(&p.local_names) {
@@ -484,6 +843,7 @@ impl CompiledModule {
objs.push(o.class.id()); objs.push(o.class.id());
w_string(&mut objs, o.parent_form.as_deref().unwrap_or("")); w_string(&mut objs, o.parent_form.as_deref().unwrap_or(""));
objs.push(o.array as u8); objs.push(o.array as u8);
o.parent.unwrap_or(u16::MAX).enc(&mut objs);
} }
objs.extend_from_slice(&(self.event_procs.len() as u32).to_le_bytes()); objs.extend_from_slice(&(self.event_procs.len() as u32).to_le_bytes());
for e in &self.event_procs { for e in &self.event_procs {
@@ -491,6 +851,17 @@ impl CompiledModule {
w_string(&mut objs, &e.event); w_string(&mut objs, &e.event);
objs.extend_from_slice(&e.proc.to_le_bytes()); objs.extend_from_slice(&e.proc.to_le_bytes());
} }
self.startup_form.unwrap_or(u16::MAX).enc(&mut objs);
(self.form_initial.len() as u32).enc(&mut objs);
for initial in &self.form_initial {
initial.object.enc(&mut objs);
initial.index.enc(&mut objs);
(initial.properties.len() as u32).enc(&mut objs);
for (id, value) in &initial.properties {
id.enc(&mut objs);
value.enc(&mut objs);
}
}
sections.push((*b"OBJS", objs)); sections.push((*b"OBJS", objs));
// Header + Abschnittstabelle // Header + Abschnittstabelle
@@ -527,8 +898,13 @@ impl CompiledModule {
if version != TBC_VERSION { if version != TBC_VERSION {
return Err(LoadError::Version(version)); return Err(LoadError::Version(version));
} }
let _flags = r.u16()?; if r.u16()? != 0 {
return Err(LoadError::Corrupt("Header-Flags"));
}
let n_sections = r.u32()? as usize; let n_sections = r.u32()? as usize;
if n_sections != 9 {
return Err(LoadError::Corrupt("Abschnittsanzahl"));
}
let mut table = Vec::new(); let mut table = Vec::new();
for _ in 0..n_sections { for _ in 0..n_sections {
let id: [u8; 4] = r.take(4)?.try_into().unwrap(); let id: [u8; 4] = r.take(4)?.try_into().unwrap();
@@ -536,6 +912,27 @@ impl CompiledModule {
let len = r.u32()? as usize; let len = r.u32()? as usize;
table.push((id, off, len)); table.push((id, off, len));
} }
let expected = [
*b"MODN", *b"SRCS", *b"CONS", *b"TYPS", *b"GLOB", *b"PROC", *b"DATA", *b"JMPT",
*b"OBJS",
];
let mut seen = std::collections::HashSet::new();
let mut ranges = Vec::new();
for (id, off, len) in &table {
if !expected.contains(id)
|| !seen.insert(*id)
|| *off < r.pos
|| *off > buf.len()
|| *len > buf.len() - *off
{
return Err(LoadError::Corrupt("Abschnittstabelle"));
}
ranges.push((*off, off + len));
}
ranges.sort_unstable();
if ranges.windows(2).any(|pair| pair[0].1 > pair[1].0) {
return Err(LoadError::Corrupt("überlappende Abschnitte"));
}
let section = |id: &[u8; 4]| -> Result<Reader, LoadError> { let section = |id: &[u8; 4]| -> Result<Reader, LoadError> {
for (sid, off, len) in &table { for (sid, off, len) in &table {
if sid == id { if sid == id {
@@ -552,44 +949,86 @@ impl CompiledModule {
let name = r.string()?; let name = r.string()?;
let option_base = r.u8()?; let option_base = r.u8()?;
r.finish()?;
let mut r = section(b"SRCS")?;
let n = r.u32()?;
let mut modules = Vec::new();
for _ in 0..n {
modules.push((r.string()?, r.u8()?));
}
let n = r.u32()?;
let mut sources = Vec::new();
for _ in 0..n {
sources.push(SourceFile {
module: r.u16()?,
path: r.string()?,
});
}
r.finish()?;
let mut r = section(b"CONS")?; let mut r = section(b"CONS")?;
let n = r.u32()? as usize; let n = r.u32()? as usize;
let mut strings = Vec::with_capacity(n); let mut strings = Vec::new();
for _ in 0..n { for _ in 0..n {
strings.push(Rc::from(r.string()?.as_str())); strings.push(Rc::from(r.string()?.as_str()));
} }
r.finish()?;
let mut r = section(b"TYPS")?; let mut r = section(b"TYPS")?;
let n = r.u32()? as usize; let n = r.u32()? as usize;
let mut udts = Vec::with_capacity(n); let mut udts = Vec::new();
for _ in 0..n { for _ in 0..n {
let name = r.string()?; let name = r.string()?;
let nf = r.u32()? as usize; let nf = r.u32()? as usize;
let mut fields = Vec::with_capacity(nf); let mut fields = Vec::new();
for _ in 0..nf { for _ in 0..nf {
fields.push(TypeInit::dec(&mut r)?); fields.push(TypeInit::dec(&mut r)?);
} }
udts.push(UdtLayout { name, fields }); udts.push(UdtLayout { name, fields });
} }
r.finish()?;
let mut r = section(b"GLOB")?; let mut r = section(b"GLOB")?;
let n = r.u32()? as usize; let n = r.u32()? as usize;
let mut globals_init = Vec::with_capacity(n); let mut globals_init = Vec::new();
let mut global_names = Vec::with_capacity(n); let mut global_names = Vec::new();
for _ in 0..n { for _ in 0..n {
globals_init.push(TypeInit::dec(&mut r)?); globals_init.push(TypeInit::dec(&mut r)?);
global_names.push(r.string()?); global_names.push(r.string()?);
} }
r.finish()?;
let mut r = section(b"PROC")?; let mut r = section(b"PROC")?;
let n = r.u32()? as usize; let n = r.u32()? as usize;
let mut procs = Vec::with_capacity(n); let mut procs = Vec::new();
for _ in 0..n { for _ in 0..n {
let name = r.string()?; let name = r.string()?;
let module = r.u16()?;
let kind = match r.u8()? {
0 => HProcKind::Main,
1 => HProcKind::Sub,
2 => HProcKind::Function,
3 => HProcKind::DefFn,
_ => return Err(LoadError::Corrupt("Prozedurart")),
};
let np = r.u32()?;
let mut params = Vec::new();
for _ in 0..np {
params.push(HParam {
name: r.string()?,
ty: HTy::dec(&mut r)?,
array: bool::dec(&mut r)?,
by_ref: bool::dec(&mut r)?,
});
}
let ret_ty = if bool::dec(&mut r)? {
Some(HTy::dec(&mut r)?)
} else {
None
};
let n_params = r.u16()?; let n_params = r.u16()?;
let nl = r.u32()? as usize; let nl = r.u32()? as usize;
let mut locals_init = Vec::with_capacity(nl); let mut locals_init = Vec::new();
let mut local_names = Vec::with_capacity(nl); let mut local_names = Vec::new();
for _ in 0..nl { for _ in 0..nl {
locals_init.push(TypeInit::dec(&mut r)?); locals_init.push(TypeInit::dec(&mut r)?);
local_names.push(r.string()?); local_names.push(r.string()?);
@@ -598,11 +1037,16 @@ impl CompiledModule {
let code_len = r.u32()? as usize; let code_len = r.u32()? as usize;
let code_bytes = r.take(code_len)?; let code_bytes = r.take(code_len)?;
let mut cr = Reader::new(code_bytes); let mut cr = Reader::new(code_bytes);
let mut code = Vec::with_capacity(n_instr); let mut code = Vec::new();
for _ in 0..n_instr { for _ in 0..n_instr {
code.push(Instr::decode(&mut cr)?); code.push(Instr::decode(&mut cr)?);
} }
cr.finish()?;
procs.push(ProcCode { procs.push(ProcCode {
module,
kind,
params,
ret_ty,
name, name,
n_params, n_params,
locals_init, locals_init,
@@ -611,44 +1055,49 @@ impl CompiledModule {
}); });
} }
r.finish()?;
let mut r = section(b"DATA")?; let mut r = section(b"DATA")?;
let n = r.u32()? as usize; let n = r.u32()? as usize;
let mut data = Vec::with_capacity(n); let mut data = Vec::new();
for _ in 0..n { for _ in 0..n {
let text = r.string()?; let text = r.string()?;
let line = r.u32()?; let line = r.u32()?;
data.push(DataItem { text, line }); data.push(DataItem { text, line });
} }
r.finish()?;
let mut r = section(b"JMPT")?; let mut r = section(b"JMPT")?;
let n = r.u32()? as usize; let n = r.u32()? as usize;
let mut jump_tables = Vec::with_capacity(n); let mut jump_tables = Vec::new();
for _ in 0..n { for _ in 0..n {
let m = r.u32()? as usize; let m = r.u32()? as usize;
let mut t = Vec::with_capacity(m); let mut t = Vec::new();
for _ in 0..m { for _ in 0..m {
t.push(r.u32()?); t.push(r.u32()?);
} }
jump_tables.push(t); jump_tables.push(t);
} }
r.finish()?;
let mut r = section(b"OBJS")?; let mut r = section(b"OBJS")?;
let n = r.u32()? as usize; let n = r.u32()? as usize;
let mut objects = Vec::with_capacity(n); let mut objects = Vec::new();
for _ in 0..n { for _ in 0..n {
let name = r.string()?; let name = r.string()?;
let class = ObjectClass::from_id(r.u8()?).ok_or(LoadError::Corrupt("Objektklasse"))?; let class = ObjectClass::from_id(r.u8()?).ok_or(LoadError::Corrupt("Objektklasse"))?;
let parent = r.string()?; let parent = r.string()?;
let array = r.u8()? != 0; let array = bool::dec(&mut r)?;
let parent_id = r.u16()?;
objects.push(FormObject { objects.push(FormObject {
name, name,
class, class,
parent_form: (!parent.is_empty()).then_some(parent), parent_form: (!parent.is_empty()).then_some(parent),
parent: (parent_id != u16::MAX).then_some(parent_id),
array, array,
}); });
} }
let n = r.u32()? as usize; let n = r.u32()? as usize;
let mut event_procs = Vec::with_capacity(n); let mut event_procs = Vec::new();
for _ in 0..n { for _ in 0..n {
event_procs.push(HEventProc { event_procs.push(HEventProc {
object: r.u16()?, object: r.u16()?,
@@ -657,7 +1106,35 @@ impl CompiledModule {
}); });
} }
Ok(CompiledModule { let startup = r.u16()?;
let startup_form = (startup != u16::MAX).then_some(startup);
let n = r.u32()?;
let mut form_initial = Vec::new();
for _ in 0..n {
let object = r.u16()?;
let index = i32::dec(&mut r)?;
let n = r.u32()?;
let mut properties = std::collections::BTreeMap::new();
for _ in 0..n {
if properties
.insert(r.u16()?, PropertyValue::dec(&mut r)?)
.is_some()
{
return Err(LoadError::Corrupt("doppelte Anfangseigenschaft"));
}
}
form_initial.push(FormInitial {
object,
index,
properties,
});
}
r.finish()?;
let module = CompiledModule {
modules,
sources,
form_initial,
startup_form,
name, name,
option_base, option_base,
strings, strings,
@@ -669,7 +1146,9 @@ impl CompiledModule {
jump_tables, jump_tables,
objects, objects,
event_procs, event_procs,
}) };
module.validate()?;
Ok(module)
} }
} }
@@ -712,6 +1191,13 @@ mod tests {
#[test] #[test]
fn tbc_roundtrip() { fn tbc_roundtrip() {
let m = CompiledModule { let m = CompiledModule {
modules: vec![("TEST".into(), 1)],
sources: vec![SourceFile {
module: 0,
path: "test.bas".into(),
}],
form_initial: vec![],
startup_form: None,
name: "TEST".into(), name: "TEST".into(),
option_base: 1, option_base: 1,
strings: vec![Rc::from("Hallo"), Rc::from("Welt")], strings: vec![Rc::from("Hallo"), Rc::from("Welt")],
@@ -722,6 +1208,10 @@ mod tests {
fields: vec![TypeInit::FixedStr(30), TypeInit::Dbl], fields: vec![TypeInit::FixedStr(30), TypeInit::Dbl],
}], }],
procs: vec![ProcCode { procs: vec![ProcCode {
module: 0,
kind: HProcKind::Main,
params: vec![],
ret_ty: None,
name: "TEST".into(), name: "TEST".into(),
n_params: 0, n_params: 0,
locals_init: vec![], locals_init: vec![],
@@ -751,6 +1241,13 @@ mod tests {
#[test] #[test]
fn unbekannte_version_wird_abgelehnt() { fn unbekannte_version_wird_abgelehnt() {
let m = CompiledModule { let m = CompiledModule {
modules: vec![("TEST".into(), 0)],
sources: vec![SourceFile {
module: 0,
path: "test.bas".into(),
}],
form_initial: vec![],
startup_form: None,
name: "T".into(), name: "T".into(),
option_base: 0, option_base: 0,
strings: vec![], strings: vec![],

View File

@@ -16,6 +16,12 @@ use tb_runtime::value::TypeInit;
pub fn compile(hir: &HirModule) -> CompiledModule { pub fn compile(hir: &HirModule) -> CompiledModule {
let mut cg = Codegen { let mut cg = Codegen {
common_arrays: hir
.commons
.iter()
.filter(|c| c.dims.is_some())
.map(|c| c.slot)
.collect(),
strings: Vec::new(), strings: Vec::new(),
string_ids: HashMap::new(), string_ids: HashMap::new(),
jump_tables: Vec::new(), jump_tables: Vec::new(),
@@ -30,6 +36,13 @@ pub fn compile(hir: &HirModule) -> CompiledModule {
procs.push(cg.compile_proc(proc)); procs.push(cg.compile_proc(proc));
} }
CompiledModule { CompiledModule {
modules: vec![(hir.name.clone(), hir.option_base)],
sources: vec![tb_frontend::source::SourceFile {
module: 0,
path: hir.name.clone(),
}],
form_initial: vec![],
startup_form: None,
name: hir.name.clone(), name: hir.name.clone(),
option_base: hir.option_base, option_base: hir.option_base,
strings: cg.strings, strings: cg.strings,
@@ -67,7 +80,7 @@ fn slot_init(v: &hir::HVar) -> TypeInit {
} }
} }
fn type_init(t: &HTy) -> TypeInit { pub(crate) fn type_init(t: &HTy) -> TypeInit {
match t { match t {
HTy::Num(NumTy::Int) => TypeInit::Int, HTy::Num(NumTy::Int) => TypeInit::Int,
HTy::Num(NumTy::Lng) => TypeInit::Lng, HTy::Num(NumTy::Lng) => TypeInit::Lng,
@@ -82,6 +95,7 @@ fn type_init(t: &HTy) -> TypeInit {
} }
struct Codegen { struct Codegen {
common_arrays: std::collections::HashSet<u16>,
strings: Vec<Rc<str>>, strings: Vec<Rc<str>>,
string_ids: HashMap<String, u16>, string_ids: HashMap<String, u16>,
jump_tables: Vec<Vec<u32>>, jump_tables: Vec<Vec<u32>>,
@@ -95,6 +109,7 @@ struct Codegen {
} }
struct ProcCtx { struct ProcCtx {
pos: tb_frontend::SourcePos,
code: Vec<Instr>, code: Vec<Instr>,
/// LabelId → Instruktionsindex. /// LabelId → Instruktionsindex.
label_pc: Vec<Option<u32>>, label_pc: Vec<Option<u32>>,
@@ -134,6 +149,10 @@ impl ProcCtx {
self.label_pc[label as usize] = Some(pc); self.label_pc[label as usize] = Some(pc);
} }
fn emit(&mut self, i: Instr) { fn emit(&mut self, i: Instr) {
if matches!(i, Instr::Stmt(_) | Instr::InitStmt(_)) {
self.code
.push(Instr::Source(self.pos.source, self.pos.column));
}
self.code.push(i); self.code.push(i);
} }
/// Sprunginstruktion mit noch unbekanntem Ziel emittieren. /// Sprunginstruktion mit noch unbekanntem Ziel emittieren.
@@ -162,6 +181,7 @@ impl Codegen {
fn compile_proc(&mut self, proc: &hir::HProc) -> ProcCode { fn compile_proc(&mut self, proc: &hir::HProc) -> ProcCode {
let mut ctx = ProcCtx { let mut ctx = ProcCtx {
pos: tb_frontend::SourcePos::default(),
code: Vec::new(), code: Vec::new(),
label_pc: vec![None; proc.label_count as usize], label_pc: vec![None; proc.label_count as usize],
fixups: Vec::new(), fixups: Vec::new(),
@@ -238,6 +258,10 @@ impl Codegen {
} }
ProcCode { ProcCode {
module: 0,
kind: proc.kind,
params: proc.params.clone(),
ret_ty: proc.ret_ty.clone(),
name: proc.name.clone(), name: proc.name.clone(),
n_params: proc.params.len() as u16, n_params: proc.params.len() as u16,
locals_init: proc.locals.iter().map(slot_init).collect(), locals_init: proc.locals.iter().map(slot_init).collect(),
@@ -268,13 +292,14 @@ impl Codegen {
} }
fn stmt_inner(&mut self, ctx: &mut ProcCtx, proc: &hir::HProc, stmt: &HStmt, boundary: bool) { fn stmt_inner(&mut self, ctx: &mut ProcCtx, proc: &hir::HProc, stmt: &HStmt, boundary: bool) {
ctx.pos = stmt.pos;
match &stmt.kind { match &stmt.kind {
HStmtKind::Label(l) => { HStmtKind::Label(l) => {
ctx.bind(*l); ctx.bind(*l);
return; return;
} }
_ if boundary => ctx.emit(Instr::Stmt(stmt.line)), _ if boundary => ctx.emit(Instr::Stmt(stmt.line)),
_ => {} _ => ctx.emit(Instr::InitStmt(stmt.line)),
} }
match &stmt.kind { match &stmt.kind {
HStmtKind::Label(_) => unreachable!(), HStmtKind::Label(_) => unreachable!(),
@@ -478,6 +503,7 @@ impl Codegen {
} }
} }
HStmtKind::Loop { HStmtKind::Loop {
end_pos,
pre, pre,
post, post,
body, body,
@@ -496,6 +522,8 @@ impl Codegen {
for s in body { for s in body {
self.stmt(ctx, proc, s); self.stmt(ctx, proc, s);
} }
ctx.pos = *end_pos;
ctx.emit(Instr::Stmt(end_pos.line));
match post { match post {
Some((is_until, cond)) => { Some((is_until, cond)) => {
self.expr(ctx, cond); self.expr(ctx, cond);
@@ -511,6 +539,7 @@ impl Codegen {
ctx.bind(*exit_label); ctx.bind(*exit_label);
} }
HStmtKind::For { HStmtKind::For {
end_pos,
var, var,
ty, ty,
from, from,
@@ -533,7 +562,7 @@ impl Codegen {
*step_slot, *step_slot,
body, body,
*exit_label, *exit_label,
stmt.line, *end_pos,
); );
} }
HStmtKind::Goto(l) => ctx.emit_jump(Instr::Jump(0), *l), HStmtKind::Goto(l) => ctx.emit_jump(Instr::Jump(0), *l),
@@ -663,6 +692,8 @@ impl Codegen {
let init = type_init(elem); let init = type_init(elem);
if *redim { if *redim {
ctx.emit(Instr::RedimArr(global, s, dims.len() as u8, init)); ctx.emit(Instr::RedimArr(global, s, dims.len() as u8, init));
} else if global && self.common_arrays.contains(&s) {
ctx.emit(Instr::CommonArr(global, s, dims.len() as u8, init));
} else { } else {
ctx.emit(Instr::DimArr(global, s, dims.len() as u8, init)); ctx.emit(Instr::DimArr(global, s, dims.len() as u8, init));
} }
@@ -697,7 +728,7 @@ impl Codegen {
step_slot: Option<VarSlot>, step_slot: Option<VarSlot>,
body: &[HStmt], body: &[HStmt],
exit_label: u16, exit_label: u16,
line: u32, end_pos: tb_frontend::SourcePos,
) { ) {
// Startwert, Grenze, ggf. Schritt einmal auswerten. // Startwert, Grenze, ggf. Schritt einmal auswerten.
self.store_place(ctx, var, |cg, ctx| cg.expr(ctx, from)); self.store_place(ctx, var, |cg, ctx| cg.expr(ctx, from));
@@ -715,15 +746,6 @@ impl Codegen {
let l_test = ctx.new_label(); let l_test = ctx.new_label();
let l_body = ctx.new_label(); let l_body = ctx.new_label();
ctx.bind(l_test); ctx.bind(l_test);
// Anweisungsgrenze für den Rücksprung von `NEXT`. Sie kann nicht
// auf die Grenze des `FOR` zeigen — dort stünde die Initialisierung
// noch einmal.
//
// ponytail: gemeldet wird die Zeile des `FOR`, nicht die des
// `NEXT` — die trägt das HIR nicht. Ceiling: braucht der Debugger
// in Phase 5 die genaue Zeile, bekommt `HStmtKind::For` ein Feld
// `next_line`.
ctx.emit(Instr::Stmt(line));
match const_step { match const_step {
Some(s) => { Some(s) => {
// Vergleichsrichtung zur Compilezeit. // Vergleichsrichtung zur Compilezeit.
@@ -757,7 +779,9 @@ impl Codegen {
for s in body { for s in body {
self.stmt(ctx, proc, s); self.stmt(ctx, proc, s);
} }
// NEXT: inkrementieren, zurück zum Test. // NEXT: eigene Quellgrenze für Fehler, RESUME und Debugger.
ctx.pos = end_pos;
ctx.emit(Instr::Stmt(end_pos.line));
self.store_place(ctx, var, |cg, ctx| { self.store_place(ctx, var, |cg, ctx| {
cg.load_place(ctx, var); cg.load_place(ctx, var);
match (step, step_slot) { match (step, step_slot) {
@@ -777,6 +801,7 @@ impl Codegen {
match e { match e {
HExpr::Int(v) => ctx.emit(Instr::PushInt(*v)), HExpr::Int(v) => ctx.emit(Instr::PushInt(*v)),
HExpr::Lng(v) => ctx.emit(Instr::PushLng(*v)), HExpr::Lng(v) => ctx.emit(Instr::PushLng(*v)),
HExpr::UdtId(id) => ctx.emit(Instr::PushUdtId(*id)),
HExpr::Sng(v) => ctx.emit(Instr::PushSng(*v)), HExpr::Sng(v) => ctx.emit(Instr::PushSng(*v)),
HExpr::Dbl(v) => ctx.emit(Instr::PushDbl(*v)), HExpr::Dbl(v) => ctx.emit(Instr::PushDbl(*v)),
HExpr::Cur(v) => ctx.emit(Instr::PushCur(*v)), HExpr::Cur(v) => ctx.emit(Instr::PushCur(*v)),

View File

@@ -70,6 +70,8 @@ struct Frame {
/// Instruktionsindex der zuletzt begonnenen Anweisung (`Stmt`). /// Instruktionsindex der zuletzt begonnenen Anweisung (`Stmt`).
last_stmt_pc: usize, last_stmt_pc: usize,
line: u32, line: u32,
source: u32,
column: u32,
/// Gesetzt, wenn dieser Frame der Handler eines Ereignis-Traps ist. /// Gesetzt, wenn dieser Frame der Handler eines Ereignis-Traps ist.
/// Er läuft im Modulrumpf und **teilt dessen Locals** — ein eigener /// Er läuft im Modulrumpf und **teilt dessen Locals** — ein eigener
/// Satz würde dem Handler leere Modulvariablen zeigen. Sein `RETURN` /// Satz würde dem Handler leere Modulvariablen zeigen. Sein `RETURN`
@@ -117,14 +119,14 @@ pub struct Vm {
erl: u32, erl: u32,
/// Zuletzt durchlaufene numerische Zeilennummer (0 = keine). /// Zuletzt durchlaufene numerische Zeilennummer (0 = keine).
zeile_nr: u32, zeile_nr: u32,
module_handler: Handler, module_handlers: Vec<Handler>,
in_handler: bool, in_handler: bool,
resume_pc: usize, resume_pc: usize,
// Steuerung // Steuerung
flags: u32, flags: u32,
/// Zählt Anweisungsgrenzen für die regelmäßige Ereignisabholung. /// Zählt Anweisungsgrenzen für die regelmäßige Ereignisabholung.
tick_zaehler: u32, tick_zaehler: u32,
breakpoints: HashSet<u32>, breakpoints: HashSet<(u16, u32)>,
data_ptr: usize, data_ptr: usize,
start_pc: Option<usize>, start_pc: Option<usize>,
pub forms: FormsModel, pub forms: FormsModel,
@@ -170,7 +172,15 @@ impl Vm {
.iter() .iter()
.map(|t| default_value(t, &module.udts)) .map(|t| default_value(t, &module.udts))
.collect(); .collect();
let forms = FormsModel::new(module.objects.clone(), 80, 25); let mut forms = FormsModel::new(module.objects.clone(), 80, 25);
for initial in &module.form_initial {
initial
.apply(&mut forms)
.expect("validierte Forms-Anfangsdaten");
}
if let Some(form) = module.startup_form {
forms.show(form, false).expect("validiertes Startformular");
}
let mut vm = Vm { let mut vm = Vm {
globals, globals,
locals: Vec::new(), locals: Vec::new(),
@@ -180,7 +190,7 @@ impl Vm {
err: 0, err: 0,
erl: 0, erl: 0,
zeile_nr: 0, zeile_nr: 0,
module_handler: Handler::None, module_handlers: vec![Handler::None; module.modules.len()],
in_handler: false, in_handler: false,
resume_pc: 0, resume_pc: 0,
flags: 0, flags: 0,
@@ -202,7 +212,12 @@ impl Vm {
let target = self.module.procs[0] let target = self.module.procs[0]
.code .code
.iter() .iter()
.position(|instruction| matches!(instruction, Instr::Stmt(found) if *found == line)); .position(|instruction| matches!(instruction, Instr::SetErl(found) if *found == line))
.or_else(|| {
self.module.procs[0].code.iter().position(
|instruction| matches!(instruction, Instr::Stmt(found) if *found == line),
)
});
let target = target.ok_or(RuntimeError(8))?; let target = target.ok_or(RuntimeError(8))?;
if self.module.procs[0] if self.module.procs[0]
.code .code
@@ -229,6 +244,8 @@ impl Vm {
} }
let stack_base = self.stack.len(); let stack_base = self.stack.len();
self.frames.push(Frame { self.frames.push(Frame {
source: 0,
column: 0,
proc, proc,
pc: 0, pc: 0,
locals_base, locals_base,
@@ -361,7 +378,12 @@ impl Vm {
if !matches!(self.module.procs[proc].code.as_slice(), [Instr::RetProc]) { if !matches!(self.module.procs[proc].code.as_slice(), [Instr::RetProc]) {
return Ok(false); return Ok(false);
} }
let name = self.module.procs[proc].name.to_ascii_uppercase(); let name = self.module.procs[proc]
.name
.rsplit('!')
.next()
.unwrap()
.to_ascii_uppercase();
if !matches!( if !matches!(
name.as_str(), name.as_str(),
"CMNDLGREGISTER" "CMNDLGREGISTER"
@@ -767,6 +789,8 @@ impl Vm {
let locals_base = self.frames[0].locals_base; let locals_base = self.frames[0].locals_base;
let stack_base = self.stack.len(); let stack_base = self.stack.len();
self.frames.push(Frame { self.frames.push(Frame {
source: 0,
column: 0,
proc: 0, proc: 0,
pc: ziel as usize, pc: ziel as usize,
locals_base, locals_base,
@@ -821,17 +845,54 @@ impl Vm {
} }
pub fn add_breakpoint(&mut self, line: u32) { pub fn add_breakpoint(&mut self, line: u32) {
self.breakpoints.insert(line); self.breakpoints.insert((0, line));
self.flags |= F_BREAK; self.flags |= F_BREAK;
} }
pub fn remove_breakpoint(&mut self, line: u32) { pub fn remove_breakpoint(&mut self, line: u32) {
self.breakpoints.remove(&line); self.breakpoints.remove(&(0, line));
if self.breakpoints.is_empty() { if self.breakpoints.is_empty() {
self.flags &= !F_BREAK; self.flags &= !F_BREAK;
} }
} }
pub fn add_module_breakpoint(&mut self, module: u16, line: u32) {
self.breakpoints.insert((module, line));
self.flags |= F_BREAK;
}
pub fn remove_module_breakpoint(&mut self, module: u16, line: u32) {
self.breakpoints.remove(&(module, line));
if self.breakpoints.is_empty() {
self.flags &= !F_BREAK;
}
}
pub fn current_source_pos(&self) -> tb_frontend::SourcePos {
self.frames
.last()
.map(|f| tb_frontend::SourcePos {
source: f.source,
line: f.line,
column: f.column,
})
.unwrap_or_default()
}
pub fn current_module(&self) -> u16 {
self.module
.sources
.get(self.current_source_pos().source as usize)
.map_or(0, |s| s.module)
}
pub fn current_file(&self) -> &str {
self.module
.sources
.get(self.current_source_pos().source as usize)
.map_or(&self.module.name, |s| &s.path)
}
pub fn current_line(&self) -> u32 { pub fn current_line(&self) -> u32 {
self.frames.last().map(|f| f.line).unwrap_or(0) self.frames.last().map(|f| f.line).unwrap_or(0)
} }
@@ -843,26 +904,58 @@ impl Vm {
.unwrap_or("") .unwrap_or("")
} }
/// Variableninspektion (Debugger): erst Locals des obersten Frames /// Variableninspektion: lokale Namen vor Modulvariablen, explizite
/// (Referenzen werden aufgelöst), dann Modulvariablen. Ein /// Typ-Suffixe vor einer eindeutigen Suche nach dem Basisnamen.
/// Typ-Suffix (`n%`, `s$`) wird toleriert — Slots tragen Basisnamen.
pub fn inspect(&self, name: &str) -> Option<Value> { pub fn inspect(&self, name: &str) -> Option<Value> {
let name = name.trim_end_matches(['%', '&', '!', '#', '$', '@']); fn matches(stored: &str, requested: &str) -> bool {
let suffixes = ['%', '&', '!', '#', '$', '@'];
stored.eq_ignore_ascii_case(requested)
|| (!(stored.ends_with(suffixes) && requested.ends_with(suffixes))
&& stored
.trim_end_matches(suffixes)
.eq_ignore_ascii_case(requested.trim_end_matches(suffixes)))
}
fn unique(mut ids: impl Iterator<Item = usize>) -> Option<usize> {
let first = ids.next()?;
ids.next().is_none().then_some(first)
}
if let Some(f) = self.frames.last() { if let Some(f) = self.frames.last() {
let p = &self.module.procs[f.proc]; let p = &self.module.procs[f.proc];
for (i, n) in p.local_names.iter().enumerate() { if let Some(i) = unique(
if n.eq_ignore_ascii_case(name) { p.local_names
let v = self.locals[f.locals_base + i].clone(); .iter()
return Some(self.deref_for_inspect(v)); .enumerate()
} .filter(|(_, n)| matches(n, name))
.map(|(i, _)| i),
) {
return Some(self.deref_for_inspect(self.locals[f.locals_base + i].clone()));
} }
} }
for (i, n) in self.module.global_names.iter().enumerate() { let qualified = format!(
if n.eq_ignore_ascii_case(name) { "{}!{name}",
return Some(self.deref_for_inspect(self.globals[i].clone())); self.module.modules[self.current_module() as usize].0
} );
if let Some(id) = unique(
self.module
.global_names
.iter()
.enumerate()
.filter(|(_, n)| matches(n, name) || matches(n, &qualified))
.map(|(i, _)| i),
) {
return Some(self.globals[id].clone());
} }
None let id = unique(
self.module
.global_names
.iter()
.enumerate()
.filter(|(_, n)| {
matches(n.split_once('!').map_or(n.as_str(), |(_, name)| name), name)
})
.map(|(i, _)| i),
)?;
Some(self.globals[id].clone())
} }
/// Arrayelement inspizieren. /// Arrayelement inspizieren.
@@ -993,8 +1086,15 @@ impl Vm {
break; break;
} }
} }
if target.is_none() && self.module_handler != Handler::None { if target.is_none() {
target = Some((0, self.module_handler)); for frame in self.frames.iter().rev() {
let module = self.module.sources[frame.source as usize].module as usize;
let handler = self.module_handlers[module];
if handler != Handler::None {
target = Some((0, handler));
break;
}
}
} }
let Some((depth, handler)) = target else { let Some((depth, handler)) = target else {
return Some(self.error_event(code, line)); return Some(self.error_event(code, line));
@@ -1039,7 +1139,7 @@ impl Vm {
let code = &self.module.procs[proc].code; let code = &self.module.procs[proc].code;
let mut i = from + 1; let mut i = from + 1;
while i < code.len() { while i < code.len() {
if matches!(code[i], Instr::Stmt(_)) { if matches!(code[i], Instr::Stmt(_) | Instr::InitStmt(_)) {
return i; return i;
} }
i += 1; i += 1;
@@ -1189,7 +1289,7 @@ impl Vm {
match cell { match cell {
Value::Arr(a) => Ok(a.clone()), Value::Arr(a) => Ok(a.clone()),
Value::Empty => { Value::Empty => {
let lo = self.module.option_base as i32; let lo = self.module.modules[self.current_module() as usize].1 as i32;
let bounds = vec![(lo, 10); dims as usize]; let bounds = vec![(lo, 10); dims as usize];
let arr = ArrayObj::new(elem.clone(), bounds, &self.module.udts)?; let arr = ArrayObj::new(elem.clone(), bounds, &self.module.udts)?;
let handle = Rc::new(std::cell::RefCell::new(arr)); let handle = Rc::new(std::cell::RefCell::new(arr));
@@ -1223,7 +1323,9 @@ impl Vm {
fn exec(&mut self, instr: Instr, pc: usize, host: &mut dyn Host) -> Result<Flow, RuntimeError> { fn exec(&mut self, instr: Instr, pc: usize, host: &mut dyn Host) -> Result<Flow, RuntimeError> {
use Instr as I; use Instr as I;
match instr { match instr {
I::Stmt(line) => { I::Source(_, _) => Ok(Flow::Normal),
I::Stmt(line) | I::InitStmt(line) => {
let initializing = matches!(instr, I::InitStmt(_));
if line == 0 { if line == 0 {
if let Some(target) = self.start_pc.take() { if let Some(target) = self.start_pc.take() {
self.frames[0].pc = target; self.frames[0].pc = target;
@@ -1231,6 +1333,13 @@ impl Vm {
} }
let f = self.frames.last_mut().unwrap(); let f = self.frames.last_mut().unwrap();
f.line = line; f.line = line;
if let Some(I::Source(source, column)) = pc
.checked_sub(1)
.and_then(|pc| self.module.procs[f.proc].code.get(pc))
{
f.source = *source;
f.column = *column;
}
f.last_stmt_pc = pc; f.last_stmt_pc = pc;
// Zustellpunkt: anzeigen, wenn sich der Bildschirm geändert // Zustellpunkt: anzeigen, wenn sich der Bildschirm geändert
// hat, und regelmäßig Ereignisse abholen. Das ist keine // hat, und regelmäßig Ereignisse abholen. Das ist keine
@@ -1238,20 +1347,27 @@ impl Vm {
// Größenänderungen kämen nie an. // Größenänderungen kämen nie an.
self.tick_zaehler = self.tick_zaehler.wrapping_add(1); self.tick_zaehler = self.tick_zaehler.wrapping_add(1);
self.forms.render(&mut self.rt.screen); self.forms.render(&mut self.rt.screen);
if self.rt.screen.ist_veraendert() || self.tick_zaehler.is_multiple_of(1024) { if !initializing
&& (self.rt.screen.ist_veraendert() || self.tick_zaehler.is_multiple_of(1024))
{
self.tick(host); self.tick(host);
self.rt.screen.veraenderung_quittieren(); self.rt.screen.veraenderung_quittieren();
} }
let erster_eintritt = let erster_eintritt =
std::mem::take(&mut self.frames.last_mut().unwrap().handler_start); std::mem::take(&mut self.frames.last_mut().unwrap().handler_start);
if !erster_eintritt && self.zustellen(host, Zustellpunkt::Anweisung) { if !initializing
&& !erster_eintritt
&& self.zustellen(host, Zustellpunkt::Anweisung)
{
return Ok(Flow::Normal); return Ok(Flow::Normal);
} }
if self.flags != 0 { if self.flags != 0 {
if self.flags & F_STEP != 0 { if self.flags & F_STEP != 0 {
return Ok(Flow::Event(RunEvent::Stepped { line })); return Ok(Flow::Event(RunEvent::Stepped { line }));
} }
if self.flags & F_BREAK != 0 && self.breakpoints.contains(&line) { if self.flags & F_BREAK != 0
&& self.breakpoints.contains(&(self.current_module(), line))
{
return Ok(Flow::Event(RunEvent::Breakpoint { line })); return Ok(Flow::Event(RunEvent::Breakpoint { line }));
} }
if self.flags & F_POLL != 0 && self.rt.abbruch { if self.flags & F_POLL != 0 && self.rt.abbruch {
@@ -1328,6 +1444,10 @@ impl Vm {
self.push(Value::Lng(v)); self.push(Value::Lng(v));
Ok(Flow::Normal) Ok(Flow::Normal)
} }
I::PushUdtId(id) => {
self.push(Value::Lng(id as i32));
Ok(Flow::Normal)
}
I::PushSng(v) => { I::PushSng(v) => {
self.push(Value::Sng(v)); self.push(Value::Sng(v));
Ok(Flow::Normal) Ok(Flow::Normal)
@@ -1716,13 +1836,25 @@ impl Vm {
a.data[flat as usize] = v; a.data[flat as usize] = v;
Ok(Flow::Normal) Ok(Flow::Normal)
} }
I::DimArr(global, slot, dims, elem) => { I::DimArr(global, slot, dims, ref elem)
| I::CommonArr(global, slot, dims, ref elem) => {
let common = matches!(instr, I::CommonArr(..));
let bounds = self.pop_bounds(dims)?; let bounds = self.pop_bounds(dims)?;
let cell = self.slot_value(global, slot); let cell = self.slot_value(global, slot);
if common {
if let Value::Arr(array) = cell {
let array = array.borrow();
return if &array.elem == elem && array.dims == bounds {
Ok(Flow::Normal)
} else {
Err(RuntimeError::TYPE_MISMATCH)
};
}
}
if !matches!(cell, Value::Empty) { if !matches!(cell, Value::Empty) {
return Err(RuntimeError::DUPLICATE_DEFINITION); return Err(RuntimeError::DUPLICATE_DEFINITION);
} }
let arr = ArrayObj::new(elem, bounds, &self.module.udts)?; let arr = ArrayObj::new(elem.clone(), bounds, &self.module.udts)?;
*self.slot_value(global, slot) = Value::Arr(Rc::new(std::cell::RefCell::new(arr))); *self.slot_value(global, slot) = Value::Arr(Rc::new(std::cell::RefCell::new(arr)));
Ok(Flow::Normal) Ok(Flow::Normal)
} }
@@ -2293,7 +2425,8 @@ impl Vm {
// ---- Fehlerbehandlung ---- // ---- Fehlerbehandlung ----
I::OnErrorGoto(t) => { I::OnErrorGoto(t) => {
self.module_handler = Handler::Goto(t); let module = self.current_module() as usize;
self.module_handlers[module] = Handler::Goto(t);
Ok(Flow::Normal) Ok(Flow::Normal)
} }
I::OnErrorLocal(t) => { I::OnErrorLocal(t) => {
@@ -2301,7 +2434,8 @@ impl Vm {
Ok(Flow::Normal) Ok(Flow::Normal)
} }
I::OnErrorDisable => { I::OnErrorDisable => {
self.module_handler = Handler::None; let module = self.current_module() as usize;
self.module_handlers[module] = Handler::None;
Ok(Flow::Normal) Ok(Flow::Normal)
} }
I::OnErrorLocalDisable => { I::OnErrorLocalDisable => {
@@ -2312,7 +2446,8 @@ impl Vm {
if local { if local {
self.frames.last_mut().unwrap().local_handler = Handler::ResumeNext; self.frames.last_mut().unwrap().local_handler = Handler::ResumeNext;
} else { } else {
self.module_handler = Handler::ResumeNext; let module = self.current_module() as usize;
self.module_handlers[module] = Handler::ResumeNext;
} }
Ok(Flow::Normal) Ok(Flow::Normal)
} }

View File

@@ -12,20 +12,20 @@ pub mod bytecode;
pub mod codegen; pub mod codegen;
pub mod interp; pub mod interp;
use tb_frontend::Diagnostic; pub mod project;
pub use project::compile_project;
use tb_frontend::{source::SourceUnit, Diagnostic};
/// Komplette Übersetzung: Quelltext → Bytecode-Modul. /// Einmodul-API auf derselben Pipeline wie vollständige Projekte.
/// Bei Diagnosen (Compile-Fehlern) wird kein Kompilat erzeugt.
pub fn compile_source( pub fn compile_source(
module_name: &str, module_name: &str,
source: &str, source: &str,
) -> Result<bytecode::CompiledModule, Vec<Diagnostic>> { ) -> Result<bytecode::CompiledModule, Vec<Diagnostic>> {
let analysis = tb_frontend::analyze_source(module_name, source); compile_source_with_forms(
if !analysis.diagnostics.is_empty() { module_name,
return Err(analysis.diagnostics); source,
} &tb_frontend::forms::FormCatalog::default(),
let hir = analysis.hir.expect("diagnose-frei, aber kein HIR"); )
Ok(codegen::compile(&hir))
} }
pub fn compile_source_with_forms( pub fn compile_source_with_forms(
@@ -33,11 +33,10 @@ pub fn compile_source_with_forms(
source: &str, source: &str,
forms: &tb_frontend::forms::FormCatalog, forms: &tb_frontend::forms::FormCatalog,
) -> Result<bytecode::CompiledModule, Vec<Diagnostic>> { ) -> Result<bytecode::CompiledModule, Vec<Diagnostic>> {
let analysis = tb_frontend::analyze_source_with_forms(module_name, source, forms); compile_project(
if !analysis.diagnostics.is_empty() { module_name,
return Err(analysis.diagnostics); &[SourceUnit::new(module_name, module_name, source)],
} forms,
Ok(codegen::compile( &[],
&analysis.hir.expect("diagnose-frei, aber kein HIR"), )
))
} }

682
crates/tb-vm/src/project.rs Normal file
View File

@@ -0,0 +1,682 @@
//! Gemeinsame Tabellenauflösung getrennter Modulübersetzungen.
use crate::{
bytecode::{CompiledModule, Instr},
codegen,
};
use std::collections::{HashMap, HashSet};
use tb_frontend::{
ast::{Module, Stmt, TypeName},
forms::FormCatalog,
hir::{HProcKind, HTy},
source::{locate_diagnostics, SourceUnit},
Diagnostic, SourcePos,
};
use tb_runtime::value::TypeInit;
use tb_ui::frm::FormFile;
fn diagnostic(message: impl Into<String>) -> Diagnostic {
Diagnostic {
file: None,
pos: SourcePos::default(),
message: message.into(),
}
}
pub fn compile_project(
name: &str,
units: &[SourceUnit],
catalog: &FormCatalog,
forms: &[FormFile],
) -> Result<CompiledModule, Vec<Diagnostic>> {
if units.is_empty() || units.len() > u16::MAX as usize {
return Err(vec![diagnostic("Projekt ohne Module oder zu viele Module")]);
}
let mut sources = Vec::new();
let mut diagnostics = Vec::new();
let mut names = HashSet::new();
let parsed: Vec<_> = units
.iter()
.enumerate()
.map(|(id, unit)| {
let (module, errors) = unit.parse(id as u16, &mut sources);
if !names.insert(unit.name.to_uppercase()) {
let mut error = diagnostic(format!("Duplicate definition: module {}", unit.name));
error.pos = module_pos(&module);
error.file = unit.segments.first().map(|s| s.file.clone());
diagnostics.push(error);
}
diagnostics.extend(errors);
module
})
.collect();
locate_diagnostics(&mut diagnostics, &sources);
if !diagnostics.is_empty() {
return Err(diagnostics);
}
let mut catalog = catalog.clone();
if catalog.find("SCREEN").is_none() {
catalog.add(
"SCREEN",
tb_frontend::forms::ObjectClass::Screen,
None,
false,
);
}
for module in &parsed {
if module
.body
.iter()
.any(|s| matches!(s, Stmt::MetaForm { .. }))
&& catalog.find(&module.name).is_none()
{
catalog.add(
&module.name,
tb_frontend::forms::ObjectClass::Form,
None,
false,
);
}
}
let mut exports: Vec<_> = parsed
.iter()
.map(|m| tb_frontend::sema::export_declarations(m, &[]))
.collect();
// Konstantenabhängigkeiten können beliebig über Module verteilt sein.
// Jeder erfolgreiche Durchlauf löst mindestens eine weitere Deklaration auf.
let constant_count: usize = exports
.iter()
.flatten()
.filter(|s| matches!(s, Stmt::ConstDecl { .. }))
.count();
for _ in 0..constant_count {
let mut constants: HashMap<_, Vec<_>> = HashMap::new();
for stmt in exports.iter().flatten() {
if let Stmt::ConstDecl { items, .. } = stmt {
constants.entry(&items[0].0).or_default().push(stmt.clone());
}
}
let constants: Vec<_> = constants
.into_values()
.filter(|s| s.len() == 1)
.flatten()
.collect();
let next: Vec<_> = parsed
.iter()
.map(|m| tb_frontend::sema::export_declarations(m, &constants))
.collect();
if next == exports {
break;
}
exports = next;
}
let mut parts = Vec::new();
let mut commons = Vec::new();
for module in &parsed {
let mut module = module.clone();
import_declarations(&mut module, &parsed, &exports);
let (hir, errors) = tb_frontend::sema::lower_with_forms(&module, &catalog);
diagnostics.extend(errors);
if let Some(hir) = hir {
parts.push(codegen::compile(&hir));
commons.push(hir.commons);
}
}
locate_diagnostics(&mut diagnostics, &sources);
if !diagnostics.is_empty() {
return Err(diagnostics);
}
let mut result = link(name, parts, &parsed, &commons).map_err(|error| {
let mut errors = vec![error];
locate_diagnostics(&mut errors, &sources);
errors
})?;
result.sources = sources;
let objects = FormCatalog {
objects: result.objects.clone(),
};
for form in forms {
result
.form_initial
.extend(form.initial_values(&objects).map_err(|e| {
vec![diagnostic(format!(
"{}: ungültige Forms-Anfangsdaten ({e})",
form.root.name
))]
})?);
}
result.startup_form = forms
.first()
.and_then(|form| objects.find(&form.root.name).map(|(id, _)| id));
result
.validate()
.map_err(|e| vec![diagnostic(e.to_string())])?;
Ok(result)
}
/// Lokale Definition gewinnt; außerhalb ihres Moduls muss ein Name eindeutig sein.
fn type_definition<'a>(name: &str, origin: usize, all: &'a [Module]) -> Option<(usize, &'a Stmt)> {
let mut candidates = all.iter().enumerate().flat_map(|(id, module)| {
module.body.iter().filter_map(move |stmt| {
matches!(stmt, Stmt::TypeDecl { name: n, .. } if n == name).then_some((id, stmt))
})
});
if let Some(local) = candidates.clone().find(|(id, _)| *id == origin) {
return Some(local);
}
let first = candidates.next()?;
candidates.next().is_none().then_some(first)
}
fn same_type(
name: &str,
a: usize,
b: usize,
all: &[Module],
visiting: &mut HashSet<(usize, usize, String)>,
) -> bool {
let (
Some((a, Stmt::TypeDecl { fields: af, .. })),
Some((b, Stmt::TypeDecl { fields: bf, .. })),
) = (type_definition(name, a, all), type_definition(name, b, all))
else {
return false;
};
if a == b {
return true;
}
if !visiting.insert((a, b, name.into())) {
return false;
}
let equal = af.len() == bf.len()
&& af.iter().zip(bf).all(|((an, at), (bn, bt))| {
an == bn
&& match (at, bt) {
(TypeName::Udt(an), TypeName::Udt(bn)) => {
an == bn && same_type(an, a, b, all, visiting)
}
_ => at == bt,
}
});
visiting.remove(&(a, b, name.into()));
equal
}
fn import_type(
name: &str,
origin: usize,
target: usize,
all: &[Module],
imported: &mut Vec<Stmt>,
seen: &mut HashMap<(usize, String), String>,
) -> String {
let Some((origin, Stmt::TypeDecl { fields, pos, .. })) = type_definition(name, origin, all)
else {
return name.into();
};
if origin == target
|| (type_definition(name, target, all).is_some_and(|(id, _)| id == target)
&& same_type(name, origin, target, all, &mut HashSet::new()))
{
return name.into();
}
let key = (origin, name.to_string());
if let Some(alias) = seen.get(&key) {
return alias.clone();
}
// Öffentliche eindeutige Namen bleiben erhalten. Konfliktbehaftete Abhängigkeiten
// bekommen einen internen Modulnamen, damit das lokale Layout unangetastet bleibt.
let alias = if type_definition(name, target, all).is_some_and(|(id, _)| id == origin) {
name.to_string()
} else {
format!("{}!{name}", all[origin].name)
};
seen.insert(key, alias.clone());
let fields = fields
.iter()
.map(|(field, ty)| {
let ty = match ty {
TypeName::Udt(name) => {
TypeName::Udt(import_type(name, origin, target, all, imported, seen))
}
_ => ty.clone(),
};
(field.clone(), ty)
})
.collect();
imported.push(Stmt::TypeDecl {
name: alias.clone(),
fields,
pos: *pos,
});
alias
}
fn import_declarations(module: &mut Module, all: &[Module], exports: &[Vec<Stmt>]) {
let target = all.iter().position(|m| m.name == module.name).unwrap();
let mut procedures: HashSet<_> = module.procs.iter().map(|p| p.sig.name.clone()).collect();
let mut local_constants = HashSet::new();
let mut local_types = HashSet::new();
for stmt in &module.body {
match stmt {
Stmt::Declare { sig, .. } => {
procedures.insert(sig.name.clone());
}
Stmt::ConstDecl { items, .. } => {
local_constants.extend(items.iter().map(|i| i.0.clone()))
}
Stmt::TypeDecl { name, .. } => {
local_types.insert(name.clone());
}
_ => {}
}
}
let mut foreign: HashMap<_, Vec<_>> = HashMap::new();
for (origin, declarations) in exports.iter().enumerate().filter(|(id, _)| *id != target) {
for stmt in declarations {
let key = match stmt {
Stmt::Declare { sig, .. } => (0, sig.name.clone()),
Stmt::ConstDecl { items, .. } => (1, items[0].0.clone()),
Stmt::TypeDecl { name, .. } => (2, name.clone()),
_ => continue,
};
foreign.entry(key).or_default().push((origin, stmt));
}
}
let mut names: Vec<_> = foreign.keys().cloned().collect();
names.sort();
let mut imported = Vec::new();
let mut seen_types = HashMap::new();
for key in names {
let candidates = &foreign[&key];
if candidates.len() != 1 {
continue;
}
let (origin, stmt) = candidates[0];
match stmt {
Stmt::Declare { sig, pos } if !procedures.contains(&sig.name) => {
let mut sig = sig.clone();
for param in &mut sig.params {
if let Some(TypeName::Udt(name)) = &param.as_type {
param.as_type = Some(TypeName::Udt(import_type(
name,
origin,
target,
all,
&mut imported,
&mut seen_types,
)));
}
}
imported.push(Stmt::Declare { sig, pos: *pos });
}
Stmt::ConstDecl { items, .. } if !local_constants.contains(&items[0].0) => {
imported.push(stmt.clone())
}
Stmt::TypeDecl { name, .. } if !local_types.contains(name) => {
import_type(name, origin, target, all, &mut imported, &mut seen_types);
}
_ => {}
}
}
imported.append(&mut module.body);
let mut types = Vec::new();
imported.retain(|stmt| {
if matches!(stmt, Stmt::TypeDecl { .. }) {
types.push(stmt.clone());
false
} else {
true
}
});
let mut known = HashSet::new();
let mut ordered = Vec::new();
while !types.is_empty() {
let next = types.iter().position(|stmt| match stmt {
Stmt::TypeDecl { fields, .. } => fields.iter().all(|(_, ty)| match ty {
TypeName::Udt(name) => known.contains(name),
_ => true,
}),
_ => unreachable!(),
});
let Some(index) = next else { break };
let stmt = types.remove(index);
if let Stmt::TypeDecl { name, .. } = &stmt {
known.insert(name.clone());
}
ordered.push(stmt);
}
ordered.extend(types); // Sema diagnostiziert fehlende oder zyklische Typen.
ordered.extend(imported);
module.body = ordered;
}
fn proc_pos(module: &Module, name: &str) -> SourcePos {
module
.body
.iter()
.find_map(|stmt| match stmt {
Stmt::Declare { sig, pos } if sig.name == name => Some(*pos),
_ => None,
})
.or_else(|| {
module
.procs
.iter()
.find(|p| p.sig.name == name)
.map(|p| p.pos)
})
.unwrap_or_else(|| module_pos(module))
}
fn module_pos(module: &Module) -> SourcePos {
module
.body
.iter()
.map(tb_frontend::sema::stmt_pos)
.find(|p| p.line > 0)
.or_else(|| module.procs.first().map(|p| p.pos))
.unwrap_or_default()
}
fn remap_type(ty: &mut TypeInit, ids: &[u16]) {
if let TypeInit::Udt(id) = ty {
*id = ids[*id as usize];
}
}
fn remap_signature(ty: &mut HTy, ids: &[u16]) {
if let HTy::Udt(id) = ty {
*id = ids[*id as usize];
}
}
fn link(
name: &str,
mut parts: Vec<CompiledModule>,
ast: &[Module],
module_commons: &[Vec<tb_frontend::hir::HCommon>],
) -> Result<CompiledModule, Diagnostic> {
let at = |pos, message| Diagnostic {
file: None,
pos,
message,
};
let mut result = codegen::compile(&tb_frontend::analyze_source(name, "").hir.unwrap());
result.modules = parts
.iter()
.map(|p| (p.name.clone(), p.option_base))
.collect();
result.objects = parts[0].objects.clone();
result.strings.clear();
result.procs.clear();
result.option_base = parts[0].option_base;
let mut proc_maps = Vec::new();
let mut count = 1usize;
let mut definitions: HashMap<String, Vec<u16>> = HashMap::new();
for (part, module) in parts.iter().zip(ast) {
let defined: HashSet<_> = module.procs.iter().map(|p| p.sig.name.as_str()).collect();
let mut map = vec![0];
for p in part.procs.iter().skip(1) {
let id = u16::try_from(count)
.map_err(|_| at(proc_pos(module, &p.name), "Zu viele Prozeduren".into()))?;
count += 1;
map.push(id);
if defined.contains(p.name.as_str()) || p.kind == HProcKind::DefFn {
definitions.entry(p.name.clone()).or_default().push(id);
}
}
proc_maps.push(map);
}
// DECLARE-Platzhalter auf die tatsächliche, eindeutig bestimmte Definition binden.
for (module_id, part) in parts.iter().enumerate() {
let defined: HashSet<_> = ast[module_id]
.procs
.iter()
.map(|p| p.sig.name.as_str())
.collect();
for (id, proc) in part.procs.iter().enumerate().skip(1) {
if !defined.contains(proc.name.as_str()) && proc.kind != HProcKind::DefFn {
if let Some(candidates) = definitions.get(&proc.name) {
if candidates.len() != 1 {
return Err(at(
proc_pos(&ast[module_id], &proc.name),
format!("Ambiguous subprogram: {}", proc.name),
));
}
proc_maps[module_id][id] = candidates[0];
}
}
}
}
let splits: Vec<_> = parts
.iter()
.map(|p| {
p.procs[0]
.code
.iter()
.position(|i| matches!(i, Instr::Stmt(0)))
.map_or(0, |i| i.saturating_sub(1))
})
.collect();
let mut init_starts = Vec::new();
let mut body_starts = Vec::new();
let mut pc = 0;
for split in &splits {
init_starts.push(pc);
pc += split;
}
for (part, split) in parts.iter().zip(&splits) {
body_starts.push(pc);
pc += part.procs[0].code.len() - split - 1;
}
let mut main = parts[0].procs[0].clone();
main.code.clear();
main.name = name.into();
let mut initializers = Vec::new();
let mut bodies = Vec::new();
let mut common: HashMap<_, (u16, tb_frontend::hir::HCommon)> = HashMap::new();
for (module_id, part) in parts.iter_mut().enumerate() {
let mut types = Vec::new();
for udt in &part.udts {
let mut udt = udt.clone();
udt.name = udt.name.rsplit('!').next().unwrap().to_string();
for ty in &mut udt.fields {
remap_type(ty, &types);
}
let id = result
.udts
.iter()
.position(|u| u.name == udt.name && u.fields == udt.fields)
.unwrap_or_else(|| {
result.udts.push(udt);
result.udts.len() - 1
});
types.push(
u16::try_from(id)
.map_err(|_| at(module_pos(&ast[module_id]), "Zu viele TYPEs".into()))?,
);
}
let commons: HashMap<_, _> = module_commons[module_id]
.iter()
.map(|c| (c.slot, c))
.collect();
let mut globals = Vec::new();
for (slot, (ty, name)) in part.globals_init.iter().zip(&part.global_names).enumerate() {
let mut ty = ty.clone();
remap_type(&mut ty, &types);
let declaration = commons.get(&(slot as u16));
let key = declaration.map(|c| (c.block.clone(), c.key.clone()));
let mut common_ty = declaration.map(|c| c.ty.clone());
if let Some(ty) = &mut common_ty {
remap_signature(ty, &types);
}
let id = if let Some((id, previous)) = key.as_ref().and_then(|key| common.get_mut(key))
{
let declaration = declaration.unwrap();
let compatible_dims = match (&previous.dims, &declaration.dims) {
(None, None) => true,
(Some(a), Some(b)) if a.is_empty() || b.is_empty() => true,
(Some(a), Some(b)) => {
a.len() == b.len()
&& a.iter().zip(b).all(|((al, ah), (bl, bh))| {
al.zip(*bl).is_none_or(|(a, b)| a == b)
&& ah.zip(*bh).is_none_or(|(a, b)| a == b)
})
}
_ => false,
};
if Some(&previous.ty) != common_ty.as_ref() || !compatible_dims {
return Err(at(
declaration.pos,
format!("COMMON type or bounds mismatch: {name}"),
));
}
if let (Some(previous), Some(current)) = (&mut previous.dims, &declaration.dims) {
if previous.is_empty() {
*previous = current.clone();
} else {
for ((lo, hi), (new_lo, new_hi)) in previous.iter_mut().zip(current) {
*lo = lo.or(*new_lo);
*hi = hi.or(*new_hi);
}
}
}
*id
} else {
let id = u16::try_from(result.globals_init.len()).map_err(|_| {
at(
module_pos(&ast[module_id]),
"Zu viele globale Variablen".into(),
)
})?;
result.globals_init.push(ty);
result.global_names.push(if ast.len() == 1 {
name.clone()
} else {
format!("{}!{name}", part.name)
});
if let Some(key) = key {
let mut declaration = (*declaration.unwrap()).clone();
declaration.ty = common_ty.unwrap();
common.insert(key, (id, declaration));
}
id
};
globals.push(id);
}
let string_offset = result.strings.len();
if string_offset + part.strings.len() >= u16::MAX as usize {
return Err(at(
module_pos(&ast[module_id]),
"Zu viele Stringkonstanten".into(),
));
}
result.strings.append(&mut part.strings);
let data_offset = result.data.len() as u32;
result.data.append(&mut part.data);
let jump_offset = result.jump_tables.len();
if jump_offset + part.jump_tables.len() > u16::MAX as usize {
return Err(at(
module_pos(&ast[module_id]),
"Zu viele Sprungtabellen".into(),
));
}
let main_pc = |pc: u32| if (pc as usize) < splits[module_id] { init_starts[module_id] + pc as usize } else { body_starts[module_id] + pc as usize - splits[module_id] } as u32;
for (proc_id, proc) in part.procs.iter_mut().enumerate() {
proc.module = module_id as u16;
for ty in &mut proc.locals_init {
remap_type(ty, &types);
}
for param in &mut proc.params {
remap_signature(&mut param.ty, &types);
}
if let Some(ty) = &mut proc.ret_ty {
remap_signature(ty, &types);
}
if ast.len() > 1 {
proc.name = format!("{}!{}", part.name, proc.name);
}
for instruction in &mut proc.code {
use Instr::*;
match instruction {
PushStr(id)
| Unsupported(id)
| LoadDynamicObjectProperty(id)
| StoreDynamicObjectProperty(id) => *id += string_offset as u16,
Input(_, _, id, _) if *id != u16::MAX => *id += string_offset as u16,
LoadGlobal(id) | StoreGlobal(id) | MakeRefGlobal(id) => {
*id = globals[*id as usize]
}
LoadArr(global, id, _, ty)
| DimArr(global, id, _, ty)
| CommonArr(global, id, _, ty)
| RedimArr(global, id, _, ty) => {
if *global {
*id = globals[*id as usize];
}
remap_type(ty, &types);
}
EraseSlot(true, id) => *id = globals[*id as usize],
GetPut(_, _, 7, id) | PushUdtId(id) => *id = types[*id as usize],
Call(id, _) => *id = proc_maps[module_id][*id as usize],
Restore(id) => *id += data_offset,
OnErrorGoto(pc) => *pc = main_pc(*pc),
Jump(pc)
| JumpIfFalse(pc)
| JumpIfTrue(pc)
| Gosub(pc)
| RetGosubTo(pc)
| OnErrorLocal(pc)
| ResumeLabel(pc)
| TrapDefine(_, pc)
if proc_id == 0 =>
{
*pc = main_pc(*pc)
}
OnJump(id, _) => {
if proc_id == 0 {
for target in &mut part.jump_tables[*id as usize] {
*target = main_pc(*target);
}
}
*id += jump_offset as u16;
}
_ => {}
}
}
}
initializers.extend_from_slice(&part.procs[0].code[..splits[module_id]]);
bodies.extend_from_slice(
&part.procs[0].code[splits[module_id]..part.procs[0].code.len() - 1],
);
result.procs.extend(part.procs.iter().skip(1).cloned());
for mut e in part.event_procs.drain(..) {
e.proc = proc_maps[module_id][e.proc as usize];
result.event_procs.push(e);
}
result.jump_tables.append(&mut part.jump_tables);
}
initializers.extend(bodies);
initializers.push(Instr::End);
main.code = initializers;
result.procs.insert(0, main);
// Vollständige Signaturen zwischen DECLARE-Platzhalter und Ziel vergleichen.
for (module, part) in parts.iter().enumerate() {
for (id, proc) in part.procs.iter().enumerate().skip(1) {
let target = &result.procs[proc_maps[module][id] as usize];
if proc.ret_ty != target.ret_ty
|| proc.kind != target.kind
|| proc.params.len() != target.params.len()
|| proc
.params
.iter()
.zip(&target.params)
.any(|(a, b)| a.ty != b.ty || a.array != b.array || a.by_ref != b.by_ref)
{
return Err(at(
proc_pos(&ast[module], proc.name.rsplit('!').next().unwrap()),
format!("Parameter type mismatch: {}", proc.name),
));
}
}
}
Ok(result)
}

View File

@@ -0,0 +1,299 @@
use tb_frontend::{
forms::FormCatalog,
source::{SourceSegment, SourceUnit},
};
use tb_runtime::{host::CaptureHost, value::Value};
use tb_ui::frm::{self, FormFile};
use tb_vm::{
bytecode::CompiledModule,
compile_project,
interp::{RunEvent, Vm},
};
fn compile(units: &[SourceUnit], forms: &[FormFile]) -> CompiledModule {
let mut catalog = FormCatalog::default();
for form in forms {
catalog.append(&form.catalog());
}
compile_project("APP", units, &catalog, forms).unwrap_or_else(|d| panic!("{d:#?}"))
}
#[test]
fn module_includes_erl_breakpoints_und_inspektion_behalten_ihren_ursprung() {
let a = SourceUnit::new("MAIN", "main.bas", "x%=1\nCALL Fehler\nEND\n");
let b = SourceUnit {
name: "LIB".into(),
segments: vec![
SourceSegment {
file: "lib.bas".into(),
first_line: 1,
text: "SUB Fehler\n".into(),
},
SourceSegment {
file: "nested.bi".into(),
first_line: 2,
text: "200 ERROR 6\n".into(),
},
SourceSegment {
file: "lib.bas".into(),
first_line: 3,
text: "END SUB\n".into(),
},
],
};
let module = compile(&[a, b], &[]);
let bytes = module.to_tbc();
let loaded = CompiledModule::from_tbc(&bytes).unwrap();
assert_eq!(loaded.to_tbc(), bytes);
let mut vm = Vm::new(loaded);
let mut host = CaptureHost::default();
vm.add_module_breakpoint(1, 2);
assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 2 });
assert_eq!(vm.current_module(), 1);
assert_eq!(vm.current_file(), "nested.bi");
assert_eq!(vm.current_source_pos().column, 1);
assert!(matches!(vm.inspect("MAIN!x"), Some(Value::Int(1))));
vm.remove_module_breakpoint(1, 2);
vm.set_step(true);
assert_eq!(vm.run(&mut host), RunEvent::Stepped { line: 2 });
assert_eq!(vm.current_file(), "nested.bi");
assert_eq!(vm.current_source_pos().column, 5);
vm.set_step(false);
assert!(matches!(
vm.run(&mut host),
RunEvent::Error {
code: 6,
line: 2,
..
}
));
let mut erl_vm = Vm::new(compile(
&[SourceUnit::new(
"NUM",
"num.bas",
"ON ERROR RESUME NEXT\n200 ERROR 6\nn%=ERL\nSTOP",
)],
&[],
));
assert!(matches!(erl_vm.run(&mut host), RunEvent::Stopped { .. }));
assert!(matches!(erl_vm.inspect("n"), Some(Value::Int(200))));
assert_eq!(vm.current_file(), "nested.bi");
}
#[test]
fn diagnose_im_include_und_echte_duplikate_bleiben_sichtbar() {
let source = SourceUnit {
name: "LIB".into(),
segments: vec![SourceSegment {
file: "nested.bi".into(),
first_line: 2,
text: "Text$ = 42\n".into(),
}],
};
let errors = compile_project("APP", &[source], &FormCatalog::default(), &[]).unwrap_err();
assert!(errors
.iter()
.any(|e| e.file.as_deref() == Some("nested.bi") && e.pos.line == 2 && e.pos.column == 1));
for text in [
"SUB X\nEND SUB\nSUB X\nEND SUB",
"CONST N=1\nCONST N=2",
"TYPE T\nx AS INTEGER\nEND TYPE\nTYPE T\nx AS INTEGER\nEND TYPE",
] {
let errors = compile_project(
"APP",
&[SourceUnit::new("M", "m.bas", text)],
&FormCatalog::default(),
&[],
)
.unwrap_err();
assert!(
errors
.iter()
.any(|e| e.message.contains("Duplicate definition")),
"{errors:?}"
);
}
}
#[test]
fn initialisierungen_def_fn_und_modulhandler_haben_eigene_quellorte() {
for (source, expected_line) in [
("' Bibliothek\nDIM a%(1 TO 0)", 2),
(
"' Bibliothek\nDEF FNkaputt(x)=1/x\nSUB Fehler\nPRINT FNkaputt(0)\nEND SUB",
2,
),
] {
let module = compile(
&[
SourceUnit::new("A", "a.bas", "CALL Fehler\nEND"),
SourceUnit::new(
"B",
"b.bas",
&format!(
"{source}\n{}",
if source.contains("SUB") {
""
} else {
"SUB Fehler\nEND SUB"
}
),
),
],
&[],
);
let mut vm = Vm::new(CompiledModule::from_tbc(&module.to_tbc()).unwrap());
let event = vm.run(&mut CaptureHost::default());
assert!(
matches!(event, RunEvent::Error { line, .. } if line == expected_line),
"{event:?}"
);
assert_eq!(vm.current_file(), "b.bas");
}
let units = [
SourceUnit::new(
"A",
"a.bas",
"ON ERROR GOTO H\nCALL Fehler\nEND\nH:\nPRINT ERR;ERL\nEND",
),
SourceUnit::new(
"B",
"b.bas",
"SUB Fehler\nON ERROR GOTO 0\n200 ERROR 6\nEND SUB",
),
];
let mut vm = Vm::new(compile(&units, &[]));
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen), " 6 200 \n");
}
#[test]
fn lokale_prozeduren_shared_common_und_wiederholte_includes_bleiben_getrennt() {
let a = SourceUnit::new("A", "a.bas", "CONST N=1\nDIM SHARED x%\nCOMMON SHARED c%\nx%=10\nCALL Privat\nCALL Zweites\nPRINT x%;c%\nEND\nSUB Privat\nx%=x%+N\nc%=c%+1\nEND SUB");
let b = SourceUnit::new("B", "b.bas", "CONST N=2\nDIM SHARED x%\nCOMMON SHARED c%\nSUB Zweites\nx%=20\nCALL Privat\nPRINT x%\nEND SUB\nSUB Privat\nx%=x%+N\nc%=c%+2\nEND SUB");
let mut vm = Vm::new(compile(&[a, b], &[]));
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen), " 22 \n 11 3 \n");
let include = SourceSegment {
file: "shared.bi".into(),
first_line: 1,
text: "CONST N=7\n".into(),
};
let units: Vec<_> = ["A", "B"]
.iter()
.map(|name| SourceUnit {
name: (*name).into(),
segments: vec![
include.clone(),
SourceSegment {
file: format!("{name}.bas"),
first_line: 2,
text: "PRINT N\n".into(),
},
],
})
.collect();
let mut vm = Vm::new(compile(&units, &[]));
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen), " 7 \n 7 \n");
}
#[test]
fn typreferenzen_signaturen_und_option_base_ueber_modulgrenzen() {
use tb_frontend::hir::{HProcKind, HTy, NumTy};
use tb_vm::bytecode::Instr;
let units = [
SourceUnit::new("A", "a.bas", "DECLARE SUB CmnDlgRegister(ok AS INTEGER)\nTYPE Klein\nx AS INTEGER\nEND TYPE\nDIM s AS Gross\nDIM a%(2)\ns.x=4\na%(1)=3\nCmnDlgRegister ok%\nPRINT ok%\nPRINT Summe%(a%(), s, 2)\nPRINT s.x\nCALL Basis\nEND"),
SourceUnit::new("B", "b.bas", "OPTION BASE 1\nTYPE Gross\nx AS LONG\ny AS STRING * 8\nEND TYPE\nFUNCTION Summe%(a%(), s AS Gross, n%)\nSumme%=a%(1)+s.x+n%\ns.x=s.x+1\nEND FUNCTION\nSUB Basis\nb%(1)=1\nPRINT LBOUND(b%)\nEND SUB\nSUB Datei\nOPEN \"unbenutzt.isam\" FOR ISAM Gross \"T\" AS #1\nCLOSE #1\nEND SUB"),
];
let module = compile(&units, &[]);
let bytes = module.to_tbc();
let loaded = CompiledModule::from_tbc(&bytes).unwrap();
assert_eq!(bytes, loaded.to_tbc());
let signature = loaded.procs.iter().find(|p| p.name == "B!SUMME").unwrap();
assert_eq!(signature.kind, HProcKind::Function);
assert_eq!(signature.ret_ty, Some(HTy::Num(NumTy::Int)));
assert!(signature.params[0].array && !signature.params[0].by_ref);
assert!(signature.params[2].by_ref);
assert!(signature.params[1].by_ref);
let HTy::Udt(udt) = signature.params[1].ty else {
panic!("UDT-Parameter fehlt")
};
assert_eq!(loaded.udts[udt as usize].name, "GROSS");
let file = loaded.procs.iter().find(|p| p.name == "B!DATEI").unwrap();
assert!(file.code.contains(&Instr::PushUdtId(udt)));
let mut vm = Vm::new(loaded);
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
assert_eq!(
tb_runtime::snapshot::text(&vm.rt.screen),
"-1 \n 9 \n 5 \n 1 \n"
);
}
#[test]
fn beschaedigte_container_werden_vor_der_ausfuehrung_abgewiesen() {
use tb_vm::bytecode::Instr;
let form = frm::read_text("f.frm", "VERSION 1.00\nBEGIN Form F\n BEGIN TextBox Text1\n Index = 2\n Text = \"hello\"\n END\nEND\n").unwrap();
let bytes = compile(&[SourceUnit::new("F", "f.frm", "END")], &[form]).to_tbc();
for length in 0..bytes.len() {
assert!(
CompiledModule::from_tbc(&bytes[..length]).is_err(),
"Länge {length}"
);
}
for (offset, value) in [(6, 1u32), (8, u32::MAX), (16, 0), (20, u32::MAX)] {
let mut bad = bytes.clone();
bad[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
assert!(CompiledModule::from_tbc(&bad).is_err(), "Offset {offset}");
}
let mut duplicate = bytes.clone();
duplicate[24..28].copy_from_slice(b"MODN");
assert!(CompiledModule::from_tbc(&duplicate).is_err());
for mutation in 0..8 {
let mut bad = CompiledModule::from_tbc(&bytes).unwrap();
match mutation {
0 => bad.sources[0].module = u16::MAX,
1 => bad.procs[0].module = u16::MAX,
2 => bad.procs[0].code.push(Instr::Call(u16::MAX, 0)),
3 => bad.procs[0].n_params = 1,
4 => bad.objects[1].parent = Some(1),
5 => bad.form_initial[1].index = 3,
6 => {
bad.form_initial[1].properties.insert(
0,
tb_ui::forms::PropertyValue::Object(Some((u16::MAX, None))),
);
}
7 => bad.procs[0].code.push(Instr::Source(u32::MAX, 1)),
_ => unreachable!(),
}
assert!(
CompiledModule::from_tbc(&bad.to_tbc()).is_err(),
"Mutation {mutation}"
);
}
}
#[test]
fn zwei_formulare_mit_gleichen_controls_und_arrays_laufen_aus_dem_kompilat() {
let form = |name: &str, text: &str| {
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\n\nSUB Form_Load\nText1.Text = Text1.Text + \"!\"\nEND SUB\n"
)).unwrap()
};
let forms = [form("Form1", "a"), form("Form2", "b")];
let mut units = vec![SourceUnit::new("MAIN", "main.bas", "Form2.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")];
units.extend(
forms
.iter()
.map(|f| SourceUnit::new(&f.root.name, &format!("{}.frm", f.root.name), &f.code)),
);
let module = compile(&units, &forms);
assert_eq!(module.event_procs.len(), 2);
let bytes = module.to_tbc();
let loaded = CompiledModule::from_tbc(&bytes).unwrap();
assert_eq!(loaded.to_tbc(), bytes);
let mut vm = Vm::new(loaded);
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen), "a!\nb!\nzwei\n");
}

View File

@@ -0,0 +1,548 @@
//! Unveränderte ursprüngliche Reviewproben und ergänzende Regressionen des Projektkompilats.
use tb_frontend::{
forms::FormCatalog,
source::{SourceSegment, SourceUnit},
};
use tb_runtime::{host::CaptureHost, value::Value};
use tb_vm::{
bytecode::CompiledModule,
compile_project,
interp::{RunEvent, Vm},
};
fn units(files: &[(&str, &str)]) -> Vec<SourceUnit> {
files
.iter()
.map(|(name, code)| SourceUnit::new(name, &format!("{name}.bas"), code))
.collect()
}
fn compile(files: &[(&str, &str)]) -> Result<CompiledModule, Vec<tb_frontend::Diagnostic>> {
compile_project("APP", &units(files), &FormCatalog::default(), &[])
}
fn run(module: CompiledModule) -> (RunEvent, String) {
let bytes = module.to_tbc();
let loaded = CompiledModule::from_tbc(&bytes).unwrap();
assert_eq!(bytes, loaded.to_tbc());
let mut vm = Vm::new(loaded);
let event = vm.run(&mut CaptureHost::default());
(event, tb_runtime::snapshot::text(&vm.rt.screen))
}
#[test]
fn v1_zweige_und_schleifen_behalten_eigene_quellorte() {
let mut failures = vec![];
for (name, code, line) in [
(
"ELSEIF",
"IF 0 THEN\nPRINT 1\nELSEIF 1/0 THEN\nPRINT 2\nEND IF",
3,
),
("CASE", "SELECT CASE 1\nCASE 1/0\nPRINT 2\nEND SELECT", 2),
("NEXT", "FOR i%=32767 TO 32767\nPRINT 1\nNEXT", 3),
("LOOP", "DO\nPRINT 1\nLOOP UNTIL 1/0", 3),
] {
let (event, _) = run(compile(&[("MAIN", code)]).unwrap());
if !matches!(event, RunEvent::Error { line: found, .. } if found == line) {
failures.push(format!("{name}: erwartet Zeile {line}, erhalten {event:?}"));
}
}
let source = SourceUnit {
name: "MAIN".into(),
segments: vec![
SourceSegment {
file: "main.bas".into(),
first_line: 1,
text: "IF 0 THEN\nPRINT 1\n".into(),
},
SourceSegment {
file: "cond.bi".into(),
first_line: 1,
text: "ELSEIF 1/0 THEN\nPRINT 2\n".into(),
},
SourceSegment {
file: "main.bas".into(),
first_line: 4,
text: "END IF\n".into(),
},
],
};
let mut vm = Vm::new(compile_project("APP", &[source], &FormCatalog::default(), &[]).unwrap());
let event = vm.run(&mut CaptureHost::default());
if vm.current_file() != "cond.bi" {
failures.push(format!(
"Include: {event:?} aus {} statt cond.bi",
vm.current_file()
));
}
// Das gleiche Problem darf nicht nur durch Umbenennen der Fehlermeldung behoben werden.
let mut vm = Vm::new(
compile(&[("MAIN", "IF 0 THEN\nPRINT 1\nELSEIF 1 THEN\nPRINT 2\nEND IF")]).unwrap(),
);
vm.add_module_breakpoint(0, 3);
let event = vm.run(&mut CaptureHost::default());
if event != (RunEvent::Breakpoint { line: 3 }) {
failures.push(format!("Breakpoint ELSEIF: {event:?}"));
}
assert!(failures.is_empty(), "{}", failures.join("\n"));
}
#[test]
fn v2_importierte_deklarationen_behalten_kontext_und_abhaengigkeiten() {
let cases = [
(
"DEFINT",
"PRINT Doppelt(3)\nEND",
"DEFINT A-Z\nFUNCTION Doppelt(x)\nDoppelt=x*2\nEND FUNCTION",
" 6 \n",
),
(
"CONST",
"CONST A=3\nPRINT B\nEND",
"CONST A=3\nCONST B=A+1",
" 4 \n",
),
(
"TYPE",
"TYPE Inner\nx AS INTEGER\nEND TYPE\nDIM a AS Outer\na.i.x=3\nPRINT a.i.x\nEND",
"TYPE Inner\nx AS INTEGER\nEND TYPE\nTYPE Outer\ni AS Inner\nEND TYPE",
" 3 \n",
),
];
let mut failures = vec![];
for (name, main, lib, output) in cases {
match compile(&[("MAIN", main), ("LIB", lib)]) {
Err(e) => failures.push(format!("{name}: {e:?}")),
Ok(module) => {
let actual = run(module);
if actual != (RunEvent::Ended, output.into()) {
failures.push(format!("{name}: {actual:?}"));
}
}
}
}
assert!(failures.is_empty(), "{}", failures.join("\n"));
}
#[test]
fn v3_form_metabefehle_erzeugen_getrennte_formularobjekte() {
let module = compile(&[
(
"A",
"'$FORM\nCaption=\"a\"\nCALL SetupB\nPRINT Caption\nEND",
),
("B", "'$FORM\nSUB SetupB\nCaption=\"b\"\nEND SUB"),
])
.unwrap();
let names: Vec<_> = module.objects.iter().map(|o| o.name.clone()).collect();
let actual = run(module);
assert_eq!(
actual,
(RunEvent::Ended, "a\n".into()),
"Objekte: {names:?}"
);
}
#[test]
fn v4_common_behaelt_typidentitaet_und_initialisiert_arrays_einmal() {
let mut failures = vec![];
for (name, files, want) in [
(
"Suffixe",
vec![(
"MAIN",
"COMMON SHARED c%, c$\nc%=7\nc$=\"ok\"\nPRINT c%;c$\nEND",
)],
" 7 ok\n",
),
(
"Array",
vec![
("MAIN", "COMMON SHARED a%(2)\na%(1)=7\nCALL F\nEND"),
("LIB", "COMMON SHARED a%(2)\nSUB F\nPRINT a%(1)\nEND SUB"),
],
" 7 \n",
),
] {
match compile(&files) {
Err(e) => failures.push(format!("{name}: {e:?}")),
Ok(module) => {
let actual = run(module);
if actual != (RunEvent::Ended, want.into()) {
failures.push(format!("{name}: {actual:?}"));
}
}
}
}
assert!(failures.is_empty(), "{}", failures.join("\n"));
}
#[test]
fn v5_load_und_unload_loesen_den_expliziten_container_auf() {
use tb_frontend::forms::ObjectClass;
let mut catalog = FormCatalog::default();
for name in ["Form1", "Form2"] {
catalog.add(name, ObjectClass::Form, None, false);
catalog.add("Text1", ObjectClass::TextBox, Some(name), true);
}
let source = units(&[("MAIN", "LOAD Form2!Text1(3)\nForm2!Text1(3).Text=\"b\"\nPRINT Form2!Text1(3).Text\nUNLOAD Form2!Text1(3)\nEND")]);
let module = compile_project("APP", &source, &catalog, &[]).unwrap();
assert_eq!(run(module), (RunEvent::Ended, "b\n".into()));
}
#[test]
fn v6_linkerdiagnose_nennt_den_urspruenglichen_dateiort() {
let errors = compile(&[
("MAIN", "DECLARE SUB F(x%)\nCALL F(1)\nEND"),
("LIB", "SUB F(x$)\nEND SUB"),
])
.unwrap_err();
assert!(
errors
.iter()
.any(|d| d.message.contains("Parameter type mismatch")),
"{errors:?}"
);
assert!(
errors
.iter()
.all(|d| d.file.is_some() && d.pos.line > 0 && d.pos.column > 0),
"{errors:?}"
);
}
#[test]
fn modulbreakpoint_rundlauf_inspektion_und_numerische_erl_bleiben_getrennt() {
let main = format!(
"DIM a%(2)\nTYPE T\nx AS INTEGER\nEND TYPE\nDIM r AS T\na%(1)=7\nr.x=8\n{}CALL F\nEND",
"\n\nn%=1\n"
);
let lib = format!("SUB F\n{}200 ERROR 6\nEND SUB", "\n".repeat(8));
let module = compile(&[("MAIN", &main), ("LIB", &lib)]).unwrap();
let bytes = module.to_tbc();
let loaded = CompiledModule::from_tbc(&bytes).unwrap();
assert_eq!(bytes, loaded.to_tbc());
let mut vm = Vm::new(loaded);
vm.add_module_breakpoint(1, 10);
let mut host = CaptureHost::default();
assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 10 });
assert_eq!(vm.current_module(), 1);
assert_eq!(vm.current_file(), "LIB.bas");
assert!(matches!(
vm.inspect_element("MAIN!a%", &[1]),
Some(Value::Int(7))
));
assert!(matches!(
vm.inspect_field("MAIN!r", &[0]),
Some(Value::Int(8))
));
vm.remove_module_breakpoint(1, 10);
vm.set_step(true);
assert_eq!(vm.run(&mut host), RunEvent::Stepped { line: 10 });
assert_eq!(vm.current_source_pos().column, 5);
vm.set_step(false);
assert!(matches!(
vm.run(&mut host),
RunEvent::Error {
code: 6,
line: 10,
..
}
));
let source = format!("{}ERROR 6", "\n".repeat(41));
assert!(matches!(
run(compile(&[("MAIN", &source)]).unwrap()).0,
RunEvent::Error {
code: 6,
line: 42,
..
}
));
let module = compile(&[(
"MAIN",
"ON ERROR RESUME NEXT\n200 ERROR 6\nPRINT ERR;ERL\nEND",
)])
.unwrap();
assert_eq!(run(module), (RunEvent::Ended, " 6 200 \n".into()));
}
#[test]
fn importkontext_bleibt_auch_bei_abweichenden_lokalen_definitionen_erhalten() {
for (files, expected) in [
(vec![("MAIN", "CONST A=99\nPRINT B\nEND"), ("LIB", "CONST A=3\nCONST B=A+1")], " 4 \n"),
(vec![("MAIN", "PRINT C\nEND"), ("LIB", "CONST C=B+1"), ("BASE", "CONST B=3")], " 4 \n"),
(vec![("MAIN", "TYPE Inner\nx AS STRING * 3\nEND TYPE\nDIM a AS Outer\na.i.x=3\nPRINT a.i.x\nEND"), ("LIB", "TYPE Inner\nx AS INTEGER\nEND TYPE\nTYPE Outer\ni AS Inner\nEND TYPE")], " 3 \n"),
(vec![("MAIN", "DEFSTR A-Z\nx%=3\nCALL Twice(x%)\nPRINT x%\nPRINT Doppelt(3)\nEND"), ("LIB", "DEFINT A-Z\nSUB Twice(x)\nx=x*2\nEND SUB\nFUNCTION Doppelt(x)\nDoppelt=x*2\nEND FUNCTION")], " 6 \n 6 \n"),
] {
let actual = run(compile(&files).unwrap_or_else(|e| panic!("{files:?}: {e:?}")));
assert_eq!(actual, (RunEvent::Ended, expected.into()), "{files:?}");
}
}
#[test]
fn common_vertraege_und_linkfehler_haben_echte_quellorte() {
for (left, right) in [
("COMMON SHARED a%(2)", "COMMON SHARED a%(3)"),
("COMMON SHARED a%(1 TO 2)", "COMMON SHARED a%(0 TO 2)"),
("COMMON SHARED a%(2)", "COMMON SHARED a%(2,2)"),
("COMMON SHARED a AS INTEGER", "COMMON SHARED a AS STRING"),
("COMMON SHARED a%(2)", "COMMON SHARED a%"),
] {
let units = [
SourceUnit::new("MAIN", "main.bas", &format!("{left}\nEND")),
SourceUnit {
name: "LIB".into(),
segments: vec![SourceSegment {
file: "shared.bi".into(),
first_line: 42,
text: format!(" {right}\n"),
}],
},
];
let errors = compile_project("APP", &units, &FormCatalog::default(), &[]).unwrap_err();
assert!(
errors.iter().any(|e| e.file.as_deref() == Some("shared.bi")
&& e.pos.line == 42
&& e.pos.column > 1
&& e.message.contains("COMMON")),
"{errors:?}"
);
}
let files = [
(
"MAIN",
"COMMON SHARED a%(2),a$(2)\na%(1)=7\na$(1)=\"ok\"\nCALL F\nEND",
),
(
"LIB",
"COMMON SHARED a%(2),a$(2)\nSUB F\nPRINT a%(1);a$(1)\nEND SUB",
),
];
assert_eq!(
run(compile(&files).unwrap()),
(RunEvent::Ended, " 7 ok\n".into())
);
let errors =
compile(&[("MAIN", "' header\nDECLARE SUB F(x$)\nSUB F(x%)\nEND SUB")]).unwrap_err();
assert!(
errors.iter().any(|e| e.pos.line == 3
&& e.file.as_deref() == Some("MAIN.bas")
&& e.message.contains("Parameter")),
"{errors:?}"
);
}
#[test]
fn schleifengrenzen_tragen_breakpoint_include_und_resume() {
for (source, line) in [
("FOR i%=1 TO 2\nPRINT i%\nNEXT\nEND", 3),
("DO\nPRINT 1\nLOOP UNTIL 1\nEND", 3),
("i%=1\nWHILE i%\ni%=0\nWEND\nEND", 4),
(
"SELECT CASE 2\nCASE 1\nPRINT 1\nCASE 2\nPRINT 2\nEND SELECT",
4,
),
] {
let module = compile(&[("MAIN", source)]).unwrap();
let mut vm = Vm::new(CompiledModule::from_tbc(&module.to_tbc()).unwrap());
vm.add_module_breakpoint(0, line);
assert_eq!(
vm.run(&mut CaptureHost::default()),
RunEvent::Breakpoint { line }
);
assert_eq!(vm.current_file(), "MAIN.bas");
assert_eq!(vm.current_source_pos().column, 1);
vm.remove_module_breakpoint(0, line);
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
}
let actual = run(compile(&[("MAIN", "x%=1\nWHILE 1/x%\nx%=0\nWEND")]).unwrap());
assert!(
matches!(
actual.0,
RunEvent::Error {
code: 11,
line: 2,
..
}
),
"{actual:?}"
);
let actual = run(compile(&[(
"MAIN",
"ON ERROR GOTO H\nDO\nn%=n%+1\nLOOP UNTIL 1/x%\nPRINT n%\nEND\nH:\nx%=1\nRESUME",
)])
.unwrap());
assert_eq!(actual, (RunEvent::Ended, " 1 \n".into()));
let actual = run(compile(&[(
"MAIN",
"ON ERROR RESUME NEXT\nFOR i%=32767 TO 32767\nNEXT\nPRINT ERR\nEND",
)])
.unwrap());
assert_eq!(actual, (RunEvent::Ended, " 6 \n".into()));
let unit = SourceUnit {
name: "MAIN".into(),
segments: vec![
SourceSegment {
file: "main.bas".into(),
first_line: 1,
text: "DO\nPRINT 1\n".into(),
},
SourceSegment {
file: "end.bi".into(),
first_line: 42,
text: " LOOP UNTIL 1/0\n".into(),
},
],
};
let module = compile_project("APP", &[unit], &FormCatalog::default(), &[]).unwrap();
let mut vm = Vm::new(CompiledModule::from_tbc(&module.to_tbc()).unwrap());
assert!(matches!(
vm.run(&mut CaptureHost::default()),
RunEvent::Error {
code: 11,
line: 42,
..
}
));
assert_eq!(vm.current_file(), "end.bi");
assert_eq!(vm.current_source_pos().column, 3);
}
#[test]
fn debugger_unterscheidet_typisierte_scalar_und_arraynamen() {
let mut vm = Vm::new(
compile(&[(
"MAIN",
"COMMON SHARED c%,c$,a%(2),a$(2)\nc%=7\nc$=\"ok\"\na%(1)=8\na$(1)=\"array\"\nSTOP",
)])
.unwrap(),
);
assert!(matches!(
vm.run(&mut CaptureHost::default()),
RunEvent::Stopped { .. }
));
assert!(matches!(vm.inspect("c%"), Some(Value::Int(7))));
assert!(matches!(vm.inspect("c$"),Some(Value::Str(s)) if s.as_ref()=="ok"));
assert!(matches!(
vm.inspect_element("a%", &[1]),
Some(Value::Int(8))
));
assert!(matches!(vm.inspect_element("a$",&[1]),Some(Value::Str(s)) if s.as_ref()=="array"));
}
#[test]
fn common_arrays_behalten_dynamische_grenzen_und_kompatible_offene_deklarationen() {
for (left, right) in [
("COMMON SHARED a%(2)", "COMMON SHARED a%()"),
("COMMON SHARED a%(2.4)", "COMMON SHARED a%(2)"),
("COMMON SHARED a%(n%+2)", "COMMON SHARED a%(n%+2)"),
] {
let files = [
("MAIN", format!("{left}\na%(1)=7\nCALL F\nEND")),
("LIB", format!("{right}\nSUB F\nPRINT a%(1)\nEND SUB")),
];
let files: Vec<_> = files.iter().map(|(n, s)| (*n, s.as_str())).collect();
assert_eq!(
run(compile(&files).unwrap()),
(RunEvent::Ended, " 7 \n".into()),
"{files:?}"
);
}
let module = compile(&[
("MAIN", "COMMON SHARED a%(n%+2)\nEND"),
("LIB", "COMMON SHARED a%(n%+3)"),
])
.unwrap();
let mut vm = Vm::new(CompiledModule::from_tbc(&module.to_tbc()).unwrap());
assert!(matches!(
vm.run(&mut CaptureHost::default()),
RunEvent::Error {
code: 13,
line: 1,
..
}
));
assert_eq!(vm.current_file(), "LIB.bas");
}
#[test]
fn prozedurlokaler_deftype_veraendert_keine_nachfolgende_signatur() {
let files=[("MAIN","PRINT F(3)\nEND"),("LIB","DEFINT A-Z\nSUB Setup\nDEFSTR A-Z\nx=\"local\"\nEND SUB\nFUNCTION F(x)\nF=x*2\nEND FUNCTION")];
assert_eq!(
run(compile(&files).unwrap()),
(RunEvent::Ended, " 6 \n".into())
);
}
#[test]
fn gemischte_frm_und_form_module_haben_getrennte_ereignisbindungen() {
let form=tb_ui::frm::read_text("A.frm","VERSION 1.00\nBEGIN Form A\n Caption = \"design\"\nEND\nSUB Form_Load\nCaption=\"a\"\nEND SUB").unwrap();
let sources = units(&[
(
"MAIN",
"B.Show\na$=A.Caption\nb$=B.Caption\nA.Hide\nB.Hide\nCLS\nPRINT a$;b$\nEND",
),
("A", &form.code),
("B", "'$FORM\nSUB Form_Load\nCaption=\"b\"\nEND SUB"),
]);
let module = compile_project("APP", &sources, &form.catalog(), &[form]).unwrap();
assert_eq!(module.event_procs.len(), 2);
assert_ne!(module.event_procs[0].object, module.event_procs[1].object);
assert_eq!(run(module), (RunEvent::Ended, "ab\n".into()));
}
#[test]
fn diagnostik_bei_literalen_im_include_hat_die_exakte_spalte() {
let unit = SourceUnit {
name: "MAIN".into(),
segments: vec![
SourceSegment {
file: "main.bas".into(),
first_line: 1,
text: "DECLARE SUB F(x%)\n".into(),
},
SourceSegment {
file: "args.bi".into(),
first_line: 42,
text: " CALL F(\"wrong\")\n".into(),
},
],
};
let errors = compile_project("APP", &[unit], &FormCatalog::default(), &[]).unwrap_err();
assert!(
errors.iter().any(|e| e.file.as_deref() == Some("args.bi")
&& e.pos.line == 42
&& e.pos.column == 10
&& e.message.contains("Parameter type mismatch")),
"{errors:?}"
);
}
#[test]
fn spaetere_common_konflikte_und_doppelte_module_werden_am_ursprung_gemeldet() {
let errors = compile(&[
("MAIN", "COMMON a%()\nEND"),
("LIB", "COMMON a%(2)"),
("THIRD", "' head\nCOMMON a%(3)"),
])
.unwrap_err();
assert!(
errors.iter().any(|e| e.file.as_deref() == Some("THIRD.bas")
&& e.pos.line == 2
&& e.message.contains("COMMON")),
"{errors:?}"
);
let units = [
SourceUnit::new("SAME", "first.bas", "END"),
SourceUnit::new("SAME", "second.bas", "' head\n END"),
];
let errors = compile_project("APP", &units, &FormCatalog::default(), &[]).unwrap_err();
assert!(
errors
.iter()
.any(|e| e.file.as_deref() == Some("second.bas")
&& e.pos.line == 2
&& e.pos.column == 3
&& e.message.contains("Duplicate")),
"{errors:?}"
);
}

View File

@@ -253,6 +253,10 @@ Das Dateiformat bleibt Version 1.
## Kompilat: `.tbc` (neu, eigenes Format) ## Kompilat: `.tbc` (neu, eigenes Format)
Container für TBVM-Bytecode, Entwurf in [tbvm-design.md](tbvm-design.md). Version 4 ist ein eigenständig ausführbarer Bytecode-Container; das vollständige
`tbc build` erzeugt wahlweise `.tbc` oder ein eigenständiges Executable Format steht in [tbvm-design.md](tbvm-design.md). `tbc build app.mak` erzeugt
(Runner + eingebettetes `.tbc`). `app.tbc`, `tbc run app.tbc` führt es ohne BAS-/FRM-/MAK-/Include-Quellen aus.
Quellorte, Prozedursignaturen und Formular-Anfangswerte einschließlich
Designzeit-Control-Arrays bleiben erhalten. Ältere Versionen 13 und unbekannte
Versionen werden mit Versionsangabe abgewiesen. Native Executable-Verpackung
ist noch nicht implementiert.

View File

@@ -31,6 +31,21 @@ einschließlich breiter Zeichen und Bildschirmränder. Die erneute Verifikation
hat keine offenen Befunde im Change: 428 Workspace-Tests und alle vier hat keine offenen Befunde im Change: 428 Workspace-Tests und alle vier
Review-Proben bestehen; eine Bildschirmmatrix deckt 3.360 Kombinationen ab. Review-Proben bestehen; eine Bildschirmmatrix deckt 3.360 Kombinationen ab.
## Umsetzungsstand: Projektmodule und Kompilat
F09, F10, F11 und F24 sind im archivierten Change
[projektmodule-und-kompilat](../../openspec/changes/archive/2026-09-05-projektmodule-und-kompilat/proposal.md)
umgesetzt. Die zunächst sechs offenen Reviewbefunde und alle bei der
Nachprüfung zusätzlich reproduzierten Abweichungen sind behoben.
Die [abschließende Verifikation](../../openspec/changes/archive/2026-09-05-projektmodule-und-kompilat/review.md)
hat keine offenen Befunde im Change: 453 Workspace-Tests, alle sieben
unveränderten Reviewproben, Clippy, Format-/Spec-Prüfung und das echte
Mehrmodul-Compile-Budget bestehen. Alle zwölf Tasks sind abgeschlossen;
Die Hauptspecs wurden synchronisiert und der Change am 05.09.2026
archiviert. Details und
Szenariozuordnung: [Implementierungsnachweis](../../openspec/changes/archive/2026-09-05-projektmodule-und-kompilat/verification.md).
Die folgenden Befundtexte bleiben die historische Bestandsaufnahme.
## Prüfmethode und Grenzen ## Prüfmethode und Grenzen
- Alle Requirements/Szenarien gelesen und gegen Einstiegspunkte, Implementierung und vorhandene Tests abgeglichen. Die Matrix unten enthält jeden Requirement-Titel einmal. - Alle Requirements/Szenarien gelesen und gegen Einstiegspunkte, Implementierung und vorhandene Tests abgeglichen. Die Matrix unten enthält jeden Requirement-Titel einmal.
@@ -113,7 +128,7 @@ Zwei Formulare mit je Text1 lassen sich laden, aber Form2!Text1.Text erzeugt Con
Beleg: [crates/tb-frontend/src/sema.rs:949](../../crates/tb-frontend/src/sema.rs#L949). Vertrag: [openspec/specs/forms-objektmodell/spec.md:14](../../openspec/specs/forms-objektmodell/spec.md#L14), [openspec/specs/forms-objektmodell/spec.md:36](../../openspec/specs/forms-objektmodell/spec.md#L36), [openspec/specs/sprach-frontend/spec.md:251](../../openspec/specs/sprach-frontend/spec.md#L251). Beleg: [crates/tb-frontend/src/sema.rs:949](../../crates/tb-frontend/src/sema.rs#L949). Vertrag: [openspec/specs/forms-objektmodell/spec.md:14](../../openspec/specs/forms-objektmodell/spec.md#L14), [openspec/specs/forms-objektmodell/spec.md:36](../../openspec/specs/forms-objektmodell/spec.md#L36), [openspec/specs/sprach-frontend/spec.md:251](../../openspec/specs/sprach-frontend/spec.md#L251).
Umsetzung: [projektmodule-und-kompilat](../../openspec/changes/projektmodule-und-kompilat/proposal.md). Umsetzung: [projektmodule-und-kompilat](../../openspec/changes/archive/2026-09-05-projektmodule-und-kompilat/proposal.md).
### F10 — Quellorte und Debuggerziele verlieren die Modulidentität (hoch) ### F10 — Quellorte und Debuggerziele verlieren die Modulidentität (hoch)
@@ -121,7 +136,7 @@ Ein Overflow in lib.bas:2 wird über app.mak als Overflow in line 5 ohne Modulda
Beleg: [crates/tb-cli/src/main.rs:184](../../crates/tb-cli/src/main.rs#L184). Vertrag: [openspec/specs/bytecode-kompilat/spec.md:46](../../openspec/specs/bytecode-kompilat/spec.md#L46), [openspec/specs/sprach-frontend/spec.md:96](../../openspec/specs/sprach-frontend/spec.md#L96), [openspec/specs/vm-ausfuehrung/spec.md:89](../../openspec/specs/vm-ausfuehrung/spec.md#L89), [openspec/specs/vm-fehlerbehandlung/spec.md:11](../../openspec/specs/vm-fehlerbehandlung/spec.md#L11). Beleg: [crates/tb-cli/src/main.rs:184](../../crates/tb-cli/src/main.rs#L184). Vertrag: [openspec/specs/bytecode-kompilat/spec.md:46](../../openspec/specs/bytecode-kompilat/spec.md#L46), [openspec/specs/sprach-frontend/spec.md:96](../../openspec/specs/sprach-frontend/spec.md#L96), [openspec/specs/vm-ausfuehrung/spec.md:89](../../openspec/specs/vm-ausfuehrung/spec.md#L89), [openspec/specs/vm-fehlerbehandlung/spec.md:11](../../openspec/specs/vm-fehlerbehandlung/spec.md#L11).
Umsetzung: [projektmodule-und-kompilat](../../openspec/changes/projektmodule-und-kompilat/proposal.md). Umsetzung: [projektmodule-und-kompilat](../../openspec/changes/archive/2026-09-05-projektmodule-und-kompilat/proposal.md).
### F11 — Formularwerte fehlen im serialisierten Kompilat (hoch) ### F11 — Formularwerte fehlen im serialisierten Kompilat (hoch)
@@ -129,7 +144,7 @@ Mit FormFile.apply liest die VM den initialen Text hello; nach Serialisierung un
Beleg: [crates/tb-vm/src/bytecode.rs:484](../../crates/tb-vm/src/bytecode.rs#L484). Vertrag: [openspec/specs/bytecode-kompilat/spec.md:28](../../openspec/specs/bytecode-kompilat/spec.md#L28). Beleg: [crates/tb-vm/src/bytecode.rs:484](../../crates/tb-vm/src/bytecode.rs#L484). Vertrag: [openspec/specs/bytecode-kompilat/spec.md:28](../../openspec/specs/bytecode-kompilat/spec.md#L28).
Umsetzung: [projektmodule-und-kompilat](../../openspec/changes/projektmodule-und-kompilat/proposal.md). Umsetzung: [projektmodule-und-kompilat](../../openspec/changes/archive/2026-09-05-projektmodule-und-kompilat/proposal.md).
### F12 — ListIndex = -1 wird abgewiesen (mittel) ### F12 — ListIndex = -1 wird abgewiesen (mittel)
@@ -233,7 +248,7 @@ Die Hauptspec verlangt separate CODE/LINES-Abschnitte und vollständige Prozedur
Beleg: [docs/tbvm-design.md:69](../../docs/tbvm-design.md#L69). Vertrag: [openspec/specs/bytecode-kompilat/spec.md:12](../../openspec/specs/bytecode-kompilat/spec.md#L12), [openspec/specs/bytecode-kompilat/spec.md:28](../../openspec/specs/bytecode-kompilat/spec.md#L28). Beleg: [docs/tbvm-design.md:69](../../docs/tbvm-design.md#L69). Vertrag: [openspec/specs/bytecode-kompilat/spec.md:12](../../openspec/specs/bytecode-kompilat/spec.md#L12), [openspec/specs/bytecode-kompilat/spec.md:28](../../openspec/specs/bytecode-kompilat/spec.md#L28).
Umsetzung: [projektmodule-und-kompilat](../../openspec/changes/projektmodule-und-kompilat/proposal.md). Umsetzung: [projektmodule-und-kompilat](../../openspec/changes/archive/2026-09-05-projektmodule-und-kompilat/proposal.md).
### F25 — FRM-Schreibvertrag enthält zwei unvereinbare Universalregeln (Klärung) ### F25 — FRM-Schreibvertrag enthält zwei unvereinbare Universalregeln (Klärung)
@@ -250,7 +265,7 @@ Umsetzung: [spezifikationsabgleich-und-regressionsnachweise](../../openspec/chan
| 1 | [isam-transaktionen-und-dateinummern](../../openspec/changes/archive/2026-09-05-isam-transaktionen-und-dateinummern/proposal.md) | F01, F02, F03 | Eigenständig; bei gemeinsamen Dateien sequenziell integrieren | | 1 | [isam-transaktionen-und-dateinummern](../../openspec/changes/archive/2026-09-05-isam-transaktionen-und-dateinummern/proposal.md) | F01, F02, F03 | Eigenständig; bei gemeinsamen Dateien sequenziell integrieren |
| 2 | [ereigniszustellung-und-hostgrenzen](../../openspec/changes/archive/2026-09-05-ereigniszustellung-und-hostgrenzen/proposal.md) | F04, F05, F06, F07, F08, F18, F20 | Eigenständig; bei gemeinsamen Dateien sequenziell integrieren | | 2 | [ereigniszustellung-und-hostgrenzen](../../openspec/changes/archive/2026-09-05-ereigniszustellung-und-hostgrenzen/proposal.md) | F04, F05, F06, F07, F08, F18, F20 | Eigenständig; bei gemeinsamen Dateien sequenziell integrieren |
| 3 | [forms-zustand-und-bedienung](../../openspec/changes/archive/2026-09-05-forms-zustand-und-bedienung/proposal.md) | F12, F13 | Nach Ereigniszustellung | | 3 | [forms-zustand-und-bedienung](../../openspec/changes/archive/2026-09-05-forms-zustand-und-bedienung/proposal.md) | F12, F13 | Nach Ereigniszustellung |
| 4 | [projektmodule-und-kompilat](../../openspec/changes/projektmodule-und-kompilat/proposal.md) | F09, F10, F11, F24 | Eigenständig; bei gemeinsamen Dateien sequenziell integrieren | | 4 | [projektmodule-und-kompilat](../../openspec/changes/archive/2026-09-05-projektmodule-und-kompilat/proposal.md) | F09, F10, F11, F24 | Eigenständig; bei gemeinsamen Dateien sequenziell integrieren |
| 5 | [laufzeit-eingabe-und-systemstatus](../../openspec/changes/laufzeit-eingabe-und-systemstatus/proposal.md) | F14, F15, F16, F17, F19 | Eigenständig; bei gemeinsamen Dateien sequenziell integrieren | | 5 | [laufzeit-eingabe-und-systemstatus](../../openspec/changes/laufzeit-eingabe-und-systemstatus/proposal.md) | F14, F15, F16, F17, F19 | Eigenständig; bei gemeinsamen Dateien sequenziell integrieren |
| 6 | [spezifikationsabgleich-und-regressionsnachweise](../../openspec/changes/spezifikationsabgleich-und-regressionsnachweise/proposal.md) | F21, F22, F23, F25 | Abschließende Gesamtabnahme nach den fünf Korrektur-Changes | | 6 | [spezifikationsabgleich-und-regressionsnachweise](../../openspec/changes/spezifikationsabgleich-und-regressionsnachweise/proposal.md) | F21, F22, F23, F25 | Abschließende Gesamtabnahme nach den fünf Korrektur-Changes |

View File

@@ -126,33 +126,100 @@ Der erste Befehl lädt ein FRM-Korpusprogramm und prüft dessen `KM`-Ausgabe
mit CaptureHost. Der zweite Baum darf crossterm, ratatui und signal-hook mit CaptureHost. Der zweite Baum darf crossterm, ratatui und signal-hook
nicht enthalten. nicht enthalten.
## `.tbc`-Container (Stand Phase 2, Formatversion 1) ## `.tbc`-Container (Formatversion 4, 2026-09-05)
``` `tbc build app.mak` erzeugt `app.tbc`; `tbc run app.tbc` benötigt die
Magic "TBC\0" · Formatversion u16 · Flags u16 · Abschnittsanzahl u32 BAS-/FRM-/MAK-/Include-Quellen nicht mehr. Dieselbe VM führt Quellprogramme
Abschnittstabelle: [ (Kennung 4 Byte, Offset u32, Länge u32) ] und geladene Kompilate aus. Versionen 13 und unbekannte Versionen werden
Abschnitte: mit Angabe der gefundenen und unterstützten Version abgewiesen.
MODN Modulname + OPTION BASE
CONS deduplizierter Stringpool
TYPS TYPE-Layouts (Feld-Initialisierungstypen)
GLOB globale Slots (Initialisierungstyp + Name für Debugger)
PROC Prozedurtabelle: Name, Parameteranzahl, Frame-Slots, Code
(Zeileninfo liegt inline im Code: Stmt-/SetErl-Instruktionen —
ersetzt den früher geplanten separaten LINES-Abschnitt)
DATA DATA-Konstanten (Rohtext + Quellzeile)
JMPT Sprungtabellen für ON n GOTO/GOSUB
FORMS (ab Phase 4: serialisierte .FRM-Beschreibungen)
```
Serialisierung mit einfachem eigenem Writer/Reader (kein serde nötig, Alle Zahlen sind little-endian, Ganzzahlen mit Vorzeichen im Zweierkomplement,
Format bleibt stabil und dokumentiert); unbekannte Formatversionen `f32`/`f64` nach IEEE 754. `bool` ist genau ein Byte (0/1). `string` bedeutet
werden mit Meldung abgewiesen. Instruktions-Encoding: 1 Opcode-Byte + `u32` UTF-8-Bytelänge, gefolgt von genau diesen Bytes ohne Abschlussnull.
Operanden little-endian, Opcode-Bytes gruppenweise mit Lücken vergeben Listen beginnen mit `u32` Elementanzahl; `[...]` bezeichnet die folgenden
(`crates/tb-vm/src/bytecode.rs` ist die normative Liste). wiederholten Elemente. Tabellen-IDs sind nullbasiert.
Der Header besteht aus `TBC\0` (4 Byte), Version `u16 = 4`, Flags `u16 = 0`,
Abschnittsanzahl `u32 = 9`. Darauf folgen neun Einträge mit Kennung (4 ASCII-Bytes),
absolutem Datei-Offset `u32` und Bytelänge `u32`. Der Writer schreibt die
folgenden Abschnitte in dieser Reihenfolge; der Reader findet sie über die Tabelle.
| Kennung | Payload in Reihenfolge |
|---|---|
| `MODN` | Projektname `string`, Vorgabe-OPTION-BASE `u8` (wie Modul 0) |
| `SRCS` | Modulanzahl `u32`, `[Name string, OPTION BASE u8 (0/1)]`; Quellenanzahl `u32`, `[Modul-ID u16, Dateipfad string]` |
| `CONS` | Anzahl `u32`, `[string]`; Stringkonstantenpool |
| `TYPS` | Anzahl `u32`, `[TYPE-Name string, Feldanzahl u32, [TypeInit]]` |
| `GLOB` | Slotanzahl `u32`, `[TypeInit, Debuggername string]` |
| `PROC` | Prozeduranzahl `u32`, `[Prozedurbeschreibung gemäß unten]` |
| `DATA` | Anzahl `u32`, `[Rohtext string, physische Zeile u32]` |
| `JMPT` | Tabellenanzahl `u32`, `[Zielanzahl u32, [Instruktionsindex u32]]` |
| `OBJS` | Objekt-, Ereignis- und Anfangsdaten gemäß unten |
Eine `PROC`-Beschreibung enthält: Name `string`, Modul-ID `u16`, Art `u8`
(0 Hauptprogramm, 1 SUB, 2 FUNCTION, 3 DEF FN), Parameteranzahl `u32`,
`[Parametername string, HTy, Array bool, BYREF bool]`, Rückgabetyp-vorhanden
`bool`, gegebenenfalls `HTy`, Aufrufparameteranzahl `u16`, Local-Anzahl `u32`,
`[TypeInit, Local-Name string]`, Instruktionsanzahl `u32`, Code-Bytelänge `u32`,
Code-Bytes. Die beiden Parameteranzahlen müssen übereinstimmen. Prozedur 0
enthält die zusammengeführten Modulrümpfe; weitere Prozeduren behalten ihre
Modul-ID. In Mehrmodulprojekten sind Prozedur-/globale Debuggernamen
`Modul!Name` mit dem aufgelösten Typ-Suffix bei implizit/suffixdeklarierten
Variablen. AS-deklarierte Namen bleiben suffixlos. Die Inspektion unterscheidet
explizite Suffixe; ohne Suffix muss der Basisname eindeutig sein. Aufrufe und
Variablenzugriffe verwenden ausschließlich IDs.
`TypeInit` belegt immer 5 Byte: Tag `u8` und Zusatz `u32`. Tags: 0 INTEGER,
1 LONG, 2 SINGLE, 3 DOUBLE, 4 CURRENCY, 5 STRING, 6 fester STRING (Zusatz:
Länge), 7 UDT (Zusatz: TYPE-ID, maximal 65535), 8 leerer Slot. Sonst ist der
Zusatz 0. UDT-Felder dürfen nur frühere TYPE-IDs referenzieren.
`HTy` ist ein `u8`-Tag mit denselben skalaren Tags 07; nur bei Tag 6 folgen
`u32` Länge und bei Tag 7 `u16` TYPE-ID. Tag 8 bezeichnet FORM, Tag 9 CONTROL.
Arrays werden separat im Parametersatz markiert; ihr Local-Slot beginnt leer.
`OBJS` enthält Objektanzahl `u32`, `[Name string, Klasse u8, Elternname string
(leer bei Wurzel), Array bool, Eltern-ID u16 (FFFF = keine)]`, dann
Ereignisanzahl `u32`, `[Objekt-ID u16, Ereignisname string, Prozedur-ID u16]`,
Startformular-ID `u16` (FFFF = keines), Anzahl Anfangsinstanzen `u32`,
`[Objekt-ID u16, Designindex i32, Eigenschaftsanzahl u32,
[Eigenschafts-ID u16, PropertyValue]]`. Eltern-IDs zeigen auf frühere Objekte;
gleichnamige Controls anderer Formulare bleiben dadurch getrennt. Auch
Designinstanzen mit Index ungleich 0 und sämtliche FRM-Anfangswerte sind enthalten.
Eigenschaften stehen in aufsteigender ID-Reihenfolge.
`PropertyValue` beginnt mit Tag `u8`: 0 Integer (`i32`), 1 Single (`f32`),
2 String (`string`), 3 Boolean (`bool`), 4 Objekt (`bool` vorhanden, falls ja
`u16` Objekt-ID und `bool` Index-vorhanden, gegebenenfalls `i32` Index),
5 Integer-Array (`u32` Anzahl, `[i32]`). Klassen-IDs 018 entsprechen in
Reihenfolge Form, CheckBox, ComboBox, CommandButton, DirListBox, DriveListBox,
FileListBox, Frame, HScrollBar, Label, ListBox, Menu, OptionButton, PictureBox,
TextBox, Timer, VScrollBar, Screen, Spin. Eigenschafts-/Methoden-IDs sind
Indizes der jeweiligen Klassentabellen in `tb-frontend/src/forms.rs`.
Code und Quellorte liegen inline in `PROC`; es gibt keine `CODE`-/`LINES`-
oder `FORMS`-Abschnitte. `Source(Datei-ID, Spalte)` steht unmittelbar vor
`Stmt(physische Zeile)`. Die VM liest diese Zuordnung auch bei einem Sprung
direkt auf `Stmt`. Globale DIM-Anweisungen tragen `InitStmt(physische Zeile)`: Fehlerorte und
Debugger bleiben aktiv, Ereignisse werden bis nach den Initialisierungen
zurückgestellt. Zeile 0 ist eine synthetische Initialisierungsgrenze.
`SetErl(BASIC-Label)` aktualisiert getrennt davon die numerische Zeilennummer
für ERL. Include-Dateien behalten ihren Pfad und physischen Quellort sowie
die ID ihres einbindenden Moduls. `add_module_breakpoint(module, line)` und
`set_step(true)` halten an diesen Grenzen; `current_module`, `current_file`
und `current_source_pos` liefern den Halt-/Fehlerort. `inspect` akzeptiert
lokale, im aktuellen Modul sichtbare und explizit qualifizierte Variablennamen.
Der Reader prüft Version, Flags, eindeutige bekannte Abschnitte,
Längen/Überlappungen, vollständige Payloads, Typ-/Objekt-/Prozedur-/Quell-IDs,
Sprungziele und Anfangseigenschaften vor der Ausführung. Beschädigte Daten
liefern einen Ladefehler. Das Format ist kein Sandbox-Format für fremden
Programmcode; ein vollständiger Stack-/Kontrollflussverifizierer ist nicht Teil
von TBC. Ein gültiges vom Writer erzeugtes Kompilat lässt sich bytegleich
laden und erneut serialisieren.
## Eigenständige Executables ## Eigenständige Executables
`tbc build --exe` kopiert den vorkompilierten Runner (dieselbe Geplant, noch nicht implementiert: `tbc build --exe` kopiert den vorkompilierten Runner (dieselbe
tb-vm/tb-runtime/tb-ui-Bibliothek wie die IDE) und hängt das `.tbc` als tb-vm/tb-runtime/tb-ui-Bibliothek wie die IDE) und hängt das `.tbc` als
Ressource an (Anhängen ans Binary + Fußzeile mit Offset/Magic; portabel für Ressource an (Anhängen ans Binary + Fußzeile mit Offset/Magic; portabel für
alle drei Plattformen). Alternative — `include_bytes!` + Cargo-Build beim alle drei Plattformen). Alternative — `include_bytes!` + Cargo-Build beim
@@ -165,9 +232,19 @@ Nutzer — verworfen: erfordert Rust-Toolchain beim Anwender.
Vorwärtsreferenzen (Prozeduren, Labels) über Fixup-Listen statt zweitem Vorwärtsreferenzen (Prozeduren, Labels) über Fixup-Listen statt zweitem
Pass. Die Sprache ist dafür gemacht — das Vorbild kompilierte auf Pass. Die Sprache ist dafür gemacht — das Vorbild kompilierte auf
1992er-Hardware gefühlt sofort. 1992er-Hardware gefühlt sofort.
- Modulweise inkrementell: `.BAS`/`.FRM` werden unabhängig zu Bytecode- - `.BAS`/`.FRM` werden getrennt geparst, semantisch aufgelöst und zu Bytecode-
Einheiten übersetzt und beim Build nur zusammengebunden; die IDE Einheiten übersetzt. Der Projektlink versetzt Slots, Prozeduren, Typen,
recompiliert nur geänderte Module (Hash über Quelltext). Strings, DATA und Sprungtabellen. Modulrümpfe laufen in Projektreihenfolge;
ihre globalen Initialisierungen liegen vor dem ersten Ereignis. Eindeutige
externe Prozeduren werden mit dem DEFtype-Kontext ihres Ursprungs importiert;
prozedurlokale DEFtype-Angaben beeinflussen keine anderen Prozeduren.
Importierte Konstanten werden im Ursprung gefaltet, auch bei transitiven
Modulabhängigkeiten. TYPE-Abhängigkeiten behalten ihr ursprüngliches Layout;
interne qualifizierte Typnamen verhindern Konflikte mit abweichenden lokalen
Definitionen. Echte lokale Duplikate werden diagnostiziert. SHARED
bleibt modullokal. COMMON-Variablen desselben Blocks und derselben vollständigen
Variablenidentität teilen einen Slot; Elementtyp, Rang und bekannte Grenzen
müssen kompatibel sein. Ein inkrementeller IDE-Cache ist weiterhin geplant.
- Keine Optimierungspasses. Erlaubt sind nur Gratis-Optimierungen im - Keine Optimierungspasses. Erlaubt sind nur Gratis-Optimierungen im
Codegen (Konstantenfaltung im Ausdruck, Peephole beim Emit). Codegen (Konstantenfaltung im Ausdruck, Peephole beim Emit).
- Budget als Test verankern (Phase 2): Benchmark-Projekt (~50k Zeilen) - Budget als Test verankern (Phase 2): Benchmark-Projekt (~50k Zeilen)
@@ -292,7 +369,7 @@ via `CINT`/`CLNG`/`CSNG`/`CDBL`/`CCUR` — identische Semantik):
`-` voran und hängt stets ein Leerzeichen an; `STR$` nur das führende `-` voran und hängt stets ein Leerzeichen an; `STR$` nur das führende
Leerzeichen/`-`, kein nachgestelltes. Leerzeichen/`-`, kein nachgestelltes.
## Opcode-Satz (Feindesign, 2026-09-02) ## Opcode-Satz (Formatversion 4, 2026-09-05)
In-Memory führt die VM dekodierte Instruktionen (`Vec<Instr>`, ein Rust- In-Memory führt die VM dekodierte Instruktionen (`Vec<Instr>`, ein Rust-
Enum mit eingebetteten Operanden — Wort-Dispatch, keine Byte-Dekodierung Enum mit eingebetteten Operanden — Wort-Dispatch, keine Byte-Dekodierung
@@ -300,26 +377,205 @@ im heißen Pfad); die `.tbc`-Serialisierung bildet jede Instruktion auf
1 Opcode-Byte + Operanden (little-endian) ab. Typkürzel: `I2`=INTEGER, 1 Opcode-Byte + Operanden (little-endian) ab. Typkürzel: `I2`=INTEGER,
`I4`=LONG, `R4`=SINGLE, `R8`=DOUBLE, `CY`=CURRENCY, `STR`=STRING. `I4`=LONG, `R4`=SINGLE, `R8`=DOUBLE, `CY`=CURRENCY, `STR`=STRING.
| Gruppe | Instruktionen | Bemerkung | Die folgende Tabelle führt jeden serialisierbaren Opcode einzeln auf;
|---|---|---| Gesamtbytes schließen das Opcode-Byte ein. `bool`, `TypeInit` und primitive
| Anweisungsgrenze | `Stmt(line:u32)` · `SetErl(n:u32)` | `Stmt` prüft das Tick-Flag-Wort (Breakpoint/Einzelschritt/Abbruch), aktualisiert Zeile und Resume-Punkt; `SetErl` bei numerischen Zeilennummern | Breiten sind im Containerabschnitt definiert. `CmpOp` belegt ein Byte:
| Konstanten/Stack | `PushInt(i16)` `PushLng(i32)` `PushSng(f32)` `PushDbl(f64)` `PushCur(i64)` `PushStr(pool:u16)` · `Dup` `Pop` | Stringpool dedupliziert | 0 gleich, 1 ungleich, 2 kleiner, 3 kleiner/gleich, 4 größer, 5 größer/gleich.
| Variablen | `LoadGlobal/StoreGlobal(u16)` · `LoadLocal/StoreLocal(u16)` · `MakeRefGlobal/MakeRefLocal(u16)` · `LoadRef/StoreRef(u16)` | Slots statisch aufgelöst; `*Ref` bedienen BYREF-Parameter (Referenz-Werte) | Nicht aufgeführte Opcode-Bytes sind ungültig.
| Arrays | `DimGlobal/DimLocal{slot,dims,elem}` `RedimGlobal/RedimLocal{…}` `EraseGlobal/EraseLocal(u16)` · `LoadElem/StoreElem{dims:u8}` · `MakeRefElem{dims:u8}` | Grenzen auf dem Stack (lo/hi je Dimension als LONG); Indexprüfung → Fehler 9 |
| UDT | `LoadField/StoreField(u16)` · `MakeRefField(u16)` | Feldindex aus Typtabelle; verschachtelt durch Verkettung | | Byte | Instruktion | Operanden in Reihenfolge | Gesamtbytes |
| Arithmetik | `Add/Sub/Mul{I2,I4,R4,R8,CY}` · `Neg{…}` · `Div{R4,R8}` · `IDiv/Mod{I2,I4}` · `PowR8` (`^` rechnet in DOUBLE, SINGLE-Ergebnis per Conv) | monomorph; Ganzzahl-/CY-Überlauf → Fehler 6, Division durch 0 → Fehler 11 | |---|---|---|---|
| Konvertierung | `Conv_<src>_<dst>` für alle 20 geordneten Paare aus {I2,I4,R4,R8,CY} | Semantik exakt nach Matrix (Rundung/Überlauf) | | `0x00` | `Stmt` | line: u32 | 5 |
| Logik | `Not/And/Or/Xor/Eqv/Imp{I2,I4}` | bitweise | | `0x01` | `SetErl` | label: u32 | 5 |
| Vergleich | `Cmp{Eq,Ne,Lt,Le,Gt,Ge}{I2,I4,R4,R8,CY,STR}` | Ergebnis INTEGER 1/0 | | `0x02` | `End` | — | 1 |
| Strings | `Concat` | `Rc<str>`, Kopien sind Pointer-Kopien | | `0x03` | `StopInstr` | — | 1 |
| Kontrollfluss | `Jump(u32)` `JumpIfFalse(u32)` `JumpIfTrue(u32)` | Ziele absolut (Instruktionsindex) innerhalb der Code-Einheit; Fixups beim Emit | | `0x04` | `SystemInstr` | — | 1 |
| GOSUB | `Gosub(u32)` `RetGosub` `RetGosubTo(u32)` | GOSUB-Stack pro Frame; leer → Fehler 3 | | `0x05` | `Unsupported` | name: u16 | 3 |
| Berechnete Sprünge | `OnJump{table:u16,gosub:bool}` | Sprungtabellen im Modul; 0/zu groß: kein Sprung; negativ/>255 → Fehler 5 | | `0x06` | `Source` | source: u32, column: u32 | 9 |
| Prozeduren | `Call(proc:u16)` `RetProc` `RetFn` `CallBuiltin{id:u16,argc:u8}` | Argumente links→rechts auf dem Stack (Werte oder Referenzen); `RetFn` transportiert den Funktionswert über den Frame-Abbau | | `0x07` | `InitStmt` | line: u32 | 5 |
| Fehler | `OnErrorGoto(u32)` `OnErrorLocal(u32)` `OnErrorDisable` `OnErrorLocalDisable` · `Resume0` `ResumeNext` `ResumeLabel(u32)` · `RaiseError` | Scoping siehe unten | | `0x10` | `PushInt` | value: i16 | 3 |
| DATA | `ReadData(art)` `Restore(u32)` | art 0 = String, 1 = Zahl (DOUBLE, dann Conv nach Matrix); Ende → Fehler 4, unkonvertierbar → Fehler 13 | | `0x11` | `PushLng` | value: i32 | 5 |
| E/A | `Input{…}` sowie Builtins (`PRINT`-Familie über `CallBuiltin`) | Konsolenwirkung ausschließlich über das `Host`-Trait | | `0x12` | `PushSng` | value: f32 | 5 |
| Ende | `End` `StopInstr` `SystemInstr` | Verhalten siehe Runner | | `0x13` | `PushDbl` | value: f64 | 9 |
| `0x14` | `PushCur` | value: i64 | 9 |
| `0x15` | `PushStr` | pool: u16 | 3 |
| `0x16` | `Dup` | — | 1 |
| `0x17` | `Pop` | — | 1 |
| `0x18` | `PushUdtId` | type: u16 | 3 |
| `0x20` | `LoadGlobal` | slot: u16 | 3 |
| `0x21` | `StoreGlobal` | slot: u16 | 3 |
| `0x22` | `LoadLocal` | slot: u16 | 3 |
| `0x23` | `StoreLocal` | slot: u16 | 3 |
| `0x24` | `LoadRef` | slot: u16 | 3 |
| `0x25` | `StoreRef` | slot: u16 | 3 |
| `0x26` | `MakeRefGlobal` | slot: u16 | 3 |
| `0x27` | `MakeRefLocal` | slot: u16 | 3 |
| `0x28` | `MakeRefElem` | dimensions: u8 | 2 |
| `0x29` | `MakeRefField` | field: u16 | 3 |
| `0x30` | `LoadArr` | global: bool, slot: u16, dimensions: u8, element: TypeInit | 10 |
| `0x31` | `LoadElem` | dimensions: u8 | 2 |
| `0x32` | `StoreElem` | dimensions: u8 | 2 |
| `0x33` | `DimArr` | global: bool, slot: u16, dimensions: u8, element: TypeInit | 10 |
| `0x34` | `RedimArr` | global: bool, slot: u16, dimensions: u8, element: TypeInit | 10 |
| `0x35` | `EraseSlot` | global: bool, slot: u16 | 4 |
| `0x36` | `LoadField` | field: u16 | 3 |
| `0x37` | `StoreField` | field: u16 | 3 |
| `0x38` | `CopyRec` | — | 1 |
| `0x39` | `ArrBound` | lower: bool | 2 |
| `0x3A` | `FixStr` | length: u32 | 5 |
| `0x3B` | `CommonArr` | global: bool, slot: u16, dimensions: u8, element: TypeInit | 10 |
| `0x40` | `AddI2` | — | 1 |
| `0x41` | `AddI4` | — | 1 |
| `0x42` | `AddR4` | — | 1 |
| `0x43` | `AddR8` | — | 1 |
| `0x44` | `AddCy` | — | 1 |
| `0x45` | `SubI2` | — | 1 |
| `0x46` | `SubI4` | — | 1 |
| `0x47` | `SubR4` | — | 1 |
| `0x48` | `SubR8` | — | 1 |
| `0x49` | `SubCy` | — | 1 |
| `0x4A` | `MulI2` | — | 1 |
| `0x4B` | `MulI4` | — | 1 |
| `0x4C` | `MulR4` | — | 1 |
| `0x4D` | `MulR8` | — | 1 |
| `0x4E` | `MulCy` | — | 1 |
| `0x4F` | `NegI2` | — | 1 |
| `0x50` | `NegI4` | — | 1 |
| `0x51` | `NegR4` | — | 1 |
| `0x52` | `NegR8` | — | 1 |
| `0x53` | `NegCy` | — | 1 |
| `0x54` | `DivR4` | — | 1 |
| `0x55` | `DivR8` | — | 1 |
| `0x56` | `IDivI2` | — | 1 |
| `0x57` | `IDivI4` | — | 1 |
| `0x58` | `ModI2` | — | 1 |
| `0x59` | `ModI4` | — | 1 |
| `0x5A` | `PowR8` | — | 1 |
| `0x5B` | `Concat` | — | 1 |
| `0x60` | `ConvI2I4` | — | 1 |
| `0x61` | `ConvI2R4` | — | 1 |
| `0x62` | `ConvI2R8` | — | 1 |
| `0x63` | `ConvI2Cy` | — | 1 |
| `0x64` | `ConvI4I2` | — | 1 |
| `0x65` | `ConvI4R4` | — | 1 |
| `0x66` | `ConvI4R8` | — | 1 |
| `0x67` | `ConvI4Cy` | — | 1 |
| `0x68` | `ConvR4I2` | — | 1 |
| `0x69` | `ConvR4I4` | — | 1 |
| `0x6A` | `ConvR4R8` | — | 1 |
| `0x6B` | `ConvR4Cy` | — | 1 |
| `0x6C` | `ConvR8I2` | — | 1 |
| `0x6D` | `ConvR8I4` | — | 1 |
| `0x6E` | `ConvR8R4` | — | 1 |
| `0x6F` | `ConvR8Cy` | — | 1 |
| `0x70` | `ConvCyI2` | — | 1 |
| `0x71` | `ConvCyI4` | — | 1 |
| `0x72` | `ConvCyR4` | — | 1 |
| `0x73` | `ConvCyR8` | — | 1 |
| `0x80` | `NotI2` | — | 1 |
| `0x81` | `NotI4` | — | 1 |
| `0x82` | `AndI2` | — | 1 |
| `0x83` | `AndI4` | — | 1 |
| `0x84` | `OrI2` | — | 1 |
| `0x85` | `OrI4` | — | 1 |
| `0x86` | `XorI2` | — | 1 |
| `0x87` | `XorI4` | — | 1 |
| `0x88` | `EqvI2` | — | 1 |
| `0x89` | `EqvI4` | — | 1 |
| `0x8A` | `ImpI2` | — | 1 |
| `0x8B` | `ImpI4` | — | 1 |
| `0x90` | `CmpI2` | compare: CmpOp | 2 |
| `0x91` | `CmpI4` | compare: CmpOp | 2 |
| `0x92` | `CmpR4` | compare: CmpOp | 2 |
| `0x93` | `CmpR8` | compare: CmpOp | 2 |
| `0x94` | `CmpCy` | compare: CmpOp | 2 |
| `0x95` | `CmpStr` | compare: CmpOp | 2 |
| `0xA0` | `Jump` | target: u32 | 5 |
| `0xA1` | `JumpIfFalse` | target: u32 | 5 |
| `0xA2` | `JumpIfTrue` | target: u32 | 5 |
| `0xA3` | `Gosub` | target: u32 | 5 |
| `0xA4` | `RetGosub` | — | 1 |
| `0xA5` | `RetGosubTo` | target: u32 | 5 |
| `0xA6` | `OnJump` | table: u16, gosub: bool | 4 |
| `0xA7` | `Run` | kind: u8 | 2 |
| `0xB0` | `Call` | proc: u16, argc: u8 | 4 |
| `0xB1` | `RetProc` | — | 1 |
| `0xB2` | `RetFn` | — | 1 |
| `0xB3` | `CallBuiltin` | builtin: u16, argc: u8 | 4 |
| `0xB4` | `LoadObjectProperty` | object: u16, property: u16, indexed: bool | 6 |
| `0xB5` | `StoreObjectProperty` | object: u16, property: u16, indexed: bool | 6 |
| `0xB6` | `PushObject` | object: u16, indexed: bool | 4 |
| `0xB7` | `TypeOf` | class: u8 | 2 |
| `0xB8` | `ObjectMethod` | object: u16, method: u16, argc_flags: u8 | 6 |
| `0xB9` | `ObjectLoad` | object: u16, unload: bool, indexed: bool | 5 |
| `0xBA` | `LoadDynamicObjectProperty` | name: u16 | 3 |
| `0xBB` | `StoreDynamicObjectProperty` | name: u16 | 3 |
| `0xBC` | `LoadObjectIndexedProperty` | object: u16, property_flags: u16 | 5 |
| `0xBD` | `ObjectMethodFn` | object: u16, method: u16, argc_flags: u8 | 6 |
| `0xBE` | `StoreObjectIndexedProperty` | object: u16, property_flags: u16 | 5 |
| `0xC0` | `OnErrorGoto` | target: u32 | 5 |
| `0xC1` | `OnErrorLocal` | target: u32 | 5 |
| `0xC2` | `OnErrorDisable` | — | 1 |
| `0xC3` | `OnErrorLocalDisable` | — | 1 |
| `0xC4` | `OnErrorResumeNext` | local: bool | 2 |
| `0xC5` | `Resume0` | — | 1 |
| `0xC6` | `ResumeNext` | — | 1 |
| `0xC7` | `ResumeLabel` | target: u32 | 5 |
| `0xC8` | `RaiseError` | — | 1 |
| `0xC9` | `LoadErr` | — | 1 |
| `0xCA` | `LoadErl` | — | 1 |
| `0xCB` | `SetErr` | — | 1 |
| `0xD0` | `ReadData` | kind: u8 | 2 |
| `0xD1` | `Restore` | data: u32 | 5 |
| `0xD2` | `Input` | argc: u8, line_mode: bool, prompt: u16, question: bool | 6 |
| `0xD3` | `InputFile` | argc: u8, line_mode: bool | 3 |
| `0xD4` | `GetPut` | put: bool, has_record: bool, kind: u8, extra: u16 | 6 |
| `0xD5` | `Field` | count: u8 | 2 |
| `0xD6` | `LsetRset` | right: bool | 2 |
| `0xE0` | `TrapDefine` | kind: u8, target: u32 | 6 |
| `0xE1` | `TrapDisable` | kind: u8 | 2 |
| `0xE2` | `TrapSet` | kind: u8, state: u8 | 3 |
| `0xE3` | `EventSwitch` | enabled: bool | 2 |
| `0xE4` | `Doevents` | — | 1 |
| `0xE5` | `Sleep` | has_seconds: bool | 2 |
`target` bezeichnet einen Instruktionsindex innerhalb der aktuellen Prozedur;
`OnErrorGoto` zeigt in Prozedur 0. `table` referenziert `JMPT`, `proc` die
Prozedurtabelle, `pool`/`name`/`prompt` den Stringpool. `global=true` wählt
`GLOB`, sonst einen Local-Slot. Arrayindizes und Dimensionsgrenzen (je lo/hi)
liegen auf dem Stack. `ArrBound(lower)` unterscheidet LBOUND/UBOUND;
`MakeRef*` erzeugt Referenzen, `CopyRec` eine UDT-Wertkopie. `PushUdtId` legt
eine beim Linken versetzte TYPE-ID als LONG für ISAM auf den Stack.
`Call`/`CallBuiltin` konsumieren `argc` Argumente in Quellreihenfolge.
`builtin` ist die feste ID aus `tb-runtime/src/builtins.rs::ids`; `RetFn`
transportiert den Funktionswert. `ObjectMethod`/`ObjectMethodFn` verwenden
klassenlokale Methodenindizes, keine Stringpool-IDs. Das höchste Bit von
`argc_flags` kennzeichnet einen zusätzlichen Control-Arrayindex vor den
Argumenten; die unteren 7 Bit enthalten die Argumentanzahl. Bei indizierten
Eigenschaften markiert Bit 15 von `property_flags` den zusätzlichen
Control-Arrayindex; die unteren 15 Bit sind die Eigenschafts-ID. Der
Eigenschaftsindex liegt anschließend auf dem Stack. `indexed` bedeutet
ansonsten ebenfalls Control-Arrayindex auf dem Stack. Dynamische
Eigenschaftszugriffe verwenden den Namen im Stringpool und einen Objektwert
vom Stack. `TypeOf(class)` verwendet die oben genannten Klassen-IDs.
`TrapDefine`/`TrapDisable`/`TrapSet`: `kind` ist 0 KEY, 1 TIMER, 2 UEVENT,
3 SIGNAL; die Kennung bzw. TIMER-Dauer kommt vom Stack. `state` ist 0 ON,
1 OFF, 2 STOP. `Doevents` legt 0 ab; `Sleep(true)` nimmt Sekunden vom Stack.
`OnErrorResumeNext(local)` wählt Frame- oder Modulhandler. `RaiseError` und
`SetErr` lesen einen Fehlercode vom Stack; `LoadErr`/`LoadErl` legen ihren Wert ab.
`Run(kind)` ist 0 Neustart, 1 numerische Startzeile, 2 Dateiname; bei 1/2
liegt das Ziel auf dem Stack. `ReadData(kind)` ist 0 STRING, 1 DOUBLE.
`Input`/`InputFile` lesen `argc` Referenzen; bei Dateiinput liegt die
Dateinummer darunter. `line_mode` wählt LINE INPUT, `prompt=FFFF` bedeutet
keinen Prompt, `question` ergänzt das Fragezeichen. `GetPut` unterscheidet
GET/PUT und eine optionale Recordnummer auf dem Stack. `kind` ist 0 ohne
Variable, 1 INTEGER, 2 LONG, 3 SINGLE, 4 DOUBLE, 5 CURRENCY, 6 fester STRING,
7 UDT, 8 variabler STRING; `extra` enthält bei 6 die Länge, bei 7 die TYPE-ID,
sonst 0. `Field` konsumiert Dateinummer und `count` Paare (Länge, Referenz).
`LsetRset(right)` konsumiert Referenz und Wert.
FOR/NEXT wird ohne Spezial-Opcodes kompiliert: Grenz-/Schrittwert einmal FOR/NEXT wird ohne Spezial-Opcodes kompiliert: Grenz-/Schrittwert einmal
in versteckte Slots ausgewertet; bei konstantem STEP wählt der Codegen in versteckte Slots ausgewertet; bei konstantem STEP wählt der Codegen
@@ -334,6 +590,8 @@ als Vergleichs-/Sprungkette abgesenkt.
- `ON ERROR GOTO x` setzt den **modulweiten** Handler — auch aus einer - `ON ERROR GOTO x` setzt den **modulweiten** Handler — auch aus einer
Prozedur heraus. `ON LOCAL ERROR GOTO x` setzt einen **Frame-lokalen** Prozedur heraus. `ON LOCAL ERROR GOTO x` setzt einen **Frame-lokalen**
Handler, der den modulweiten für die Dauer des Prozedurlaufs verdeckt. Handler, der den modulweiten für die Dauer des Prozedurlaufs verdeckt.
Jedes Quellmodul besitzt einen eigenen Modulhandler; `GOTO 0` in einer
Bibliothek deaktiviert daher keinen Handler des aufrufenden Moduls.
`… GOTO 0` deaktiviert den jeweiligen Handler. `… GOTO 0` deaktiviert den jeweiligen Handler.
- Fehlerfall: erst die Frame-Kette von innen nach außen nach - Fehlerfall: erst die Frame-Kette von innen nach außen nach
LOCAL-Handlern absuchen, sonst modulweiter Handler; Stack wird bis zum LOCAL-Handlern absuchen, sonst modulweiter Handler; Stack wird bis zum
@@ -365,3 +623,8 @@ denen die Original-Hilfe schweigt, sind am 2026-09-04 als
`tests/compat/konvertierung.bas` ausführbar verankert. Sollte je eine `tests/compat/konvertierung.bas` ausführbar verankert. Sollte je eine
belastbare Fundstelle auftauchen, die ihnen widerspricht, gilt sie — belastbare Fundstelle auftauchen, die ihnen widerspricht, gilt sie —
die Entscheidungen sind Lückenfüller, keine Setzungen gegen das Vorbild. die Entscheidungen sind Lückenfüller, keine Setzungen gegen das Vorbild.
`CommonArr` dimensioniert einen leeren COMMON-Array-Slot einmal. Bei bereits
vorhandenem Array prüft es Elementtyp und die ausgewerteten Grenzen, ohne
Werte oder Handle zu ersetzen (Konflikt: Fehler 13). Statisch erkennbare
COMMON-Typ-/Grenzenkonflikte werden bereits beim Linken diagnostiziert.

View File

@@ -1,6 +1,6 @@
## Why ## Why
Mehrere Module/Formulare werden zu einem Quelltext und globalen Objektraum zusammengelegt. Dadurch scheitern formularlokale Namen, Fehlerorte stimmen nicht und FRM-Anfangswerte fehlen nach dem Kompilat-Rundlauf (F09F11); Containerdokumentation und Spec sind außerdem überholt (F24). Siehe [Review vom 05.09.2026](../../../docs/reviews/2026-09-05-openspec-code-review.md). Mehrere Module/Formulare werden zu einem Quelltext und globalen Objektraum zusammengelegt. Dadurch scheitern formularlokale Namen, Fehlerorte stimmen nicht und FRM-Anfangswerte fehlen nach dem Kompilat-Rundlauf (F09F11); Containerdokumentation und Spec sind außerdem überholt (F24). Siehe [Review vom 05.09.2026](../../../../docs/reviews/2026-09-05-openspec-code-review.md).
## What Changes ## What Changes

View File

@@ -0,0 +1,223 @@
# Verifikation: projektmodule-und-kompilat
Geprüft am 05.09.2026: Arbeitsbaum auf Basis von `58b1f62`, alle sieben
Delta-Requirements mit 21 Szenarien, Tasks, D1D4, Implementierung und
Abnahmenachweis. **Sechs offene Befunde; der Change ist nicht abnahmebereit.**
Die vorherige Aussage vollständiger Umsetzung wird durch diese Verifikation
korrigiert. Der Produktcode wurde bei diesem Review nicht verändert.
| Dimension | Ergebnis |
| --- | --- |
| Vollständigkeit | 7/7 Tasks sind angehakt und alle sieben Requirements haben Implementierungspfade. Die Befunde widerlegen jedoch die vollständige Erfüllung insbesondere von 1.1, 1.2 und 2.2; Checkboxen allein sind kein Abnahmenachweis. |
| Korrektheit | 437 bestehende Workspace-Tests bestanden; zusätzliche unabhängige Review-Proben: 1 bestanden, 6 fehlgeschlagen. Vier Kernfunktionslücken (CRITICAL), zwei weitere Vertragsabweichungen (WARNING). |
| Kohärenz | D4 und die FRM-Grundpfade von D3 sind umgesetzt. Quellorte und Modulkontexte gehen in weiteren Absenkungs-/Importpfaden verloren; der gemeinsame Objektraum berücksichtigt `$FORM` nicht vollständig. |
## CRITICAL — vor Archivierung beheben
### V1 — Quellorte und Breakpoints fehlen an Zweig- und Schleifengrenzen
**Priorität: hoch.** Delta „Zeilentabelle für Fehlerortung“ und
„Unterbrechbare step()-Ausführung“, D1, Tasks 1.1/2.2.
Die Fehlerortung funktioniert bei einfachen Anweisungen, nicht jedoch bei
weiteren ausführbaren Teilen zusammengesetzter Anweisungen:
| Probe | Erwartet | Beobachtet |
| --- | --- | --- |
| `IF 0 THEN` / `PRINT 1` / `ELSEIF 1/0 THEN` | Fehler 11 in Zeile 3 | Zeile 1 |
| `SELECT CASE 1` / `CASE 1/0` | Fehler 11 in Zeile 2 | Zeile 1 |
| `FOR i%=32767 TO 32767` / `PRINT 1` / `NEXT` | Überlauf beim NEXT in Zeile 3 | Zeile 2 |
| `DO` / `PRINT 1` / `LOOP UNTIL 1/0` | Fehler 11 in Zeile 3 | Zeile 2 |
| ELSEIF-Bedingung aus `cond.bi:1` | Include-Datei `cond.bi` | `main.bas:1:1` |
| Breakpoint auf ELSEIF in Zeile 3 | Halt vor Bedingung | Programm endet ohne Halt |
**Ursache:** [ELSEIF-Absenkung](../../../../crates/tb-frontend/src/sema.rs#L1937)
und [CASE-Absenkung](../../../../crates/tb-frontend/src/sema.rs#L2914) verwenden
den Quellort des äußeren Statements. NEXT-/LOOP-Positionen werden nicht
vollständig durch AST/HIR bis zur auszuführenden Grenze getragen. Der
[FOR-Codegenerator](../../../../crates/tb-vm/src/codegen.rs#L737) dokumentiert
selbst die bisherige Verschiebung einer genauen NEXT-Zuordnung auf Phase 5;
das widerspricht der hier übernommenen vollständigen Quellortanforderung.
**Korrektur:** Physische Positionen aller ausführbaren Zweig-/Schleifenteile
bereits beim Parsen erhalten und passende Source-/Stmt-Grenzen ausgeben.
Nicht nur Fehlermeldungen umbenennen: Breakpoints, Einzelschritt und RESUME
müssen dieselbe richtige Zuordnung verwenden. Numerische ERL-Labels separat
lassen. Probe `v1_zweige_und_schleifen_behalten_eigene_quellorte`.
### V2 — Deklarationsimport verändert Typkontext und Abhängigkeitsreihenfolge
**Priorität: hoch.** Projektpipeline und vollständige Prozedursignaturen,
D2, Tasks 1.2/2.1.
Drei zulässige Mehrmodulkonstellationen werden abgewiesen:
- MAIN ruft `Doppelt(3)` auf; LIB enthält `DEFINT A-Z` und
`FUNCTION Doppelt(x)`. Erwartete Ausgabe 6, tatsächlich
`Parameter type mismatch: MAIN!DOPPELT`.
- MAIN enthält `CONST A=3` und verwendet B; LIB enthält `CONST A=3` und
`CONST B=A+1`. Erwartet B=4, tatsächlich `LIB.bas:2:1: Invalid constant`.
- Beide Module enthalten denselben TYPE Inner. LIB definiert anschließend
TYPE Outer mit einem Feld `i AS Inner`; MAIN verwendet Outer.
Erwartet korrektes Layout und Feldausgabe 3, tatsächlich `Type not defined`
an der gültigen Outer-Definition sowie Folgefehler für Feldzugriffe.
**Ursache:** [import_declarations](../../../../crates/tb-vm/src/project.rs#L103)
kopiert rohe Signatur-ASTs ohne den DEFtype-Kontext des definierenden Moduls.
Fremde CONST-/TYPE-Knoten werden [vor sämtliche lokalen Deklarationen
gehängt](../../../../crates/tb-vm/src/project.rs#L158). Dadurch fehlen ihnen zu
diesem Zeitpunkt lokal vorhandene Abhängigkeiten. Die Fehlerposition in LIB
ist hier irreführend: Die dortige Originalreihenfolge ist gültig.
**Korrektur:** Exportierte Signaturen und Konstanten im Ursprungskontext
auflösen; abhängige Typdefinitionen mit erhaltenen Identitäten und gültiger
Abhängigkeitsreihenfolge verbinden. Lokale Definitionen und echte Duplikate
nicht wieder durch Text-Deduplizierung verstecken. Probe
`v2_importierte_deklarationen_behalten_kontext_und_abhaengigkeiten`.
### V3 — `$FORM`-Module teilen versehentlich dasselbe Formularobjekt
**Priorität: hoch.** Delta „Objektzugriffe in Grammatik und Semantik“
einschließlich `$FORM`, getrennte Formulare, D2/D3, Task 1.2.
A.bas und B.bas enthalten jeweils `'$FORM`. A setzt `Caption="a"`, ruft eine
SUB aus B auf, die dort `Caption="b"` setzt, und liest anschließend die
eigene Caption. **Erwartet: a. Beobachtet: b.** Das verknüpfte Objektverzeichnis
enthält nur SCREEN und A. Auch `B.Caption` aus A wird als unbekanntes Objekt
abgewiesen. Der Fehler besteht nach TBC-Rundlauf fort.
**Ursache:** [lower_with_forms](../../../../crates/tb-frontend/src/sema.rs#L404)
erweitert pro Modul eine lokale Katalogkopie um das jeweilige `$FORM`-Objekt.
Der Linker übernimmt [nur die Objekte des ersten Moduls](../../../../crates/tb-vm/src/project.rs#L188)
und versetzt die Objektoperanden weiterer Module nicht. Deren Form-ID zeigt
anschließend auf A.
**Korrektur:** Sämtliche FRM- und `$FORM`-Formulare vor der modulweisen
Semantik in einen gemeinsamen Katalog mit stabilen IDs aufnehmen oder die
Objekt-/Ereignisreferenzen beim Linken vollständig versetzen. Zugriffe,
Form_Load-Bindungen und gegenseitige Formreferenzen für beide Quellarten
prüfen. Probe `v3_form_metabefehle_erzeugen_getrennte_formularobjekte`.
### V4 — COMMON verliert Variablenidentität und initialisiert Arrays mehrfach
**Priorität: hoch.** Designrisiko SHARED/COMMON, D2, Task 1.2 und Erhalt der
bestehenden COMMON-Sprachsemantik.
- Bereits ein Modul mit `COMMON SHARED c%, c$` wird mit
`COMMON type mismatch: C` abgewiesen. Die beiden durch Typ-Suffixe
verschiedenen Variablen dürfen nebeneinander existieren.
- MAIN und LIB deklarieren dasselbe `COMMON SHARED a%(2)`. MAIN soll
`a%(1)=7` setzen und LIB den gemeinsamen Wert 7 ausgeben. Stattdessen
entsteht vor dem Nutzcode `LIB.bas:1:1: Runtime error 10: Duplicate definition`.
**Ursache:** Die [COMMON-Zuordnung](../../../../crates/tb-vm/src/project.rs#L270)
verwendet nur Block und untypisierten Basisnamen. Arrays haben zusätzlich
nur `TypeInit::Empty` als Slotvorbelegung; Elementtyp und Form gehen in der
Vergleichsprüfung verloren. Der Slot wird zwar zusammengeführt, aber
[beide DIM-Initialisierungen](../../../../crates/tb-vm/src/project.rs#L381)
werden ausgeführt.
**Korrektur:** Vollständige Variablenidentität und Arrayvertrag beim Binden
berücksichtigen. Kompatible gemeinsame Arrays einmal initialisieren;
inkompatible Deklarationen vor der Ausführung am ursprünglichen Quellort
ablehnen. Probe `v4_common_behaelt_typidentitaet_und_initialisiert_arrays_einmal`.
## WARNING — weitere Vertragsabweichungen
### V5 — LOAD/UNLOAD umgehen die explizite Containerauflösung
**Priorität: mittel.** Formularnamensraum und expliziter Containerzugriff,
D2, Task 1.2.
Bei zwei Formularen mit jeweils Text1 als Control-Array werden
`LOAD Form2!Text1(3)` und `UNLOAD Form2!Text1(3)` mit
`Unknown object 'FORM2!TEXT1'` abgewiesen. Property-Zugriffe und die Übergabe
desselben qualifizierten Controls an einen CONTROL-Parameter funktionieren.
**Ursache:** Der [LOAD-/UNLOAD-Pfad](../../../../crates/tb-frontend/src/sema.rs#L3370)
übergibt den gesamten Bang-Pfad an `find_object`; die bereits vorhandene
containerbezogene Auflösung wird hier nicht verwendet.
**Korrektur:** LOAD/UNLOAD über dieselbe Objekt-/Containerauflösung wie
Property- und Referenzzugriffe führen; gleichnamige Arrays beider Formulare
und dynamische Instanzen gemeinsam prüfen. Probe
`v5_load_und_unload_loesen_den_expliziten_container_auf`.
### V6 — Linkerdiagnosen verlieren Datei, Zeile und Spalte
**Priorität: mittel.** Delta „Diagnostik mit Vorbild-Meldungen“, D1, Task 1.1.
MAIN deklariert `SUB F(x%)`, LIB implementiert `SUB F(x$)`. Die Ablehnung ist
richtig, die Meldung jedoch `0:0: Parameter type mismatch: MAIN!F` ohne Datei.
Das ist eine quellbezogene Diagnose, kein Fehler ohne zuordenbare Quelle.
**Ursache:** [diagnostic](../../../../crates/tb-vm/src/project.rs#L17) erzeugt
`file=None` und `SourcePos::default()`. [Linkfehler](../../../../crates/tb-vm/src/project.rs#L66)
werden erst nach `locate_diagnostics` in dieses Format umgewandelt. Dasselbe
Problem betrifft COMMON-Konflikte und weitere symbolbezogene Linkfehler.
**Korrektur:** Ursprung der betroffenen Deklaration bzw. Aufrufstelle mit
in die Linker-Symbolinformationen übernehmen und strukturierte Diagnosen
mit tatsächlicher SourcePos zurückgeben. Keine pauschale Ersatzposition
1:1 eintragen. Probe `v6_linkerdiagnose_nennt_den_urspruenglichen_dateiort`.
## Szenarioabdeckung und positive Nachweise
Alle 21 vorhandenen Delta-Szenarien wurden abgeglichen. Die einfachen
Originalbeispiele besitzen Implementierungs- und Testnachweise; die oben
reproduzierten Randpfade widersprechen den umfassenderen Requirements.
| Requirement / Szenarien | Ergebnis |
| --- | --- |
| Monomorpher Opcode-Satz: gemischter Ausdruck, aufgelöste Namen | Vorhandene Codegen-/HIR-Tests bestanden. Alle 152 Opcodes samt Operandentypen und Encodingbreiten stimmen mit der Dokumentation überein. |
| TBC: Roundtrip, unbekannte Version, Formularwerte | CLI-/VM-Projekttests bestanden: bytegleicher Rundlauf, hello/world aus Designarrays 0/2 nach Entfernen sämtlicher BAS-/FRM-/MAK-/BI-Quellen, Ablehnung alter/höherer Versionen. V2/V3 begrenzen dennoch das vollständige Projektmodell. |
| Quellorte: Fehlerzeile, Bibliotheksfehler | `lib.bas:2` und verschachtelte Include-Fehler bestehen. Unabhängig zusätzlich Fehler in physischer Zeile 42 geprüft. V1 zeigt fehlende Zweig-/Schleifenorte. |
| Benannte Objekte: Lesen/Schreiben, keine Standardeigenschaft, Wertebereich, getrennte Controls | Bestehende Semantik-/VM-/Modelltests und neuer Zwei-FRM-Test bestanden; Check1.Value=3 bleibt Fehler 5. V3 betrifft weitere Formularquellen, V5 die weiteren qualifizierten Zugriffspfade. |
| Diagnosen: Position, Include-Diagnose | Lexer-/Parser-/Semantikpfad trägt Originalpositionen. V6 betrifft die anschließende Linkerphase. |
| Objektgrammatik/-semantik: unbekanntes Objekt/Eigenschaft, Typkonflikt, TYPEOF, gleiche Controls | Bestehende Tests bestanden. V3/V5 verletzen den allgemeinen Modul-/Containervertrag trotz grüner Property-Beispiele. |
| Unterbrechung: Breakpoint, Inspektion, gleiche Zeile in zwei Modulen | Unabhängige Probe: MAIN passiert Zeile 10, LIB hält in Zeile 10; qualifizierte Array-/UDT-Inspektion liefert 7/8; Einzelschritt meldet Spalte 5; ERR/ERL bleiben 6/200. V1 lässt Breakpoints an ELSEIF aus. |
Erneut ausgeführt:
- `cargo test --workspace`: **437 bestanden, 0 fehlgeschlagen, 1 ignoriert**.
Golden-Generator nicht ausgeführt, keine Sollausgaben geändert.
- [Review-Proben](review-probes.rs): **1 bestanden, 6 fehlgeschlagen**.
Jede fehlgeschlagene Probe entspricht einem der sechs Befunde; mehrere
verwandte Reproduktionen werden innerhalb einer Probe gesammelt.
- Zusätzlich zeitlich begrenzte CLI-Proben für die Befunde; temporäre
Quellverzeichnisse anschließend entfernt.
- `cargo bench -p tb-vm --bench compile`: 508 Zeilen **0,54 ms**, 49.760 Zeilen
in 20 verbundenen Modulen **67 ms**; beide Budgets erfüllt.
- Dokumentation: **152/152 Opcodes**, Operanden-/Gesamtbreiten und **9/9
Containerabschnitte** gegen den aktuellen Code abgeglichen.
- `cargo fmt --all -- --check`, `openspec validate projektmodule-und-kompilat --strict`
und `git diff --check`: bestanden.
- Clippy nicht nochmals ausgeführt: keine Produktcodeänderung seit dem
erfolgreichen Clippy-Lauf der Implementierungsabnahme. Kein neuer
interaktiver Originalsystemvergleich und keine Windows-/Linux-Testmatrix.
## Review-Proben reproduzieren
Im Repository-Wurzelverzeichnis ausführen. Der temporäre Treiber hält die
noch roten Vertragsproben außerhalb der normalen Workspace-Suite; ihre
Erwartungen beschreiben das Sollverhalten. Exit-Code ist bis zur Behebung 101.
```sh
python3 - <<'PY'
from pathlib import Path
import json, subprocess, tempfile, os
root = Path.cwd()
with tempfile.TemporaryDirectory(prefix='tb-project-review-') as folder:
p = Path(folder)
manifest = '[package]\nname="tb-project-review"\nversion="0.0.0"\nedition="2021"\n[workspace]\n'
manifest += '[[test]]\nname="probes"\npath=' + json.dumps(str(root / 'openspec/changes/archive/2026-09-05-projektmodule-und-kompilat/review-probes.rs')) + '\n[dependencies]\n'
for name in ['tb-ui', 'tb-vm', 'tb-runtime', 'tb-frontend']:
manifest += name + ' = {path=' + json.dumps(str(root / 'crates' / name)) + '}\n'
(p / 'Cargo.toml').write_text(manifest)
result = subprocess.run(['cargo', 'test', '--manifest-path', str(p / 'Cargo.toml'), '--offline', '--test', 'probes', '--', '--nocapture'], env={**os.environ, 'CARGO_TARGET_DIR': str(root / 'target/review-projektmodule')}, timeout=120)
raise SystemExit(result.returncode)
PY
```
**Abnahmeentscheidung:** V1V4 vor Archivierung beheben; V5/V6 ebenfalls in
die vollständige Umsetzung aufnehmen. Keine zusätzlichen Stilvorschläge.

View File

@@ -0,0 +1,266 @@
//! Unabhängige Vertragsproben; Ausführung mit dem temporären Treiber aus review.md.
use tb_frontend::{
forms::FormCatalog,
source::{SourceSegment, SourceUnit},
};
use tb_runtime::{host::CaptureHost, value::Value};
use tb_vm::{
bytecode::CompiledModule,
compile_project,
interp::{RunEvent, Vm},
};
fn units(files: &[(&str, &str)]) -> Vec<SourceUnit> {
files
.iter()
.map(|(name, code)| SourceUnit::new(name, &format!("{name}.bas"), code))
.collect()
}
fn compile(files: &[(&str, &str)]) -> Result<CompiledModule, Vec<tb_frontend::Diagnostic>> {
compile_project("APP", &units(files), &FormCatalog::default(), &[])
}
fn run(module: CompiledModule) -> (RunEvent, String) {
let bytes = module.to_tbc();
let loaded = CompiledModule::from_tbc(&bytes).unwrap();
assert_eq!(bytes, loaded.to_tbc());
let mut vm = Vm::new(loaded);
let event = vm.run(&mut CaptureHost::default());
(event, tb_runtime::snapshot::text(&vm.rt.screen))
}
#[test]
fn v1_zweige_und_schleifen_behalten_eigene_quellorte() {
let mut failures = vec![];
for (name, code, line) in [
(
"ELSEIF",
"IF 0 THEN\nPRINT 1\nELSEIF 1/0 THEN\nPRINT 2\nEND IF",
3,
),
("CASE", "SELECT CASE 1\nCASE 1/0\nPRINT 2\nEND SELECT", 2),
("NEXT", "FOR i%=32767 TO 32767\nPRINT 1\nNEXT", 3),
("LOOP", "DO\nPRINT 1\nLOOP UNTIL 1/0", 3),
] {
let (event, _) = run(compile(&[("MAIN", code)]).unwrap());
if !matches!(event, RunEvent::Error { line: found, .. } if found == line) {
failures.push(format!("{name}: erwartet Zeile {line}, erhalten {event:?}"));
}
}
let source = SourceUnit {
name: "MAIN".into(),
segments: vec![
SourceSegment {
file: "main.bas".into(),
first_line: 1,
text: "IF 0 THEN\nPRINT 1\n".into(),
},
SourceSegment {
file: "cond.bi".into(),
first_line: 1,
text: "ELSEIF 1/0 THEN\nPRINT 2\n".into(),
},
SourceSegment {
file: "main.bas".into(),
first_line: 4,
text: "END IF\n".into(),
},
],
};
let mut vm = Vm::new(compile_project("APP", &[source], &FormCatalog::default(), &[]).unwrap());
let event = vm.run(&mut CaptureHost::default());
if vm.current_file() != "cond.bi" {
failures.push(format!(
"Include: {event:?} aus {} statt cond.bi",
vm.current_file()
));
}
// Das gleiche Problem darf nicht nur durch Umbenennen der Fehlermeldung behoben werden.
let mut vm = Vm::new(
compile(&[("MAIN", "IF 0 THEN\nPRINT 1\nELSEIF 1 THEN\nPRINT 2\nEND IF")]).unwrap(),
);
vm.add_module_breakpoint(0, 3);
let event = vm.run(&mut CaptureHost::default());
if event != (RunEvent::Breakpoint { line: 3 }) {
failures.push(format!("Breakpoint ELSEIF: {event:?}"));
}
assert!(failures.is_empty(), "{}", failures.join("\n"));
}
#[test]
fn v2_importierte_deklarationen_behalten_kontext_und_abhaengigkeiten() {
let cases = [
(
"DEFINT",
"PRINT Doppelt(3)\nEND",
"DEFINT A-Z\nFUNCTION Doppelt(x)\nDoppelt=x*2\nEND FUNCTION",
" 6 \n",
),
(
"CONST",
"CONST A=3\nPRINT B\nEND",
"CONST A=3\nCONST B=A+1",
" 4 \n",
),
(
"TYPE",
"TYPE Inner\nx AS INTEGER\nEND TYPE\nDIM a AS Outer\na.i.x=3\nPRINT a.i.x\nEND",
"TYPE Inner\nx AS INTEGER\nEND TYPE\nTYPE Outer\ni AS Inner\nEND TYPE",
" 3 \n",
),
];
let mut failures = vec![];
for (name, main, lib, output) in cases {
match compile(&[("MAIN", main), ("LIB", lib)]) {
Err(e) => failures.push(format!("{name}: {e:?}")),
Ok(module) => {
let actual = run(module);
if actual != (RunEvent::Ended, output.into()) {
failures.push(format!("{name}: {actual:?}"));
}
}
}
}
assert!(failures.is_empty(), "{}", failures.join("\n"));
}
#[test]
fn v3_form_metabefehle_erzeugen_getrennte_formularobjekte() {
let module = compile(&[
(
"A",
"'$FORM\nCaption=\"a\"\nCALL SetupB\nPRINT Caption\nEND",
),
("B", "'$FORM\nSUB SetupB\nCaption=\"b\"\nEND SUB"),
])
.unwrap();
let names: Vec<_> = module.objects.iter().map(|o| o.name.clone()).collect();
let actual = run(module);
assert_eq!(
actual,
(RunEvent::Ended, "a\n".into()),
"Objekte: {names:?}"
);
}
#[test]
fn v4_common_behaelt_typidentitaet_und_initialisiert_arrays_einmal() {
let mut failures = vec![];
for (name, files, want) in [
(
"Suffixe",
vec![(
"MAIN",
"COMMON SHARED c%, c$\nc%=7\nc$=\"ok\"\nPRINT c%;c$\nEND",
)],
" 7 ok\n",
),
(
"Array",
vec![
("MAIN", "COMMON SHARED a%(2)\na%(1)=7\nCALL F\nEND"),
("LIB", "COMMON SHARED a%(2)\nSUB F\nPRINT a%(1)\nEND SUB"),
],
" 7 \n",
),
] {
match compile(&files) {
Err(e) => failures.push(format!("{name}: {e:?}")),
Ok(module) => {
let actual = run(module);
if actual != (RunEvent::Ended, want.into()) {
failures.push(format!("{name}: {actual:?}"));
}
}
}
}
assert!(failures.is_empty(), "{}", failures.join("\n"));
}
#[test]
fn v5_load_und_unload_loesen_den_expliziten_container_auf() {
use tb_frontend::forms::ObjectClass;
let mut catalog = FormCatalog::default();
for name in ["Form1", "Form2"] {
catalog.add(name, ObjectClass::Form, None, false);
catalog.add("Text1", ObjectClass::TextBox, Some(name), true);
}
let source = units(&[("MAIN", "LOAD Form2!Text1(3)\nForm2!Text1(3).Text=\"b\"\nPRINT Form2!Text1(3).Text\nUNLOAD Form2!Text1(3)\nEND")]);
let module = compile_project("APP", &source, &catalog, &[]).unwrap();
assert_eq!(run(module), (RunEvent::Ended, "b\n".into()));
}
#[test]
fn v6_linkerdiagnose_nennt_den_urspruenglichen_dateiort() {
let errors = compile(&[
("MAIN", "DECLARE SUB F(x%)\nCALL F(1)\nEND"),
("LIB", "SUB F(x$)\nEND SUB"),
])
.unwrap_err();
assert!(
errors
.iter()
.any(|d| d.message.contains("Parameter type mismatch")),
"{errors:?}"
);
assert!(
errors
.iter()
.all(|d| d.file.is_some() && d.pos.line > 0 && d.pos.column > 0),
"{errors:?}"
);
}
#[test]
fn modulbreakpoint_rundlauf_inspektion_und_numerische_erl_bleiben_getrennt() {
let main = format!(
"DIM a%(2)\nTYPE T\nx AS INTEGER\nEND TYPE\nDIM r AS T\na%(1)=7\nr.x=8\n{}CALL F\nEND",
"\n\nn%=1\n"
);
let lib = format!("SUB F\n{}200 ERROR 6\nEND SUB", "\n".repeat(8));
let module = compile(&[("MAIN", &main), ("LIB", &lib)]).unwrap();
let bytes = module.to_tbc();
let loaded = CompiledModule::from_tbc(&bytes).unwrap();
assert_eq!(bytes, loaded.to_tbc());
let mut vm = Vm::new(loaded);
vm.add_module_breakpoint(1, 10);
let mut host = CaptureHost::default();
assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 10 });
assert_eq!(vm.current_module(), 1);
assert_eq!(vm.current_file(), "LIB.bas");
assert!(matches!(
vm.inspect_element("MAIN!a%", &[1]),
Some(Value::Int(7))
));
assert!(matches!(
vm.inspect_field("MAIN!r", &[0]),
Some(Value::Int(8))
));
vm.remove_module_breakpoint(1, 10);
vm.set_step(true);
assert_eq!(vm.run(&mut host), RunEvent::Stepped { line: 10 });
assert_eq!(vm.current_source_pos().column, 5);
vm.set_step(false);
assert!(matches!(
vm.run(&mut host),
RunEvent::Error {
code: 6,
line: 10,
..
}
));
let source = format!("{}ERROR 6", "\n".repeat(41));
assert!(matches!(
run(compile(&[("MAIN", &source)]).unwrap()).0,
RunEvent::Error {
code: 6,
line: 42,
..
}
));
let module = compile(&[(
"MAIN",
"ON ERROR RESUME NEXT\n200 ERROR 6\nPRINT ERR;ERL\nEND",
)])
.unwrap();
assert_eq!(run(module), (RunEvent::Ended, " 6 200 \n".into()));
}

View File

@@ -0,0 +1,79 @@
# Verifikation: projektmodule-und-kompilat
Stand: 05.09.2026, Arbeitsbaum auf Basis von `58b1f62`.
**Keine offenen Befunde: 0 CRITICAL, 0 WARNING, 0 SUGGESTION.**
Die sechs Befunde des [ersten Reviews](review-initial.md) sind behoben.
Weitere bei der Nachprüfung reproduzierte Abweichungen wurden ebenfalls
korrigiert und mit Regressionen abgesichert. Der Change ist abnahmebereit.
| Dimension | Ergebnis |
| --- | --- |
| Vollständigkeit | 12/12 Tasks umgesetzt; alle 7 Requirements und 21 Delta-Szenarien abgeglichen. |
| Korrektheit | 453 Workspace-Tests bestanden, 0 fehlgeschlagen, 1 vorhandener Golden-Generator ignoriert. Die 7 unveränderten ursprünglichen Review-Proben bestehen auch im unabhängigen temporären Treiber. |
| Kohärenz | D1D4 erfüllt: physische Quellorte, getrennte Scopes, selbstständiges TBC 4 und dokumentierte Inline-Instruktionen. |
## Geschlossene Befunde
| Befund | Korrektur und Nachweis |
| --- | --- |
| V1: Zweig-/Schleifenorte | Parser und HIR erhalten ELSEIF-, CASE-, NEXT-, LOOP- und WEND-Positionen. Codegen emittiert die tatsächlichen Anweisungsgrenzen. Fehler, Breakpoints, WHILE-Rücksprung, LOOP-RESUME, NEXT-RESUME NEXT und Include-Enden in Zeile 42/Spalte 3 sind geprüft. Numerische ERL-Labels bleiben getrennt. |
| V2: Importkontext | Exportierte Signaturen erhalten die DEFtype-Typen ihres Ursprungs. Konstanten werden dort gefaltet, einschließlich transitiver Abhängigkeiten über drei Module. TYPE-Abhängigkeiten werden geordnet und bei abweichenden lokalen Layouts intern qualifiziert. Echte lokale Duplikate bleiben Fehler. |
| V3: `$FORM`-Identitäten | FRM- und `$FORM`-Objekte stehen vor jeder Modulübersetzung in einem gemeinsamen Katalog. Getrennte Captions, gegenseitige Referenzen und unterschiedliche Form_Load-Bindungen sind auch für gemischte FRM/BAS-Projekte nach TBC-Rundlauf geprüft. |
| V4: COMMON | HIR übergibt Block, vollständigen Variablenschlüssel, Elementtyp und Arraygrenzen an den Linker. Typen, Ränge und bekannte Grenzen werden geprüft. `CommonArr` initialisiert einmal und prüft spätere dynamische Grenzen ohne Zurücksetzen des Handles oder der Werte. Offene Arraydeklarationen und gerundete Grenzen bleiben unterstützt. |
| V5: LOAD/UNLOAD | Beide Anweisungen verwenden dieselbe explizite Containerauflösung und Indexprüfung wie andere Objektzugriffe. `Form2!Text1(3)` wird geladen, beschrieben, gelesen und entladen, auch wenn Form1 ebenfalls Text1 besitzt. |
| V6: Diagnosen | Der Linker gibt strukturierte Diagnosen an den ursprünglichen Deklarationen zurück; Source-ID, Datei, Zeile und Spalte bleiben erhalten. Signaturkonflikte, COMMON-Konflikte im Include sowie doppelte Module sind geprüft. |
Die erweiterten Proben fanden zusätzlich folgende inzwischen behobene
Abweichungen: transitive Konstantenimports, fremde TYPE-Layouts bei lokal
abweichenden Definitionen, prozedurübergreifendes Durchsickern von DEFtype,
Verwechslung typisierter Debuggernamen, Verwechslung von Formnamen mit
suffixdeklarierten Variablen und fehlende Literalpositionen in Diagnosen.
Letzteres ist durch `CALL F("wrong")` in `args.bi:42:10` abgesichert.
Auch Konflikte erst in der dritten COMMON-Deklaration werden erkannt.
Die [16 dauerhaften Regressionstests](../../../../crates/tb-vm/tests/project_regressions.rs)
enthalten die sieben ursprünglichen Proben mit unveränderten Erwartungen
sowie neun ergänzende Tests mit mehreren verwandten Fällen. Die ursprüngliche
[Probe-Datei](review-probes.rs) wurde nicht geändert. Es wurden keine Goldens
neu erzeugt und keine Fehlererwartungen abgeschwächt.
## Abgleich aller Requirements und Szenarien
| Requirement | Szenarien / Nachweise |
| --- | --- |
| Monomorpher Opcode-Satz | Gemischter Ausdruck und aufgelöste Namen: vorhandene HIR-/Codegentests; numerische Projektverknüpfung. Alle 153 Opcode-Kennungen, Operandenfolgen und Encodingbreiten gegen die Dokumentation geprüft. |
| TBC-Container | Bytegleicher Roundtrip; klare Ablehnung alter/unbekannter Versionen; vollständige Formularwerte und Designarrays. CLI-Projekttests entfernen sämtliche BAS-/BI-/FRM-/MAK-Quellen und führen das TBC selbstständig aus. |
| Quellorte | Fehler in physischer Zeile 42; Fehler in lib.bas:2; verschachtelte Includes; zusätzliche ausführbare Zweig-/Schleifengrenzen und Initialisierungen. |
| Benannte Objekte | Eigenschaften lesen/schreiben; keine Standardeigenschaft; Check1.Value=3 erzeugt Fehler 5; voneinander unabhängige Werte gleichnamiger Controls. Zwei FRM- und gemischte FRM/`$FORM`-Projekte geprüft. |
| Diagnostik | 1-basierte Datei-/Zeilen-/Spaltenangaben in Lexer, Parser, Semantik und Linker; Include-Fehler einschließlich Literalargumenten. |
| Objektgrammatik/-semantik | Unbekanntes Objekt und unbekannte Eigenschaft benennen; Eigenschaftstyp prüfen; TYPEOF; expliziter Form1/Form2-Container bei gleichnamigen Controls. Qualifiziertes LOAD/UNLOAD nutzt dieselbe Auflösung. |
| Unterbrechung/Inspektion | Modulbezogener Breakpoint; scalar-, array- und UDT-Inspektion; Fortsetzen und Einzelschritt. Gleiche physische Zeile in MAIN und LIB bleibt getrennt, explizite Typ-Suffixe unterscheiden Variablen. ERR/ERL bleiben 6/200. |
Die konkreten ursprünglichen Szenario-/Testnamen und weitere Containerproben
stehen im [Implementierungsnachweis](verification.md), den
[VM-Projekttests](../../../../crates/tb-vm/tests/project.rs) und den
[CLI-Projekttests](../../../../crates/tb-cli/tests/project.rs).
## Abschließende Prüfläufe
- `cargo test --workspace`: **453 bestanden, 0 fehlgeschlagen, 1 ignoriert**.
- Unabhängiger Treiber aus [review-initial.md](review-initial.md): **7 bestanden**.
- `cargo clippy --workspace --all-targets --all-features -- -D warnings`: bestanden.
- `cargo fmt --all -- --check`: bestanden.
- `cargo bench -p tb-vm --bench compile`: 508 Zeilen **0,56 ms** (Budget 50 ms);
49.760 Zeilen in 20 verbundenen Modulen **77 ms** (Budget 1000 ms).
- Formatabgleich: **153/153 Opcodes**, Operandentypen und Gesamtbreiten;
**9/9 Abschnittskennungen** stimmen zwischen Writer, Reader und Dokumentation überein.
- `openspec validate projektmodule-und-kompilat --strict`: bestanden.
- `git diff --check`: bestanden.
Die Verifikation prüft den aktuellen Code und die gesamten Change-Artefakte
lokal und headless auf macOS. Ein neuer interaktiver Originalsystemvergleich
oder eine Windows-/Linux-Testmatrix war nicht Teil dieses Prüflaufs.
Die vier betroffenen Hauptspecs wurden am 05.09.2026 synchronisiert und
mit `openspec validate --specs --strict` validiert (15/15 bestanden).
Alle sieben Delta-Requirements stimmen vollständig mit den Hauptspecs
überein; alle zuvor vorhandenen Szenarien und unberührten Requirements
bleiben erhalten. Der Change ist unter
`openspec/changes/archive/2026-09-05-projektmodule-und-kompilat` archiviert.

View File

@@ -0,0 +1,23 @@
## 1. Quellen und Scopes
- [x] 1.1 Quell-/Modulidentitäten durch Include-Expansion und Frontend erhalten; verifizieren mit Compile- und Runtimefehlern aus lib.bas:2 und verschachtelten Includes.
- [x] 1.2 Form-/Prozedurscopes korrekt auflösen; verifizieren mit zwei Form_Load-Prozeduren, zwei Text1-Controls und unterschiedlichen Form1!Text1/Form2!Text1-Werten sowie echten Duplikaten im selben Scope.
## 2. Kompilat und Debugger
- [x] 2.1 Vollständige Forms-Anfangsdaten, Quelltabelle und Prozedursignaturen serialisieren; verifizieren durch bytegleichen Kompilat-Rundlauf und gleiche Ausführung inklusive Designzeit-Control-Arrays.
- [x] 2.2 Modulbezogene Breakpoints/Quellortmeldungen ergänzen; verifizieren mit identischen physischen Zeilennummern in zwei Modulen, Include-Fehlern, Einzelschritt und unverändertem ERR/ERL.
- [x] 2.3 CLI-run für TBC anschließen und Versionsgrenze anheben; verifizieren mit build, Entfernen aller Quellen, run des Kompilats und klarer Ablehnung inkompatibler Versionen.
## 3. Abnahme
- [x] 3.1 Container und vollständige Opcode-/Operandentabelle dokumentieren; verifizieren mit Format-/Opcode-Abgleich, cargo test --workspace und dem Compile-Budget.
- [x] 3.2 Delta-Specs vollständig gegen die erhaltenen Szenarien prüfen; verifizieren mit openspec validate projektmodule-und-kompilat --strict und git diff --check.
## 4. Reviewbefunde vollständig beheben
- [x] 4.1 V1: Physische Quellpositionen aller Zweige und Schleifenenden erhalten; Fehler, Breakpoints und RESUME prüfen.
- [x] 4.2 V2/V6: Importierte Deklarationen im Ursprungskontext auflösen und Linkdiagnosen an Originalstellen melden.
- [x] 4.3 V3/V5: Gemeinsame Formidentitäten und qualifiziertes LOAD/UNLOAD korrigieren.
- [x] 4.4 V4: Vollständige COMMON-Identitäten und kompatible einmalige Arrayinitialisierung sicherstellen.
- [x] 4.5 Unveränderte Reviewproben, ergänzende Nachbarfälle und sämtliche Change-Szenarien erneut prüfen; alle weiteren Befunde beheben.

View File

@@ -0,0 +1,106 @@
# Implementierungsnachweis — 05.09.2026
Change: `projektmodule-und-kompilat`, Ausgangscommit `58b1f62`.
**Abschließende Verifikation:** [review.md](review.md) bestätigt die Behebung
aller sechs ursprünglichen und aller zusätzlich reproduzierten Befunde.
Der [erste Review](review-initial.md) bleibt als historischer Nachweis erhalten.
Alle zwölf Tasks (sieben Implementierungs- und fünf Review-Aufgaben) sind
umgesetzt. F09, F10, F11 und F24 sind im Arbeitsbaum bearbeitet; die
Hauptspec-Synchronisierung und Archivierung sind am 05.09.2026 abgeschlossen. Die Abnahme
umfasst alle sieben Delta-Requirements mit ihren 21 Szenarien.
## Umsetzung
| Task / Befund | Umsetzung und Nachweis |
| --- | --- |
| 1.1 / F10 | `SourceUnit`/`SourceSegment` erhalten Modul, Datei und physische Zeile vor dem Parsen. Lexer- und Parserpositionen werden vor der Semantik den Originalquellen zugeordnet. CLI-Tests prüfen Compile-/Runtimefehler aus `lib.bas:2`, verschachtelten Includes und dem Codeabschnitt eines entfernten FRM. Auch leere Quellen funktionieren. |
| 1.2 / F09 | Module werden getrennt aufgelöst und danach numerisch verbunden. Lokale Prozeduren und Controls haben Vorrang, `Form!Control` sucht direkt im angegebenen Formular. Eltern-IDs ersetzen Namensvergleiche auch bei Frames, Menüs, Optionsgruppen und Geometrie. Zwei Form_Load-Prozeduren initialisieren ihre jeweiligen Text1-Controls unabhängig; echte SUB-/CONST-/TYPE-Duplikate im selben Modul bleiben Fehler. |
| 2.1 / F11 | TBC 4 enthält Module/Quellen, vollständige Parameter- und Rückgabetypen, Locals, Forms-Instanzen mit Eigenschaften, Designindizes und Eltern-IDs sowie das Startformular. `Vm::new` stellt diesen Anfangszustand her. Bytegleicher Rundlauf und Ausführung mit zwei Formularen und Designarrays 0/2 sind geprüft. |
| 2.2 / F10 | `Source` plus `Stmt` liefern Datei/Spalte/physische Zeile. `InitStmt` erhält dieselben Informationen für globale Initialisierungen, ohne vorzeitig Ereignisse zuzustellen. Breakpoints verwenden Modul-ID und Zeile. Einzelschritt, Variableninspektion und Fortsetzen bleiben möglich. Numerische Labels führen separat `SetErl`; DEF FN erhält den tatsächlichen Quellort. |
| 2.3 / F11 | `tbc run` lädt TBC direkt. Der CLI-Test baut ein Projekt, entfernt alle BAS-/BI-/FRM-/MAK-Dateien und vergleicht die Ausgabe des Kompilats mit der Quellausführung (`hello`, `world`, `7`). Ein zweiter Fall erhält nach Entfernen des FRM den Fehlerort `error.frm:6:1`. Versionen 1, 2, 3, 5 und 32767 werden ausdrücklich benannt und abgewiesen. |
| 3.1 / F24 | [TBVM-Format](../../../../docs/tbvm-design.md) dokumentiert alle neun Abschnitte und alle 153 Opcodes einzeln mit Operanden, Encodingbreiten und Kennungen. Der Compile-Benchmark übersetzt und verbindet nun tatsächlich ein Projekt mit 20 Modulen. |
| 3.2 | Alle Delta-Requirement-Blöcke behalten ihre bisherigen Szenarien. Neue Szenarien und die Implementierungsentscheidungen D1D4 wurden gegen die unten genannten Tests und Codepfade abgeglichen. OpenSpec- und Diff-Prüfung bestanden. |
## Projektgrenzen und ergänzende Regressionen
Der Projektlink versetzt Prozedur-, Slot-, TYPE-, String-, DATA- und
Sprungtabellenreferenzen. Hauptprogramminitialisierungen aller Mitglieder
laufen vor der ersten Ereigniszustellung; die übrigen Modulrümpfe folgen der
Projektreihenfolge. Eindeutige externe Prozeduren werden als Deklarationen
importiert, lokale Namen bleiben erhalten. Eindeutige fremde Konstanten und
TYPEs bleiben als Kompatibilitätsregel sichtbar. Es wird kein Quelltext zur
Unterdrückung von Duplikaten gelöscht.
Geprüft sind wiederholt eingebundene Konstanten, gleichnamige lokale SUBs,
modullokales SHARED, gemeinsame COMMON-Skalare und unterschiedliches OPTION
BASE. Modulhandler werden getrennt gespeichert: ON ERROR GOTO 0 in der
Bibliothek deaktiviert den Handler des Aufrufers nicht. Bestehende ERR-/ERL-
und RESUME-Korpustests bestehen unverändert.
Der Signaturtest übergibt ein INTEGER-Array, einen UDT und einen Skalar an
eine Funktion in einem anderen Modul. Er prüft Rückgabetyp, Array-/BYREF-
Merkmale, Ausgabe und Rückwirkung einer UDT-Feldänderung auf den Aufrufer.
Die dabei gefundene Inkonsistenz zwischen UDT-Argument und Parameter wurde
im gemeinsamen Semantikpfad behoben. Bekannte externe Dialogdeklarationen
funktionieren auch mit qualifizierten Prozedurnamen. ISAM-OPEN verwendet mit
`PushUdtId` eine explizit versetzbare TYPE-Referenz; der Test vergleicht sie
nach dem Rundlauf mit dem tatsächlichen UDT-Index.
Der Loader weist abgeschnittene Dateien an jedem Byteoffset, ungültige
Flags/Abschnittsgrößen, doppelte Abschnitte sowie ungültige Quell-, Modul-,
Prozedur- und Elternreferenzen ab. Inkonsistente Signaturen, doppelte
Anfangseigenschaften, falsche Property-Typen und abweichende Designindizes
werden vor dem VM-Start geprüft. Das ersetzt keinen vollständigen
Stackverifizierer für beliebigen fremden Bytecode.
## Requirement- und Szenarioabgleich
Die neuen ausführbaren Nachweise liegen in
[VM-Projekttests](../../../../crates/tb-vm/tests/project.rs) und
[CLI-Projekttests](../../../../crates/tb-cli/tests/project.rs).
Die folgende Tabelle führt auch die erhaltenen Szenarien auf; bestehende
Tests werden durch neue Mehrmodul- und Include-Proben ergänzt.
| Requirement | Szenarien und Belege |
| --- | --- |
| Monomorpher Opcode-Satz | Gemischter Ausdruck: `codegen::tests::ausdruck_monomorph_mit_conv`; aufgelöste Namen: `sema::tests::hir_slots_aufgeloest` und die numerisch verbundenen Projektaufrufe. Vollständigkeit der Dokumentation zusätzlich gegen das `instrs!`-Makro geprüft. |
| .tbc-Containerformat | Roundtrip: mehrere VM-Projekttests vergleichen die gesamten Bytes; unbekannte Version: CLI-Versionstests; Formularwerte: CLI-Ausgabe `hello` und `world` nach Entfernen sämtlicher Quellen. |
| Zeilentabelle für Fehlerortung | Physische Fehlerzeile: Stmt-/Source-Zuordnung und CLI-Fehlerproben; Projektbibliothek: `lib.bas:2:5`, Include: `inner.bi:2:5`; zusätzlich InitStmt-, DEF-FN- und FRM-Codepositionen. |
| Formulare und Steuerelemente als benannte Objekte | Lesen/Schreiben: bestehender VM-Test `objektzugriff_laeuft_ueber_objekt_und_eigenschaftsindex`; keine Standardeigenschaft: `sema::tests::eigenschaftstyp_schreibbarkeit_bang_und_hir_index`; Wertebereich: `forms::tests::defaults_bereich_und_implizites_laden` prüft Check1.Value = 3 → Fehler 5; unabhängige Werte: zwei Formulare mit Text1 und eigenem Form_Load im neuen Projekttest. |
| Diagnostik mit Vorbild-Meldungen | Exakte Position: SourcePos bleibt 1-basiert; neue Tests prüfen Datei, Zeile und Spalte. Include-Diagnose: sowohl direktes SourceSegment als auch verschachtelte CLI-Expansion liefern die ursprüngliche Position des Typfehlers. |
| Objektzugriffe in Grammatik und Semantik | Unbekanntes Objekt/Eigenschaft und Eigenschaftstyp: bestehende Semantiktests für Text9.Text, Text1.Farbe und Text1.Text = 5; TYPEOF: `typeof_form_control_und_form_metabefehl`; gleichnamige Controls: Form1!Text1/Form2!Text1 sowie lokale Text1-Zugriffe beider Form_Load-Rümpfe. |
| Unterbrechbare step()-Ausführung | Breakpoint und Inspektion: vorhandene VM-Tests plus neuer Modulbreakpoint; gleiche Zeile in zwei Modulen: Modul A passiert Zeile 2, Modul B hält dort. Fortsetzen im Einzelschritt erhält `nested.bi:2`, physische Spalte 5 und getrenntes ERL = 200. |
## Ausgeführte Abnahme
- `cargo test --workspace`: **453 bestanden, 0 fehlgeschlagen, 1 ignoriert**.
Der ignorierte Golden-Generator wurde nicht ausgeführt; Sollausgaben blieben unverändert.
- `cargo clippy --workspace --all-targets --all-features -- -D warnings`: bestanden.
- `cargo fmt --all -- --check`: bestanden.
- `cargo bench -p tb-vm --bench compile`: 508 Zeilen **0,56 ms** (Budget 50 ms);
49.760 Zeilen in 20 verbundenen Modulen **77 ms** (Budget 1000 ms),
335.421 Instruktionen. Release-Messung auf diesem Rechner.
- Formatabgleich: **153/153 Opcodezeilen** einschließlich Operandentypen und
Bytebreiten sowie **9/9 Abschnittskennungen** gegen Writer/Reader abgeglichen.
- `openspec validate projektmodule-und-kompilat --strict`: bestanden.
- `git diff --check`: bestanden.
Die Abnahme ist eine lokale Code-/Testprüfung auf macOS; sie ist kein neuer
interaktiver Originalsystemvergleich und keine Windows-/Linux-Testmatrix.
## Reviewkorrekturen
Die ursprünglichen sieben Reviewproben bestehen unverändert, sowohl im
separaten Treiber als auch in der Workspace-Suite. Die dauerhafte Datei
`crates/tb-vm/tests/project_regressions.rs` ergänzt sie um neun Tests mit
verwandten Fällen. [review.md](review.md) ordnet die Korrekturen V1V6 und
alle zusätzlich reproduzierten Abweichungen den Nachweisen zu.
`CommonArr` (`0x3B`) initialisiert gemeinsame Arrays einmal und prüft spätere
Laufzeitgrenzen auf Kompatibilität; vorab bekannte Konflikte meldet der
Linker an der ursprünglichen COMMON-Deklaration. Die neue Instruktion ist
in TBC 4 serialisiert, validiert und vollständig dokumentiert. Typ-Suffixe
bleiben in Debugger-Slotnamen erhalten. AST-Literale und sämtliche ausführbaren
Zweig-/Schleifenteile tragen ihre tatsächlichen Quellpositionen.

View File

@@ -1,15 +0,0 @@
## 1. Quellen und Scopes
- [ ] 1.1 Quell-/Modulidentitäten durch Include-Expansion und Frontend erhalten; verifizieren mit Compile- und Runtimefehlern aus lib.bas:2 und verschachtelten Includes.
- [ ] 1.2 Form-/Prozedurscopes korrekt auflösen; verifizieren mit zwei Form_Load-Prozeduren, zwei Text1-Controls und unterschiedlichen Form1!Text1/Form2!Text1-Werten sowie echten Duplikaten im selben Scope.
## 2. Kompilat und Debugger
- [ ] 2.1 Vollständige Forms-Anfangsdaten, Quelltabelle und Prozedursignaturen serialisieren; verifizieren durch bytegleichen Kompilat-Rundlauf und gleiche Ausführung inklusive Designzeit-Control-Arrays.
- [ ] 2.2 Modulbezogene Breakpoints/Quellortmeldungen ergänzen; verifizieren mit identischen physischen Zeilennummern in zwei Modulen, Include-Fehlern, Einzelschritt und unverändertem ERR/ERL.
- [ ] 2.3 CLI-run für TBC anschließen und Versionsgrenze anheben; verifizieren mit build, Entfernen aller Quellen, run des Kompilats und klarer Ablehnung inkompatibler Versionen.
## 3. Abnahme
- [ ] 3.1 Container und vollständige Opcode-/Operandentabelle dokumentieren; verifizieren mit Format-/Opcode-Abgleich, cargo test --workspace und dem Compile-Budget.
- [ ] 3.2 Delta-Specs vollständig gegen die erhaltenen Szenarien prüfen; verifizieren mit openspec validate projektmodule-und-kompilat --strict und git diff --check.

View File

@@ -17,6 +17,8 @@ implizite Konvertierungen erscheinen als explizite Konvertierungs-Opcodes
an den vom HIR bestimmten Stellen. Der Opcode-Satz SHALL in an den vom HIR bestimmten Stellen. Der Opcode-Satz SHALL in
docs/tbvm-design.md vollständig dokumentiert sein. docs/tbvm-design.md vollständig dokumentiert sein.
Die Dokumentation SHALL sämtliche tatsächlich serialisierbaren Instruktionen einschließlich Forms, Ereignissteuerung und Dateioperationen mit Operanden und Encoding führen.
#### Scenario: Gemischter Ausdruck wird monomorph #### Scenario: Gemischter Ausdruck wird monomorph
- **WHEN** `d# = i% + 1.5#` übersetzt wird (INTEGER-Variable, DOUBLE-Ziel) - **WHEN** `d# = i% + 1.5#` übersetzt wird (INTEGER-Variable, DOUBLE-Ziel)
- **THEN** enthält der Bytecode einen Konvertierungs-Opcode INTEGER→DOUBLE und eine DOUBLE-Addition, keinen generischen Additions-Opcode - **THEN** enthält der Bytecode einen Konvertierungs-Opcode INTEGER→DOUBLE und eine DOUBLE-Addition, keinen generischen Additions-Opcode
@@ -27,14 +29,17 @@ docs/tbvm-design.md vollständig dokumentiert sein.
### Requirement: `.tbc`-Containerformat ### Requirement: `.tbc`-Containerformat
Das Kompilat SHALL als `.tbc`-Datei serialisierbar und wieder ladbar sein: Das Kompilat SHALL als `.tbc`-Datei serialisierbar und wieder ladbar sein:
Magic `TBC\0`, Formatversion, Abschnittstabelle mit den Abschnitten Magic `TBC\0`, Formatversion und dokumentierte Abschnittstabelle.
CONSTS (deduplizierter Konstantenpool), TYPES (TYPE-Layouts), PROCS Der Container SHALL Quellen und Module, Konstantenpool, TYPE-Layouts,
(modul-qualifizierte Prozedurtabelle mit Signatur, Locals-Anzahl, modulqualifizierte Prozeduren mit vollständiger Signatur, Locals und Code,
Code-Offset), CODE, DATA (`READ`/`RESTORE`-Segment) und LINES DATA sowie die vollständigen Forms-Anfangsbeschreibungen führen.
(Zeilentabelle). Laden und erneutes Serialisieren MUST verlustfrei sein; Code und Quellortinformationen SHALL inline in den Prozeduren liegen
dürfen; eigenständige CODE-/LINES-Abschnitte sind nicht erforderlich. Laden und erneutes Serialisieren MUST verlustfrei sein;
eine unbekannte Formatversion MUST mit einer klaren Fehlermeldung eine unbekannte Formatversion MUST mit einer klaren Fehlermeldung
abgewiesen werden. abgewiesen werden.
Ein aus FRM- oder Projektquellen gebautes Kompilat SHALL ohne erneuten Zugriff auf diese Quellen ausführbar sein und ihre Anfangseigenschaften und Designzeit-Control-Arrays bewahren.
#### Scenario: Roundtrip #### Scenario: Roundtrip
- **WHEN** ein kompiliertes Modul als `.tbc` geschrieben und wieder geladen wird - **WHEN** ein kompiliertes Modul als `.tbc` geschrieben und wieder geladen wird
- **THEN** ist das geladene Kompilat funktional identisch (gleiche Ausführung, gleiche Zeilenzuordnung) - **THEN** ist das geladene Kompilat funktional identisch (gleiche Ausführung, gleiche Zeilenzuordnung)
@@ -43,15 +48,25 @@ abgewiesen werden.
- **WHEN** eine `.tbc`-Datei mit höherer Formatversion geladen wird - **WHEN** eine `.tbc`-Datei mit höherer Formatversion geladen wird
- **THEN** wird das Laden mit einer Meldung abgelehnt, die die Version nennt - **THEN** wird das Laden mit einer Meldung abgelehnt, die die Version nennt
#### Scenario: Formularwerte nach Kompilat-Rundlauf
- **WHEN** ein Formular mit Text1.Text = "hello" kompiliert, gespeichert und ohne Quelldatei geladen wird
- **THEN** liefert der erste Zugriff auf Text1.Text weiterhin hello
### Requirement: Zeilentabelle für Fehlerortung ### Requirement: Zeilentabelle für Fehlerortung
Der Bytecode SHALL jede Anweisung ihrem Ursprung (Moduldatei, Zeile) Der Bytecode SHALL jede Anweisung ihrem Ursprung (Moduldatei, Zeile)
zuordnen, sodass Laufzeitfehlermeldungen, `ERL`, Breakpoints und zuordnen, sodass Laufzeitfehlermeldungen, `ERL`, Breakpoints und
Einzelschritt die Quellzeile exakt benennen können. Einzelschritt die Quellzeile exakt benennen können.
Die Zuordnung SHALL auch nach Include-Expansion und bei mehreren Projektmitgliedern die ursprüngliche Datei und physische Zeile bewahren. Numerische BASIC-Zeilenlabels für ERL SHALL davon getrennt bleiben.
#### Scenario: Fehler nennt Zeile #### Scenario: Fehler nennt Zeile
- **WHEN** in Zeile 42 eines Programms ein Laufzeitfehler ohne Handler auftritt - **WHEN** in Zeile 42 eines Programms ein Laufzeitfehler ohne Handler auftritt
- **THEN** nennt die Fehlermeldung Zeile 42 - **THEN** nennt die Fehlermeldung Zeile 42
#### Scenario: Fehler aus Projektbibliothek
- **WHEN** lib.bas in physischer Zeile 2 einen Fehler auslöst und über app.mak aufgerufen wird
- **THEN** nennt die Meldung lib.bas und Zeile 2 statt einer verketteten Projektzeile
### Requirement: Instant-Compile-Budget ### Requirement: Instant-Compile-Budget
Die Übersetzung (Lexen bis Bytecode) SHALL ohne globale Analysepasses Die Übersetzung (Lexen bis Bytecode) SHALL ohne globale Analysepasses
auskommen; Vorwärtsreferenzen werden über Fixups aufgelöst. Ein auskommen; Vorwärtsreferenzen werden über Fixups aufgelöst. Ein

View File

@@ -33,6 +33,10 @@ Zuweisung einen Fehler auslösen.
- **WHEN** `Check1.Value = 3` ausgeführt wird - **WHEN** `Check1.Value = 3` ausgeführt wird
- **THEN** tritt Laufzeitfehler 5 auf - **THEN** tritt Laufzeitfehler 5 auf
#### Scenario: Getrennte Werte gleichnamiger Controls
- **WHEN** Form1!Text1.Text auf a und Form2!Text1.Text auf b gesetzt werden
- **THEN** liefern die beiden Zugriffe unabhängig a und b
### Requirement: Container-Zugriff über den Bang-Operator ### Requirement: Container-Zugriff über den Bang-Operator
`form!steuerelement.eigenschaft` SHALL ein Steuerelement eines benannten `form!steuerelement.eigenschaft` SHALL ein Steuerelement eines benannten
Formulars ansprechen. Die Schreibweise MUST NOT mit dem Typsuffix `!` Formulars ansprechen. Die Schreibweise MUST NOT mit dem Typsuffix `!`

View File

@@ -99,10 +99,16 @@ tragen; die Meldungstexte SHALL dem Katalog des Vorbilds folgen, wo eine
Entsprechung existiert (z. B. „Type mismatch", „Duplicate definition", Entsprechung existiert (z. B. „Type mismatch", „Duplicate definition",
„Label not defined", „Subprogram not defined", „Overflow"). „Label not defined", „Subprogram not defined", „Overflow").
Bei Projekten und Includes SHALL jede Diagnose den ursprünglichen Dateinamen mit dessen physischer Zeile und Spalte tragen.
#### Scenario: Position in der Diagnose #### Scenario: Position in der Diagnose
- **WHEN** in Zeile 3 ein Typkonflikt auftritt - **WHEN** in Zeile 3 ein Typkonflikt auftritt
- **THEN** nennt die Diagnose Zeile 3 und die Spalte des Ausdrucks - **THEN** nennt die Diagnose Zeile 3 und die Spalte des Ausdrucks
#### Scenario: Include-Diagnose
- **WHEN** ein eingebundenes Include einen Typfehler enthält
- **THEN** nennt die Diagnose die Include-Datei und ihre eigene Quellposition
### Requirement: Typisiertes HIR als Sema-Ausgabe ### Requirement: Typisiertes HIR als Sema-Ausgabe
Die semantische Analyse SHALL neben den Diagnosen einen typisierten, Die semantische Analyse SHALL neben den Diagnosen einen typisierten,
abgesenkten Zwischenbaum (HIR) liefern, der die Codegen-Eingabe bildet: abgesenkten Zwischenbaum (HIR) liefern, der die Codegen-Eingabe bildet:
@@ -259,6 +265,8 @@ Basis weder ein benutzerdefinierter Typ noch ein bekanntes Objekt ist,
MUST eine Diagnose erzeugen, die Basis und Eigenschaft namentlich nennt; MUST eine Diagnose erzeugen, die Basis und Eigenschaft namentlich nennt;
er MUST NOT zu einer impliziten Variablen mit Punkt im Namen werden. er MUST NOT zu einer impliziten Variablen mit Punkt im Namen werden.
Gleichnamige Steuerelemente unterschiedlicher Formulare SHALL im jeweiligen Formularnamensraum aufgelöst werden; die explizite Containerangabe SHALL vor der lokalen Namenssuche wirken.
#### Scenario: Unbekanntes Objekt wird benannt #### Scenario: Unbekanntes Objekt wird benannt
- **WHEN** ein Modul `Text9.Text = "a"` ohne Steuerelement `Text9` enthält - **WHEN** ein Modul `Text9.Text = "a"` ohne Steuerelement `Text9` enthält
- **THEN** nennt die Diagnose `Text9` als unbekanntes Objekt statt „Type mismatch" zu melden - **THEN** nennt die Diagnose `Text9` als unbekanntes Objekt statt „Type mismatch" zu melden
@@ -275,6 +283,10 @@ er MUST NOT zu einer impliziten Variablen mit Punkt im Namen werden.
- **WHEN** ein Modul `IF TYPEOF Ziel IS CommandButton THEN` enthält - **WHEN** ein Modul `IF TYPEOF Ziel IS CommandButton THEN` enthält
- **THEN** wird es übersetzt und die Klasse gegen die bekannten Steuerelementklassen geprüft - **THEN** wird es übersetzt und die Klasse gegen die bekannten Steuerelementklassen geprüft
#### Scenario: Gleichnamige Controls verschiedener Formulare
- **WHEN** Form1 und Form2 jeweils Text1 enthalten und Form2!Text1.Text gelesen wird
- **THEN** wird Text1 in Form2 aufgelöst und das Programm übersetzt diagnosefrei
### Requirement: Bang-Operator neben dem SINGLE-Suffix ### Requirement: Bang-Operator neben dem SINGLE-Suffix
Der Lexer SHALL `!` als Container-Operator erkennen, wenn ihm ein Der Lexer SHALL `!` als Container-Operator erkennen, wenn ihm ein
Bezeichner folgt, und andernfalls als Typsuffix für SINGLE. Beide Formen Bezeichner folgt, und andernfalls als Typsuffix für SINGLE. Beide Formen

View File

@@ -95,6 +95,8 @@ Arrayelemente, UDT-Felder) inspizieren und die Ausführung fortsetzen
können. Die Tick-Prüfung erfolgt nur an Anweisungsgrenzen, nicht pro können. Die Tick-Prüfung erfolgt nur an Anweisungsgrenzen, nicht pro
Opcode. Opcode.
Ein Breakpoint SHALL durch Modulidentität und physische Quellzeile bestimmt sein. Gleiche Zeilennummern anderer Module MUST NOT denselben Breakpoint auslösen.
#### Scenario: Breakpoint hält an #### Scenario: Breakpoint hält an
- **WHEN** ein Breakpoint auf Zeile 10 gesetzt ist und die Ausführung Zeile 10 erreicht - **WHEN** ein Breakpoint auf Zeile 10 gesetzt ist und die Ausführung Zeile 10 erreicht
- **THEN** hält die VM vor der Anweisung an und meldet Modul und Zeile - **THEN** hält die VM vor der Anweisung an und meldet Modul und Zeile
@@ -103,6 +105,10 @@ Opcode.
- **WHEN** die VM an einem Breakpoint steht - **WHEN** die VM an einem Breakpoint steht
- **THEN** liefert die Inspektions-API den aktuellen Wert einer benannten Variablen - **THEN** liefert die Inspektions-API den aktuellen Wert einer benannten Variablen
#### Scenario: Gleiche Zeile in zwei Modulen
- **WHEN** ein Breakpoint auf Modul A, Zeile 10 gesetzt ist und Modul B Zeile 10 ausführt
- **THEN** läuft B weiter; erst A Zeile 10 hält an
### Requirement: Programmende im Runner ### Requirement: Programmende im Runner
Außerhalb der IDE SHALL `END` (und das Erreichen des Programmendes) den Außerhalb der IDE SHALL `END` (und das Erreichen des Programmendes) den
Prozess mit Exit-Code 0 beenden; `STOP` SHALL mit der Meldung Prozess mit Exit-Code 0 beenden; `STOP` SHALL mit der Meldung