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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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