Projektmodule und vollständiges TBC-Kompilat umsetzen und Change archivieren
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
//! `tbc` — Standalone-Compiler von Terminal Basic.
|
||||
//!
|
||||
//! 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 check <datei.bas|datei.frm|projekt.mak>` Syntax/Semantik prüfen
|
||||
//! - `tbc convert-frm <quelle.frm> <ziel.frm>` Binärformular in Text wandeln
|
||||
@@ -10,7 +10,6 @@
|
||||
//! 0 = END/SYSTEM/Programmende · 3 = STOP · 2 = Laufzeitfehler ·
|
||||
//! 1 = Compile-Fehler/Bedienfehler.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::ExitCode;
|
||||
use tb_runtime::host::{Ereignis, Host};
|
||||
@@ -26,7 +25,7 @@ fn main() -> ExitCode {
|
||||
Some("convert-frm") => cmd_convert_frm(&args[1..]),
|
||||
_ => {
|
||||
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)
|
||||
}
|
||||
@@ -128,33 +127,47 @@ fn include_name(line: &str) -> Option<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());
|
||||
if stack.contains(&canonical) {
|
||||
return Err(format!("{}: zyklisches $INCLUDE", path.display()));
|
||||
}
|
||||
stack.push(canonical);
|
||||
let mut out = String::new();
|
||||
for line in source.split_inclusive('\n') {
|
||||
let mut segments = vec![tb_frontend::source::SourceSegment {
|
||||
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 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 =
|
||||
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)
|
||||
.map_err(|error| format!("{}: {error}", included.display()))?;
|
||||
out.push_str(&expand_includes(&included, &text, stack)?);
|
||||
if !out.ends_with('\n') {
|
||||
out.push('\n');
|
||||
}
|
||||
.map_err(|e| format!("{}: {e}", included.display()))?;
|
||||
segments.extend(expand_includes(&included, &text, 1, stack)?);
|
||||
} 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();
|
||||
Ok(out)
|
||||
Ok(segments)
|
||||
}
|
||||
|
||||
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 ") {
|
||||
let source = std::str::from_utf8(&bytes)
|
||||
.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())?;
|
||||
form.code = expand_includes(path, &form.code, &mut Vec::new())?;
|
||||
Ok(form)
|
||||
} else {
|
||||
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)]
|
||||
struct ProjectSymbols {
|
||||
constants: HashSet<String>,
|
||||
types: HashSet<String>,
|
||||
}
|
||||
|
||||
fn append_project_source(target: &mut String, source: &str, symbols: &mut ProjectSymbols) {
|
||||
let mut in_proc = false;
|
||||
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> {
|
||||
fn input_sources(
|
||||
path: &Path,
|
||||
) -> Result<
|
||||
(
|
||||
Vec<tb_frontend::source::SourceUnit>,
|
||||
Vec<tb_ui::frm::FormFile>,
|
||||
),
|
||||
String,
|
||||
> {
|
||||
let extension = path.extension().and_then(|ext| ext.to_str()).unwrap_or("");
|
||||
let paths = if extension.eq_ignore_ascii_case("mak") {
|
||||
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
|
||||
.lines()
|
||||
.map(str::trim)
|
||||
.filter(|line| !line.is_empty())
|
||||
.filter(|line| !line.is_empty() && !line.starts_with('\''))
|
||||
.map(|line| {
|
||||
relative_case_insensitive(base, line)
|
||||
.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 {
|
||||
vec![path.to_path_buf()]
|
||||
};
|
||||
let mut source = String::new();
|
||||
let mut source = Vec::new();
|
||||
let mut forms = Vec::new();
|
||||
let mut symbols = ProjectSymbols::default();
|
||||
for member in paths {
|
||||
if member
|
||||
.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"))
|
||||
{
|
||||
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);
|
||||
} else {
|
||||
let text = std::fs::read_to_string(&member)
|
||||
.map_err(|error| format!("{}: {error}", member.display()))?;
|
||||
let expanded = expand_includes(&member, &text, &mut Vec::new())?;
|
||||
append_project_source(&mut source, &expanded, &mut symbols);
|
||||
source.push(tb_frontend::source::SourceUnit {
|
||||
name: module_name(&member),
|
||||
segments: expand_includes(&member, &text, 1, &mut Vec::new())?,
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok((source, forms))
|
||||
@@ -273,19 +239,26 @@ fn input_sources(path: &Path) -> Result<(String, Vec<tb_ui::frm::FormFile>), Str
|
||||
|
||||
fn compile(
|
||||
path_arg: Option<&String>,
|
||||
) -> Result<
|
||||
(
|
||||
PathBuf,
|
||||
tb_vm::bytecode::CompiledModule,
|
||||
Vec<tb_ui::frm::FormFile>,
|
||||
),
|
||||
ExitCode,
|
||||
> {
|
||||
) -> Result<(PathBuf, tb_vm::bytecode::CompiledModule), ExitCode> {
|
||||
let Some(path) = path_arg else {
|
||||
eprintln!("Aufruf: tbc run|build|check <datei.bas|datei.frm|projekt.mak>");
|
||||
eprintln!("Aufruf: tbc run|build|check <datei.bas|datei.frm|projekt.mak|datei.tbc>");
|
||||
return Err(ExitCode::from(1));
|
||||
};
|
||||
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) {
|
||||
Ok(input) => input,
|
||||
Err(error) => {
|
||||
@@ -293,22 +266,16 @@ fn compile(
|
||||
return Err(ExitCode::from(1));
|
||||
}
|
||||
};
|
||||
let compiled = if forms.is_empty() {
|
||||
tb_vm::compile_source(&module_name(&path), &source)
|
||||
} else {
|
||||
let mut catalog = tb_frontend::forms::FormCatalog::default();
|
||||
for form in &forms {
|
||||
for object in form.catalog().objects {
|
||||
catalog.objects.push(object);
|
||||
}
|
||||
}
|
||||
tb_vm::compile_source_with_forms(&forms[0].root.name, &source, &catalog)
|
||||
};
|
||||
let mut catalog = tb_frontend::forms::FormCatalog::default();
|
||||
for form in &forms {
|
||||
catalog.append(&form.catalog());
|
||||
}
|
||||
let compiled = tb_vm::compile_project(&module_name(&path), &source, &catalog, &forms);
|
||||
match compiled {
|
||||
Ok(m) => Ok((path, m, forms)),
|
||||
Ok(m) => Ok((path, m)),
|
||||
Err(diags) => {
|
||||
for d in &diags {
|
||||
eprintln!("{}:{d}", path.display());
|
||||
eprintln!("{d}");
|
||||
}
|
||||
eprintln!("{} Fehler.", diags.len());
|
||||
Err(ExitCode::from(1))
|
||||
@@ -324,7 +291,7 @@ fn cmd_check(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,
|
||||
Err(code) => return code,
|
||||
};
|
||||
@@ -371,7 +338,7 @@ fn cmd_run(args: &[String]) -> ExitCode {
|
||||
RunEvent::Ended => ExitCode::SUCCESS,
|
||||
RunEvent::Stopped { line } => {
|
||||
// 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)
|
||||
}
|
||||
RunEvent::Error {
|
||||
@@ -379,7 +346,11 @@ fn cmd_run(args: &[String]) -> ExitCode {
|
||||
line,
|
||||
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)
|
||||
}
|
||||
// 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() {
|
||||
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}");
|
||||
if let Ok(path) = relative_case_insensitive(base, &candidate) {
|
||||
return Ok(path);
|
||||
@@ -413,7 +384,7 @@ fn run_chain(
|
||||
size: Option<(usize, usize)>,
|
||||
) -> Result<(RunEvent, Vm), ExitCode> {
|
||||
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));
|
||||
};
|
||||
let mut current = PathBuf::from(first);
|
||||
@@ -421,26 +392,8 @@ fn run_chain(
|
||||
let command = args[1..].join(" ");
|
||||
loop {
|
||||
let current_arg = current.display().to_string();
|
||||
let (path, module, forms) = compile(Some(¤t_arg))?;
|
||||
let (path, module) = compile(Some(¤t_arg))?;
|
||||
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 Err(error) = vm.start_at_line(line) {
|
||||
eprintln!("Runtime error {}: {}", error.0, error);
|
||||
|
||||
130
crates/tb-cli/tests/project.rs
Normal file
130
crates/tb-cli/tests/project.rs
Normal 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();
|
||||
}
|
||||
@@ -49,12 +49,12 @@ pub enum BinOp {
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Expr {
|
||||
IntLit(i16),
|
||||
LongLit(i32),
|
||||
SingleLit(f32),
|
||||
DoubleLit(f64),
|
||||
CurrencyLit(i64),
|
||||
StrLit(String),
|
||||
IntLit(i16, SourcePos),
|
||||
LongLit(i32, SourcePos),
|
||||
SingleLit(f32, SourcePos),
|
||||
DoubleLit(f64, SourcePos),
|
||||
CurrencyLit(i64, SourcePos),
|
||||
StrLit(String, SourcePos),
|
||||
/// Benannter Zugriff: Variable, Arrayelement, Funktionsaufruf oder
|
||||
/// Konstante — Auflösung erfolgt in der Semantik.
|
||||
Name {
|
||||
@@ -90,7 +90,13 @@ pub enum Expr {
|
||||
impl Expr {
|
||||
pub fn pos(&self) -> SourcePos {
|
||||
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::Binary { pos, .. }
|
||||
| Expr::TypeOf { pos, .. } => *pos,
|
||||
@@ -234,7 +240,7 @@ pub enum EventAction {
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Stmt {
|
||||
Label(String),
|
||||
LineNumber(u32),
|
||||
LineNumber(u32, SourcePos),
|
||||
Assign {
|
||||
target: Expr,
|
||||
value: Expr,
|
||||
@@ -259,7 +265,7 @@ pub enum Stmt {
|
||||
If {
|
||||
cond: Expr,
|
||||
then_body: Vec<Stmt>,
|
||||
elseifs: Vec<(Expr, Vec<Stmt>)>,
|
||||
elseifs: Vec<(Expr, Vec<Stmt>, SourcePos)>,
|
||||
else_body: Option<Vec<Stmt>>,
|
||||
pos: SourcePos,
|
||||
},
|
||||
@@ -275,17 +281,20 @@ pub enum Stmt {
|
||||
step: Option<Expr>,
|
||||
body: Vec<Stmt>,
|
||||
pos: SourcePos,
|
||||
end_pos: SourcePos,
|
||||
},
|
||||
DoLoop {
|
||||
pre: Option<(bool, Expr)>, // (ist UNTIL, Bedingung)
|
||||
post: Option<(bool, Expr)>,
|
||||
body: Vec<Stmt>,
|
||||
pos: SourcePos,
|
||||
end_pos: SourcePos,
|
||||
},
|
||||
While {
|
||||
cond: Expr,
|
||||
body: Vec<Stmt>,
|
||||
pos: SourcePos,
|
||||
end_pos: SourcePos,
|
||||
},
|
||||
Goto {
|
||||
target: LabelRef,
|
||||
|
||||
@@ -678,6 +678,7 @@ pub struct FormObject {
|
||||
pub name: String,
|
||||
pub class: ObjectClass,
|
||||
pub parent_form: Option<String>,
|
||||
pub parent: Option<u16>,
|
||||
pub array: bool,
|
||||
}
|
||||
|
||||
@@ -698,6 +699,13 @@ impl FormCatalog {
|
||||
self.objects.push(FormObject {
|
||||
name: name.into().to_uppercase(),
|
||||
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),
|
||||
array,
|
||||
});
|
||||
@@ -712,16 +720,33 @@ impl FormCatalog {
|
||||
.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 {
|
||||
let mut parent = object.parent_form.as_deref();
|
||||
let mut parent = object.parent;
|
||||
for _ in 0..self.objects.len() {
|
||||
let Some(name) = parent else { return false };
|
||||
if name.eq_ignore_ascii_case(form) {
|
||||
let Some(object) = parent.and_then(|id| self.objects.get(id as usize)) else {
|
||||
return false;
|
||||
};
|
||||
if object.name.eq_ignore_ascii_case(form) {
|
||||
return true;
|
||||
}
|
||||
parent = self
|
||||
.find(name)
|
||||
.and_then(|(_, object)| object.parent_form.as_deref());
|
||||
parent = object.parent;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
@@ -124,10 +124,22 @@ pub struct DataItem {
|
||||
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)]
|
||||
pub struct HirModule {
|
||||
pub name: String,
|
||||
pub globals: Vec<HVar>,
|
||||
pub commons: Vec<HCommon>,
|
||||
pub udts: Vec<HUdt>,
|
||||
/// Prozeduren; Index 0 ist das Hauptprogramm.
|
||||
pub procs: Vec<HProc>,
|
||||
@@ -382,6 +394,8 @@ pub enum Builtin {
|
||||
pub enum HExpr {
|
||||
Int(i16),
|
||||
Lng(i32),
|
||||
/// TYPE-Tabellenreferenz für ISAM; beim Projektlinken zu versetzen.
|
||||
UdtId(u16),
|
||||
Sng(f32),
|
||||
Dbl(f64),
|
||||
Cur(i64),
|
||||
@@ -513,6 +527,7 @@ pub enum HResume {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct HStmt {
|
||||
pub pos: crate::SourcePos,
|
||||
pub line: u32,
|
||||
pub kind: HStmtKind,
|
||||
}
|
||||
@@ -596,12 +611,14 @@ pub enum HStmtKind {
|
||||
},
|
||||
/// DO/LOOP, WHILE/WEND (nur `pre`) — Bedingungen: (ist_until, Ausdruck).
|
||||
Loop {
|
||||
end_pos: crate::SourcePos,
|
||||
pre: Option<(bool, HExpr)>,
|
||||
post: Option<(bool, HExpr)>,
|
||||
body: Vec<HStmt>,
|
||||
exit_label: LabelId,
|
||||
},
|
||||
For {
|
||||
end_pos: crate::SourcePos,
|
||||
var: HPlace,
|
||||
ty: NumTy,
|
||||
from: HExpr,
|
||||
|
||||
@@ -342,6 +342,7 @@ pub fn lex(source: &str) -> LexOutput {
|
||||
}
|
||||
let start = i;
|
||||
let pos = SourcePos {
|
||||
source: 0,
|
||||
line: line_no,
|
||||
column: (start + 1) as u32,
|
||||
};
|
||||
@@ -413,6 +414,7 @@ pub fn lex(source: &str) -> LexOutput {
|
||||
TokenKind::Num(NumValue::Int(v as u16 as i16))
|
||||
} else {
|
||||
diagnostics.push(Diagnostic {
|
||||
file: None,
|
||||
pos,
|
||||
message: "Overflow".into(),
|
||||
});
|
||||
@@ -421,6 +423,7 @@ pub fn lex(source: &str) -> LexOutput {
|
||||
tokens.push(Token { kind, pos });
|
||||
}
|
||||
Err(_) => diagnostics.push(Diagnostic {
|
||||
file: None,
|
||||
pos,
|
||||
message: "Syntax error".into(),
|
||||
}),
|
||||
@@ -496,6 +499,7 @@ pub fn lex(source: &str) -> LexOutput {
|
||||
(Some(Suffix::Integer), _) => {
|
||||
if dval > i16::MAX as f64 || dval < i16::MIN as f64 {
|
||||
diagnostics.push(Diagnostic {
|
||||
file: None,
|
||||
pos,
|
||||
message: "Overflow".into(),
|
||||
});
|
||||
@@ -505,6 +509,7 @@ pub fn lex(source: &str) -> LexOutput {
|
||||
(Some(Suffix::Long), _) => {
|
||||
if dval > i32::MAX as f64 || dval < i32::MIN as f64 {
|
||||
diagnostics.push(Diagnostic {
|
||||
file: None,
|
||||
pos,
|
||||
message: "Overflow".into(),
|
||||
});
|
||||
@@ -518,6 +523,7 @@ pub fn lex(source: &str) -> LexOutput {
|
||||
}
|
||||
(Some(Suffix::Str), _) => {
|
||||
diagnostics.push(Diagnostic {
|
||||
file: None,
|
||||
pos,
|
||||
message: "Syntax error".into(),
|
||||
});
|
||||
@@ -674,6 +680,7 @@ pub fn lex(source: &str) -> LexOutput {
|
||||
}
|
||||
other => {
|
||||
diagnostics.push(Diagnostic {
|
||||
file: None,
|
||||
pos,
|
||||
message: format!("Syntax error ('{other}')"),
|
||||
});
|
||||
@@ -692,6 +699,7 @@ pub fn lex(source: &str) -> LexOutput {
|
||||
tokens.push(Token {
|
||||
kind: TokenKind::Eol,
|
||||
pos: SourcePos {
|
||||
source: 0,
|
||||
line: line_no,
|
||||
column: (chars.len() + 1) as u32,
|
||||
},
|
||||
@@ -704,6 +712,7 @@ pub fn lex(source: &str) -> LexOutput {
|
||||
tokens.push(Token {
|
||||
kind: TokenKind::Eof,
|
||||
pos: SourcePos {
|
||||
source: 0,
|
||||
line: (source.lines().count() + 1) as u32,
|
||||
column: 1,
|
||||
},
|
||||
|
||||
@@ -11,10 +11,12 @@ pub mod hir;
|
||||
pub mod lexer;
|
||||
pub mod parser;
|
||||
pub mod sema;
|
||||
pub mod source;
|
||||
|
||||
/// Quelltextposition für Diagnostik (1-basiert, wie im IDE-Vorbild).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct SourcePos {
|
||||
pub source: u32,
|
||||
pub line: u32,
|
||||
pub column: u32,
|
||||
}
|
||||
@@ -23,12 +25,16 @@ pub struct SourcePos {
|
||||
/// Meldungen des Vorbilds, wo es eine Entsprechung gibt.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Diagnostic {
|
||||
pub file: Option<String>,
|
||||
pub pos: SourcePos,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Diagnostic {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,6 +106,7 @@ impl<'a> P<'a> {
|
||||
fn err(&mut self, msg: impl Into<String>) {
|
||||
let pos = self.pos();
|
||||
self.diags.push(Diagnostic {
|
||||
file: None,
|
||||
pos,
|
||||
message: msg.into(),
|
||||
});
|
||||
@@ -199,13 +200,13 @@ impl<'a> P<'a> {
|
||||
if let TokenKind::Num(NumValue::Int(n)) = self.k() {
|
||||
if n >= 0 {
|
||||
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 n >= 0 {
|
||||
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() {
|
||||
@@ -816,7 +817,7 @@ impl<'a> P<'a> {
|
||||
{
|
||||
if w == "ALL" {
|
||||
self.advance();
|
||||
let args = vec![Expr::LongLit(ROLLBACK_ALL)];
|
||||
let args = vec![Expr::LongLit(ROLLBACK_ALL, pos)];
|
||||
return Some(Stmt::Call {
|
||||
name,
|
||||
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 then_body = self.parse_stmt_list(stop);
|
||||
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()?;
|
||||
self.expect_kw(Kw::Then, "THEN");
|
||||
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) {
|
||||
Some(self.parse_stmt_list(|p: &P| p.at_end_pair(Kw::If)))
|
||||
@@ -1349,6 +1352,7 @@ impl<'a> P<'a> {
|
||||
None
|
||||
};
|
||||
let body = self.parse_stmt_list(|p: &P| p.is_kw(Kw::Next));
|
||||
let end_pos = self.pos();
|
||||
if self.eat_kw(Kw::Next) {
|
||||
// Optional: Zählvariable(n) hinter NEXT
|
||||
while matches!(self.k(), TokenKind::Ident { .. }) {
|
||||
@@ -1367,6 +1371,7 @@ impl<'a> P<'a> {
|
||||
step,
|
||||
body,
|
||||
pos,
|
||||
end_pos,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1380,6 +1385,7 @@ impl<'a> P<'a> {
|
||||
None
|
||||
};
|
||||
let body = self.parse_stmt_list(|p: &P| p.is_kw(Kw::Loop));
|
||||
let end_pos = self.pos();
|
||||
let mut post = None;
|
||||
if self.eat_kw(Kw::Loop) {
|
||||
if self.eat_kw(Kw::While) {
|
||||
@@ -1395,6 +1401,7 @@ impl<'a> P<'a> {
|
||||
post,
|
||||
body,
|
||||
pos,
|
||||
end_pos,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1402,10 +1409,16 @@ impl<'a> P<'a> {
|
||||
self.advance(); // WHILE
|
||||
let cond = self.parse_expr()?;
|
||||
let body = self.parse_stmt_list(|p: &P| p.is_kw(Kw::Wend));
|
||||
let end_pos = self.pos();
|
||||
if !self.eat_kw(Kw::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
|
||||
@@ -2214,18 +2227,20 @@ impl<'a> P<'a> {
|
||||
}
|
||||
match self.k() {
|
||||
TokenKind::Num(n) => {
|
||||
let pos = self.pos();
|
||||
self.advance();
|
||||
Some(match n {
|
||||
NumValue::Int(v) => Expr::IntLit(v),
|
||||
NumValue::Long(v) => Expr::LongLit(v),
|
||||
NumValue::Single(v) => Expr::SingleLit(v),
|
||||
NumValue::Double(v) => Expr::DoubleLit(v),
|
||||
NumValue::Currency(v) => Expr::CurrencyLit(v),
|
||||
NumValue::Int(v) => Expr::IntLit(v, pos),
|
||||
NumValue::Long(v) => Expr::LongLit(v, pos),
|
||||
NumValue::Single(v) => Expr::SingleLit(v, pos),
|
||||
NumValue::Double(v) => Expr::DoubleLit(v, pos),
|
||||
NumValue::Currency(v) => Expr::CurrencyLit(v, pos),
|
||||
})
|
||||
}
|
||||
TokenKind::Str(s) => {
|
||||
let pos = self.pos();
|
||||
self.advance();
|
||||
Some(Expr::StrLit(s))
|
||||
Some(Expr::StrLit(s, pos))
|
||||
}
|
||||
TokenKind::LParen => {
|
||||
self.advance();
|
||||
|
||||
@@ -150,6 +150,7 @@ struct Scope {
|
||||
/// Stack der Schleifen-Exit-Labels für `EXIT FOR`/`EXIT DO`.
|
||||
loop_exits: Vec<(LoopKind, LabelId)>,
|
||||
current_line: u32,
|
||||
current_pos: SourcePos,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Copy)]
|
||||
@@ -412,7 +413,13 @@ pub fn lower_with_forms(
|
||||
{
|
||||
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(),
|
||||
deftypes: [const { None }; 26],
|
||||
explicit: false,
|
||||
@@ -427,16 +434,79 @@ pub fn lower_with_forms(
|
||||
module_labels: HashMap::new(),
|
||||
module_line_labels: HashMap::new(),
|
||||
globals: Vec::new(),
|
||||
commons: Vec::new(),
|
||||
data: Vec::new(),
|
||||
data_marks_name: HashMap::new(),
|
||||
data_marks_line: HashMap::new(),
|
||||
hir_procs: Vec::new(),
|
||||
isam_typ: HashMap::new(),
|
||||
forms: catalog,
|
||||
forms: catalog.clone(),
|
||||
module_name: module.name.clone(),
|
||||
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(¶m.name, &None, false).chars().last().unwrap());
|
||||
}
|
||||
}
|
||||
declarations.push(Stmt::Declare { sig, pos: proc.pos });
|
||||
}
|
||||
declarations
|
||||
}
|
||||
|
||||
struct Sema {
|
||||
@@ -467,6 +537,7 @@ struct Sema {
|
||||
module_line_labels: HashMap<u32, LabelId>,
|
||||
/// Globale Slots (Modulvariablen, STATICs, versteckte Temps).
|
||||
globals: Vec<hir::HVar>,
|
||||
commons: Vec<hir::HCommon>,
|
||||
/// DATA-Konstanten des Moduls (aus dem Prescan, statisch).
|
||||
data: Vec<hir::DataItem>,
|
||||
data_marks_name: HashMap<String, u32>,
|
||||
@@ -474,12 +545,14 @@ struct Sema {
|
||||
/// Fertige HIR-Prozeduren nach Id (0 = Hauptprogramm).
|
||||
hir_procs: Vec<Option<hir::HProc>>,
|
||||
forms: FormCatalog,
|
||||
module_name: String,
|
||||
event_procs: Vec<hir::HEventProc>,
|
||||
}
|
||||
|
||||
impl Sema {
|
||||
fn err(&mut self, pos: SourcePos, msg: impl Into<String>) {
|
||||
self.diags.push(Diagnostic {
|
||||
file: None,
|
||||
pos,
|
||||
message: msg.into(),
|
||||
});
|
||||
@@ -489,7 +562,10 @@ impl Sema {
|
||||
// Pass 1: Prozeduren, DECLAREs und TYPEs registrieren.
|
||||
for stmt in &module.body {
|
||||
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 } => {
|
||||
let mut hfields = Vec::new();
|
||||
for (fname, ftype) in fields {
|
||||
@@ -514,8 +590,12 @@ impl Sema {
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let mut defined = HashSet::new();
|
||||
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 {
|
||||
self.register_event_proc(proc);
|
||||
@@ -524,6 +604,8 @@ impl Sema {
|
||||
self.hir_procs
|
||||
.resize_with(self.next_proc_id as usize, || None);
|
||||
|
||||
self.deftypes = [const { None }; 26];
|
||||
|
||||
// Pass 2: Modulrumpf. Labels, Zeilennummern und DATA-Positionen
|
||||
// werden vorab eingesammelt (RESTORE nach vorn, statisches DATA).
|
||||
let mut scope = Scope {
|
||||
@@ -550,7 +632,9 @@ impl Sema {
|
||||
|
||||
// Pass 3: Prozedurrümpfe.
|
||||
for proc in &module.procs {
|
||||
let deftypes = self.deftypes.clone();
|
||||
self.lower_proc(proc);
|
||||
self.deftypes = deftypes;
|
||||
}
|
||||
|
||||
// Zusammensetzen: fehlende Rümpfe (nur DECLARE) behalten ihre
|
||||
@@ -574,7 +658,7 @@ impl Sema {
|
||||
name: format!("ARG{index}"),
|
||||
ty: self.h_ty(ty),
|
||||
array: *array,
|
||||
by_ref: !array && !matches!(ty, Ty::Udt(_)),
|
||||
by_ref: !array,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut locals = params
|
||||
@@ -614,6 +698,7 @@ impl Sema {
|
||||
Some(hir::HirModule {
|
||||
name: module.name.clone(),
|
||||
globals: std::mem::take(&mut self.globals),
|
||||
commons: std::mem::take(&mut self.commons),
|
||||
udts: std::mem::take(&mut self.udt_defs),
|
||||
procs,
|
||||
data: std::mem::take(&mut self.data),
|
||||
@@ -638,11 +723,11 @@ impl Sema {
|
||||
let hty = self.h_ty(&ty);
|
||||
let slot = scope.locals.len() as u16;
|
||||
scope.locals.push(hir::HVar {
|
||||
name: p.name.clone(),
|
||||
name: key.trim_end_matches("\u{1}AS").to_string(),
|
||||
ty: hty.clone(),
|
||||
array: p.array,
|
||||
});
|
||||
let by_ref = !p.array && !matches!(ty, Ty::Udt(_));
|
||||
let by_ref = !p.array;
|
||||
scope.vars.insert(
|
||||
key,
|
||||
VarInfo {
|
||||
@@ -669,7 +754,7 @@ impl Sema {
|
||||
let key = self.var_key(&proc.sig.name, &proc.sig.suffix, false);
|
||||
let slot = scope.locals.len() as u16;
|
||||
scope.locals.push(hir::HVar {
|
||||
name: proc.sig.name.clone(),
|
||||
name: key.clone(),
|
||||
ty: self.h_ty(&ret),
|
||||
array: false,
|
||||
});
|
||||
@@ -721,7 +806,7 @@ impl Sema {
|
||||
.insert(n.clone(), self.data.len() as u32);
|
||||
}
|
||||
}
|
||||
Stmt::LineNumber(n) => {
|
||||
Stmt::LineNumber(n, _) => {
|
||||
let id = scope.new_label();
|
||||
scope.line_labels.insert(*n, id);
|
||||
if module_data {
|
||||
@@ -745,7 +830,7 @@ impl Sema {
|
||||
..
|
||||
} => {
|
||||
self.prescan(then_body, scope, module_data);
|
||||
for (_, b) in elseifs {
|
||||
for (_, b, _) in elseifs {
|
||||
self.prescan(b, scope, module_data);
|
||||
}
|
||||
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 {
|
||||
ProcKind::Function => self.name_ty(&sig.name, &sig.suffix),
|
||||
ProcKind::Sub => Ty::Unknown,
|
||||
};
|
||||
let params = sig
|
||||
let params: Vec<_> = sig
|
||||
.params
|
||||
.iter()
|
||||
.map(|p| (self.param_ty(p), p.array))
|
||||
.collect();
|
||||
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 => {
|
||||
let id = self.next_proc_id;
|
||||
self.next_proc_id += 1;
|
||||
@@ -895,14 +986,9 @@ impl Sema {
|
||||
fn event_binding(&self, name: &str) -> Option<(u16, ObjectClass, String)> {
|
||||
let (base, event) = name.rsplit_once('_')?;
|
||||
let found = if base == "FORM" {
|
||||
self.forms
|
||||
.objects
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, o)| o.class == ObjectClass::Form)
|
||||
.map(|(i, o)| (i as u16, o))
|
||||
self.current_form()
|
||||
} else {
|
||||
self.forms.find(base)
|
||||
self.find_object(base)
|
||||
}?;
|
||||
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(
|
||||
&mut self,
|
||||
name: &str,
|
||||
pos: SourcePos,
|
||||
) -> 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()));
|
||||
}
|
||||
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) {
|
||||
return None;
|
||||
}
|
||||
let (object, form) = self
|
||||
.forms
|
||||
.objects
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, object)| object.class == ObjectClass::Form)?;
|
||||
let (object, form) = self.current_form()?;
|
||||
let (property, spec) = forms::property(form.class, name)?;
|
||||
Some((object as u16, property, spec))
|
||||
Some((object, property, spec))
|
||||
}
|
||||
|
||||
fn object_property(
|
||||
@@ -958,17 +1095,7 @@ impl Sema {
|
||||
let (object, member) = path.split_once('.')?;
|
||||
(object, member, None)
|
||||
};
|
||||
let (id, object) = self.object_by_name(object_name, 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 (id, object) = self.scoped_object(object_name, parent, pos)?;
|
||||
let Some((property, spec)) = forms::property(object.class, member) else {
|
||||
self.err(
|
||||
pos,
|
||||
@@ -991,16 +1118,7 @@ impl Sema {
|
||||
let (object, member) = path.split_once('.')?;
|
||||
(object, member, None)
|
||||
};
|
||||
let (id, object) = self.object_by_name(object_name, 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;
|
||||
}
|
||||
}
|
||||
let (id, object) = self.scoped_object(object_name, parent, pos)?;
|
||||
Some((id, object, member.to_string()))
|
||||
}
|
||||
|
||||
@@ -1118,7 +1236,7 @@ impl Sema {
|
||||
fn alloc_global(&mut self, name: &str, ty: &Ty, array: bool) -> u16 {
|
||||
let slot = self.globals.len() as u16;
|
||||
self.globals.push(hir::HVar {
|
||||
name: name.to_string(),
|
||||
name: name.trim_end_matches("\u{1}AS").to_string(),
|
||||
ty: self.h_ty(ty),
|
||||
array,
|
||||
});
|
||||
@@ -1138,7 +1256,7 @@ impl Sema {
|
||||
} else {
|
||||
let slot = scope.locals.len() as u16;
|
||||
scope.locals.push(hir::HVar {
|
||||
name: name.to_string(),
|
||||
name: name.trim_end_matches("\u{1}AS").to_string(),
|
||||
ty: self.h_ty(ty),
|
||||
array,
|
||||
});
|
||||
@@ -1255,7 +1373,7 @@ impl Sema {
|
||||
let ty = self.name_ty(name, suffix);
|
||||
// DEF FN: freie Namen binden an Modulvariablen (Vorbild-Semantik).
|
||||
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 {
|
||||
ty,
|
||||
array,
|
||||
@@ -1267,7 +1385,7 @@ impl Sema {
|
||||
self.module_vars.insert(key, info.clone());
|
||||
info
|
||||
} else {
|
||||
let (slot, global) = self.alloc_var(scope, name, &ty, array);
|
||||
let (slot, global) = self.alloc_var(scope, &key, &ty, array);
|
||||
let info = VarInfo {
|
||||
ty,
|
||||
array,
|
||||
@@ -1285,12 +1403,12 @@ impl Sema {
|
||||
/// Konstantenfaltung für `CONST`-Ausdrücke.
|
||||
fn fold_const(&self, e: &Expr) -> Option<ConstVal> {
|
||||
match e {
|
||||
Expr::IntLit(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::DoubleLit(v) => Some(ConstVal::Num(*v)),
|
||||
Expr::CurrencyLit(v) => Some(ConstVal::Num(*v as f64 / 10_000.0)),
|
||||
Expr::StrLit(s) => Some(ConstVal::Str(s.clone())),
|
||||
Expr::IntLit(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::DoubleLit(v, _) => Some(ConstVal::Num(*v)),
|
||||
Expr::CurrencyLit(v, _) => Some(ConstVal::Num(*v as f64 / 10_000.0)),
|
||||
Expr::StrLit(s, _) => Some(ConstVal::Str(s.clone())),
|
||||
Expr::Paren(e) => self.fold_const(e),
|
||||
Expr::Name {
|
||||
name, args: None, ..
|
||||
@@ -1400,15 +1518,18 @@ impl Sema {
|
||||
// ---- Anweisungen -------------------------------------------------------
|
||||
|
||||
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();
|
||||
for stmt in stmts {
|
||||
self.lower_stmt(stmt, scope, &mut out);
|
||||
}
|
||||
(scope.current_line, scope.current_pos) = enclosing;
|
||||
out
|
||||
}
|
||||
|
||||
fn push(&mut self, out: &mut Vec<HStmt>, scope: &Scope, kind: HStmtKind) {
|
||||
out.push(HStmt {
|
||||
pos: scope.current_pos,
|
||||
line: scope.current_line,
|
||||
kind,
|
||||
});
|
||||
@@ -1419,6 +1540,7 @@ impl Sema {
|
||||
let pos = stmt_pos(stmt);
|
||||
if pos.line > 0 {
|
||||
scope.current_line = pos.line;
|
||||
scope.current_pos = pos;
|
||||
}
|
||||
match stmt {
|
||||
Stmt::Data { .. }
|
||||
@@ -1430,7 +1552,7 @@ impl Sema {
|
||||
self.push(out, scope, HStmtKind::Label(id));
|
||||
}
|
||||
}
|
||||
Stmt::LineNumber(n) => {
|
||||
Stmt::LineNumber(n, _) => {
|
||||
if let Some(id) = scope.line_labels.get(n).copied() {
|
||||
self.push(out, scope, HStmtKind::Label(id));
|
||||
}
|
||||
@@ -1637,7 +1759,7 @@ impl Sema {
|
||||
}
|
||||
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.lower_expr(value, scope);
|
||||
return;
|
||||
@@ -1895,11 +2017,12 @@ impl Sema {
|
||||
Some(b) => self.lower_body(b, scope),
|
||||
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 body2 = self.lower_body(eb, scope);
|
||||
let stmt = HStmt {
|
||||
line: scope.current_line,
|
||||
pos: *elseif_pos,
|
||||
line: elseif_pos.line,
|
||||
kind: HStmtKind::If {
|
||||
cond: c2,
|
||||
then: body2,
|
||||
@@ -1920,6 +2043,7 @@ impl Sema {
|
||||
step,
|
||||
body,
|
||||
pos,
|
||||
end_pos,
|
||||
} => {
|
||||
let (place, vt) = self.lower_place(var, scope);
|
||||
if !is_num(&vt) {
|
||||
@@ -1956,6 +2080,7 @@ impl Sema {
|
||||
ty,
|
||||
from: from_e,
|
||||
to: to_e,
|
||||
end_pos: *end_pos,
|
||||
step: step_e,
|
||||
limit_slot,
|
||||
step_slot,
|
||||
@@ -1966,7 +2091,11 @@ impl Sema {
|
||||
}
|
||||
}
|
||||
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 exit_label = scope.new_label();
|
||||
@@ -1978,6 +2107,7 @@ impl Sema {
|
||||
out,
|
||||
scope,
|
||||
HStmtKind::Loop {
|
||||
end_pos: *end_pos,
|
||||
pre: pre_c,
|
||||
post: post_c,
|
||||
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 exit_label = scope.new_label();
|
||||
let hbody = self.lower_body(body, scope);
|
||||
@@ -1993,6 +2128,7 @@ impl Sema {
|
||||
out,
|
||||
scope,
|
||||
HStmtKind::Loop {
|
||||
end_pos: *end_pos,
|
||||
pre: Some((false, c)),
|
||||
post: None,
|
||||
body: hbody,
|
||||
@@ -2120,8 +2256,11 @@ impl Sema {
|
||||
let ty = self.decl_ty(d);
|
||||
let key = self.var_key(&d.name, &d.suffix, d.as_type.is_some());
|
||||
if let Entry::Vacant(entry) = scope.vars.entry(key) {
|
||||
let slot =
|
||||
self.alloc_global(&format!("STATIC.{}", d.name), &ty, d.dims.is_some());
|
||||
let slot = self.alloc_global(
|
||||
&format!("STATIC.{}", entry.key()),
|
||||
&ty,
|
||||
d.dims.is_some(),
|
||||
);
|
||||
entry.insert(VarInfo {
|
||||
ty,
|
||||
array: d.dims.is_some(),
|
||||
@@ -2133,7 +2272,12 @@ impl Sema {
|
||||
}
|
||||
}
|
||||
}
|
||||
Stmt::CommonDecl { shared, decls, .. } => {
|
||||
Stmt::CommonDecl {
|
||||
shared,
|
||||
decls,
|
||||
block,
|
||||
..
|
||||
} => {
|
||||
for d in decls {
|
||||
if *shared && scope.is_module {
|
||||
self.shared_vars.insert(self.var_key(
|
||||
@@ -2143,6 +2287,30 @@ impl Sema {
|
||||
));
|
||||
}
|
||||
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, .. } => {
|
||||
@@ -2153,7 +2321,7 @@ impl Sema {
|
||||
Some(v) => v.clone(),
|
||||
None => {
|
||||
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 {
|
||||
ty,
|
||||
array: d.dims.is_some(),
|
||||
@@ -2255,7 +2423,7 @@ impl Sema {
|
||||
body,
|
||||
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 {
|
||||
name,
|
||||
@@ -2264,7 +2432,7 @@ impl Sema {
|
||||
body,
|
||||
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) ----
|
||||
Stmt::Open {
|
||||
@@ -2318,7 +2486,7 @@ impl Sema {
|
||||
ne,
|
||||
HExpr::Str(table.clone()),
|
||||
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.
|
||||
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();
|
||||
for arm in arms {
|
||||
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.
|
||||
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 {
|
||||
line: scope.current_line,
|
||||
pos,
|
||||
line: pos.line,
|
||||
kind: HStmtKind::If {
|
||||
cond,
|
||||
then: body,
|
||||
@@ -2947,7 +3116,6 @@ impl Sema {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn lower_def_fn(
|
||||
&mut self,
|
||||
name: &str,
|
||||
@@ -2955,7 +3123,6 @@ impl Sema {
|
||||
params: &[Param],
|
||||
block: Option<&[Stmt]>,
|
||||
single: Option<&Expr>,
|
||||
scope: &mut Scope,
|
||||
pos: SourcePos,
|
||||
) {
|
||||
// Registrierung (mit Prozedur-Id).
|
||||
@@ -2981,12 +3148,12 @@ impl Sema {
|
||||
def_fn: true,
|
||||
},
|
||||
);
|
||||
let _ = pos;
|
||||
|
||||
// Rumpf-Scope: Parameter (BYVAL) und Rückgabevariable lokal,
|
||||
// freie Namen binden an Modulvariablen.
|
||||
let mut fscope = Scope {
|
||||
is_def_fn: true,
|
||||
current_line: pos.line,
|
||||
current_pos: pos,
|
||||
..Scope::default()
|
||||
};
|
||||
let mut hparams = Vec::new();
|
||||
@@ -2995,7 +3162,7 @@ impl Sema {
|
||||
let key = self.var_key(&p.name, &p.suffix, false);
|
||||
let slot = fscope.locals.len() as u16;
|
||||
fscope.locals.push(hir::HVar {
|
||||
name: p.name.clone(),
|
||||
name: key.trim_end_matches("\u{1}AS").to_string(),
|
||||
ty: self.h_ty(&ty),
|
||||
array: false,
|
||||
});
|
||||
@@ -3020,7 +3187,7 @@ impl Sema {
|
||||
let ret_key = self.var_key(name, suffix, false);
|
||||
let ret_slot_idx = fscope.locals.len() as u16;
|
||||
fscope.locals.push(hir::HVar {
|
||||
name: name.to_string(),
|
||||
name: name.trim_end_matches("\u{1}AS").to_string(),
|
||||
ty: self.h_ty(&ret),
|
||||
array: false,
|
||||
});
|
||||
@@ -3045,6 +3212,7 @@ impl Sema {
|
||||
let (e, et) = self.lower_expr(expr, &mut fscope);
|
||||
let value = self.coerce(e, &et, &ret, expr.pos());
|
||||
vec![HStmt {
|
||||
pos: fscope.current_pos,
|
||||
line: fscope.current_line,
|
||||
kind: HStmtKind::Assign {
|
||||
place: HPlace {
|
||||
@@ -3072,14 +3240,24 @@ impl Sema {
|
||||
body,
|
||||
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 {
|
||||
self.hir_procs.push(None);
|
||||
}
|
||||
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>) {
|
||||
let ty = self.decl_ty(d);
|
||||
let mut hdims = Vec::new();
|
||||
@@ -3120,7 +3298,7 @@ impl Sema {
|
||||
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(
|
||||
scope,
|
||||
key,
|
||||
@@ -3249,13 +3427,7 @@ impl Sema {
|
||||
return;
|
||||
}
|
||||
if matches!(name, "SHOW" | "HIDE") && args.is_empty() {
|
||||
if let Some((object, _)) = self
|
||||
.forms
|
||||
.objects
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, object)| object.class == ObjectClass::Form)
|
||||
{
|
||||
if let Some((object, _)) = self.current_form() {
|
||||
let method = forms::methods(ObjectClass::Form)
|
||||
.iter()
|
||||
.position(|method| *method == name)
|
||||
@@ -3264,7 +3436,7 @@ impl Sema {
|
||||
out,
|
||||
scope,
|
||||
HStmtKind::ObjectMethod {
|
||||
object: object as u16,
|
||||
object,
|
||||
index: None,
|
||||
method,
|
||||
args: Vec::new(),
|
||||
@@ -3343,20 +3515,15 @@ impl Sema {
|
||||
pos: object_pos,
|
||||
} = &args[0]
|
||||
{
|
||||
if let Some((object, info)) =
|
||||
self.forms.find(object_name).map(|(i, o)| (i, o.clone()))
|
||||
{
|
||||
if index.is_some() && !info.array {
|
||||
self.err(
|
||||
*object_pos,
|
||||
format!("Object '{object_name}' is not an array"),
|
||||
);
|
||||
let (parent, object_name) = object_name
|
||||
.split_once('!')
|
||||
.map_or((None, object_name.as_str()), |(parent, name)| {
|
||||
(Some(parent), name)
|
||||
});
|
||||
if let Some((object, _)) = self.scoped_object(object_name, parent, *object_pos) {
|
||||
let Ok(index) = self.object_index(object, index, scope, *object_pos) else {
|
||||
return;
|
||||
}
|
||||
let index = index
|
||||
.as_ref()
|
||||
.and_then(|a| a.first())
|
||||
.map(|e| self.lower_num_as(e, scope, NumTy::Lng));
|
||||
};
|
||||
self.push(
|
||||
out,
|
||||
scope,
|
||||
@@ -3368,7 +3535,6 @@ impl Sema {
|
||||
);
|
||||
return;
|
||||
}
|
||||
self.err(*object_pos, format!("Unknown object '{object_name}'"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -3514,7 +3680,7 @@ impl Sema {
|
||||
if self.consts.contains_key(name) {
|
||||
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))
|
||||
{
|
||||
return false;
|
||||
@@ -3978,7 +4144,7 @@ impl Sema {
|
||||
/// Greift nur bei Stringliteralen; ein zur Laufzeit gebildeter Gerätename
|
||||
/// bleibt der Datei-E/A überlassen.
|
||||
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) {
|
||||
self.err(pos, "Feature unavailable");
|
||||
}
|
||||
@@ -4108,7 +4274,7 @@ impl Sema {
|
||||
return (
|
||||
Some(HPlace {
|
||||
base: base_slot,
|
||||
base_is_ref: false,
|
||||
base_is_ref: info.by_ref,
|
||||
indices: indices.unwrap_or_default(),
|
||||
fields: fpath,
|
||||
ty: hty,
|
||||
@@ -4157,12 +4323,12 @@ impl Sema {
|
||||
|
||||
fn lower_expr(&mut self, e: &Expr, scope: &mut Scope) -> (HExpr, Ty) {
|
||||
match e {
|
||||
Expr::IntLit(v) => (HExpr::Int(*v), Ty::Int),
|
||||
Expr::LongLit(v) => (HExpr::Lng(*v), Ty::Lng),
|
||||
Expr::SingleLit(v) => (HExpr::Sng(*v), Ty::Sng),
|
||||
Expr::DoubleLit(v) => (HExpr::Dbl(*v), Ty::Dbl),
|
||||
Expr::CurrencyLit(v) => (HExpr::Cur(*v), Ty::Cur),
|
||||
Expr::StrLit(s) => (HExpr::Str(s.clone()), Ty::Str),
|
||||
Expr::IntLit(v, _) => (HExpr::Int(*v), Ty::Int),
|
||||
Expr::LongLit(v, _) => (HExpr::Lng(*v), Ty::Lng),
|
||||
Expr::SingleLit(v, _) => (HExpr::Sng(*v), Ty::Sng),
|
||||
Expr::DoubleLit(v, _) => (HExpr::Dbl(*v), Ty::Dbl),
|
||||
Expr::CurrencyLit(v, _) => (HExpr::Cur(*v), Ty::Cur),
|
||||
Expr::StrLit(s, _) => (HExpr::Str(s.clone()), Ty::Str),
|
||||
Expr::Paren(inner) => self.lower_expr(inner, scope),
|
||||
Expr::Missing => (HExpr::Int(0), Ty::Unknown),
|
||||
Expr::TypeOf { value, class, pos } => {
|
||||
@@ -4659,7 +4825,11 @@ impl Sema {
|
||||
);
|
||||
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 {
|
||||
let Ok(index) = self.object_index(object, args, scope, pos) else {
|
||||
return (HExpr::Int(0), Ty::Unknown);
|
||||
@@ -5296,7 +5466,7 @@ impl Sema {
|
||||
}
|
||||
|
||||
/// Position einer Anweisung (für Zeileninfo der Anweisungsgrenzen).
|
||||
fn stmt_pos(stmt: &Stmt) -> SourcePos {
|
||||
pub fn stmt_pos(stmt: &Stmt) -> SourcePos {
|
||||
match stmt {
|
||||
Stmt::Assign { pos, .. }
|
||||
| Stmt::Print { pos, .. }
|
||||
@@ -5352,7 +5522,8 @@ fn stmt_pos(stmt: &Stmt) -> SourcePos {
|
||||
| Stmt::End(pos)
|
||||
| Stmt::StopStmt(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.
|
||||
fn const_zahl(e: &Expr) -> Option<i64> {
|
||||
match e {
|
||||
Expr::IntLit(n) => Some(*n as i64),
|
||||
Expr::LongLit(n) => Some(*n as i64),
|
||||
Expr::SingleLit(n) => Some(*n as i64),
|
||||
Expr::DoubleLit(n) => Some(*n as i64),
|
||||
Expr::IntLit(n, _) => Some(*n as i64),
|
||||
Expr::LongLit(n, _) => Some(*n as i64),
|
||||
Expr::SingleLit(n, _) => Some(*n as i64),
|
||||
Expr::DoubleLit(n, _) => Some(*n as i64),
|
||||
Expr::Paren(inner) => const_zahl(inner),
|
||||
_ => None,
|
||||
}
|
||||
|
||||
96
crates/tb-frontend/src/source.rs
Normal file
96
crates/tb-frontend/src/source.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
@@ -252,12 +252,7 @@ impl FormsModel {
|
||||
return Ok(PropertyValue::Integer(value.saturating_sub(2)));
|
||||
}
|
||||
if spec.name == "PARENT" {
|
||||
let parent = obj.description.parent_form.as_deref().and_then(|name| {
|
||||
self.objects
|
||||
.iter()
|
||||
.position(|candidate| candidate.description.name.eq_ignore_ascii_case(name))
|
||||
.map(|id| (id as u16, None))
|
||||
});
|
||||
let parent = obj.description.parent.map(|id| (id, None));
|
||||
return Ok(PropertyValue::Object(parent));
|
||||
}
|
||||
Ok(obj.properties[property as usize].clone())
|
||||
@@ -834,12 +829,7 @@ impl FormsModel {
|
||||
if obj.description.class == ObjectClass::Form {
|
||||
return Some(current);
|
||||
}
|
||||
let parent = obj.description.parent_form.as_deref()?;
|
||||
current = self
|
||||
.objects
|
||||
.iter()
|
||||
.position(|candidate| candidate.description.name.eq_ignore_ascii_case(parent))?
|
||||
as u16;
|
||||
current = obj.description.parent?;
|
||||
}
|
||||
None
|
||||
}
|
||||
@@ -959,7 +949,7 @@ impl FormsModel {
|
||||
}
|
||||
|
||||
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")
|
||||
.unwrap()
|
||||
.0 as usize;
|
||||
@@ -969,7 +959,7 @@ impl FormsModel {
|
||||
}
|
||||
let same_group = self.instance(other).is_ok_and(|obj| {
|
||||
obj.description.class == ObjectClass::OptionButton
|
||||
&& obj.description.parent_form == parent
|
||||
&& obj.description.parent == parent
|
||||
});
|
||||
if same_group {
|
||||
self.instance_mut(other)?.properties[value_id] = PropertyValue::Integer(0);
|
||||
@@ -1006,12 +996,8 @@ impl FormsModel {
|
||||
&& self
|
||||
.instance(key)
|
||||
.ok()
|
||||
.and_then(|obj| obj.description.parent_form.as_deref())
|
||||
.and_then(|parent| {
|
||||
self.objects
|
||||
.iter()
|
||||
.find(|obj| obj.description.name.eq_ignore_ascii_case(parent))
|
||||
})
|
||||
.and_then(|obj| obj.description.parent)
|
||||
.and_then(|parent| self.objects.get(parent as usize))
|
||||
.is_some_and(|parent| parent.description.class == ObjectClass::Form);
|
||||
if invalid || title_shortcut {
|
||||
Err(RuntimeError::ILLEGAL_FUNCTION_CALL)
|
||||
@@ -1325,19 +1311,12 @@ impl FormsModel {
|
||||
}
|
||||
|
||||
fn menu_children(&self, parent: ObjectKey) -> Vec<ObjectKey> {
|
||||
let Ok(parent) = self.instance(parent) else {
|
||||
return Vec::new();
|
||||
};
|
||||
self.keys()
|
||||
.into_iter()
|
||||
.filter(|key| {
|
||||
self.instance(*key).is_ok_and(|object| {
|
||||
object.description.class == ObjectClass::Menu
|
||||
&& object
|
||||
.description
|
||||
.parent_form
|
||||
.as_deref()
|
||||
.is_some_and(|name| name.eq_ignore_ascii_case(&parent.description.name))
|
||||
&& object.description.parent == Some(parent.0)
|
||||
}) && self.boolean(*key, "VISIBLE")
|
||||
})
|
||||
.collect()
|
||||
@@ -1385,23 +1364,15 @@ impl FormsModel {
|
||||
}
|
||||
if shift & umschalt::ALT != 0 {
|
||||
if let Some(ch) = key.chars().next().map(|ch| ch.to_ascii_uppercase()) {
|
||||
if let Some(target) =
|
||||
self.form_controls(form).into_iter().find(|candidate| {
|
||||
Self::access_key(&self.string(*candidate, "CAPTION")) == Some(ch)
|
||||
&& self.boolean(*candidate, "ENABLED")
|
||||
&& self.boolean(*candidate, "VISIBLE")
|
||||
&& self.instance(*candidate).is_ok_and(|obj| {
|
||||
obj.description.class != ObjectClass::Menu
|
||||
|| obj.description.parent_form.as_deref().is_some_and(
|
||||
|parent| {
|
||||
parent.eq_ignore_ascii_case(
|
||||
&self.objects[form as usize].description.name,
|
||||
)
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
{
|
||||
if let Some(target) = self.form_controls(form).into_iter().find(|candidate| {
|
||||
Self::access_key(&self.string(*candidate, "CAPTION")) == Some(ch)
|
||||
&& self.boolean(*candidate, "ENABLED")
|
||||
&& self.boolean(*candidate, "VISIBLE")
|
||||
&& self.instance(*candidate).is_ok_and(|obj| {
|
||||
obj.description.class != ObjectClass::Menu
|
||||
|| obj.description.parent == Some(form)
|
||||
})
|
||||
}) {
|
||||
if self
|
||||
.instance(target)
|
||||
.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 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 mut parent = obj.description.parent_form.as_deref();
|
||||
let mut parent = obj.description.parent;
|
||||
for _ in 0..self.objects.len() {
|
||||
let Some(name) = parent else { break };
|
||||
let Some((id, parent_obj)) = self
|
||||
.objects
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, candidate)| candidate.description.name.eq_ignore_ascii_case(name))
|
||||
else {
|
||||
break;
|
||||
};
|
||||
let parent_key = (id as u16, None);
|
||||
let Some(id) = parent else { break };
|
||||
let parent_obj = self.objects.get(id as usize)?;
|
||||
let parent_key = (id, None);
|
||||
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;
|
||||
parent = parent_obj.description.parent_form.as_deref();
|
||||
parent = parent_obj.description.parent;
|
||||
}
|
||||
Some((left, top, width, height))
|
||||
}
|
||||
@@ -2357,12 +2321,7 @@ impl FormsModel {
|
||||
&& self
|
||||
.instance(key)
|
||||
.ok()
|
||||
.and_then(|obj| obj.description.parent_form.as_deref())
|
||||
.is_some_and(|parent| {
|
||||
parent.eq_ignore_ascii_case(
|
||||
&self.objects[form as usize].description.name,
|
||||
)
|
||||
})
|
||||
.is_some_and(|obj| obj.description.parent == Some(form))
|
||||
{
|
||||
if self.boolean(key, "VISIBLE") {
|
||||
let caption = format!(" {} ", self.caption(key));
|
||||
@@ -2419,20 +2378,12 @@ impl FormsModel {
|
||||
.is_some_and(|root| self.root_form(*root) == Some(form))
|
||||
{
|
||||
let mut popup_left = left + 1;
|
||||
for root in
|
||||
self.form_controls(form).into_iter().filter(|key| {
|
||||
self.instance(*key).is_ok_and(|object| {
|
||||
object.description.class == ObjectClass::Menu
|
||||
&& object.description.parent_form.as_deref().is_some_and(
|
||||
|parent| {
|
||||
parent.eq_ignore_ascii_case(
|
||||
&self.objects[form as usize].description.name,
|
||||
)
|
||||
},
|
||||
)
|
||||
}) && self.boolean(*key, "VISIBLE")
|
||||
})
|
||||
{
|
||||
for root in self.form_controls(form).into_iter().filter(|key| {
|
||||
self.instance(*key).is_ok_and(|object| {
|
||||
object.description.class == ObjectClass::Menu
|
||||
&& object.description.parent == Some(form)
|
||||
}) && self.boolean(*key, "VISIBLE")
|
||||
}) {
|
||||
if Some(&root) == self.menu_path.first() {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -56,9 +56,7 @@ impl FormFile {
|
||||
} else {
|
||||
let array = forms::property(node.class, "INDEX")
|
||||
.and_then(|(property, _)| node.properties.get(&property))
|
||||
.is_some_and(
|
||||
|value| matches!(value, PropertyValue::Integer(index) if *index != 0),
|
||||
);
|
||||
.is_some();
|
||||
catalog.add(&node.name, node.class, parent, array);
|
||||
}
|
||||
for child in &node.children {
|
||||
@@ -70,44 +68,99 @@ impl FormFile {
|
||||
catalog
|
||||
}
|
||||
|
||||
pub fn apply(&self, model: &mut FormsModel) -> Result<(), tb_runtime::errors::RuntimeError> {
|
||||
fn apply_node(
|
||||
pub fn code_line(&self) -> u32 {
|
||||
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,
|
||||
model: &mut FormsModel,
|
||||
menu_depth: usize,
|
||||
parent: Option<u16>,
|
||||
catalog: &FormCatalog,
|
||||
out: &mut Vec<FormInitial>,
|
||||
depth: usize,
|
||||
) -> Result<(), tb_runtime::errors::RuntimeError> {
|
||||
let menu_depth = if node.class == ObjectClass::Menu {
|
||||
menu_depth + 1
|
||||
} else {
|
||||
menu_depth
|
||||
};
|
||||
if menu_depth > 6 {
|
||||
return Err(tb_runtime::errors::RuntimeError::ILLEGAL_FUNCTION_CALL);
|
||||
let depth = depth + usize::from(node.class == ObjectClass::Menu);
|
||||
if depth > 6 {
|
||||
return Err(tb_runtime::errors::RuntimeError(5));
|
||||
}
|
||||
let object = model
|
||||
let object = catalog
|
||||
.objects
|
||||
.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;
|
||||
let index = forms::property(node.class, "INDEX")
|
||||
.and_then(|(property, _)| node.properties.get(&property))
|
||||
.and_then(|value| match value {
|
||||
PropertyValue::Integer(value) => Some(*value),
|
||||
_ => None,
|
||||
.and_then(|(id, _)| node.properties.get(&id))
|
||||
.and_then(|v| {
|
||||
if let PropertyValue::Integer(n) = v {
|
||||
Some(*n)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or(0);
|
||||
if index != 0 && !model.is_loaded_at(object, Some(index)) {
|
||||
model.load_design_array(object, index)?;
|
||||
}
|
||||
for (property, value) in &node.properties {
|
||||
model.set_initial_at(object, Some(index), *property, value.clone())?;
|
||||
if out.iter().any(|v| v.object == object && v.index == index) {
|
||||
return Err(tb_runtime::errors::RuntimeError(5));
|
||||
}
|
||||
out.push(FormInitial {
|
||||
object,
|
||||
index,
|
||||
properties: node.properties.clone(),
|
||||
});
|
||||
for child in &node.children {
|
||||
apply_node(child, model, menu_depth)?;
|
||||
collect(child, Some(object), catalog, out, depth)?;
|
||||
}
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,12 +35,13 @@ fn generate_module(n_blocks: usize, seed: usize) -> String {
|
||||
}
|
||||
|
||||
fn compile_all(sources: &[(String, String)]) -> usize {
|
||||
let mut total = 0;
|
||||
for (name, src) in sources {
|
||||
let m = tb_vm::compile_source(name, src).expect("Benchmark-Quelle muss kompilieren");
|
||||
total += m.procs.iter().map(|p| p.code.len()).sum::<usize>();
|
||||
}
|
||||
total
|
||||
let units: Vec<_> = sources
|
||||
.iter()
|
||||
.map(|(name, src)| tb_frontend::source::SourceUnit::new(name, &format!("{name}.bas"), src))
|
||||
.collect();
|
||||
let project = tb_vm::compile_project("BENCH", &units, &Default::default(), &[])
|
||||
.expect("Benchmark-Projekt muss kompilieren");
|
||||
project.procs.iter().map(|p| p.code.len()).sum()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
|
||||
@@ -9,11 +9,14 @@
|
||||
use std::fmt;
|
||||
use std::rc::Rc;
|
||||
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_ui::forms::PropertyValue;
|
||||
use tb_ui::frm::FormInitial;
|
||||
|
||||
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).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -75,8 +78,15 @@ impl<'a> Reader<'a> {
|
||||
pub fn new(buf: &'a [u8]) -> Self {
|
||||
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> {
|
||||
if self.pos + n > self.buf.len() {
|
||||
if n > self.buf.len().saturating_sub(self.pos) {
|
||||
return Err(LoadError::Corrupt("unerwartetes Dateiende"));
|
||||
}
|
||||
let s = &self.buf[self.pos..self.pos + n];
|
||||
@@ -138,7 +148,11 @@ impl Enc for bool {
|
||||
out.push(*self as u8);
|
||||
}
|
||||
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> {
|
||||
let tag = r.u8()?;
|
||||
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 {
|
||||
0 => TypeInit::Int,
|
||||
1 => TypeInit::Lng,
|
||||
@@ -224,6 +241,8 @@ instrs! {
|
||||
0x03 StopInstr;
|
||||
0x04 SystemInstr;
|
||||
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 PushInt(a: i16);
|
||||
@@ -234,6 +253,7 @@ instrs! {
|
||||
0x15 PushStr(a: u16);
|
||||
0x16 Dup;
|
||||
0x17 Pop;
|
||||
0x18 PushUdtId(a: u16); // TYPE-Index als LONG auf dem Stack (ISAM)
|
||||
|
||||
// 0x20 — Variablen und Referenzen
|
||||
0x20 LoadGlobal(a: u16);
|
||||
@@ -260,6 +280,8 @@ instrs! {
|
||||
0x39 ArrBound(a: bool); // true = LBOUND
|
||||
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 AddI2; 0x41 AddI4; 0x42 AddR4; 0x43 AddR8; 0x44 AddCy;
|
||||
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
|
||||
}
|
||||
|
||||
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 ------------------------------------------------------------
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProcCode {
|
||||
pub module: u16,
|
||||
pub kind: HProcKind,
|
||||
pub params: Vec<HParam>,
|
||||
pub ret_ty: Option<HTy>,
|
||||
pub name: String,
|
||||
pub n_params: u16,
|
||||
/// Initialisierung aller Frame-Slots (Parameter zuerst; deren Init
|
||||
@@ -378,6 +512,10 @@ pub struct DataItem {
|
||||
/// Übersetztes Modul — Inhalt des `.tbc`-Containers.
|
||||
#[derive(Debug)]
|
||||
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,
|
||||
/// `OPTION BASE` (Untergrenze impliziter Arrays).
|
||||
pub option_base: u8,
|
||||
@@ -402,9 +540,204 @@ fn w_string(out: &mut Vec<u8>, s: &str) {
|
||||
}
|
||||
|
||||
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(¶m.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
|
||||
/// (Kennung/Offset/Länge), Abschnitte MODN, CONS, TYPS, GLOB, PROC
|
||||
/// (mit eingebettetem Code und Zeileninfo), DATA, JMPT.
|
||||
/// (Kennung/Offset/Länge), Abschnitte MODN, SRCS, CONS, TYPS, GLOB, PROC
|
||||
/// (mit eingebettetem Code und Quellorten), DATA, JMPT, OBJS.
|
||||
pub fn to_tbc(&self) -> Vec<u8> {
|
||||
let mut sections: Vec<([u8; 4], Vec<u8>)> = Vec::new();
|
||||
|
||||
@@ -413,6 +746,19 @@ impl CompiledModule {
|
||||
modn.push(self.option_base);
|
||||
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();
|
||||
cons.extend_from_slice(&(self.strings.len() as u32).to_le_bytes());
|
||||
for s in &self.strings {
|
||||
@@ -443,6 +789,19 @@ impl CompiledModule {
|
||||
proc.extend_from_slice(&(self.procs.len() as u32).to_le_bytes());
|
||||
for p in &self.procs {
|
||||
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, ¶m.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.locals_init.len() as u32).to_le_bytes());
|
||||
for (init, name) in p.locals_init.iter().zip(&p.local_names) {
|
||||
@@ -484,6 +843,7 @@ impl CompiledModule {
|
||||
objs.push(o.class.id());
|
||||
w_string(&mut objs, o.parent_form.as_deref().unwrap_or(""));
|
||||
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());
|
||||
for e in &self.event_procs {
|
||||
@@ -491,6 +851,17 @@ impl CompiledModule {
|
||||
w_string(&mut objs, &e.event);
|
||||
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));
|
||||
|
||||
// Header + Abschnittstabelle
|
||||
@@ -527,8 +898,13 @@ impl CompiledModule {
|
||||
if version != TBC_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;
|
||||
if n_sections != 9 {
|
||||
return Err(LoadError::Corrupt("Abschnittsanzahl"));
|
||||
}
|
||||
let mut table = Vec::new();
|
||||
for _ in 0..n_sections {
|
||||
let id: [u8; 4] = r.take(4)?.try_into().unwrap();
|
||||
@@ -536,6 +912,27 @@ impl CompiledModule {
|
||||
let len = r.u32()? as usize;
|
||||
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> {
|
||||
for (sid, off, len) in &table {
|
||||
if sid == id {
|
||||
@@ -552,44 +949,86 @@ impl CompiledModule {
|
||||
let name = r.string()?;
|
||||
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 n = r.u32()? as usize;
|
||||
let mut strings = Vec::with_capacity(n);
|
||||
let mut strings = Vec::new();
|
||||
for _ in 0..n {
|
||||
strings.push(Rc::from(r.string()?.as_str()));
|
||||
}
|
||||
|
||||
r.finish()?;
|
||||
let mut r = section(b"TYPS")?;
|
||||
let n = r.u32()? as usize;
|
||||
let mut udts = Vec::with_capacity(n);
|
||||
let mut udts = Vec::new();
|
||||
for _ in 0..n {
|
||||
let name = r.string()?;
|
||||
let nf = r.u32()? as usize;
|
||||
let mut fields = Vec::with_capacity(nf);
|
||||
let mut fields = Vec::new();
|
||||
for _ in 0..nf {
|
||||
fields.push(TypeInit::dec(&mut r)?);
|
||||
}
|
||||
udts.push(UdtLayout { name, fields });
|
||||
}
|
||||
|
||||
r.finish()?;
|
||||
let mut r = section(b"GLOB")?;
|
||||
let n = r.u32()? as usize;
|
||||
let mut globals_init = Vec::with_capacity(n);
|
||||
let mut global_names = Vec::with_capacity(n);
|
||||
let mut globals_init = Vec::new();
|
||||
let mut global_names = Vec::new();
|
||||
for _ in 0..n {
|
||||
globals_init.push(TypeInit::dec(&mut r)?);
|
||||
global_names.push(r.string()?);
|
||||
}
|
||||
|
||||
r.finish()?;
|
||||
let mut r = section(b"PROC")?;
|
||||
let n = r.u32()? as usize;
|
||||
let mut procs = Vec::with_capacity(n);
|
||||
let mut procs = Vec::new();
|
||||
for _ in 0..n {
|
||||
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 nl = r.u32()? as usize;
|
||||
let mut locals_init = Vec::with_capacity(nl);
|
||||
let mut local_names = Vec::with_capacity(nl);
|
||||
let mut locals_init = Vec::new();
|
||||
let mut local_names = Vec::new();
|
||||
for _ in 0..nl {
|
||||
locals_init.push(TypeInit::dec(&mut r)?);
|
||||
local_names.push(r.string()?);
|
||||
@@ -598,11 +1037,16 @@ impl CompiledModule {
|
||||
let code_len = r.u32()? as usize;
|
||||
let code_bytes = r.take(code_len)?;
|
||||
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 {
|
||||
code.push(Instr::decode(&mut cr)?);
|
||||
}
|
||||
cr.finish()?;
|
||||
procs.push(ProcCode {
|
||||
module,
|
||||
kind,
|
||||
params,
|
||||
ret_ty,
|
||||
name,
|
||||
n_params,
|
||||
locals_init,
|
||||
@@ -611,44 +1055,49 @@ impl CompiledModule {
|
||||
});
|
||||
}
|
||||
|
||||
r.finish()?;
|
||||
let mut r = section(b"DATA")?;
|
||||
let n = r.u32()? as usize;
|
||||
let mut data = Vec::with_capacity(n);
|
||||
let mut data = Vec::new();
|
||||
for _ in 0..n {
|
||||
let text = r.string()?;
|
||||
let line = r.u32()?;
|
||||
data.push(DataItem { text, line });
|
||||
}
|
||||
|
||||
r.finish()?;
|
||||
let mut r = section(b"JMPT")?;
|
||||
let n = r.u32()? as usize;
|
||||
let mut jump_tables = Vec::with_capacity(n);
|
||||
let mut jump_tables = Vec::new();
|
||||
for _ in 0..n {
|
||||
let m = r.u32()? as usize;
|
||||
let mut t = Vec::with_capacity(m);
|
||||
let mut t = Vec::new();
|
||||
for _ in 0..m {
|
||||
t.push(r.u32()?);
|
||||
}
|
||||
jump_tables.push(t);
|
||||
}
|
||||
|
||||
r.finish()?;
|
||||
let mut r = section(b"OBJS")?;
|
||||
let n = r.u32()? as usize;
|
||||
let mut objects = Vec::with_capacity(n);
|
||||
let mut objects = Vec::new();
|
||||
for _ in 0..n {
|
||||
let name = r.string()?;
|
||||
let class = ObjectClass::from_id(r.u8()?).ok_or(LoadError::Corrupt("Objektklasse"))?;
|
||||
let parent = r.string()?;
|
||||
let array = r.u8()? != 0;
|
||||
let array = bool::dec(&mut r)?;
|
||||
let parent_id = r.u16()?;
|
||||
objects.push(FormObject {
|
||||
name,
|
||||
class,
|
||||
parent_form: (!parent.is_empty()).then_some(parent),
|
||||
parent: (parent_id != u16::MAX).then_some(parent_id),
|
||||
array,
|
||||
});
|
||||
}
|
||||
let n = r.u32()? as usize;
|
||||
let mut event_procs = Vec::with_capacity(n);
|
||||
let mut event_procs = Vec::new();
|
||||
for _ in 0..n {
|
||||
event_procs.push(HEventProc {
|
||||
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,
|
||||
option_base,
|
||||
strings,
|
||||
@@ -669,7 +1146,9 @@ impl CompiledModule {
|
||||
jump_tables,
|
||||
objects,
|
||||
event_procs,
|
||||
})
|
||||
};
|
||||
module.validate()?;
|
||||
Ok(module)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -712,6 +1191,13 @@ mod tests {
|
||||
#[test]
|
||||
fn tbc_roundtrip() {
|
||||
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(),
|
||||
option_base: 1,
|
||||
strings: vec![Rc::from("Hallo"), Rc::from("Welt")],
|
||||
@@ -722,6 +1208,10 @@ mod tests {
|
||||
fields: vec![TypeInit::FixedStr(30), TypeInit::Dbl],
|
||||
}],
|
||||
procs: vec![ProcCode {
|
||||
module: 0,
|
||||
kind: HProcKind::Main,
|
||||
params: vec![],
|
||||
ret_ty: None,
|
||||
name: "TEST".into(),
|
||||
n_params: 0,
|
||||
locals_init: vec![],
|
||||
@@ -751,6 +1241,13 @@ mod tests {
|
||||
#[test]
|
||||
fn unbekannte_version_wird_abgelehnt() {
|
||||
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(),
|
||||
option_base: 0,
|
||||
strings: vec![],
|
||||
|
||||
@@ -16,6 +16,12 @@ use tb_runtime::value::TypeInit;
|
||||
|
||||
pub fn compile(hir: &HirModule) -> CompiledModule {
|
||||
let mut cg = Codegen {
|
||||
common_arrays: hir
|
||||
.commons
|
||||
.iter()
|
||||
.filter(|c| c.dims.is_some())
|
||||
.map(|c| c.slot)
|
||||
.collect(),
|
||||
strings: Vec::new(),
|
||||
string_ids: HashMap::new(),
|
||||
jump_tables: Vec::new(),
|
||||
@@ -30,6 +36,13 @@ pub fn compile(hir: &HirModule) -> CompiledModule {
|
||||
procs.push(cg.compile_proc(proc));
|
||||
}
|
||||
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(),
|
||||
option_base: hir.option_base,
|
||||
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 {
|
||||
HTy::Num(NumTy::Int) => TypeInit::Int,
|
||||
HTy::Num(NumTy::Lng) => TypeInit::Lng,
|
||||
@@ -82,6 +95,7 @@ fn type_init(t: &HTy) -> TypeInit {
|
||||
}
|
||||
|
||||
struct Codegen {
|
||||
common_arrays: std::collections::HashSet<u16>,
|
||||
strings: Vec<Rc<str>>,
|
||||
string_ids: HashMap<String, u16>,
|
||||
jump_tables: Vec<Vec<u32>>,
|
||||
@@ -95,6 +109,7 @@ struct Codegen {
|
||||
}
|
||||
|
||||
struct ProcCtx {
|
||||
pos: tb_frontend::SourcePos,
|
||||
code: Vec<Instr>,
|
||||
/// LabelId → Instruktionsindex.
|
||||
label_pc: Vec<Option<u32>>,
|
||||
@@ -134,6 +149,10 @@ impl ProcCtx {
|
||||
self.label_pc[label as usize] = Some(pc);
|
||||
}
|
||||
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);
|
||||
}
|
||||
/// Sprunginstruktion mit noch unbekanntem Ziel emittieren.
|
||||
@@ -162,6 +181,7 @@ impl Codegen {
|
||||
|
||||
fn compile_proc(&mut self, proc: &hir::HProc) -> ProcCode {
|
||||
let mut ctx = ProcCtx {
|
||||
pos: tb_frontend::SourcePos::default(),
|
||||
code: Vec::new(),
|
||||
label_pc: vec![None; proc.label_count as usize],
|
||||
fixups: Vec::new(),
|
||||
@@ -238,6 +258,10 @@ impl Codegen {
|
||||
}
|
||||
|
||||
ProcCode {
|
||||
module: 0,
|
||||
kind: proc.kind,
|
||||
params: proc.params.clone(),
|
||||
ret_ty: proc.ret_ty.clone(),
|
||||
name: proc.name.clone(),
|
||||
n_params: proc.params.len() as u16,
|
||||
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) {
|
||||
ctx.pos = stmt.pos;
|
||||
match &stmt.kind {
|
||||
HStmtKind::Label(l) => {
|
||||
ctx.bind(*l);
|
||||
return;
|
||||
}
|
||||
_ if boundary => ctx.emit(Instr::Stmt(stmt.line)),
|
||||
_ => {}
|
||||
_ => ctx.emit(Instr::InitStmt(stmt.line)),
|
||||
}
|
||||
match &stmt.kind {
|
||||
HStmtKind::Label(_) => unreachable!(),
|
||||
@@ -478,6 +503,7 @@ impl Codegen {
|
||||
}
|
||||
}
|
||||
HStmtKind::Loop {
|
||||
end_pos,
|
||||
pre,
|
||||
post,
|
||||
body,
|
||||
@@ -496,6 +522,8 @@ impl Codegen {
|
||||
for s in body {
|
||||
self.stmt(ctx, proc, s);
|
||||
}
|
||||
ctx.pos = *end_pos;
|
||||
ctx.emit(Instr::Stmt(end_pos.line));
|
||||
match post {
|
||||
Some((is_until, cond)) => {
|
||||
self.expr(ctx, cond);
|
||||
@@ -511,6 +539,7 @@ impl Codegen {
|
||||
ctx.bind(*exit_label);
|
||||
}
|
||||
HStmtKind::For {
|
||||
end_pos,
|
||||
var,
|
||||
ty,
|
||||
from,
|
||||
@@ -533,7 +562,7 @@ impl Codegen {
|
||||
*step_slot,
|
||||
body,
|
||||
*exit_label,
|
||||
stmt.line,
|
||||
*end_pos,
|
||||
);
|
||||
}
|
||||
HStmtKind::Goto(l) => ctx.emit_jump(Instr::Jump(0), *l),
|
||||
@@ -663,6 +692,8 @@ impl Codegen {
|
||||
let init = type_init(elem);
|
||||
if *redim {
|
||||
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 {
|
||||
ctx.emit(Instr::DimArr(global, s, dims.len() as u8, init));
|
||||
}
|
||||
@@ -697,7 +728,7 @@ impl Codegen {
|
||||
step_slot: Option<VarSlot>,
|
||||
body: &[HStmt],
|
||||
exit_label: u16,
|
||||
line: u32,
|
||||
end_pos: tb_frontend::SourcePos,
|
||||
) {
|
||||
// Startwert, Grenze, ggf. Schritt einmal auswerten.
|
||||
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_body = ctx.new_label();
|
||||
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 {
|
||||
Some(s) => {
|
||||
// Vergleichsrichtung zur Compilezeit.
|
||||
@@ -757,7 +779,9 @@ impl Codegen {
|
||||
for s in body {
|
||||
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| {
|
||||
cg.load_place(ctx, var);
|
||||
match (step, step_slot) {
|
||||
@@ -777,6 +801,7 @@ impl Codegen {
|
||||
match e {
|
||||
HExpr::Int(v) => ctx.emit(Instr::PushInt(*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::Dbl(v) => ctx.emit(Instr::PushDbl(*v)),
|
||||
HExpr::Cur(v) => ctx.emit(Instr::PushCur(*v)),
|
||||
|
||||
@@ -70,6 +70,8 @@ struct Frame {
|
||||
/// Instruktionsindex der zuletzt begonnenen Anweisung (`Stmt`).
|
||||
last_stmt_pc: usize,
|
||||
line: u32,
|
||||
source: u32,
|
||||
column: u32,
|
||||
/// Gesetzt, wenn dieser Frame der Handler eines Ereignis-Traps ist.
|
||||
/// Er läuft im Modulrumpf und **teilt dessen Locals** — ein eigener
|
||||
/// Satz würde dem Handler leere Modulvariablen zeigen. Sein `RETURN`
|
||||
@@ -117,14 +119,14 @@ pub struct Vm {
|
||||
erl: u32,
|
||||
/// Zuletzt durchlaufene numerische Zeilennummer (0 = keine).
|
||||
zeile_nr: u32,
|
||||
module_handler: Handler,
|
||||
module_handlers: Vec<Handler>,
|
||||
in_handler: bool,
|
||||
resume_pc: usize,
|
||||
// Steuerung
|
||||
flags: u32,
|
||||
/// Zählt Anweisungsgrenzen für die regelmäßige Ereignisabholung.
|
||||
tick_zaehler: u32,
|
||||
breakpoints: HashSet<u32>,
|
||||
breakpoints: HashSet<(u16, u32)>,
|
||||
data_ptr: usize,
|
||||
start_pc: Option<usize>,
|
||||
pub forms: FormsModel,
|
||||
@@ -170,7 +172,15 @@ impl Vm {
|
||||
.iter()
|
||||
.map(|t| default_value(t, &module.udts))
|
||||
.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 {
|
||||
globals,
|
||||
locals: Vec::new(),
|
||||
@@ -180,7 +190,7 @@ impl Vm {
|
||||
err: 0,
|
||||
erl: 0,
|
||||
zeile_nr: 0,
|
||||
module_handler: Handler::None,
|
||||
module_handlers: vec![Handler::None; module.modules.len()],
|
||||
in_handler: false,
|
||||
resume_pc: 0,
|
||||
flags: 0,
|
||||
@@ -202,7 +212,12 @@ impl Vm {
|
||||
let target = self.module.procs[0]
|
||||
.code
|
||||
.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))?;
|
||||
if self.module.procs[0]
|
||||
.code
|
||||
@@ -229,6 +244,8 @@ impl Vm {
|
||||
}
|
||||
let stack_base = self.stack.len();
|
||||
self.frames.push(Frame {
|
||||
source: 0,
|
||||
column: 0,
|
||||
proc,
|
||||
pc: 0,
|
||||
locals_base,
|
||||
@@ -361,7 +378,12 @@ impl Vm {
|
||||
if !matches!(self.module.procs[proc].code.as_slice(), [Instr::RetProc]) {
|
||||
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!(
|
||||
name.as_str(),
|
||||
"CMNDLGREGISTER"
|
||||
@@ -767,6 +789,8 @@ impl Vm {
|
||||
let locals_base = self.frames[0].locals_base;
|
||||
let stack_base = self.stack.len();
|
||||
self.frames.push(Frame {
|
||||
source: 0,
|
||||
column: 0,
|
||||
proc: 0,
|
||||
pc: ziel as usize,
|
||||
locals_base,
|
||||
@@ -821,17 +845,54 @@ impl Vm {
|
||||
}
|
||||
|
||||
pub fn add_breakpoint(&mut self, line: u32) {
|
||||
self.breakpoints.insert(line);
|
||||
self.breakpoints.insert((0, line));
|
||||
self.flags |= F_BREAK;
|
||||
}
|
||||
|
||||
pub fn remove_breakpoint(&mut self, line: u32) {
|
||||
self.breakpoints.remove(&line);
|
||||
self.breakpoints.remove(&(0, line));
|
||||
if self.breakpoints.is_empty() {
|
||||
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 {
|
||||
self.frames.last().map(|f| f.line).unwrap_or(0)
|
||||
}
|
||||
@@ -843,26 +904,58 @@ impl Vm {
|
||||
.unwrap_or("")
|
||||
}
|
||||
|
||||
/// Variableninspektion (Debugger): erst Locals des obersten Frames
|
||||
/// (Referenzen werden aufgelöst), dann Modulvariablen. Ein
|
||||
/// Typ-Suffix (`n%`, `s$`) wird toleriert — Slots tragen Basisnamen.
|
||||
/// Variableninspektion: lokale Namen vor Modulvariablen, explizite
|
||||
/// Typ-Suffixe vor einer eindeutigen Suche nach dem Basisnamen.
|
||||
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() {
|
||||
let p = &self.module.procs[f.proc];
|
||||
for (i, n) in p.local_names.iter().enumerate() {
|
||||
if n.eq_ignore_ascii_case(name) {
|
||||
let v = self.locals[f.locals_base + i].clone();
|
||||
return Some(self.deref_for_inspect(v));
|
||||
}
|
||||
if let Some(i) = unique(
|
||||
p.local_names
|
||||
.iter()
|
||||
.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() {
|
||||
if n.eq_ignore_ascii_case(name) {
|
||||
return Some(self.deref_for_inspect(self.globals[i].clone()));
|
||||
}
|
||||
let qualified = format!(
|
||||
"{}!{name}",
|
||||
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.
|
||||
@@ -993,8 +1086,15 @@ impl Vm {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if target.is_none() && self.module_handler != Handler::None {
|
||||
target = Some((0, self.module_handler));
|
||||
if target.is_none() {
|
||||
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 {
|
||||
return Some(self.error_event(code, line));
|
||||
@@ -1039,7 +1139,7 @@ impl Vm {
|
||||
let code = &self.module.procs[proc].code;
|
||||
let mut i = from + 1;
|
||||
while i < code.len() {
|
||||
if matches!(code[i], Instr::Stmt(_)) {
|
||||
if matches!(code[i], Instr::Stmt(_) | Instr::InitStmt(_)) {
|
||||
return i;
|
||||
}
|
||||
i += 1;
|
||||
@@ -1189,7 +1289,7 @@ impl Vm {
|
||||
match cell {
|
||||
Value::Arr(a) => Ok(a.clone()),
|
||||
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 arr = ArrayObj::new(elem.clone(), bounds, &self.module.udts)?;
|
||||
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> {
|
||||
use Instr as I;
|
||||
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 let Some(target) = self.start_pc.take() {
|
||||
self.frames[0].pc = target;
|
||||
@@ -1231,6 +1333,13 @@ impl Vm {
|
||||
}
|
||||
let f = self.frames.last_mut().unwrap();
|
||||
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;
|
||||
// Zustellpunkt: anzeigen, wenn sich der Bildschirm geändert
|
||||
// hat, und regelmäßig Ereignisse abholen. Das ist keine
|
||||
@@ -1238,20 +1347,27 @@ impl Vm {
|
||||
// Größenänderungen kämen nie an.
|
||||
self.tick_zaehler = self.tick_zaehler.wrapping_add(1);
|
||||
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.rt.screen.veraenderung_quittieren();
|
||||
}
|
||||
let erster_eintritt =
|
||||
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);
|
||||
}
|
||||
if self.flags != 0 {
|
||||
if self.flags & F_STEP != 0 {
|
||||
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 }));
|
||||
}
|
||||
if self.flags & F_POLL != 0 && self.rt.abbruch {
|
||||
@@ -1328,6 +1444,10 @@ impl Vm {
|
||||
self.push(Value::Lng(v));
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
I::PushUdtId(id) => {
|
||||
self.push(Value::Lng(id as i32));
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
I::PushSng(v) => {
|
||||
self.push(Value::Sng(v));
|
||||
Ok(Flow::Normal)
|
||||
@@ -1716,13 +1836,25 @@ impl Vm {
|
||||
a.data[flat as usize] = v;
|
||||
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 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) {
|
||||
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)));
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
@@ -2293,7 +2425,8 @@ impl Vm {
|
||||
|
||||
// ---- Fehlerbehandlung ----
|
||||
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)
|
||||
}
|
||||
I::OnErrorLocal(t) => {
|
||||
@@ -2301,7 +2434,8 @@ impl Vm {
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
I::OnErrorDisable => {
|
||||
self.module_handler = Handler::None;
|
||||
let module = self.current_module() as usize;
|
||||
self.module_handlers[module] = Handler::None;
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
I::OnErrorLocalDisable => {
|
||||
@@ -2312,7 +2446,8 @@ impl Vm {
|
||||
if local {
|
||||
self.frames.last_mut().unwrap().local_handler = Handler::ResumeNext;
|
||||
} else {
|
||||
self.module_handler = Handler::ResumeNext;
|
||||
let module = self.current_module() as usize;
|
||||
self.module_handlers[module] = Handler::ResumeNext;
|
||||
}
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
|
||||
@@ -12,20 +12,20 @@ pub mod bytecode;
|
||||
pub mod codegen;
|
||||
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.
|
||||
/// Bei Diagnosen (Compile-Fehlern) wird kein Kompilat erzeugt.
|
||||
/// Einmodul-API auf derselben Pipeline wie vollständige Projekte.
|
||||
pub fn compile_source(
|
||||
module_name: &str,
|
||||
source: &str,
|
||||
) -> Result<bytecode::CompiledModule, Vec<Diagnostic>> {
|
||||
let analysis = tb_frontend::analyze_source(module_name, source);
|
||||
if !analysis.diagnostics.is_empty() {
|
||||
return Err(analysis.diagnostics);
|
||||
}
|
||||
let hir = analysis.hir.expect("diagnose-frei, aber kein HIR");
|
||||
Ok(codegen::compile(&hir))
|
||||
compile_source_with_forms(
|
||||
module_name,
|
||||
source,
|
||||
&tb_frontend::forms::FormCatalog::default(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn compile_source_with_forms(
|
||||
@@ -33,11 +33,10 @@ pub fn compile_source_with_forms(
|
||||
source: &str,
|
||||
forms: &tb_frontend::forms::FormCatalog,
|
||||
) -> Result<bytecode::CompiledModule, Vec<Diagnostic>> {
|
||||
let analysis = tb_frontend::analyze_source_with_forms(module_name, source, forms);
|
||||
if !analysis.diagnostics.is_empty() {
|
||||
return Err(analysis.diagnostics);
|
||||
}
|
||||
Ok(codegen::compile(
|
||||
&analysis.hir.expect("diagnose-frei, aber kein HIR"),
|
||||
))
|
||||
compile_project(
|
||||
module_name,
|
||||
&[SourceUnit::new(module_name, module_name, source)],
|
||||
forms,
|
||||
&[],
|
||||
)
|
||||
}
|
||||
|
||||
682
crates/tb-vm/src/project.rs
Normal file
682
crates/tb-vm/src/project.rs
Normal 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)) = ¶m.as_type {
|
||||
param.as_type = Some(TypeName::Udt(import_type(
|
||||
name,
|
||||
origin,
|
||||
target,
|
||||
all,
|
||||
&mut imported,
|
||||
&mut seen_types,
|
||||
)));
|
||||
}
|
||||
}
|
||||
imported.push(Stmt::Declare { sig, pos: *pos });
|
||||
}
|
||||
Stmt::ConstDecl { items, .. } if !local_constants.contains(&items[0].0) => {
|
||||
imported.push(stmt.clone())
|
||||
}
|
||||
Stmt::TypeDecl { name, .. } if !local_types.contains(name) => {
|
||||
import_type(name, origin, target, all, &mut imported, &mut seen_types);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
299
crates/tb-vm/tests/project.rs
Normal file
299
crates/tb-vm/tests/project.rs
Normal 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");
|
||||
}
|
||||
548
crates/tb-vm/tests/project_regressions.rs
Normal file
548
crates/tb-vm/tests/project_regressions.rs
Normal 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:?}"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user