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.
|
//! `tbc` — Standalone-Compiler von Terminal Basic.
|
||||||
//!
|
//!
|
||||||
//! Unterbefehle (Phase 2):
|
//! Unterbefehle (Phase 2):
|
||||||
//! - `tbc run <datei.bas|datei.frm|projekt.mak>` Kompilieren und ausführen
|
//! - `tbc run <datei.bas|datei.frm|projekt.mak|datei.tbc>` Kompilieren und ausführen
|
||||||
//! - `tbc build <datei.bas|datei.frm|projekt.mak>` Zu `.tbc` kompilieren
|
//! - `tbc build <datei.bas|datei.frm|projekt.mak>` Zu `.tbc` kompilieren
|
||||||
//! - `tbc check <datei.bas|datei.frm|projekt.mak>` Syntax/Semantik prüfen
|
//! - `tbc check <datei.bas|datei.frm|projekt.mak>` Syntax/Semantik prüfen
|
||||||
//! - `tbc convert-frm <quelle.frm> <ziel.frm>` Binärformular in Text wandeln
|
//! - `tbc convert-frm <quelle.frm> <ziel.frm>` Binärformular in Text wandeln
|
||||||
@@ -10,7 +10,6 @@
|
|||||||
//! 0 = END/SYSTEM/Programmende · 3 = STOP · 2 = Laufzeitfehler ·
|
//! 0 = END/SYSTEM/Programmende · 3 = STOP · 2 = Laufzeitfehler ·
|
||||||
//! 1 = Compile-Fehler/Bedienfehler.
|
//! 1 = Compile-Fehler/Bedienfehler.
|
||||||
|
|
||||||
use std::collections::HashSet;
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::ExitCode;
|
use std::process::ExitCode;
|
||||||
use tb_runtime::host::{Ereignis, Host};
|
use tb_runtime::host::{Ereignis, Host};
|
||||||
@@ -26,7 +25,7 @@ fn main() -> ExitCode {
|
|||||||
Some("convert-frm") => cmd_convert_frm(&args[1..]),
|
Some("convert-frm") => cmd_convert_frm(&args[1..]),
|
||||||
_ => {
|
_ => {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"Aufruf: tbc run|build|check <datei.bas|datei.frm|projekt.mak> | tbc convert-frm <quelle.frm> <ziel.frm>"
|
"Aufruf: tbc run|build|check <datei.bas|datei.frm|projekt.mak|datei.tbc> | tbc convert-frm <quelle.frm> <ziel.frm>"
|
||||||
);
|
);
|
||||||
ExitCode::from(1)
|
ExitCode::from(1)
|
||||||
}
|
}
|
||||||
@@ -128,33 +127,47 @@ fn include_name(line: &str) -> Option<String> {
|
|||||||
Some(rest.split('\'').next().unwrap_or_default().to_string())
|
Some(rest.split('\'').next().unwrap_or_default().to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn expand_includes(path: &Path, source: &str, stack: &mut Vec<PathBuf>) -> Result<String, String> {
|
fn expand_includes(
|
||||||
|
path: &Path,
|
||||||
|
source: &str,
|
||||||
|
first_line: u32,
|
||||||
|
stack: &mut Vec<PathBuf>,
|
||||||
|
) -> Result<Vec<tb_frontend::source::SourceSegment>, String> {
|
||||||
let canonical = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
|
let canonical = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
|
||||||
if stack.contains(&canonical) {
|
if stack.contains(&canonical) {
|
||||||
return Err(format!("{}: zyklisches $INCLUDE", path.display()));
|
return Err(format!("{}: zyklisches $INCLUDE", path.display()));
|
||||||
}
|
}
|
||||||
stack.push(canonical);
|
stack.push(canonical);
|
||||||
let mut out = String::new();
|
let mut segments = vec![tb_frontend::source::SourceSegment {
|
||||||
for line in source.split_inclusive('\n') {
|
file: path.display().to_string(),
|
||||||
|
first_line,
|
||||||
|
text: String::new(),
|
||||||
|
}];
|
||||||
|
for (line_no, line) in source.split_inclusive('\n').enumerate() {
|
||||||
if let Some(name) = include_name(line) {
|
if let Some(name) = include_name(line) {
|
||||||
if name.is_empty() {
|
if name.is_empty() {
|
||||||
return Err(format!("{}: leeres $INCLUDE", path.display()));
|
return Err(format!(
|
||||||
|
"{}:{}: leeres $INCLUDE",
|
||||||
|
path.display(),
|
||||||
|
first_line + line_no as u32
|
||||||
|
));
|
||||||
}
|
}
|
||||||
let included =
|
let included =
|
||||||
relative_case_insensitive(path.parent().unwrap_or(Path::new(".")), &name)
|
relative_case_insensitive(path.parent().unwrap_or(Path::new(".")), &name)
|
||||||
.map_err(|error| format!("{}: {error}", path.display()))?;
|
.map_err(|e| format!("{}: {e}", path.display()))?;
|
||||||
let text = std::fs::read_to_string(&included)
|
let text = std::fs::read_to_string(&included)
|
||||||
.map_err(|error| format!("{}: {error}", included.display()))?;
|
.map_err(|e| format!("{}: {e}", included.display()))?;
|
||||||
out.push_str(&expand_includes(&included, &text, stack)?);
|
segments.extend(expand_includes(&included, &text, 1, stack)?);
|
||||||
if !out.ends_with('\n') {
|
|
||||||
out.push('\n');
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
out.push_str(line);
|
segments.push(tb_frontend::source::SourceSegment {
|
||||||
|
file: path.display().to_string(),
|
||||||
|
first_line: first_line + line_no as u32,
|
||||||
|
text: line.into(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
stack.pop();
|
stack.pop();
|
||||||
Ok(out)
|
Ok(segments)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_form(path: &Path) -> Result<tb_ui::frm::FormFile, String> {
|
fn read_form(path: &Path) -> Result<tb_ui::frm::FormFile, String> {
|
||||||
@@ -162,9 +175,8 @@ fn read_form(path: &Path) -> Result<tb_ui::frm::FormFile, String> {
|
|||||||
if bytes.starts_with(b"VERSION ") || bytes.starts_with(b"Version ") {
|
if bytes.starts_with(b"VERSION ") || bytes.starts_with(b"Version ") {
|
||||||
let source = std::str::from_utf8(&bytes)
|
let source = std::str::from_utf8(&bytes)
|
||||||
.map_err(|_| format!("{}: ungültige Textkodierung", path.display()))?;
|
.map_err(|_| format!("{}: ungültige Textkodierung", path.display()))?;
|
||||||
let mut form = tb_ui::frm::read_text(&path.display().to_string(), source)
|
let form = tb_ui::frm::read_text(&path.display().to_string(), source)
|
||||||
.map_err(|error| error.to_string())?;
|
.map_err(|error| error.to_string())?;
|
||||||
form.code = expand_includes(path, &form.code, &mut Vec::new())?;
|
|
||||||
Ok(form)
|
Ok(form)
|
||||||
} else {
|
} else {
|
||||||
tb_ui::frm::read_binary(&path.display().to_string(), &bytes)
|
tb_ui::frm::read_binary(&path.display().to_string(), &bytes)
|
||||||
@@ -173,65 +185,15 @@ fn read_form(path: &Path) -> Result<tb_ui::frm::FormFile, String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
fn input_sources(
|
||||||
struct ProjectSymbols {
|
path: &Path,
|
||||||
constants: HashSet<String>,
|
) -> Result<
|
||||||
types: HashSet<String>,
|
(
|
||||||
}
|
Vec<tb_frontend::source::SourceUnit>,
|
||||||
|
Vec<tb_ui::frm::FormFile>,
|
||||||
fn append_project_source(target: &mut String, source: &str, symbols: &mut ProjectSymbols) {
|
),
|
||||||
let mut in_proc = false;
|
String,
|
||||||
let mut skip_type = false;
|
> {
|
||||||
for line in source.split_inclusive('\n') {
|
|
||||||
let upper = line.trim().to_ascii_uppercase();
|
|
||||||
if skip_type {
|
|
||||||
if upper == "END TYPE" {
|
|
||||||
skip_type = false;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if !in_proc {
|
|
||||||
if let Some(name) = upper
|
|
||||||
.strip_prefix("TYPE ")
|
|
||||||
.and_then(|rest| rest.split_whitespace().next())
|
|
||||||
{
|
|
||||||
if !symbols.types.insert(name.to_string()) {
|
|
||||||
skip_type = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(rest) = upper.strip_prefix("CONST ") {
|
|
||||||
let names = rest
|
|
||||||
.split(',')
|
|
||||||
.filter_map(|part| part.split('=').next())
|
|
||||||
.map(str::trim)
|
|
||||||
.filter(|name| !name.is_empty())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
if !names.is_empty() && names.iter().all(|name| symbols.constants.contains(*name)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
symbols
|
|
||||||
.constants
|
|
||||||
.extend(names.into_iter().map(str::to_string));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
target.push_str(line);
|
|
||||||
if upper.starts_with("SUB ")
|
|
||||||
|| upper.starts_with("FUNCTION ")
|
|
||||||
|| upper.starts_with("STATIC SUB ")
|
|
||||||
|| upper.starts_with("STATIC FUNCTION ")
|
|
||||||
{
|
|
||||||
in_proc = true;
|
|
||||||
} else if matches!(upper.as_str(), "END SUB" | "END FUNCTION") {
|
|
||||||
in_proc = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !target.ends_with('\n') {
|
|
||||||
target.push('\n');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn input_sources(path: &Path) -> Result<(String, Vec<tb_ui::frm::FormFile>), String> {
|
|
||||||
let extension = path.extension().and_then(|ext| ext.to_str()).unwrap_or("");
|
let extension = path.extension().and_then(|ext| ext.to_str()).unwrap_or("");
|
||||||
let paths = if extension.eq_ignore_ascii_case("mak") {
|
let paths = if extension.eq_ignore_ascii_case("mak") {
|
||||||
let project = std::fs::read_to_string(path)
|
let project = std::fs::read_to_string(path)
|
||||||
@@ -240,7 +202,7 @@ fn input_sources(path: &Path) -> Result<(String, Vec<tb_ui::frm::FormFile>), Str
|
|||||||
project
|
project
|
||||||
.lines()
|
.lines()
|
||||||
.map(str::trim)
|
.map(str::trim)
|
||||||
.filter(|line| !line.is_empty())
|
.filter(|line| !line.is_empty() && !line.starts_with('\''))
|
||||||
.map(|line| {
|
.map(|line| {
|
||||||
relative_case_insensitive(base, line)
|
relative_case_insensitive(base, line)
|
||||||
.map_err(|error| format!("{}: {error}", path.display()))
|
.map_err(|error| format!("{}: {error}", path.display()))
|
||||||
@@ -249,9 +211,8 @@ fn input_sources(path: &Path) -> Result<(String, Vec<tb_ui::frm::FormFile>), Str
|
|||||||
} else {
|
} else {
|
||||||
vec![path.to_path_buf()]
|
vec![path.to_path_buf()]
|
||||||
};
|
};
|
||||||
let mut source = String::new();
|
let mut source = Vec::new();
|
||||||
let mut forms = Vec::new();
|
let mut forms = Vec::new();
|
||||||
let mut symbols = ProjectSymbols::default();
|
|
||||||
for member in paths {
|
for member in paths {
|
||||||
if member
|
if member
|
||||||
.extension()
|
.extension()
|
||||||
@@ -259,13 +220,18 @@ fn input_sources(path: &Path) -> Result<(String, Vec<tb_ui::frm::FormFile>), Str
|
|||||||
.is_some_and(|ext| ext.eq_ignore_ascii_case("frm"))
|
.is_some_and(|ext| ext.eq_ignore_ascii_case("frm"))
|
||||||
{
|
{
|
||||||
let form = read_form(&member)?;
|
let form = read_form(&member)?;
|
||||||
append_project_source(&mut source, &form.code, &mut symbols);
|
source.push(tb_frontend::source::SourceUnit {
|
||||||
|
name: form.root.name.clone(),
|
||||||
|
segments: expand_includes(&member, &form.code, form.code_line(), &mut Vec::new())?,
|
||||||
|
});
|
||||||
forms.push(form);
|
forms.push(form);
|
||||||
} else {
|
} else {
|
||||||
let text = std::fs::read_to_string(&member)
|
let text = std::fs::read_to_string(&member)
|
||||||
.map_err(|error| format!("{}: {error}", member.display()))?;
|
.map_err(|error| format!("{}: {error}", member.display()))?;
|
||||||
let expanded = expand_includes(&member, &text, &mut Vec::new())?;
|
source.push(tb_frontend::source::SourceUnit {
|
||||||
append_project_source(&mut source, &expanded, &mut symbols);
|
name: module_name(&member),
|
||||||
|
segments: expand_includes(&member, &text, 1, &mut Vec::new())?,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok((source, forms))
|
Ok((source, forms))
|
||||||
@@ -273,19 +239,26 @@ fn input_sources(path: &Path) -> Result<(String, Vec<tb_ui::frm::FormFile>), Str
|
|||||||
|
|
||||||
fn compile(
|
fn compile(
|
||||||
path_arg: Option<&String>,
|
path_arg: Option<&String>,
|
||||||
) -> Result<
|
) -> Result<(PathBuf, tb_vm::bytecode::CompiledModule), ExitCode> {
|
||||||
(
|
|
||||||
PathBuf,
|
|
||||||
tb_vm::bytecode::CompiledModule,
|
|
||||||
Vec<tb_ui::frm::FormFile>,
|
|
||||||
),
|
|
||||||
ExitCode,
|
|
||||||
> {
|
|
||||||
let Some(path) = path_arg else {
|
let Some(path) = path_arg else {
|
||||||
eprintln!("Aufruf: tbc run|build|check <datei.bas|datei.frm|projekt.mak>");
|
eprintln!("Aufruf: tbc run|build|check <datei.bas|datei.frm|projekt.mak|datei.tbc>");
|
||||||
return Err(ExitCode::from(1));
|
return Err(ExitCode::from(1));
|
||||||
};
|
};
|
||||||
let path = PathBuf::from(path);
|
let path = PathBuf::from(path);
|
||||||
|
if path
|
||||||
|
.extension()
|
||||||
|
.is_some_and(|ext| ext.eq_ignore_ascii_case("tbc"))
|
||||||
|
{
|
||||||
|
let bytes = std::fs::read(&path).map_err(|e| {
|
||||||
|
eprintln!("{}: {e}", path.display());
|
||||||
|
ExitCode::from(1)
|
||||||
|
})?;
|
||||||
|
let module = tb_vm::bytecode::CompiledModule::from_tbc(&bytes).map_err(|e| {
|
||||||
|
eprintln!("{}: {e}", path.display());
|
||||||
|
ExitCode::from(1)
|
||||||
|
})?;
|
||||||
|
return Ok((path, module));
|
||||||
|
}
|
||||||
let (source, forms) = match input_sources(&path) {
|
let (source, forms) = match input_sources(&path) {
|
||||||
Ok(input) => input,
|
Ok(input) => input,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
@@ -293,22 +266,16 @@ fn compile(
|
|||||||
return Err(ExitCode::from(1));
|
return Err(ExitCode::from(1));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let compiled = if forms.is_empty() {
|
let mut catalog = tb_frontend::forms::FormCatalog::default();
|
||||||
tb_vm::compile_source(&module_name(&path), &source)
|
for form in &forms {
|
||||||
} else {
|
catalog.append(&form.catalog());
|
||||||
let mut catalog = tb_frontend::forms::FormCatalog::default();
|
}
|
||||||
for form in &forms {
|
let compiled = tb_vm::compile_project(&module_name(&path), &source, &catalog, &forms);
|
||||||
for object in form.catalog().objects {
|
|
||||||
catalog.objects.push(object);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tb_vm::compile_source_with_forms(&forms[0].root.name, &source, &catalog)
|
|
||||||
};
|
|
||||||
match compiled {
|
match compiled {
|
||||||
Ok(m) => Ok((path, m, forms)),
|
Ok(m) => Ok((path, m)),
|
||||||
Err(diags) => {
|
Err(diags) => {
|
||||||
for d in &diags {
|
for d in &diags {
|
||||||
eprintln!("{}:{d}", path.display());
|
eprintln!("{d}");
|
||||||
}
|
}
|
||||||
eprintln!("{} Fehler.", diags.len());
|
eprintln!("{} Fehler.", diags.len());
|
||||||
Err(ExitCode::from(1))
|
Err(ExitCode::from(1))
|
||||||
@@ -324,7 +291,7 @@ fn cmd_check(args: &[String]) -> ExitCode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn cmd_build(args: &[String]) -> ExitCode {
|
fn cmd_build(args: &[String]) -> ExitCode {
|
||||||
let (path, module, _) = match compile(args.first()) {
|
let (path, module) = match compile(args.first()) {
|
||||||
Ok(x) => x,
|
Ok(x) => x,
|
||||||
Err(code) => return code,
|
Err(code) => return code,
|
||||||
};
|
};
|
||||||
@@ -371,7 +338,7 @@ fn cmd_run(args: &[String]) -> ExitCode {
|
|||||||
RunEvent::Ended => ExitCode::SUCCESS,
|
RunEvent::Ended => ExitCode::SUCCESS,
|
||||||
RunEvent::Stopped { line } => {
|
RunEvent::Stopped { line } => {
|
||||||
// STOP außerhalb der IDE: Meldung + Exit-Code ≠ 0 (D6).
|
// STOP außerhalb der IDE: Meldung + Exit-Code ≠ 0 (D6).
|
||||||
eprintln!("STOP in line {line}");
|
eprintln!("{}:{line}: STOP in line {line}", vm.current_file());
|
||||||
ExitCode::from(3)
|
ExitCode::from(3)
|
||||||
}
|
}
|
||||||
RunEvent::Error {
|
RunEvent::Error {
|
||||||
@@ -379,7 +346,11 @@ fn cmd_run(args: &[String]) -> ExitCode {
|
|||||||
line,
|
line,
|
||||||
message,
|
message,
|
||||||
} => {
|
} => {
|
||||||
eprintln!("Runtime error {code}: {message} in line {line}");
|
eprintln!(
|
||||||
|
"{}:{line}:{}: Runtime error {code}: {message} in line {line}",
|
||||||
|
vm.current_file(),
|
||||||
|
vm.current_source_pos().column
|
||||||
|
);
|
||||||
ExitCode::from(2)
|
ExitCode::from(2)
|
||||||
}
|
}
|
||||||
// Ohne Debugger-Flags treten diese Ereignisse nicht auf.
|
// Ohne Debugger-Flags treten diese Ereignisse nicht auf.
|
||||||
@@ -398,7 +369,7 @@ fn run_target(current: &Path, program: &str) -> Result<PathBuf, String> {
|
|||||||
if Path::new(program).extension().is_some() {
|
if Path::new(program).extension().is_some() {
|
||||||
return relative_case_insensitive(base, program).map_err(|error| error.to_string());
|
return relative_case_insensitive(base, program).map_err(|error| error.to_string());
|
||||||
}
|
}
|
||||||
for extension in ["bas", "frm", "mak"] {
|
for extension in ["bas", "frm", "mak", "tbc"] {
|
||||||
let candidate = format!("{program}.{extension}");
|
let candidate = format!("{program}.{extension}");
|
||||||
if let Ok(path) = relative_case_insensitive(base, &candidate) {
|
if let Ok(path) = relative_case_insensitive(base, &candidate) {
|
||||||
return Ok(path);
|
return Ok(path);
|
||||||
@@ -413,7 +384,7 @@ fn run_chain(
|
|||||||
size: Option<(usize, usize)>,
|
size: Option<(usize, usize)>,
|
||||||
) -> Result<(RunEvent, Vm), ExitCode> {
|
) -> Result<(RunEvent, Vm), ExitCode> {
|
||||||
let Some(first) = args.first() else {
|
let Some(first) = args.first() else {
|
||||||
eprintln!("Aufruf: tbc run <datei.bas|datei.frm|projekt.mak>");
|
eprintln!("Aufruf: tbc run <datei.bas|datei.frm|projekt.mak|datei.tbc>");
|
||||||
return Err(ExitCode::from(1));
|
return Err(ExitCode::from(1));
|
||||||
};
|
};
|
||||||
let mut current = PathBuf::from(first);
|
let mut current = PathBuf::from(first);
|
||||||
@@ -421,26 +392,8 @@ fn run_chain(
|
|||||||
let command = args[1..].join(" ");
|
let command = args[1..].join(" ");
|
||||||
loop {
|
loop {
|
||||||
let current_arg = current.display().to_string();
|
let current_arg = current.display().to_string();
|
||||||
let (path, module, forms) = compile(Some(¤t_arg))?;
|
let (path, module) = compile(Some(¤t_arg))?;
|
||||||
let mut vm = Vm::new(module);
|
let mut vm = Vm::new(module);
|
||||||
if !forms.is_empty() {
|
|
||||||
let applied = forms.iter().try_for_each(|form| form.apply(&mut vm.forms));
|
|
||||||
let first = vm
|
|
||||||
.forms
|
|
||||||
.objects
|
|
||||||
.iter()
|
|
||||||
.position(|object| {
|
|
||||||
object
|
|
||||||
.description
|
|
||||||
.name
|
|
||||||
.eq_ignore_ascii_case(&forms[0].root.name)
|
|
||||||
})
|
|
||||||
.unwrap() as u16;
|
|
||||||
if let Err(error) = applied.and_then(|_| vm.forms.show(first, false).map(|_| ())) {
|
|
||||||
eprintln!("Runtime error {}: {}", error.0, error);
|
|
||||||
return Err(ExitCode::from(2));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(line) = start_line.take() {
|
if let Some(line) = start_line.take() {
|
||||||
if let Err(error) = vm.start_at_line(line) {
|
if let Err(error) = vm.start_at_line(line) {
|
||||||
eprintln!("Runtime error {}: {}", error.0, error);
|
eprintln!("Runtime error {}: {}", error.0, error);
|
||||||
|
|||||||
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)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum Expr {
|
pub enum Expr {
|
||||||
IntLit(i16),
|
IntLit(i16, SourcePos),
|
||||||
LongLit(i32),
|
LongLit(i32, SourcePos),
|
||||||
SingleLit(f32),
|
SingleLit(f32, SourcePos),
|
||||||
DoubleLit(f64),
|
DoubleLit(f64, SourcePos),
|
||||||
CurrencyLit(i64),
|
CurrencyLit(i64, SourcePos),
|
||||||
StrLit(String),
|
StrLit(String, SourcePos),
|
||||||
/// Benannter Zugriff: Variable, Arrayelement, Funktionsaufruf oder
|
/// Benannter Zugriff: Variable, Arrayelement, Funktionsaufruf oder
|
||||||
/// Konstante — Auflösung erfolgt in der Semantik.
|
/// Konstante — Auflösung erfolgt in der Semantik.
|
||||||
Name {
|
Name {
|
||||||
@@ -90,7 +90,13 @@ pub enum Expr {
|
|||||||
impl Expr {
|
impl Expr {
|
||||||
pub fn pos(&self) -> SourcePos {
|
pub fn pos(&self) -> SourcePos {
|
||||||
match self {
|
match self {
|
||||||
Expr::Name { pos, .. }
|
Expr::IntLit(_, pos)
|
||||||
|
| Expr::LongLit(_, pos)
|
||||||
|
| Expr::SingleLit(_, pos)
|
||||||
|
| Expr::DoubleLit(_, pos)
|
||||||
|
| Expr::CurrencyLit(_, pos)
|
||||||
|
| Expr::StrLit(_, pos)
|
||||||
|
| Expr::Name { pos, .. }
|
||||||
| Expr::Unary { pos, .. }
|
| Expr::Unary { pos, .. }
|
||||||
| Expr::Binary { pos, .. }
|
| Expr::Binary { pos, .. }
|
||||||
| Expr::TypeOf { pos, .. } => *pos,
|
| Expr::TypeOf { pos, .. } => *pos,
|
||||||
@@ -234,7 +240,7 @@ pub enum EventAction {
|
|||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum Stmt {
|
pub enum Stmt {
|
||||||
Label(String),
|
Label(String),
|
||||||
LineNumber(u32),
|
LineNumber(u32, SourcePos),
|
||||||
Assign {
|
Assign {
|
||||||
target: Expr,
|
target: Expr,
|
||||||
value: Expr,
|
value: Expr,
|
||||||
@@ -259,7 +265,7 @@ pub enum Stmt {
|
|||||||
If {
|
If {
|
||||||
cond: Expr,
|
cond: Expr,
|
||||||
then_body: Vec<Stmt>,
|
then_body: Vec<Stmt>,
|
||||||
elseifs: Vec<(Expr, Vec<Stmt>)>,
|
elseifs: Vec<(Expr, Vec<Stmt>, SourcePos)>,
|
||||||
else_body: Option<Vec<Stmt>>,
|
else_body: Option<Vec<Stmt>>,
|
||||||
pos: SourcePos,
|
pos: SourcePos,
|
||||||
},
|
},
|
||||||
@@ -275,17 +281,20 @@ pub enum Stmt {
|
|||||||
step: Option<Expr>,
|
step: Option<Expr>,
|
||||||
body: Vec<Stmt>,
|
body: Vec<Stmt>,
|
||||||
pos: SourcePos,
|
pos: SourcePos,
|
||||||
|
end_pos: SourcePos,
|
||||||
},
|
},
|
||||||
DoLoop {
|
DoLoop {
|
||||||
pre: Option<(bool, Expr)>, // (ist UNTIL, Bedingung)
|
pre: Option<(bool, Expr)>, // (ist UNTIL, Bedingung)
|
||||||
post: Option<(bool, Expr)>,
|
post: Option<(bool, Expr)>,
|
||||||
body: Vec<Stmt>,
|
body: Vec<Stmt>,
|
||||||
pos: SourcePos,
|
pos: SourcePos,
|
||||||
|
end_pos: SourcePos,
|
||||||
},
|
},
|
||||||
While {
|
While {
|
||||||
cond: Expr,
|
cond: Expr,
|
||||||
body: Vec<Stmt>,
|
body: Vec<Stmt>,
|
||||||
pos: SourcePos,
|
pos: SourcePos,
|
||||||
|
end_pos: SourcePos,
|
||||||
},
|
},
|
||||||
Goto {
|
Goto {
|
||||||
target: LabelRef,
|
target: LabelRef,
|
||||||
|
|||||||
@@ -678,6 +678,7 @@ pub struct FormObject {
|
|||||||
pub name: String,
|
pub name: String,
|
||||||
pub class: ObjectClass,
|
pub class: ObjectClass,
|
||||||
pub parent_form: Option<String>,
|
pub parent_form: Option<String>,
|
||||||
|
pub parent: Option<u16>,
|
||||||
pub array: bool,
|
pub array: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -698,6 +699,13 @@ impl FormCatalog {
|
|||||||
self.objects.push(FormObject {
|
self.objects.push(FormObject {
|
||||||
name: name.into().to_uppercase(),
|
name: name.into().to_uppercase(),
|
||||||
class,
|
class,
|
||||||
|
parent: parent_form
|
||||||
|
.and_then(|name| {
|
||||||
|
self.objects
|
||||||
|
.iter()
|
||||||
|
.rposition(|o| o.name.eq_ignore_ascii_case(name))
|
||||||
|
})
|
||||||
|
.map(|id| id as u16),
|
||||||
parent_form: parent_form.map(str::to_uppercase),
|
parent_form: parent_form.map(str::to_uppercase),
|
||||||
array,
|
array,
|
||||||
});
|
});
|
||||||
@@ -712,16 +720,33 @@ impl FormCatalog {
|
|||||||
.map(|(i, o)| (i as u16, o))
|
.map(|(i, o)| (i as u16, o))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn append(&mut self, other: &Self) {
|
||||||
|
let offset = self.objects.len() as u16;
|
||||||
|
self.objects
|
||||||
|
.extend(other.objects.iter().cloned().map(|mut object| {
|
||||||
|
object.parent = object.parent.map(|id| id + offset);
|
||||||
|
object
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_in(&self, name: &str, form: &str) -> Option<(u16, &FormObject)> {
|
||||||
|
self.objects
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.find(|(_, o)| o.name.eq_ignore_ascii_case(name) && self.belongs_to(o, form))
|
||||||
|
.map(|(id, o)| (id as u16, o))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn belongs_to(&self, object: &FormObject, form: &str) -> bool {
|
pub fn belongs_to(&self, object: &FormObject, form: &str) -> bool {
|
||||||
let mut parent = object.parent_form.as_deref();
|
let mut parent = object.parent;
|
||||||
for _ in 0..self.objects.len() {
|
for _ in 0..self.objects.len() {
|
||||||
let Some(name) = parent else { return false };
|
let Some(object) = parent.and_then(|id| self.objects.get(id as usize)) else {
|
||||||
if name.eq_ignore_ascii_case(form) {
|
return false;
|
||||||
|
};
|
||||||
|
if object.name.eq_ignore_ascii_case(form) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
parent = self
|
parent = object.parent;
|
||||||
.find(name)
|
|
||||||
.and_then(|(_, object)| object.parent_form.as_deref());
|
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,10 +124,22 @@ pub struct DataItem {
|
|||||||
pub line: u32,
|
pub line: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Aufgelöste COMMON-Deklaration für die projektweite Slotverknüpfung.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct HCommon {
|
||||||
|
pub slot: u16,
|
||||||
|
pub block: Option<String>,
|
||||||
|
pub key: String,
|
||||||
|
pub ty: HTy,
|
||||||
|
pub dims: Option<Vec<(Option<i32>, Option<i32>)>>,
|
||||||
|
pub pos: crate::SourcePos,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct HirModule {
|
pub struct HirModule {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub globals: Vec<HVar>,
|
pub globals: Vec<HVar>,
|
||||||
|
pub commons: Vec<HCommon>,
|
||||||
pub udts: Vec<HUdt>,
|
pub udts: Vec<HUdt>,
|
||||||
/// Prozeduren; Index 0 ist das Hauptprogramm.
|
/// Prozeduren; Index 0 ist das Hauptprogramm.
|
||||||
pub procs: Vec<HProc>,
|
pub procs: Vec<HProc>,
|
||||||
@@ -382,6 +394,8 @@ pub enum Builtin {
|
|||||||
pub enum HExpr {
|
pub enum HExpr {
|
||||||
Int(i16),
|
Int(i16),
|
||||||
Lng(i32),
|
Lng(i32),
|
||||||
|
/// TYPE-Tabellenreferenz für ISAM; beim Projektlinken zu versetzen.
|
||||||
|
UdtId(u16),
|
||||||
Sng(f32),
|
Sng(f32),
|
||||||
Dbl(f64),
|
Dbl(f64),
|
||||||
Cur(i64),
|
Cur(i64),
|
||||||
@@ -513,6 +527,7 @@ pub enum HResume {
|
|||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct HStmt {
|
pub struct HStmt {
|
||||||
|
pub pos: crate::SourcePos,
|
||||||
pub line: u32,
|
pub line: u32,
|
||||||
pub kind: HStmtKind,
|
pub kind: HStmtKind,
|
||||||
}
|
}
|
||||||
@@ -596,12 +611,14 @@ pub enum HStmtKind {
|
|||||||
},
|
},
|
||||||
/// DO/LOOP, WHILE/WEND (nur `pre`) — Bedingungen: (ist_until, Ausdruck).
|
/// DO/LOOP, WHILE/WEND (nur `pre`) — Bedingungen: (ist_until, Ausdruck).
|
||||||
Loop {
|
Loop {
|
||||||
|
end_pos: crate::SourcePos,
|
||||||
pre: Option<(bool, HExpr)>,
|
pre: Option<(bool, HExpr)>,
|
||||||
post: Option<(bool, HExpr)>,
|
post: Option<(bool, HExpr)>,
|
||||||
body: Vec<HStmt>,
|
body: Vec<HStmt>,
|
||||||
exit_label: LabelId,
|
exit_label: LabelId,
|
||||||
},
|
},
|
||||||
For {
|
For {
|
||||||
|
end_pos: crate::SourcePos,
|
||||||
var: HPlace,
|
var: HPlace,
|
||||||
ty: NumTy,
|
ty: NumTy,
|
||||||
from: HExpr,
|
from: HExpr,
|
||||||
|
|||||||
@@ -342,6 +342,7 @@ pub fn lex(source: &str) -> LexOutput {
|
|||||||
}
|
}
|
||||||
let start = i;
|
let start = i;
|
||||||
let pos = SourcePos {
|
let pos = SourcePos {
|
||||||
|
source: 0,
|
||||||
line: line_no,
|
line: line_no,
|
||||||
column: (start + 1) as u32,
|
column: (start + 1) as u32,
|
||||||
};
|
};
|
||||||
@@ -413,6 +414,7 @@ pub fn lex(source: &str) -> LexOutput {
|
|||||||
TokenKind::Num(NumValue::Int(v as u16 as i16))
|
TokenKind::Num(NumValue::Int(v as u16 as i16))
|
||||||
} else {
|
} else {
|
||||||
diagnostics.push(Diagnostic {
|
diagnostics.push(Diagnostic {
|
||||||
|
file: None,
|
||||||
pos,
|
pos,
|
||||||
message: "Overflow".into(),
|
message: "Overflow".into(),
|
||||||
});
|
});
|
||||||
@@ -421,6 +423,7 @@ pub fn lex(source: &str) -> LexOutput {
|
|||||||
tokens.push(Token { kind, pos });
|
tokens.push(Token { kind, pos });
|
||||||
}
|
}
|
||||||
Err(_) => diagnostics.push(Diagnostic {
|
Err(_) => diagnostics.push(Diagnostic {
|
||||||
|
file: None,
|
||||||
pos,
|
pos,
|
||||||
message: "Syntax error".into(),
|
message: "Syntax error".into(),
|
||||||
}),
|
}),
|
||||||
@@ -496,6 +499,7 @@ pub fn lex(source: &str) -> LexOutput {
|
|||||||
(Some(Suffix::Integer), _) => {
|
(Some(Suffix::Integer), _) => {
|
||||||
if dval > i16::MAX as f64 || dval < i16::MIN as f64 {
|
if dval > i16::MAX as f64 || dval < i16::MIN as f64 {
|
||||||
diagnostics.push(Diagnostic {
|
diagnostics.push(Diagnostic {
|
||||||
|
file: None,
|
||||||
pos,
|
pos,
|
||||||
message: "Overflow".into(),
|
message: "Overflow".into(),
|
||||||
});
|
});
|
||||||
@@ -505,6 +509,7 @@ pub fn lex(source: &str) -> LexOutput {
|
|||||||
(Some(Suffix::Long), _) => {
|
(Some(Suffix::Long), _) => {
|
||||||
if dval > i32::MAX as f64 || dval < i32::MIN as f64 {
|
if dval > i32::MAX as f64 || dval < i32::MIN as f64 {
|
||||||
diagnostics.push(Diagnostic {
|
diagnostics.push(Diagnostic {
|
||||||
|
file: None,
|
||||||
pos,
|
pos,
|
||||||
message: "Overflow".into(),
|
message: "Overflow".into(),
|
||||||
});
|
});
|
||||||
@@ -518,6 +523,7 @@ pub fn lex(source: &str) -> LexOutput {
|
|||||||
}
|
}
|
||||||
(Some(Suffix::Str), _) => {
|
(Some(Suffix::Str), _) => {
|
||||||
diagnostics.push(Diagnostic {
|
diagnostics.push(Diagnostic {
|
||||||
|
file: None,
|
||||||
pos,
|
pos,
|
||||||
message: "Syntax error".into(),
|
message: "Syntax error".into(),
|
||||||
});
|
});
|
||||||
@@ -674,6 +680,7 @@ pub fn lex(source: &str) -> LexOutput {
|
|||||||
}
|
}
|
||||||
other => {
|
other => {
|
||||||
diagnostics.push(Diagnostic {
|
diagnostics.push(Diagnostic {
|
||||||
|
file: None,
|
||||||
pos,
|
pos,
|
||||||
message: format!("Syntax error ('{other}')"),
|
message: format!("Syntax error ('{other}')"),
|
||||||
});
|
});
|
||||||
@@ -692,6 +699,7 @@ pub fn lex(source: &str) -> LexOutput {
|
|||||||
tokens.push(Token {
|
tokens.push(Token {
|
||||||
kind: TokenKind::Eol,
|
kind: TokenKind::Eol,
|
||||||
pos: SourcePos {
|
pos: SourcePos {
|
||||||
|
source: 0,
|
||||||
line: line_no,
|
line: line_no,
|
||||||
column: (chars.len() + 1) as u32,
|
column: (chars.len() + 1) as u32,
|
||||||
},
|
},
|
||||||
@@ -704,6 +712,7 @@ pub fn lex(source: &str) -> LexOutput {
|
|||||||
tokens.push(Token {
|
tokens.push(Token {
|
||||||
kind: TokenKind::Eof,
|
kind: TokenKind::Eof,
|
||||||
pos: SourcePos {
|
pos: SourcePos {
|
||||||
|
source: 0,
|
||||||
line: (source.lines().count() + 1) as u32,
|
line: (source.lines().count() + 1) as u32,
|
||||||
column: 1,
|
column: 1,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,10 +11,12 @@ pub mod hir;
|
|||||||
pub mod lexer;
|
pub mod lexer;
|
||||||
pub mod parser;
|
pub mod parser;
|
||||||
pub mod sema;
|
pub mod sema;
|
||||||
|
pub mod source;
|
||||||
|
|
||||||
/// Quelltextposition für Diagnostik (1-basiert, wie im IDE-Vorbild).
|
/// Quelltextposition für Diagnostik (1-basiert, wie im IDE-Vorbild).
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
pub struct SourcePos {
|
pub struct SourcePos {
|
||||||
|
pub source: u32,
|
||||||
pub line: u32,
|
pub line: u32,
|
||||||
pub column: u32,
|
pub column: u32,
|
||||||
}
|
}
|
||||||
@@ -23,12 +25,16 @@ pub struct SourcePos {
|
|||||||
/// Meldungen des Vorbilds, wo es eine Entsprechung gibt.
|
/// Meldungen des Vorbilds, wo es eine Entsprechung gibt.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Diagnostic {
|
pub struct Diagnostic {
|
||||||
|
pub file: Option<String>,
|
||||||
pub pos: SourcePos,
|
pub pos: SourcePos,
|
||||||
pub message: String,
|
pub message: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for Diagnostic {
|
impl std::fmt::Display for Diagnostic {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
if let Some(file) = &self.file {
|
||||||
|
write!(f, "{file}:")?;
|
||||||
|
}
|
||||||
write!(f, "{}:{}: {}", self.pos.line, self.pos.column, self.message)
|
write!(f, "{}:{}: {}", self.pos.line, self.pos.column, self.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ impl<'a> P<'a> {
|
|||||||
fn err(&mut self, msg: impl Into<String>) {
|
fn err(&mut self, msg: impl Into<String>) {
|
||||||
let pos = self.pos();
|
let pos = self.pos();
|
||||||
self.diags.push(Diagnostic {
|
self.diags.push(Diagnostic {
|
||||||
|
file: None,
|
||||||
pos,
|
pos,
|
||||||
message: msg.into(),
|
message: msg.into(),
|
||||||
});
|
});
|
||||||
@@ -199,13 +200,13 @@ impl<'a> P<'a> {
|
|||||||
if let TokenKind::Num(NumValue::Int(n)) = self.k() {
|
if let TokenKind::Num(NumValue::Int(n)) = self.k() {
|
||||||
if n >= 0 {
|
if n >= 0 {
|
||||||
self.advance();
|
self.advance();
|
||||||
return Some(Stmt::LineNumber(n as u32));
|
return Some(Stmt::LineNumber(n as u32, pos));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let TokenKind::Num(NumValue::Long(n)) = self.k() {
|
if let TokenKind::Num(NumValue::Long(n)) = self.k() {
|
||||||
if n >= 0 {
|
if n >= 0 {
|
||||||
self.advance();
|
self.advance();
|
||||||
return Some(Stmt::LineNumber(n as u32));
|
return Some(Stmt::LineNumber(n as u32, pos));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let TokenKind::Ident { name, suffix: None } = self.k() {
|
if let TokenKind::Ident { name, suffix: None } = self.k() {
|
||||||
@@ -816,7 +817,7 @@ impl<'a> P<'a> {
|
|||||||
{
|
{
|
||||||
if w == "ALL" {
|
if w == "ALL" {
|
||||||
self.advance();
|
self.advance();
|
||||||
let args = vec![Expr::LongLit(ROLLBACK_ALL)];
|
let args = vec![Expr::LongLit(ROLLBACK_ALL, pos)];
|
||||||
return Some(Stmt::Call {
|
return Some(Stmt::Call {
|
||||||
name,
|
name,
|
||||||
suffix: None,
|
suffix: None,
|
||||||
@@ -1210,11 +1211,13 @@ impl<'a> P<'a> {
|
|||||||
let stop = |p: &P| p.is_kw(Kw::ElseIf) || p.is_kw(Kw::Else) || p.at_end_pair(Kw::If);
|
let stop = |p: &P| p.is_kw(Kw::ElseIf) || p.is_kw(Kw::Else) || p.at_end_pair(Kw::If);
|
||||||
let then_body = self.parse_stmt_list(stop);
|
let then_body = self.parse_stmt_list(stop);
|
||||||
let mut elseifs = Vec::new();
|
let mut elseifs = Vec::new();
|
||||||
while self.eat_kw(Kw::ElseIf) {
|
while self.is_kw(Kw::ElseIf) {
|
||||||
|
let elseif_pos = self.pos();
|
||||||
|
self.advance();
|
||||||
let c = self.parse_expr()?;
|
let c = self.parse_expr()?;
|
||||||
self.expect_kw(Kw::Then, "THEN");
|
self.expect_kw(Kw::Then, "THEN");
|
||||||
let b = self.parse_stmt_list(stop);
|
let b = self.parse_stmt_list(stop);
|
||||||
elseifs.push((c, b));
|
elseifs.push((c, b, elseif_pos));
|
||||||
}
|
}
|
||||||
let else_body = if self.eat_kw(Kw::Else) {
|
let else_body = if self.eat_kw(Kw::Else) {
|
||||||
Some(self.parse_stmt_list(|p: &P| p.at_end_pair(Kw::If)))
|
Some(self.parse_stmt_list(|p: &P| p.at_end_pair(Kw::If)))
|
||||||
@@ -1349,6 +1352,7 @@ impl<'a> P<'a> {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
let body = self.parse_stmt_list(|p: &P| p.is_kw(Kw::Next));
|
let body = self.parse_stmt_list(|p: &P| p.is_kw(Kw::Next));
|
||||||
|
let end_pos = self.pos();
|
||||||
if self.eat_kw(Kw::Next) {
|
if self.eat_kw(Kw::Next) {
|
||||||
// Optional: Zählvariable(n) hinter NEXT
|
// Optional: Zählvariable(n) hinter NEXT
|
||||||
while matches!(self.k(), TokenKind::Ident { .. }) {
|
while matches!(self.k(), TokenKind::Ident { .. }) {
|
||||||
@@ -1367,6 +1371,7 @@ impl<'a> P<'a> {
|
|||||||
step,
|
step,
|
||||||
body,
|
body,
|
||||||
pos,
|
pos,
|
||||||
|
end_pos,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1380,6 +1385,7 @@ impl<'a> P<'a> {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
let body = self.parse_stmt_list(|p: &P| p.is_kw(Kw::Loop));
|
let body = self.parse_stmt_list(|p: &P| p.is_kw(Kw::Loop));
|
||||||
|
let end_pos = self.pos();
|
||||||
let mut post = None;
|
let mut post = None;
|
||||||
if self.eat_kw(Kw::Loop) {
|
if self.eat_kw(Kw::Loop) {
|
||||||
if self.eat_kw(Kw::While) {
|
if self.eat_kw(Kw::While) {
|
||||||
@@ -1395,6 +1401,7 @@ impl<'a> P<'a> {
|
|||||||
post,
|
post,
|
||||||
body,
|
body,
|
||||||
pos,
|
pos,
|
||||||
|
end_pos,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1402,10 +1409,16 @@ impl<'a> P<'a> {
|
|||||||
self.advance(); // WHILE
|
self.advance(); // WHILE
|
||||||
let cond = self.parse_expr()?;
|
let cond = self.parse_expr()?;
|
||||||
let body = self.parse_stmt_list(|p: &P| p.is_kw(Kw::Wend));
|
let body = self.parse_stmt_list(|p: &P| p.is_kw(Kw::Wend));
|
||||||
|
let end_pos = self.pos();
|
||||||
if !self.eat_kw(Kw::Wend) {
|
if !self.eat_kw(Kw::Wend) {
|
||||||
self.err("WHILE without WEND");
|
self.err("WHILE without WEND");
|
||||||
}
|
}
|
||||||
Some(Stmt::While { cond, body, pos })
|
Some(Stmt::While {
|
||||||
|
cond,
|
||||||
|
body,
|
||||||
|
pos,
|
||||||
|
end_pos,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `ON <quelle>[(n)] GOSUB ziel`. Liefert `None` und lässt den Cursor
|
/// `ON <quelle>[(n)] GOSUB ziel`. Liefert `None` und lässt den Cursor
|
||||||
@@ -2214,18 +2227,20 @@ impl<'a> P<'a> {
|
|||||||
}
|
}
|
||||||
match self.k() {
|
match self.k() {
|
||||||
TokenKind::Num(n) => {
|
TokenKind::Num(n) => {
|
||||||
|
let pos = self.pos();
|
||||||
self.advance();
|
self.advance();
|
||||||
Some(match n {
|
Some(match n {
|
||||||
NumValue::Int(v) => Expr::IntLit(v),
|
NumValue::Int(v) => Expr::IntLit(v, pos),
|
||||||
NumValue::Long(v) => Expr::LongLit(v),
|
NumValue::Long(v) => Expr::LongLit(v, pos),
|
||||||
NumValue::Single(v) => Expr::SingleLit(v),
|
NumValue::Single(v) => Expr::SingleLit(v, pos),
|
||||||
NumValue::Double(v) => Expr::DoubleLit(v),
|
NumValue::Double(v) => Expr::DoubleLit(v, pos),
|
||||||
NumValue::Currency(v) => Expr::CurrencyLit(v),
|
NumValue::Currency(v) => Expr::CurrencyLit(v, pos),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
TokenKind::Str(s) => {
|
TokenKind::Str(s) => {
|
||||||
|
let pos = self.pos();
|
||||||
self.advance();
|
self.advance();
|
||||||
Some(Expr::StrLit(s))
|
Some(Expr::StrLit(s, pos))
|
||||||
}
|
}
|
||||||
TokenKind::LParen => {
|
TokenKind::LParen => {
|
||||||
self.advance();
|
self.advance();
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ struct Scope {
|
|||||||
/// Stack der Schleifen-Exit-Labels für `EXIT FOR`/`EXIT DO`.
|
/// Stack der Schleifen-Exit-Labels für `EXIT FOR`/`EXIT DO`.
|
||||||
loop_exits: Vec<(LoopKind, LabelId)>,
|
loop_exits: Vec<(LoopKind, LabelId)>,
|
||||||
current_line: u32,
|
current_line: u32,
|
||||||
|
current_pos: SourcePos,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Clone, Copy)]
|
#[derive(PartialEq, Clone, Copy)]
|
||||||
@@ -412,7 +413,13 @@ pub fn lower_with_forms(
|
|||||||
{
|
{
|
||||||
catalog.add(&module.name, ObjectClass::Form, None, false);
|
catalog.add(&module.name, ObjectClass::Form, None, false);
|
||||||
}
|
}
|
||||||
let mut s = Sema {
|
let mut s = new_sema(module, &catalog);
|
||||||
|
let hir = s.run(module);
|
||||||
|
(hir, s.diags)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_sema(module: &Module, catalog: &FormCatalog) -> Sema {
|
||||||
|
Sema {
|
||||||
diags: Vec::new(),
|
diags: Vec::new(),
|
||||||
deftypes: [const { None }; 26],
|
deftypes: [const { None }; 26],
|
||||||
explicit: false,
|
explicit: false,
|
||||||
@@ -427,16 +434,79 @@ pub fn lower_with_forms(
|
|||||||
module_labels: HashMap::new(),
|
module_labels: HashMap::new(),
|
||||||
module_line_labels: HashMap::new(),
|
module_line_labels: HashMap::new(),
|
||||||
globals: Vec::new(),
|
globals: Vec::new(),
|
||||||
|
commons: Vec::new(),
|
||||||
data: Vec::new(),
|
data: Vec::new(),
|
||||||
data_marks_name: HashMap::new(),
|
data_marks_name: HashMap::new(),
|
||||||
data_marks_line: HashMap::new(),
|
data_marks_line: HashMap::new(),
|
||||||
hir_procs: Vec::new(),
|
hir_procs: Vec::new(),
|
||||||
isam_typ: HashMap::new(),
|
isam_typ: HashMap::new(),
|
||||||
forms: catalog,
|
forms: catalog.clone(),
|
||||||
|
module_name: module.name.clone(),
|
||||||
event_procs: Vec::new(),
|
event_procs: Vec::new(),
|
||||||
};
|
}
|
||||||
let hir = s.run(module);
|
}
|
||||||
(hir, s.diags)
|
|
||||||
|
/// Exportdeklarationen mit den Typen und Konstanten ihres Ursprungsmoduls.
|
||||||
|
/// Rümpfe werden hier nicht übersetzt; dafür bleibt der reguläre Sema-Durchlauf zuständig.
|
||||||
|
pub fn export_declarations(module: &Module, constants: &[Stmt]) -> Vec<Stmt> {
|
||||||
|
let mut s = new_sema(module, &FormCatalog::default());
|
||||||
|
let mut scope = Scope::default();
|
||||||
|
let mut declarations = Vec::new();
|
||||||
|
let local: HashSet<_> = module
|
||||||
|
.body
|
||||||
|
.iter()
|
||||||
|
.flat_map(|stmt| match stmt {
|
||||||
|
Stmt::ConstDecl { items, .. } => items.iter().map(|i| i.0.as_str()).collect(),
|
||||||
|
_ => Vec::new(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
for stmt in constants {
|
||||||
|
if let Stmt::ConstDecl { items, .. } = stmt {
|
||||||
|
if !local.contains(items[0].0.as_str()) {
|
||||||
|
s.lower_stmt(stmt, &mut scope, &mut Vec::new());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for stmt in &module.body {
|
||||||
|
match stmt {
|
||||||
|
Stmt::DefType { .. } => s.lower_stmt(stmt, &mut scope, &mut Vec::new()),
|
||||||
|
Stmt::ConstDecl { items, pos } => {
|
||||||
|
for item in items {
|
||||||
|
let declaration = Stmt::ConstDecl {
|
||||||
|
items: vec![item.clone()],
|
||||||
|
pos: *pos,
|
||||||
|
};
|
||||||
|
s.lower_stmt(&declaration, &mut scope, &mut Vec::new());
|
||||||
|
let value = match s.consts.get(&item.0).and_then(|(_, value)| value.as_ref()) {
|
||||||
|
Some(ConstVal::Num(n)) => Expr::DoubleLit(*n, item.2.pos()),
|
||||||
|
Some(ConstVal::Str(value)) => Expr::StrLit(value.clone(), item.2.pos()),
|
||||||
|
None => item.2.clone(),
|
||||||
|
};
|
||||||
|
declarations.push(Stmt::ConstDecl {
|
||||||
|
items: vec![(item.0.clone(), item.1, value)],
|
||||||
|
pos: *pos,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Stmt::TypeDecl { .. } => declarations.push(stmt.clone()),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for proc in &module.procs {
|
||||||
|
let mut sig = proc.sig.clone();
|
||||||
|
if sig.suffix.is_none() && sig.kind == ProcKind::Function {
|
||||||
|
sig.suffix =
|
||||||
|
Suffix::from_char(s.var_key(&sig.name, &None, false).chars().last().unwrap());
|
||||||
|
}
|
||||||
|
for param in &mut sig.params {
|
||||||
|
if param.suffix.is_none() && param.as_type.is_none() {
|
||||||
|
param.suffix =
|
||||||
|
Suffix::from_char(s.var_key(¶m.name, &None, false).chars().last().unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
declarations.push(Stmt::Declare { sig, pos: proc.pos });
|
||||||
|
}
|
||||||
|
declarations
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Sema {
|
struct Sema {
|
||||||
@@ -467,6 +537,7 @@ struct Sema {
|
|||||||
module_line_labels: HashMap<u32, LabelId>,
|
module_line_labels: HashMap<u32, LabelId>,
|
||||||
/// Globale Slots (Modulvariablen, STATICs, versteckte Temps).
|
/// Globale Slots (Modulvariablen, STATICs, versteckte Temps).
|
||||||
globals: Vec<hir::HVar>,
|
globals: Vec<hir::HVar>,
|
||||||
|
commons: Vec<hir::HCommon>,
|
||||||
/// DATA-Konstanten des Moduls (aus dem Prescan, statisch).
|
/// DATA-Konstanten des Moduls (aus dem Prescan, statisch).
|
||||||
data: Vec<hir::DataItem>,
|
data: Vec<hir::DataItem>,
|
||||||
data_marks_name: HashMap<String, u32>,
|
data_marks_name: HashMap<String, u32>,
|
||||||
@@ -474,12 +545,14 @@ struct Sema {
|
|||||||
/// Fertige HIR-Prozeduren nach Id (0 = Hauptprogramm).
|
/// Fertige HIR-Prozeduren nach Id (0 = Hauptprogramm).
|
||||||
hir_procs: Vec<Option<hir::HProc>>,
|
hir_procs: Vec<Option<hir::HProc>>,
|
||||||
forms: FormCatalog,
|
forms: FormCatalog,
|
||||||
|
module_name: String,
|
||||||
event_procs: Vec<hir::HEventProc>,
|
event_procs: Vec<hir::HEventProc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Sema {
|
impl Sema {
|
||||||
fn err(&mut self, pos: SourcePos, msg: impl Into<String>) {
|
fn err(&mut self, pos: SourcePos, msg: impl Into<String>) {
|
||||||
self.diags.push(Diagnostic {
|
self.diags.push(Diagnostic {
|
||||||
|
file: None,
|
||||||
pos,
|
pos,
|
||||||
message: msg.into(),
|
message: msg.into(),
|
||||||
});
|
});
|
||||||
@@ -489,7 +562,10 @@ impl Sema {
|
|||||||
// Pass 1: Prozeduren, DECLAREs und TYPEs registrieren.
|
// Pass 1: Prozeduren, DECLAREs und TYPEs registrieren.
|
||||||
for stmt in &module.body {
|
for stmt in &module.body {
|
||||||
match stmt {
|
match stmt {
|
||||||
Stmt::Declare { sig, .. } => self.register_proc(sig),
|
Stmt::Declare { sig, pos } => self.register_proc(sig, *pos),
|
||||||
|
Stmt::DefType { .. } => {
|
||||||
|
self.lower_stmt(stmt, &mut Scope::default(), &mut Vec::new())
|
||||||
|
}
|
||||||
Stmt::TypeDecl { name, fields, pos } => {
|
Stmt::TypeDecl { name, fields, pos } => {
|
||||||
let mut hfields = Vec::new();
|
let mut hfields = Vec::new();
|
||||||
for (fname, ftype) in fields {
|
for (fname, ftype) in fields {
|
||||||
@@ -514,8 +590,12 @@ impl Sema {
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let mut defined = HashSet::new();
|
||||||
for proc in &module.procs {
|
for proc in &module.procs {
|
||||||
self.register_proc(&proc.sig);
|
if !defined.insert(proc.sig.name.clone()) {
|
||||||
|
self.err(proc.pos, "Duplicate definition");
|
||||||
|
}
|
||||||
|
self.register_proc(&proc.sig, proc.pos);
|
||||||
}
|
}
|
||||||
for proc in &module.procs {
|
for proc in &module.procs {
|
||||||
self.register_event_proc(proc);
|
self.register_event_proc(proc);
|
||||||
@@ -524,6 +604,8 @@ impl Sema {
|
|||||||
self.hir_procs
|
self.hir_procs
|
||||||
.resize_with(self.next_proc_id as usize, || None);
|
.resize_with(self.next_proc_id as usize, || None);
|
||||||
|
|
||||||
|
self.deftypes = [const { None }; 26];
|
||||||
|
|
||||||
// Pass 2: Modulrumpf. Labels, Zeilennummern und DATA-Positionen
|
// Pass 2: Modulrumpf. Labels, Zeilennummern und DATA-Positionen
|
||||||
// werden vorab eingesammelt (RESTORE nach vorn, statisches DATA).
|
// werden vorab eingesammelt (RESTORE nach vorn, statisches DATA).
|
||||||
let mut scope = Scope {
|
let mut scope = Scope {
|
||||||
@@ -550,7 +632,9 @@ impl Sema {
|
|||||||
|
|
||||||
// Pass 3: Prozedurrümpfe.
|
// Pass 3: Prozedurrümpfe.
|
||||||
for proc in &module.procs {
|
for proc in &module.procs {
|
||||||
|
let deftypes = self.deftypes.clone();
|
||||||
self.lower_proc(proc);
|
self.lower_proc(proc);
|
||||||
|
self.deftypes = deftypes;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Zusammensetzen: fehlende Rümpfe (nur DECLARE) behalten ihre
|
// Zusammensetzen: fehlende Rümpfe (nur DECLARE) behalten ihre
|
||||||
@@ -574,7 +658,7 @@ impl Sema {
|
|||||||
name: format!("ARG{index}"),
|
name: format!("ARG{index}"),
|
||||||
ty: self.h_ty(ty),
|
ty: self.h_ty(ty),
|
||||||
array: *array,
|
array: *array,
|
||||||
by_ref: !array && !matches!(ty, Ty::Udt(_)),
|
by_ref: !array,
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let mut locals = params
|
let mut locals = params
|
||||||
@@ -614,6 +698,7 @@ impl Sema {
|
|||||||
Some(hir::HirModule {
|
Some(hir::HirModule {
|
||||||
name: module.name.clone(),
|
name: module.name.clone(),
|
||||||
globals: std::mem::take(&mut self.globals),
|
globals: std::mem::take(&mut self.globals),
|
||||||
|
commons: std::mem::take(&mut self.commons),
|
||||||
udts: std::mem::take(&mut self.udt_defs),
|
udts: std::mem::take(&mut self.udt_defs),
|
||||||
procs,
|
procs,
|
||||||
data: std::mem::take(&mut self.data),
|
data: std::mem::take(&mut self.data),
|
||||||
@@ -638,11 +723,11 @@ impl Sema {
|
|||||||
let hty = self.h_ty(&ty);
|
let hty = self.h_ty(&ty);
|
||||||
let slot = scope.locals.len() as u16;
|
let slot = scope.locals.len() as u16;
|
||||||
scope.locals.push(hir::HVar {
|
scope.locals.push(hir::HVar {
|
||||||
name: p.name.clone(),
|
name: key.trim_end_matches("\u{1}AS").to_string(),
|
||||||
ty: hty.clone(),
|
ty: hty.clone(),
|
||||||
array: p.array,
|
array: p.array,
|
||||||
});
|
});
|
||||||
let by_ref = !p.array && !matches!(ty, Ty::Udt(_));
|
let by_ref = !p.array;
|
||||||
scope.vars.insert(
|
scope.vars.insert(
|
||||||
key,
|
key,
|
||||||
VarInfo {
|
VarInfo {
|
||||||
@@ -669,7 +754,7 @@ impl Sema {
|
|||||||
let key = self.var_key(&proc.sig.name, &proc.sig.suffix, false);
|
let key = self.var_key(&proc.sig.name, &proc.sig.suffix, false);
|
||||||
let slot = scope.locals.len() as u16;
|
let slot = scope.locals.len() as u16;
|
||||||
scope.locals.push(hir::HVar {
|
scope.locals.push(hir::HVar {
|
||||||
name: proc.sig.name.clone(),
|
name: key.clone(),
|
||||||
ty: self.h_ty(&ret),
|
ty: self.h_ty(&ret),
|
||||||
array: false,
|
array: false,
|
||||||
});
|
});
|
||||||
@@ -721,7 +806,7 @@ impl Sema {
|
|||||||
.insert(n.clone(), self.data.len() as u32);
|
.insert(n.clone(), self.data.len() as u32);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Stmt::LineNumber(n) => {
|
Stmt::LineNumber(n, _) => {
|
||||||
let id = scope.new_label();
|
let id = scope.new_label();
|
||||||
scope.line_labels.insert(*n, id);
|
scope.line_labels.insert(*n, id);
|
||||||
if module_data {
|
if module_data {
|
||||||
@@ -745,7 +830,7 @@ impl Sema {
|
|||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
self.prescan(then_body, scope, module_data);
|
self.prescan(then_body, scope, module_data);
|
||||||
for (_, b) in elseifs {
|
for (_, b, _) in elseifs {
|
||||||
self.prescan(b, scope, module_data);
|
self.prescan(b, scope, module_data);
|
||||||
}
|
}
|
||||||
if let Some(b) = else_body {
|
if let Some(b) = else_body {
|
||||||
@@ -773,18 +858,24 @@ impl Sema {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn register_proc(&mut self, sig: &ProcSig) {
|
fn register_proc(&mut self, sig: &ProcSig, pos: SourcePos) {
|
||||||
let ret = match sig.kind {
|
let ret = match sig.kind {
|
||||||
ProcKind::Function => self.name_ty(&sig.name, &sig.suffix),
|
ProcKind::Function => self.name_ty(&sig.name, &sig.suffix),
|
||||||
ProcKind::Sub => Ty::Unknown,
|
ProcKind::Sub => Ty::Unknown,
|
||||||
};
|
};
|
||||||
let params = sig
|
let params: Vec<_> = sig
|
||||||
.params
|
.params
|
||||||
.iter()
|
.iter()
|
||||||
.map(|p| (self.param_ty(p), p.array))
|
.map(|p| (self.param_ty(p), p.array))
|
||||||
.collect();
|
.collect();
|
||||||
let id = match self.procs.get(&sig.name) {
|
let id = match self.procs.get(&sig.name) {
|
||||||
Some(p) => p.id,
|
Some(p) => {
|
||||||
|
let id = p.id;
|
||||||
|
if p.kind != sig.kind || p.ret != ret || p.params != params {
|
||||||
|
self.err(pos, "Parameter type mismatch");
|
||||||
|
}
|
||||||
|
id
|
||||||
|
}
|
||||||
None => {
|
None => {
|
||||||
let id = self.next_proc_id;
|
let id = self.next_proc_id;
|
||||||
self.next_proc_id += 1;
|
self.next_proc_id += 1;
|
||||||
@@ -895,14 +986,9 @@ impl Sema {
|
|||||||
fn event_binding(&self, name: &str) -> Option<(u16, ObjectClass, String)> {
|
fn event_binding(&self, name: &str) -> Option<(u16, ObjectClass, String)> {
|
||||||
let (base, event) = name.rsplit_once('_')?;
|
let (base, event) = name.rsplit_once('_')?;
|
||||||
let found = if base == "FORM" {
|
let found = if base == "FORM" {
|
||||||
self.forms
|
self.current_form()
|
||||||
.objects
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.find(|(_, o)| o.class == ObjectClass::Form)
|
|
||||||
.map(|(i, o)| (i as u16, o))
|
|
||||||
} else {
|
} else {
|
||||||
self.forms.find(base)
|
self.find_object(base)
|
||||||
}?;
|
}?;
|
||||||
Some((found.0, found.1.class, event.to_string()))
|
Some((found.0, found.1.class, event.to_string()))
|
||||||
}
|
}
|
||||||
@@ -916,12 +1002,68 @@ impl Sema {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn current_form(&self) -> Option<(u16, &forms::FormObject)> {
|
||||||
|
self.forms
|
||||||
|
.find(&self.module_name)
|
||||||
|
.filter(|(_, o)| o.class == ObjectClass::Form)
|
||||||
|
.or_else(|| {
|
||||||
|
let mut forms = self
|
||||||
|
.forms
|
||||||
|
.objects
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_, o)| o.class == ObjectClass::Form);
|
||||||
|
let (id, object) = forms.next()?;
|
||||||
|
forms.next().is_none().then_some((id as u16, object))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_object(&self, name: &str) -> Option<(u16, &forms::FormObject)> {
|
||||||
|
self.forms
|
||||||
|
.find_in(name, &self.module_name)
|
||||||
|
.or_else(|| {
|
||||||
|
self.forms
|
||||||
|
.find(name)
|
||||||
|
.filter(|(_, o)| o.class == ObjectClass::Form || o.class == ObjectClass::Screen)
|
||||||
|
})
|
||||||
|
.or_else(|| {
|
||||||
|
let mut matches = self
|
||||||
|
.forms
|
||||||
|
.objects
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_, o)| o.name.eq_ignore_ascii_case(name));
|
||||||
|
let (id, object) = matches.next()?;
|
||||||
|
matches.next().is_none().then_some((id as u16, object))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scoped_object(
|
||||||
|
&mut self,
|
||||||
|
name: &str,
|
||||||
|
parent: Option<&str>,
|
||||||
|
pos: SourcePos,
|
||||||
|
) -> Option<(u16, forms::FormObject)> {
|
||||||
|
if let Some(parent) = parent {
|
||||||
|
if let Some((id, object)) = self.forms.find_in(name, parent) {
|
||||||
|
return Some((id, object.clone()));
|
||||||
|
}
|
||||||
|
self.err(
|
||||||
|
pos,
|
||||||
|
format!("Control '{name}' not found in form '{parent}'"),
|
||||||
|
);
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
self.object_by_name(name, pos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn object_by_name(
|
fn object_by_name(
|
||||||
&mut self,
|
&mut self,
|
||||||
name: &str,
|
name: &str,
|
||||||
pos: SourcePos,
|
pos: SourcePos,
|
||||||
) -> Option<(u16, crate::forms::FormObject)> {
|
) -> Option<(u16, crate::forms::FormObject)> {
|
||||||
if let Some((id, object)) = self.forms.find(name) {
|
if let Some((id, object)) = self.find_object(name) {
|
||||||
return Some((id, object.clone()));
|
return Some((id, object.clone()));
|
||||||
}
|
}
|
||||||
self.err(pos, format!("Unknown object '{name}'"));
|
self.err(pos, format!("Unknown object '{name}'"));
|
||||||
@@ -936,14 +1078,9 @@ impl Sema {
|
|||||||
if self.declared_var(scope, name).is_some() || self.consts.contains_key(name) {
|
if self.declared_var(scope, name).is_some() || self.consts.contains_key(name) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let (object, form) = self
|
let (object, form) = self.current_form()?;
|
||||||
.forms
|
|
||||||
.objects
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.find(|(_, object)| object.class == ObjectClass::Form)?;
|
|
||||||
let (property, spec) = forms::property(form.class, name)?;
|
let (property, spec) = forms::property(form.class, name)?;
|
||||||
Some((object as u16, property, spec))
|
Some((object, property, spec))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn object_property(
|
fn object_property(
|
||||||
@@ -958,17 +1095,7 @@ impl Sema {
|
|||||||
let (object, member) = path.split_once('.')?;
|
let (object, member) = path.split_once('.')?;
|
||||||
(object, member, None)
|
(object, member, None)
|
||||||
};
|
};
|
||||||
let (id, object) = self.object_by_name(object_name, pos)?;
|
let (id, object) = self.scoped_object(object_name, parent, pos)?;
|
||||||
if let Some(parent) = parent {
|
|
||||||
let parent_ok = self.forms.belongs_to(&object, parent);
|
|
||||||
if !parent_ok {
|
|
||||||
self.err(
|
|
||||||
pos,
|
|
||||||
format!("Control '{object_name}' not found in form '{parent}'"),
|
|
||||||
);
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let Some((property, spec)) = forms::property(object.class, member) else {
|
let Some((property, spec)) = forms::property(object.class, member) else {
|
||||||
self.err(
|
self.err(
|
||||||
pos,
|
pos,
|
||||||
@@ -991,16 +1118,7 @@ impl Sema {
|
|||||||
let (object, member) = path.split_once('.')?;
|
let (object, member) = path.split_once('.')?;
|
||||||
(object, member, None)
|
(object, member, None)
|
||||||
};
|
};
|
||||||
let (id, object) = self.object_by_name(object_name, pos)?;
|
let (id, object) = self.scoped_object(object_name, parent, pos)?;
|
||||||
if let Some(parent) = parent {
|
|
||||||
if !self.forms.belongs_to(&object, parent) {
|
|
||||||
self.err(
|
|
||||||
pos,
|
|
||||||
format!("Control '{object_name}' not found in form '{parent}'"),
|
|
||||||
);
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some((id, object, member.to_string()))
|
Some((id, object, member.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1118,7 +1236,7 @@ impl Sema {
|
|||||||
fn alloc_global(&mut self, name: &str, ty: &Ty, array: bool) -> u16 {
|
fn alloc_global(&mut self, name: &str, ty: &Ty, array: bool) -> u16 {
|
||||||
let slot = self.globals.len() as u16;
|
let slot = self.globals.len() as u16;
|
||||||
self.globals.push(hir::HVar {
|
self.globals.push(hir::HVar {
|
||||||
name: name.to_string(),
|
name: name.trim_end_matches("\u{1}AS").to_string(),
|
||||||
ty: self.h_ty(ty),
|
ty: self.h_ty(ty),
|
||||||
array,
|
array,
|
||||||
});
|
});
|
||||||
@@ -1138,7 +1256,7 @@ impl Sema {
|
|||||||
} else {
|
} else {
|
||||||
let slot = scope.locals.len() as u16;
|
let slot = scope.locals.len() as u16;
|
||||||
scope.locals.push(hir::HVar {
|
scope.locals.push(hir::HVar {
|
||||||
name: name.to_string(),
|
name: name.trim_end_matches("\u{1}AS").to_string(),
|
||||||
ty: self.h_ty(ty),
|
ty: self.h_ty(ty),
|
||||||
array,
|
array,
|
||||||
});
|
});
|
||||||
@@ -1255,7 +1373,7 @@ impl Sema {
|
|||||||
let ty = self.name_ty(name, suffix);
|
let ty = self.name_ty(name, suffix);
|
||||||
// DEF FN: freie Namen binden an Modulvariablen (Vorbild-Semantik).
|
// DEF FN: freie Namen binden an Modulvariablen (Vorbild-Semantik).
|
||||||
let info = if scope.is_def_fn {
|
let info = if scope.is_def_fn {
|
||||||
let slot = self.alloc_global(name, &ty, array);
|
let slot = self.alloc_global(&key, &ty, array);
|
||||||
let info = VarInfo {
|
let info = VarInfo {
|
||||||
ty,
|
ty,
|
||||||
array,
|
array,
|
||||||
@@ -1267,7 +1385,7 @@ impl Sema {
|
|||||||
self.module_vars.insert(key, info.clone());
|
self.module_vars.insert(key, info.clone());
|
||||||
info
|
info
|
||||||
} else {
|
} else {
|
||||||
let (slot, global) = self.alloc_var(scope, name, &ty, array);
|
let (slot, global) = self.alloc_var(scope, &key, &ty, array);
|
||||||
let info = VarInfo {
|
let info = VarInfo {
|
||||||
ty,
|
ty,
|
||||||
array,
|
array,
|
||||||
@@ -1285,12 +1403,12 @@ impl Sema {
|
|||||||
/// Konstantenfaltung für `CONST`-Ausdrücke.
|
/// Konstantenfaltung für `CONST`-Ausdrücke.
|
||||||
fn fold_const(&self, e: &Expr) -> Option<ConstVal> {
|
fn fold_const(&self, e: &Expr) -> Option<ConstVal> {
|
||||||
match e {
|
match e {
|
||||||
Expr::IntLit(v) => Some(ConstVal::Num(*v as f64)),
|
Expr::IntLit(v, _) => Some(ConstVal::Num(*v as f64)),
|
||||||
Expr::LongLit(v) => Some(ConstVal::Num(*v as f64)),
|
Expr::LongLit(v, _) => Some(ConstVal::Num(*v as f64)),
|
||||||
Expr::SingleLit(v) => Some(ConstVal::Num(*v as f64)),
|
Expr::SingleLit(v, _) => Some(ConstVal::Num(*v as f64)),
|
||||||
Expr::DoubleLit(v) => Some(ConstVal::Num(*v)),
|
Expr::DoubleLit(v, _) => Some(ConstVal::Num(*v)),
|
||||||
Expr::CurrencyLit(v) => Some(ConstVal::Num(*v as f64 / 10_000.0)),
|
Expr::CurrencyLit(v, _) => Some(ConstVal::Num(*v as f64 / 10_000.0)),
|
||||||
Expr::StrLit(s) => Some(ConstVal::Str(s.clone())),
|
Expr::StrLit(s, _) => Some(ConstVal::Str(s.clone())),
|
||||||
Expr::Paren(e) => self.fold_const(e),
|
Expr::Paren(e) => self.fold_const(e),
|
||||||
Expr::Name {
|
Expr::Name {
|
||||||
name, args: None, ..
|
name, args: None, ..
|
||||||
@@ -1400,15 +1518,18 @@ impl Sema {
|
|||||||
// ---- Anweisungen -------------------------------------------------------
|
// ---- Anweisungen -------------------------------------------------------
|
||||||
|
|
||||||
fn lower_body(&mut self, stmts: &[Stmt], scope: &mut Scope) -> Vec<HStmt> {
|
fn lower_body(&mut self, stmts: &[Stmt], scope: &mut Scope) -> Vec<HStmt> {
|
||||||
|
let enclosing = (scope.current_line, scope.current_pos);
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
for stmt in stmts {
|
for stmt in stmts {
|
||||||
self.lower_stmt(stmt, scope, &mut out);
|
self.lower_stmt(stmt, scope, &mut out);
|
||||||
}
|
}
|
||||||
|
(scope.current_line, scope.current_pos) = enclosing;
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push(&mut self, out: &mut Vec<HStmt>, scope: &Scope, kind: HStmtKind) {
|
fn push(&mut self, out: &mut Vec<HStmt>, scope: &Scope, kind: HStmtKind) {
|
||||||
out.push(HStmt {
|
out.push(HStmt {
|
||||||
|
pos: scope.current_pos,
|
||||||
line: scope.current_line,
|
line: scope.current_line,
|
||||||
kind,
|
kind,
|
||||||
});
|
});
|
||||||
@@ -1419,6 +1540,7 @@ impl Sema {
|
|||||||
let pos = stmt_pos(stmt);
|
let pos = stmt_pos(stmt);
|
||||||
if pos.line > 0 {
|
if pos.line > 0 {
|
||||||
scope.current_line = pos.line;
|
scope.current_line = pos.line;
|
||||||
|
scope.current_pos = pos;
|
||||||
}
|
}
|
||||||
match stmt {
|
match stmt {
|
||||||
Stmt::Data { .. }
|
Stmt::Data { .. }
|
||||||
@@ -1430,7 +1552,7 @@ impl Sema {
|
|||||||
self.push(out, scope, HStmtKind::Label(id));
|
self.push(out, scope, HStmtKind::Label(id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Stmt::LineNumber(n) => {
|
Stmt::LineNumber(n, _) => {
|
||||||
if let Some(id) = scope.line_labels.get(n).copied() {
|
if let Some(id) = scope.line_labels.get(n).copied() {
|
||||||
self.push(out, scope, HStmtKind::Label(id));
|
self.push(out, scope, HStmtKind::Label(id));
|
||||||
}
|
}
|
||||||
@@ -1637,7 +1759,7 @@ impl Sema {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if self.forms.find(name).is_some() {
|
if self.find_object(name).is_some() {
|
||||||
self.err(*pos, format!("Object '{name}' has no default property"));
|
self.err(*pos, format!("Object '{name}' has no default property"));
|
||||||
self.lower_expr(value, scope);
|
self.lower_expr(value, scope);
|
||||||
return;
|
return;
|
||||||
@@ -1895,11 +2017,12 @@ impl Sema {
|
|||||||
Some(b) => self.lower_body(b, scope),
|
Some(b) => self.lower_body(b, scope),
|
||||||
None => Vec::new(),
|
None => Vec::new(),
|
||||||
};
|
};
|
||||||
for (ec, eb) in elseifs.iter().rev() {
|
for (ec, eb, elseif_pos) in elseifs.iter().rev() {
|
||||||
let c2 = self.lower_cond(ec, scope);
|
let c2 = self.lower_cond(ec, scope);
|
||||||
let body2 = self.lower_body(eb, scope);
|
let body2 = self.lower_body(eb, scope);
|
||||||
let stmt = HStmt {
|
let stmt = HStmt {
|
||||||
line: scope.current_line,
|
pos: *elseif_pos,
|
||||||
|
line: elseif_pos.line,
|
||||||
kind: HStmtKind::If {
|
kind: HStmtKind::If {
|
||||||
cond: c2,
|
cond: c2,
|
||||||
then: body2,
|
then: body2,
|
||||||
@@ -1920,6 +2043,7 @@ impl Sema {
|
|||||||
step,
|
step,
|
||||||
body,
|
body,
|
||||||
pos,
|
pos,
|
||||||
|
end_pos,
|
||||||
} => {
|
} => {
|
||||||
let (place, vt) = self.lower_place(var, scope);
|
let (place, vt) = self.lower_place(var, scope);
|
||||||
if !is_num(&vt) {
|
if !is_num(&vt) {
|
||||||
@@ -1956,6 +2080,7 @@ impl Sema {
|
|||||||
ty,
|
ty,
|
||||||
from: from_e,
|
from: from_e,
|
||||||
to: to_e,
|
to: to_e,
|
||||||
|
end_pos: *end_pos,
|
||||||
step: step_e,
|
step: step_e,
|
||||||
limit_slot,
|
limit_slot,
|
||||||
step_slot,
|
step_slot,
|
||||||
@@ -1966,7 +2091,11 @@ impl Sema {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Stmt::DoLoop {
|
Stmt::DoLoop {
|
||||||
pre, post, body, ..
|
pre,
|
||||||
|
post,
|
||||||
|
body,
|
||||||
|
end_pos,
|
||||||
|
..
|
||||||
} => {
|
} => {
|
||||||
let pre_c = pre.as_ref().map(|(u, c)| (*u, self.lower_cond(c, scope)));
|
let pre_c = pre.as_ref().map(|(u, c)| (*u, self.lower_cond(c, scope)));
|
||||||
let exit_label = scope.new_label();
|
let exit_label = scope.new_label();
|
||||||
@@ -1978,6 +2107,7 @@ impl Sema {
|
|||||||
out,
|
out,
|
||||||
scope,
|
scope,
|
||||||
HStmtKind::Loop {
|
HStmtKind::Loop {
|
||||||
|
end_pos: *end_pos,
|
||||||
pre: pre_c,
|
pre: pre_c,
|
||||||
post: post_c,
|
post: post_c,
|
||||||
body: hbody,
|
body: hbody,
|
||||||
@@ -1985,7 +2115,12 @@ impl Sema {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Stmt::While { cond, body, .. } => {
|
Stmt::While {
|
||||||
|
cond,
|
||||||
|
body,
|
||||||
|
end_pos,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
let c = self.lower_cond(cond, scope);
|
let c = self.lower_cond(cond, scope);
|
||||||
let exit_label = scope.new_label();
|
let exit_label = scope.new_label();
|
||||||
let hbody = self.lower_body(body, scope);
|
let hbody = self.lower_body(body, scope);
|
||||||
@@ -1993,6 +2128,7 @@ impl Sema {
|
|||||||
out,
|
out,
|
||||||
scope,
|
scope,
|
||||||
HStmtKind::Loop {
|
HStmtKind::Loop {
|
||||||
|
end_pos: *end_pos,
|
||||||
pre: Some((false, c)),
|
pre: Some((false, c)),
|
||||||
post: None,
|
post: None,
|
||||||
body: hbody,
|
body: hbody,
|
||||||
@@ -2120,8 +2256,11 @@ impl Sema {
|
|||||||
let ty = self.decl_ty(d);
|
let ty = self.decl_ty(d);
|
||||||
let key = self.var_key(&d.name, &d.suffix, d.as_type.is_some());
|
let key = self.var_key(&d.name, &d.suffix, d.as_type.is_some());
|
||||||
if let Entry::Vacant(entry) = scope.vars.entry(key) {
|
if let Entry::Vacant(entry) = scope.vars.entry(key) {
|
||||||
let slot =
|
let slot = self.alloc_global(
|
||||||
self.alloc_global(&format!("STATIC.{}", d.name), &ty, d.dims.is_some());
|
&format!("STATIC.{}", entry.key()),
|
||||||
|
&ty,
|
||||||
|
d.dims.is_some(),
|
||||||
|
);
|
||||||
entry.insert(VarInfo {
|
entry.insert(VarInfo {
|
||||||
ty,
|
ty,
|
||||||
array: d.dims.is_some(),
|
array: d.dims.is_some(),
|
||||||
@@ -2133,7 +2272,12 @@ impl Sema {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Stmt::CommonDecl { shared, decls, .. } => {
|
Stmt::CommonDecl {
|
||||||
|
shared,
|
||||||
|
decls,
|
||||||
|
block,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
for d in decls {
|
for d in decls {
|
||||||
if *shared && scope.is_module {
|
if *shared && scope.is_module {
|
||||||
self.shared_vars.insert(self.var_key(
|
self.shared_vars.insert(self.var_key(
|
||||||
@@ -2143,6 +2287,30 @@ impl Sema {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
self.declare(d, false, scope, out);
|
self.declare(d, false, scope, out);
|
||||||
|
if scope.is_module {
|
||||||
|
let key = self.var_key(&d.name, &d.suffix, d.as_type.is_some());
|
||||||
|
let slot = self.module_vars[&key].slot;
|
||||||
|
let dims = d.dims.as_ref().map(|dims| {
|
||||||
|
dims.iter()
|
||||||
|
.map(|(lo, hi)| {
|
||||||
|
let lower =
|
||||||
|
lo.as_ref().map_or(Some(self.option_base as i32), |e| {
|
||||||
|
self.common_bound(e)
|
||||||
|
});
|
||||||
|
let upper = self.common_bound(hi);
|
||||||
|
(lower, upper)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
});
|
||||||
|
self.commons.push(hir::HCommon {
|
||||||
|
slot,
|
||||||
|
block: block.clone(),
|
||||||
|
key,
|
||||||
|
ty: self.globals[slot as usize].ty.clone(),
|
||||||
|
dims,
|
||||||
|
pos: d.pos,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Stmt::SharedDecl { decls, .. } => {
|
Stmt::SharedDecl { decls, .. } => {
|
||||||
@@ -2153,7 +2321,7 @@ impl Sema {
|
|||||||
Some(v) => v.clone(),
|
Some(v) => v.clone(),
|
||||||
None => {
|
None => {
|
||||||
let ty = self.decl_ty(d);
|
let ty = self.decl_ty(d);
|
||||||
let slot = self.alloc_global(&d.name, &ty, d.dims.is_some());
|
let slot = self.alloc_global(&key, &ty, d.dims.is_some());
|
||||||
let v = VarInfo {
|
let v = VarInfo {
|
||||||
ty,
|
ty,
|
||||||
array: d.dims.is_some(),
|
array: d.dims.is_some(),
|
||||||
@@ -2255,7 +2423,7 @@ impl Sema {
|
|||||||
body,
|
body,
|
||||||
pos,
|
pos,
|
||||||
} => {
|
} => {
|
||||||
self.lower_def_fn(name, suffix, params, None, Some(body), scope, *pos);
|
self.lower_def_fn(name, suffix, params, None, Some(body), *pos);
|
||||||
}
|
}
|
||||||
Stmt::DefFnBlock {
|
Stmt::DefFnBlock {
|
||||||
name,
|
name,
|
||||||
@@ -2264,7 +2432,7 @@ impl Sema {
|
|||||||
body,
|
body,
|
||||||
pos,
|
pos,
|
||||||
} => {
|
} => {
|
||||||
self.lower_def_fn(name, suffix, params, Some(body), None, scope, *pos);
|
self.lower_def_fn(name, suffix, params, Some(body), None, *pos);
|
||||||
}
|
}
|
||||||
// ---- Datei-E/A (Grammatik Phase 1, Laufzeit Phase 3) ----
|
// ---- Datei-E/A (Grammatik Phase 1, Laufzeit Phase 3) ----
|
||||||
Stmt::Open {
|
Stmt::Open {
|
||||||
@@ -2318,7 +2486,7 @@ impl Sema {
|
|||||||
ne,
|
ne,
|
||||||
HExpr::Str(table.clone()),
|
HExpr::Str(table.clone()),
|
||||||
HExpr::Str(spalten),
|
HExpr::Str(spalten),
|
||||||
HExpr::Lng(udt as i32),
|
HExpr::UdtId(udt),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -2848,7 +3016,7 @@ impl Sema {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Arme in Bedingungs-/Rumpf-Paare übersetzen; CASE ELSE gesondert.
|
// Arme in Bedingungs-/Rumpf-Paare übersetzen; CASE ELSE gesondert.
|
||||||
let mut cases: Vec<(HExpr, Vec<HStmt>)> = Vec::new();
|
let mut cases: Vec<(HExpr, Vec<HStmt>, SourcePos)> = Vec::new();
|
||||||
let mut else_body: Vec<HStmt> = Vec::new();
|
let mut else_body: Vec<HStmt> = Vec::new();
|
||||||
for arm in arms {
|
for arm in arms {
|
||||||
let body = self.lower_body(&arm.body, scope);
|
let body = self.lower_body(&arm.body, scope);
|
||||||
@@ -2869,13 +3037,14 @@ impl Sema {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
cases.push((cond.unwrap(), body));
|
cases.push((cond.unwrap(), body, arm.pos));
|
||||||
}
|
}
|
||||||
// Von hinten zu verschachteltem If zusammensetzen.
|
// Von hinten zu verschachteltem If zusammensetzen.
|
||||||
let mut els = else_body;
|
let mut els = else_body;
|
||||||
for (cond, body) in cases.into_iter().rev() {
|
for (cond, body, pos) in cases.into_iter().rev() {
|
||||||
let stmt = HStmt {
|
let stmt = HStmt {
|
||||||
line: scope.current_line,
|
pos,
|
||||||
|
line: pos.line,
|
||||||
kind: HStmtKind::If {
|
kind: HStmtKind::If {
|
||||||
cond,
|
cond,
|
||||||
then: body,
|
then: body,
|
||||||
@@ -2947,7 +3116,6 @@ impl Sema {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn lower_def_fn(
|
fn lower_def_fn(
|
||||||
&mut self,
|
&mut self,
|
||||||
name: &str,
|
name: &str,
|
||||||
@@ -2955,7 +3123,6 @@ impl Sema {
|
|||||||
params: &[Param],
|
params: &[Param],
|
||||||
block: Option<&[Stmt]>,
|
block: Option<&[Stmt]>,
|
||||||
single: Option<&Expr>,
|
single: Option<&Expr>,
|
||||||
scope: &mut Scope,
|
|
||||||
pos: SourcePos,
|
pos: SourcePos,
|
||||||
) {
|
) {
|
||||||
// Registrierung (mit Prozedur-Id).
|
// Registrierung (mit Prozedur-Id).
|
||||||
@@ -2981,12 +3148,12 @@ impl Sema {
|
|||||||
def_fn: true,
|
def_fn: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
let _ = pos;
|
|
||||||
|
|
||||||
// Rumpf-Scope: Parameter (BYVAL) und Rückgabevariable lokal,
|
// Rumpf-Scope: Parameter (BYVAL) und Rückgabevariable lokal,
|
||||||
// freie Namen binden an Modulvariablen.
|
// freie Namen binden an Modulvariablen.
|
||||||
let mut fscope = Scope {
|
let mut fscope = Scope {
|
||||||
is_def_fn: true,
|
is_def_fn: true,
|
||||||
|
current_line: pos.line,
|
||||||
|
current_pos: pos,
|
||||||
..Scope::default()
|
..Scope::default()
|
||||||
};
|
};
|
||||||
let mut hparams = Vec::new();
|
let mut hparams = Vec::new();
|
||||||
@@ -2995,7 +3162,7 @@ impl Sema {
|
|||||||
let key = self.var_key(&p.name, &p.suffix, false);
|
let key = self.var_key(&p.name, &p.suffix, false);
|
||||||
let slot = fscope.locals.len() as u16;
|
let slot = fscope.locals.len() as u16;
|
||||||
fscope.locals.push(hir::HVar {
|
fscope.locals.push(hir::HVar {
|
||||||
name: p.name.clone(),
|
name: key.trim_end_matches("\u{1}AS").to_string(),
|
||||||
ty: self.h_ty(&ty),
|
ty: self.h_ty(&ty),
|
||||||
array: false,
|
array: false,
|
||||||
});
|
});
|
||||||
@@ -3020,7 +3187,7 @@ impl Sema {
|
|||||||
let ret_key = self.var_key(name, suffix, false);
|
let ret_key = self.var_key(name, suffix, false);
|
||||||
let ret_slot_idx = fscope.locals.len() as u16;
|
let ret_slot_idx = fscope.locals.len() as u16;
|
||||||
fscope.locals.push(hir::HVar {
|
fscope.locals.push(hir::HVar {
|
||||||
name: name.to_string(),
|
name: name.trim_end_matches("\u{1}AS").to_string(),
|
||||||
ty: self.h_ty(&ret),
|
ty: self.h_ty(&ret),
|
||||||
array: false,
|
array: false,
|
||||||
});
|
});
|
||||||
@@ -3045,6 +3212,7 @@ impl Sema {
|
|||||||
let (e, et) = self.lower_expr(expr, &mut fscope);
|
let (e, et) = self.lower_expr(expr, &mut fscope);
|
||||||
let value = self.coerce(e, &et, &ret, expr.pos());
|
let value = self.coerce(e, &et, &ret, expr.pos());
|
||||||
vec![HStmt {
|
vec![HStmt {
|
||||||
|
pos: fscope.current_pos,
|
||||||
line: fscope.current_line,
|
line: fscope.current_line,
|
||||||
kind: HStmtKind::Assign {
|
kind: HStmtKind::Assign {
|
||||||
place: HPlace {
|
place: HPlace {
|
||||||
@@ -3072,14 +3240,24 @@ impl Sema {
|
|||||||
body,
|
body,
|
||||||
label_count: fscope.next_label,
|
label_count: fscope.next_label,
|
||||||
};
|
};
|
||||||
// Modul-Scope-Zeile weiterführen.
|
|
||||||
scope.current_line = scope.current_line.max(fscope.current_line);
|
|
||||||
while self.hir_procs.len() <= id as usize {
|
while self.hir_procs.len() <= id as usize {
|
||||||
self.hir_procs.push(None);
|
self.hir_procs.push(None);
|
||||||
}
|
}
|
||||||
self.hir_procs[id as usize] = Some(hproc);
|
self.hir_procs[id as usize] = Some(hproc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn common_bound(&self, expr: &Expr) -> Option<i32> {
|
||||||
|
match self.fold_const(expr)? {
|
||||||
|
ConstVal::Num(n)
|
||||||
|
if n.is_finite()
|
||||||
|
&& (i32::MIN as f64..=i32::MAX as f64).contains(&n.round_ties_even()) =>
|
||||||
|
{
|
||||||
|
Some(n.round_ties_even() as i32)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn declare(&mut self, d: &VarDecl, redim: bool, scope: &mut Scope, out: &mut Vec<HStmt>) {
|
fn declare(&mut self, d: &VarDecl, redim: bool, scope: &mut Scope, out: &mut Vec<HStmt>) {
|
||||||
let ty = self.decl_ty(d);
|
let ty = self.decl_ty(d);
|
||||||
let mut hdims = Vec::new();
|
let mut hdims = Vec::new();
|
||||||
@@ -3120,7 +3298,7 @@ impl Sema {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let (slot, global) = self.alloc_var(scope, &d.name, &ty, is_array);
|
let (slot, global) = self.alloc_var(scope, &key, &ty, is_array);
|
||||||
self.insert_var(
|
self.insert_var(
|
||||||
scope,
|
scope,
|
||||||
key,
|
key,
|
||||||
@@ -3249,13 +3427,7 @@ impl Sema {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if matches!(name, "SHOW" | "HIDE") && args.is_empty() {
|
if matches!(name, "SHOW" | "HIDE") && args.is_empty() {
|
||||||
if let Some((object, _)) = self
|
if let Some((object, _)) = self.current_form() {
|
||||||
.forms
|
|
||||||
.objects
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.find(|(_, object)| object.class == ObjectClass::Form)
|
|
||||||
{
|
|
||||||
let method = forms::methods(ObjectClass::Form)
|
let method = forms::methods(ObjectClass::Form)
|
||||||
.iter()
|
.iter()
|
||||||
.position(|method| *method == name)
|
.position(|method| *method == name)
|
||||||
@@ -3264,7 +3436,7 @@ impl Sema {
|
|||||||
out,
|
out,
|
||||||
scope,
|
scope,
|
||||||
HStmtKind::ObjectMethod {
|
HStmtKind::ObjectMethod {
|
||||||
object: object as u16,
|
object,
|
||||||
index: None,
|
index: None,
|
||||||
method,
|
method,
|
||||||
args: Vec::new(),
|
args: Vec::new(),
|
||||||
@@ -3343,20 +3515,15 @@ impl Sema {
|
|||||||
pos: object_pos,
|
pos: object_pos,
|
||||||
} = &args[0]
|
} = &args[0]
|
||||||
{
|
{
|
||||||
if let Some((object, info)) =
|
let (parent, object_name) = object_name
|
||||||
self.forms.find(object_name).map(|(i, o)| (i, o.clone()))
|
.split_once('!')
|
||||||
{
|
.map_or((None, object_name.as_str()), |(parent, name)| {
|
||||||
if index.is_some() && !info.array {
|
(Some(parent), name)
|
||||||
self.err(
|
});
|
||||||
*object_pos,
|
if let Some((object, _)) = self.scoped_object(object_name, parent, *object_pos) {
|
||||||
format!("Object '{object_name}' is not an array"),
|
let Ok(index) = self.object_index(object, index, scope, *object_pos) else {
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
};
|
||||||
let index = index
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|a| a.first())
|
|
||||||
.map(|e| self.lower_num_as(e, scope, NumTy::Lng));
|
|
||||||
self.push(
|
self.push(
|
||||||
out,
|
out,
|
||||||
scope,
|
scope,
|
||||||
@@ -3368,7 +3535,6 @@ impl Sema {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.err(*object_pos, format!("Unknown object '{object_name}'"));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3514,7 +3680,7 @@ impl Sema {
|
|||||||
if self.consts.contains_key(name) {
|
if self.consts.contains_key(name) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if self.forms.find(name).is_some()
|
if (suffix.is_none() && self.find_object(name).is_some())
|
||||||
|| (name.contains('!') && !self.is_udt_path(scope, name))
|
|| (name.contains('!') && !self.is_udt_path(scope, name))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -3978,7 +4144,7 @@ impl Sema {
|
|||||||
/// Greift nur bei Stringliteralen; ein zur Laufzeit gebildeter Gerätename
|
/// Greift nur bei Stringliteralen; ein zur Laufzeit gebildeter Gerätename
|
||||||
/// bleibt der Datei-E/A überlassen.
|
/// bleibt der Datei-E/A überlassen.
|
||||||
fn reject_com_device(&mut self, file: &Expr, pos: SourcePos) {
|
fn reject_com_device(&mut self, file: &Expr, pos: SourcePos) {
|
||||||
if let Expr::StrLit(s) = file {
|
if let Expr::StrLit(s, _) = file {
|
||||||
if ist_com_geraet(s) {
|
if ist_com_geraet(s) {
|
||||||
self.err(pos, "Feature unavailable");
|
self.err(pos, "Feature unavailable");
|
||||||
}
|
}
|
||||||
@@ -4108,7 +4274,7 @@ impl Sema {
|
|||||||
return (
|
return (
|
||||||
Some(HPlace {
|
Some(HPlace {
|
||||||
base: base_slot,
|
base: base_slot,
|
||||||
base_is_ref: false,
|
base_is_ref: info.by_ref,
|
||||||
indices: indices.unwrap_or_default(),
|
indices: indices.unwrap_or_default(),
|
||||||
fields: fpath,
|
fields: fpath,
|
||||||
ty: hty,
|
ty: hty,
|
||||||
@@ -4157,12 +4323,12 @@ impl Sema {
|
|||||||
|
|
||||||
fn lower_expr(&mut self, e: &Expr, scope: &mut Scope) -> (HExpr, Ty) {
|
fn lower_expr(&mut self, e: &Expr, scope: &mut Scope) -> (HExpr, Ty) {
|
||||||
match e {
|
match e {
|
||||||
Expr::IntLit(v) => (HExpr::Int(*v), Ty::Int),
|
Expr::IntLit(v, _) => (HExpr::Int(*v), Ty::Int),
|
||||||
Expr::LongLit(v) => (HExpr::Lng(*v), Ty::Lng),
|
Expr::LongLit(v, _) => (HExpr::Lng(*v), Ty::Lng),
|
||||||
Expr::SingleLit(v) => (HExpr::Sng(*v), Ty::Sng),
|
Expr::SingleLit(v, _) => (HExpr::Sng(*v), Ty::Sng),
|
||||||
Expr::DoubleLit(v) => (HExpr::Dbl(*v), Ty::Dbl),
|
Expr::DoubleLit(v, _) => (HExpr::Dbl(*v), Ty::Dbl),
|
||||||
Expr::CurrencyLit(v) => (HExpr::Cur(*v), Ty::Cur),
|
Expr::CurrencyLit(v, _) => (HExpr::Cur(*v), Ty::Cur),
|
||||||
Expr::StrLit(s) => (HExpr::Str(s.clone()), Ty::Str),
|
Expr::StrLit(s, _) => (HExpr::Str(s.clone()), Ty::Str),
|
||||||
Expr::Paren(inner) => self.lower_expr(inner, scope),
|
Expr::Paren(inner) => self.lower_expr(inner, scope),
|
||||||
Expr::Missing => (HExpr::Int(0), Ty::Unknown),
|
Expr::Missing => (HExpr::Int(0), Ty::Unknown),
|
||||||
Expr::TypeOf { value, class, pos } => {
|
Expr::TypeOf { value, class, pos } => {
|
||||||
@@ -4659,7 +4825,11 @@ impl Sema {
|
|||||||
);
|
);
|
||||||
return (HExpr::Int(0), Ty::Unknown);
|
return (HExpr::Int(0), Ty::Unknown);
|
||||||
}
|
}
|
||||||
if let Some((object, info)) = self.forms.find(name).map(|(id, info)| (id, info.clone())) {
|
if let Some((object, info)) = self
|
||||||
|
.find_object(name)
|
||||||
|
.filter(|_| suffix.is_none())
|
||||||
|
.map(|(id, info)| (id, info.clone()))
|
||||||
|
{
|
||||||
if args.is_none() || info.array {
|
if args.is_none() || info.array {
|
||||||
let Ok(index) = self.object_index(object, args, scope, pos) else {
|
let Ok(index) = self.object_index(object, args, scope, pos) else {
|
||||||
return (HExpr::Int(0), Ty::Unknown);
|
return (HExpr::Int(0), Ty::Unknown);
|
||||||
@@ -5296,7 +5466,7 @@ impl Sema {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Position einer Anweisung (für Zeileninfo der Anweisungsgrenzen).
|
/// Position einer Anweisung (für Zeileninfo der Anweisungsgrenzen).
|
||||||
fn stmt_pos(stmt: &Stmt) -> SourcePos {
|
pub fn stmt_pos(stmt: &Stmt) -> SourcePos {
|
||||||
match stmt {
|
match stmt {
|
||||||
Stmt::Assign { pos, .. }
|
Stmt::Assign { pos, .. }
|
||||||
| Stmt::Print { pos, .. }
|
| Stmt::Print { pos, .. }
|
||||||
@@ -5352,7 +5522,8 @@ fn stmt_pos(stmt: &Stmt) -> SourcePos {
|
|||||||
| Stmt::End(pos)
|
| Stmt::End(pos)
|
||||||
| Stmt::StopStmt(pos)
|
| Stmt::StopStmt(pos)
|
||||||
| Stmt::System(pos) => *pos,
|
| Stmt::System(pos) => *pos,
|
||||||
Stmt::Label(_) | Stmt::LineNumber(_) => SourcePos::default(),
|
Stmt::LineNumber(_, pos) => *pos,
|
||||||
|
Stmt::Label(_) => SourcePos::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5377,10 +5548,10 @@ fn trap_art(device: &str) -> Option<u8> {
|
|||||||
/// Konstanter Zahlenwert eines Ausdrucks, soweit direkt ablesbar.
|
/// Konstanter Zahlenwert eines Ausdrucks, soweit direkt ablesbar.
|
||||||
fn const_zahl(e: &Expr) -> Option<i64> {
|
fn const_zahl(e: &Expr) -> Option<i64> {
|
||||||
match e {
|
match e {
|
||||||
Expr::IntLit(n) => Some(*n as i64),
|
Expr::IntLit(n, _) => Some(*n as i64),
|
||||||
Expr::LongLit(n) => Some(*n as i64),
|
Expr::LongLit(n, _) => Some(*n as i64),
|
||||||
Expr::SingleLit(n) => Some(*n as i64),
|
Expr::SingleLit(n, _) => Some(*n as i64),
|
||||||
Expr::DoubleLit(n) => Some(*n as i64),
|
Expr::DoubleLit(n, _) => Some(*n as i64),
|
||||||
Expr::Paren(inner) => const_zahl(inner),
|
Expr::Paren(inner) => const_zahl(inner),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
|
|||||||
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)));
|
return Ok(PropertyValue::Integer(value.saturating_sub(2)));
|
||||||
}
|
}
|
||||||
if spec.name == "PARENT" {
|
if spec.name == "PARENT" {
|
||||||
let parent = obj.description.parent_form.as_deref().and_then(|name| {
|
let parent = obj.description.parent.map(|id| (id, None));
|
||||||
self.objects
|
|
||||||
.iter()
|
|
||||||
.position(|candidate| candidate.description.name.eq_ignore_ascii_case(name))
|
|
||||||
.map(|id| (id as u16, None))
|
|
||||||
});
|
|
||||||
return Ok(PropertyValue::Object(parent));
|
return Ok(PropertyValue::Object(parent));
|
||||||
}
|
}
|
||||||
Ok(obj.properties[property as usize].clone())
|
Ok(obj.properties[property as usize].clone())
|
||||||
@@ -834,12 +829,7 @@ impl FormsModel {
|
|||||||
if obj.description.class == ObjectClass::Form {
|
if obj.description.class == ObjectClass::Form {
|
||||||
return Some(current);
|
return Some(current);
|
||||||
}
|
}
|
||||||
let parent = obj.description.parent_form.as_deref()?;
|
current = obj.description.parent?;
|
||||||
current = self
|
|
||||||
.objects
|
|
||||||
.iter()
|
|
||||||
.position(|candidate| candidate.description.name.eq_ignore_ascii_case(parent))?
|
|
||||||
as u16;
|
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@@ -959,7 +949,7 @@ impl FormsModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn select_option(&mut self, key: ObjectKey) -> Result<(), RuntimeError> {
|
fn select_option(&mut self, key: ObjectKey) -> Result<(), RuntimeError> {
|
||||||
let parent = self.instance(key)?.description.parent_form.clone();
|
let parent = self.instance(key)?.description.parent;
|
||||||
let value_id = forms::property(ObjectClass::OptionButton, "VALUE")
|
let value_id = forms::property(ObjectClass::OptionButton, "VALUE")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.0 as usize;
|
.0 as usize;
|
||||||
@@ -969,7 +959,7 @@ impl FormsModel {
|
|||||||
}
|
}
|
||||||
let same_group = self.instance(other).is_ok_and(|obj| {
|
let same_group = self.instance(other).is_ok_and(|obj| {
|
||||||
obj.description.class == ObjectClass::OptionButton
|
obj.description.class == ObjectClass::OptionButton
|
||||||
&& obj.description.parent_form == parent
|
&& obj.description.parent == parent
|
||||||
});
|
});
|
||||||
if same_group {
|
if same_group {
|
||||||
self.instance_mut(other)?.properties[value_id] = PropertyValue::Integer(0);
|
self.instance_mut(other)?.properties[value_id] = PropertyValue::Integer(0);
|
||||||
@@ -1006,12 +996,8 @@ impl FormsModel {
|
|||||||
&& self
|
&& self
|
||||||
.instance(key)
|
.instance(key)
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|obj| obj.description.parent_form.as_deref())
|
.and_then(|obj| obj.description.parent)
|
||||||
.and_then(|parent| {
|
.and_then(|parent| self.objects.get(parent as usize))
|
||||||
self.objects
|
|
||||||
.iter()
|
|
||||||
.find(|obj| obj.description.name.eq_ignore_ascii_case(parent))
|
|
||||||
})
|
|
||||||
.is_some_and(|parent| parent.description.class == ObjectClass::Form);
|
.is_some_and(|parent| parent.description.class == ObjectClass::Form);
|
||||||
if invalid || title_shortcut {
|
if invalid || title_shortcut {
|
||||||
Err(RuntimeError::ILLEGAL_FUNCTION_CALL)
|
Err(RuntimeError::ILLEGAL_FUNCTION_CALL)
|
||||||
@@ -1325,19 +1311,12 @@ impl FormsModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn menu_children(&self, parent: ObjectKey) -> Vec<ObjectKey> {
|
fn menu_children(&self, parent: ObjectKey) -> Vec<ObjectKey> {
|
||||||
let Ok(parent) = self.instance(parent) else {
|
|
||||||
return Vec::new();
|
|
||||||
};
|
|
||||||
self.keys()
|
self.keys()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|key| {
|
.filter(|key| {
|
||||||
self.instance(*key).is_ok_and(|object| {
|
self.instance(*key).is_ok_and(|object| {
|
||||||
object.description.class == ObjectClass::Menu
|
object.description.class == ObjectClass::Menu
|
||||||
&& object
|
&& object.description.parent == Some(parent.0)
|
||||||
.description
|
|
||||||
.parent_form
|
|
||||||
.as_deref()
|
|
||||||
.is_some_and(|name| name.eq_ignore_ascii_case(&parent.description.name))
|
|
||||||
}) && self.boolean(*key, "VISIBLE")
|
}) && self.boolean(*key, "VISIBLE")
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
@@ -1385,23 +1364,15 @@ impl FormsModel {
|
|||||||
}
|
}
|
||||||
if shift & umschalt::ALT != 0 {
|
if shift & umschalt::ALT != 0 {
|
||||||
if let Some(ch) = key.chars().next().map(|ch| ch.to_ascii_uppercase()) {
|
if let Some(ch) = key.chars().next().map(|ch| ch.to_ascii_uppercase()) {
|
||||||
if let Some(target) =
|
if let Some(target) = self.form_controls(form).into_iter().find(|candidate| {
|
||||||
self.form_controls(form).into_iter().find(|candidate| {
|
Self::access_key(&self.string(*candidate, "CAPTION")) == Some(ch)
|
||||||
Self::access_key(&self.string(*candidate, "CAPTION")) == Some(ch)
|
&& self.boolean(*candidate, "ENABLED")
|
||||||
&& self.boolean(*candidate, "ENABLED")
|
&& self.boolean(*candidate, "VISIBLE")
|
||||||
&& self.boolean(*candidate, "VISIBLE")
|
&& self.instance(*candidate).is_ok_and(|obj| {
|
||||||
&& self.instance(*candidate).is_ok_and(|obj| {
|
obj.description.class != ObjectClass::Menu
|
||||||
obj.description.class != ObjectClass::Menu
|
|| obj.description.parent == Some(form)
|
||||||
|| obj.description.parent_form.as_deref().is_some_and(
|
})
|
||||||
|parent| {
|
}) {
|
||||||
parent.eq_ignore_ascii_case(
|
|
||||||
&self.objects[form as usize].description.name,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
{
|
|
||||||
if self
|
if self
|
||||||
.instance(target)
|
.instance(target)
|
||||||
.is_ok_and(|obj| obj.description.class == ObjectClass::Menu)
|
.is_ok_and(|obj| obj.description.class == ObjectClass::Menu)
|
||||||
@@ -1580,21 +1551,14 @@ impl FormsModel {
|
|||||||
let mut top = self.integer(key, "TOP").unwrap_or(0).max(0) as usize + 1;
|
let mut top = self.integer(key, "TOP").unwrap_or(0).max(0) as usize + 1;
|
||||||
let width = self.integer(key, "WIDTH").unwrap_or(1).max(1) as usize;
|
let width = self.integer(key, "WIDTH").unwrap_or(1).max(1) as usize;
|
||||||
let height = self.integer(key, "HEIGHT").unwrap_or(1).max(1) as usize;
|
let height = self.integer(key, "HEIGHT").unwrap_or(1).max(1) as usize;
|
||||||
let mut parent = obj.description.parent_form.as_deref();
|
let mut parent = obj.description.parent;
|
||||||
for _ in 0..self.objects.len() {
|
for _ in 0..self.objects.len() {
|
||||||
let Some(name) = parent else { break };
|
let Some(id) = parent else { break };
|
||||||
let Some((id, parent_obj)) = self
|
let parent_obj = self.objects.get(id as usize)?;
|
||||||
.objects
|
let parent_key = (id, None);
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.find(|(_, candidate)| candidate.description.name.eq_ignore_ascii_case(name))
|
|
||||||
else {
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
let parent_key = (id as u16, None);
|
|
||||||
left += self.integer(parent_key, "LEFT").unwrap_or(0).max(0) as usize;
|
left += self.integer(parent_key, "LEFT").unwrap_or(0).max(0) as usize;
|
||||||
top += self.integer(parent_key, "TOP").unwrap_or(0).max(0) as usize;
|
top += self.integer(parent_key, "TOP").unwrap_or(0).max(0) as usize;
|
||||||
parent = parent_obj.description.parent_form.as_deref();
|
parent = parent_obj.description.parent;
|
||||||
}
|
}
|
||||||
Some((left, top, width, height))
|
Some((left, top, width, height))
|
||||||
}
|
}
|
||||||
@@ -2357,12 +2321,7 @@ impl FormsModel {
|
|||||||
&& self
|
&& self
|
||||||
.instance(key)
|
.instance(key)
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|obj| obj.description.parent_form.as_deref())
|
.is_some_and(|obj| obj.description.parent == Some(form))
|
||||||
.is_some_and(|parent| {
|
|
||||||
parent.eq_ignore_ascii_case(
|
|
||||||
&self.objects[form as usize].description.name,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
{
|
{
|
||||||
if self.boolean(key, "VISIBLE") {
|
if self.boolean(key, "VISIBLE") {
|
||||||
let caption = format!(" {} ", self.caption(key));
|
let caption = format!(" {} ", self.caption(key));
|
||||||
@@ -2419,20 +2378,12 @@ impl FormsModel {
|
|||||||
.is_some_and(|root| self.root_form(*root) == Some(form))
|
.is_some_and(|root| self.root_form(*root) == Some(form))
|
||||||
{
|
{
|
||||||
let mut popup_left = left + 1;
|
let mut popup_left = left + 1;
|
||||||
for root in
|
for root in self.form_controls(form).into_iter().filter(|key| {
|
||||||
self.form_controls(form).into_iter().filter(|key| {
|
self.instance(*key).is_ok_and(|object| {
|
||||||
self.instance(*key).is_ok_and(|object| {
|
object.description.class == ObjectClass::Menu
|
||||||
object.description.class == ObjectClass::Menu
|
&& object.description.parent == Some(form)
|
||||||
&& object.description.parent_form.as_deref().is_some_and(
|
}) && self.boolean(*key, "VISIBLE")
|
||||||
|parent| {
|
}) {
|
||||||
parent.eq_ignore_ascii_case(
|
|
||||||
&self.objects[form as usize].description.name,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}) && self.boolean(*key, "VISIBLE")
|
|
||||||
})
|
|
||||||
{
|
|
||||||
if Some(&root) == self.menu_path.first() {
|
if Some(&root) == self.menu_path.first() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,9 +56,7 @@ impl FormFile {
|
|||||||
} else {
|
} else {
|
||||||
let array = forms::property(node.class, "INDEX")
|
let array = forms::property(node.class, "INDEX")
|
||||||
.and_then(|(property, _)| node.properties.get(&property))
|
.and_then(|(property, _)| node.properties.get(&property))
|
||||||
.is_some_and(
|
.is_some();
|
||||||
|value| matches!(value, PropertyValue::Integer(index) if *index != 0),
|
|
||||||
);
|
|
||||||
catalog.add(&node.name, node.class, parent, array);
|
catalog.add(&node.name, node.class, parent, array);
|
||||||
}
|
}
|
||||||
for child in &node.children {
|
for child in &node.children {
|
||||||
@@ -70,44 +68,99 @@ impl FormFile {
|
|||||||
catalog
|
catalog
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn apply(&self, model: &mut FormsModel) -> Result<(), tb_runtime::errors::RuntimeError> {
|
pub fn code_line(&self) -> u32 {
|
||||||
fn apply_node(
|
self.original.as_ref().map_or(1, |original| {
|
||||||
|
original.bytes[..original.bytes.len() - original.code.len()]
|
||||||
|
.bytes()
|
||||||
|
.filter(|b| *b == b'\n')
|
||||||
|
.count() as u32
|
||||||
|
+ 1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initial_values(
|
||||||
|
&self,
|
||||||
|
catalog: &FormCatalog,
|
||||||
|
) -> Result<Vec<FormInitial>, tb_runtime::errors::RuntimeError> {
|
||||||
|
fn collect(
|
||||||
node: &FormNode,
|
node: &FormNode,
|
||||||
model: &mut FormsModel,
|
parent: Option<u16>,
|
||||||
menu_depth: usize,
|
catalog: &FormCatalog,
|
||||||
|
out: &mut Vec<FormInitial>,
|
||||||
|
depth: usize,
|
||||||
) -> Result<(), tb_runtime::errors::RuntimeError> {
|
) -> Result<(), tb_runtime::errors::RuntimeError> {
|
||||||
let menu_depth = if node.class == ObjectClass::Menu {
|
let depth = depth + usize::from(node.class == ObjectClass::Menu);
|
||||||
menu_depth + 1
|
if depth > 6 {
|
||||||
} else {
|
return Err(tb_runtime::errors::RuntimeError(5));
|
||||||
menu_depth
|
|
||||||
};
|
|
||||||
if menu_depth > 6 {
|
|
||||||
return Err(tb_runtime::errors::RuntimeError::ILLEGAL_FUNCTION_CALL);
|
|
||||||
}
|
}
|
||||||
let object = model
|
let object = catalog
|
||||||
.objects
|
.objects
|
||||||
.iter()
|
.iter()
|
||||||
.position(|candidate| candidate.description.name.eq_ignore_ascii_case(&node.name))
|
.position(|o| {
|
||||||
|
o.name.eq_ignore_ascii_case(&node.name)
|
||||||
|
&& o.parent == parent
|
||||||
|
&& o.class == node.class
|
||||||
|
})
|
||||||
.ok_or(tb_runtime::errors::RuntimeError(420))? as u16;
|
.ok_or(tb_runtime::errors::RuntimeError(420))? as u16;
|
||||||
let index = forms::property(node.class, "INDEX")
|
let index = forms::property(node.class, "INDEX")
|
||||||
.and_then(|(property, _)| node.properties.get(&property))
|
.and_then(|(id, _)| node.properties.get(&id))
|
||||||
.and_then(|value| match value {
|
.and_then(|v| {
|
||||||
PropertyValue::Integer(value) => Some(*value),
|
if let PropertyValue::Integer(n) = v {
|
||||||
_ => None,
|
Some(*n)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
if index != 0 && !model.is_loaded_at(object, Some(index)) {
|
if out.iter().any(|v| v.object == object && v.index == index) {
|
||||||
model.load_design_array(object, index)?;
|
return Err(tb_runtime::errors::RuntimeError(5));
|
||||||
}
|
|
||||||
for (property, value) in &node.properties {
|
|
||||||
model.set_initial_at(object, Some(index), *property, value.clone())?;
|
|
||||||
}
|
}
|
||||||
|
out.push(FormInitial {
|
||||||
|
object,
|
||||||
|
index,
|
||||||
|
properties: node.properties.clone(),
|
||||||
|
});
|
||||||
for child in &node.children {
|
for child in &node.children {
|
||||||
apply_node(child, model, menu_depth)?;
|
collect(child, Some(object), catalog, out, depth)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
apply_node(&self.root, model, 0)
|
let mut values = Vec::new();
|
||||||
|
collect(&self.root, None, catalog, &mut values, 0)?;
|
||||||
|
Ok(values)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply(&self, model: &mut FormsModel) -> Result<(), tb_runtime::errors::RuntimeError> {
|
||||||
|
let catalog = FormCatalog {
|
||||||
|
objects: model
|
||||||
|
.objects
|
||||||
|
.iter()
|
||||||
|
.map(|o| o.description.clone())
|
||||||
|
.collect(),
|
||||||
|
};
|
||||||
|
for initial in self.initial_values(&catalog)? {
|
||||||
|
initial.apply(model)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct FormInitial {
|
||||||
|
pub object: u16,
|
||||||
|
pub index: i32,
|
||||||
|
pub properties: BTreeMap<u16, PropertyValue>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FormInitial {
|
||||||
|
pub fn apply(&self, model: &mut FormsModel) -> Result<(), tb_runtime::errors::RuntimeError> {
|
||||||
|
if self.index != 0 && !model.is_loaded_at(self.object, Some(self.index)) {
|
||||||
|
model.load_design_array(self.object, self.index)?;
|
||||||
|
}
|
||||||
|
for (property, value) in &self.properties {
|
||||||
|
model.set_initial_at(self.object, Some(self.index), *property, value.clone())?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,12 +35,13 @@ fn generate_module(n_blocks: usize, seed: usize) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn compile_all(sources: &[(String, String)]) -> usize {
|
fn compile_all(sources: &[(String, String)]) -> usize {
|
||||||
let mut total = 0;
|
let units: Vec<_> = sources
|
||||||
for (name, src) in sources {
|
.iter()
|
||||||
let m = tb_vm::compile_source(name, src).expect("Benchmark-Quelle muss kompilieren");
|
.map(|(name, src)| tb_frontend::source::SourceUnit::new(name, &format!("{name}.bas"), src))
|
||||||
total += m.procs.iter().map(|p| p.code.len()).sum::<usize>();
|
.collect();
|
||||||
}
|
let project = tb_vm::compile_project("BENCH", &units, &Default::default(), &[])
|
||||||
total
|
.expect("Benchmark-Projekt muss kompilieren");
|
||||||
|
project.procs.iter().map(|p| p.code.len()).sum()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
|||||||
@@ -9,11 +9,14 @@
|
|||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use tb_frontend::forms::{FormObject, ObjectClass};
|
use tb_frontend::forms::{FormObject, ObjectClass};
|
||||||
use tb_frontend::hir::HEventProc;
|
use tb_frontend::hir::{HEventProc, HParam, HProcKind, HTy, NumTy};
|
||||||
|
use tb_frontend::source::SourceFile;
|
||||||
use tb_runtime::value::{TypeInit, UdtLayout};
|
use tb_runtime::value::{TypeInit, UdtLayout};
|
||||||
|
use tb_ui::forms::PropertyValue;
|
||||||
|
use tb_ui::frm::FormInitial;
|
||||||
|
|
||||||
pub const TBC_MAGIC: &[u8; 4] = b"TBC\0";
|
pub const TBC_MAGIC: &[u8; 4] = b"TBC\0";
|
||||||
pub const TBC_VERSION: u16 = 3;
|
pub const TBC_VERSION: u16 = 4;
|
||||||
|
|
||||||
/// Vergleichsoperator (Operand der `Cmp*`-Instruktionen).
|
/// Vergleichsoperator (Operand der `Cmp*`-Instruktionen).
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
@@ -75,8 +78,15 @@ impl<'a> Reader<'a> {
|
|||||||
pub fn new(buf: &'a [u8]) -> Self {
|
pub fn new(buf: &'a [u8]) -> Self {
|
||||||
Reader { buf, pos: 0 }
|
Reader { buf, pos: 0 }
|
||||||
}
|
}
|
||||||
|
fn finish(&self) -> Result<(), LoadError> {
|
||||||
|
if self.pos != self.buf.len() {
|
||||||
|
Err(LoadError::Corrupt("überzählige Abschnittsdaten"))
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
fn take(&mut self, n: usize) -> Result<&'a [u8], LoadError> {
|
fn take(&mut self, n: usize) -> Result<&'a [u8], LoadError> {
|
||||||
if self.pos + n > self.buf.len() {
|
if n > self.buf.len().saturating_sub(self.pos) {
|
||||||
return Err(LoadError::Corrupt("unerwartetes Dateiende"));
|
return Err(LoadError::Corrupt("unerwartetes Dateiende"));
|
||||||
}
|
}
|
||||||
let s = &self.buf[self.pos..self.pos + n];
|
let s = &self.buf[self.pos..self.pos + n];
|
||||||
@@ -138,7 +148,11 @@ impl Enc for bool {
|
|||||||
out.push(*self as u8);
|
out.push(*self as u8);
|
||||||
}
|
}
|
||||||
fn dec(r: &mut Reader) -> Result<Self, LoadError> {
|
fn dec(r: &mut Reader) -> Result<Self, LoadError> {
|
||||||
Ok(r.u8()? != 0)
|
match r.u8()? {
|
||||||
|
0 => Ok(false),
|
||||||
|
1 => Ok(true),
|
||||||
|
_ => Err(LoadError::Corrupt("bool")),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,6 +184,9 @@ impl Enc for TypeInit {
|
|||||||
fn dec(r: &mut Reader) -> Result<Self, LoadError> {
|
fn dec(r: &mut Reader) -> Result<Self, LoadError> {
|
||||||
let tag = r.u8()?;
|
let tag = r.u8()?;
|
||||||
let extra = r.u32()?;
|
let extra = r.u32()?;
|
||||||
|
if (tag != 6 && tag != 7 && extra != 0) || (tag == 7 && extra > u16::MAX as u32) {
|
||||||
|
return Err(LoadError::Corrupt("TypeInit-Zusatz"));
|
||||||
|
}
|
||||||
Ok(match tag {
|
Ok(match tag {
|
||||||
0 => TypeInit::Int,
|
0 => TypeInit::Int,
|
||||||
1 => TypeInit::Lng,
|
1 => TypeInit::Lng,
|
||||||
@@ -224,6 +241,8 @@ instrs! {
|
|||||||
0x03 StopInstr;
|
0x03 StopInstr;
|
||||||
0x04 SystemInstr;
|
0x04 SystemInstr;
|
||||||
0x05 Unsupported(a: u16); // Name im Stringpool → Fehler 73
|
0x05 Unsupported(a: u16); // Name im Stringpool → Fehler 73
|
||||||
|
0x06 Source(a: u32, b: u32); // Quelldatei-ID, physische Spalte der folgenden Stmt-Grenze
|
||||||
|
0x07 InitStmt(a: u32); // globale Initialisierung: Quellort/Debugger, noch keine Ereignisse
|
||||||
|
|
||||||
// 0x10 — Konstanten und Stack
|
// 0x10 — Konstanten und Stack
|
||||||
0x10 PushInt(a: i16);
|
0x10 PushInt(a: i16);
|
||||||
@@ -234,6 +253,7 @@ instrs! {
|
|||||||
0x15 PushStr(a: u16);
|
0x15 PushStr(a: u16);
|
||||||
0x16 Dup;
|
0x16 Dup;
|
||||||
0x17 Pop;
|
0x17 Pop;
|
||||||
|
0x18 PushUdtId(a: u16); // TYPE-Index als LONG auf dem Stack (ISAM)
|
||||||
|
|
||||||
// 0x20 — Variablen und Referenzen
|
// 0x20 — Variablen und Referenzen
|
||||||
0x20 LoadGlobal(a: u16);
|
0x20 LoadGlobal(a: u16);
|
||||||
@@ -260,6 +280,8 @@ instrs! {
|
|||||||
0x39 ArrBound(a: bool); // true = LBOUND
|
0x39 ArrBound(a: bool); // true = LBOUND
|
||||||
0x3A FixStr(a: u32); // auf feste Länge kürzen/padden
|
0x3A FixStr(a: u32); // auf feste Länge kürzen/padden
|
||||||
|
|
||||||
|
0x3B CommonArr(a: bool, b: u16, c: u8, d: TypeInit); // gemeinsame Initialisierung/Layoutprüfung
|
||||||
|
|
||||||
// 0x40 — Arithmetik (monomorph)
|
// 0x40 — Arithmetik (monomorph)
|
||||||
0x40 AddI2; 0x41 AddI4; 0x42 AddR4; 0x43 AddR8; 0x44 AddCy;
|
0x40 AddI2; 0x41 AddI4; 0x42 AddR4; 0x43 AddR8; 0x44 AddCy;
|
||||||
0x45 SubI2; 0x46 SubI4; 0x47 SubR4; 0x48 SubR8; 0x49 SubCy;
|
0x45 SubI2; 0x46 SubI4; 0x47 SubR4; 0x48 SubR8; 0x49 SubCy;
|
||||||
@@ -355,10 +377,122 @@ instrs! {
|
|||||||
0xD6 LsetRset(a: bool); // rset?; Stack: Referenz, Wert // argc, line_mode — Dateinummer liegt unter den Referenzen
|
0xD6 LsetRset(a: bool); // rset?; Stack: Referenz, Wert // argc, line_mode — Dateinummer liegt unter den Referenzen
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Enc for HTy {
|
||||||
|
fn enc(&self, out: &mut Vec<u8>) {
|
||||||
|
let tag = match self {
|
||||||
|
HTy::Num(NumTy::Int) => 0,
|
||||||
|
HTy::Num(NumTy::Lng) => 1,
|
||||||
|
HTy::Num(NumTy::Sng) => 2,
|
||||||
|
HTy::Num(NumTy::Dbl) => 3,
|
||||||
|
HTy::Num(NumTy::Cur) => 4,
|
||||||
|
HTy::Str => 5,
|
||||||
|
HTy::FixedStr(_) => 6,
|
||||||
|
HTy::Udt(_) => 7,
|
||||||
|
HTy::Form => 8,
|
||||||
|
HTy::Control => 9,
|
||||||
|
};
|
||||||
|
out.push(tag);
|
||||||
|
match self {
|
||||||
|
HTy::FixedStr(n) => n.enc(out),
|
||||||
|
HTy::Udt(n) => n.enc(out),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn dec(r: &mut Reader) -> Result<Self, LoadError> {
|
||||||
|
Ok(match r.u8()? {
|
||||||
|
0 => HTy::Num(NumTy::Int),
|
||||||
|
1 => HTy::Num(NumTy::Lng),
|
||||||
|
2 => HTy::Num(NumTy::Sng),
|
||||||
|
3 => HTy::Num(NumTy::Dbl),
|
||||||
|
4 => HTy::Num(NumTy::Cur),
|
||||||
|
5 => HTy::Str,
|
||||||
|
6 => HTy::FixedStr(r.u32()?),
|
||||||
|
7 => HTy::Udt(r.u16()?),
|
||||||
|
8 => HTy::Form,
|
||||||
|
9 => HTy::Control,
|
||||||
|
_ => return Err(LoadError::Corrupt("Signaturtyp")),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Enc for PropertyValue {
|
||||||
|
fn enc(&self, out: &mut Vec<u8>) {
|
||||||
|
match self {
|
||||||
|
Self::Integer(v) => {
|
||||||
|
out.push(0);
|
||||||
|
v.enc(out);
|
||||||
|
}
|
||||||
|
Self::Single(v) => {
|
||||||
|
out.push(1);
|
||||||
|
v.enc(out);
|
||||||
|
}
|
||||||
|
Self::String(v) => {
|
||||||
|
out.push(2);
|
||||||
|
w_string(out, v);
|
||||||
|
}
|
||||||
|
Self::Boolean(v) => {
|
||||||
|
out.push(3);
|
||||||
|
v.enc(out);
|
||||||
|
}
|
||||||
|
Self::Object(v) => {
|
||||||
|
out.push(4);
|
||||||
|
v.is_some().enc(out);
|
||||||
|
if let Some((object, index)) = v {
|
||||||
|
object.enc(out);
|
||||||
|
index.is_some().enc(out);
|
||||||
|
if let Some(index) = index {
|
||||||
|
index.enc(out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self::IntegerArray(v) => {
|
||||||
|
out.push(5);
|
||||||
|
(v.len() as u32).enc(out);
|
||||||
|
for v in v {
|
||||||
|
v.enc(out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn dec(r: &mut Reader) -> Result<Self, LoadError> {
|
||||||
|
Ok(match r.u8()? {
|
||||||
|
0 => Self::Integer(i32::dec(r)?),
|
||||||
|
1 => Self::Single(f32::dec(r)?),
|
||||||
|
2 => Self::String(r.string()?),
|
||||||
|
3 => Self::Boolean(bool::dec(r)?),
|
||||||
|
4 => Self::Object(if bool::dec(r)? {
|
||||||
|
Some((
|
||||||
|
r.u16()?,
|
||||||
|
if bool::dec(r)? {
|
||||||
|
Some(i32::dec(r)?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}),
|
||||||
|
5 => {
|
||||||
|
let n = r.u32()?;
|
||||||
|
let mut values = Vec::new();
|
||||||
|
for _ in 0..n {
|
||||||
|
values.push(i32::dec(r)?);
|
||||||
|
}
|
||||||
|
Self::IntegerArray(values)
|
||||||
|
}
|
||||||
|
_ => return Err(LoadError::Corrupt("Anfangswerttyp")),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Modulstruktur ------------------------------------------------------------
|
// ---- Modulstruktur ------------------------------------------------------------
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ProcCode {
|
pub struct ProcCode {
|
||||||
|
pub module: u16,
|
||||||
|
pub kind: HProcKind,
|
||||||
|
pub params: Vec<HParam>,
|
||||||
|
pub ret_ty: Option<HTy>,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub n_params: u16,
|
pub n_params: u16,
|
||||||
/// Initialisierung aller Frame-Slots (Parameter zuerst; deren Init
|
/// Initialisierung aller Frame-Slots (Parameter zuerst; deren Init
|
||||||
@@ -378,6 +512,10 @@ pub struct DataItem {
|
|||||||
/// Übersetztes Modul — Inhalt des `.tbc`-Containers.
|
/// Übersetztes Modul — Inhalt des `.tbc`-Containers.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct CompiledModule {
|
pub struct CompiledModule {
|
||||||
|
pub modules: Vec<(String, u8)>,
|
||||||
|
pub sources: Vec<SourceFile>,
|
||||||
|
pub form_initial: Vec<FormInitial>,
|
||||||
|
pub startup_form: Option<u16>,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
/// `OPTION BASE` (Untergrenze impliziter Arrays).
|
/// `OPTION BASE` (Untergrenze impliziter Arrays).
|
||||||
pub option_base: u8,
|
pub option_base: u8,
|
||||||
@@ -402,9 +540,204 @@ fn w_string(out: &mut Vec<u8>, s: &str) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl CompiledModule {
|
impl CompiledModule {
|
||||||
|
pub fn validate(&self) -> Result<(), LoadError> {
|
||||||
|
let bad = || LoadError::Corrupt("ungültige Tabellenreferenz oder Anfangsdaten");
|
||||||
|
if self.procs.is_empty()
|
||||||
|
|| self.sources.is_empty()
|
||||||
|
|| self.modules.is_empty()
|
||||||
|
|| self.modules.len() > u16::MAX as usize
|
||||||
|
|| self.procs.len() > u16::MAX as usize
|
||||||
|
|| self.objects.len() >= u16::MAX as usize
|
||||||
|
|| self.strings.len() >= u16::MAX as usize
|
||||||
|
|| self.globals_init.len() > u16::MAX as usize
|
||||||
|
|| self.global_names.len() != self.globals_init.len()
|
||||||
|
|| self.modules.iter().any(|(_, base)| *base > 1)
|
||||||
|
|| self.option_base != self.modules[0].1
|
||||||
|
|| self
|
||||||
|
.sources
|
||||||
|
.iter()
|
||||||
|
.any(|s| s.module as usize >= self.modules.len())
|
||||||
|
{
|
||||||
|
return Err(bad());
|
||||||
|
}
|
||||||
|
let ty_ok =
|
||||||
|
|ty: &TypeInit| !matches!(ty, TypeInit::Udt(id) if *id as usize >= self.udts.len());
|
||||||
|
let sig_ok = |ty: &HTy| !matches!(ty, HTy::Udt(id) if *id as usize >= self.udts.len());
|
||||||
|
if self.globals_init.iter().any(|t| !ty_ok(t)) {
|
||||||
|
return Err(bad());
|
||||||
|
}
|
||||||
|
for (id, udt) in self.udts.iter().enumerate() {
|
||||||
|
if udt
|
||||||
|
.fields
|
||||||
|
.iter()
|
||||||
|
.any(|t| matches!(t, TypeInit::Udt(n) if *n as usize >= id))
|
||||||
|
{
|
||||||
|
return Err(bad());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (id, o) in self.objects.iter().enumerate() {
|
||||||
|
if o.parent.is_some_and(|parent| parent as usize >= id) {
|
||||||
|
return Err(bad());
|
||||||
|
}
|
||||||
|
if o.parent_form.as_deref()
|
||||||
|
!= o.parent
|
||||||
|
.map(|parent| self.objects[parent as usize].name.as_str())
|
||||||
|
{
|
||||||
|
return Err(bad());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.startup_form.is_some_and(|id| {
|
||||||
|
self.objects
|
||||||
|
.get(id as usize)
|
||||||
|
.is_none_or(|o| o.class != ObjectClass::Form)
|
||||||
|
}) {
|
||||||
|
return Err(bad());
|
||||||
|
}
|
||||||
|
for e in &self.event_procs {
|
||||||
|
if e.object as usize >= self.objects.len() || e.proc as usize >= self.procs.len() {
|
||||||
|
return Err(bad());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut initials = std::collections::HashSet::new();
|
||||||
|
for initial in &self.form_initial {
|
||||||
|
let object = self.objects.get(initial.object as usize).ok_or_else(bad)?;
|
||||||
|
if !initials.insert((initial.object, initial.index))
|
||||||
|
|| (initial.index != 0 && !object.array)
|
||||||
|
{
|
||||||
|
return Err(bad());
|
||||||
|
}
|
||||||
|
for (id, value) in &initial.properties {
|
||||||
|
use tb_frontend::forms::PropertyType as T;
|
||||||
|
let spec = tb_frontend::forms::properties(object.class)
|
||||||
|
.get(*id as usize)
|
||||||
|
.copied()
|
||||||
|
.ok_or_else(bad)?;
|
||||||
|
let valid = match (value, spec.ty) {
|
||||||
|
(PropertyValue::Integer(v), T::Integer) => {
|
||||||
|
!spec.min.is_some_and(|min| *v < min)
|
||||||
|
&& !spec.max.is_some_and(|max| *v > max)
|
||||||
|
&& (spec.name != "INDEX" || *v == initial.index)
|
||||||
|
}
|
||||||
|
(PropertyValue::Single(v), T::Single) => v.is_finite(),
|
||||||
|
(PropertyValue::String(_), T::String)
|
||||||
|
| (PropertyValue::Boolean(_), T::Boolean)
|
||||||
|
| (PropertyValue::IntegerArray(_), T::IntegerArray) => true,
|
||||||
|
(PropertyValue::Object(v), T::Object) => {
|
||||||
|
v.is_none_or(|(id, _)| (id as usize) < self.objects.len())
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
if !valid {
|
||||||
|
return Err(bad());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for p in &self.procs {
|
||||||
|
if p.module as usize >= self.modules.len()
|
||||||
|
|| p.n_params as usize != p.params.len()
|
||||||
|
|| p.params.len() > p.locals_init.len()
|
||||||
|
|| p.locals_init.len() != p.local_names.len()
|
||||||
|
|| p.locals_init.iter().any(|t| !ty_ok(t))
|
||||||
|
|| p.params.iter().any(|p| !sig_ok(&p.ty))
|
||||||
|
|| p.ret_ty.as_ref().is_some_and(|t| !sig_ok(t))
|
||||||
|
|| matches!(p.kind, HProcKind::Function | HProcKind::DefFn) != p.ret_ty.is_some()
|
||||||
|
|| p.params.iter().zip(&p.locals_init).any(|(param, init)| {
|
||||||
|
*init
|
||||||
|
!= if param.array {
|
||||||
|
TypeInit::Empty
|
||||||
|
} else {
|
||||||
|
crate::codegen::type_init(¶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
|
/// `.tbc`-Container schreiben: Magic, Version, Flags, Abschnittstabelle
|
||||||
/// (Kennung/Offset/Länge), Abschnitte MODN, CONS, TYPS, GLOB, PROC
|
/// (Kennung/Offset/Länge), Abschnitte MODN, SRCS, CONS, TYPS, GLOB, PROC
|
||||||
/// (mit eingebettetem Code und Zeileninfo), DATA, JMPT.
|
/// (mit eingebettetem Code und Quellorten), DATA, JMPT, OBJS.
|
||||||
pub fn to_tbc(&self) -> Vec<u8> {
|
pub fn to_tbc(&self) -> Vec<u8> {
|
||||||
let mut sections: Vec<([u8; 4], Vec<u8>)> = Vec::new();
|
let mut sections: Vec<([u8; 4], Vec<u8>)> = Vec::new();
|
||||||
|
|
||||||
@@ -413,6 +746,19 @@ impl CompiledModule {
|
|||||||
modn.push(self.option_base);
|
modn.push(self.option_base);
|
||||||
sections.push((*b"MODN", modn));
|
sections.push((*b"MODN", modn));
|
||||||
|
|
||||||
|
let mut srcs = Vec::new();
|
||||||
|
(self.modules.len() as u32).enc(&mut srcs);
|
||||||
|
for (name, base) in &self.modules {
|
||||||
|
w_string(&mut srcs, name);
|
||||||
|
base.enc(&mut srcs);
|
||||||
|
}
|
||||||
|
(self.sources.len() as u32).enc(&mut srcs);
|
||||||
|
for source in &self.sources {
|
||||||
|
source.module.enc(&mut srcs);
|
||||||
|
w_string(&mut srcs, &source.path);
|
||||||
|
}
|
||||||
|
sections.push((*b"SRCS", srcs));
|
||||||
|
|
||||||
let mut cons = Vec::new();
|
let mut cons = Vec::new();
|
||||||
cons.extend_from_slice(&(self.strings.len() as u32).to_le_bytes());
|
cons.extend_from_slice(&(self.strings.len() as u32).to_le_bytes());
|
||||||
for s in &self.strings {
|
for s in &self.strings {
|
||||||
@@ -443,6 +789,19 @@ impl CompiledModule {
|
|||||||
proc.extend_from_slice(&(self.procs.len() as u32).to_le_bytes());
|
proc.extend_from_slice(&(self.procs.len() as u32).to_le_bytes());
|
||||||
for p in &self.procs {
|
for p in &self.procs {
|
||||||
w_string(&mut proc, &p.name);
|
w_string(&mut proc, &p.name);
|
||||||
|
p.module.enc(&mut proc);
|
||||||
|
(p.kind as u8).enc(&mut proc);
|
||||||
|
(p.params.len() as u32).enc(&mut proc);
|
||||||
|
for param in &p.params {
|
||||||
|
w_string(&mut proc, ¶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.n_params.to_le_bytes());
|
||||||
proc.extend_from_slice(&(p.locals_init.len() as u32).to_le_bytes());
|
proc.extend_from_slice(&(p.locals_init.len() as u32).to_le_bytes());
|
||||||
for (init, name) in p.locals_init.iter().zip(&p.local_names) {
|
for (init, name) in p.locals_init.iter().zip(&p.local_names) {
|
||||||
@@ -484,6 +843,7 @@ impl CompiledModule {
|
|||||||
objs.push(o.class.id());
|
objs.push(o.class.id());
|
||||||
w_string(&mut objs, o.parent_form.as_deref().unwrap_or(""));
|
w_string(&mut objs, o.parent_form.as_deref().unwrap_or(""));
|
||||||
objs.push(o.array as u8);
|
objs.push(o.array as u8);
|
||||||
|
o.parent.unwrap_or(u16::MAX).enc(&mut objs);
|
||||||
}
|
}
|
||||||
objs.extend_from_slice(&(self.event_procs.len() as u32).to_le_bytes());
|
objs.extend_from_slice(&(self.event_procs.len() as u32).to_le_bytes());
|
||||||
for e in &self.event_procs {
|
for e in &self.event_procs {
|
||||||
@@ -491,6 +851,17 @@ impl CompiledModule {
|
|||||||
w_string(&mut objs, &e.event);
|
w_string(&mut objs, &e.event);
|
||||||
objs.extend_from_slice(&e.proc.to_le_bytes());
|
objs.extend_from_slice(&e.proc.to_le_bytes());
|
||||||
}
|
}
|
||||||
|
self.startup_form.unwrap_or(u16::MAX).enc(&mut objs);
|
||||||
|
(self.form_initial.len() as u32).enc(&mut objs);
|
||||||
|
for initial in &self.form_initial {
|
||||||
|
initial.object.enc(&mut objs);
|
||||||
|
initial.index.enc(&mut objs);
|
||||||
|
(initial.properties.len() as u32).enc(&mut objs);
|
||||||
|
for (id, value) in &initial.properties {
|
||||||
|
id.enc(&mut objs);
|
||||||
|
value.enc(&mut objs);
|
||||||
|
}
|
||||||
|
}
|
||||||
sections.push((*b"OBJS", objs));
|
sections.push((*b"OBJS", objs));
|
||||||
|
|
||||||
// Header + Abschnittstabelle
|
// Header + Abschnittstabelle
|
||||||
@@ -527,8 +898,13 @@ impl CompiledModule {
|
|||||||
if version != TBC_VERSION {
|
if version != TBC_VERSION {
|
||||||
return Err(LoadError::Version(version));
|
return Err(LoadError::Version(version));
|
||||||
}
|
}
|
||||||
let _flags = r.u16()?;
|
if r.u16()? != 0 {
|
||||||
|
return Err(LoadError::Corrupt("Header-Flags"));
|
||||||
|
}
|
||||||
let n_sections = r.u32()? as usize;
|
let n_sections = r.u32()? as usize;
|
||||||
|
if n_sections != 9 {
|
||||||
|
return Err(LoadError::Corrupt("Abschnittsanzahl"));
|
||||||
|
}
|
||||||
let mut table = Vec::new();
|
let mut table = Vec::new();
|
||||||
for _ in 0..n_sections {
|
for _ in 0..n_sections {
|
||||||
let id: [u8; 4] = r.take(4)?.try_into().unwrap();
|
let id: [u8; 4] = r.take(4)?.try_into().unwrap();
|
||||||
@@ -536,6 +912,27 @@ impl CompiledModule {
|
|||||||
let len = r.u32()? as usize;
|
let len = r.u32()? as usize;
|
||||||
table.push((id, off, len));
|
table.push((id, off, len));
|
||||||
}
|
}
|
||||||
|
let expected = [
|
||||||
|
*b"MODN", *b"SRCS", *b"CONS", *b"TYPS", *b"GLOB", *b"PROC", *b"DATA", *b"JMPT",
|
||||||
|
*b"OBJS",
|
||||||
|
];
|
||||||
|
let mut seen = std::collections::HashSet::new();
|
||||||
|
let mut ranges = Vec::new();
|
||||||
|
for (id, off, len) in &table {
|
||||||
|
if !expected.contains(id)
|
||||||
|
|| !seen.insert(*id)
|
||||||
|
|| *off < r.pos
|
||||||
|
|| *off > buf.len()
|
||||||
|
|| *len > buf.len() - *off
|
||||||
|
{
|
||||||
|
return Err(LoadError::Corrupt("Abschnittstabelle"));
|
||||||
|
}
|
||||||
|
ranges.push((*off, off + len));
|
||||||
|
}
|
||||||
|
ranges.sort_unstable();
|
||||||
|
if ranges.windows(2).any(|pair| pair[0].1 > pair[1].0) {
|
||||||
|
return Err(LoadError::Corrupt("überlappende Abschnitte"));
|
||||||
|
}
|
||||||
let section = |id: &[u8; 4]| -> Result<Reader, LoadError> {
|
let section = |id: &[u8; 4]| -> Result<Reader, LoadError> {
|
||||||
for (sid, off, len) in &table {
|
for (sid, off, len) in &table {
|
||||||
if sid == id {
|
if sid == id {
|
||||||
@@ -552,44 +949,86 @@ impl CompiledModule {
|
|||||||
let name = r.string()?;
|
let name = r.string()?;
|
||||||
let option_base = r.u8()?;
|
let option_base = r.u8()?;
|
||||||
|
|
||||||
|
r.finish()?;
|
||||||
|
let mut r = section(b"SRCS")?;
|
||||||
|
let n = r.u32()?;
|
||||||
|
let mut modules = Vec::new();
|
||||||
|
for _ in 0..n {
|
||||||
|
modules.push((r.string()?, r.u8()?));
|
||||||
|
}
|
||||||
|
let n = r.u32()?;
|
||||||
|
let mut sources = Vec::new();
|
||||||
|
for _ in 0..n {
|
||||||
|
sources.push(SourceFile {
|
||||||
|
module: r.u16()?,
|
||||||
|
path: r.string()?,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
r.finish()?;
|
||||||
let mut r = section(b"CONS")?;
|
let mut r = section(b"CONS")?;
|
||||||
let n = r.u32()? as usize;
|
let n = r.u32()? as usize;
|
||||||
let mut strings = Vec::with_capacity(n);
|
let mut strings = Vec::new();
|
||||||
for _ in 0..n {
|
for _ in 0..n {
|
||||||
strings.push(Rc::from(r.string()?.as_str()));
|
strings.push(Rc::from(r.string()?.as_str()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
r.finish()?;
|
||||||
let mut r = section(b"TYPS")?;
|
let mut r = section(b"TYPS")?;
|
||||||
let n = r.u32()? as usize;
|
let n = r.u32()? as usize;
|
||||||
let mut udts = Vec::with_capacity(n);
|
let mut udts = Vec::new();
|
||||||
for _ in 0..n {
|
for _ in 0..n {
|
||||||
let name = r.string()?;
|
let name = r.string()?;
|
||||||
let nf = r.u32()? as usize;
|
let nf = r.u32()? as usize;
|
||||||
let mut fields = Vec::with_capacity(nf);
|
let mut fields = Vec::new();
|
||||||
for _ in 0..nf {
|
for _ in 0..nf {
|
||||||
fields.push(TypeInit::dec(&mut r)?);
|
fields.push(TypeInit::dec(&mut r)?);
|
||||||
}
|
}
|
||||||
udts.push(UdtLayout { name, fields });
|
udts.push(UdtLayout { name, fields });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
r.finish()?;
|
||||||
let mut r = section(b"GLOB")?;
|
let mut r = section(b"GLOB")?;
|
||||||
let n = r.u32()? as usize;
|
let n = r.u32()? as usize;
|
||||||
let mut globals_init = Vec::with_capacity(n);
|
let mut globals_init = Vec::new();
|
||||||
let mut global_names = Vec::with_capacity(n);
|
let mut global_names = Vec::new();
|
||||||
for _ in 0..n {
|
for _ in 0..n {
|
||||||
globals_init.push(TypeInit::dec(&mut r)?);
|
globals_init.push(TypeInit::dec(&mut r)?);
|
||||||
global_names.push(r.string()?);
|
global_names.push(r.string()?);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
r.finish()?;
|
||||||
let mut r = section(b"PROC")?;
|
let mut r = section(b"PROC")?;
|
||||||
let n = r.u32()? as usize;
|
let n = r.u32()? as usize;
|
||||||
let mut procs = Vec::with_capacity(n);
|
let mut procs = Vec::new();
|
||||||
for _ in 0..n {
|
for _ in 0..n {
|
||||||
let name = r.string()?;
|
let name = r.string()?;
|
||||||
|
let module = r.u16()?;
|
||||||
|
let kind = match r.u8()? {
|
||||||
|
0 => HProcKind::Main,
|
||||||
|
1 => HProcKind::Sub,
|
||||||
|
2 => HProcKind::Function,
|
||||||
|
3 => HProcKind::DefFn,
|
||||||
|
_ => return Err(LoadError::Corrupt("Prozedurart")),
|
||||||
|
};
|
||||||
|
let np = r.u32()?;
|
||||||
|
let mut params = Vec::new();
|
||||||
|
for _ in 0..np {
|
||||||
|
params.push(HParam {
|
||||||
|
name: r.string()?,
|
||||||
|
ty: HTy::dec(&mut r)?,
|
||||||
|
array: bool::dec(&mut r)?,
|
||||||
|
by_ref: bool::dec(&mut r)?,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let ret_ty = if bool::dec(&mut r)? {
|
||||||
|
Some(HTy::dec(&mut r)?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
let n_params = r.u16()?;
|
let n_params = r.u16()?;
|
||||||
let nl = r.u32()? as usize;
|
let nl = r.u32()? as usize;
|
||||||
let mut locals_init = Vec::with_capacity(nl);
|
let mut locals_init = Vec::new();
|
||||||
let mut local_names = Vec::with_capacity(nl);
|
let mut local_names = Vec::new();
|
||||||
for _ in 0..nl {
|
for _ in 0..nl {
|
||||||
locals_init.push(TypeInit::dec(&mut r)?);
|
locals_init.push(TypeInit::dec(&mut r)?);
|
||||||
local_names.push(r.string()?);
|
local_names.push(r.string()?);
|
||||||
@@ -598,11 +1037,16 @@ impl CompiledModule {
|
|||||||
let code_len = r.u32()? as usize;
|
let code_len = r.u32()? as usize;
|
||||||
let code_bytes = r.take(code_len)?;
|
let code_bytes = r.take(code_len)?;
|
||||||
let mut cr = Reader::new(code_bytes);
|
let mut cr = Reader::new(code_bytes);
|
||||||
let mut code = Vec::with_capacity(n_instr);
|
let mut code = Vec::new();
|
||||||
for _ in 0..n_instr {
|
for _ in 0..n_instr {
|
||||||
code.push(Instr::decode(&mut cr)?);
|
code.push(Instr::decode(&mut cr)?);
|
||||||
}
|
}
|
||||||
|
cr.finish()?;
|
||||||
procs.push(ProcCode {
|
procs.push(ProcCode {
|
||||||
|
module,
|
||||||
|
kind,
|
||||||
|
params,
|
||||||
|
ret_ty,
|
||||||
name,
|
name,
|
||||||
n_params,
|
n_params,
|
||||||
locals_init,
|
locals_init,
|
||||||
@@ -611,44 +1055,49 @@ impl CompiledModule {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
r.finish()?;
|
||||||
let mut r = section(b"DATA")?;
|
let mut r = section(b"DATA")?;
|
||||||
let n = r.u32()? as usize;
|
let n = r.u32()? as usize;
|
||||||
let mut data = Vec::with_capacity(n);
|
let mut data = Vec::new();
|
||||||
for _ in 0..n {
|
for _ in 0..n {
|
||||||
let text = r.string()?;
|
let text = r.string()?;
|
||||||
let line = r.u32()?;
|
let line = r.u32()?;
|
||||||
data.push(DataItem { text, line });
|
data.push(DataItem { text, line });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
r.finish()?;
|
||||||
let mut r = section(b"JMPT")?;
|
let mut r = section(b"JMPT")?;
|
||||||
let n = r.u32()? as usize;
|
let n = r.u32()? as usize;
|
||||||
let mut jump_tables = Vec::with_capacity(n);
|
let mut jump_tables = Vec::new();
|
||||||
for _ in 0..n {
|
for _ in 0..n {
|
||||||
let m = r.u32()? as usize;
|
let m = r.u32()? as usize;
|
||||||
let mut t = Vec::with_capacity(m);
|
let mut t = Vec::new();
|
||||||
for _ in 0..m {
|
for _ in 0..m {
|
||||||
t.push(r.u32()?);
|
t.push(r.u32()?);
|
||||||
}
|
}
|
||||||
jump_tables.push(t);
|
jump_tables.push(t);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
r.finish()?;
|
||||||
let mut r = section(b"OBJS")?;
|
let mut r = section(b"OBJS")?;
|
||||||
let n = r.u32()? as usize;
|
let n = r.u32()? as usize;
|
||||||
let mut objects = Vec::with_capacity(n);
|
let mut objects = Vec::new();
|
||||||
for _ in 0..n {
|
for _ in 0..n {
|
||||||
let name = r.string()?;
|
let name = r.string()?;
|
||||||
let class = ObjectClass::from_id(r.u8()?).ok_or(LoadError::Corrupt("Objektklasse"))?;
|
let class = ObjectClass::from_id(r.u8()?).ok_or(LoadError::Corrupt("Objektklasse"))?;
|
||||||
let parent = r.string()?;
|
let parent = r.string()?;
|
||||||
let array = r.u8()? != 0;
|
let array = bool::dec(&mut r)?;
|
||||||
|
let parent_id = r.u16()?;
|
||||||
objects.push(FormObject {
|
objects.push(FormObject {
|
||||||
name,
|
name,
|
||||||
class,
|
class,
|
||||||
parent_form: (!parent.is_empty()).then_some(parent),
|
parent_form: (!parent.is_empty()).then_some(parent),
|
||||||
|
parent: (parent_id != u16::MAX).then_some(parent_id),
|
||||||
array,
|
array,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let n = r.u32()? as usize;
|
let n = r.u32()? as usize;
|
||||||
let mut event_procs = Vec::with_capacity(n);
|
let mut event_procs = Vec::new();
|
||||||
for _ in 0..n {
|
for _ in 0..n {
|
||||||
event_procs.push(HEventProc {
|
event_procs.push(HEventProc {
|
||||||
object: r.u16()?,
|
object: r.u16()?,
|
||||||
@@ -657,7 +1106,35 @@ impl CompiledModule {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(CompiledModule {
|
let startup = r.u16()?;
|
||||||
|
let startup_form = (startup != u16::MAX).then_some(startup);
|
||||||
|
let n = r.u32()?;
|
||||||
|
let mut form_initial = Vec::new();
|
||||||
|
for _ in 0..n {
|
||||||
|
let object = r.u16()?;
|
||||||
|
let index = i32::dec(&mut r)?;
|
||||||
|
let n = r.u32()?;
|
||||||
|
let mut properties = std::collections::BTreeMap::new();
|
||||||
|
for _ in 0..n {
|
||||||
|
if properties
|
||||||
|
.insert(r.u16()?, PropertyValue::dec(&mut r)?)
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
return Err(LoadError::Corrupt("doppelte Anfangseigenschaft"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
form_initial.push(FormInitial {
|
||||||
|
object,
|
||||||
|
index,
|
||||||
|
properties,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
r.finish()?;
|
||||||
|
let module = CompiledModule {
|
||||||
|
modules,
|
||||||
|
sources,
|
||||||
|
form_initial,
|
||||||
|
startup_form,
|
||||||
name,
|
name,
|
||||||
option_base,
|
option_base,
|
||||||
strings,
|
strings,
|
||||||
@@ -669,7 +1146,9 @@ impl CompiledModule {
|
|||||||
jump_tables,
|
jump_tables,
|
||||||
objects,
|
objects,
|
||||||
event_procs,
|
event_procs,
|
||||||
})
|
};
|
||||||
|
module.validate()?;
|
||||||
|
Ok(module)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -712,6 +1191,13 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn tbc_roundtrip() {
|
fn tbc_roundtrip() {
|
||||||
let m = CompiledModule {
|
let m = CompiledModule {
|
||||||
|
modules: vec![("TEST".into(), 1)],
|
||||||
|
sources: vec![SourceFile {
|
||||||
|
module: 0,
|
||||||
|
path: "test.bas".into(),
|
||||||
|
}],
|
||||||
|
form_initial: vec![],
|
||||||
|
startup_form: None,
|
||||||
name: "TEST".into(),
|
name: "TEST".into(),
|
||||||
option_base: 1,
|
option_base: 1,
|
||||||
strings: vec![Rc::from("Hallo"), Rc::from("Welt")],
|
strings: vec![Rc::from("Hallo"), Rc::from("Welt")],
|
||||||
@@ -722,6 +1208,10 @@ mod tests {
|
|||||||
fields: vec![TypeInit::FixedStr(30), TypeInit::Dbl],
|
fields: vec![TypeInit::FixedStr(30), TypeInit::Dbl],
|
||||||
}],
|
}],
|
||||||
procs: vec![ProcCode {
|
procs: vec![ProcCode {
|
||||||
|
module: 0,
|
||||||
|
kind: HProcKind::Main,
|
||||||
|
params: vec![],
|
||||||
|
ret_ty: None,
|
||||||
name: "TEST".into(),
|
name: "TEST".into(),
|
||||||
n_params: 0,
|
n_params: 0,
|
||||||
locals_init: vec![],
|
locals_init: vec![],
|
||||||
@@ -751,6 +1241,13 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn unbekannte_version_wird_abgelehnt() {
|
fn unbekannte_version_wird_abgelehnt() {
|
||||||
let m = CompiledModule {
|
let m = CompiledModule {
|
||||||
|
modules: vec![("TEST".into(), 0)],
|
||||||
|
sources: vec![SourceFile {
|
||||||
|
module: 0,
|
||||||
|
path: "test.bas".into(),
|
||||||
|
}],
|
||||||
|
form_initial: vec![],
|
||||||
|
startup_form: None,
|
||||||
name: "T".into(),
|
name: "T".into(),
|
||||||
option_base: 0,
|
option_base: 0,
|
||||||
strings: vec![],
|
strings: vec![],
|
||||||
|
|||||||
@@ -16,6 +16,12 @@ use tb_runtime::value::TypeInit;
|
|||||||
|
|
||||||
pub fn compile(hir: &HirModule) -> CompiledModule {
|
pub fn compile(hir: &HirModule) -> CompiledModule {
|
||||||
let mut cg = Codegen {
|
let mut cg = Codegen {
|
||||||
|
common_arrays: hir
|
||||||
|
.commons
|
||||||
|
.iter()
|
||||||
|
.filter(|c| c.dims.is_some())
|
||||||
|
.map(|c| c.slot)
|
||||||
|
.collect(),
|
||||||
strings: Vec::new(),
|
strings: Vec::new(),
|
||||||
string_ids: HashMap::new(),
|
string_ids: HashMap::new(),
|
||||||
jump_tables: Vec::new(),
|
jump_tables: Vec::new(),
|
||||||
@@ -30,6 +36,13 @@ pub fn compile(hir: &HirModule) -> CompiledModule {
|
|||||||
procs.push(cg.compile_proc(proc));
|
procs.push(cg.compile_proc(proc));
|
||||||
}
|
}
|
||||||
CompiledModule {
|
CompiledModule {
|
||||||
|
modules: vec![(hir.name.clone(), hir.option_base)],
|
||||||
|
sources: vec![tb_frontend::source::SourceFile {
|
||||||
|
module: 0,
|
||||||
|
path: hir.name.clone(),
|
||||||
|
}],
|
||||||
|
form_initial: vec![],
|
||||||
|
startup_form: None,
|
||||||
name: hir.name.clone(),
|
name: hir.name.clone(),
|
||||||
option_base: hir.option_base,
|
option_base: hir.option_base,
|
||||||
strings: cg.strings,
|
strings: cg.strings,
|
||||||
@@ -67,7 +80,7 @@ fn slot_init(v: &hir::HVar) -> TypeInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn type_init(t: &HTy) -> TypeInit {
|
pub(crate) fn type_init(t: &HTy) -> TypeInit {
|
||||||
match t {
|
match t {
|
||||||
HTy::Num(NumTy::Int) => TypeInit::Int,
|
HTy::Num(NumTy::Int) => TypeInit::Int,
|
||||||
HTy::Num(NumTy::Lng) => TypeInit::Lng,
|
HTy::Num(NumTy::Lng) => TypeInit::Lng,
|
||||||
@@ -82,6 +95,7 @@ fn type_init(t: &HTy) -> TypeInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct Codegen {
|
struct Codegen {
|
||||||
|
common_arrays: std::collections::HashSet<u16>,
|
||||||
strings: Vec<Rc<str>>,
|
strings: Vec<Rc<str>>,
|
||||||
string_ids: HashMap<String, u16>,
|
string_ids: HashMap<String, u16>,
|
||||||
jump_tables: Vec<Vec<u32>>,
|
jump_tables: Vec<Vec<u32>>,
|
||||||
@@ -95,6 +109,7 @@ struct Codegen {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct ProcCtx {
|
struct ProcCtx {
|
||||||
|
pos: tb_frontend::SourcePos,
|
||||||
code: Vec<Instr>,
|
code: Vec<Instr>,
|
||||||
/// LabelId → Instruktionsindex.
|
/// LabelId → Instruktionsindex.
|
||||||
label_pc: Vec<Option<u32>>,
|
label_pc: Vec<Option<u32>>,
|
||||||
@@ -134,6 +149,10 @@ impl ProcCtx {
|
|||||||
self.label_pc[label as usize] = Some(pc);
|
self.label_pc[label as usize] = Some(pc);
|
||||||
}
|
}
|
||||||
fn emit(&mut self, i: Instr) {
|
fn emit(&mut self, i: Instr) {
|
||||||
|
if matches!(i, Instr::Stmt(_) | Instr::InitStmt(_)) {
|
||||||
|
self.code
|
||||||
|
.push(Instr::Source(self.pos.source, self.pos.column));
|
||||||
|
}
|
||||||
self.code.push(i);
|
self.code.push(i);
|
||||||
}
|
}
|
||||||
/// Sprunginstruktion mit noch unbekanntem Ziel emittieren.
|
/// Sprunginstruktion mit noch unbekanntem Ziel emittieren.
|
||||||
@@ -162,6 +181,7 @@ impl Codegen {
|
|||||||
|
|
||||||
fn compile_proc(&mut self, proc: &hir::HProc) -> ProcCode {
|
fn compile_proc(&mut self, proc: &hir::HProc) -> ProcCode {
|
||||||
let mut ctx = ProcCtx {
|
let mut ctx = ProcCtx {
|
||||||
|
pos: tb_frontend::SourcePos::default(),
|
||||||
code: Vec::new(),
|
code: Vec::new(),
|
||||||
label_pc: vec![None; proc.label_count as usize],
|
label_pc: vec![None; proc.label_count as usize],
|
||||||
fixups: Vec::new(),
|
fixups: Vec::new(),
|
||||||
@@ -238,6 +258,10 @@ impl Codegen {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ProcCode {
|
ProcCode {
|
||||||
|
module: 0,
|
||||||
|
kind: proc.kind,
|
||||||
|
params: proc.params.clone(),
|
||||||
|
ret_ty: proc.ret_ty.clone(),
|
||||||
name: proc.name.clone(),
|
name: proc.name.clone(),
|
||||||
n_params: proc.params.len() as u16,
|
n_params: proc.params.len() as u16,
|
||||||
locals_init: proc.locals.iter().map(slot_init).collect(),
|
locals_init: proc.locals.iter().map(slot_init).collect(),
|
||||||
@@ -268,13 +292,14 @@ impl Codegen {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn stmt_inner(&mut self, ctx: &mut ProcCtx, proc: &hir::HProc, stmt: &HStmt, boundary: bool) {
|
fn stmt_inner(&mut self, ctx: &mut ProcCtx, proc: &hir::HProc, stmt: &HStmt, boundary: bool) {
|
||||||
|
ctx.pos = stmt.pos;
|
||||||
match &stmt.kind {
|
match &stmt.kind {
|
||||||
HStmtKind::Label(l) => {
|
HStmtKind::Label(l) => {
|
||||||
ctx.bind(*l);
|
ctx.bind(*l);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_ if boundary => ctx.emit(Instr::Stmt(stmt.line)),
|
_ if boundary => ctx.emit(Instr::Stmt(stmt.line)),
|
||||||
_ => {}
|
_ => ctx.emit(Instr::InitStmt(stmt.line)),
|
||||||
}
|
}
|
||||||
match &stmt.kind {
|
match &stmt.kind {
|
||||||
HStmtKind::Label(_) => unreachable!(),
|
HStmtKind::Label(_) => unreachable!(),
|
||||||
@@ -478,6 +503,7 @@ impl Codegen {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
HStmtKind::Loop {
|
HStmtKind::Loop {
|
||||||
|
end_pos,
|
||||||
pre,
|
pre,
|
||||||
post,
|
post,
|
||||||
body,
|
body,
|
||||||
@@ -496,6 +522,8 @@ impl Codegen {
|
|||||||
for s in body {
|
for s in body {
|
||||||
self.stmt(ctx, proc, s);
|
self.stmt(ctx, proc, s);
|
||||||
}
|
}
|
||||||
|
ctx.pos = *end_pos;
|
||||||
|
ctx.emit(Instr::Stmt(end_pos.line));
|
||||||
match post {
|
match post {
|
||||||
Some((is_until, cond)) => {
|
Some((is_until, cond)) => {
|
||||||
self.expr(ctx, cond);
|
self.expr(ctx, cond);
|
||||||
@@ -511,6 +539,7 @@ impl Codegen {
|
|||||||
ctx.bind(*exit_label);
|
ctx.bind(*exit_label);
|
||||||
}
|
}
|
||||||
HStmtKind::For {
|
HStmtKind::For {
|
||||||
|
end_pos,
|
||||||
var,
|
var,
|
||||||
ty,
|
ty,
|
||||||
from,
|
from,
|
||||||
@@ -533,7 +562,7 @@ impl Codegen {
|
|||||||
*step_slot,
|
*step_slot,
|
||||||
body,
|
body,
|
||||||
*exit_label,
|
*exit_label,
|
||||||
stmt.line,
|
*end_pos,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
HStmtKind::Goto(l) => ctx.emit_jump(Instr::Jump(0), *l),
|
HStmtKind::Goto(l) => ctx.emit_jump(Instr::Jump(0), *l),
|
||||||
@@ -663,6 +692,8 @@ impl Codegen {
|
|||||||
let init = type_init(elem);
|
let init = type_init(elem);
|
||||||
if *redim {
|
if *redim {
|
||||||
ctx.emit(Instr::RedimArr(global, s, dims.len() as u8, init));
|
ctx.emit(Instr::RedimArr(global, s, dims.len() as u8, init));
|
||||||
|
} else if global && self.common_arrays.contains(&s) {
|
||||||
|
ctx.emit(Instr::CommonArr(global, s, dims.len() as u8, init));
|
||||||
} else {
|
} else {
|
||||||
ctx.emit(Instr::DimArr(global, s, dims.len() as u8, init));
|
ctx.emit(Instr::DimArr(global, s, dims.len() as u8, init));
|
||||||
}
|
}
|
||||||
@@ -697,7 +728,7 @@ impl Codegen {
|
|||||||
step_slot: Option<VarSlot>,
|
step_slot: Option<VarSlot>,
|
||||||
body: &[HStmt],
|
body: &[HStmt],
|
||||||
exit_label: u16,
|
exit_label: u16,
|
||||||
line: u32,
|
end_pos: tb_frontend::SourcePos,
|
||||||
) {
|
) {
|
||||||
// Startwert, Grenze, ggf. Schritt einmal auswerten.
|
// Startwert, Grenze, ggf. Schritt einmal auswerten.
|
||||||
self.store_place(ctx, var, |cg, ctx| cg.expr(ctx, from));
|
self.store_place(ctx, var, |cg, ctx| cg.expr(ctx, from));
|
||||||
@@ -715,15 +746,6 @@ impl Codegen {
|
|||||||
let l_test = ctx.new_label();
|
let l_test = ctx.new_label();
|
||||||
let l_body = ctx.new_label();
|
let l_body = ctx.new_label();
|
||||||
ctx.bind(l_test);
|
ctx.bind(l_test);
|
||||||
// Anweisungsgrenze für den Rücksprung von `NEXT`. Sie kann nicht
|
|
||||||
// auf die Grenze des `FOR` zeigen — dort stünde die Initialisierung
|
|
||||||
// noch einmal.
|
|
||||||
//
|
|
||||||
// ponytail: gemeldet wird die Zeile des `FOR`, nicht die des
|
|
||||||
// `NEXT` — die trägt das HIR nicht. Ceiling: braucht der Debugger
|
|
||||||
// in Phase 5 die genaue Zeile, bekommt `HStmtKind::For` ein Feld
|
|
||||||
// `next_line`.
|
|
||||||
ctx.emit(Instr::Stmt(line));
|
|
||||||
match const_step {
|
match const_step {
|
||||||
Some(s) => {
|
Some(s) => {
|
||||||
// Vergleichsrichtung zur Compilezeit.
|
// Vergleichsrichtung zur Compilezeit.
|
||||||
@@ -757,7 +779,9 @@ impl Codegen {
|
|||||||
for s in body {
|
for s in body {
|
||||||
self.stmt(ctx, proc, s);
|
self.stmt(ctx, proc, s);
|
||||||
}
|
}
|
||||||
// NEXT: inkrementieren, zurück zum Test.
|
// NEXT: eigene Quellgrenze für Fehler, RESUME und Debugger.
|
||||||
|
ctx.pos = end_pos;
|
||||||
|
ctx.emit(Instr::Stmt(end_pos.line));
|
||||||
self.store_place(ctx, var, |cg, ctx| {
|
self.store_place(ctx, var, |cg, ctx| {
|
||||||
cg.load_place(ctx, var);
|
cg.load_place(ctx, var);
|
||||||
match (step, step_slot) {
|
match (step, step_slot) {
|
||||||
@@ -777,6 +801,7 @@ impl Codegen {
|
|||||||
match e {
|
match e {
|
||||||
HExpr::Int(v) => ctx.emit(Instr::PushInt(*v)),
|
HExpr::Int(v) => ctx.emit(Instr::PushInt(*v)),
|
||||||
HExpr::Lng(v) => ctx.emit(Instr::PushLng(*v)),
|
HExpr::Lng(v) => ctx.emit(Instr::PushLng(*v)),
|
||||||
|
HExpr::UdtId(id) => ctx.emit(Instr::PushUdtId(*id)),
|
||||||
HExpr::Sng(v) => ctx.emit(Instr::PushSng(*v)),
|
HExpr::Sng(v) => ctx.emit(Instr::PushSng(*v)),
|
||||||
HExpr::Dbl(v) => ctx.emit(Instr::PushDbl(*v)),
|
HExpr::Dbl(v) => ctx.emit(Instr::PushDbl(*v)),
|
||||||
HExpr::Cur(v) => ctx.emit(Instr::PushCur(*v)),
|
HExpr::Cur(v) => ctx.emit(Instr::PushCur(*v)),
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ struct Frame {
|
|||||||
/// Instruktionsindex der zuletzt begonnenen Anweisung (`Stmt`).
|
/// Instruktionsindex der zuletzt begonnenen Anweisung (`Stmt`).
|
||||||
last_stmt_pc: usize,
|
last_stmt_pc: usize,
|
||||||
line: u32,
|
line: u32,
|
||||||
|
source: u32,
|
||||||
|
column: u32,
|
||||||
/// Gesetzt, wenn dieser Frame der Handler eines Ereignis-Traps ist.
|
/// Gesetzt, wenn dieser Frame der Handler eines Ereignis-Traps ist.
|
||||||
/// Er läuft im Modulrumpf und **teilt dessen Locals** — ein eigener
|
/// Er läuft im Modulrumpf und **teilt dessen Locals** — ein eigener
|
||||||
/// Satz würde dem Handler leere Modulvariablen zeigen. Sein `RETURN`
|
/// Satz würde dem Handler leere Modulvariablen zeigen. Sein `RETURN`
|
||||||
@@ -117,14 +119,14 @@ pub struct Vm {
|
|||||||
erl: u32,
|
erl: u32,
|
||||||
/// Zuletzt durchlaufene numerische Zeilennummer (0 = keine).
|
/// Zuletzt durchlaufene numerische Zeilennummer (0 = keine).
|
||||||
zeile_nr: u32,
|
zeile_nr: u32,
|
||||||
module_handler: Handler,
|
module_handlers: Vec<Handler>,
|
||||||
in_handler: bool,
|
in_handler: bool,
|
||||||
resume_pc: usize,
|
resume_pc: usize,
|
||||||
// Steuerung
|
// Steuerung
|
||||||
flags: u32,
|
flags: u32,
|
||||||
/// Zählt Anweisungsgrenzen für die regelmäßige Ereignisabholung.
|
/// Zählt Anweisungsgrenzen für die regelmäßige Ereignisabholung.
|
||||||
tick_zaehler: u32,
|
tick_zaehler: u32,
|
||||||
breakpoints: HashSet<u32>,
|
breakpoints: HashSet<(u16, u32)>,
|
||||||
data_ptr: usize,
|
data_ptr: usize,
|
||||||
start_pc: Option<usize>,
|
start_pc: Option<usize>,
|
||||||
pub forms: FormsModel,
|
pub forms: FormsModel,
|
||||||
@@ -170,7 +172,15 @@ impl Vm {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|t| default_value(t, &module.udts))
|
.map(|t| default_value(t, &module.udts))
|
||||||
.collect();
|
.collect();
|
||||||
let forms = FormsModel::new(module.objects.clone(), 80, 25);
|
let mut forms = FormsModel::new(module.objects.clone(), 80, 25);
|
||||||
|
for initial in &module.form_initial {
|
||||||
|
initial
|
||||||
|
.apply(&mut forms)
|
||||||
|
.expect("validierte Forms-Anfangsdaten");
|
||||||
|
}
|
||||||
|
if let Some(form) = module.startup_form {
|
||||||
|
forms.show(form, false).expect("validiertes Startformular");
|
||||||
|
}
|
||||||
let mut vm = Vm {
|
let mut vm = Vm {
|
||||||
globals,
|
globals,
|
||||||
locals: Vec::new(),
|
locals: Vec::new(),
|
||||||
@@ -180,7 +190,7 @@ impl Vm {
|
|||||||
err: 0,
|
err: 0,
|
||||||
erl: 0,
|
erl: 0,
|
||||||
zeile_nr: 0,
|
zeile_nr: 0,
|
||||||
module_handler: Handler::None,
|
module_handlers: vec![Handler::None; module.modules.len()],
|
||||||
in_handler: false,
|
in_handler: false,
|
||||||
resume_pc: 0,
|
resume_pc: 0,
|
||||||
flags: 0,
|
flags: 0,
|
||||||
@@ -202,7 +212,12 @@ impl Vm {
|
|||||||
let target = self.module.procs[0]
|
let target = self.module.procs[0]
|
||||||
.code
|
.code
|
||||||
.iter()
|
.iter()
|
||||||
.position(|instruction| matches!(instruction, Instr::Stmt(found) if *found == line));
|
.position(|instruction| matches!(instruction, Instr::SetErl(found) if *found == line))
|
||||||
|
.or_else(|| {
|
||||||
|
self.module.procs[0].code.iter().position(
|
||||||
|
|instruction| matches!(instruction, Instr::Stmt(found) if *found == line),
|
||||||
|
)
|
||||||
|
});
|
||||||
let target = target.ok_or(RuntimeError(8))?;
|
let target = target.ok_or(RuntimeError(8))?;
|
||||||
if self.module.procs[0]
|
if self.module.procs[0]
|
||||||
.code
|
.code
|
||||||
@@ -229,6 +244,8 @@ impl Vm {
|
|||||||
}
|
}
|
||||||
let stack_base = self.stack.len();
|
let stack_base = self.stack.len();
|
||||||
self.frames.push(Frame {
|
self.frames.push(Frame {
|
||||||
|
source: 0,
|
||||||
|
column: 0,
|
||||||
proc,
|
proc,
|
||||||
pc: 0,
|
pc: 0,
|
||||||
locals_base,
|
locals_base,
|
||||||
@@ -361,7 +378,12 @@ impl Vm {
|
|||||||
if !matches!(self.module.procs[proc].code.as_slice(), [Instr::RetProc]) {
|
if !matches!(self.module.procs[proc].code.as_slice(), [Instr::RetProc]) {
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
let name = self.module.procs[proc].name.to_ascii_uppercase();
|
let name = self.module.procs[proc]
|
||||||
|
.name
|
||||||
|
.rsplit('!')
|
||||||
|
.next()
|
||||||
|
.unwrap()
|
||||||
|
.to_ascii_uppercase();
|
||||||
if !matches!(
|
if !matches!(
|
||||||
name.as_str(),
|
name.as_str(),
|
||||||
"CMNDLGREGISTER"
|
"CMNDLGREGISTER"
|
||||||
@@ -767,6 +789,8 @@ impl Vm {
|
|||||||
let locals_base = self.frames[0].locals_base;
|
let locals_base = self.frames[0].locals_base;
|
||||||
let stack_base = self.stack.len();
|
let stack_base = self.stack.len();
|
||||||
self.frames.push(Frame {
|
self.frames.push(Frame {
|
||||||
|
source: 0,
|
||||||
|
column: 0,
|
||||||
proc: 0,
|
proc: 0,
|
||||||
pc: ziel as usize,
|
pc: ziel as usize,
|
||||||
locals_base,
|
locals_base,
|
||||||
@@ -821,17 +845,54 @@ impl Vm {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_breakpoint(&mut self, line: u32) {
|
pub fn add_breakpoint(&mut self, line: u32) {
|
||||||
self.breakpoints.insert(line);
|
self.breakpoints.insert((0, line));
|
||||||
self.flags |= F_BREAK;
|
self.flags |= F_BREAK;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_breakpoint(&mut self, line: u32) {
|
pub fn remove_breakpoint(&mut self, line: u32) {
|
||||||
self.breakpoints.remove(&line);
|
self.breakpoints.remove(&(0, line));
|
||||||
if self.breakpoints.is_empty() {
|
if self.breakpoints.is_empty() {
|
||||||
self.flags &= !F_BREAK;
|
self.flags &= !F_BREAK;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn add_module_breakpoint(&mut self, module: u16, line: u32) {
|
||||||
|
self.breakpoints.insert((module, line));
|
||||||
|
self.flags |= F_BREAK;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_module_breakpoint(&mut self, module: u16, line: u32) {
|
||||||
|
self.breakpoints.remove(&(module, line));
|
||||||
|
if self.breakpoints.is_empty() {
|
||||||
|
self.flags &= !F_BREAK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current_source_pos(&self) -> tb_frontend::SourcePos {
|
||||||
|
self.frames
|
||||||
|
.last()
|
||||||
|
.map(|f| tb_frontend::SourcePos {
|
||||||
|
source: f.source,
|
||||||
|
line: f.line,
|
||||||
|
column: f.column,
|
||||||
|
})
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current_module(&self) -> u16 {
|
||||||
|
self.module
|
||||||
|
.sources
|
||||||
|
.get(self.current_source_pos().source as usize)
|
||||||
|
.map_or(0, |s| s.module)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current_file(&self) -> &str {
|
||||||
|
self.module
|
||||||
|
.sources
|
||||||
|
.get(self.current_source_pos().source as usize)
|
||||||
|
.map_or(&self.module.name, |s| &s.path)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn current_line(&self) -> u32 {
|
pub fn current_line(&self) -> u32 {
|
||||||
self.frames.last().map(|f| f.line).unwrap_or(0)
|
self.frames.last().map(|f| f.line).unwrap_or(0)
|
||||||
}
|
}
|
||||||
@@ -843,26 +904,58 @@ impl Vm {
|
|||||||
.unwrap_or("")
|
.unwrap_or("")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Variableninspektion (Debugger): erst Locals des obersten Frames
|
/// Variableninspektion: lokale Namen vor Modulvariablen, explizite
|
||||||
/// (Referenzen werden aufgelöst), dann Modulvariablen. Ein
|
/// Typ-Suffixe vor einer eindeutigen Suche nach dem Basisnamen.
|
||||||
/// Typ-Suffix (`n%`, `s$`) wird toleriert — Slots tragen Basisnamen.
|
|
||||||
pub fn inspect(&self, name: &str) -> Option<Value> {
|
pub fn inspect(&self, name: &str) -> Option<Value> {
|
||||||
let name = name.trim_end_matches(['%', '&', '!', '#', '$', '@']);
|
fn matches(stored: &str, requested: &str) -> bool {
|
||||||
|
let suffixes = ['%', '&', '!', '#', '$', '@'];
|
||||||
|
stored.eq_ignore_ascii_case(requested)
|
||||||
|
|| (!(stored.ends_with(suffixes) && requested.ends_with(suffixes))
|
||||||
|
&& stored
|
||||||
|
.trim_end_matches(suffixes)
|
||||||
|
.eq_ignore_ascii_case(requested.trim_end_matches(suffixes)))
|
||||||
|
}
|
||||||
|
fn unique(mut ids: impl Iterator<Item = usize>) -> Option<usize> {
|
||||||
|
let first = ids.next()?;
|
||||||
|
ids.next().is_none().then_some(first)
|
||||||
|
}
|
||||||
if let Some(f) = self.frames.last() {
|
if let Some(f) = self.frames.last() {
|
||||||
let p = &self.module.procs[f.proc];
|
let p = &self.module.procs[f.proc];
|
||||||
for (i, n) in p.local_names.iter().enumerate() {
|
if let Some(i) = unique(
|
||||||
if n.eq_ignore_ascii_case(name) {
|
p.local_names
|
||||||
let v = self.locals[f.locals_base + i].clone();
|
.iter()
|
||||||
return Some(self.deref_for_inspect(v));
|
.enumerate()
|
||||||
}
|
.filter(|(_, n)| matches(n, name))
|
||||||
|
.map(|(i, _)| i),
|
||||||
|
) {
|
||||||
|
return Some(self.deref_for_inspect(self.locals[f.locals_base + i].clone()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (i, n) in self.module.global_names.iter().enumerate() {
|
let qualified = format!(
|
||||||
if n.eq_ignore_ascii_case(name) {
|
"{}!{name}",
|
||||||
return Some(self.deref_for_inspect(self.globals[i].clone()));
|
self.module.modules[self.current_module() as usize].0
|
||||||
}
|
);
|
||||||
|
if let Some(id) = unique(
|
||||||
|
self.module
|
||||||
|
.global_names
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_, n)| matches(n, name) || matches(n, &qualified))
|
||||||
|
.map(|(i, _)| i),
|
||||||
|
) {
|
||||||
|
return Some(self.globals[id].clone());
|
||||||
}
|
}
|
||||||
None
|
let id = unique(
|
||||||
|
self.module
|
||||||
|
.global_names
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_, n)| {
|
||||||
|
matches(n.split_once('!').map_or(n.as_str(), |(_, name)| name), name)
|
||||||
|
})
|
||||||
|
.map(|(i, _)| i),
|
||||||
|
)?;
|
||||||
|
Some(self.globals[id].clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Arrayelement inspizieren.
|
/// Arrayelement inspizieren.
|
||||||
@@ -993,8 +1086,15 @@ impl Vm {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if target.is_none() && self.module_handler != Handler::None {
|
if target.is_none() {
|
||||||
target = Some((0, self.module_handler));
|
for frame in self.frames.iter().rev() {
|
||||||
|
let module = self.module.sources[frame.source as usize].module as usize;
|
||||||
|
let handler = self.module_handlers[module];
|
||||||
|
if handler != Handler::None {
|
||||||
|
target = Some((0, handler));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let Some((depth, handler)) = target else {
|
let Some((depth, handler)) = target else {
|
||||||
return Some(self.error_event(code, line));
|
return Some(self.error_event(code, line));
|
||||||
@@ -1039,7 +1139,7 @@ impl Vm {
|
|||||||
let code = &self.module.procs[proc].code;
|
let code = &self.module.procs[proc].code;
|
||||||
let mut i = from + 1;
|
let mut i = from + 1;
|
||||||
while i < code.len() {
|
while i < code.len() {
|
||||||
if matches!(code[i], Instr::Stmt(_)) {
|
if matches!(code[i], Instr::Stmt(_) | Instr::InitStmt(_)) {
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
i += 1;
|
i += 1;
|
||||||
@@ -1189,7 +1289,7 @@ impl Vm {
|
|||||||
match cell {
|
match cell {
|
||||||
Value::Arr(a) => Ok(a.clone()),
|
Value::Arr(a) => Ok(a.clone()),
|
||||||
Value::Empty => {
|
Value::Empty => {
|
||||||
let lo = self.module.option_base as i32;
|
let lo = self.module.modules[self.current_module() as usize].1 as i32;
|
||||||
let bounds = vec![(lo, 10); dims as usize];
|
let bounds = vec![(lo, 10); dims as usize];
|
||||||
let arr = ArrayObj::new(elem.clone(), bounds, &self.module.udts)?;
|
let arr = ArrayObj::new(elem.clone(), bounds, &self.module.udts)?;
|
||||||
let handle = Rc::new(std::cell::RefCell::new(arr));
|
let handle = Rc::new(std::cell::RefCell::new(arr));
|
||||||
@@ -1223,7 +1323,9 @@ impl Vm {
|
|||||||
fn exec(&mut self, instr: Instr, pc: usize, host: &mut dyn Host) -> Result<Flow, RuntimeError> {
|
fn exec(&mut self, instr: Instr, pc: usize, host: &mut dyn Host) -> Result<Flow, RuntimeError> {
|
||||||
use Instr as I;
|
use Instr as I;
|
||||||
match instr {
|
match instr {
|
||||||
I::Stmt(line) => {
|
I::Source(_, _) => Ok(Flow::Normal),
|
||||||
|
I::Stmt(line) | I::InitStmt(line) => {
|
||||||
|
let initializing = matches!(instr, I::InitStmt(_));
|
||||||
if line == 0 {
|
if line == 0 {
|
||||||
if let Some(target) = self.start_pc.take() {
|
if let Some(target) = self.start_pc.take() {
|
||||||
self.frames[0].pc = target;
|
self.frames[0].pc = target;
|
||||||
@@ -1231,6 +1333,13 @@ impl Vm {
|
|||||||
}
|
}
|
||||||
let f = self.frames.last_mut().unwrap();
|
let f = self.frames.last_mut().unwrap();
|
||||||
f.line = line;
|
f.line = line;
|
||||||
|
if let Some(I::Source(source, column)) = pc
|
||||||
|
.checked_sub(1)
|
||||||
|
.and_then(|pc| self.module.procs[f.proc].code.get(pc))
|
||||||
|
{
|
||||||
|
f.source = *source;
|
||||||
|
f.column = *column;
|
||||||
|
}
|
||||||
f.last_stmt_pc = pc;
|
f.last_stmt_pc = pc;
|
||||||
// Zustellpunkt: anzeigen, wenn sich der Bildschirm geändert
|
// Zustellpunkt: anzeigen, wenn sich der Bildschirm geändert
|
||||||
// hat, und regelmäßig Ereignisse abholen. Das ist keine
|
// hat, und regelmäßig Ereignisse abholen. Das ist keine
|
||||||
@@ -1238,20 +1347,27 @@ impl Vm {
|
|||||||
// Größenänderungen kämen nie an.
|
// Größenänderungen kämen nie an.
|
||||||
self.tick_zaehler = self.tick_zaehler.wrapping_add(1);
|
self.tick_zaehler = self.tick_zaehler.wrapping_add(1);
|
||||||
self.forms.render(&mut self.rt.screen);
|
self.forms.render(&mut self.rt.screen);
|
||||||
if self.rt.screen.ist_veraendert() || self.tick_zaehler.is_multiple_of(1024) {
|
if !initializing
|
||||||
|
&& (self.rt.screen.ist_veraendert() || self.tick_zaehler.is_multiple_of(1024))
|
||||||
|
{
|
||||||
self.tick(host);
|
self.tick(host);
|
||||||
self.rt.screen.veraenderung_quittieren();
|
self.rt.screen.veraenderung_quittieren();
|
||||||
}
|
}
|
||||||
let erster_eintritt =
|
let erster_eintritt =
|
||||||
std::mem::take(&mut self.frames.last_mut().unwrap().handler_start);
|
std::mem::take(&mut self.frames.last_mut().unwrap().handler_start);
|
||||||
if !erster_eintritt && self.zustellen(host, Zustellpunkt::Anweisung) {
|
if !initializing
|
||||||
|
&& !erster_eintritt
|
||||||
|
&& self.zustellen(host, Zustellpunkt::Anweisung)
|
||||||
|
{
|
||||||
return Ok(Flow::Normal);
|
return Ok(Flow::Normal);
|
||||||
}
|
}
|
||||||
if self.flags != 0 {
|
if self.flags != 0 {
|
||||||
if self.flags & F_STEP != 0 {
|
if self.flags & F_STEP != 0 {
|
||||||
return Ok(Flow::Event(RunEvent::Stepped { line }));
|
return Ok(Flow::Event(RunEvent::Stepped { line }));
|
||||||
}
|
}
|
||||||
if self.flags & F_BREAK != 0 && self.breakpoints.contains(&line) {
|
if self.flags & F_BREAK != 0
|
||||||
|
&& self.breakpoints.contains(&(self.current_module(), line))
|
||||||
|
{
|
||||||
return Ok(Flow::Event(RunEvent::Breakpoint { line }));
|
return Ok(Flow::Event(RunEvent::Breakpoint { line }));
|
||||||
}
|
}
|
||||||
if self.flags & F_POLL != 0 && self.rt.abbruch {
|
if self.flags & F_POLL != 0 && self.rt.abbruch {
|
||||||
@@ -1328,6 +1444,10 @@ impl Vm {
|
|||||||
self.push(Value::Lng(v));
|
self.push(Value::Lng(v));
|
||||||
Ok(Flow::Normal)
|
Ok(Flow::Normal)
|
||||||
}
|
}
|
||||||
|
I::PushUdtId(id) => {
|
||||||
|
self.push(Value::Lng(id as i32));
|
||||||
|
Ok(Flow::Normal)
|
||||||
|
}
|
||||||
I::PushSng(v) => {
|
I::PushSng(v) => {
|
||||||
self.push(Value::Sng(v));
|
self.push(Value::Sng(v));
|
||||||
Ok(Flow::Normal)
|
Ok(Flow::Normal)
|
||||||
@@ -1716,13 +1836,25 @@ impl Vm {
|
|||||||
a.data[flat as usize] = v;
|
a.data[flat as usize] = v;
|
||||||
Ok(Flow::Normal)
|
Ok(Flow::Normal)
|
||||||
}
|
}
|
||||||
I::DimArr(global, slot, dims, elem) => {
|
I::DimArr(global, slot, dims, ref elem)
|
||||||
|
| I::CommonArr(global, slot, dims, ref elem) => {
|
||||||
|
let common = matches!(instr, I::CommonArr(..));
|
||||||
let bounds = self.pop_bounds(dims)?;
|
let bounds = self.pop_bounds(dims)?;
|
||||||
let cell = self.slot_value(global, slot);
|
let cell = self.slot_value(global, slot);
|
||||||
|
if common {
|
||||||
|
if let Value::Arr(array) = cell {
|
||||||
|
let array = array.borrow();
|
||||||
|
return if &array.elem == elem && array.dims == bounds {
|
||||||
|
Ok(Flow::Normal)
|
||||||
|
} else {
|
||||||
|
Err(RuntimeError::TYPE_MISMATCH)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
if !matches!(cell, Value::Empty) {
|
if !matches!(cell, Value::Empty) {
|
||||||
return Err(RuntimeError::DUPLICATE_DEFINITION);
|
return Err(RuntimeError::DUPLICATE_DEFINITION);
|
||||||
}
|
}
|
||||||
let arr = ArrayObj::new(elem, bounds, &self.module.udts)?;
|
let arr = ArrayObj::new(elem.clone(), bounds, &self.module.udts)?;
|
||||||
*self.slot_value(global, slot) = Value::Arr(Rc::new(std::cell::RefCell::new(arr)));
|
*self.slot_value(global, slot) = Value::Arr(Rc::new(std::cell::RefCell::new(arr)));
|
||||||
Ok(Flow::Normal)
|
Ok(Flow::Normal)
|
||||||
}
|
}
|
||||||
@@ -2293,7 +2425,8 @@ impl Vm {
|
|||||||
|
|
||||||
// ---- Fehlerbehandlung ----
|
// ---- Fehlerbehandlung ----
|
||||||
I::OnErrorGoto(t) => {
|
I::OnErrorGoto(t) => {
|
||||||
self.module_handler = Handler::Goto(t);
|
let module = self.current_module() as usize;
|
||||||
|
self.module_handlers[module] = Handler::Goto(t);
|
||||||
Ok(Flow::Normal)
|
Ok(Flow::Normal)
|
||||||
}
|
}
|
||||||
I::OnErrorLocal(t) => {
|
I::OnErrorLocal(t) => {
|
||||||
@@ -2301,7 +2434,8 @@ impl Vm {
|
|||||||
Ok(Flow::Normal)
|
Ok(Flow::Normal)
|
||||||
}
|
}
|
||||||
I::OnErrorDisable => {
|
I::OnErrorDisable => {
|
||||||
self.module_handler = Handler::None;
|
let module = self.current_module() as usize;
|
||||||
|
self.module_handlers[module] = Handler::None;
|
||||||
Ok(Flow::Normal)
|
Ok(Flow::Normal)
|
||||||
}
|
}
|
||||||
I::OnErrorLocalDisable => {
|
I::OnErrorLocalDisable => {
|
||||||
@@ -2312,7 +2446,8 @@ impl Vm {
|
|||||||
if local {
|
if local {
|
||||||
self.frames.last_mut().unwrap().local_handler = Handler::ResumeNext;
|
self.frames.last_mut().unwrap().local_handler = Handler::ResumeNext;
|
||||||
} else {
|
} else {
|
||||||
self.module_handler = Handler::ResumeNext;
|
let module = self.current_module() as usize;
|
||||||
|
self.module_handlers[module] = Handler::ResumeNext;
|
||||||
}
|
}
|
||||||
Ok(Flow::Normal)
|
Ok(Flow::Normal)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,20 +12,20 @@ pub mod bytecode;
|
|||||||
pub mod codegen;
|
pub mod codegen;
|
||||||
pub mod interp;
|
pub mod interp;
|
||||||
|
|
||||||
use tb_frontend::Diagnostic;
|
pub mod project;
|
||||||
|
pub use project::compile_project;
|
||||||
|
use tb_frontend::{source::SourceUnit, Diagnostic};
|
||||||
|
|
||||||
/// Komplette Übersetzung: Quelltext → Bytecode-Modul.
|
/// Einmodul-API auf derselben Pipeline wie vollständige Projekte.
|
||||||
/// Bei Diagnosen (Compile-Fehlern) wird kein Kompilat erzeugt.
|
|
||||||
pub fn compile_source(
|
pub fn compile_source(
|
||||||
module_name: &str,
|
module_name: &str,
|
||||||
source: &str,
|
source: &str,
|
||||||
) -> Result<bytecode::CompiledModule, Vec<Diagnostic>> {
|
) -> Result<bytecode::CompiledModule, Vec<Diagnostic>> {
|
||||||
let analysis = tb_frontend::analyze_source(module_name, source);
|
compile_source_with_forms(
|
||||||
if !analysis.diagnostics.is_empty() {
|
module_name,
|
||||||
return Err(analysis.diagnostics);
|
source,
|
||||||
}
|
&tb_frontend::forms::FormCatalog::default(),
|
||||||
let hir = analysis.hir.expect("diagnose-frei, aber kein HIR");
|
)
|
||||||
Ok(codegen::compile(&hir))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn compile_source_with_forms(
|
pub fn compile_source_with_forms(
|
||||||
@@ -33,11 +33,10 @@ pub fn compile_source_with_forms(
|
|||||||
source: &str,
|
source: &str,
|
||||||
forms: &tb_frontend::forms::FormCatalog,
|
forms: &tb_frontend::forms::FormCatalog,
|
||||||
) -> Result<bytecode::CompiledModule, Vec<Diagnostic>> {
|
) -> Result<bytecode::CompiledModule, Vec<Diagnostic>> {
|
||||||
let analysis = tb_frontend::analyze_source_with_forms(module_name, source, forms);
|
compile_project(
|
||||||
if !analysis.diagnostics.is_empty() {
|
module_name,
|
||||||
return Err(analysis.diagnostics);
|
&[SourceUnit::new(module_name, module_name, source)],
|
||||||
}
|
forms,
|
||||||
Ok(codegen::compile(
|
&[],
|
||||||
&analysis.hir.expect("diagnose-frei, aber kein HIR"),
|
)
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|||||||
682
crates/tb-vm/src/project.rs
Normal file
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:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -253,6 +253,10 @@ Das Dateiformat bleibt Version 1.
|
|||||||
|
|
||||||
## Kompilat: `.tbc` (neu, eigenes Format)
|
## Kompilat: `.tbc` (neu, eigenes Format)
|
||||||
|
|
||||||
Container für TBVM-Bytecode, Entwurf in [tbvm-design.md](tbvm-design.md).
|
Version 4 ist ein eigenständig ausführbarer Bytecode-Container; das vollständige
|
||||||
`tbc build` erzeugt wahlweise `.tbc` oder ein eigenständiges Executable
|
Format steht in [tbvm-design.md](tbvm-design.md). `tbc build app.mak` erzeugt
|
||||||
(Runner + eingebettetes `.tbc`).
|
`app.tbc`, `tbc run app.tbc` führt es ohne BAS-/FRM-/MAK-/Include-Quellen aus.
|
||||||
|
Quellorte, Prozedursignaturen und Formular-Anfangswerte einschließlich
|
||||||
|
Designzeit-Control-Arrays bleiben erhalten. Ältere Versionen 1–3 und unbekannte
|
||||||
|
Versionen werden mit Versionsangabe abgewiesen. Native Executable-Verpackung
|
||||||
|
ist noch nicht implementiert.
|
||||||
|
|||||||
@@ -31,6 +31,21 @@ einschließlich breiter Zeichen und Bildschirmränder. Die erneute Verifikation
|
|||||||
hat keine offenen Befunde im Change: 428 Workspace-Tests und alle vier
|
hat keine offenen Befunde im Change: 428 Workspace-Tests und alle vier
|
||||||
Review-Proben bestehen; eine Bildschirmmatrix deckt 3.360 Kombinationen ab.
|
Review-Proben bestehen; eine Bildschirmmatrix deckt 3.360 Kombinationen ab.
|
||||||
|
|
||||||
|
## Umsetzungsstand: Projektmodule und Kompilat
|
||||||
|
|
||||||
|
F09, F10, F11 und F24 sind im archivierten Change
|
||||||
|
[projektmodule-und-kompilat](../../openspec/changes/archive/2026-09-05-projektmodule-und-kompilat/proposal.md)
|
||||||
|
umgesetzt. Die zunächst sechs offenen Reviewbefunde und alle bei der
|
||||||
|
Nachprüfung zusätzlich reproduzierten Abweichungen sind behoben.
|
||||||
|
Die [abschließende Verifikation](../../openspec/changes/archive/2026-09-05-projektmodule-und-kompilat/review.md)
|
||||||
|
hat keine offenen Befunde im Change: 453 Workspace-Tests, alle sieben
|
||||||
|
unveränderten Reviewproben, Clippy, Format-/Spec-Prüfung und das echte
|
||||||
|
Mehrmodul-Compile-Budget bestehen. Alle zwölf Tasks sind abgeschlossen;
|
||||||
|
Die Hauptspecs wurden synchronisiert und der Change am 05.09.2026
|
||||||
|
archiviert. Details und
|
||||||
|
Szenariozuordnung: [Implementierungsnachweis](../../openspec/changes/archive/2026-09-05-projektmodule-und-kompilat/verification.md).
|
||||||
|
Die folgenden Befundtexte bleiben die historische Bestandsaufnahme.
|
||||||
|
|
||||||
## Prüfmethode und Grenzen
|
## Prüfmethode und Grenzen
|
||||||
|
|
||||||
- Alle Requirements/Szenarien gelesen und gegen Einstiegspunkte, Implementierung und vorhandene Tests abgeglichen. Die Matrix unten enthält jeden Requirement-Titel einmal.
|
- Alle Requirements/Szenarien gelesen und gegen Einstiegspunkte, Implementierung und vorhandene Tests abgeglichen. Die Matrix unten enthält jeden Requirement-Titel einmal.
|
||||||
@@ -113,7 +128,7 @@ Zwei Formulare mit je Text1 lassen sich laden, aber Form2!Text1.Text erzeugt Con
|
|||||||
|
|
||||||
Beleg: [crates/tb-frontend/src/sema.rs:949](../../crates/tb-frontend/src/sema.rs#L949). Vertrag: [openspec/specs/forms-objektmodell/spec.md:14](../../openspec/specs/forms-objektmodell/spec.md#L14), [openspec/specs/forms-objektmodell/spec.md:36](../../openspec/specs/forms-objektmodell/spec.md#L36), [openspec/specs/sprach-frontend/spec.md:251](../../openspec/specs/sprach-frontend/spec.md#L251).
|
Beleg: [crates/tb-frontend/src/sema.rs:949](../../crates/tb-frontend/src/sema.rs#L949). Vertrag: [openspec/specs/forms-objektmodell/spec.md:14](../../openspec/specs/forms-objektmodell/spec.md#L14), [openspec/specs/forms-objektmodell/spec.md:36](../../openspec/specs/forms-objektmodell/spec.md#L36), [openspec/specs/sprach-frontend/spec.md:251](../../openspec/specs/sprach-frontend/spec.md#L251).
|
||||||
|
|
||||||
Umsetzung: [projektmodule-und-kompilat](../../openspec/changes/projektmodule-und-kompilat/proposal.md).
|
Umsetzung: [projektmodule-und-kompilat](../../openspec/changes/archive/2026-09-05-projektmodule-und-kompilat/proposal.md).
|
||||||
|
|
||||||
### F10 — Quellorte und Debuggerziele verlieren die Modulidentität (hoch)
|
### F10 — Quellorte und Debuggerziele verlieren die Modulidentität (hoch)
|
||||||
|
|
||||||
@@ -121,7 +136,7 @@ Ein Overflow in lib.bas:2 wird über app.mak als Overflow in line 5 ohne Modulda
|
|||||||
|
|
||||||
Beleg: [crates/tb-cli/src/main.rs:184](../../crates/tb-cli/src/main.rs#L184). Vertrag: [openspec/specs/bytecode-kompilat/spec.md:46](../../openspec/specs/bytecode-kompilat/spec.md#L46), [openspec/specs/sprach-frontend/spec.md:96](../../openspec/specs/sprach-frontend/spec.md#L96), [openspec/specs/vm-ausfuehrung/spec.md:89](../../openspec/specs/vm-ausfuehrung/spec.md#L89), [openspec/specs/vm-fehlerbehandlung/spec.md:11](../../openspec/specs/vm-fehlerbehandlung/spec.md#L11).
|
Beleg: [crates/tb-cli/src/main.rs:184](../../crates/tb-cli/src/main.rs#L184). Vertrag: [openspec/specs/bytecode-kompilat/spec.md:46](../../openspec/specs/bytecode-kompilat/spec.md#L46), [openspec/specs/sprach-frontend/spec.md:96](../../openspec/specs/sprach-frontend/spec.md#L96), [openspec/specs/vm-ausfuehrung/spec.md:89](../../openspec/specs/vm-ausfuehrung/spec.md#L89), [openspec/specs/vm-fehlerbehandlung/spec.md:11](../../openspec/specs/vm-fehlerbehandlung/spec.md#L11).
|
||||||
|
|
||||||
Umsetzung: [projektmodule-und-kompilat](../../openspec/changes/projektmodule-und-kompilat/proposal.md).
|
Umsetzung: [projektmodule-und-kompilat](../../openspec/changes/archive/2026-09-05-projektmodule-und-kompilat/proposal.md).
|
||||||
|
|
||||||
### F11 — Formularwerte fehlen im serialisierten Kompilat (hoch)
|
### F11 — Formularwerte fehlen im serialisierten Kompilat (hoch)
|
||||||
|
|
||||||
@@ -129,7 +144,7 @@ Mit FormFile.apply liest die VM den initialen Text hello; nach Serialisierung un
|
|||||||
|
|
||||||
Beleg: [crates/tb-vm/src/bytecode.rs:484](../../crates/tb-vm/src/bytecode.rs#L484). Vertrag: [openspec/specs/bytecode-kompilat/spec.md:28](../../openspec/specs/bytecode-kompilat/spec.md#L28).
|
Beleg: [crates/tb-vm/src/bytecode.rs:484](../../crates/tb-vm/src/bytecode.rs#L484). Vertrag: [openspec/specs/bytecode-kompilat/spec.md:28](../../openspec/specs/bytecode-kompilat/spec.md#L28).
|
||||||
|
|
||||||
Umsetzung: [projektmodule-und-kompilat](../../openspec/changes/projektmodule-und-kompilat/proposal.md).
|
Umsetzung: [projektmodule-und-kompilat](../../openspec/changes/archive/2026-09-05-projektmodule-und-kompilat/proposal.md).
|
||||||
|
|
||||||
### F12 — ListIndex = -1 wird abgewiesen (mittel)
|
### F12 — ListIndex = -1 wird abgewiesen (mittel)
|
||||||
|
|
||||||
@@ -233,7 +248,7 @@ Die Hauptspec verlangt separate CODE/LINES-Abschnitte und vollständige Prozedur
|
|||||||
|
|
||||||
Beleg: [docs/tbvm-design.md:69](../../docs/tbvm-design.md#L69). Vertrag: [openspec/specs/bytecode-kompilat/spec.md:12](../../openspec/specs/bytecode-kompilat/spec.md#L12), [openspec/specs/bytecode-kompilat/spec.md:28](../../openspec/specs/bytecode-kompilat/spec.md#L28).
|
Beleg: [docs/tbvm-design.md:69](../../docs/tbvm-design.md#L69). Vertrag: [openspec/specs/bytecode-kompilat/spec.md:12](../../openspec/specs/bytecode-kompilat/spec.md#L12), [openspec/specs/bytecode-kompilat/spec.md:28](../../openspec/specs/bytecode-kompilat/spec.md#L28).
|
||||||
|
|
||||||
Umsetzung: [projektmodule-und-kompilat](../../openspec/changes/projektmodule-und-kompilat/proposal.md).
|
Umsetzung: [projektmodule-und-kompilat](../../openspec/changes/archive/2026-09-05-projektmodule-und-kompilat/proposal.md).
|
||||||
|
|
||||||
### F25 — FRM-Schreibvertrag enthält zwei unvereinbare Universalregeln (Klärung)
|
### F25 — FRM-Schreibvertrag enthält zwei unvereinbare Universalregeln (Klärung)
|
||||||
|
|
||||||
@@ -250,7 +265,7 @@ Umsetzung: [spezifikationsabgleich-und-regressionsnachweise](../../openspec/chan
|
|||||||
| 1 | [isam-transaktionen-und-dateinummern](../../openspec/changes/archive/2026-09-05-isam-transaktionen-und-dateinummern/proposal.md) | F01, F02, F03 | Eigenständig; bei gemeinsamen Dateien sequenziell integrieren |
|
| 1 | [isam-transaktionen-und-dateinummern](../../openspec/changes/archive/2026-09-05-isam-transaktionen-und-dateinummern/proposal.md) | F01, F02, F03 | Eigenständig; bei gemeinsamen Dateien sequenziell integrieren |
|
||||||
| 2 | [ereigniszustellung-und-hostgrenzen](../../openspec/changes/archive/2026-09-05-ereigniszustellung-und-hostgrenzen/proposal.md) | F04, F05, F06, F07, F08, F18, F20 | Eigenständig; bei gemeinsamen Dateien sequenziell integrieren |
|
| 2 | [ereigniszustellung-und-hostgrenzen](../../openspec/changes/archive/2026-09-05-ereigniszustellung-und-hostgrenzen/proposal.md) | F04, F05, F06, F07, F08, F18, F20 | Eigenständig; bei gemeinsamen Dateien sequenziell integrieren |
|
||||||
| 3 | [forms-zustand-und-bedienung](../../openspec/changes/archive/2026-09-05-forms-zustand-und-bedienung/proposal.md) | F12, F13 | Nach Ereigniszustellung |
|
| 3 | [forms-zustand-und-bedienung](../../openspec/changes/archive/2026-09-05-forms-zustand-und-bedienung/proposal.md) | F12, F13 | Nach Ereigniszustellung |
|
||||||
| 4 | [projektmodule-und-kompilat](../../openspec/changes/projektmodule-und-kompilat/proposal.md) | F09, F10, F11, F24 | Eigenständig; bei gemeinsamen Dateien sequenziell integrieren |
|
| 4 | [projektmodule-und-kompilat](../../openspec/changes/archive/2026-09-05-projektmodule-und-kompilat/proposal.md) | F09, F10, F11, F24 | Eigenständig; bei gemeinsamen Dateien sequenziell integrieren |
|
||||||
| 5 | [laufzeit-eingabe-und-systemstatus](../../openspec/changes/laufzeit-eingabe-und-systemstatus/proposal.md) | F14, F15, F16, F17, F19 | Eigenständig; bei gemeinsamen Dateien sequenziell integrieren |
|
| 5 | [laufzeit-eingabe-und-systemstatus](../../openspec/changes/laufzeit-eingabe-und-systemstatus/proposal.md) | F14, F15, F16, F17, F19 | Eigenständig; bei gemeinsamen Dateien sequenziell integrieren |
|
||||||
| 6 | [spezifikationsabgleich-und-regressionsnachweise](../../openspec/changes/spezifikationsabgleich-und-regressionsnachweise/proposal.md) | F21, F22, F23, F25 | Abschließende Gesamtabnahme nach den fünf Korrektur-Changes |
|
| 6 | [spezifikationsabgleich-und-regressionsnachweise](../../openspec/changes/spezifikationsabgleich-und-regressionsnachweise/proposal.md) | F21, F22, F23, F25 | Abschließende Gesamtabnahme nach den fünf Korrektur-Changes |
|
||||||
|
|
||||||
|
|||||||
@@ -126,33 +126,100 @@ Der erste Befehl lädt ein FRM-Korpusprogramm und prüft dessen `KM`-Ausgabe
|
|||||||
mit CaptureHost. Der zweite Baum darf crossterm, ratatui und signal-hook
|
mit CaptureHost. Der zweite Baum darf crossterm, ratatui und signal-hook
|
||||||
nicht enthalten.
|
nicht enthalten.
|
||||||
|
|
||||||
## `.tbc`-Container (Stand Phase 2, Formatversion 1)
|
## `.tbc`-Container (Formatversion 4, 2026-09-05)
|
||||||
|
|
||||||
```
|
`tbc build app.mak` erzeugt `app.tbc`; `tbc run app.tbc` benötigt die
|
||||||
Magic "TBC\0" · Formatversion u16 · Flags u16 · Abschnittsanzahl u32
|
BAS-/FRM-/MAK-/Include-Quellen nicht mehr. Dieselbe VM führt Quellprogramme
|
||||||
Abschnittstabelle: [ (Kennung 4 Byte, Offset u32, Länge u32) ]
|
und geladene Kompilate aus. Versionen 1–3 und unbekannte Versionen werden
|
||||||
Abschnitte:
|
mit Angabe der gefundenen und unterstützten Version abgewiesen.
|
||||||
MODN Modulname + OPTION BASE
|
|
||||||
CONS deduplizierter Stringpool
|
|
||||||
TYPS TYPE-Layouts (Feld-Initialisierungstypen)
|
|
||||||
GLOB globale Slots (Initialisierungstyp + Name für Debugger)
|
|
||||||
PROC Prozedurtabelle: Name, Parameteranzahl, Frame-Slots, Code
|
|
||||||
(Zeileninfo liegt inline im Code: Stmt-/SetErl-Instruktionen —
|
|
||||||
ersetzt den früher geplanten separaten LINES-Abschnitt)
|
|
||||||
DATA DATA-Konstanten (Rohtext + Quellzeile)
|
|
||||||
JMPT Sprungtabellen für ON n GOTO/GOSUB
|
|
||||||
FORMS (ab Phase 4: serialisierte .FRM-Beschreibungen)
|
|
||||||
```
|
|
||||||
|
|
||||||
Serialisierung mit einfachem eigenem Writer/Reader (kein serde nötig,
|
Alle Zahlen sind little-endian, Ganzzahlen mit Vorzeichen im Zweierkomplement,
|
||||||
Format bleibt stabil und dokumentiert); unbekannte Formatversionen
|
`f32`/`f64` nach IEEE 754. `bool` ist genau ein Byte (0/1). `string` bedeutet
|
||||||
werden mit Meldung abgewiesen. Instruktions-Encoding: 1 Opcode-Byte +
|
`u32` UTF-8-Bytelänge, gefolgt von genau diesen Bytes ohne Abschlussnull.
|
||||||
Operanden little-endian, Opcode-Bytes gruppenweise mit Lücken vergeben
|
Listen beginnen mit `u32` Elementanzahl; `[...]` bezeichnet die folgenden
|
||||||
(`crates/tb-vm/src/bytecode.rs` ist die normative Liste).
|
wiederholten Elemente. Tabellen-IDs sind nullbasiert.
|
||||||
|
|
||||||
|
Der Header besteht aus `TBC\0` (4 Byte), Version `u16 = 4`, Flags `u16 = 0`,
|
||||||
|
Abschnittsanzahl `u32 = 9`. Darauf folgen neun Einträge mit Kennung (4 ASCII-Bytes),
|
||||||
|
absolutem Datei-Offset `u32` und Bytelänge `u32`. Der Writer schreibt die
|
||||||
|
folgenden Abschnitte in dieser Reihenfolge; der Reader findet sie über die Tabelle.
|
||||||
|
|
||||||
|
| Kennung | Payload in Reihenfolge |
|
||||||
|
|---|---|
|
||||||
|
| `MODN` | Projektname `string`, Vorgabe-OPTION-BASE `u8` (wie Modul 0) |
|
||||||
|
| `SRCS` | Modulanzahl `u32`, `[Name string, OPTION BASE u8 (0/1)]`; Quellenanzahl `u32`, `[Modul-ID u16, Dateipfad string]` |
|
||||||
|
| `CONS` | Anzahl `u32`, `[string]`; Stringkonstantenpool |
|
||||||
|
| `TYPS` | Anzahl `u32`, `[TYPE-Name string, Feldanzahl u32, [TypeInit]]` |
|
||||||
|
| `GLOB` | Slotanzahl `u32`, `[TypeInit, Debuggername string]` |
|
||||||
|
| `PROC` | Prozeduranzahl `u32`, `[Prozedurbeschreibung gemäß unten]` |
|
||||||
|
| `DATA` | Anzahl `u32`, `[Rohtext string, physische Zeile u32]` |
|
||||||
|
| `JMPT` | Tabellenanzahl `u32`, `[Zielanzahl u32, [Instruktionsindex u32]]` |
|
||||||
|
| `OBJS` | Objekt-, Ereignis- und Anfangsdaten gemäß unten |
|
||||||
|
|
||||||
|
Eine `PROC`-Beschreibung enthält: Name `string`, Modul-ID `u16`, Art `u8`
|
||||||
|
(0 Hauptprogramm, 1 SUB, 2 FUNCTION, 3 DEF FN), Parameteranzahl `u32`,
|
||||||
|
`[Parametername string, HTy, Array bool, BYREF bool]`, Rückgabetyp-vorhanden
|
||||||
|
`bool`, gegebenenfalls `HTy`, Aufrufparameteranzahl `u16`, Local-Anzahl `u32`,
|
||||||
|
`[TypeInit, Local-Name string]`, Instruktionsanzahl `u32`, Code-Bytelänge `u32`,
|
||||||
|
Code-Bytes. Die beiden Parameteranzahlen müssen übereinstimmen. Prozedur 0
|
||||||
|
enthält die zusammengeführten Modulrümpfe; weitere Prozeduren behalten ihre
|
||||||
|
Modul-ID. In Mehrmodulprojekten sind Prozedur-/globale Debuggernamen
|
||||||
|
`Modul!Name` mit dem aufgelösten Typ-Suffix bei implizit/suffixdeklarierten
|
||||||
|
Variablen. AS-deklarierte Namen bleiben suffixlos. Die Inspektion unterscheidet
|
||||||
|
explizite Suffixe; ohne Suffix muss der Basisname eindeutig sein. Aufrufe und
|
||||||
|
Variablenzugriffe verwenden ausschließlich IDs.
|
||||||
|
|
||||||
|
`TypeInit` belegt immer 5 Byte: Tag `u8` und Zusatz `u32`. Tags: 0 INTEGER,
|
||||||
|
1 LONG, 2 SINGLE, 3 DOUBLE, 4 CURRENCY, 5 STRING, 6 fester STRING (Zusatz:
|
||||||
|
Länge), 7 UDT (Zusatz: TYPE-ID, maximal 65535), 8 leerer Slot. Sonst ist der
|
||||||
|
Zusatz 0. UDT-Felder dürfen nur frühere TYPE-IDs referenzieren.
|
||||||
|
`HTy` ist ein `u8`-Tag mit denselben skalaren Tags 0–7; nur bei Tag 6 folgen
|
||||||
|
`u32` Länge und bei Tag 7 `u16` TYPE-ID. Tag 8 bezeichnet FORM, Tag 9 CONTROL.
|
||||||
|
Arrays werden separat im Parametersatz markiert; ihr Local-Slot beginnt leer.
|
||||||
|
|
||||||
|
`OBJS` enthält Objektanzahl `u32`, `[Name string, Klasse u8, Elternname string
|
||||||
|
(leer bei Wurzel), Array bool, Eltern-ID u16 (FFFF = keine)]`, dann
|
||||||
|
Ereignisanzahl `u32`, `[Objekt-ID u16, Ereignisname string, Prozedur-ID u16]`,
|
||||||
|
Startformular-ID `u16` (FFFF = keines), Anzahl Anfangsinstanzen `u32`,
|
||||||
|
`[Objekt-ID u16, Designindex i32, Eigenschaftsanzahl u32,
|
||||||
|
[Eigenschafts-ID u16, PropertyValue]]`. Eltern-IDs zeigen auf frühere Objekte;
|
||||||
|
gleichnamige Controls anderer Formulare bleiben dadurch getrennt. Auch
|
||||||
|
Designinstanzen mit Index ungleich 0 und sämtliche FRM-Anfangswerte sind enthalten.
|
||||||
|
Eigenschaften stehen in aufsteigender ID-Reihenfolge.
|
||||||
|
|
||||||
|
`PropertyValue` beginnt mit Tag `u8`: 0 Integer (`i32`), 1 Single (`f32`),
|
||||||
|
2 String (`string`), 3 Boolean (`bool`), 4 Objekt (`bool` vorhanden, falls ja
|
||||||
|
`u16` Objekt-ID und `bool` Index-vorhanden, gegebenenfalls `i32` Index),
|
||||||
|
5 Integer-Array (`u32` Anzahl, `[i32]`). Klassen-IDs 0–18 entsprechen in
|
||||||
|
Reihenfolge Form, CheckBox, ComboBox, CommandButton, DirListBox, DriveListBox,
|
||||||
|
FileListBox, Frame, HScrollBar, Label, ListBox, Menu, OptionButton, PictureBox,
|
||||||
|
TextBox, Timer, VScrollBar, Screen, Spin. Eigenschafts-/Methoden-IDs sind
|
||||||
|
Indizes der jeweiligen Klassentabellen in `tb-frontend/src/forms.rs`.
|
||||||
|
|
||||||
|
Code und Quellorte liegen inline in `PROC`; es gibt keine `CODE`-/`LINES`-
|
||||||
|
oder `FORMS`-Abschnitte. `Source(Datei-ID, Spalte)` steht unmittelbar vor
|
||||||
|
`Stmt(physische Zeile)`. Die VM liest diese Zuordnung auch bei einem Sprung
|
||||||
|
direkt auf `Stmt`. Globale DIM-Anweisungen tragen `InitStmt(physische Zeile)`: Fehlerorte und
|
||||||
|
Debugger bleiben aktiv, Ereignisse werden bis nach den Initialisierungen
|
||||||
|
zurückgestellt. Zeile 0 ist eine synthetische Initialisierungsgrenze.
|
||||||
|
`SetErl(BASIC-Label)` aktualisiert getrennt davon die numerische Zeilennummer
|
||||||
|
für ERL. Include-Dateien behalten ihren Pfad und physischen Quellort sowie
|
||||||
|
die ID ihres einbindenden Moduls. `add_module_breakpoint(module, line)` und
|
||||||
|
`set_step(true)` halten an diesen Grenzen; `current_module`, `current_file`
|
||||||
|
und `current_source_pos` liefern den Halt-/Fehlerort. `inspect` akzeptiert
|
||||||
|
lokale, im aktuellen Modul sichtbare und explizit qualifizierte Variablennamen.
|
||||||
|
|
||||||
|
Der Reader prüft Version, Flags, eindeutige bekannte Abschnitte,
|
||||||
|
Längen/Überlappungen, vollständige Payloads, Typ-/Objekt-/Prozedur-/Quell-IDs,
|
||||||
|
Sprungziele und Anfangseigenschaften vor der Ausführung. Beschädigte Daten
|
||||||
|
liefern einen Ladefehler. Das Format ist kein Sandbox-Format für fremden
|
||||||
|
Programmcode; ein vollständiger Stack-/Kontrollflussverifizierer ist nicht Teil
|
||||||
|
von TBC. Ein gültiges vom Writer erzeugtes Kompilat lässt sich bytegleich
|
||||||
|
laden und erneut serialisieren.
|
||||||
|
|
||||||
## Eigenständige Executables
|
## Eigenständige Executables
|
||||||
|
|
||||||
`tbc build --exe` kopiert den vorkompilierten Runner (dieselbe
|
Geplant, noch nicht implementiert: `tbc build --exe` kopiert den vorkompilierten Runner (dieselbe
|
||||||
tb-vm/tb-runtime/tb-ui-Bibliothek wie die IDE) und hängt das `.tbc` als
|
tb-vm/tb-runtime/tb-ui-Bibliothek wie die IDE) und hängt das `.tbc` als
|
||||||
Ressource an (Anhängen ans Binary + Fußzeile mit Offset/Magic; portabel für
|
Ressource an (Anhängen ans Binary + Fußzeile mit Offset/Magic; portabel für
|
||||||
alle drei Plattformen). Alternative — `include_bytes!` + Cargo-Build beim
|
alle drei Plattformen). Alternative — `include_bytes!` + Cargo-Build beim
|
||||||
@@ -165,9 +232,19 @@ Nutzer — verworfen: erfordert Rust-Toolchain beim Anwender.
|
|||||||
Vorwärtsreferenzen (Prozeduren, Labels) über Fixup-Listen statt zweitem
|
Vorwärtsreferenzen (Prozeduren, Labels) über Fixup-Listen statt zweitem
|
||||||
Pass. Die Sprache ist dafür gemacht — das Vorbild kompilierte auf
|
Pass. Die Sprache ist dafür gemacht — das Vorbild kompilierte auf
|
||||||
1992er-Hardware gefühlt sofort.
|
1992er-Hardware gefühlt sofort.
|
||||||
- Modulweise inkrementell: `.BAS`/`.FRM` werden unabhängig zu Bytecode-
|
- `.BAS`/`.FRM` werden getrennt geparst, semantisch aufgelöst und zu Bytecode-
|
||||||
Einheiten übersetzt und beim Build nur zusammengebunden; die IDE
|
Einheiten übersetzt. Der Projektlink versetzt Slots, Prozeduren, Typen,
|
||||||
recompiliert nur geänderte Module (Hash über Quelltext).
|
Strings, DATA und Sprungtabellen. Modulrümpfe laufen in Projektreihenfolge;
|
||||||
|
ihre globalen Initialisierungen liegen vor dem ersten Ereignis. Eindeutige
|
||||||
|
externe Prozeduren werden mit dem DEFtype-Kontext ihres Ursprungs importiert;
|
||||||
|
prozedurlokale DEFtype-Angaben beeinflussen keine anderen Prozeduren.
|
||||||
|
Importierte Konstanten werden im Ursprung gefaltet, auch bei transitiven
|
||||||
|
Modulabhängigkeiten. TYPE-Abhängigkeiten behalten ihr ursprüngliches Layout;
|
||||||
|
interne qualifizierte Typnamen verhindern Konflikte mit abweichenden lokalen
|
||||||
|
Definitionen. Echte lokale Duplikate werden diagnostiziert. SHARED
|
||||||
|
bleibt modullokal. COMMON-Variablen desselben Blocks und derselben vollständigen
|
||||||
|
Variablenidentität teilen einen Slot; Elementtyp, Rang und bekannte Grenzen
|
||||||
|
müssen kompatibel sein. Ein inkrementeller IDE-Cache ist weiterhin geplant.
|
||||||
- Keine Optimierungspasses. Erlaubt sind nur Gratis-Optimierungen im
|
- Keine Optimierungspasses. Erlaubt sind nur Gratis-Optimierungen im
|
||||||
Codegen (Konstantenfaltung im Ausdruck, Peephole beim Emit).
|
Codegen (Konstantenfaltung im Ausdruck, Peephole beim Emit).
|
||||||
- Budget als Test verankern (Phase 2): Benchmark-Projekt (~50k Zeilen)
|
- Budget als Test verankern (Phase 2): Benchmark-Projekt (~50k Zeilen)
|
||||||
@@ -292,7 +369,7 @@ via `CINT`/`CLNG`/`CSNG`/`CDBL`/`CCUR` — identische Semantik):
|
|||||||
`-` voran und hängt stets ein Leerzeichen an; `STR$` nur das führende
|
`-` voran und hängt stets ein Leerzeichen an; `STR$` nur das führende
|
||||||
Leerzeichen/`-`, kein nachgestelltes.
|
Leerzeichen/`-`, kein nachgestelltes.
|
||||||
|
|
||||||
## Opcode-Satz (Feindesign, 2026-09-02)
|
## Opcode-Satz (Formatversion 4, 2026-09-05)
|
||||||
|
|
||||||
In-Memory führt die VM dekodierte Instruktionen (`Vec<Instr>`, ein Rust-
|
In-Memory führt die VM dekodierte Instruktionen (`Vec<Instr>`, ein Rust-
|
||||||
Enum mit eingebetteten Operanden — Wort-Dispatch, keine Byte-Dekodierung
|
Enum mit eingebetteten Operanden — Wort-Dispatch, keine Byte-Dekodierung
|
||||||
@@ -300,26 +377,205 @@ im heißen Pfad); die `.tbc`-Serialisierung bildet jede Instruktion auf
|
|||||||
1 Opcode-Byte + Operanden (little-endian) ab. Typkürzel: `I2`=INTEGER,
|
1 Opcode-Byte + Operanden (little-endian) ab. Typkürzel: `I2`=INTEGER,
|
||||||
`I4`=LONG, `R4`=SINGLE, `R8`=DOUBLE, `CY`=CURRENCY, `STR`=STRING.
|
`I4`=LONG, `R4`=SINGLE, `R8`=DOUBLE, `CY`=CURRENCY, `STR`=STRING.
|
||||||
|
|
||||||
| Gruppe | Instruktionen | Bemerkung |
|
Die folgende Tabelle führt jeden serialisierbaren Opcode einzeln auf;
|
||||||
|---|---|---|
|
Gesamtbytes schließen das Opcode-Byte ein. `bool`, `TypeInit` und primitive
|
||||||
| Anweisungsgrenze | `Stmt(line:u32)` · `SetErl(n:u32)` | `Stmt` prüft das Tick-Flag-Wort (Breakpoint/Einzelschritt/Abbruch), aktualisiert Zeile und Resume-Punkt; `SetErl` bei numerischen Zeilennummern |
|
Breiten sind im Containerabschnitt definiert. `CmpOp` belegt ein Byte:
|
||||||
| Konstanten/Stack | `PushInt(i16)` `PushLng(i32)` `PushSng(f32)` `PushDbl(f64)` `PushCur(i64)` `PushStr(pool:u16)` · `Dup` `Pop` | Stringpool dedupliziert |
|
0 gleich, 1 ungleich, 2 kleiner, 3 kleiner/gleich, 4 größer, 5 größer/gleich.
|
||||||
| Variablen | `LoadGlobal/StoreGlobal(u16)` · `LoadLocal/StoreLocal(u16)` · `MakeRefGlobal/MakeRefLocal(u16)` · `LoadRef/StoreRef(u16)` | Slots statisch aufgelöst; `*Ref` bedienen BYREF-Parameter (Referenz-Werte) |
|
Nicht aufgeführte Opcode-Bytes sind ungültig.
|
||||||
| Arrays | `DimGlobal/DimLocal{slot,dims,elem}` `RedimGlobal/RedimLocal{…}` `EraseGlobal/EraseLocal(u16)` · `LoadElem/StoreElem{dims:u8}` · `MakeRefElem{dims:u8}` | Grenzen auf dem Stack (lo/hi je Dimension als LONG); Indexprüfung → Fehler 9 |
|
|
||||||
| UDT | `LoadField/StoreField(u16)` · `MakeRefField(u16)` | Feldindex aus Typtabelle; verschachtelt durch Verkettung |
|
| Byte | Instruktion | Operanden in Reihenfolge | Gesamtbytes |
|
||||||
| Arithmetik | `Add/Sub/Mul{I2,I4,R4,R8,CY}` · `Neg{…}` · `Div{R4,R8}` · `IDiv/Mod{I2,I4}` · `PowR8` (`^` rechnet in DOUBLE, SINGLE-Ergebnis per Conv) | monomorph; Ganzzahl-/CY-Überlauf → Fehler 6, Division durch 0 → Fehler 11 |
|
|---|---|---|---|
|
||||||
| Konvertierung | `Conv_<src>_<dst>` für alle 20 geordneten Paare aus {I2,I4,R4,R8,CY} | Semantik exakt nach Matrix (Rundung/Überlauf) |
|
| `0x00` | `Stmt` | line: u32 | 5 |
|
||||||
| Logik | `Not/And/Or/Xor/Eqv/Imp{I2,I4}` | bitweise |
|
| `0x01` | `SetErl` | label: u32 | 5 |
|
||||||
| Vergleich | `Cmp{Eq,Ne,Lt,Le,Gt,Ge}{I2,I4,R4,R8,CY,STR}` | Ergebnis INTEGER −1/0 |
|
| `0x02` | `End` | — | 1 |
|
||||||
| Strings | `Concat` | `Rc<str>`, Kopien sind Pointer-Kopien |
|
| `0x03` | `StopInstr` | — | 1 |
|
||||||
| Kontrollfluss | `Jump(u32)` `JumpIfFalse(u32)` `JumpIfTrue(u32)` | Ziele absolut (Instruktionsindex) innerhalb der Code-Einheit; Fixups beim Emit |
|
| `0x04` | `SystemInstr` | — | 1 |
|
||||||
| GOSUB | `Gosub(u32)` `RetGosub` `RetGosubTo(u32)` | GOSUB-Stack pro Frame; leer → Fehler 3 |
|
| `0x05` | `Unsupported` | name: u16 | 3 |
|
||||||
| Berechnete Sprünge | `OnJump{table:u16,gosub:bool}` | Sprungtabellen im Modul; 0/zu groß: kein Sprung; negativ/>255 → Fehler 5 |
|
| `0x06` | `Source` | source: u32, column: u32 | 9 |
|
||||||
| Prozeduren | `Call(proc:u16)` `RetProc` `RetFn` `CallBuiltin{id:u16,argc:u8}` | Argumente links→rechts auf dem Stack (Werte oder Referenzen); `RetFn` transportiert den Funktionswert über den Frame-Abbau |
|
| `0x07` | `InitStmt` | line: u32 | 5 |
|
||||||
| Fehler | `OnErrorGoto(u32)` `OnErrorLocal(u32)` `OnErrorDisable` `OnErrorLocalDisable` · `Resume0` `ResumeNext` `ResumeLabel(u32)` · `RaiseError` | Scoping siehe unten |
|
| `0x10` | `PushInt` | value: i16 | 3 |
|
||||||
| DATA | `ReadData(art)` `Restore(u32)` | art 0 = String, 1 = Zahl (DOUBLE, dann Conv nach Matrix); Ende → Fehler 4, unkonvertierbar → Fehler 13 |
|
| `0x11` | `PushLng` | value: i32 | 5 |
|
||||||
| E/A | `Input{…}` sowie Builtins (`PRINT`-Familie über `CallBuiltin`) | Konsolenwirkung ausschließlich über das `Host`-Trait |
|
| `0x12` | `PushSng` | value: f32 | 5 |
|
||||||
| Ende | `End` `StopInstr` `SystemInstr` | Verhalten siehe Runner |
|
| `0x13` | `PushDbl` | value: f64 | 9 |
|
||||||
|
| `0x14` | `PushCur` | value: i64 | 9 |
|
||||||
|
| `0x15` | `PushStr` | pool: u16 | 3 |
|
||||||
|
| `0x16` | `Dup` | — | 1 |
|
||||||
|
| `0x17` | `Pop` | — | 1 |
|
||||||
|
| `0x18` | `PushUdtId` | type: u16 | 3 |
|
||||||
|
| `0x20` | `LoadGlobal` | slot: u16 | 3 |
|
||||||
|
| `0x21` | `StoreGlobal` | slot: u16 | 3 |
|
||||||
|
| `0x22` | `LoadLocal` | slot: u16 | 3 |
|
||||||
|
| `0x23` | `StoreLocal` | slot: u16 | 3 |
|
||||||
|
| `0x24` | `LoadRef` | slot: u16 | 3 |
|
||||||
|
| `0x25` | `StoreRef` | slot: u16 | 3 |
|
||||||
|
| `0x26` | `MakeRefGlobal` | slot: u16 | 3 |
|
||||||
|
| `0x27` | `MakeRefLocal` | slot: u16 | 3 |
|
||||||
|
| `0x28` | `MakeRefElem` | dimensions: u8 | 2 |
|
||||||
|
| `0x29` | `MakeRefField` | field: u16 | 3 |
|
||||||
|
| `0x30` | `LoadArr` | global: bool, slot: u16, dimensions: u8, element: TypeInit | 10 |
|
||||||
|
| `0x31` | `LoadElem` | dimensions: u8 | 2 |
|
||||||
|
| `0x32` | `StoreElem` | dimensions: u8 | 2 |
|
||||||
|
| `0x33` | `DimArr` | global: bool, slot: u16, dimensions: u8, element: TypeInit | 10 |
|
||||||
|
| `0x34` | `RedimArr` | global: bool, slot: u16, dimensions: u8, element: TypeInit | 10 |
|
||||||
|
| `0x35` | `EraseSlot` | global: bool, slot: u16 | 4 |
|
||||||
|
| `0x36` | `LoadField` | field: u16 | 3 |
|
||||||
|
| `0x37` | `StoreField` | field: u16 | 3 |
|
||||||
|
| `0x38` | `CopyRec` | — | 1 |
|
||||||
|
| `0x39` | `ArrBound` | lower: bool | 2 |
|
||||||
|
| `0x3A` | `FixStr` | length: u32 | 5 |
|
||||||
|
| `0x3B` | `CommonArr` | global: bool, slot: u16, dimensions: u8, element: TypeInit | 10 |
|
||||||
|
| `0x40` | `AddI2` | — | 1 |
|
||||||
|
| `0x41` | `AddI4` | — | 1 |
|
||||||
|
| `0x42` | `AddR4` | — | 1 |
|
||||||
|
| `0x43` | `AddR8` | — | 1 |
|
||||||
|
| `0x44` | `AddCy` | — | 1 |
|
||||||
|
| `0x45` | `SubI2` | — | 1 |
|
||||||
|
| `0x46` | `SubI4` | — | 1 |
|
||||||
|
| `0x47` | `SubR4` | — | 1 |
|
||||||
|
| `0x48` | `SubR8` | — | 1 |
|
||||||
|
| `0x49` | `SubCy` | — | 1 |
|
||||||
|
| `0x4A` | `MulI2` | — | 1 |
|
||||||
|
| `0x4B` | `MulI4` | — | 1 |
|
||||||
|
| `0x4C` | `MulR4` | — | 1 |
|
||||||
|
| `0x4D` | `MulR8` | — | 1 |
|
||||||
|
| `0x4E` | `MulCy` | — | 1 |
|
||||||
|
| `0x4F` | `NegI2` | — | 1 |
|
||||||
|
| `0x50` | `NegI4` | — | 1 |
|
||||||
|
| `0x51` | `NegR4` | — | 1 |
|
||||||
|
| `0x52` | `NegR8` | — | 1 |
|
||||||
|
| `0x53` | `NegCy` | — | 1 |
|
||||||
|
| `0x54` | `DivR4` | — | 1 |
|
||||||
|
| `0x55` | `DivR8` | — | 1 |
|
||||||
|
| `0x56` | `IDivI2` | — | 1 |
|
||||||
|
| `0x57` | `IDivI4` | — | 1 |
|
||||||
|
| `0x58` | `ModI2` | — | 1 |
|
||||||
|
| `0x59` | `ModI4` | — | 1 |
|
||||||
|
| `0x5A` | `PowR8` | — | 1 |
|
||||||
|
| `0x5B` | `Concat` | — | 1 |
|
||||||
|
| `0x60` | `ConvI2I4` | — | 1 |
|
||||||
|
| `0x61` | `ConvI2R4` | — | 1 |
|
||||||
|
| `0x62` | `ConvI2R8` | — | 1 |
|
||||||
|
| `0x63` | `ConvI2Cy` | — | 1 |
|
||||||
|
| `0x64` | `ConvI4I2` | — | 1 |
|
||||||
|
| `0x65` | `ConvI4R4` | — | 1 |
|
||||||
|
| `0x66` | `ConvI4R8` | — | 1 |
|
||||||
|
| `0x67` | `ConvI4Cy` | — | 1 |
|
||||||
|
| `0x68` | `ConvR4I2` | — | 1 |
|
||||||
|
| `0x69` | `ConvR4I4` | — | 1 |
|
||||||
|
| `0x6A` | `ConvR4R8` | — | 1 |
|
||||||
|
| `0x6B` | `ConvR4Cy` | — | 1 |
|
||||||
|
| `0x6C` | `ConvR8I2` | — | 1 |
|
||||||
|
| `0x6D` | `ConvR8I4` | — | 1 |
|
||||||
|
| `0x6E` | `ConvR8R4` | — | 1 |
|
||||||
|
| `0x6F` | `ConvR8Cy` | — | 1 |
|
||||||
|
| `0x70` | `ConvCyI2` | — | 1 |
|
||||||
|
| `0x71` | `ConvCyI4` | — | 1 |
|
||||||
|
| `0x72` | `ConvCyR4` | — | 1 |
|
||||||
|
| `0x73` | `ConvCyR8` | — | 1 |
|
||||||
|
| `0x80` | `NotI2` | — | 1 |
|
||||||
|
| `0x81` | `NotI4` | — | 1 |
|
||||||
|
| `0x82` | `AndI2` | — | 1 |
|
||||||
|
| `0x83` | `AndI4` | — | 1 |
|
||||||
|
| `0x84` | `OrI2` | — | 1 |
|
||||||
|
| `0x85` | `OrI4` | — | 1 |
|
||||||
|
| `0x86` | `XorI2` | — | 1 |
|
||||||
|
| `0x87` | `XorI4` | — | 1 |
|
||||||
|
| `0x88` | `EqvI2` | — | 1 |
|
||||||
|
| `0x89` | `EqvI4` | — | 1 |
|
||||||
|
| `0x8A` | `ImpI2` | — | 1 |
|
||||||
|
| `0x8B` | `ImpI4` | — | 1 |
|
||||||
|
| `0x90` | `CmpI2` | compare: CmpOp | 2 |
|
||||||
|
| `0x91` | `CmpI4` | compare: CmpOp | 2 |
|
||||||
|
| `0x92` | `CmpR4` | compare: CmpOp | 2 |
|
||||||
|
| `0x93` | `CmpR8` | compare: CmpOp | 2 |
|
||||||
|
| `0x94` | `CmpCy` | compare: CmpOp | 2 |
|
||||||
|
| `0x95` | `CmpStr` | compare: CmpOp | 2 |
|
||||||
|
| `0xA0` | `Jump` | target: u32 | 5 |
|
||||||
|
| `0xA1` | `JumpIfFalse` | target: u32 | 5 |
|
||||||
|
| `0xA2` | `JumpIfTrue` | target: u32 | 5 |
|
||||||
|
| `0xA3` | `Gosub` | target: u32 | 5 |
|
||||||
|
| `0xA4` | `RetGosub` | — | 1 |
|
||||||
|
| `0xA5` | `RetGosubTo` | target: u32 | 5 |
|
||||||
|
| `0xA6` | `OnJump` | table: u16, gosub: bool | 4 |
|
||||||
|
| `0xA7` | `Run` | kind: u8 | 2 |
|
||||||
|
| `0xB0` | `Call` | proc: u16, argc: u8 | 4 |
|
||||||
|
| `0xB1` | `RetProc` | — | 1 |
|
||||||
|
| `0xB2` | `RetFn` | — | 1 |
|
||||||
|
| `0xB3` | `CallBuiltin` | builtin: u16, argc: u8 | 4 |
|
||||||
|
| `0xB4` | `LoadObjectProperty` | object: u16, property: u16, indexed: bool | 6 |
|
||||||
|
| `0xB5` | `StoreObjectProperty` | object: u16, property: u16, indexed: bool | 6 |
|
||||||
|
| `0xB6` | `PushObject` | object: u16, indexed: bool | 4 |
|
||||||
|
| `0xB7` | `TypeOf` | class: u8 | 2 |
|
||||||
|
| `0xB8` | `ObjectMethod` | object: u16, method: u16, argc_flags: u8 | 6 |
|
||||||
|
| `0xB9` | `ObjectLoad` | object: u16, unload: bool, indexed: bool | 5 |
|
||||||
|
| `0xBA` | `LoadDynamicObjectProperty` | name: u16 | 3 |
|
||||||
|
| `0xBB` | `StoreDynamicObjectProperty` | name: u16 | 3 |
|
||||||
|
| `0xBC` | `LoadObjectIndexedProperty` | object: u16, property_flags: u16 | 5 |
|
||||||
|
| `0xBD` | `ObjectMethodFn` | object: u16, method: u16, argc_flags: u8 | 6 |
|
||||||
|
| `0xBE` | `StoreObjectIndexedProperty` | object: u16, property_flags: u16 | 5 |
|
||||||
|
| `0xC0` | `OnErrorGoto` | target: u32 | 5 |
|
||||||
|
| `0xC1` | `OnErrorLocal` | target: u32 | 5 |
|
||||||
|
| `0xC2` | `OnErrorDisable` | — | 1 |
|
||||||
|
| `0xC3` | `OnErrorLocalDisable` | — | 1 |
|
||||||
|
| `0xC4` | `OnErrorResumeNext` | local: bool | 2 |
|
||||||
|
| `0xC5` | `Resume0` | — | 1 |
|
||||||
|
| `0xC6` | `ResumeNext` | — | 1 |
|
||||||
|
| `0xC7` | `ResumeLabel` | target: u32 | 5 |
|
||||||
|
| `0xC8` | `RaiseError` | — | 1 |
|
||||||
|
| `0xC9` | `LoadErr` | — | 1 |
|
||||||
|
| `0xCA` | `LoadErl` | — | 1 |
|
||||||
|
| `0xCB` | `SetErr` | — | 1 |
|
||||||
|
| `0xD0` | `ReadData` | kind: u8 | 2 |
|
||||||
|
| `0xD1` | `Restore` | data: u32 | 5 |
|
||||||
|
| `0xD2` | `Input` | argc: u8, line_mode: bool, prompt: u16, question: bool | 6 |
|
||||||
|
| `0xD3` | `InputFile` | argc: u8, line_mode: bool | 3 |
|
||||||
|
| `0xD4` | `GetPut` | put: bool, has_record: bool, kind: u8, extra: u16 | 6 |
|
||||||
|
| `0xD5` | `Field` | count: u8 | 2 |
|
||||||
|
| `0xD6` | `LsetRset` | right: bool | 2 |
|
||||||
|
| `0xE0` | `TrapDefine` | kind: u8, target: u32 | 6 |
|
||||||
|
| `0xE1` | `TrapDisable` | kind: u8 | 2 |
|
||||||
|
| `0xE2` | `TrapSet` | kind: u8, state: u8 | 3 |
|
||||||
|
| `0xE3` | `EventSwitch` | enabled: bool | 2 |
|
||||||
|
| `0xE4` | `Doevents` | — | 1 |
|
||||||
|
| `0xE5` | `Sleep` | has_seconds: bool | 2 |
|
||||||
|
|
||||||
|
`target` bezeichnet einen Instruktionsindex innerhalb der aktuellen Prozedur;
|
||||||
|
`OnErrorGoto` zeigt in Prozedur 0. `table` referenziert `JMPT`, `proc` die
|
||||||
|
Prozedurtabelle, `pool`/`name`/`prompt` den Stringpool. `global=true` wählt
|
||||||
|
`GLOB`, sonst einen Local-Slot. Arrayindizes und Dimensionsgrenzen (je lo/hi)
|
||||||
|
liegen auf dem Stack. `ArrBound(lower)` unterscheidet LBOUND/UBOUND;
|
||||||
|
`MakeRef*` erzeugt Referenzen, `CopyRec` eine UDT-Wertkopie. `PushUdtId` legt
|
||||||
|
eine beim Linken versetzte TYPE-ID als LONG für ISAM auf den Stack.
|
||||||
|
|
||||||
|
`Call`/`CallBuiltin` konsumieren `argc` Argumente in Quellreihenfolge.
|
||||||
|
`builtin` ist die feste ID aus `tb-runtime/src/builtins.rs::ids`; `RetFn`
|
||||||
|
transportiert den Funktionswert. `ObjectMethod`/`ObjectMethodFn` verwenden
|
||||||
|
klassenlokale Methodenindizes, keine Stringpool-IDs. Das höchste Bit von
|
||||||
|
`argc_flags` kennzeichnet einen zusätzlichen Control-Arrayindex vor den
|
||||||
|
Argumenten; die unteren 7 Bit enthalten die Argumentanzahl. Bei indizierten
|
||||||
|
Eigenschaften markiert Bit 15 von `property_flags` den zusätzlichen
|
||||||
|
Control-Arrayindex; die unteren 15 Bit sind die Eigenschafts-ID. Der
|
||||||
|
Eigenschaftsindex liegt anschließend auf dem Stack. `indexed` bedeutet
|
||||||
|
ansonsten ebenfalls Control-Arrayindex auf dem Stack. Dynamische
|
||||||
|
Eigenschaftszugriffe verwenden den Namen im Stringpool und einen Objektwert
|
||||||
|
vom Stack. `TypeOf(class)` verwendet die oben genannten Klassen-IDs.
|
||||||
|
|
||||||
|
`TrapDefine`/`TrapDisable`/`TrapSet`: `kind` ist 0 KEY, 1 TIMER, 2 UEVENT,
|
||||||
|
3 SIGNAL; die Kennung bzw. TIMER-Dauer kommt vom Stack. `state` ist 0 ON,
|
||||||
|
1 OFF, 2 STOP. `Doevents` legt 0 ab; `Sleep(true)` nimmt Sekunden vom Stack.
|
||||||
|
`OnErrorResumeNext(local)` wählt Frame- oder Modulhandler. `RaiseError` und
|
||||||
|
`SetErr` lesen einen Fehlercode vom Stack; `LoadErr`/`LoadErl` legen ihren Wert ab.
|
||||||
|
`Run(kind)` ist 0 Neustart, 1 numerische Startzeile, 2 Dateiname; bei 1/2
|
||||||
|
liegt das Ziel auf dem Stack. `ReadData(kind)` ist 0 STRING, 1 DOUBLE.
|
||||||
|
|
||||||
|
`Input`/`InputFile` lesen `argc` Referenzen; bei Dateiinput liegt die
|
||||||
|
Dateinummer darunter. `line_mode` wählt LINE INPUT, `prompt=FFFF` bedeutet
|
||||||
|
keinen Prompt, `question` ergänzt das Fragezeichen. `GetPut` unterscheidet
|
||||||
|
GET/PUT und eine optionale Recordnummer auf dem Stack. `kind` ist 0 ohne
|
||||||
|
Variable, 1 INTEGER, 2 LONG, 3 SINGLE, 4 DOUBLE, 5 CURRENCY, 6 fester STRING,
|
||||||
|
7 UDT, 8 variabler STRING; `extra` enthält bei 6 die Länge, bei 7 die TYPE-ID,
|
||||||
|
sonst 0. `Field` konsumiert Dateinummer und `count` Paare (Länge, Referenz).
|
||||||
|
`LsetRset(right)` konsumiert Referenz und Wert.
|
||||||
|
|
||||||
FOR/NEXT wird ohne Spezial-Opcodes kompiliert: Grenz-/Schrittwert einmal
|
FOR/NEXT wird ohne Spezial-Opcodes kompiliert: Grenz-/Schrittwert einmal
|
||||||
in versteckte Slots ausgewertet; bei konstantem STEP wählt der Codegen
|
in versteckte Slots ausgewertet; bei konstantem STEP wählt der Codegen
|
||||||
@@ -334,6 +590,8 @@ als Vergleichs-/Sprungkette abgesenkt.
|
|||||||
- `ON ERROR GOTO x` setzt den **modulweiten** Handler — auch aus einer
|
- `ON ERROR GOTO x` setzt den **modulweiten** Handler — auch aus einer
|
||||||
Prozedur heraus. `ON LOCAL ERROR GOTO x` setzt einen **Frame-lokalen**
|
Prozedur heraus. `ON LOCAL ERROR GOTO x` setzt einen **Frame-lokalen**
|
||||||
Handler, der den modulweiten für die Dauer des Prozedurlaufs verdeckt.
|
Handler, der den modulweiten für die Dauer des Prozedurlaufs verdeckt.
|
||||||
|
Jedes Quellmodul besitzt einen eigenen Modulhandler; `GOTO 0` in einer
|
||||||
|
Bibliothek deaktiviert daher keinen Handler des aufrufenden Moduls.
|
||||||
`… GOTO 0` deaktiviert den jeweiligen Handler.
|
`… GOTO 0` deaktiviert den jeweiligen Handler.
|
||||||
- Fehlerfall: erst die Frame-Kette von innen nach außen nach
|
- Fehlerfall: erst die Frame-Kette von innen nach außen nach
|
||||||
LOCAL-Handlern absuchen, sonst modulweiter Handler; Stack wird bis zum
|
LOCAL-Handlern absuchen, sonst modulweiter Handler; Stack wird bis zum
|
||||||
@@ -365,3 +623,8 @@ denen die Original-Hilfe schweigt, sind am 2026-09-04 als
|
|||||||
`tests/compat/konvertierung.bas` ausführbar verankert. Sollte je eine
|
`tests/compat/konvertierung.bas` ausführbar verankert. Sollte je eine
|
||||||
belastbare Fundstelle auftauchen, die ihnen widerspricht, gilt sie —
|
belastbare Fundstelle auftauchen, die ihnen widerspricht, gilt sie —
|
||||||
die Entscheidungen sind Lückenfüller, keine Setzungen gegen das Vorbild.
|
die Entscheidungen sind Lückenfüller, keine Setzungen gegen das Vorbild.
|
||||||
|
|
||||||
|
`CommonArr` dimensioniert einen leeren COMMON-Array-Slot einmal. Bei bereits
|
||||||
|
vorhandenem Array prüft es Elementtyp und die ausgewerteten Grenzen, ohne
|
||||||
|
Werte oder Handle zu ersetzen (Konflikt: Fehler 13). Statisch erkennbare
|
||||||
|
COMMON-Typ-/Grenzenkonflikte werden bereits beim Linken diagnostiziert.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
## Why
|
## Why
|
||||||
|
|
||||||
Mehrere Module/Formulare werden zu einem Quelltext und globalen Objektraum zusammengelegt. Dadurch scheitern formularlokale Namen, Fehlerorte stimmen nicht und FRM-Anfangswerte fehlen nach dem Kompilat-Rundlauf (F09–F11); Containerdokumentation und Spec sind außerdem überholt (F24). Siehe [Review vom 05.09.2026](../../../docs/reviews/2026-09-05-openspec-code-review.md).
|
Mehrere Module/Formulare werden zu einem Quelltext und globalen Objektraum zusammengelegt. Dadurch scheitern formularlokale Namen, Fehlerorte stimmen nicht und FRM-Anfangswerte fehlen nach dem Kompilat-Rundlauf (F09–F11); Containerdokumentation und Spec sind außerdem überholt (F24). Siehe [Review vom 05.09.2026](../../../../docs/reviews/2026-09-05-openspec-code-review.md).
|
||||||
|
|
||||||
## What Changes
|
## What Changes
|
||||||
|
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
# Verifikation: projektmodule-und-kompilat
|
||||||
|
|
||||||
|
Geprüft am 05.09.2026: Arbeitsbaum auf Basis von `58b1f62`, alle sieben
|
||||||
|
Delta-Requirements mit 21 Szenarien, Tasks, D1–D4, Implementierung und
|
||||||
|
Abnahmenachweis. **Sechs offene Befunde; der Change ist nicht abnahmebereit.**
|
||||||
|
Die vorherige Aussage vollständiger Umsetzung wird durch diese Verifikation
|
||||||
|
korrigiert. Der Produktcode wurde bei diesem Review nicht verändert.
|
||||||
|
|
||||||
|
| Dimension | Ergebnis |
|
||||||
|
| --- | --- |
|
||||||
|
| Vollständigkeit | 7/7 Tasks sind angehakt und alle sieben Requirements haben Implementierungspfade. Die Befunde widerlegen jedoch die vollständige Erfüllung insbesondere von 1.1, 1.2 und 2.2; Checkboxen allein sind kein Abnahmenachweis. |
|
||||||
|
| Korrektheit | 437 bestehende Workspace-Tests bestanden; zusätzliche unabhängige Review-Proben: 1 bestanden, 6 fehlgeschlagen. Vier Kernfunktionslücken (CRITICAL), zwei weitere Vertragsabweichungen (WARNING). |
|
||||||
|
| Kohärenz | D4 und die FRM-Grundpfade von D3 sind umgesetzt. Quellorte und Modulkontexte gehen in weiteren Absenkungs-/Importpfaden verloren; der gemeinsame Objektraum berücksichtigt `$FORM` nicht vollständig. |
|
||||||
|
|
||||||
|
## CRITICAL — vor Archivierung beheben
|
||||||
|
|
||||||
|
### V1 — Quellorte und Breakpoints fehlen an Zweig- und Schleifengrenzen
|
||||||
|
|
||||||
|
**Priorität: hoch.** Delta „Zeilentabelle für Fehlerortung“ und
|
||||||
|
„Unterbrechbare step()-Ausführung“, D1, Tasks 1.1/2.2.
|
||||||
|
|
||||||
|
Die Fehlerortung funktioniert bei einfachen Anweisungen, nicht jedoch bei
|
||||||
|
weiteren ausführbaren Teilen zusammengesetzter Anweisungen:
|
||||||
|
|
||||||
|
| Probe | Erwartet | Beobachtet |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `IF 0 THEN` / `PRINT 1` / `ELSEIF 1/0 THEN` | Fehler 11 in Zeile 3 | Zeile 1 |
|
||||||
|
| `SELECT CASE 1` / `CASE 1/0` | Fehler 11 in Zeile 2 | Zeile 1 |
|
||||||
|
| `FOR i%=32767 TO 32767` / `PRINT 1` / `NEXT` | Überlauf beim NEXT in Zeile 3 | Zeile 2 |
|
||||||
|
| `DO` / `PRINT 1` / `LOOP UNTIL 1/0` | Fehler 11 in Zeile 3 | Zeile 2 |
|
||||||
|
| ELSEIF-Bedingung aus `cond.bi:1` | Include-Datei `cond.bi` | `main.bas:1:1` |
|
||||||
|
| Breakpoint auf ELSEIF in Zeile 3 | Halt vor Bedingung | Programm endet ohne Halt |
|
||||||
|
|
||||||
|
**Ursache:** [ELSEIF-Absenkung](../../../../crates/tb-frontend/src/sema.rs#L1937)
|
||||||
|
und [CASE-Absenkung](../../../../crates/tb-frontend/src/sema.rs#L2914) verwenden
|
||||||
|
den Quellort des äußeren Statements. NEXT-/LOOP-Positionen werden nicht
|
||||||
|
vollständig durch AST/HIR bis zur auszuführenden Grenze getragen. Der
|
||||||
|
[FOR-Codegenerator](../../../../crates/tb-vm/src/codegen.rs#L737) dokumentiert
|
||||||
|
selbst die bisherige Verschiebung einer genauen NEXT-Zuordnung auf Phase 5;
|
||||||
|
das widerspricht der hier übernommenen vollständigen Quellortanforderung.
|
||||||
|
|
||||||
|
**Korrektur:** Physische Positionen aller ausführbaren Zweig-/Schleifenteile
|
||||||
|
bereits beim Parsen erhalten und passende Source-/Stmt-Grenzen ausgeben.
|
||||||
|
Nicht nur Fehlermeldungen umbenennen: Breakpoints, Einzelschritt und RESUME
|
||||||
|
müssen dieselbe richtige Zuordnung verwenden. Numerische ERL-Labels separat
|
||||||
|
lassen. Probe `v1_zweige_und_schleifen_behalten_eigene_quellorte`.
|
||||||
|
|
||||||
|
### V2 — Deklarationsimport verändert Typkontext und Abhängigkeitsreihenfolge
|
||||||
|
|
||||||
|
**Priorität: hoch.** Projektpipeline und vollständige Prozedursignaturen,
|
||||||
|
D2, Tasks 1.2/2.1.
|
||||||
|
|
||||||
|
Drei zulässige Mehrmodulkonstellationen werden abgewiesen:
|
||||||
|
|
||||||
|
- MAIN ruft `Doppelt(3)` auf; LIB enthält `DEFINT A-Z` und
|
||||||
|
`FUNCTION Doppelt(x)`. Erwartete Ausgabe 6, tatsächlich
|
||||||
|
`Parameter type mismatch: MAIN!DOPPELT`.
|
||||||
|
- MAIN enthält `CONST A=3` und verwendet B; LIB enthält `CONST A=3` und
|
||||||
|
`CONST B=A+1`. Erwartet B=4, tatsächlich `LIB.bas:2:1: Invalid constant`.
|
||||||
|
- Beide Module enthalten denselben TYPE Inner. LIB definiert anschließend
|
||||||
|
TYPE Outer mit einem Feld `i AS Inner`; MAIN verwendet Outer.
|
||||||
|
Erwartet korrektes Layout und Feldausgabe 3, tatsächlich `Type not defined`
|
||||||
|
an der gültigen Outer-Definition sowie Folgefehler für Feldzugriffe.
|
||||||
|
|
||||||
|
**Ursache:** [import_declarations](../../../../crates/tb-vm/src/project.rs#L103)
|
||||||
|
kopiert rohe Signatur-ASTs ohne den DEFtype-Kontext des definierenden Moduls.
|
||||||
|
Fremde CONST-/TYPE-Knoten werden [vor sämtliche lokalen Deklarationen
|
||||||
|
gehängt](../../../../crates/tb-vm/src/project.rs#L158). Dadurch fehlen ihnen zu
|
||||||
|
diesem Zeitpunkt lokal vorhandene Abhängigkeiten. Die Fehlerposition in LIB
|
||||||
|
ist hier irreführend: Die dortige Originalreihenfolge ist gültig.
|
||||||
|
|
||||||
|
**Korrektur:** Exportierte Signaturen und Konstanten im Ursprungskontext
|
||||||
|
auflösen; abhängige Typdefinitionen mit erhaltenen Identitäten und gültiger
|
||||||
|
Abhängigkeitsreihenfolge verbinden. Lokale Definitionen und echte Duplikate
|
||||||
|
nicht wieder durch Text-Deduplizierung verstecken. Probe
|
||||||
|
`v2_importierte_deklarationen_behalten_kontext_und_abhaengigkeiten`.
|
||||||
|
|
||||||
|
### V3 — `$FORM`-Module teilen versehentlich dasselbe Formularobjekt
|
||||||
|
|
||||||
|
**Priorität: hoch.** Delta „Objektzugriffe in Grammatik und Semantik“
|
||||||
|
einschließlich `$FORM`, getrennte Formulare, D2/D3, Task 1.2.
|
||||||
|
|
||||||
|
A.bas und B.bas enthalten jeweils `'$FORM`. A setzt `Caption="a"`, ruft eine
|
||||||
|
SUB aus B auf, die dort `Caption="b"` setzt, und liest anschließend die
|
||||||
|
eigene Caption. **Erwartet: a. Beobachtet: b.** Das verknüpfte Objektverzeichnis
|
||||||
|
enthält nur SCREEN und A. Auch `B.Caption` aus A wird als unbekanntes Objekt
|
||||||
|
abgewiesen. Der Fehler besteht nach TBC-Rundlauf fort.
|
||||||
|
|
||||||
|
**Ursache:** [lower_with_forms](../../../../crates/tb-frontend/src/sema.rs#L404)
|
||||||
|
erweitert pro Modul eine lokale Katalogkopie um das jeweilige `$FORM`-Objekt.
|
||||||
|
Der Linker übernimmt [nur die Objekte des ersten Moduls](../../../../crates/tb-vm/src/project.rs#L188)
|
||||||
|
und versetzt die Objektoperanden weiterer Module nicht. Deren Form-ID zeigt
|
||||||
|
anschließend auf A.
|
||||||
|
|
||||||
|
**Korrektur:** Sämtliche FRM- und `$FORM`-Formulare vor der modulweisen
|
||||||
|
Semantik in einen gemeinsamen Katalog mit stabilen IDs aufnehmen oder die
|
||||||
|
Objekt-/Ereignisreferenzen beim Linken vollständig versetzen. Zugriffe,
|
||||||
|
Form_Load-Bindungen und gegenseitige Formreferenzen für beide Quellarten
|
||||||
|
prüfen. Probe `v3_form_metabefehle_erzeugen_getrennte_formularobjekte`.
|
||||||
|
|
||||||
|
### V4 — COMMON verliert Variablenidentität und initialisiert Arrays mehrfach
|
||||||
|
|
||||||
|
**Priorität: hoch.** Designrisiko SHARED/COMMON, D2, Task 1.2 und Erhalt der
|
||||||
|
bestehenden COMMON-Sprachsemantik.
|
||||||
|
|
||||||
|
- Bereits ein Modul mit `COMMON SHARED c%, c$` wird mit
|
||||||
|
`COMMON type mismatch: C` abgewiesen. Die beiden durch Typ-Suffixe
|
||||||
|
verschiedenen Variablen dürfen nebeneinander existieren.
|
||||||
|
- MAIN und LIB deklarieren dasselbe `COMMON SHARED a%(2)`. MAIN soll
|
||||||
|
`a%(1)=7` setzen und LIB den gemeinsamen Wert 7 ausgeben. Stattdessen
|
||||||
|
entsteht vor dem Nutzcode `LIB.bas:1:1: Runtime error 10: Duplicate definition`.
|
||||||
|
|
||||||
|
**Ursache:** Die [COMMON-Zuordnung](../../../../crates/tb-vm/src/project.rs#L270)
|
||||||
|
verwendet nur Block und untypisierten Basisnamen. Arrays haben zusätzlich
|
||||||
|
nur `TypeInit::Empty` als Slotvorbelegung; Elementtyp und Form gehen in der
|
||||||
|
Vergleichsprüfung verloren. Der Slot wird zwar zusammengeführt, aber
|
||||||
|
[beide DIM-Initialisierungen](../../../../crates/tb-vm/src/project.rs#L381)
|
||||||
|
werden ausgeführt.
|
||||||
|
|
||||||
|
**Korrektur:** Vollständige Variablenidentität und Arrayvertrag beim Binden
|
||||||
|
berücksichtigen. Kompatible gemeinsame Arrays einmal initialisieren;
|
||||||
|
inkompatible Deklarationen vor der Ausführung am ursprünglichen Quellort
|
||||||
|
ablehnen. Probe `v4_common_behaelt_typidentitaet_und_initialisiert_arrays_einmal`.
|
||||||
|
|
||||||
|
## WARNING — weitere Vertragsabweichungen
|
||||||
|
|
||||||
|
### V5 — LOAD/UNLOAD umgehen die explizite Containerauflösung
|
||||||
|
|
||||||
|
**Priorität: mittel.** Formularnamensraum und expliziter Containerzugriff,
|
||||||
|
D2, Task 1.2.
|
||||||
|
|
||||||
|
Bei zwei Formularen mit jeweils Text1 als Control-Array werden
|
||||||
|
`LOAD Form2!Text1(3)` und `UNLOAD Form2!Text1(3)` mit
|
||||||
|
`Unknown object 'FORM2!TEXT1'` abgewiesen. Property-Zugriffe und die Übergabe
|
||||||
|
desselben qualifizierten Controls an einen CONTROL-Parameter funktionieren.
|
||||||
|
|
||||||
|
**Ursache:** Der [LOAD-/UNLOAD-Pfad](../../../../crates/tb-frontend/src/sema.rs#L3370)
|
||||||
|
übergibt den gesamten Bang-Pfad an `find_object`; die bereits vorhandene
|
||||||
|
containerbezogene Auflösung wird hier nicht verwendet.
|
||||||
|
|
||||||
|
**Korrektur:** LOAD/UNLOAD über dieselbe Objekt-/Containerauflösung wie
|
||||||
|
Property- und Referenzzugriffe führen; gleichnamige Arrays beider Formulare
|
||||||
|
und dynamische Instanzen gemeinsam prüfen. Probe
|
||||||
|
`v5_load_und_unload_loesen_den_expliziten_container_auf`.
|
||||||
|
|
||||||
|
### V6 — Linkerdiagnosen verlieren Datei, Zeile und Spalte
|
||||||
|
|
||||||
|
**Priorität: mittel.** Delta „Diagnostik mit Vorbild-Meldungen“, D1, Task 1.1.
|
||||||
|
|
||||||
|
MAIN deklariert `SUB F(x%)`, LIB implementiert `SUB F(x$)`. Die Ablehnung ist
|
||||||
|
richtig, die Meldung jedoch `0:0: Parameter type mismatch: MAIN!F` ohne Datei.
|
||||||
|
Das ist eine quellbezogene Diagnose, kein Fehler ohne zuordenbare Quelle.
|
||||||
|
|
||||||
|
**Ursache:** [diagnostic](../../../../crates/tb-vm/src/project.rs#L17) erzeugt
|
||||||
|
`file=None` und `SourcePos::default()`. [Linkfehler](../../../../crates/tb-vm/src/project.rs#L66)
|
||||||
|
werden erst nach `locate_diagnostics` in dieses Format umgewandelt. Dasselbe
|
||||||
|
Problem betrifft COMMON-Konflikte und weitere symbolbezogene Linkfehler.
|
||||||
|
|
||||||
|
**Korrektur:** Ursprung der betroffenen Deklaration bzw. Aufrufstelle mit
|
||||||
|
in die Linker-Symbolinformationen übernehmen und strukturierte Diagnosen
|
||||||
|
mit tatsächlicher SourcePos zurückgeben. Keine pauschale Ersatzposition
|
||||||
|
1:1 eintragen. Probe `v6_linkerdiagnose_nennt_den_urspruenglichen_dateiort`.
|
||||||
|
|
||||||
|
## Szenarioabdeckung und positive Nachweise
|
||||||
|
|
||||||
|
Alle 21 vorhandenen Delta-Szenarien wurden abgeglichen. Die einfachen
|
||||||
|
Originalbeispiele besitzen Implementierungs- und Testnachweise; die oben
|
||||||
|
reproduzierten Randpfade widersprechen den umfassenderen Requirements.
|
||||||
|
|
||||||
|
| Requirement / Szenarien | Ergebnis |
|
||||||
|
| --- | --- |
|
||||||
|
| Monomorpher Opcode-Satz: gemischter Ausdruck, aufgelöste Namen | Vorhandene Codegen-/HIR-Tests bestanden. Alle 152 Opcodes samt Operandentypen und Encodingbreiten stimmen mit der Dokumentation überein. |
|
||||||
|
| TBC: Roundtrip, unbekannte Version, Formularwerte | CLI-/VM-Projekttests bestanden: bytegleicher Rundlauf, hello/world aus Designarrays 0/2 nach Entfernen sämtlicher BAS-/FRM-/MAK-/BI-Quellen, Ablehnung alter/höherer Versionen. V2/V3 begrenzen dennoch das vollständige Projektmodell. |
|
||||||
|
| Quellorte: Fehlerzeile, Bibliotheksfehler | `lib.bas:2` und verschachtelte Include-Fehler bestehen. Unabhängig zusätzlich Fehler in physischer Zeile 42 geprüft. V1 zeigt fehlende Zweig-/Schleifenorte. |
|
||||||
|
| Benannte Objekte: Lesen/Schreiben, keine Standardeigenschaft, Wertebereich, getrennte Controls | Bestehende Semantik-/VM-/Modelltests und neuer Zwei-FRM-Test bestanden; Check1.Value=3 bleibt Fehler 5. V3 betrifft weitere Formularquellen, V5 die weiteren qualifizierten Zugriffspfade. |
|
||||||
|
| Diagnosen: Position, Include-Diagnose | Lexer-/Parser-/Semantikpfad trägt Originalpositionen. V6 betrifft die anschließende Linkerphase. |
|
||||||
|
| Objektgrammatik/-semantik: unbekanntes Objekt/Eigenschaft, Typkonflikt, TYPEOF, gleiche Controls | Bestehende Tests bestanden. V3/V5 verletzen den allgemeinen Modul-/Containervertrag trotz grüner Property-Beispiele. |
|
||||||
|
| Unterbrechung: Breakpoint, Inspektion, gleiche Zeile in zwei Modulen | Unabhängige Probe: MAIN passiert Zeile 10, LIB hält in Zeile 10; qualifizierte Array-/UDT-Inspektion liefert 7/8; Einzelschritt meldet Spalte 5; ERR/ERL bleiben 6/200. V1 lässt Breakpoints an ELSEIF aus. |
|
||||||
|
|
||||||
|
Erneut ausgeführt:
|
||||||
|
|
||||||
|
- `cargo test --workspace`: **437 bestanden, 0 fehlgeschlagen, 1 ignoriert**.
|
||||||
|
Golden-Generator nicht ausgeführt, keine Sollausgaben geändert.
|
||||||
|
- [Review-Proben](review-probes.rs): **1 bestanden, 6 fehlgeschlagen**.
|
||||||
|
Jede fehlgeschlagene Probe entspricht einem der sechs Befunde; mehrere
|
||||||
|
verwandte Reproduktionen werden innerhalb einer Probe gesammelt.
|
||||||
|
- Zusätzlich zeitlich begrenzte CLI-Proben für die Befunde; temporäre
|
||||||
|
Quellverzeichnisse anschließend entfernt.
|
||||||
|
- `cargo bench -p tb-vm --bench compile`: 508 Zeilen **0,54 ms**, 49.760 Zeilen
|
||||||
|
in 20 verbundenen Modulen **67 ms**; beide Budgets erfüllt.
|
||||||
|
- Dokumentation: **152/152 Opcodes**, Operanden-/Gesamtbreiten und **9/9
|
||||||
|
Containerabschnitte** gegen den aktuellen Code abgeglichen.
|
||||||
|
- `cargo fmt --all -- --check`, `openspec validate projektmodule-und-kompilat --strict`
|
||||||
|
und `git diff --check`: bestanden.
|
||||||
|
- Clippy nicht nochmals ausgeführt: keine Produktcodeänderung seit dem
|
||||||
|
erfolgreichen Clippy-Lauf der Implementierungsabnahme. Kein neuer
|
||||||
|
interaktiver Originalsystemvergleich und keine Windows-/Linux-Testmatrix.
|
||||||
|
|
||||||
|
## Review-Proben reproduzieren
|
||||||
|
|
||||||
|
Im Repository-Wurzelverzeichnis ausführen. Der temporäre Treiber hält die
|
||||||
|
noch roten Vertragsproben außerhalb der normalen Workspace-Suite; ihre
|
||||||
|
Erwartungen beschreiben das Sollverhalten. Exit-Code ist bis zur Behebung 101.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python3 - <<'PY'
|
||||||
|
from pathlib import Path
|
||||||
|
import json, subprocess, tempfile, os
|
||||||
|
root = Path.cwd()
|
||||||
|
with tempfile.TemporaryDirectory(prefix='tb-project-review-') as folder:
|
||||||
|
p = Path(folder)
|
||||||
|
manifest = '[package]\nname="tb-project-review"\nversion="0.0.0"\nedition="2021"\n[workspace]\n'
|
||||||
|
manifest += '[[test]]\nname="probes"\npath=' + json.dumps(str(root / 'openspec/changes/archive/2026-09-05-projektmodule-und-kompilat/review-probes.rs')) + '\n[dependencies]\n'
|
||||||
|
for name in ['tb-ui', 'tb-vm', 'tb-runtime', 'tb-frontend']:
|
||||||
|
manifest += name + ' = {path=' + json.dumps(str(root / 'crates' / name)) + '}\n'
|
||||||
|
(p / 'Cargo.toml').write_text(manifest)
|
||||||
|
result = subprocess.run(['cargo', 'test', '--manifest-path', str(p / 'Cargo.toml'), '--offline', '--test', 'probes', '--', '--nocapture'], env={**os.environ, 'CARGO_TARGET_DIR': str(root / 'target/review-projektmodule')}, timeout=120)
|
||||||
|
raise SystemExit(result.returncode)
|
||||||
|
PY
|
||||||
|
```
|
||||||
|
|
||||||
|
**Abnahmeentscheidung:** V1–V4 vor Archivierung beheben; V5/V6 ebenfalls in
|
||||||
|
die vollständige Umsetzung aufnehmen. Keine zusätzlichen Stilvorschläge.
|
||||||
@@ -0,0 +1,266 @@
|
|||||||
|
//! Unabhängige Vertragsproben; Ausführung mit dem temporären Treiber aus review.md.
|
||||||
|
use tb_frontend::{
|
||||||
|
forms::FormCatalog,
|
||||||
|
source::{SourceSegment, SourceUnit},
|
||||||
|
};
|
||||||
|
use tb_runtime::{host::CaptureHost, value::Value};
|
||||||
|
use tb_vm::{
|
||||||
|
bytecode::CompiledModule,
|
||||||
|
compile_project,
|
||||||
|
interp::{RunEvent, Vm},
|
||||||
|
};
|
||||||
|
|
||||||
|
fn units(files: &[(&str, &str)]) -> Vec<SourceUnit> {
|
||||||
|
files
|
||||||
|
.iter()
|
||||||
|
.map(|(name, code)| SourceUnit::new(name, &format!("{name}.bas"), code))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
fn compile(files: &[(&str, &str)]) -> Result<CompiledModule, Vec<tb_frontend::Diagnostic>> {
|
||||||
|
compile_project("APP", &units(files), &FormCatalog::default(), &[])
|
||||||
|
}
|
||||||
|
fn run(module: CompiledModule) -> (RunEvent, String) {
|
||||||
|
let bytes = module.to_tbc();
|
||||||
|
let loaded = CompiledModule::from_tbc(&bytes).unwrap();
|
||||||
|
assert_eq!(bytes, loaded.to_tbc());
|
||||||
|
let mut vm = Vm::new(loaded);
|
||||||
|
let event = vm.run(&mut CaptureHost::default());
|
||||||
|
(event, tb_runtime::snapshot::text(&vm.rt.screen))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn v1_zweige_und_schleifen_behalten_eigene_quellorte() {
|
||||||
|
let mut failures = vec![];
|
||||||
|
for (name, code, line) in [
|
||||||
|
(
|
||||||
|
"ELSEIF",
|
||||||
|
"IF 0 THEN\nPRINT 1\nELSEIF 1/0 THEN\nPRINT 2\nEND IF",
|
||||||
|
3,
|
||||||
|
),
|
||||||
|
("CASE", "SELECT CASE 1\nCASE 1/0\nPRINT 2\nEND SELECT", 2),
|
||||||
|
("NEXT", "FOR i%=32767 TO 32767\nPRINT 1\nNEXT", 3),
|
||||||
|
("LOOP", "DO\nPRINT 1\nLOOP UNTIL 1/0", 3),
|
||||||
|
] {
|
||||||
|
let (event, _) = run(compile(&[("MAIN", code)]).unwrap());
|
||||||
|
if !matches!(event, RunEvent::Error { line: found, .. } if found == line) {
|
||||||
|
failures.push(format!("{name}: erwartet Zeile {line}, erhalten {event:?}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let source = SourceUnit {
|
||||||
|
name: "MAIN".into(),
|
||||||
|
segments: vec![
|
||||||
|
SourceSegment {
|
||||||
|
file: "main.bas".into(),
|
||||||
|
first_line: 1,
|
||||||
|
text: "IF 0 THEN\nPRINT 1\n".into(),
|
||||||
|
},
|
||||||
|
SourceSegment {
|
||||||
|
file: "cond.bi".into(),
|
||||||
|
first_line: 1,
|
||||||
|
text: "ELSEIF 1/0 THEN\nPRINT 2\n".into(),
|
||||||
|
},
|
||||||
|
SourceSegment {
|
||||||
|
file: "main.bas".into(),
|
||||||
|
first_line: 4,
|
||||||
|
text: "END IF\n".into(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
let mut vm = Vm::new(compile_project("APP", &[source], &FormCatalog::default(), &[]).unwrap());
|
||||||
|
let event = vm.run(&mut CaptureHost::default());
|
||||||
|
if vm.current_file() != "cond.bi" {
|
||||||
|
failures.push(format!(
|
||||||
|
"Include: {event:?} aus {} statt cond.bi",
|
||||||
|
vm.current_file()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
// Das gleiche Problem darf nicht nur durch Umbenennen der Fehlermeldung behoben werden.
|
||||||
|
let mut vm = Vm::new(
|
||||||
|
compile(&[("MAIN", "IF 0 THEN\nPRINT 1\nELSEIF 1 THEN\nPRINT 2\nEND IF")]).unwrap(),
|
||||||
|
);
|
||||||
|
vm.add_module_breakpoint(0, 3);
|
||||||
|
let event = vm.run(&mut CaptureHost::default());
|
||||||
|
if event != (RunEvent::Breakpoint { line: 3 }) {
|
||||||
|
failures.push(format!("Breakpoint ELSEIF: {event:?}"));
|
||||||
|
}
|
||||||
|
assert!(failures.is_empty(), "{}", failures.join("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn v2_importierte_deklarationen_behalten_kontext_und_abhaengigkeiten() {
|
||||||
|
let cases = [
|
||||||
|
(
|
||||||
|
"DEFINT",
|
||||||
|
"PRINT Doppelt(3)\nEND",
|
||||||
|
"DEFINT A-Z\nFUNCTION Doppelt(x)\nDoppelt=x*2\nEND FUNCTION",
|
||||||
|
" 6 \n",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"CONST",
|
||||||
|
"CONST A=3\nPRINT B\nEND",
|
||||||
|
"CONST A=3\nCONST B=A+1",
|
||||||
|
" 4 \n",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"TYPE",
|
||||||
|
"TYPE Inner\nx AS INTEGER\nEND TYPE\nDIM a AS Outer\na.i.x=3\nPRINT a.i.x\nEND",
|
||||||
|
"TYPE Inner\nx AS INTEGER\nEND TYPE\nTYPE Outer\ni AS Inner\nEND TYPE",
|
||||||
|
" 3 \n",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
let mut failures = vec![];
|
||||||
|
for (name, main, lib, output) in cases {
|
||||||
|
match compile(&[("MAIN", main), ("LIB", lib)]) {
|
||||||
|
Err(e) => failures.push(format!("{name}: {e:?}")),
|
||||||
|
Ok(module) => {
|
||||||
|
let actual = run(module);
|
||||||
|
if actual != (RunEvent::Ended, output.into()) {
|
||||||
|
failures.push(format!("{name}: {actual:?}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(failures.is_empty(), "{}", failures.join("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn v3_form_metabefehle_erzeugen_getrennte_formularobjekte() {
|
||||||
|
let module = compile(&[
|
||||||
|
(
|
||||||
|
"A",
|
||||||
|
"'$FORM\nCaption=\"a\"\nCALL SetupB\nPRINT Caption\nEND",
|
||||||
|
),
|
||||||
|
("B", "'$FORM\nSUB SetupB\nCaption=\"b\"\nEND SUB"),
|
||||||
|
])
|
||||||
|
.unwrap();
|
||||||
|
let names: Vec<_> = module.objects.iter().map(|o| o.name.clone()).collect();
|
||||||
|
let actual = run(module);
|
||||||
|
assert_eq!(
|
||||||
|
actual,
|
||||||
|
(RunEvent::Ended, "a\n".into()),
|
||||||
|
"Objekte: {names:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn v4_common_behaelt_typidentitaet_und_initialisiert_arrays_einmal() {
|
||||||
|
let mut failures = vec![];
|
||||||
|
for (name, files, want) in [
|
||||||
|
(
|
||||||
|
"Suffixe",
|
||||||
|
vec![(
|
||||||
|
"MAIN",
|
||||||
|
"COMMON SHARED c%, c$\nc%=7\nc$=\"ok\"\nPRINT c%;c$\nEND",
|
||||||
|
)],
|
||||||
|
" 7 ok\n",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Array",
|
||||||
|
vec![
|
||||||
|
("MAIN", "COMMON SHARED a%(2)\na%(1)=7\nCALL F\nEND"),
|
||||||
|
("LIB", "COMMON SHARED a%(2)\nSUB F\nPRINT a%(1)\nEND SUB"),
|
||||||
|
],
|
||||||
|
" 7 \n",
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
match compile(&files) {
|
||||||
|
Err(e) => failures.push(format!("{name}: {e:?}")),
|
||||||
|
Ok(module) => {
|
||||||
|
let actual = run(module);
|
||||||
|
if actual != (RunEvent::Ended, want.into()) {
|
||||||
|
failures.push(format!("{name}: {actual:?}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(failures.is_empty(), "{}", failures.join("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn v5_load_und_unload_loesen_den_expliziten_container_auf() {
|
||||||
|
use tb_frontend::forms::ObjectClass;
|
||||||
|
let mut catalog = FormCatalog::default();
|
||||||
|
for name in ["Form1", "Form2"] {
|
||||||
|
catalog.add(name, ObjectClass::Form, None, false);
|
||||||
|
catalog.add("Text1", ObjectClass::TextBox, Some(name), true);
|
||||||
|
}
|
||||||
|
let source = units(&[("MAIN", "LOAD Form2!Text1(3)\nForm2!Text1(3).Text=\"b\"\nPRINT Form2!Text1(3).Text\nUNLOAD Form2!Text1(3)\nEND")]);
|
||||||
|
let module = compile_project("APP", &source, &catalog, &[]).unwrap();
|
||||||
|
assert_eq!(run(module), (RunEvent::Ended, "b\n".into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn v6_linkerdiagnose_nennt_den_urspruenglichen_dateiort() {
|
||||||
|
let errors = compile(&[
|
||||||
|
("MAIN", "DECLARE SUB F(x%)\nCALL F(1)\nEND"),
|
||||||
|
("LIB", "SUB F(x$)\nEND SUB"),
|
||||||
|
])
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(
|
||||||
|
errors
|
||||||
|
.iter()
|
||||||
|
.any(|d| d.message.contains("Parameter type mismatch")),
|
||||||
|
"{errors:?}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
errors
|
||||||
|
.iter()
|
||||||
|
.all(|d| d.file.is_some() && d.pos.line > 0 && d.pos.column > 0),
|
||||||
|
"{errors:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn modulbreakpoint_rundlauf_inspektion_und_numerische_erl_bleiben_getrennt() {
|
||||||
|
let main = format!(
|
||||||
|
"DIM a%(2)\nTYPE T\nx AS INTEGER\nEND TYPE\nDIM r AS T\na%(1)=7\nr.x=8\n{}CALL F\nEND",
|
||||||
|
"\n\nn%=1\n"
|
||||||
|
);
|
||||||
|
let lib = format!("SUB F\n{}200 ERROR 6\nEND SUB", "\n".repeat(8));
|
||||||
|
let module = compile(&[("MAIN", &main), ("LIB", &lib)]).unwrap();
|
||||||
|
let bytes = module.to_tbc();
|
||||||
|
let loaded = CompiledModule::from_tbc(&bytes).unwrap();
|
||||||
|
assert_eq!(bytes, loaded.to_tbc());
|
||||||
|
let mut vm = Vm::new(loaded);
|
||||||
|
vm.add_module_breakpoint(1, 10);
|
||||||
|
let mut host = CaptureHost::default();
|
||||||
|
assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 10 });
|
||||||
|
assert_eq!(vm.current_module(), 1);
|
||||||
|
assert_eq!(vm.current_file(), "LIB.bas");
|
||||||
|
assert!(matches!(
|
||||||
|
vm.inspect_element("MAIN!a%", &[1]),
|
||||||
|
Some(Value::Int(7))
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
vm.inspect_field("MAIN!r", &[0]),
|
||||||
|
Some(Value::Int(8))
|
||||||
|
));
|
||||||
|
vm.remove_module_breakpoint(1, 10);
|
||||||
|
vm.set_step(true);
|
||||||
|
assert_eq!(vm.run(&mut host), RunEvent::Stepped { line: 10 });
|
||||||
|
assert_eq!(vm.current_source_pos().column, 5);
|
||||||
|
vm.set_step(false);
|
||||||
|
assert!(matches!(
|
||||||
|
vm.run(&mut host),
|
||||||
|
RunEvent::Error {
|
||||||
|
code: 6,
|
||||||
|
line: 10,
|
||||||
|
..
|
||||||
|
}
|
||||||
|
));
|
||||||
|
let source = format!("{}ERROR 6", "\n".repeat(41));
|
||||||
|
assert!(matches!(
|
||||||
|
run(compile(&[("MAIN", &source)]).unwrap()).0,
|
||||||
|
RunEvent::Error {
|
||||||
|
code: 6,
|
||||||
|
line: 42,
|
||||||
|
..
|
||||||
|
}
|
||||||
|
));
|
||||||
|
let module = compile(&[(
|
||||||
|
"MAIN",
|
||||||
|
"ON ERROR RESUME NEXT\n200 ERROR 6\nPRINT ERR;ERL\nEND",
|
||||||
|
)])
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(run(module), (RunEvent::Ended, " 6 200 \n".into()));
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# Verifikation: projektmodule-und-kompilat
|
||||||
|
|
||||||
|
Stand: 05.09.2026, Arbeitsbaum auf Basis von `58b1f62`.
|
||||||
|
**Keine offenen Befunde: 0 CRITICAL, 0 WARNING, 0 SUGGESTION.**
|
||||||
|
|
||||||
|
Die sechs Befunde des [ersten Reviews](review-initial.md) sind behoben.
|
||||||
|
Weitere bei der Nachprüfung reproduzierte Abweichungen wurden ebenfalls
|
||||||
|
korrigiert und mit Regressionen abgesichert. Der Change ist abnahmebereit.
|
||||||
|
|
||||||
|
| Dimension | Ergebnis |
|
||||||
|
| --- | --- |
|
||||||
|
| Vollständigkeit | 12/12 Tasks umgesetzt; alle 7 Requirements und 21 Delta-Szenarien abgeglichen. |
|
||||||
|
| Korrektheit | 453 Workspace-Tests bestanden, 0 fehlgeschlagen, 1 vorhandener Golden-Generator ignoriert. Die 7 unveränderten ursprünglichen Review-Proben bestehen auch im unabhängigen temporären Treiber. |
|
||||||
|
| Kohärenz | D1–D4 erfüllt: physische Quellorte, getrennte Scopes, selbstständiges TBC 4 und dokumentierte Inline-Instruktionen. |
|
||||||
|
|
||||||
|
## Geschlossene Befunde
|
||||||
|
|
||||||
|
| Befund | Korrektur und Nachweis |
|
||||||
|
| --- | --- |
|
||||||
|
| V1: Zweig-/Schleifenorte | Parser und HIR erhalten ELSEIF-, CASE-, NEXT-, LOOP- und WEND-Positionen. Codegen emittiert die tatsächlichen Anweisungsgrenzen. Fehler, Breakpoints, WHILE-Rücksprung, LOOP-RESUME, NEXT-RESUME NEXT und Include-Enden in Zeile 42/Spalte 3 sind geprüft. Numerische ERL-Labels bleiben getrennt. |
|
||||||
|
| V2: Importkontext | Exportierte Signaturen erhalten die DEFtype-Typen ihres Ursprungs. Konstanten werden dort gefaltet, einschließlich transitiver Abhängigkeiten über drei Module. TYPE-Abhängigkeiten werden geordnet und bei abweichenden lokalen Layouts intern qualifiziert. Echte lokale Duplikate bleiben Fehler. |
|
||||||
|
| V3: `$FORM`-Identitäten | FRM- und `$FORM`-Objekte stehen vor jeder Modulübersetzung in einem gemeinsamen Katalog. Getrennte Captions, gegenseitige Referenzen und unterschiedliche Form_Load-Bindungen sind auch für gemischte FRM/BAS-Projekte nach TBC-Rundlauf geprüft. |
|
||||||
|
| V4: COMMON | HIR übergibt Block, vollständigen Variablenschlüssel, Elementtyp und Arraygrenzen an den Linker. Typen, Ränge und bekannte Grenzen werden geprüft. `CommonArr` initialisiert einmal und prüft spätere dynamische Grenzen ohne Zurücksetzen des Handles oder der Werte. Offene Arraydeklarationen und gerundete Grenzen bleiben unterstützt. |
|
||||||
|
| V5: LOAD/UNLOAD | Beide Anweisungen verwenden dieselbe explizite Containerauflösung und Indexprüfung wie andere Objektzugriffe. `Form2!Text1(3)` wird geladen, beschrieben, gelesen und entladen, auch wenn Form1 ebenfalls Text1 besitzt. |
|
||||||
|
| V6: Diagnosen | Der Linker gibt strukturierte Diagnosen an den ursprünglichen Deklarationen zurück; Source-ID, Datei, Zeile und Spalte bleiben erhalten. Signaturkonflikte, COMMON-Konflikte im Include sowie doppelte Module sind geprüft. |
|
||||||
|
|
||||||
|
Die erweiterten Proben fanden zusätzlich folgende inzwischen behobene
|
||||||
|
Abweichungen: transitive Konstantenimports, fremde TYPE-Layouts bei lokal
|
||||||
|
abweichenden Definitionen, prozedurübergreifendes Durchsickern von DEFtype,
|
||||||
|
Verwechslung typisierter Debuggernamen, Verwechslung von Formnamen mit
|
||||||
|
suffixdeklarierten Variablen und fehlende Literalpositionen in Diagnosen.
|
||||||
|
Letzteres ist durch `CALL F("wrong")` in `args.bi:42:10` abgesichert.
|
||||||
|
Auch Konflikte erst in der dritten COMMON-Deklaration werden erkannt.
|
||||||
|
|
||||||
|
Die [16 dauerhaften Regressionstests](../../../../crates/tb-vm/tests/project_regressions.rs)
|
||||||
|
enthalten die sieben ursprünglichen Proben mit unveränderten Erwartungen
|
||||||
|
sowie neun ergänzende Tests mit mehreren verwandten Fällen. Die ursprüngliche
|
||||||
|
[Probe-Datei](review-probes.rs) wurde nicht geändert. Es wurden keine Goldens
|
||||||
|
neu erzeugt und keine Fehlererwartungen abgeschwächt.
|
||||||
|
|
||||||
|
## Abgleich aller Requirements und Szenarien
|
||||||
|
|
||||||
|
| Requirement | Szenarien / Nachweise |
|
||||||
|
| --- | --- |
|
||||||
|
| Monomorpher Opcode-Satz | Gemischter Ausdruck und aufgelöste Namen: vorhandene HIR-/Codegentests; numerische Projektverknüpfung. Alle 153 Opcode-Kennungen, Operandenfolgen und Encodingbreiten gegen die Dokumentation geprüft. |
|
||||||
|
| TBC-Container | Bytegleicher Roundtrip; klare Ablehnung alter/unbekannter Versionen; vollständige Formularwerte und Designarrays. CLI-Projekttests entfernen sämtliche BAS-/BI-/FRM-/MAK-Quellen und führen das TBC selbstständig aus. |
|
||||||
|
| Quellorte | Fehler in physischer Zeile 42; Fehler in lib.bas:2; verschachtelte Includes; zusätzliche ausführbare Zweig-/Schleifengrenzen und Initialisierungen. |
|
||||||
|
| Benannte Objekte | Eigenschaften lesen/schreiben; keine Standardeigenschaft; Check1.Value=3 erzeugt Fehler 5; voneinander unabhängige Werte gleichnamiger Controls. Zwei FRM- und gemischte FRM/`$FORM`-Projekte geprüft. |
|
||||||
|
| Diagnostik | 1-basierte Datei-/Zeilen-/Spaltenangaben in Lexer, Parser, Semantik und Linker; Include-Fehler einschließlich Literalargumenten. |
|
||||||
|
| Objektgrammatik/-semantik | Unbekanntes Objekt und unbekannte Eigenschaft benennen; Eigenschaftstyp prüfen; TYPEOF; expliziter Form1/Form2-Container bei gleichnamigen Controls. Qualifiziertes LOAD/UNLOAD nutzt dieselbe Auflösung. |
|
||||||
|
| Unterbrechung/Inspektion | Modulbezogener Breakpoint; scalar-, array- und UDT-Inspektion; Fortsetzen und Einzelschritt. Gleiche physische Zeile in MAIN und LIB bleibt getrennt, explizite Typ-Suffixe unterscheiden Variablen. ERR/ERL bleiben 6/200. |
|
||||||
|
|
||||||
|
Die konkreten ursprünglichen Szenario-/Testnamen und weitere Containerproben
|
||||||
|
stehen im [Implementierungsnachweis](verification.md), den
|
||||||
|
[VM-Projekttests](../../../../crates/tb-vm/tests/project.rs) und den
|
||||||
|
[CLI-Projekttests](../../../../crates/tb-cli/tests/project.rs).
|
||||||
|
|
||||||
|
## Abschließende Prüfläufe
|
||||||
|
|
||||||
|
- `cargo test --workspace`: **453 bestanden, 0 fehlgeschlagen, 1 ignoriert**.
|
||||||
|
- Unabhängiger Treiber aus [review-initial.md](review-initial.md): **7 bestanden**.
|
||||||
|
- `cargo clippy --workspace --all-targets --all-features -- -D warnings`: bestanden.
|
||||||
|
- `cargo fmt --all -- --check`: bestanden.
|
||||||
|
- `cargo bench -p tb-vm --bench compile`: 508 Zeilen **0,56 ms** (Budget 50 ms);
|
||||||
|
49.760 Zeilen in 20 verbundenen Modulen **77 ms** (Budget 1000 ms).
|
||||||
|
- Formatabgleich: **153/153 Opcodes**, Operandentypen und Gesamtbreiten;
|
||||||
|
**9/9 Abschnittskennungen** stimmen zwischen Writer, Reader und Dokumentation überein.
|
||||||
|
- `openspec validate projektmodule-und-kompilat --strict`: bestanden.
|
||||||
|
- `git diff --check`: bestanden.
|
||||||
|
|
||||||
|
Die Verifikation prüft den aktuellen Code und die gesamten Change-Artefakte
|
||||||
|
lokal und headless auf macOS. Ein neuer interaktiver Originalsystemvergleich
|
||||||
|
oder eine Windows-/Linux-Testmatrix war nicht Teil dieses Prüflaufs.
|
||||||
|
Die vier betroffenen Hauptspecs wurden am 05.09.2026 synchronisiert und
|
||||||
|
mit `openspec validate --specs --strict` validiert (15/15 bestanden).
|
||||||
|
Alle sieben Delta-Requirements stimmen vollständig mit den Hauptspecs
|
||||||
|
überein; alle zuvor vorhandenen Szenarien und unberührten Requirements
|
||||||
|
bleiben erhalten. Der Change ist unter
|
||||||
|
`openspec/changes/archive/2026-09-05-projektmodule-und-kompilat` archiviert.
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
## 1. Quellen und Scopes
|
||||||
|
|
||||||
|
- [x] 1.1 Quell-/Modulidentitäten durch Include-Expansion und Frontend erhalten; verifizieren mit Compile- und Runtimefehlern aus lib.bas:2 und verschachtelten Includes.
|
||||||
|
- [x] 1.2 Form-/Prozedurscopes korrekt auflösen; verifizieren mit zwei Form_Load-Prozeduren, zwei Text1-Controls und unterschiedlichen Form1!Text1/Form2!Text1-Werten sowie echten Duplikaten im selben Scope.
|
||||||
|
|
||||||
|
## 2. Kompilat und Debugger
|
||||||
|
|
||||||
|
- [x] 2.1 Vollständige Forms-Anfangsdaten, Quelltabelle und Prozedursignaturen serialisieren; verifizieren durch bytegleichen Kompilat-Rundlauf und gleiche Ausführung inklusive Designzeit-Control-Arrays.
|
||||||
|
- [x] 2.2 Modulbezogene Breakpoints/Quellortmeldungen ergänzen; verifizieren mit identischen physischen Zeilennummern in zwei Modulen, Include-Fehlern, Einzelschritt und unverändertem ERR/ERL.
|
||||||
|
- [x] 2.3 CLI-run für TBC anschließen und Versionsgrenze anheben; verifizieren mit build, Entfernen aller Quellen, run des Kompilats und klarer Ablehnung inkompatibler Versionen.
|
||||||
|
|
||||||
|
## 3. Abnahme
|
||||||
|
|
||||||
|
- [x] 3.1 Container und vollständige Opcode-/Operandentabelle dokumentieren; verifizieren mit Format-/Opcode-Abgleich, cargo test --workspace und dem Compile-Budget.
|
||||||
|
- [x] 3.2 Delta-Specs vollständig gegen die erhaltenen Szenarien prüfen; verifizieren mit openspec validate projektmodule-und-kompilat --strict und git diff --check.
|
||||||
|
|
||||||
|
## 4. Reviewbefunde vollständig beheben
|
||||||
|
|
||||||
|
- [x] 4.1 V1: Physische Quellpositionen aller Zweige und Schleifenenden erhalten; Fehler, Breakpoints und RESUME prüfen.
|
||||||
|
- [x] 4.2 V2/V6: Importierte Deklarationen im Ursprungskontext auflösen und Linkdiagnosen an Originalstellen melden.
|
||||||
|
- [x] 4.3 V3/V5: Gemeinsame Formidentitäten und qualifiziertes LOAD/UNLOAD korrigieren.
|
||||||
|
- [x] 4.4 V4: Vollständige COMMON-Identitäten und kompatible einmalige Arrayinitialisierung sicherstellen.
|
||||||
|
- [x] 4.5 Unveränderte Reviewproben, ergänzende Nachbarfälle und sämtliche Change-Szenarien erneut prüfen; alle weiteren Befunde beheben.
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
# Implementierungsnachweis — 05.09.2026
|
||||||
|
|
||||||
|
Change: `projektmodule-und-kompilat`, Ausgangscommit `58b1f62`.
|
||||||
|
|
||||||
|
**Abschließende Verifikation:** [review.md](review.md) bestätigt die Behebung
|
||||||
|
aller sechs ursprünglichen und aller zusätzlich reproduzierten Befunde.
|
||||||
|
Der [erste Review](review-initial.md) bleibt als historischer Nachweis erhalten.
|
||||||
|
|
||||||
|
Alle zwölf Tasks (sieben Implementierungs- und fünf Review-Aufgaben) sind
|
||||||
|
umgesetzt. F09, F10, F11 und F24 sind im Arbeitsbaum bearbeitet; die
|
||||||
|
Hauptspec-Synchronisierung und Archivierung sind am 05.09.2026 abgeschlossen. Die Abnahme
|
||||||
|
umfasst alle sieben Delta-Requirements mit ihren 21 Szenarien.
|
||||||
|
|
||||||
|
## Umsetzung
|
||||||
|
|
||||||
|
| Task / Befund | Umsetzung und Nachweis |
|
||||||
|
| --- | --- |
|
||||||
|
| 1.1 / F10 | `SourceUnit`/`SourceSegment` erhalten Modul, Datei und physische Zeile vor dem Parsen. Lexer- und Parserpositionen werden vor der Semantik den Originalquellen zugeordnet. CLI-Tests prüfen Compile-/Runtimefehler aus `lib.bas:2`, verschachtelten Includes und dem Codeabschnitt eines entfernten FRM. Auch leere Quellen funktionieren. |
|
||||||
|
| 1.2 / F09 | Module werden getrennt aufgelöst und danach numerisch verbunden. Lokale Prozeduren und Controls haben Vorrang, `Form!Control` sucht direkt im angegebenen Formular. Eltern-IDs ersetzen Namensvergleiche auch bei Frames, Menüs, Optionsgruppen und Geometrie. Zwei Form_Load-Prozeduren initialisieren ihre jeweiligen Text1-Controls unabhängig; echte SUB-/CONST-/TYPE-Duplikate im selben Modul bleiben Fehler. |
|
||||||
|
| 2.1 / F11 | TBC 4 enthält Module/Quellen, vollständige Parameter- und Rückgabetypen, Locals, Forms-Instanzen mit Eigenschaften, Designindizes und Eltern-IDs sowie das Startformular. `Vm::new` stellt diesen Anfangszustand her. Bytegleicher Rundlauf und Ausführung mit zwei Formularen und Designarrays 0/2 sind geprüft. |
|
||||||
|
| 2.2 / F10 | `Source` plus `Stmt` liefern Datei/Spalte/physische Zeile. `InitStmt` erhält dieselben Informationen für globale Initialisierungen, ohne vorzeitig Ereignisse zuzustellen. Breakpoints verwenden Modul-ID und Zeile. Einzelschritt, Variableninspektion und Fortsetzen bleiben möglich. Numerische Labels führen separat `SetErl`; DEF FN erhält den tatsächlichen Quellort. |
|
||||||
|
| 2.3 / F11 | `tbc run` lädt TBC direkt. Der CLI-Test baut ein Projekt, entfernt alle BAS-/BI-/FRM-/MAK-Dateien und vergleicht die Ausgabe des Kompilats mit der Quellausführung (`hello`, `world`, `7`). Ein zweiter Fall erhält nach Entfernen des FRM den Fehlerort `error.frm:6:1`. Versionen 1, 2, 3, 5 und 32767 werden ausdrücklich benannt und abgewiesen. |
|
||||||
|
| 3.1 / F24 | [TBVM-Format](../../../../docs/tbvm-design.md) dokumentiert alle neun Abschnitte und alle 153 Opcodes einzeln mit Operanden, Encodingbreiten und Kennungen. Der Compile-Benchmark übersetzt und verbindet nun tatsächlich ein Projekt mit 20 Modulen. |
|
||||||
|
| 3.2 | Alle Delta-Requirement-Blöcke behalten ihre bisherigen Szenarien. Neue Szenarien und die Implementierungsentscheidungen D1–D4 wurden gegen die unten genannten Tests und Codepfade abgeglichen. OpenSpec- und Diff-Prüfung bestanden. |
|
||||||
|
|
||||||
|
## Projektgrenzen und ergänzende Regressionen
|
||||||
|
|
||||||
|
Der Projektlink versetzt Prozedur-, Slot-, TYPE-, String-, DATA- und
|
||||||
|
Sprungtabellenreferenzen. Hauptprogramminitialisierungen aller Mitglieder
|
||||||
|
laufen vor der ersten Ereigniszustellung; die übrigen Modulrümpfe folgen der
|
||||||
|
Projektreihenfolge. Eindeutige externe Prozeduren werden als Deklarationen
|
||||||
|
importiert, lokale Namen bleiben erhalten. Eindeutige fremde Konstanten und
|
||||||
|
TYPEs bleiben als Kompatibilitätsregel sichtbar. Es wird kein Quelltext zur
|
||||||
|
Unterdrückung von Duplikaten gelöscht.
|
||||||
|
|
||||||
|
Geprüft sind wiederholt eingebundene Konstanten, gleichnamige lokale SUBs,
|
||||||
|
modullokales SHARED, gemeinsame COMMON-Skalare und unterschiedliches OPTION
|
||||||
|
BASE. Modulhandler werden getrennt gespeichert: ON ERROR GOTO 0 in der
|
||||||
|
Bibliothek deaktiviert den Handler des Aufrufers nicht. Bestehende ERR-/ERL-
|
||||||
|
und RESUME-Korpustests bestehen unverändert.
|
||||||
|
|
||||||
|
Der Signaturtest übergibt ein INTEGER-Array, einen UDT und einen Skalar an
|
||||||
|
eine Funktion in einem anderen Modul. Er prüft Rückgabetyp, Array-/BYREF-
|
||||||
|
Merkmale, Ausgabe und Rückwirkung einer UDT-Feldänderung auf den Aufrufer.
|
||||||
|
Die dabei gefundene Inkonsistenz zwischen UDT-Argument und Parameter wurde
|
||||||
|
im gemeinsamen Semantikpfad behoben. Bekannte externe Dialogdeklarationen
|
||||||
|
funktionieren auch mit qualifizierten Prozedurnamen. ISAM-OPEN verwendet mit
|
||||||
|
`PushUdtId` eine explizit versetzbare TYPE-Referenz; der Test vergleicht sie
|
||||||
|
nach dem Rundlauf mit dem tatsächlichen UDT-Index.
|
||||||
|
|
||||||
|
Der Loader weist abgeschnittene Dateien an jedem Byteoffset, ungültige
|
||||||
|
Flags/Abschnittsgrößen, doppelte Abschnitte sowie ungültige Quell-, Modul-,
|
||||||
|
Prozedur- und Elternreferenzen ab. Inkonsistente Signaturen, doppelte
|
||||||
|
Anfangseigenschaften, falsche Property-Typen und abweichende Designindizes
|
||||||
|
werden vor dem VM-Start geprüft. Das ersetzt keinen vollständigen
|
||||||
|
Stackverifizierer für beliebigen fremden Bytecode.
|
||||||
|
|
||||||
|
## Requirement- und Szenarioabgleich
|
||||||
|
|
||||||
|
Die neuen ausführbaren Nachweise liegen in
|
||||||
|
[VM-Projekttests](../../../../crates/tb-vm/tests/project.rs) und
|
||||||
|
[CLI-Projekttests](../../../../crates/tb-cli/tests/project.rs).
|
||||||
|
Die folgende Tabelle führt auch die erhaltenen Szenarien auf; bestehende
|
||||||
|
Tests werden durch neue Mehrmodul- und Include-Proben ergänzt.
|
||||||
|
|
||||||
|
| Requirement | Szenarien und Belege |
|
||||||
|
| --- | --- |
|
||||||
|
| Monomorpher Opcode-Satz | Gemischter Ausdruck: `codegen::tests::ausdruck_monomorph_mit_conv`; aufgelöste Namen: `sema::tests::hir_slots_aufgeloest` und die numerisch verbundenen Projektaufrufe. Vollständigkeit der Dokumentation zusätzlich gegen das `instrs!`-Makro geprüft. |
|
||||||
|
| .tbc-Containerformat | Roundtrip: mehrere VM-Projekttests vergleichen die gesamten Bytes; unbekannte Version: CLI-Versionstests; Formularwerte: CLI-Ausgabe `hello` und `world` nach Entfernen sämtlicher Quellen. |
|
||||||
|
| Zeilentabelle für Fehlerortung | Physische Fehlerzeile: Stmt-/Source-Zuordnung und CLI-Fehlerproben; Projektbibliothek: `lib.bas:2:5`, Include: `inner.bi:2:5`; zusätzlich InitStmt-, DEF-FN- und FRM-Codepositionen. |
|
||||||
|
| Formulare und Steuerelemente als benannte Objekte | Lesen/Schreiben: bestehender VM-Test `objektzugriff_laeuft_ueber_objekt_und_eigenschaftsindex`; keine Standardeigenschaft: `sema::tests::eigenschaftstyp_schreibbarkeit_bang_und_hir_index`; Wertebereich: `forms::tests::defaults_bereich_und_implizites_laden` prüft Check1.Value = 3 → Fehler 5; unabhängige Werte: zwei Formulare mit Text1 und eigenem Form_Load im neuen Projekttest. |
|
||||||
|
| Diagnostik mit Vorbild-Meldungen | Exakte Position: SourcePos bleibt 1-basiert; neue Tests prüfen Datei, Zeile und Spalte. Include-Diagnose: sowohl direktes SourceSegment als auch verschachtelte CLI-Expansion liefern die ursprüngliche Position des Typfehlers. |
|
||||||
|
| Objektzugriffe in Grammatik und Semantik | Unbekanntes Objekt/Eigenschaft und Eigenschaftstyp: bestehende Semantiktests für Text9.Text, Text1.Farbe und Text1.Text = 5; TYPEOF: `typeof_form_control_und_form_metabefehl`; gleichnamige Controls: Form1!Text1/Form2!Text1 sowie lokale Text1-Zugriffe beider Form_Load-Rümpfe. |
|
||||||
|
| Unterbrechbare step()-Ausführung | Breakpoint und Inspektion: vorhandene VM-Tests plus neuer Modulbreakpoint; gleiche Zeile in zwei Modulen: Modul A passiert Zeile 2, Modul B hält dort. Fortsetzen im Einzelschritt erhält `nested.bi:2`, physische Spalte 5 und getrenntes ERL = 200. |
|
||||||
|
|
||||||
|
## Ausgeführte Abnahme
|
||||||
|
|
||||||
|
- `cargo test --workspace`: **453 bestanden, 0 fehlgeschlagen, 1 ignoriert**.
|
||||||
|
Der ignorierte Golden-Generator wurde nicht ausgeführt; Sollausgaben blieben unverändert.
|
||||||
|
- `cargo clippy --workspace --all-targets --all-features -- -D warnings`: bestanden.
|
||||||
|
- `cargo fmt --all -- --check`: bestanden.
|
||||||
|
- `cargo bench -p tb-vm --bench compile`: 508 Zeilen **0,56 ms** (Budget 50 ms);
|
||||||
|
49.760 Zeilen in 20 verbundenen Modulen **77 ms** (Budget 1000 ms),
|
||||||
|
335.421 Instruktionen. Release-Messung auf diesem Rechner.
|
||||||
|
- Formatabgleich: **153/153 Opcodezeilen** einschließlich Operandentypen und
|
||||||
|
Bytebreiten sowie **9/9 Abschnittskennungen** gegen Writer/Reader abgeglichen.
|
||||||
|
- `openspec validate projektmodule-und-kompilat --strict`: bestanden.
|
||||||
|
- `git diff --check`: bestanden.
|
||||||
|
|
||||||
|
Die Abnahme ist eine lokale Code-/Testprüfung auf macOS; sie ist kein neuer
|
||||||
|
interaktiver Originalsystemvergleich und keine Windows-/Linux-Testmatrix.
|
||||||
|
|
||||||
|
## Reviewkorrekturen
|
||||||
|
|
||||||
|
Die ursprünglichen sieben Reviewproben bestehen unverändert, sowohl im
|
||||||
|
separaten Treiber als auch in der Workspace-Suite. Die dauerhafte Datei
|
||||||
|
`crates/tb-vm/tests/project_regressions.rs` ergänzt sie um neun Tests mit
|
||||||
|
verwandten Fällen. [review.md](review.md) ordnet die Korrekturen V1–V6 und
|
||||||
|
alle zusätzlich reproduzierten Abweichungen den Nachweisen zu.
|
||||||
|
|
||||||
|
`CommonArr` (`0x3B`) initialisiert gemeinsame Arrays einmal und prüft spätere
|
||||||
|
Laufzeitgrenzen auf Kompatibilität; vorab bekannte Konflikte meldet der
|
||||||
|
Linker an der ursprünglichen COMMON-Deklaration. Die neue Instruktion ist
|
||||||
|
in TBC 4 serialisiert, validiert und vollständig dokumentiert. Typ-Suffixe
|
||||||
|
bleiben in Debugger-Slotnamen erhalten. AST-Literale und sämtliche ausführbaren
|
||||||
|
Zweig-/Schleifenteile tragen ihre tatsächlichen Quellpositionen.
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
## 1. Quellen und Scopes
|
|
||||||
|
|
||||||
- [ ] 1.1 Quell-/Modulidentitäten durch Include-Expansion und Frontend erhalten; verifizieren mit Compile- und Runtimefehlern aus lib.bas:2 und verschachtelten Includes.
|
|
||||||
- [ ] 1.2 Form-/Prozedurscopes korrekt auflösen; verifizieren mit zwei Form_Load-Prozeduren, zwei Text1-Controls und unterschiedlichen Form1!Text1/Form2!Text1-Werten sowie echten Duplikaten im selben Scope.
|
|
||||||
|
|
||||||
## 2. Kompilat und Debugger
|
|
||||||
|
|
||||||
- [ ] 2.1 Vollständige Forms-Anfangsdaten, Quelltabelle und Prozedursignaturen serialisieren; verifizieren durch bytegleichen Kompilat-Rundlauf und gleiche Ausführung inklusive Designzeit-Control-Arrays.
|
|
||||||
- [ ] 2.2 Modulbezogene Breakpoints/Quellortmeldungen ergänzen; verifizieren mit identischen physischen Zeilennummern in zwei Modulen, Include-Fehlern, Einzelschritt und unverändertem ERR/ERL.
|
|
||||||
- [ ] 2.3 CLI-run für TBC anschließen und Versionsgrenze anheben; verifizieren mit build, Entfernen aller Quellen, run des Kompilats und klarer Ablehnung inkompatibler Versionen.
|
|
||||||
|
|
||||||
## 3. Abnahme
|
|
||||||
|
|
||||||
- [ ] 3.1 Container und vollständige Opcode-/Operandentabelle dokumentieren; verifizieren mit Format-/Opcode-Abgleich, cargo test --workspace und dem Compile-Budget.
|
|
||||||
- [ ] 3.2 Delta-Specs vollständig gegen die erhaltenen Szenarien prüfen; verifizieren mit openspec validate projektmodule-und-kompilat --strict und git diff --check.
|
|
||||||
@@ -17,6 +17,8 @@ implizite Konvertierungen erscheinen als explizite Konvertierungs-Opcodes
|
|||||||
an den vom HIR bestimmten Stellen. Der Opcode-Satz SHALL in
|
an den vom HIR bestimmten Stellen. Der Opcode-Satz SHALL in
|
||||||
docs/tbvm-design.md vollständig dokumentiert sein.
|
docs/tbvm-design.md vollständig dokumentiert sein.
|
||||||
|
|
||||||
|
Die Dokumentation SHALL sämtliche tatsächlich serialisierbaren Instruktionen einschließlich Forms, Ereignissteuerung und Dateioperationen mit Operanden und Encoding führen.
|
||||||
|
|
||||||
#### Scenario: Gemischter Ausdruck wird monomorph
|
#### Scenario: Gemischter Ausdruck wird monomorph
|
||||||
- **WHEN** `d# = i% + 1.5#` übersetzt wird (INTEGER-Variable, DOUBLE-Ziel)
|
- **WHEN** `d# = i% + 1.5#` übersetzt wird (INTEGER-Variable, DOUBLE-Ziel)
|
||||||
- **THEN** enthält der Bytecode einen Konvertierungs-Opcode INTEGER→DOUBLE und eine DOUBLE-Addition, keinen generischen Additions-Opcode
|
- **THEN** enthält der Bytecode einen Konvertierungs-Opcode INTEGER→DOUBLE und eine DOUBLE-Addition, keinen generischen Additions-Opcode
|
||||||
@@ -27,14 +29,17 @@ docs/tbvm-design.md vollständig dokumentiert sein.
|
|||||||
|
|
||||||
### Requirement: `.tbc`-Containerformat
|
### Requirement: `.tbc`-Containerformat
|
||||||
Das Kompilat SHALL als `.tbc`-Datei serialisierbar und wieder ladbar sein:
|
Das Kompilat SHALL als `.tbc`-Datei serialisierbar und wieder ladbar sein:
|
||||||
Magic `TBC\0`, Formatversion, Abschnittstabelle mit den Abschnitten
|
Magic `TBC\0`, Formatversion und dokumentierte Abschnittstabelle.
|
||||||
CONSTS (deduplizierter Konstantenpool), TYPES (TYPE-Layouts), PROCS
|
Der Container SHALL Quellen und Module, Konstantenpool, TYPE-Layouts,
|
||||||
(modul-qualifizierte Prozedurtabelle mit Signatur, Locals-Anzahl,
|
modulqualifizierte Prozeduren mit vollständiger Signatur, Locals und Code,
|
||||||
Code-Offset), CODE, DATA (`READ`/`RESTORE`-Segment) und LINES
|
DATA sowie die vollständigen Forms-Anfangsbeschreibungen führen.
|
||||||
(Zeilentabelle). Laden und erneutes Serialisieren MUST verlustfrei sein;
|
Code und Quellortinformationen SHALL inline in den Prozeduren liegen
|
||||||
|
dürfen; eigenständige CODE-/LINES-Abschnitte sind nicht erforderlich. Laden und erneutes Serialisieren MUST verlustfrei sein;
|
||||||
eine unbekannte Formatversion MUST mit einer klaren Fehlermeldung
|
eine unbekannte Formatversion MUST mit einer klaren Fehlermeldung
|
||||||
abgewiesen werden.
|
abgewiesen werden.
|
||||||
|
|
||||||
|
Ein aus FRM- oder Projektquellen gebautes Kompilat SHALL ohne erneuten Zugriff auf diese Quellen ausführbar sein und ihre Anfangseigenschaften und Designzeit-Control-Arrays bewahren.
|
||||||
|
|
||||||
#### Scenario: Roundtrip
|
#### Scenario: Roundtrip
|
||||||
- **WHEN** ein kompiliertes Modul als `.tbc` geschrieben und wieder geladen wird
|
- **WHEN** ein kompiliertes Modul als `.tbc` geschrieben und wieder geladen wird
|
||||||
- **THEN** ist das geladene Kompilat funktional identisch (gleiche Ausführung, gleiche Zeilenzuordnung)
|
- **THEN** ist das geladene Kompilat funktional identisch (gleiche Ausführung, gleiche Zeilenzuordnung)
|
||||||
@@ -43,15 +48,25 @@ abgewiesen werden.
|
|||||||
- **WHEN** eine `.tbc`-Datei mit höherer Formatversion geladen wird
|
- **WHEN** eine `.tbc`-Datei mit höherer Formatversion geladen wird
|
||||||
- **THEN** wird das Laden mit einer Meldung abgelehnt, die die Version nennt
|
- **THEN** wird das Laden mit einer Meldung abgelehnt, die die Version nennt
|
||||||
|
|
||||||
|
#### Scenario: Formularwerte nach Kompilat-Rundlauf
|
||||||
|
- **WHEN** ein Formular mit Text1.Text = "hello" kompiliert, gespeichert und ohne Quelldatei geladen wird
|
||||||
|
- **THEN** liefert der erste Zugriff auf Text1.Text weiterhin hello
|
||||||
|
|
||||||
### Requirement: Zeilentabelle für Fehlerortung
|
### Requirement: Zeilentabelle für Fehlerortung
|
||||||
Der Bytecode SHALL jede Anweisung ihrem Ursprung (Moduldatei, Zeile)
|
Der Bytecode SHALL jede Anweisung ihrem Ursprung (Moduldatei, Zeile)
|
||||||
zuordnen, sodass Laufzeitfehlermeldungen, `ERL`, Breakpoints und
|
zuordnen, sodass Laufzeitfehlermeldungen, `ERL`, Breakpoints und
|
||||||
Einzelschritt die Quellzeile exakt benennen können.
|
Einzelschritt die Quellzeile exakt benennen können.
|
||||||
|
|
||||||
|
Die Zuordnung SHALL auch nach Include-Expansion und bei mehreren Projektmitgliedern die ursprüngliche Datei und physische Zeile bewahren. Numerische BASIC-Zeilenlabels für ERL SHALL davon getrennt bleiben.
|
||||||
|
|
||||||
#### Scenario: Fehler nennt Zeile
|
#### Scenario: Fehler nennt Zeile
|
||||||
- **WHEN** in Zeile 42 eines Programms ein Laufzeitfehler ohne Handler auftritt
|
- **WHEN** in Zeile 42 eines Programms ein Laufzeitfehler ohne Handler auftritt
|
||||||
- **THEN** nennt die Fehlermeldung Zeile 42
|
- **THEN** nennt die Fehlermeldung Zeile 42
|
||||||
|
|
||||||
|
#### Scenario: Fehler aus Projektbibliothek
|
||||||
|
- **WHEN** lib.bas in physischer Zeile 2 einen Fehler auslöst und über app.mak aufgerufen wird
|
||||||
|
- **THEN** nennt die Meldung lib.bas und Zeile 2 statt einer verketteten Projektzeile
|
||||||
|
|
||||||
### Requirement: Instant-Compile-Budget
|
### Requirement: Instant-Compile-Budget
|
||||||
Die Übersetzung (Lexen bis Bytecode) SHALL ohne globale Analysepasses
|
Die Übersetzung (Lexen bis Bytecode) SHALL ohne globale Analysepasses
|
||||||
auskommen; Vorwärtsreferenzen werden über Fixups aufgelöst. Ein
|
auskommen; Vorwärtsreferenzen werden über Fixups aufgelöst. Ein
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ Zuweisung einen Fehler auslösen.
|
|||||||
- **WHEN** `Check1.Value = 3` ausgeführt wird
|
- **WHEN** `Check1.Value = 3` ausgeführt wird
|
||||||
- **THEN** tritt Laufzeitfehler 5 auf
|
- **THEN** tritt Laufzeitfehler 5 auf
|
||||||
|
|
||||||
|
#### Scenario: Getrennte Werte gleichnamiger Controls
|
||||||
|
- **WHEN** Form1!Text1.Text auf a und Form2!Text1.Text auf b gesetzt werden
|
||||||
|
- **THEN** liefern die beiden Zugriffe unabhängig a und b
|
||||||
|
|
||||||
### Requirement: Container-Zugriff über den Bang-Operator
|
### Requirement: Container-Zugriff über den Bang-Operator
|
||||||
`form!steuerelement.eigenschaft` SHALL ein Steuerelement eines benannten
|
`form!steuerelement.eigenschaft` SHALL ein Steuerelement eines benannten
|
||||||
Formulars ansprechen. Die Schreibweise MUST NOT mit dem Typsuffix `!`
|
Formulars ansprechen. Die Schreibweise MUST NOT mit dem Typsuffix `!`
|
||||||
|
|||||||
@@ -99,10 +99,16 @@ tragen; die Meldungstexte SHALL dem Katalog des Vorbilds folgen, wo eine
|
|||||||
Entsprechung existiert (z. B. „Type mismatch", „Duplicate definition",
|
Entsprechung existiert (z. B. „Type mismatch", „Duplicate definition",
|
||||||
„Label not defined", „Subprogram not defined", „Overflow").
|
„Label not defined", „Subprogram not defined", „Overflow").
|
||||||
|
|
||||||
|
Bei Projekten und Includes SHALL jede Diagnose den ursprünglichen Dateinamen mit dessen physischer Zeile und Spalte tragen.
|
||||||
|
|
||||||
#### Scenario: Position in der Diagnose
|
#### Scenario: Position in der Diagnose
|
||||||
- **WHEN** in Zeile 3 ein Typkonflikt auftritt
|
- **WHEN** in Zeile 3 ein Typkonflikt auftritt
|
||||||
- **THEN** nennt die Diagnose Zeile 3 und die Spalte des Ausdrucks
|
- **THEN** nennt die Diagnose Zeile 3 und die Spalte des Ausdrucks
|
||||||
|
|
||||||
|
#### Scenario: Include-Diagnose
|
||||||
|
- **WHEN** ein eingebundenes Include einen Typfehler enthält
|
||||||
|
- **THEN** nennt die Diagnose die Include-Datei und ihre eigene Quellposition
|
||||||
|
|
||||||
### Requirement: Typisiertes HIR als Sema-Ausgabe
|
### Requirement: Typisiertes HIR als Sema-Ausgabe
|
||||||
Die semantische Analyse SHALL neben den Diagnosen einen typisierten,
|
Die semantische Analyse SHALL neben den Diagnosen einen typisierten,
|
||||||
abgesenkten Zwischenbaum (HIR) liefern, der die Codegen-Eingabe bildet:
|
abgesenkten Zwischenbaum (HIR) liefern, der die Codegen-Eingabe bildet:
|
||||||
@@ -259,6 +265,8 @@ Basis weder ein benutzerdefinierter Typ noch ein bekanntes Objekt ist,
|
|||||||
MUST eine Diagnose erzeugen, die Basis und Eigenschaft namentlich nennt;
|
MUST eine Diagnose erzeugen, die Basis und Eigenschaft namentlich nennt;
|
||||||
er MUST NOT zu einer impliziten Variablen mit Punkt im Namen werden.
|
er MUST NOT zu einer impliziten Variablen mit Punkt im Namen werden.
|
||||||
|
|
||||||
|
Gleichnamige Steuerelemente unterschiedlicher Formulare SHALL im jeweiligen Formularnamensraum aufgelöst werden; die explizite Containerangabe SHALL vor der lokalen Namenssuche wirken.
|
||||||
|
|
||||||
#### Scenario: Unbekanntes Objekt wird benannt
|
#### Scenario: Unbekanntes Objekt wird benannt
|
||||||
- **WHEN** ein Modul `Text9.Text = "a"` ohne Steuerelement `Text9` enthält
|
- **WHEN** ein Modul `Text9.Text = "a"` ohne Steuerelement `Text9` enthält
|
||||||
- **THEN** nennt die Diagnose `Text9` als unbekanntes Objekt statt „Type mismatch" zu melden
|
- **THEN** nennt die Diagnose `Text9` als unbekanntes Objekt statt „Type mismatch" zu melden
|
||||||
@@ -275,6 +283,10 @@ er MUST NOT zu einer impliziten Variablen mit Punkt im Namen werden.
|
|||||||
- **WHEN** ein Modul `IF TYPEOF Ziel IS CommandButton THEN` enthält
|
- **WHEN** ein Modul `IF TYPEOF Ziel IS CommandButton THEN` enthält
|
||||||
- **THEN** wird es übersetzt und die Klasse gegen die bekannten Steuerelementklassen geprüft
|
- **THEN** wird es übersetzt und die Klasse gegen die bekannten Steuerelementklassen geprüft
|
||||||
|
|
||||||
|
#### Scenario: Gleichnamige Controls verschiedener Formulare
|
||||||
|
- **WHEN** Form1 und Form2 jeweils Text1 enthalten und Form2!Text1.Text gelesen wird
|
||||||
|
- **THEN** wird Text1 in Form2 aufgelöst und das Programm übersetzt diagnosefrei
|
||||||
|
|
||||||
### Requirement: Bang-Operator neben dem SINGLE-Suffix
|
### Requirement: Bang-Operator neben dem SINGLE-Suffix
|
||||||
Der Lexer SHALL `!` als Container-Operator erkennen, wenn ihm ein
|
Der Lexer SHALL `!` als Container-Operator erkennen, wenn ihm ein
|
||||||
Bezeichner folgt, und andernfalls als Typsuffix für SINGLE. Beide Formen
|
Bezeichner folgt, und andernfalls als Typsuffix für SINGLE. Beide Formen
|
||||||
|
|||||||
@@ -95,6 +95,8 @@ Arrayelemente, UDT-Felder) inspizieren und die Ausführung fortsetzen
|
|||||||
können. Die Tick-Prüfung erfolgt nur an Anweisungsgrenzen, nicht pro
|
können. Die Tick-Prüfung erfolgt nur an Anweisungsgrenzen, nicht pro
|
||||||
Opcode.
|
Opcode.
|
||||||
|
|
||||||
|
Ein Breakpoint SHALL durch Modulidentität und physische Quellzeile bestimmt sein. Gleiche Zeilennummern anderer Module MUST NOT denselben Breakpoint auslösen.
|
||||||
|
|
||||||
#### Scenario: Breakpoint hält an
|
#### Scenario: Breakpoint hält an
|
||||||
- **WHEN** ein Breakpoint auf Zeile 10 gesetzt ist und die Ausführung Zeile 10 erreicht
|
- **WHEN** ein Breakpoint auf Zeile 10 gesetzt ist und die Ausführung Zeile 10 erreicht
|
||||||
- **THEN** hält die VM vor der Anweisung an und meldet Modul und Zeile
|
- **THEN** hält die VM vor der Anweisung an und meldet Modul und Zeile
|
||||||
@@ -103,6 +105,10 @@ Opcode.
|
|||||||
- **WHEN** die VM an einem Breakpoint steht
|
- **WHEN** die VM an einem Breakpoint steht
|
||||||
- **THEN** liefert die Inspektions-API den aktuellen Wert einer benannten Variablen
|
- **THEN** liefert die Inspektions-API den aktuellen Wert einer benannten Variablen
|
||||||
|
|
||||||
|
#### Scenario: Gleiche Zeile in zwei Modulen
|
||||||
|
- **WHEN** ein Breakpoint auf Modul A, Zeile 10 gesetzt ist und Modul B Zeile 10 ausführt
|
||||||
|
- **THEN** läuft B weiter; erst A Zeile 10 hält an
|
||||||
|
|
||||||
### Requirement: Programmende im Runner
|
### Requirement: Programmende im Runner
|
||||||
Außerhalb der IDE SHALL `END` (und das Erreichen des Programmendes) den
|
Außerhalb der IDE SHALL `END` (und das Erreichen des Programmendes) den
|
||||||
Prozess mit Exit-Code 0 beenden; `STOP` SHALL mit der Meldung
|
Prozess mit Exit-Code 0 beenden; `STOP` SHALL mit der Meldung
|
||||||
|
|||||||
Reference in New Issue
Block a user