Projektmodule und vollständiges TBC-Kompilat umsetzen und Change archivieren
This commit is contained in:
@@ -35,12 +35,13 @@ fn generate_module(n_blocks: usize, seed: usize) -> String {
|
||||
}
|
||||
|
||||
fn compile_all(sources: &[(String, String)]) -> usize {
|
||||
let mut total = 0;
|
||||
for (name, src) in sources {
|
||||
let m = tb_vm::compile_source(name, src).expect("Benchmark-Quelle muss kompilieren");
|
||||
total += m.procs.iter().map(|p| p.code.len()).sum::<usize>();
|
||||
}
|
||||
total
|
||||
let units: Vec<_> = sources
|
||||
.iter()
|
||||
.map(|(name, src)| tb_frontend::source::SourceUnit::new(name, &format!("{name}.bas"), src))
|
||||
.collect();
|
||||
let project = tb_vm::compile_project("BENCH", &units, &Default::default(), &[])
|
||||
.expect("Benchmark-Projekt muss kompilieren");
|
||||
project.procs.iter().map(|p| p.code.len()).sum()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
|
||||
@@ -9,11 +9,14 @@
|
||||
use std::fmt;
|
||||
use std::rc::Rc;
|
||||
use tb_frontend::forms::{FormObject, ObjectClass};
|
||||
use tb_frontend::hir::HEventProc;
|
||||
use tb_frontend::hir::{HEventProc, HParam, HProcKind, HTy, NumTy};
|
||||
use tb_frontend::source::SourceFile;
|
||||
use tb_runtime::value::{TypeInit, UdtLayout};
|
||||
use tb_ui::forms::PropertyValue;
|
||||
use tb_ui::frm::FormInitial;
|
||||
|
||||
pub const TBC_MAGIC: &[u8; 4] = b"TBC\0";
|
||||
pub const TBC_VERSION: u16 = 3;
|
||||
pub const TBC_VERSION: u16 = 4;
|
||||
|
||||
/// Vergleichsoperator (Operand der `Cmp*`-Instruktionen).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -75,8 +78,15 @@ impl<'a> Reader<'a> {
|
||||
pub fn new(buf: &'a [u8]) -> Self {
|
||||
Reader { buf, pos: 0 }
|
||||
}
|
||||
fn finish(&self) -> Result<(), LoadError> {
|
||||
if self.pos != self.buf.len() {
|
||||
Err(LoadError::Corrupt("überzählige Abschnittsdaten"))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
fn take(&mut self, n: usize) -> Result<&'a [u8], LoadError> {
|
||||
if self.pos + n > self.buf.len() {
|
||||
if n > self.buf.len().saturating_sub(self.pos) {
|
||||
return Err(LoadError::Corrupt("unerwartetes Dateiende"));
|
||||
}
|
||||
let s = &self.buf[self.pos..self.pos + n];
|
||||
@@ -138,7 +148,11 @@ impl Enc for bool {
|
||||
out.push(*self as u8);
|
||||
}
|
||||
fn dec(r: &mut Reader) -> Result<Self, LoadError> {
|
||||
Ok(r.u8()? != 0)
|
||||
match r.u8()? {
|
||||
0 => Ok(false),
|
||||
1 => Ok(true),
|
||||
_ => Err(LoadError::Corrupt("bool")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,6 +184,9 @@ impl Enc for TypeInit {
|
||||
fn dec(r: &mut Reader) -> Result<Self, LoadError> {
|
||||
let tag = r.u8()?;
|
||||
let extra = r.u32()?;
|
||||
if (tag != 6 && tag != 7 && extra != 0) || (tag == 7 && extra > u16::MAX as u32) {
|
||||
return Err(LoadError::Corrupt("TypeInit-Zusatz"));
|
||||
}
|
||||
Ok(match tag {
|
||||
0 => TypeInit::Int,
|
||||
1 => TypeInit::Lng,
|
||||
@@ -224,6 +241,8 @@ instrs! {
|
||||
0x03 StopInstr;
|
||||
0x04 SystemInstr;
|
||||
0x05 Unsupported(a: u16); // Name im Stringpool → Fehler 73
|
||||
0x06 Source(a: u32, b: u32); // Quelldatei-ID, physische Spalte der folgenden Stmt-Grenze
|
||||
0x07 InitStmt(a: u32); // globale Initialisierung: Quellort/Debugger, noch keine Ereignisse
|
||||
|
||||
// 0x10 — Konstanten und Stack
|
||||
0x10 PushInt(a: i16);
|
||||
@@ -234,6 +253,7 @@ instrs! {
|
||||
0x15 PushStr(a: u16);
|
||||
0x16 Dup;
|
||||
0x17 Pop;
|
||||
0x18 PushUdtId(a: u16); // TYPE-Index als LONG auf dem Stack (ISAM)
|
||||
|
||||
// 0x20 — Variablen und Referenzen
|
||||
0x20 LoadGlobal(a: u16);
|
||||
@@ -260,6 +280,8 @@ instrs! {
|
||||
0x39 ArrBound(a: bool); // true = LBOUND
|
||||
0x3A FixStr(a: u32); // auf feste Länge kürzen/padden
|
||||
|
||||
0x3B CommonArr(a: bool, b: u16, c: u8, d: TypeInit); // gemeinsame Initialisierung/Layoutprüfung
|
||||
|
||||
// 0x40 — Arithmetik (monomorph)
|
||||
0x40 AddI2; 0x41 AddI4; 0x42 AddR4; 0x43 AddR8; 0x44 AddCy;
|
||||
0x45 SubI2; 0x46 SubI4; 0x47 SubR4; 0x48 SubR8; 0x49 SubCy;
|
||||
@@ -355,10 +377,122 @@ instrs! {
|
||||
0xD6 LsetRset(a: bool); // rset?; Stack: Referenz, Wert // argc, line_mode — Dateinummer liegt unter den Referenzen
|
||||
}
|
||||
|
||||
impl Enc for HTy {
|
||||
fn enc(&self, out: &mut Vec<u8>) {
|
||||
let tag = match self {
|
||||
HTy::Num(NumTy::Int) => 0,
|
||||
HTy::Num(NumTy::Lng) => 1,
|
||||
HTy::Num(NumTy::Sng) => 2,
|
||||
HTy::Num(NumTy::Dbl) => 3,
|
||||
HTy::Num(NumTy::Cur) => 4,
|
||||
HTy::Str => 5,
|
||||
HTy::FixedStr(_) => 6,
|
||||
HTy::Udt(_) => 7,
|
||||
HTy::Form => 8,
|
||||
HTy::Control => 9,
|
||||
};
|
||||
out.push(tag);
|
||||
match self {
|
||||
HTy::FixedStr(n) => n.enc(out),
|
||||
HTy::Udt(n) => n.enc(out),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
fn dec(r: &mut Reader) -> Result<Self, LoadError> {
|
||||
Ok(match r.u8()? {
|
||||
0 => HTy::Num(NumTy::Int),
|
||||
1 => HTy::Num(NumTy::Lng),
|
||||
2 => HTy::Num(NumTy::Sng),
|
||||
3 => HTy::Num(NumTy::Dbl),
|
||||
4 => HTy::Num(NumTy::Cur),
|
||||
5 => HTy::Str,
|
||||
6 => HTy::FixedStr(r.u32()?),
|
||||
7 => HTy::Udt(r.u16()?),
|
||||
8 => HTy::Form,
|
||||
9 => HTy::Control,
|
||||
_ => return Err(LoadError::Corrupt("Signaturtyp")),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Enc for PropertyValue {
|
||||
fn enc(&self, out: &mut Vec<u8>) {
|
||||
match self {
|
||||
Self::Integer(v) => {
|
||||
out.push(0);
|
||||
v.enc(out);
|
||||
}
|
||||
Self::Single(v) => {
|
||||
out.push(1);
|
||||
v.enc(out);
|
||||
}
|
||||
Self::String(v) => {
|
||||
out.push(2);
|
||||
w_string(out, v);
|
||||
}
|
||||
Self::Boolean(v) => {
|
||||
out.push(3);
|
||||
v.enc(out);
|
||||
}
|
||||
Self::Object(v) => {
|
||||
out.push(4);
|
||||
v.is_some().enc(out);
|
||||
if let Some((object, index)) = v {
|
||||
object.enc(out);
|
||||
index.is_some().enc(out);
|
||||
if let Some(index) = index {
|
||||
index.enc(out);
|
||||
}
|
||||
}
|
||||
}
|
||||
Self::IntegerArray(v) => {
|
||||
out.push(5);
|
||||
(v.len() as u32).enc(out);
|
||||
for v in v {
|
||||
v.enc(out);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn dec(r: &mut Reader) -> Result<Self, LoadError> {
|
||||
Ok(match r.u8()? {
|
||||
0 => Self::Integer(i32::dec(r)?),
|
||||
1 => Self::Single(f32::dec(r)?),
|
||||
2 => Self::String(r.string()?),
|
||||
3 => Self::Boolean(bool::dec(r)?),
|
||||
4 => Self::Object(if bool::dec(r)? {
|
||||
Some((
|
||||
r.u16()?,
|
||||
if bool::dec(r)? {
|
||||
Some(i32::dec(r)?)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}),
|
||||
5 => {
|
||||
let n = r.u32()?;
|
||||
let mut values = Vec::new();
|
||||
for _ in 0..n {
|
||||
values.push(i32::dec(r)?);
|
||||
}
|
||||
Self::IntegerArray(values)
|
||||
}
|
||||
_ => return Err(LoadError::Corrupt("Anfangswerttyp")),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Modulstruktur ------------------------------------------------------------
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProcCode {
|
||||
pub module: u16,
|
||||
pub kind: HProcKind,
|
||||
pub params: Vec<HParam>,
|
||||
pub ret_ty: Option<HTy>,
|
||||
pub name: String,
|
||||
pub n_params: u16,
|
||||
/// Initialisierung aller Frame-Slots (Parameter zuerst; deren Init
|
||||
@@ -378,6 +512,10 @@ pub struct DataItem {
|
||||
/// Übersetztes Modul — Inhalt des `.tbc`-Containers.
|
||||
#[derive(Debug)]
|
||||
pub struct CompiledModule {
|
||||
pub modules: Vec<(String, u8)>,
|
||||
pub sources: Vec<SourceFile>,
|
||||
pub form_initial: Vec<FormInitial>,
|
||||
pub startup_form: Option<u16>,
|
||||
pub name: String,
|
||||
/// `OPTION BASE` (Untergrenze impliziter Arrays).
|
||||
pub option_base: u8,
|
||||
@@ -402,9 +540,204 @@ fn w_string(out: &mut Vec<u8>, s: &str) {
|
||||
}
|
||||
|
||||
impl CompiledModule {
|
||||
pub fn validate(&self) -> Result<(), LoadError> {
|
||||
let bad = || LoadError::Corrupt("ungültige Tabellenreferenz oder Anfangsdaten");
|
||||
if self.procs.is_empty()
|
||||
|| self.sources.is_empty()
|
||||
|| self.modules.is_empty()
|
||||
|| self.modules.len() > u16::MAX as usize
|
||||
|| self.procs.len() > u16::MAX as usize
|
||||
|| self.objects.len() >= u16::MAX as usize
|
||||
|| self.strings.len() >= u16::MAX as usize
|
||||
|| self.globals_init.len() > u16::MAX as usize
|
||||
|| self.global_names.len() != self.globals_init.len()
|
||||
|| self.modules.iter().any(|(_, base)| *base > 1)
|
||||
|| self.option_base != self.modules[0].1
|
||||
|| self
|
||||
.sources
|
||||
.iter()
|
||||
.any(|s| s.module as usize >= self.modules.len())
|
||||
{
|
||||
return Err(bad());
|
||||
}
|
||||
let ty_ok =
|
||||
|ty: &TypeInit| !matches!(ty, TypeInit::Udt(id) if *id as usize >= self.udts.len());
|
||||
let sig_ok = |ty: &HTy| !matches!(ty, HTy::Udt(id) if *id as usize >= self.udts.len());
|
||||
if self.globals_init.iter().any(|t| !ty_ok(t)) {
|
||||
return Err(bad());
|
||||
}
|
||||
for (id, udt) in self.udts.iter().enumerate() {
|
||||
if udt
|
||||
.fields
|
||||
.iter()
|
||||
.any(|t| matches!(t, TypeInit::Udt(n) if *n as usize >= id))
|
||||
{
|
||||
return Err(bad());
|
||||
}
|
||||
}
|
||||
for (id, o) in self.objects.iter().enumerate() {
|
||||
if o.parent.is_some_and(|parent| parent as usize >= id) {
|
||||
return Err(bad());
|
||||
}
|
||||
if o.parent_form.as_deref()
|
||||
!= o.parent
|
||||
.map(|parent| self.objects[parent as usize].name.as_str())
|
||||
{
|
||||
return Err(bad());
|
||||
}
|
||||
}
|
||||
if self.startup_form.is_some_and(|id| {
|
||||
self.objects
|
||||
.get(id as usize)
|
||||
.is_none_or(|o| o.class != ObjectClass::Form)
|
||||
}) {
|
||||
return Err(bad());
|
||||
}
|
||||
for e in &self.event_procs {
|
||||
if e.object as usize >= self.objects.len() || e.proc as usize >= self.procs.len() {
|
||||
return Err(bad());
|
||||
}
|
||||
}
|
||||
let mut initials = std::collections::HashSet::new();
|
||||
for initial in &self.form_initial {
|
||||
let object = self.objects.get(initial.object as usize).ok_or_else(bad)?;
|
||||
if !initials.insert((initial.object, initial.index))
|
||||
|| (initial.index != 0 && !object.array)
|
||||
{
|
||||
return Err(bad());
|
||||
}
|
||||
for (id, value) in &initial.properties {
|
||||
use tb_frontend::forms::PropertyType as T;
|
||||
let spec = tb_frontend::forms::properties(object.class)
|
||||
.get(*id as usize)
|
||||
.copied()
|
||||
.ok_or_else(bad)?;
|
||||
let valid = match (value, spec.ty) {
|
||||
(PropertyValue::Integer(v), T::Integer) => {
|
||||
!spec.min.is_some_and(|min| *v < min)
|
||||
&& !spec.max.is_some_and(|max| *v > max)
|
||||
&& (spec.name != "INDEX" || *v == initial.index)
|
||||
}
|
||||
(PropertyValue::Single(v), T::Single) => v.is_finite(),
|
||||
(PropertyValue::String(_), T::String)
|
||||
| (PropertyValue::Boolean(_), T::Boolean)
|
||||
| (PropertyValue::IntegerArray(_), T::IntegerArray) => true,
|
||||
(PropertyValue::Object(v), T::Object) => {
|
||||
v.is_none_or(|(id, _)| (id as usize) < self.objects.len())
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
if !valid {
|
||||
return Err(bad());
|
||||
}
|
||||
}
|
||||
}
|
||||
for p in &self.procs {
|
||||
if p.module as usize >= self.modules.len()
|
||||
|| p.n_params as usize != p.params.len()
|
||||
|| p.params.len() > p.locals_init.len()
|
||||
|| p.locals_init.len() != p.local_names.len()
|
||||
|| p.locals_init.iter().any(|t| !ty_ok(t))
|
||||
|| p.params.iter().any(|p| !sig_ok(&p.ty))
|
||||
|| p.ret_ty.as_ref().is_some_and(|t| !sig_ok(t))
|
||||
|| matches!(p.kind, HProcKind::Function | HProcKind::DefFn) != p.ret_ty.is_some()
|
||||
|| p.params.iter().zip(&p.locals_init).any(|(param, init)| {
|
||||
*init
|
||||
!= if param.array {
|
||||
TypeInit::Empty
|
||||
} else {
|
||||
crate::codegen::type_init(¶m.ty)
|
||||
}
|
||||
})
|
||||
{
|
||||
return Err(bad());
|
||||
}
|
||||
for instruction in &p.code {
|
||||
use Instr::*;
|
||||
let valid = match instruction {
|
||||
Source(id, _) => (*id as usize) < self.sources.len(),
|
||||
PushUdtId(id) => (*id as usize) < self.udts.len(),
|
||||
PushStr(id) | Unsupported(id) => (*id as usize) < self.strings.len(),
|
||||
LoadGlobal(id) | StoreGlobal(id) | MakeRefGlobal(id) => {
|
||||
(*id as usize) < self.globals_init.len()
|
||||
}
|
||||
LoadLocal(id) | StoreLocal(id) | MakeRefLocal(id) | LoadRef(id)
|
||||
| StoreRef(id) => (*id as usize) < p.locals_init.len(),
|
||||
LoadArr(global, id, _, ty)
|
||||
| DimArr(global, id, _, ty)
|
||||
| CommonArr(global, id, _, ty)
|
||||
| RedimArr(global, id, _, ty) => {
|
||||
ty_ok(ty)
|
||||
&& (*id as usize)
|
||||
< if *global {
|
||||
self.globals_init.len()
|
||||
} else {
|
||||
p.locals_init.len()
|
||||
}
|
||||
}
|
||||
EraseSlot(global, id) => {
|
||||
(*id as usize)
|
||||
< if *global {
|
||||
self.globals_init.len()
|
||||
} else {
|
||||
p.locals_init.len()
|
||||
}
|
||||
}
|
||||
Call(id, argc) => self
|
||||
.procs
|
||||
.get(*id as usize)
|
||||
.is_some_and(|p| p.n_params == *argc as u16),
|
||||
Jump(pc) | JumpIfFalse(pc) | JumpIfTrue(pc) | Gosub(pc) | RetGosubTo(pc)
|
||||
| OnErrorLocal(pc) | ResumeLabel(pc) => (*pc as usize) < p.code.len(),
|
||||
OnErrorGoto(pc) => self
|
||||
.procs
|
||||
.first()
|
||||
.is_some_and(|p| (*pc as usize) < p.code.len()),
|
||||
OnJump(id, _) => self
|
||||
.jump_tables
|
||||
.get(*id as usize)
|
||||
.is_some_and(|t| t.iter().all(|pc| (*pc as usize) < p.code.len())),
|
||||
Restore(id) => (*id as usize) <= self.data.len(),
|
||||
Input(_, _, id, _) => *id == u16::MAX || (*id as usize) < self.strings.len(),
|
||||
LoadObjectProperty(id, prop, _)
|
||||
| StoreObjectProperty(id, prop, _)
|
||||
| LoadObjectIndexedProperty(id, prop)
|
||||
| StoreObjectIndexedProperty(id, prop) => {
|
||||
self.objects.get(*id as usize).is_some_and(|o| {
|
||||
((*prop & 0x7fff) as usize)
|
||||
< tb_frontend::forms::properties(o.class).len()
|
||||
})
|
||||
}
|
||||
PushObject(id, _) | ObjectLoad(id, _, _) => (*id as usize) < self.objects.len(),
|
||||
ObjectMethod(id, method, _) | ObjectMethodFn(id, method, _) => {
|
||||
self.objects.get(*id as usize).is_some_and(|o| {
|
||||
(*method as usize) < tb_frontend::forms::methods(o.class).len()
|
||||
})
|
||||
}
|
||||
LoadDynamicObjectProperty(name) | StoreDynamicObjectProperty(name) => {
|
||||
(*name as usize) < self.strings.len()
|
||||
}
|
||||
GetPut(_, _, 7, id) => (*id as usize) < self.udts.len(),
|
||||
GetPut(_, _, kind, _) => *kind <= 8,
|
||||
TypeOf(class) => ObjectClass::from_id(*class).is_some(),
|
||||
TrapDefine(kind, pc) => *kind <= 3 && (*pc as usize) < p.code.len(),
|
||||
TrapDisable(kind) => *kind <= 3,
|
||||
TrapSet(kind, state) => *kind <= 3 && *state <= 2,
|
||||
Run(kind) => *kind <= 2,
|
||||
ReadData(kind) => *kind <= 1,
|
||||
_ => true,
|
||||
};
|
||||
if !valid {
|
||||
return Err(bad());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `.tbc`-Container schreiben: Magic, Version, Flags, Abschnittstabelle
|
||||
/// (Kennung/Offset/Länge), Abschnitte MODN, CONS, TYPS, GLOB, PROC
|
||||
/// (mit eingebettetem Code und Zeileninfo), DATA, JMPT.
|
||||
/// (Kennung/Offset/Länge), Abschnitte MODN, SRCS, CONS, TYPS, GLOB, PROC
|
||||
/// (mit eingebettetem Code und Quellorten), DATA, JMPT, OBJS.
|
||||
pub fn to_tbc(&self) -> Vec<u8> {
|
||||
let mut sections: Vec<([u8; 4], Vec<u8>)> = Vec::new();
|
||||
|
||||
@@ -413,6 +746,19 @@ impl CompiledModule {
|
||||
modn.push(self.option_base);
|
||||
sections.push((*b"MODN", modn));
|
||||
|
||||
let mut srcs = Vec::new();
|
||||
(self.modules.len() as u32).enc(&mut srcs);
|
||||
for (name, base) in &self.modules {
|
||||
w_string(&mut srcs, name);
|
||||
base.enc(&mut srcs);
|
||||
}
|
||||
(self.sources.len() as u32).enc(&mut srcs);
|
||||
for source in &self.sources {
|
||||
source.module.enc(&mut srcs);
|
||||
w_string(&mut srcs, &source.path);
|
||||
}
|
||||
sections.push((*b"SRCS", srcs));
|
||||
|
||||
let mut cons = Vec::new();
|
||||
cons.extend_from_slice(&(self.strings.len() as u32).to_le_bytes());
|
||||
for s in &self.strings {
|
||||
@@ -443,6 +789,19 @@ impl CompiledModule {
|
||||
proc.extend_from_slice(&(self.procs.len() as u32).to_le_bytes());
|
||||
for p in &self.procs {
|
||||
w_string(&mut proc, &p.name);
|
||||
p.module.enc(&mut proc);
|
||||
(p.kind as u8).enc(&mut proc);
|
||||
(p.params.len() as u32).enc(&mut proc);
|
||||
for param in &p.params {
|
||||
w_string(&mut proc, ¶m.name);
|
||||
param.ty.enc(&mut proc);
|
||||
param.array.enc(&mut proc);
|
||||
param.by_ref.enc(&mut proc);
|
||||
}
|
||||
p.ret_ty.is_some().enc(&mut proc);
|
||||
if let Some(ty) = &p.ret_ty {
|
||||
ty.enc(&mut proc);
|
||||
}
|
||||
proc.extend_from_slice(&p.n_params.to_le_bytes());
|
||||
proc.extend_from_slice(&(p.locals_init.len() as u32).to_le_bytes());
|
||||
for (init, name) in p.locals_init.iter().zip(&p.local_names) {
|
||||
@@ -484,6 +843,7 @@ impl CompiledModule {
|
||||
objs.push(o.class.id());
|
||||
w_string(&mut objs, o.parent_form.as_deref().unwrap_or(""));
|
||||
objs.push(o.array as u8);
|
||||
o.parent.unwrap_or(u16::MAX).enc(&mut objs);
|
||||
}
|
||||
objs.extend_from_slice(&(self.event_procs.len() as u32).to_le_bytes());
|
||||
for e in &self.event_procs {
|
||||
@@ -491,6 +851,17 @@ impl CompiledModule {
|
||||
w_string(&mut objs, &e.event);
|
||||
objs.extend_from_slice(&e.proc.to_le_bytes());
|
||||
}
|
||||
self.startup_form.unwrap_or(u16::MAX).enc(&mut objs);
|
||||
(self.form_initial.len() as u32).enc(&mut objs);
|
||||
for initial in &self.form_initial {
|
||||
initial.object.enc(&mut objs);
|
||||
initial.index.enc(&mut objs);
|
||||
(initial.properties.len() as u32).enc(&mut objs);
|
||||
for (id, value) in &initial.properties {
|
||||
id.enc(&mut objs);
|
||||
value.enc(&mut objs);
|
||||
}
|
||||
}
|
||||
sections.push((*b"OBJS", objs));
|
||||
|
||||
// Header + Abschnittstabelle
|
||||
@@ -527,8 +898,13 @@ impl CompiledModule {
|
||||
if version != TBC_VERSION {
|
||||
return Err(LoadError::Version(version));
|
||||
}
|
||||
let _flags = r.u16()?;
|
||||
if r.u16()? != 0 {
|
||||
return Err(LoadError::Corrupt("Header-Flags"));
|
||||
}
|
||||
let n_sections = r.u32()? as usize;
|
||||
if n_sections != 9 {
|
||||
return Err(LoadError::Corrupt("Abschnittsanzahl"));
|
||||
}
|
||||
let mut table = Vec::new();
|
||||
for _ in 0..n_sections {
|
||||
let id: [u8; 4] = r.take(4)?.try_into().unwrap();
|
||||
@@ -536,6 +912,27 @@ impl CompiledModule {
|
||||
let len = r.u32()? as usize;
|
||||
table.push((id, off, len));
|
||||
}
|
||||
let expected = [
|
||||
*b"MODN", *b"SRCS", *b"CONS", *b"TYPS", *b"GLOB", *b"PROC", *b"DATA", *b"JMPT",
|
||||
*b"OBJS",
|
||||
];
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
let mut ranges = Vec::new();
|
||||
for (id, off, len) in &table {
|
||||
if !expected.contains(id)
|
||||
|| !seen.insert(*id)
|
||||
|| *off < r.pos
|
||||
|| *off > buf.len()
|
||||
|| *len > buf.len() - *off
|
||||
{
|
||||
return Err(LoadError::Corrupt("Abschnittstabelle"));
|
||||
}
|
||||
ranges.push((*off, off + len));
|
||||
}
|
||||
ranges.sort_unstable();
|
||||
if ranges.windows(2).any(|pair| pair[0].1 > pair[1].0) {
|
||||
return Err(LoadError::Corrupt("überlappende Abschnitte"));
|
||||
}
|
||||
let section = |id: &[u8; 4]| -> Result<Reader, LoadError> {
|
||||
for (sid, off, len) in &table {
|
||||
if sid == id {
|
||||
@@ -552,44 +949,86 @@ impl CompiledModule {
|
||||
let name = r.string()?;
|
||||
let option_base = r.u8()?;
|
||||
|
||||
r.finish()?;
|
||||
let mut r = section(b"SRCS")?;
|
||||
let n = r.u32()?;
|
||||
let mut modules = Vec::new();
|
||||
for _ in 0..n {
|
||||
modules.push((r.string()?, r.u8()?));
|
||||
}
|
||||
let n = r.u32()?;
|
||||
let mut sources = Vec::new();
|
||||
for _ in 0..n {
|
||||
sources.push(SourceFile {
|
||||
module: r.u16()?,
|
||||
path: r.string()?,
|
||||
});
|
||||
}
|
||||
r.finish()?;
|
||||
let mut r = section(b"CONS")?;
|
||||
let n = r.u32()? as usize;
|
||||
let mut strings = Vec::with_capacity(n);
|
||||
let mut strings = Vec::new();
|
||||
for _ in 0..n {
|
||||
strings.push(Rc::from(r.string()?.as_str()));
|
||||
}
|
||||
|
||||
r.finish()?;
|
||||
let mut r = section(b"TYPS")?;
|
||||
let n = r.u32()? as usize;
|
||||
let mut udts = Vec::with_capacity(n);
|
||||
let mut udts = Vec::new();
|
||||
for _ in 0..n {
|
||||
let name = r.string()?;
|
||||
let nf = r.u32()? as usize;
|
||||
let mut fields = Vec::with_capacity(nf);
|
||||
let mut fields = Vec::new();
|
||||
for _ in 0..nf {
|
||||
fields.push(TypeInit::dec(&mut r)?);
|
||||
}
|
||||
udts.push(UdtLayout { name, fields });
|
||||
}
|
||||
|
||||
r.finish()?;
|
||||
let mut r = section(b"GLOB")?;
|
||||
let n = r.u32()? as usize;
|
||||
let mut globals_init = Vec::with_capacity(n);
|
||||
let mut global_names = Vec::with_capacity(n);
|
||||
let mut globals_init = Vec::new();
|
||||
let mut global_names = Vec::new();
|
||||
for _ in 0..n {
|
||||
globals_init.push(TypeInit::dec(&mut r)?);
|
||||
global_names.push(r.string()?);
|
||||
}
|
||||
|
||||
r.finish()?;
|
||||
let mut r = section(b"PROC")?;
|
||||
let n = r.u32()? as usize;
|
||||
let mut procs = Vec::with_capacity(n);
|
||||
let mut procs = Vec::new();
|
||||
for _ in 0..n {
|
||||
let name = r.string()?;
|
||||
let module = r.u16()?;
|
||||
let kind = match r.u8()? {
|
||||
0 => HProcKind::Main,
|
||||
1 => HProcKind::Sub,
|
||||
2 => HProcKind::Function,
|
||||
3 => HProcKind::DefFn,
|
||||
_ => return Err(LoadError::Corrupt("Prozedurart")),
|
||||
};
|
||||
let np = r.u32()?;
|
||||
let mut params = Vec::new();
|
||||
for _ in 0..np {
|
||||
params.push(HParam {
|
||||
name: r.string()?,
|
||||
ty: HTy::dec(&mut r)?,
|
||||
array: bool::dec(&mut r)?,
|
||||
by_ref: bool::dec(&mut r)?,
|
||||
});
|
||||
}
|
||||
let ret_ty = if bool::dec(&mut r)? {
|
||||
Some(HTy::dec(&mut r)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let n_params = r.u16()?;
|
||||
let nl = r.u32()? as usize;
|
||||
let mut locals_init = Vec::with_capacity(nl);
|
||||
let mut local_names = Vec::with_capacity(nl);
|
||||
let mut locals_init = Vec::new();
|
||||
let mut local_names = Vec::new();
|
||||
for _ in 0..nl {
|
||||
locals_init.push(TypeInit::dec(&mut r)?);
|
||||
local_names.push(r.string()?);
|
||||
@@ -598,11 +1037,16 @@ impl CompiledModule {
|
||||
let code_len = r.u32()? as usize;
|
||||
let code_bytes = r.take(code_len)?;
|
||||
let mut cr = Reader::new(code_bytes);
|
||||
let mut code = Vec::with_capacity(n_instr);
|
||||
let mut code = Vec::new();
|
||||
for _ in 0..n_instr {
|
||||
code.push(Instr::decode(&mut cr)?);
|
||||
}
|
||||
cr.finish()?;
|
||||
procs.push(ProcCode {
|
||||
module,
|
||||
kind,
|
||||
params,
|
||||
ret_ty,
|
||||
name,
|
||||
n_params,
|
||||
locals_init,
|
||||
@@ -611,44 +1055,49 @@ impl CompiledModule {
|
||||
});
|
||||
}
|
||||
|
||||
r.finish()?;
|
||||
let mut r = section(b"DATA")?;
|
||||
let n = r.u32()? as usize;
|
||||
let mut data = Vec::with_capacity(n);
|
||||
let mut data = Vec::new();
|
||||
for _ in 0..n {
|
||||
let text = r.string()?;
|
||||
let line = r.u32()?;
|
||||
data.push(DataItem { text, line });
|
||||
}
|
||||
|
||||
r.finish()?;
|
||||
let mut r = section(b"JMPT")?;
|
||||
let n = r.u32()? as usize;
|
||||
let mut jump_tables = Vec::with_capacity(n);
|
||||
let mut jump_tables = Vec::new();
|
||||
for _ in 0..n {
|
||||
let m = r.u32()? as usize;
|
||||
let mut t = Vec::with_capacity(m);
|
||||
let mut t = Vec::new();
|
||||
for _ in 0..m {
|
||||
t.push(r.u32()?);
|
||||
}
|
||||
jump_tables.push(t);
|
||||
}
|
||||
|
||||
r.finish()?;
|
||||
let mut r = section(b"OBJS")?;
|
||||
let n = r.u32()? as usize;
|
||||
let mut objects = Vec::with_capacity(n);
|
||||
let mut objects = Vec::new();
|
||||
for _ in 0..n {
|
||||
let name = r.string()?;
|
||||
let class = ObjectClass::from_id(r.u8()?).ok_or(LoadError::Corrupt("Objektklasse"))?;
|
||||
let parent = r.string()?;
|
||||
let array = r.u8()? != 0;
|
||||
let array = bool::dec(&mut r)?;
|
||||
let parent_id = r.u16()?;
|
||||
objects.push(FormObject {
|
||||
name,
|
||||
class,
|
||||
parent_form: (!parent.is_empty()).then_some(parent),
|
||||
parent: (parent_id != u16::MAX).then_some(parent_id),
|
||||
array,
|
||||
});
|
||||
}
|
||||
let n = r.u32()? as usize;
|
||||
let mut event_procs = Vec::with_capacity(n);
|
||||
let mut event_procs = Vec::new();
|
||||
for _ in 0..n {
|
||||
event_procs.push(HEventProc {
|
||||
object: r.u16()?,
|
||||
@@ -657,7 +1106,35 @@ impl CompiledModule {
|
||||
});
|
||||
}
|
||||
|
||||
Ok(CompiledModule {
|
||||
let startup = r.u16()?;
|
||||
let startup_form = (startup != u16::MAX).then_some(startup);
|
||||
let n = r.u32()?;
|
||||
let mut form_initial = Vec::new();
|
||||
for _ in 0..n {
|
||||
let object = r.u16()?;
|
||||
let index = i32::dec(&mut r)?;
|
||||
let n = r.u32()?;
|
||||
let mut properties = std::collections::BTreeMap::new();
|
||||
for _ in 0..n {
|
||||
if properties
|
||||
.insert(r.u16()?, PropertyValue::dec(&mut r)?)
|
||||
.is_some()
|
||||
{
|
||||
return Err(LoadError::Corrupt("doppelte Anfangseigenschaft"));
|
||||
}
|
||||
}
|
||||
form_initial.push(FormInitial {
|
||||
object,
|
||||
index,
|
||||
properties,
|
||||
});
|
||||
}
|
||||
r.finish()?;
|
||||
let module = CompiledModule {
|
||||
modules,
|
||||
sources,
|
||||
form_initial,
|
||||
startup_form,
|
||||
name,
|
||||
option_base,
|
||||
strings,
|
||||
@@ -669,7 +1146,9 @@ impl CompiledModule {
|
||||
jump_tables,
|
||||
objects,
|
||||
event_procs,
|
||||
})
|
||||
};
|
||||
module.validate()?;
|
||||
Ok(module)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -712,6 +1191,13 @@ mod tests {
|
||||
#[test]
|
||||
fn tbc_roundtrip() {
|
||||
let m = CompiledModule {
|
||||
modules: vec![("TEST".into(), 1)],
|
||||
sources: vec![SourceFile {
|
||||
module: 0,
|
||||
path: "test.bas".into(),
|
||||
}],
|
||||
form_initial: vec![],
|
||||
startup_form: None,
|
||||
name: "TEST".into(),
|
||||
option_base: 1,
|
||||
strings: vec![Rc::from("Hallo"), Rc::from("Welt")],
|
||||
@@ -722,6 +1208,10 @@ mod tests {
|
||||
fields: vec![TypeInit::FixedStr(30), TypeInit::Dbl],
|
||||
}],
|
||||
procs: vec![ProcCode {
|
||||
module: 0,
|
||||
kind: HProcKind::Main,
|
||||
params: vec![],
|
||||
ret_ty: None,
|
||||
name: "TEST".into(),
|
||||
n_params: 0,
|
||||
locals_init: vec![],
|
||||
@@ -751,6 +1241,13 @@ mod tests {
|
||||
#[test]
|
||||
fn unbekannte_version_wird_abgelehnt() {
|
||||
let m = CompiledModule {
|
||||
modules: vec![("TEST".into(), 0)],
|
||||
sources: vec![SourceFile {
|
||||
module: 0,
|
||||
path: "test.bas".into(),
|
||||
}],
|
||||
form_initial: vec![],
|
||||
startup_form: None,
|
||||
name: "T".into(),
|
||||
option_base: 0,
|
||||
strings: vec![],
|
||||
|
||||
@@ -16,6 +16,12 @@ use tb_runtime::value::TypeInit;
|
||||
|
||||
pub fn compile(hir: &HirModule) -> CompiledModule {
|
||||
let mut cg = Codegen {
|
||||
common_arrays: hir
|
||||
.commons
|
||||
.iter()
|
||||
.filter(|c| c.dims.is_some())
|
||||
.map(|c| c.slot)
|
||||
.collect(),
|
||||
strings: Vec::new(),
|
||||
string_ids: HashMap::new(),
|
||||
jump_tables: Vec::new(),
|
||||
@@ -30,6 +36,13 @@ pub fn compile(hir: &HirModule) -> CompiledModule {
|
||||
procs.push(cg.compile_proc(proc));
|
||||
}
|
||||
CompiledModule {
|
||||
modules: vec![(hir.name.clone(), hir.option_base)],
|
||||
sources: vec![tb_frontend::source::SourceFile {
|
||||
module: 0,
|
||||
path: hir.name.clone(),
|
||||
}],
|
||||
form_initial: vec![],
|
||||
startup_form: None,
|
||||
name: hir.name.clone(),
|
||||
option_base: hir.option_base,
|
||||
strings: cg.strings,
|
||||
@@ -67,7 +80,7 @@ fn slot_init(v: &hir::HVar) -> TypeInit {
|
||||
}
|
||||
}
|
||||
|
||||
fn type_init(t: &HTy) -> TypeInit {
|
||||
pub(crate) fn type_init(t: &HTy) -> TypeInit {
|
||||
match t {
|
||||
HTy::Num(NumTy::Int) => TypeInit::Int,
|
||||
HTy::Num(NumTy::Lng) => TypeInit::Lng,
|
||||
@@ -82,6 +95,7 @@ fn type_init(t: &HTy) -> TypeInit {
|
||||
}
|
||||
|
||||
struct Codegen {
|
||||
common_arrays: std::collections::HashSet<u16>,
|
||||
strings: Vec<Rc<str>>,
|
||||
string_ids: HashMap<String, u16>,
|
||||
jump_tables: Vec<Vec<u32>>,
|
||||
@@ -95,6 +109,7 @@ struct Codegen {
|
||||
}
|
||||
|
||||
struct ProcCtx {
|
||||
pos: tb_frontend::SourcePos,
|
||||
code: Vec<Instr>,
|
||||
/// LabelId → Instruktionsindex.
|
||||
label_pc: Vec<Option<u32>>,
|
||||
@@ -134,6 +149,10 @@ impl ProcCtx {
|
||||
self.label_pc[label as usize] = Some(pc);
|
||||
}
|
||||
fn emit(&mut self, i: Instr) {
|
||||
if matches!(i, Instr::Stmt(_) | Instr::InitStmt(_)) {
|
||||
self.code
|
||||
.push(Instr::Source(self.pos.source, self.pos.column));
|
||||
}
|
||||
self.code.push(i);
|
||||
}
|
||||
/// Sprunginstruktion mit noch unbekanntem Ziel emittieren.
|
||||
@@ -162,6 +181,7 @@ impl Codegen {
|
||||
|
||||
fn compile_proc(&mut self, proc: &hir::HProc) -> ProcCode {
|
||||
let mut ctx = ProcCtx {
|
||||
pos: tb_frontend::SourcePos::default(),
|
||||
code: Vec::new(),
|
||||
label_pc: vec![None; proc.label_count as usize],
|
||||
fixups: Vec::new(),
|
||||
@@ -238,6 +258,10 @@ impl Codegen {
|
||||
}
|
||||
|
||||
ProcCode {
|
||||
module: 0,
|
||||
kind: proc.kind,
|
||||
params: proc.params.clone(),
|
||||
ret_ty: proc.ret_ty.clone(),
|
||||
name: proc.name.clone(),
|
||||
n_params: proc.params.len() as u16,
|
||||
locals_init: proc.locals.iter().map(slot_init).collect(),
|
||||
@@ -268,13 +292,14 @@ impl Codegen {
|
||||
}
|
||||
|
||||
fn stmt_inner(&mut self, ctx: &mut ProcCtx, proc: &hir::HProc, stmt: &HStmt, boundary: bool) {
|
||||
ctx.pos = stmt.pos;
|
||||
match &stmt.kind {
|
||||
HStmtKind::Label(l) => {
|
||||
ctx.bind(*l);
|
||||
return;
|
||||
}
|
||||
_ if boundary => ctx.emit(Instr::Stmt(stmt.line)),
|
||||
_ => {}
|
||||
_ => ctx.emit(Instr::InitStmt(stmt.line)),
|
||||
}
|
||||
match &stmt.kind {
|
||||
HStmtKind::Label(_) => unreachable!(),
|
||||
@@ -478,6 +503,7 @@ impl Codegen {
|
||||
}
|
||||
}
|
||||
HStmtKind::Loop {
|
||||
end_pos,
|
||||
pre,
|
||||
post,
|
||||
body,
|
||||
@@ -496,6 +522,8 @@ impl Codegen {
|
||||
for s in body {
|
||||
self.stmt(ctx, proc, s);
|
||||
}
|
||||
ctx.pos = *end_pos;
|
||||
ctx.emit(Instr::Stmt(end_pos.line));
|
||||
match post {
|
||||
Some((is_until, cond)) => {
|
||||
self.expr(ctx, cond);
|
||||
@@ -511,6 +539,7 @@ impl Codegen {
|
||||
ctx.bind(*exit_label);
|
||||
}
|
||||
HStmtKind::For {
|
||||
end_pos,
|
||||
var,
|
||||
ty,
|
||||
from,
|
||||
@@ -533,7 +562,7 @@ impl Codegen {
|
||||
*step_slot,
|
||||
body,
|
||||
*exit_label,
|
||||
stmt.line,
|
||||
*end_pos,
|
||||
);
|
||||
}
|
||||
HStmtKind::Goto(l) => ctx.emit_jump(Instr::Jump(0), *l),
|
||||
@@ -663,6 +692,8 @@ impl Codegen {
|
||||
let init = type_init(elem);
|
||||
if *redim {
|
||||
ctx.emit(Instr::RedimArr(global, s, dims.len() as u8, init));
|
||||
} else if global && self.common_arrays.contains(&s) {
|
||||
ctx.emit(Instr::CommonArr(global, s, dims.len() as u8, init));
|
||||
} else {
|
||||
ctx.emit(Instr::DimArr(global, s, dims.len() as u8, init));
|
||||
}
|
||||
@@ -697,7 +728,7 @@ impl Codegen {
|
||||
step_slot: Option<VarSlot>,
|
||||
body: &[HStmt],
|
||||
exit_label: u16,
|
||||
line: u32,
|
||||
end_pos: tb_frontend::SourcePos,
|
||||
) {
|
||||
// Startwert, Grenze, ggf. Schritt einmal auswerten.
|
||||
self.store_place(ctx, var, |cg, ctx| cg.expr(ctx, from));
|
||||
@@ -715,15 +746,6 @@ impl Codegen {
|
||||
let l_test = ctx.new_label();
|
||||
let l_body = ctx.new_label();
|
||||
ctx.bind(l_test);
|
||||
// Anweisungsgrenze für den Rücksprung von `NEXT`. Sie kann nicht
|
||||
// auf die Grenze des `FOR` zeigen — dort stünde die Initialisierung
|
||||
// noch einmal.
|
||||
//
|
||||
// ponytail: gemeldet wird die Zeile des `FOR`, nicht die des
|
||||
// `NEXT` — die trägt das HIR nicht. Ceiling: braucht der Debugger
|
||||
// in Phase 5 die genaue Zeile, bekommt `HStmtKind::For` ein Feld
|
||||
// `next_line`.
|
||||
ctx.emit(Instr::Stmt(line));
|
||||
match const_step {
|
||||
Some(s) => {
|
||||
// Vergleichsrichtung zur Compilezeit.
|
||||
@@ -757,7 +779,9 @@ impl Codegen {
|
||||
for s in body {
|
||||
self.stmt(ctx, proc, s);
|
||||
}
|
||||
// NEXT: inkrementieren, zurück zum Test.
|
||||
// NEXT: eigene Quellgrenze für Fehler, RESUME und Debugger.
|
||||
ctx.pos = end_pos;
|
||||
ctx.emit(Instr::Stmt(end_pos.line));
|
||||
self.store_place(ctx, var, |cg, ctx| {
|
||||
cg.load_place(ctx, var);
|
||||
match (step, step_slot) {
|
||||
@@ -777,6 +801,7 @@ impl Codegen {
|
||||
match e {
|
||||
HExpr::Int(v) => ctx.emit(Instr::PushInt(*v)),
|
||||
HExpr::Lng(v) => ctx.emit(Instr::PushLng(*v)),
|
||||
HExpr::UdtId(id) => ctx.emit(Instr::PushUdtId(*id)),
|
||||
HExpr::Sng(v) => ctx.emit(Instr::PushSng(*v)),
|
||||
HExpr::Dbl(v) => ctx.emit(Instr::PushDbl(*v)),
|
||||
HExpr::Cur(v) => ctx.emit(Instr::PushCur(*v)),
|
||||
|
||||
@@ -70,6 +70,8 @@ struct Frame {
|
||||
/// Instruktionsindex der zuletzt begonnenen Anweisung (`Stmt`).
|
||||
last_stmt_pc: usize,
|
||||
line: u32,
|
||||
source: u32,
|
||||
column: u32,
|
||||
/// Gesetzt, wenn dieser Frame der Handler eines Ereignis-Traps ist.
|
||||
/// Er läuft im Modulrumpf und **teilt dessen Locals** — ein eigener
|
||||
/// Satz würde dem Handler leere Modulvariablen zeigen. Sein `RETURN`
|
||||
@@ -117,14 +119,14 @@ pub struct Vm {
|
||||
erl: u32,
|
||||
/// Zuletzt durchlaufene numerische Zeilennummer (0 = keine).
|
||||
zeile_nr: u32,
|
||||
module_handler: Handler,
|
||||
module_handlers: Vec<Handler>,
|
||||
in_handler: bool,
|
||||
resume_pc: usize,
|
||||
// Steuerung
|
||||
flags: u32,
|
||||
/// Zählt Anweisungsgrenzen für die regelmäßige Ereignisabholung.
|
||||
tick_zaehler: u32,
|
||||
breakpoints: HashSet<u32>,
|
||||
breakpoints: HashSet<(u16, u32)>,
|
||||
data_ptr: usize,
|
||||
start_pc: Option<usize>,
|
||||
pub forms: FormsModel,
|
||||
@@ -170,7 +172,15 @@ impl Vm {
|
||||
.iter()
|
||||
.map(|t| default_value(t, &module.udts))
|
||||
.collect();
|
||||
let forms = FormsModel::new(module.objects.clone(), 80, 25);
|
||||
let mut forms = FormsModel::new(module.objects.clone(), 80, 25);
|
||||
for initial in &module.form_initial {
|
||||
initial
|
||||
.apply(&mut forms)
|
||||
.expect("validierte Forms-Anfangsdaten");
|
||||
}
|
||||
if let Some(form) = module.startup_form {
|
||||
forms.show(form, false).expect("validiertes Startformular");
|
||||
}
|
||||
let mut vm = Vm {
|
||||
globals,
|
||||
locals: Vec::new(),
|
||||
@@ -180,7 +190,7 @@ impl Vm {
|
||||
err: 0,
|
||||
erl: 0,
|
||||
zeile_nr: 0,
|
||||
module_handler: Handler::None,
|
||||
module_handlers: vec![Handler::None; module.modules.len()],
|
||||
in_handler: false,
|
||||
resume_pc: 0,
|
||||
flags: 0,
|
||||
@@ -202,7 +212,12 @@ impl Vm {
|
||||
let target = self.module.procs[0]
|
||||
.code
|
||||
.iter()
|
||||
.position(|instruction| matches!(instruction, Instr::Stmt(found) if *found == line));
|
||||
.position(|instruction| matches!(instruction, Instr::SetErl(found) if *found == line))
|
||||
.or_else(|| {
|
||||
self.module.procs[0].code.iter().position(
|
||||
|instruction| matches!(instruction, Instr::Stmt(found) if *found == line),
|
||||
)
|
||||
});
|
||||
let target = target.ok_or(RuntimeError(8))?;
|
||||
if self.module.procs[0]
|
||||
.code
|
||||
@@ -229,6 +244,8 @@ impl Vm {
|
||||
}
|
||||
let stack_base = self.stack.len();
|
||||
self.frames.push(Frame {
|
||||
source: 0,
|
||||
column: 0,
|
||||
proc,
|
||||
pc: 0,
|
||||
locals_base,
|
||||
@@ -361,7 +378,12 @@ impl Vm {
|
||||
if !matches!(self.module.procs[proc].code.as_slice(), [Instr::RetProc]) {
|
||||
return Ok(false);
|
||||
}
|
||||
let name = self.module.procs[proc].name.to_ascii_uppercase();
|
||||
let name = self.module.procs[proc]
|
||||
.name
|
||||
.rsplit('!')
|
||||
.next()
|
||||
.unwrap()
|
||||
.to_ascii_uppercase();
|
||||
if !matches!(
|
||||
name.as_str(),
|
||||
"CMNDLGREGISTER"
|
||||
@@ -767,6 +789,8 @@ impl Vm {
|
||||
let locals_base = self.frames[0].locals_base;
|
||||
let stack_base = self.stack.len();
|
||||
self.frames.push(Frame {
|
||||
source: 0,
|
||||
column: 0,
|
||||
proc: 0,
|
||||
pc: ziel as usize,
|
||||
locals_base,
|
||||
@@ -821,17 +845,54 @@ impl Vm {
|
||||
}
|
||||
|
||||
pub fn add_breakpoint(&mut self, line: u32) {
|
||||
self.breakpoints.insert(line);
|
||||
self.breakpoints.insert((0, line));
|
||||
self.flags |= F_BREAK;
|
||||
}
|
||||
|
||||
pub fn remove_breakpoint(&mut self, line: u32) {
|
||||
self.breakpoints.remove(&line);
|
||||
self.breakpoints.remove(&(0, line));
|
||||
if self.breakpoints.is_empty() {
|
||||
self.flags &= !F_BREAK;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_module_breakpoint(&mut self, module: u16, line: u32) {
|
||||
self.breakpoints.insert((module, line));
|
||||
self.flags |= F_BREAK;
|
||||
}
|
||||
|
||||
pub fn remove_module_breakpoint(&mut self, module: u16, line: u32) {
|
||||
self.breakpoints.remove(&(module, line));
|
||||
if self.breakpoints.is_empty() {
|
||||
self.flags &= !F_BREAK;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn current_source_pos(&self) -> tb_frontend::SourcePos {
|
||||
self.frames
|
||||
.last()
|
||||
.map(|f| tb_frontend::SourcePos {
|
||||
source: f.source,
|
||||
line: f.line,
|
||||
column: f.column,
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn current_module(&self) -> u16 {
|
||||
self.module
|
||||
.sources
|
||||
.get(self.current_source_pos().source as usize)
|
||||
.map_or(0, |s| s.module)
|
||||
}
|
||||
|
||||
pub fn current_file(&self) -> &str {
|
||||
self.module
|
||||
.sources
|
||||
.get(self.current_source_pos().source as usize)
|
||||
.map_or(&self.module.name, |s| &s.path)
|
||||
}
|
||||
|
||||
pub fn current_line(&self) -> u32 {
|
||||
self.frames.last().map(|f| f.line).unwrap_or(0)
|
||||
}
|
||||
@@ -843,26 +904,58 @@ impl Vm {
|
||||
.unwrap_or("")
|
||||
}
|
||||
|
||||
/// Variableninspektion (Debugger): erst Locals des obersten Frames
|
||||
/// (Referenzen werden aufgelöst), dann Modulvariablen. Ein
|
||||
/// Typ-Suffix (`n%`, `s$`) wird toleriert — Slots tragen Basisnamen.
|
||||
/// Variableninspektion: lokale Namen vor Modulvariablen, explizite
|
||||
/// Typ-Suffixe vor einer eindeutigen Suche nach dem Basisnamen.
|
||||
pub fn inspect(&self, name: &str) -> Option<Value> {
|
||||
let name = name.trim_end_matches(['%', '&', '!', '#', '$', '@']);
|
||||
fn matches(stored: &str, requested: &str) -> bool {
|
||||
let suffixes = ['%', '&', '!', '#', '$', '@'];
|
||||
stored.eq_ignore_ascii_case(requested)
|
||||
|| (!(stored.ends_with(suffixes) && requested.ends_with(suffixes))
|
||||
&& stored
|
||||
.trim_end_matches(suffixes)
|
||||
.eq_ignore_ascii_case(requested.trim_end_matches(suffixes)))
|
||||
}
|
||||
fn unique(mut ids: impl Iterator<Item = usize>) -> Option<usize> {
|
||||
let first = ids.next()?;
|
||||
ids.next().is_none().then_some(first)
|
||||
}
|
||||
if let Some(f) = self.frames.last() {
|
||||
let p = &self.module.procs[f.proc];
|
||||
for (i, n) in p.local_names.iter().enumerate() {
|
||||
if n.eq_ignore_ascii_case(name) {
|
||||
let v = self.locals[f.locals_base + i].clone();
|
||||
return Some(self.deref_for_inspect(v));
|
||||
}
|
||||
if let Some(i) = unique(
|
||||
p.local_names
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, n)| matches(n, name))
|
||||
.map(|(i, _)| i),
|
||||
) {
|
||||
return Some(self.deref_for_inspect(self.locals[f.locals_base + i].clone()));
|
||||
}
|
||||
}
|
||||
for (i, n) in self.module.global_names.iter().enumerate() {
|
||||
if n.eq_ignore_ascii_case(name) {
|
||||
return Some(self.deref_for_inspect(self.globals[i].clone()));
|
||||
}
|
||||
let qualified = format!(
|
||||
"{}!{name}",
|
||||
self.module.modules[self.current_module() as usize].0
|
||||
);
|
||||
if let Some(id) = unique(
|
||||
self.module
|
||||
.global_names
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, n)| matches(n, name) || matches(n, &qualified))
|
||||
.map(|(i, _)| i),
|
||||
) {
|
||||
return Some(self.globals[id].clone());
|
||||
}
|
||||
None
|
||||
let id = unique(
|
||||
self.module
|
||||
.global_names
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, n)| {
|
||||
matches(n.split_once('!').map_or(n.as_str(), |(_, name)| name), name)
|
||||
})
|
||||
.map(|(i, _)| i),
|
||||
)?;
|
||||
Some(self.globals[id].clone())
|
||||
}
|
||||
|
||||
/// Arrayelement inspizieren.
|
||||
@@ -993,8 +1086,15 @@ impl Vm {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if target.is_none() && self.module_handler != Handler::None {
|
||||
target = Some((0, self.module_handler));
|
||||
if target.is_none() {
|
||||
for frame in self.frames.iter().rev() {
|
||||
let module = self.module.sources[frame.source as usize].module as usize;
|
||||
let handler = self.module_handlers[module];
|
||||
if handler != Handler::None {
|
||||
target = Some((0, handler));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let Some((depth, handler)) = target else {
|
||||
return Some(self.error_event(code, line));
|
||||
@@ -1039,7 +1139,7 @@ impl Vm {
|
||||
let code = &self.module.procs[proc].code;
|
||||
let mut i = from + 1;
|
||||
while i < code.len() {
|
||||
if matches!(code[i], Instr::Stmt(_)) {
|
||||
if matches!(code[i], Instr::Stmt(_) | Instr::InitStmt(_)) {
|
||||
return i;
|
||||
}
|
||||
i += 1;
|
||||
@@ -1189,7 +1289,7 @@ impl Vm {
|
||||
match cell {
|
||||
Value::Arr(a) => Ok(a.clone()),
|
||||
Value::Empty => {
|
||||
let lo = self.module.option_base as i32;
|
||||
let lo = self.module.modules[self.current_module() as usize].1 as i32;
|
||||
let bounds = vec![(lo, 10); dims as usize];
|
||||
let arr = ArrayObj::new(elem.clone(), bounds, &self.module.udts)?;
|
||||
let handle = Rc::new(std::cell::RefCell::new(arr));
|
||||
@@ -1223,7 +1323,9 @@ impl Vm {
|
||||
fn exec(&mut self, instr: Instr, pc: usize, host: &mut dyn Host) -> Result<Flow, RuntimeError> {
|
||||
use Instr as I;
|
||||
match instr {
|
||||
I::Stmt(line) => {
|
||||
I::Source(_, _) => Ok(Flow::Normal),
|
||||
I::Stmt(line) | I::InitStmt(line) => {
|
||||
let initializing = matches!(instr, I::InitStmt(_));
|
||||
if line == 0 {
|
||||
if let Some(target) = self.start_pc.take() {
|
||||
self.frames[0].pc = target;
|
||||
@@ -1231,6 +1333,13 @@ impl Vm {
|
||||
}
|
||||
let f = self.frames.last_mut().unwrap();
|
||||
f.line = line;
|
||||
if let Some(I::Source(source, column)) = pc
|
||||
.checked_sub(1)
|
||||
.and_then(|pc| self.module.procs[f.proc].code.get(pc))
|
||||
{
|
||||
f.source = *source;
|
||||
f.column = *column;
|
||||
}
|
||||
f.last_stmt_pc = pc;
|
||||
// Zustellpunkt: anzeigen, wenn sich der Bildschirm geändert
|
||||
// hat, und regelmäßig Ereignisse abholen. Das ist keine
|
||||
@@ -1238,20 +1347,27 @@ impl Vm {
|
||||
// Größenänderungen kämen nie an.
|
||||
self.tick_zaehler = self.tick_zaehler.wrapping_add(1);
|
||||
self.forms.render(&mut self.rt.screen);
|
||||
if self.rt.screen.ist_veraendert() || self.tick_zaehler.is_multiple_of(1024) {
|
||||
if !initializing
|
||||
&& (self.rt.screen.ist_veraendert() || self.tick_zaehler.is_multiple_of(1024))
|
||||
{
|
||||
self.tick(host);
|
||||
self.rt.screen.veraenderung_quittieren();
|
||||
}
|
||||
let erster_eintritt =
|
||||
std::mem::take(&mut self.frames.last_mut().unwrap().handler_start);
|
||||
if !erster_eintritt && self.zustellen(host, Zustellpunkt::Anweisung) {
|
||||
if !initializing
|
||||
&& !erster_eintritt
|
||||
&& self.zustellen(host, Zustellpunkt::Anweisung)
|
||||
{
|
||||
return Ok(Flow::Normal);
|
||||
}
|
||||
if self.flags != 0 {
|
||||
if self.flags & F_STEP != 0 {
|
||||
return Ok(Flow::Event(RunEvent::Stepped { line }));
|
||||
}
|
||||
if self.flags & F_BREAK != 0 && self.breakpoints.contains(&line) {
|
||||
if self.flags & F_BREAK != 0
|
||||
&& self.breakpoints.contains(&(self.current_module(), line))
|
||||
{
|
||||
return Ok(Flow::Event(RunEvent::Breakpoint { line }));
|
||||
}
|
||||
if self.flags & F_POLL != 0 && self.rt.abbruch {
|
||||
@@ -1328,6 +1444,10 @@ impl Vm {
|
||||
self.push(Value::Lng(v));
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
I::PushUdtId(id) => {
|
||||
self.push(Value::Lng(id as i32));
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
I::PushSng(v) => {
|
||||
self.push(Value::Sng(v));
|
||||
Ok(Flow::Normal)
|
||||
@@ -1716,13 +1836,25 @@ impl Vm {
|
||||
a.data[flat as usize] = v;
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
I::DimArr(global, slot, dims, elem) => {
|
||||
I::DimArr(global, slot, dims, ref elem)
|
||||
| I::CommonArr(global, slot, dims, ref elem) => {
|
||||
let common = matches!(instr, I::CommonArr(..));
|
||||
let bounds = self.pop_bounds(dims)?;
|
||||
let cell = self.slot_value(global, slot);
|
||||
if common {
|
||||
if let Value::Arr(array) = cell {
|
||||
let array = array.borrow();
|
||||
return if &array.elem == elem && array.dims == bounds {
|
||||
Ok(Flow::Normal)
|
||||
} else {
|
||||
Err(RuntimeError::TYPE_MISMATCH)
|
||||
};
|
||||
}
|
||||
}
|
||||
if !matches!(cell, Value::Empty) {
|
||||
return Err(RuntimeError::DUPLICATE_DEFINITION);
|
||||
}
|
||||
let arr = ArrayObj::new(elem, bounds, &self.module.udts)?;
|
||||
let arr = ArrayObj::new(elem.clone(), bounds, &self.module.udts)?;
|
||||
*self.slot_value(global, slot) = Value::Arr(Rc::new(std::cell::RefCell::new(arr)));
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
@@ -2293,7 +2425,8 @@ impl Vm {
|
||||
|
||||
// ---- Fehlerbehandlung ----
|
||||
I::OnErrorGoto(t) => {
|
||||
self.module_handler = Handler::Goto(t);
|
||||
let module = self.current_module() as usize;
|
||||
self.module_handlers[module] = Handler::Goto(t);
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
I::OnErrorLocal(t) => {
|
||||
@@ -2301,7 +2434,8 @@ impl Vm {
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
I::OnErrorDisable => {
|
||||
self.module_handler = Handler::None;
|
||||
let module = self.current_module() as usize;
|
||||
self.module_handlers[module] = Handler::None;
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
I::OnErrorLocalDisable => {
|
||||
@@ -2312,7 +2446,8 @@ impl Vm {
|
||||
if local {
|
||||
self.frames.last_mut().unwrap().local_handler = Handler::ResumeNext;
|
||||
} else {
|
||||
self.module_handler = Handler::ResumeNext;
|
||||
let module = self.current_module() as usize;
|
||||
self.module_handlers[module] = Handler::ResumeNext;
|
||||
}
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
|
||||
@@ -12,20 +12,20 @@ pub mod bytecode;
|
||||
pub mod codegen;
|
||||
pub mod interp;
|
||||
|
||||
use tb_frontend::Diagnostic;
|
||||
pub mod project;
|
||||
pub use project::compile_project;
|
||||
use tb_frontend::{source::SourceUnit, Diagnostic};
|
||||
|
||||
/// Komplette Übersetzung: Quelltext → Bytecode-Modul.
|
||||
/// Bei Diagnosen (Compile-Fehlern) wird kein Kompilat erzeugt.
|
||||
/// Einmodul-API auf derselben Pipeline wie vollständige Projekte.
|
||||
pub fn compile_source(
|
||||
module_name: &str,
|
||||
source: &str,
|
||||
) -> Result<bytecode::CompiledModule, Vec<Diagnostic>> {
|
||||
let analysis = tb_frontend::analyze_source(module_name, source);
|
||||
if !analysis.diagnostics.is_empty() {
|
||||
return Err(analysis.diagnostics);
|
||||
}
|
||||
let hir = analysis.hir.expect("diagnose-frei, aber kein HIR");
|
||||
Ok(codegen::compile(&hir))
|
||||
compile_source_with_forms(
|
||||
module_name,
|
||||
source,
|
||||
&tb_frontend::forms::FormCatalog::default(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn compile_source_with_forms(
|
||||
@@ -33,11 +33,10 @@ pub fn compile_source_with_forms(
|
||||
source: &str,
|
||||
forms: &tb_frontend::forms::FormCatalog,
|
||||
) -> Result<bytecode::CompiledModule, Vec<Diagnostic>> {
|
||||
let analysis = tb_frontend::analyze_source_with_forms(module_name, source, forms);
|
||||
if !analysis.diagnostics.is_empty() {
|
||||
return Err(analysis.diagnostics);
|
||||
}
|
||||
Ok(codegen::compile(
|
||||
&analysis.hir.expect("diagnose-frei, aber kein HIR"),
|
||||
))
|
||||
compile_project(
|
||||
module_name,
|
||||
&[SourceUnit::new(module_name, module_name, source)],
|
||||
forms,
|
||||
&[],
|
||||
)
|
||||
}
|
||||
|
||||
682
crates/tb-vm/src/project.rs
Normal file
682
crates/tb-vm/src/project.rs
Normal file
@@ -0,0 +1,682 @@
|
||||
//! Gemeinsame Tabellenauflösung getrennter Modulübersetzungen.
|
||||
use crate::{
|
||||
bytecode::{CompiledModule, Instr},
|
||||
codegen,
|
||||
};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use tb_frontend::{
|
||||
ast::{Module, Stmt, TypeName},
|
||||
forms::FormCatalog,
|
||||
hir::{HProcKind, HTy},
|
||||
source::{locate_diagnostics, SourceUnit},
|
||||
Diagnostic, SourcePos,
|
||||
};
|
||||
use tb_runtime::value::TypeInit;
|
||||
use tb_ui::frm::FormFile;
|
||||
|
||||
fn diagnostic(message: impl Into<String>) -> Diagnostic {
|
||||
Diagnostic {
|
||||
file: None,
|
||||
pos: SourcePos::default(),
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compile_project(
|
||||
name: &str,
|
||||
units: &[SourceUnit],
|
||||
catalog: &FormCatalog,
|
||||
forms: &[FormFile],
|
||||
) -> Result<CompiledModule, Vec<Diagnostic>> {
|
||||
if units.is_empty() || units.len() > u16::MAX as usize {
|
||||
return Err(vec![diagnostic("Projekt ohne Module oder zu viele Module")]);
|
||||
}
|
||||
let mut sources = Vec::new();
|
||||
let mut diagnostics = Vec::new();
|
||||
let mut names = HashSet::new();
|
||||
let parsed: Vec<_> = units
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, unit)| {
|
||||
let (module, errors) = unit.parse(id as u16, &mut sources);
|
||||
if !names.insert(unit.name.to_uppercase()) {
|
||||
let mut error = diagnostic(format!("Duplicate definition: module {}", unit.name));
|
||||
error.pos = module_pos(&module);
|
||||
error.file = unit.segments.first().map(|s| s.file.clone());
|
||||
diagnostics.push(error);
|
||||
}
|
||||
diagnostics.extend(errors);
|
||||
module
|
||||
})
|
||||
.collect();
|
||||
locate_diagnostics(&mut diagnostics, &sources);
|
||||
if !diagnostics.is_empty() {
|
||||
return Err(diagnostics);
|
||||
}
|
||||
let mut catalog = catalog.clone();
|
||||
if catalog.find("SCREEN").is_none() {
|
||||
catalog.add(
|
||||
"SCREEN",
|
||||
tb_frontend::forms::ObjectClass::Screen,
|
||||
None,
|
||||
false,
|
||||
);
|
||||
}
|
||||
for module in &parsed {
|
||||
if module
|
||||
.body
|
||||
.iter()
|
||||
.any(|s| matches!(s, Stmt::MetaForm { .. }))
|
||||
&& catalog.find(&module.name).is_none()
|
||||
{
|
||||
catalog.add(
|
||||
&module.name,
|
||||
tb_frontend::forms::ObjectClass::Form,
|
||||
None,
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
let mut exports: Vec<_> = parsed
|
||||
.iter()
|
||||
.map(|m| tb_frontend::sema::export_declarations(m, &[]))
|
||||
.collect();
|
||||
// Konstantenabhängigkeiten können beliebig über Module verteilt sein.
|
||||
// Jeder erfolgreiche Durchlauf löst mindestens eine weitere Deklaration auf.
|
||||
let constant_count: usize = exports
|
||||
.iter()
|
||||
.flatten()
|
||||
.filter(|s| matches!(s, Stmt::ConstDecl { .. }))
|
||||
.count();
|
||||
for _ in 0..constant_count {
|
||||
let mut constants: HashMap<_, Vec<_>> = HashMap::new();
|
||||
for stmt in exports.iter().flatten() {
|
||||
if let Stmt::ConstDecl { items, .. } = stmt {
|
||||
constants.entry(&items[0].0).or_default().push(stmt.clone());
|
||||
}
|
||||
}
|
||||
let constants: Vec<_> = constants
|
||||
.into_values()
|
||||
.filter(|s| s.len() == 1)
|
||||
.flatten()
|
||||
.collect();
|
||||
let next: Vec<_> = parsed
|
||||
.iter()
|
||||
.map(|m| tb_frontend::sema::export_declarations(m, &constants))
|
||||
.collect();
|
||||
if next == exports {
|
||||
break;
|
||||
}
|
||||
exports = next;
|
||||
}
|
||||
let mut parts = Vec::new();
|
||||
let mut commons = Vec::new();
|
||||
for module in &parsed {
|
||||
let mut module = module.clone();
|
||||
import_declarations(&mut module, &parsed, &exports);
|
||||
let (hir, errors) = tb_frontend::sema::lower_with_forms(&module, &catalog);
|
||||
diagnostics.extend(errors);
|
||||
if let Some(hir) = hir {
|
||||
parts.push(codegen::compile(&hir));
|
||||
commons.push(hir.commons);
|
||||
}
|
||||
}
|
||||
locate_diagnostics(&mut diagnostics, &sources);
|
||||
if !diagnostics.is_empty() {
|
||||
return Err(diagnostics);
|
||||
}
|
||||
let mut result = link(name, parts, &parsed, &commons).map_err(|error| {
|
||||
let mut errors = vec![error];
|
||||
locate_diagnostics(&mut errors, &sources);
|
||||
errors
|
||||
})?;
|
||||
result.sources = sources;
|
||||
let objects = FormCatalog {
|
||||
objects: result.objects.clone(),
|
||||
};
|
||||
for form in forms {
|
||||
result
|
||||
.form_initial
|
||||
.extend(form.initial_values(&objects).map_err(|e| {
|
||||
vec![diagnostic(format!(
|
||||
"{}: ungültige Forms-Anfangsdaten ({e})",
|
||||
form.root.name
|
||||
))]
|
||||
})?);
|
||||
}
|
||||
result.startup_form = forms
|
||||
.first()
|
||||
.and_then(|form| objects.find(&form.root.name).map(|(id, _)| id));
|
||||
result
|
||||
.validate()
|
||||
.map_err(|e| vec![diagnostic(e.to_string())])?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Lokale Definition gewinnt; außerhalb ihres Moduls muss ein Name eindeutig sein.
|
||||
fn type_definition<'a>(name: &str, origin: usize, all: &'a [Module]) -> Option<(usize, &'a Stmt)> {
|
||||
let mut candidates = all.iter().enumerate().flat_map(|(id, module)| {
|
||||
module.body.iter().filter_map(move |stmt| {
|
||||
matches!(stmt, Stmt::TypeDecl { name: n, .. } if n == name).then_some((id, stmt))
|
||||
})
|
||||
});
|
||||
if let Some(local) = candidates.clone().find(|(id, _)| *id == origin) {
|
||||
return Some(local);
|
||||
}
|
||||
let first = candidates.next()?;
|
||||
candidates.next().is_none().then_some(first)
|
||||
}
|
||||
|
||||
fn same_type(
|
||||
name: &str,
|
||||
a: usize,
|
||||
b: usize,
|
||||
all: &[Module],
|
||||
visiting: &mut HashSet<(usize, usize, String)>,
|
||||
) -> bool {
|
||||
let (
|
||||
Some((a, Stmt::TypeDecl { fields: af, .. })),
|
||||
Some((b, Stmt::TypeDecl { fields: bf, .. })),
|
||||
) = (type_definition(name, a, all), type_definition(name, b, all))
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
if a == b {
|
||||
return true;
|
||||
}
|
||||
if !visiting.insert((a, b, name.into())) {
|
||||
return false;
|
||||
}
|
||||
let equal = af.len() == bf.len()
|
||||
&& af.iter().zip(bf).all(|((an, at), (bn, bt))| {
|
||||
an == bn
|
||||
&& match (at, bt) {
|
||||
(TypeName::Udt(an), TypeName::Udt(bn)) => {
|
||||
an == bn && same_type(an, a, b, all, visiting)
|
||||
}
|
||||
_ => at == bt,
|
||||
}
|
||||
});
|
||||
visiting.remove(&(a, b, name.into()));
|
||||
equal
|
||||
}
|
||||
|
||||
fn import_type(
|
||||
name: &str,
|
||||
origin: usize,
|
||||
target: usize,
|
||||
all: &[Module],
|
||||
imported: &mut Vec<Stmt>,
|
||||
seen: &mut HashMap<(usize, String), String>,
|
||||
) -> String {
|
||||
let Some((origin, Stmt::TypeDecl { fields, pos, .. })) = type_definition(name, origin, all)
|
||||
else {
|
||||
return name.into();
|
||||
};
|
||||
if origin == target
|
||||
|| (type_definition(name, target, all).is_some_and(|(id, _)| id == target)
|
||||
&& same_type(name, origin, target, all, &mut HashSet::new()))
|
||||
{
|
||||
return name.into();
|
||||
}
|
||||
let key = (origin, name.to_string());
|
||||
if let Some(alias) = seen.get(&key) {
|
||||
return alias.clone();
|
||||
}
|
||||
// Öffentliche eindeutige Namen bleiben erhalten. Konfliktbehaftete Abhängigkeiten
|
||||
// bekommen einen internen Modulnamen, damit das lokale Layout unangetastet bleibt.
|
||||
let alias = if type_definition(name, target, all).is_some_and(|(id, _)| id == origin) {
|
||||
name.to_string()
|
||||
} else {
|
||||
format!("{}!{name}", all[origin].name)
|
||||
};
|
||||
seen.insert(key, alias.clone());
|
||||
let fields = fields
|
||||
.iter()
|
||||
.map(|(field, ty)| {
|
||||
let ty = match ty {
|
||||
TypeName::Udt(name) => {
|
||||
TypeName::Udt(import_type(name, origin, target, all, imported, seen))
|
||||
}
|
||||
_ => ty.clone(),
|
||||
};
|
||||
(field.clone(), ty)
|
||||
})
|
||||
.collect();
|
||||
imported.push(Stmt::TypeDecl {
|
||||
name: alias.clone(),
|
||||
fields,
|
||||
pos: *pos,
|
||||
});
|
||||
alias
|
||||
}
|
||||
|
||||
fn import_declarations(module: &mut Module, all: &[Module], exports: &[Vec<Stmt>]) {
|
||||
let target = all.iter().position(|m| m.name == module.name).unwrap();
|
||||
let mut procedures: HashSet<_> = module.procs.iter().map(|p| p.sig.name.clone()).collect();
|
||||
let mut local_constants = HashSet::new();
|
||||
let mut local_types = HashSet::new();
|
||||
for stmt in &module.body {
|
||||
match stmt {
|
||||
Stmt::Declare { sig, .. } => {
|
||||
procedures.insert(sig.name.clone());
|
||||
}
|
||||
Stmt::ConstDecl { items, .. } => {
|
||||
local_constants.extend(items.iter().map(|i| i.0.clone()))
|
||||
}
|
||||
Stmt::TypeDecl { name, .. } => {
|
||||
local_types.insert(name.clone());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let mut foreign: HashMap<_, Vec<_>> = HashMap::new();
|
||||
for (origin, declarations) in exports.iter().enumerate().filter(|(id, _)| *id != target) {
|
||||
for stmt in declarations {
|
||||
let key = match stmt {
|
||||
Stmt::Declare { sig, .. } => (0, sig.name.clone()),
|
||||
Stmt::ConstDecl { items, .. } => (1, items[0].0.clone()),
|
||||
Stmt::TypeDecl { name, .. } => (2, name.clone()),
|
||||
_ => continue,
|
||||
};
|
||||
foreign.entry(key).or_default().push((origin, stmt));
|
||||
}
|
||||
}
|
||||
let mut names: Vec<_> = foreign.keys().cloned().collect();
|
||||
names.sort();
|
||||
let mut imported = Vec::new();
|
||||
let mut seen_types = HashMap::new();
|
||||
for key in names {
|
||||
let candidates = &foreign[&key];
|
||||
if candidates.len() != 1 {
|
||||
continue;
|
||||
}
|
||||
let (origin, stmt) = candidates[0];
|
||||
match stmt {
|
||||
Stmt::Declare { sig, pos } if !procedures.contains(&sig.name) => {
|
||||
let mut sig = sig.clone();
|
||||
for param in &mut sig.params {
|
||||
if let Some(TypeName::Udt(name)) = ¶m.as_type {
|
||||
param.as_type = Some(TypeName::Udt(import_type(
|
||||
name,
|
||||
origin,
|
||||
target,
|
||||
all,
|
||||
&mut imported,
|
||||
&mut seen_types,
|
||||
)));
|
||||
}
|
||||
}
|
||||
imported.push(Stmt::Declare { sig, pos: *pos });
|
||||
}
|
||||
Stmt::ConstDecl { items, .. } if !local_constants.contains(&items[0].0) => {
|
||||
imported.push(stmt.clone())
|
||||
}
|
||||
Stmt::TypeDecl { name, .. } if !local_types.contains(name) => {
|
||||
import_type(name, origin, target, all, &mut imported, &mut seen_types);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
imported.append(&mut module.body);
|
||||
let mut types = Vec::new();
|
||||
imported.retain(|stmt| {
|
||||
if matches!(stmt, Stmt::TypeDecl { .. }) {
|
||||
types.push(stmt.clone());
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
let mut known = HashSet::new();
|
||||
let mut ordered = Vec::new();
|
||||
while !types.is_empty() {
|
||||
let next = types.iter().position(|stmt| match stmt {
|
||||
Stmt::TypeDecl { fields, .. } => fields.iter().all(|(_, ty)| match ty {
|
||||
TypeName::Udt(name) => known.contains(name),
|
||||
_ => true,
|
||||
}),
|
||||
_ => unreachable!(),
|
||||
});
|
||||
let Some(index) = next else { break };
|
||||
let stmt = types.remove(index);
|
||||
if let Stmt::TypeDecl { name, .. } = &stmt {
|
||||
known.insert(name.clone());
|
||||
}
|
||||
ordered.push(stmt);
|
||||
}
|
||||
ordered.extend(types); // Sema diagnostiziert fehlende oder zyklische Typen.
|
||||
ordered.extend(imported);
|
||||
module.body = ordered;
|
||||
}
|
||||
|
||||
fn proc_pos(module: &Module, name: &str) -> SourcePos {
|
||||
module
|
||||
.body
|
||||
.iter()
|
||||
.find_map(|stmt| match stmt {
|
||||
Stmt::Declare { sig, pos } if sig.name == name => Some(*pos),
|
||||
_ => None,
|
||||
})
|
||||
.or_else(|| {
|
||||
module
|
||||
.procs
|
||||
.iter()
|
||||
.find(|p| p.sig.name == name)
|
||||
.map(|p| p.pos)
|
||||
})
|
||||
.unwrap_or_else(|| module_pos(module))
|
||||
}
|
||||
fn module_pos(module: &Module) -> SourcePos {
|
||||
module
|
||||
.body
|
||||
.iter()
|
||||
.map(tb_frontend::sema::stmt_pos)
|
||||
.find(|p| p.line > 0)
|
||||
.or_else(|| module.procs.first().map(|p| p.pos))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn remap_type(ty: &mut TypeInit, ids: &[u16]) {
|
||||
if let TypeInit::Udt(id) = ty {
|
||||
*id = ids[*id as usize];
|
||||
}
|
||||
}
|
||||
fn remap_signature(ty: &mut HTy, ids: &[u16]) {
|
||||
if let HTy::Udt(id) = ty {
|
||||
*id = ids[*id as usize];
|
||||
}
|
||||
}
|
||||
|
||||
fn link(
|
||||
name: &str,
|
||||
mut parts: Vec<CompiledModule>,
|
||||
ast: &[Module],
|
||||
module_commons: &[Vec<tb_frontend::hir::HCommon>],
|
||||
) -> Result<CompiledModule, Diagnostic> {
|
||||
let at = |pos, message| Diagnostic {
|
||||
file: None,
|
||||
pos,
|
||||
message,
|
||||
};
|
||||
let mut result = codegen::compile(&tb_frontend::analyze_source(name, "").hir.unwrap());
|
||||
result.modules = parts
|
||||
.iter()
|
||||
.map(|p| (p.name.clone(), p.option_base))
|
||||
.collect();
|
||||
result.objects = parts[0].objects.clone();
|
||||
result.strings.clear();
|
||||
result.procs.clear();
|
||||
result.option_base = parts[0].option_base;
|
||||
let mut proc_maps = Vec::new();
|
||||
let mut count = 1usize;
|
||||
let mut definitions: HashMap<String, Vec<u16>> = HashMap::new();
|
||||
for (part, module) in parts.iter().zip(ast) {
|
||||
let defined: HashSet<_> = module.procs.iter().map(|p| p.sig.name.as_str()).collect();
|
||||
let mut map = vec![0];
|
||||
for p in part.procs.iter().skip(1) {
|
||||
let id = u16::try_from(count)
|
||||
.map_err(|_| at(proc_pos(module, &p.name), "Zu viele Prozeduren".into()))?;
|
||||
count += 1;
|
||||
map.push(id);
|
||||
if defined.contains(p.name.as_str()) || p.kind == HProcKind::DefFn {
|
||||
definitions.entry(p.name.clone()).or_default().push(id);
|
||||
}
|
||||
}
|
||||
proc_maps.push(map);
|
||||
}
|
||||
// DECLARE-Platzhalter auf die tatsächliche, eindeutig bestimmte Definition binden.
|
||||
for (module_id, part) in parts.iter().enumerate() {
|
||||
let defined: HashSet<_> = ast[module_id]
|
||||
.procs
|
||||
.iter()
|
||||
.map(|p| p.sig.name.as_str())
|
||||
.collect();
|
||||
for (id, proc) in part.procs.iter().enumerate().skip(1) {
|
||||
if !defined.contains(proc.name.as_str()) && proc.kind != HProcKind::DefFn {
|
||||
if let Some(candidates) = definitions.get(&proc.name) {
|
||||
if candidates.len() != 1 {
|
||||
return Err(at(
|
||||
proc_pos(&ast[module_id], &proc.name),
|
||||
format!("Ambiguous subprogram: {}", proc.name),
|
||||
));
|
||||
}
|
||||
proc_maps[module_id][id] = candidates[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let splits: Vec<_> = parts
|
||||
.iter()
|
||||
.map(|p| {
|
||||
p.procs[0]
|
||||
.code
|
||||
.iter()
|
||||
.position(|i| matches!(i, Instr::Stmt(0)))
|
||||
.map_or(0, |i| i.saturating_sub(1))
|
||||
})
|
||||
.collect();
|
||||
let mut init_starts = Vec::new();
|
||||
let mut body_starts = Vec::new();
|
||||
let mut pc = 0;
|
||||
for split in &splits {
|
||||
init_starts.push(pc);
|
||||
pc += split;
|
||||
}
|
||||
for (part, split) in parts.iter().zip(&splits) {
|
||||
body_starts.push(pc);
|
||||
pc += part.procs[0].code.len() - split - 1;
|
||||
}
|
||||
let mut main = parts[0].procs[0].clone();
|
||||
main.code.clear();
|
||||
main.name = name.into();
|
||||
let mut initializers = Vec::new();
|
||||
let mut bodies = Vec::new();
|
||||
let mut common: HashMap<_, (u16, tb_frontend::hir::HCommon)> = HashMap::new();
|
||||
for (module_id, part) in parts.iter_mut().enumerate() {
|
||||
let mut types = Vec::new();
|
||||
for udt in &part.udts {
|
||||
let mut udt = udt.clone();
|
||||
udt.name = udt.name.rsplit('!').next().unwrap().to_string();
|
||||
for ty in &mut udt.fields {
|
||||
remap_type(ty, &types);
|
||||
}
|
||||
let id = result
|
||||
.udts
|
||||
.iter()
|
||||
.position(|u| u.name == udt.name && u.fields == udt.fields)
|
||||
.unwrap_or_else(|| {
|
||||
result.udts.push(udt);
|
||||
result.udts.len() - 1
|
||||
});
|
||||
types.push(
|
||||
u16::try_from(id)
|
||||
.map_err(|_| at(module_pos(&ast[module_id]), "Zu viele TYPEs".into()))?,
|
||||
);
|
||||
}
|
||||
let commons: HashMap<_, _> = module_commons[module_id]
|
||||
.iter()
|
||||
.map(|c| (c.slot, c))
|
||||
.collect();
|
||||
let mut globals = Vec::new();
|
||||
for (slot, (ty, name)) in part.globals_init.iter().zip(&part.global_names).enumerate() {
|
||||
let mut ty = ty.clone();
|
||||
remap_type(&mut ty, &types);
|
||||
let declaration = commons.get(&(slot as u16));
|
||||
let key = declaration.map(|c| (c.block.clone(), c.key.clone()));
|
||||
let mut common_ty = declaration.map(|c| c.ty.clone());
|
||||
if let Some(ty) = &mut common_ty {
|
||||
remap_signature(ty, &types);
|
||||
}
|
||||
let id = if let Some((id, previous)) = key.as_ref().and_then(|key| common.get_mut(key))
|
||||
{
|
||||
let declaration = declaration.unwrap();
|
||||
let compatible_dims = match (&previous.dims, &declaration.dims) {
|
||||
(None, None) => true,
|
||||
(Some(a), Some(b)) if a.is_empty() || b.is_empty() => true,
|
||||
(Some(a), Some(b)) => {
|
||||
a.len() == b.len()
|
||||
&& a.iter().zip(b).all(|((al, ah), (bl, bh))| {
|
||||
al.zip(*bl).is_none_or(|(a, b)| a == b)
|
||||
&& ah.zip(*bh).is_none_or(|(a, b)| a == b)
|
||||
})
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
if Some(&previous.ty) != common_ty.as_ref() || !compatible_dims {
|
||||
return Err(at(
|
||||
declaration.pos,
|
||||
format!("COMMON type or bounds mismatch: {name}"),
|
||||
));
|
||||
}
|
||||
if let (Some(previous), Some(current)) = (&mut previous.dims, &declaration.dims) {
|
||||
if previous.is_empty() {
|
||||
*previous = current.clone();
|
||||
} else {
|
||||
for ((lo, hi), (new_lo, new_hi)) in previous.iter_mut().zip(current) {
|
||||
*lo = lo.or(*new_lo);
|
||||
*hi = hi.or(*new_hi);
|
||||
}
|
||||
}
|
||||
}
|
||||
*id
|
||||
} else {
|
||||
let id = u16::try_from(result.globals_init.len()).map_err(|_| {
|
||||
at(
|
||||
module_pos(&ast[module_id]),
|
||||
"Zu viele globale Variablen".into(),
|
||||
)
|
||||
})?;
|
||||
result.globals_init.push(ty);
|
||||
result.global_names.push(if ast.len() == 1 {
|
||||
name.clone()
|
||||
} else {
|
||||
format!("{}!{name}", part.name)
|
||||
});
|
||||
if let Some(key) = key {
|
||||
let mut declaration = (*declaration.unwrap()).clone();
|
||||
declaration.ty = common_ty.unwrap();
|
||||
common.insert(key, (id, declaration));
|
||||
}
|
||||
id
|
||||
};
|
||||
globals.push(id);
|
||||
}
|
||||
let string_offset = result.strings.len();
|
||||
if string_offset + part.strings.len() >= u16::MAX as usize {
|
||||
return Err(at(
|
||||
module_pos(&ast[module_id]),
|
||||
"Zu viele Stringkonstanten".into(),
|
||||
));
|
||||
}
|
||||
result.strings.append(&mut part.strings);
|
||||
let data_offset = result.data.len() as u32;
|
||||
result.data.append(&mut part.data);
|
||||
let jump_offset = result.jump_tables.len();
|
||||
if jump_offset + part.jump_tables.len() > u16::MAX as usize {
|
||||
return Err(at(
|
||||
module_pos(&ast[module_id]),
|
||||
"Zu viele Sprungtabellen".into(),
|
||||
));
|
||||
}
|
||||
let main_pc = |pc: u32| if (pc as usize) < splits[module_id] { init_starts[module_id] + pc as usize } else { body_starts[module_id] + pc as usize - splits[module_id] } as u32;
|
||||
for (proc_id, proc) in part.procs.iter_mut().enumerate() {
|
||||
proc.module = module_id as u16;
|
||||
for ty in &mut proc.locals_init {
|
||||
remap_type(ty, &types);
|
||||
}
|
||||
for param in &mut proc.params {
|
||||
remap_signature(&mut param.ty, &types);
|
||||
}
|
||||
if let Some(ty) = &mut proc.ret_ty {
|
||||
remap_signature(ty, &types);
|
||||
}
|
||||
if ast.len() > 1 {
|
||||
proc.name = format!("{}!{}", part.name, proc.name);
|
||||
}
|
||||
for instruction in &mut proc.code {
|
||||
use Instr::*;
|
||||
match instruction {
|
||||
PushStr(id)
|
||||
| Unsupported(id)
|
||||
| LoadDynamicObjectProperty(id)
|
||||
| StoreDynamicObjectProperty(id) => *id += string_offset as u16,
|
||||
Input(_, _, id, _) if *id != u16::MAX => *id += string_offset as u16,
|
||||
LoadGlobal(id) | StoreGlobal(id) | MakeRefGlobal(id) => {
|
||||
*id = globals[*id as usize]
|
||||
}
|
||||
LoadArr(global, id, _, ty)
|
||||
| DimArr(global, id, _, ty)
|
||||
| CommonArr(global, id, _, ty)
|
||||
| RedimArr(global, id, _, ty) => {
|
||||
if *global {
|
||||
*id = globals[*id as usize];
|
||||
}
|
||||
remap_type(ty, &types);
|
||||
}
|
||||
EraseSlot(true, id) => *id = globals[*id as usize],
|
||||
GetPut(_, _, 7, id) | PushUdtId(id) => *id = types[*id as usize],
|
||||
Call(id, _) => *id = proc_maps[module_id][*id as usize],
|
||||
Restore(id) => *id += data_offset,
|
||||
OnErrorGoto(pc) => *pc = main_pc(*pc),
|
||||
Jump(pc)
|
||||
| JumpIfFalse(pc)
|
||||
| JumpIfTrue(pc)
|
||||
| Gosub(pc)
|
||||
| RetGosubTo(pc)
|
||||
| OnErrorLocal(pc)
|
||||
| ResumeLabel(pc)
|
||||
| TrapDefine(_, pc)
|
||||
if proc_id == 0 =>
|
||||
{
|
||||
*pc = main_pc(*pc)
|
||||
}
|
||||
OnJump(id, _) => {
|
||||
if proc_id == 0 {
|
||||
for target in &mut part.jump_tables[*id as usize] {
|
||||
*target = main_pc(*target);
|
||||
}
|
||||
}
|
||||
*id += jump_offset as u16;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
initializers.extend_from_slice(&part.procs[0].code[..splits[module_id]]);
|
||||
bodies.extend_from_slice(
|
||||
&part.procs[0].code[splits[module_id]..part.procs[0].code.len() - 1],
|
||||
);
|
||||
result.procs.extend(part.procs.iter().skip(1).cloned());
|
||||
for mut e in part.event_procs.drain(..) {
|
||||
e.proc = proc_maps[module_id][e.proc as usize];
|
||||
result.event_procs.push(e);
|
||||
}
|
||||
result.jump_tables.append(&mut part.jump_tables);
|
||||
}
|
||||
initializers.extend(bodies);
|
||||
initializers.push(Instr::End);
|
||||
main.code = initializers;
|
||||
result.procs.insert(0, main);
|
||||
// Vollständige Signaturen zwischen DECLARE-Platzhalter und Ziel vergleichen.
|
||||
for (module, part) in parts.iter().enumerate() {
|
||||
for (id, proc) in part.procs.iter().enumerate().skip(1) {
|
||||
let target = &result.procs[proc_maps[module][id] as usize];
|
||||
if proc.ret_ty != target.ret_ty
|
||||
|| proc.kind != target.kind
|
||||
|| proc.params.len() != target.params.len()
|
||||
|| proc
|
||||
.params
|
||||
.iter()
|
||||
.zip(&target.params)
|
||||
.any(|(a, b)| a.ty != b.ty || a.array != b.array || a.by_ref != b.by_ref)
|
||||
{
|
||||
return Err(at(
|
||||
proc_pos(&ast[module], proc.name.rsplit('!').next().unwrap()),
|
||||
format!("Parameter type mismatch: {}", proc.name),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
299
crates/tb-vm/tests/project.rs
Normal file
299
crates/tb-vm/tests/project.rs
Normal file
@@ -0,0 +1,299 @@
|
||||
use tb_frontend::{
|
||||
forms::FormCatalog,
|
||||
source::{SourceSegment, SourceUnit},
|
||||
};
|
||||
use tb_runtime::{host::CaptureHost, value::Value};
|
||||
use tb_ui::frm::{self, FormFile};
|
||||
use tb_vm::{
|
||||
bytecode::CompiledModule,
|
||||
compile_project,
|
||||
interp::{RunEvent, Vm},
|
||||
};
|
||||
|
||||
fn compile(units: &[SourceUnit], forms: &[FormFile]) -> CompiledModule {
|
||||
let mut catalog = FormCatalog::default();
|
||||
for form in forms {
|
||||
catalog.append(&form.catalog());
|
||||
}
|
||||
compile_project("APP", units, &catalog, forms).unwrap_or_else(|d| panic!("{d:#?}"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn module_includes_erl_breakpoints_und_inspektion_behalten_ihren_ursprung() {
|
||||
let a = SourceUnit::new("MAIN", "main.bas", "x%=1\nCALL Fehler\nEND\n");
|
||||
let b = SourceUnit {
|
||||
name: "LIB".into(),
|
||||
segments: vec![
|
||||
SourceSegment {
|
||||
file: "lib.bas".into(),
|
||||
first_line: 1,
|
||||
text: "SUB Fehler\n".into(),
|
||||
},
|
||||
SourceSegment {
|
||||
file: "nested.bi".into(),
|
||||
first_line: 2,
|
||||
text: "200 ERROR 6\n".into(),
|
||||
},
|
||||
SourceSegment {
|
||||
file: "lib.bas".into(),
|
||||
first_line: 3,
|
||||
text: "END SUB\n".into(),
|
||||
},
|
||||
],
|
||||
};
|
||||
let module = compile(&[a, b], &[]);
|
||||
let bytes = module.to_tbc();
|
||||
let loaded = CompiledModule::from_tbc(&bytes).unwrap();
|
||||
assert_eq!(loaded.to_tbc(), bytes);
|
||||
let mut vm = Vm::new(loaded);
|
||||
let mut host = CaptureHost::default();
|
||||
vm.add_module_breakpoint(1, 2);
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 2 });
|
||||
assert_eq!(vm.current_module(), 1);
|
||||
assert_eq!(vm.current_file(), "nested.bi");
|
||||
assert_eq!(vm.current_source_pos().column, 1);
|
||||
assert!(matches!(vm.inspect("MAIN!x"), Some(Value::Int(1))));
|
||||
vm.remove_module_breakpoint(1, 2);
|
||||
vm.set_step(true);
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Stepped { line: 2 });
|
||||
assert_eq!(vm.current_file(), "nested.bi");
|
||||
assert_eq!(vm.current_source_pos().column, 5);
|
||||
vm.set_step(false);
|
||||
assert!(matches!(
|
||||
vm.run(&mut host),
|
||||
RunEvent::Error {
|
||||
code: 6,
|
||||
line: 2,
|
||||
..
|
||||
}
|
||||
));
|
||||
let mut erl_vm = Vm::new(compile(
|
||||
&[SourceUnit::new(
|
||||
"NUM",
|
||||
"num.bas",
|
||||
"ON ERROR RESUME NEXT\n200 ERROR 6\nn%=ERL\nSTOP",
|
||||
)],
|
||||
&[],
|
||||
));
|
||||
assert!(matches!(erl_vm.run(&mut host), RunEvent::Stopped { .. }));
|
||||
assert!(matches!(erl_vm.inspect("n"), Some(Value::Int(200))));
|
||||
assert_eq!(vm.current_file(), "nested.bi");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diagnose_im_include_und_echte_duplikate_bleiben_sichtbar() {
|
||||
let source = SourceUnit {
|
||||
name: "LIB".into(),
|
||||
segments: vec![SourceSegment {
|
||||
file: "nested.bi".into(),
|
||||
first_line: 2,
|
||||
text: "Text$ = 42\n".into(),
|
||||
}],
|
||||
};
|
||||
let errors = compile_project("APP", &[source], &FormCatalog::default(), &[]).unwrap_err();
|
||||
assert!(errors
|
||||
.iter()
|
||||
.any(|e| e.file.as_deref() == Some("nested.bi") && e.pos.line == 2 && e.pos.column == 1));
|
||||
for text in [
|
||||
"SUB X\nEND SUB\nSUB X\nEND SUB",
|
||||
"CONST N=1\nCONST N=2",
|
||||
"TYPE T\nx AS INTEGER\nEND TYPE\nTYPE T\nx AS INTEGER\nEND TYPE",
|
||||
] {
|
||||
let errors = compile_project(
|
||||
"APP",
|
||||
&[SourceUnit::new("M", "m.bas", text)],
|
||||
&FormCatalog::default(),
|
||||
&[],
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
errors
|
||||
.iter()
|
||||
.any(|e| e.message.contains("Duplicate definition")),
|
||||
"{errors:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn initialisierungen_def_fn_und_modulhandler_haben_eigene_quellorte() {
|
||||
for (source, expected_line) in [
|
||||
("' Bibliothek\nDIM a%(1 TO 0)", 2),
|
||||
(
|
||||
"' Bibliothek\nDEF FNkaputt(x)=1/x\nSUB Fehler\nPRINT FNkaputt(0)\nEND SUB",
|
||||
2,
|
||||
),
|
||||
] {
|
||||
let module = compile(
|
||||
&[
|
||||
SourceUnit::new("A", "a.bas", "CALL Fehler\nEND"),
|
||||
SourceUnit::new(
|
||||
"B",
|
||||
"b.bas",
|
||||
&format!(
|
||||
"{source}\n{}",
|
||||
if source.contains("SUB") {
|
||||
""
|
||||
} else {
|
||||
"SUB Fehler\nEND SUB"
|
||||
}
|
||||
),
|
||||
),
|
||||
],
|
||||
&[],
|
||||
);
|
||||
let mut vm = Vm::new(CompiledModule::from_tbc(&module.to_tbc()).unwrap());
|
||||
let event = vm.run(&mut CaptureHost::default());
|
||||
assert!(
|
||||
matches!(event, RunEvent::Error { line, .. } if line == expected_line),
|
||||
"{event:?}"
|
||||
);
|
||||
assert_eq!(vm.current_file(), "b.bas");
|
||||
}
|
||||
let units = [
|
||||
SourceUnit::new(
|
||||
"A",
|
||||
"a.bas",
|
||||
"ON ERROR GOTO H\nCALL Fehler\nEND\nH:\nPRINT ERR;ERL\nEND",
|
||||
),
|
||||
SourceUnit::new(
|
||||
"B",
|
||||
"b.bas",
|
||||
"SUB Fehler\nON ERROR GOTO 0\n200 ERROR 6\nEND SUB",
|
||||
),
|
||||
];
|
||||
let mut vm = Vm::new(compile(&units, &[]));
|
||||
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
|
||||
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen), " 6 200 \n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lokale_prozeduren_shared_common_und_wiederholte_includes_bleiben_getrennt() {
|
||||
let a = SourceUnit::new("A", "a.bas", "CONST N=1\nDIM SHARED x%\nCOMMON SHARED c%\nx%=10\nCALL Privat\nCALL Zweites\nPRINT x%;c%\nEND\nSUB Privat\nx%=x%+N\nc%=c%+1\nEND SUB");
|
||||
let b = SourceUnit::new("B", "b.bas", "CONST N=2\nDIM SHARED x%\nCOMMON SHARED c%\nSUB Zweites\nx%=20\nCALL Privat\nPRINT x%\nEND SUB\nSUB Privat\nx%=x%+N\nc%=c%+2\nEND SUB");
|
||||
let mut vm = Vm::new(compile(&[a, b], &[]));
|
||||
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
|
||||
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen), " 22 \n 11 3 \n");
|
||||
let include = SourceSegment {
|
||||
file: "shared.bi".into(),
|
||||
first_line: 1,
|
||||
text: "CONST N=7\n".into(),
|
||||
};
|
||||
let units: Vec<_> = ["A", "B"]
|
||||
.iter()
|
||||
.map(|name| SourceUnit {
|
||||
name: (*name).into(),
|
||||
segments: vec![
|
||||
include.clone(),
|
||||
SourceSegment {
|
||||
file: format!("{name}.bas"),
|
||||
first_line: 2,
|
||||
text: "PRINT N\n".into(),
|
||||
},
|
||||
],
|
||||
})
|
||||
.collect();
|
||||
let mut vm = Vm::new(compile(&units, &[]));
|
||||
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
|
||||
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen), " 7 \n 7 \n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typreferenzen_signaturen_und_option_base_ueber_modulgrenzen() {
|
||||
use tb_frontend::hir::{HProcKind, HTy, NumTy};
|
||||
use tb_vm::bytecode::Instr;
|
||||
let units = [
|
||||
SourceUnit::new("A", "a.bas", "DECLARE SUB CmnDlgRegister(ok AS INTEGER)\nTYPE Klein\nx AS INTEGER\nEND TYPE\nDIM s AS Gross\nDIM a%(2)\ns.x=4\na%(1)=3\nCmnDlgRegister ok%\nPRINT ok%\nPRINT Summe%(a%(), s, 2)\nPRINT s.x\nCALL Basis\nEND"),
|
||||
SourceUnit::new("B", "b.bas", "OPTION BASE 1\nTYPE Gross\nx AS LONG\ny AS STRING * 8\nEND TYPE\nFUNCTION Summe%(a%(), s AS Gross, n%)\nSumme%=a%(1)+s.x+n%\ns.x=s.x+1\nEND FUNCTION\nSUB Basis\nb%(1)=1\nPRINT LBOUND(b%)\nEND SUB\nSUB Datei\nOPEN \"unbenutzt.isam\" FOR ISAM Gross \"T\" AS #1\nCLOSE #1\nEND SUB"),
|
||||
];
|
||||
let module = compile(&units, &[]);
|
||||
let bytes = module.to_tbc();
|
||||
let loaded = CompiledModule::from_tbc(&bytes).unwrap();
|
||||
assert_eq!(bytes, loaded.to_tbc());
|
||||
let signature = loaded.procs.iter().find(|p| p.name == "B!SUMME").unwrap();
|
||||
assert_eq!(signature.kind, HProcKind::Function);
|
||||
assert_eq!(signature.ret_ty, Some(HTy::Num(NumTy::Int)));
|
||||
assert!(signature.params[0].array && !signature.params[0].by_ref);
|
||||
assert!(signature.params[2].by_ref);
|
||||
assert!(signature.params[1].by_ref);
|
||||
let HTy::Udt(udt) = signature.params[1].ty else {
|
||||
panic!("UDT-Parameter fehlt")
|
||||
};
|
||||
assert_eq!(loaded.udts[udt as usize].name, "GROSS");
|
||||
let file = loaded.procs.iter().find(|p| p.name == "B!DATEI").unwrap();
|
||||
assert!(file.code.contains(&Instr::PushUdtId(udt)));
|
||||
let mut vm = Vm::new(loaded);
|
||||
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
|
||||
assert_eq!(
|
||||
tb_runtime::snapshot::text(&vm.rt.screen),
|
||||
"-1 \n 9 \n 5 \n 1 \n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn beschaedigte_container_werden_vor_der_ausfuehrung_abgewiesen() {
|
||||
use tb_vm::bytecode::Instr;
|
||||
let form = frm::read_text("f.frm", "VERSION 1.00\nBEGIN Form F\n BEGIN TextBox Text1\n Index = 2\n Text = \"hello\"\n END\nEND\n").unwrap();
|
||||
let bytes = compile(&[SourceUnit::new("F", "f.frm", "END")], &[form]).to_tbc();
|
||||
for length in 0..bytes.len() {
|
||||
assert!(
|
||||
CompiledModule::from_tbc(&bytes[..length]).is_err(),
|
||||
"Länge {length}"
|
||||
);
|
||||
}
|
||||
for (offset, value) in [(6, 1u32), (8, u32::MAX), (16, 0), (20, u32::MAX)] {
|
||||
let mut bad = bytes.clone();
|
||||
bad[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
|
||||
assert!(CompiledModule::from_tbc(&bad).is_err(), "Offset {offset}");
|
||||
}
|
||||
let mut duplicate = bytes.clone();
|
||||
duplicate[24..28].copy_from_slice(b"MODN");
|
||||
assert!(CompiledModule::from_tbc(&duplicate).is_err());
|
||||
for mutation in 0..8 {
|
||||
let mut bad = CompiledModule::from_tbc(&bytes).unwrap();
|
||||
match mutation {
|
||||
0 => bad.sources[0].module = u16::MAX,
|
||||
1 => bad.procs[0].module = u16::MAX,
|
||||
2 => bad.procs[0].code.push(Instr::Call(u16::MAX, 0)),
|
||||
3 => bad.procs[0].n_params = 1,
|
||||
4 => bad.objects[1].parent = Some(1),
|
||||
5 => bad.form_initial[1].index = 3,
|
||||
6 => {
|
||||
bad.form_initial[1].properties.insert(
|
||||
0,
|
||||
tb_ui::forms::PropertyValue::Object(Some((u16::MAX, None))),
|
||||
);
|
||||
}
|
||||
7 => bad.procs[0].code.push(Instr::Source(u32::MAX, 1)),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
assert!(
|
||||
CompiledModule::from_tbc(&bad.to_tbc()).is_err(),
|
||||
"Mutation {mutation}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zwei_formulare_mit_gleichen_controls_und_arrays_laufen_aus_dem_kompilat() {
|
||||
let form = |name: &str, text: &str| {
|
||||
frm::read_text(&format!("{name}.frm"), &format!(
|
||||
"VERSION 1.00\nBEGIN Form {name}\n Width = 30\n Height = 10\n BEGIN Frame Frame1\n BEGIN TextBox Text1\n Text = \"{text}\"\n END\n END\n BEGIN TextBox Feld\n Index = 0\n Text = \"null\"\n END\n BEGIN TextBox Feld\n Index = 2\n Text = \"zwei\"\n END\nEND\n\nSUB Form_Load\nText1.Text = Text1.Text + \"!\"\nEND SUB\n"
|
||||
)).unwrap()
|
||||
};
|
||||
let forms = [form("Form1", "a"), form("Form2", "b")];
|
||||
let mut units = vec![SourceUnit::new("MAIN", "main.bas", "Form2.Show\na$=Form1!Text1.Text\nb$=Form2!Text1.Text\nc$=Form2!Feld(2).Text\nForm1.Hide\nForm2.Hide\nCLS\nPRINT a$\nPRINT b$\nPRINT c$\nEND")];
|
||||
units.extend(
|
||||
forms
|
||||
.iter()
|
||||
.map(|f| SourceUnit::new(&f.root.name, &format!("{}.frm", f.root.name), &f.code)),
|
||||
);
|
||||
let module = compile(&units, &forms);
|
||||
assert_eq!(module.event_procs.len(), 2);
|
||||
let bytes = module.to_tbc();
|
||||
let loaded = CompiledModule::from_tbc(&bytes).unwrap();
|
||||
assert_eq!(loaded.to_tbc(), bytes);
|
||||
let mut vm = Vm::new(loaded);
|
||||
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
|
||||
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen), "a!\nb!\nzwei\n");
|
||||
}
|
||||
548
crates/tb-vm/tests/project_regressions.rs
Normal file
548
crates/tb-vm/tests/project_regressions.rs
Normal file
@@ -0,0 +1,548 @@
|
||||
//! Unveränderte ursprüngliche Reviewproben und ergänzende Regressionen des Projektkompilats.
|
||||
use tb_frontend::{
|
||||
forms::FormCatalog,
|
||||
source::{SourceSegment, SourceUnit},
|
||||
};
|
||||
use tb_runtime::{host::CaptureHost, value::Value};
|
||||
use tb_vm::{
|
||||
bytecode::CompiledModule,
|
||||
compile_project,
|
||||
interp::{RunEvent, Vm},
|
||||
};
|
||||
|
||||
fn units(files: &[(&str, &str)]) -> Vec<SourceUnit> {
|
||||
files
|
||||
.iter()
|
||||
.map(|(name, code)| SourceUnit::new(name, &format!("{name}.bas"), code))
|
||||
.collect()
|
||||
}
|
||||
fn compile(files: &[(&str, &str)]) -> Result<CompiledModule, Vec<tb_frontend::Diagnostic>> {
|
||||
compile_project("APP", &units(files), &FormCatalog::default(), &[])
|
||||
}
|
||||
fn run(module: CompiledModule) -> (RunEvent, String) {
|
||||
let bytes = module.to_tbc();
|
||||
let loaded = CompiledModule::from_tbc(&bytes).unwrap();
|
||||
assert_eq!(bytes, loaded.to_tbc());
|
||||
let mut vm = Vm::new(loaded);
|
||||
let event = vm.run(&mut CaptureHost::default());
|
||||
(event, tb_runtime::snapshot::text(&vm.rt.screen))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v1_zweige_und_schleifen_behalten_eigene_quellorte() {
|
||||
let mut failures = vec![];
|
||||
for (name, code, line) in [
|
||||
(
|
||||
"ELSEIF",
|
||||
"IF 0 THEN\nPRINT 1\nELSEIF 1/0 THEN\nPRINT 2\nEND IF",
|
||||
3,
|
||||
),
|
||||
("CASE", "SELECT CASE 1\nCASE 1/0\nPRINT 2\nEND SELECT", 2),
|
||||
("NEXT", "FOR i%=32767 TO 32767\nPRINT 1\nNEXT", 3),
|
||||
("LOOP", "DO\nPRINT 1\nLOOP UNTIL 1/0", 3),
|
||||
] {
|
||||
let (event, _) = run(compile(&[("MAIN", code)]).unwrap());
|
||||
if !matches!(event, RunEvent::Error { line: found, .. } if found == line) {
|
||||
failures.push(format!("{name}: erwartet Zeile {line}, erhalten {event:?}"));
|
||||
}
|
||||
}
|
||||
let source = SourceUnit {
|
||||
name: "MAIN".into(),
|
||||
segments: vec![
|
||||
SourceSegment {
|
||||
file: "main.bas".into(),
|
||||
first_line: 1,
|
||||
text: "IF 0 THEN\nPRINT 1\n".into(),
|
||||
},
|
||||
SourceSegment {
|
||||
file: "cond.bi".into(),
|
||||
first_line: 1,
|
||||
text: "ELSEIF 1/0 THEN\nPRINT 2\n".into(),
|
||||
},
|
||||
SourceSegment {
|
||||
file: "main.bas".into(),
|
||||
first_line: 4,
|
||||
text: "END IF\n".into(),
|
||||
},
|
||||
],
|
||||
};
|
||||
let mut vm = Vm::new(compile_project("APP", &[source], &FormCatalog::default(), &[]).unwrap());
|
||||
let event = vm.run(&mut CaptureHost::default());
|
||||
if vm.current_file() != "cond.bi" {
|
||||
failures.push(format!(
|
||||
"Include: {event:?} aus {} statt cond.bi",
|
||||
vm.current_file()
|
||||
));
|
||||
}
|
||||
// Das gleiche Problem darf nicht nur durch Umbenennen der Fehlermeldung behoben werden.
|
||||
let mut vm = Vm::new(
|
||||
compile(&[("MAIN", "IF 0 THEN\nPRINT 1\nELSEIF 1 THEN\nPRINT 2\nEND IF")]).unwrap(),
|
||||
);
|
||||
vm.add_module_breakpoint(0, 3);
|
||||
let event = vm.run(&mut CaptureHost::default());
|
||||
if event != (RunEvent::Breakpoint { line: 3 }) {
|
||||
failures.push(format!("Breakpoint ELSEIF: {event:?}"));
|
||||
}
|
||||
assert!(failures.is_empty(), "{}", failures.join("\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v2_importierte_deklarationen_behalten_kontext_und_abhaengigkeiten() {
|
||||
let cases = [
|
||||
(
|
||||
"DEFINT",
|
||||
"PRINT Doppelt(3)\nEND",
|
||||
"DEFINT A-Z\nFUNCTION Doppelt(x)\nDoppelt=x*2\nEND FUNCTION",
|
||||
" 6 \n",
|
||||
),
|
||||
(
|
||||
"CONST",
|
||||
"CONST A=3\nPRINT B\nEND",
|
||||
"CONST A=3\nCONST B=A+1",
|
||||
" 4 \n",
|
||||
),
|
||||
(
|
||||
"TYPE",
|
||||
"TYPE Inner\nx AS INTEGER\nEND TYPE\nDIM a AS Outer\na.i.x=3\nPRINT a.i.x\nEND",
|
||||
"TYPE Inner\nx AS INTEGER\nEND TYPE\nTYPE Outer\ni AS Inner\nEND TYPE",
|
||||
" 3 \n",
|
||||
),
|
||||
];
|
||||
let mut failures = vec![];
|
||||
for (name, main, lib, output) in cases {
|
||||
match compile(&[("MAIN", main), ("LIB", lib)]) {
|
||||
Err(e) => failures.push(format!("{name}: {e:?}")),
|
||||
Ok(module) => {
|
||||
let actual = run(module);
|
||||
if actual != (RunEvent::Ended, output.into()) {
|
||||
failures.push(format!("{name}: {actual:?}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(failures.is_empty(), "{}", failures.join("\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v3_form_metabefehle_erzeugen_getrennte_formularobjekte() {
|
||||
let module = compile(&[
|
||||
(
|
||||
"A",
|
||||
"'$FORM\nCaption=\"a\"\nCALL SetupB\nPRINT Caption\nEND",
|
||||
),
|
||||
("B", "'$FORM\nSUB SetupB\nCaption=\"b\"\nEND SUB"),
|
||||
])
|
||||
.unwrap();
|
||||
let names: Vec<_> = module.objects.iter().map(|o| o.name.clone()).collect();
|
||||
let actual = run(module);
|
||||
assert_eq!(
|
||||
actual,
|
||||
(RunEvent::Ended, "a\n".into()),
|
||||
"Objekte: {names:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v4_common_behaelt_typidentitaet_und_initialisiert_arrays_einmal() {
|
||||
let mut failures = vec![];
|
||||
for (name, files, want) in [
|
||||
(
|
||||
"Suffixe",
|
||||
vec![(
|
||||
"MAIN",
|
||||
"COMMON SHARED c%, c$\nc%=7\nc$=\"ok\"\nPRINT c%;c$\nEND",
|
||||
)],
|
||||
" 7 ok\n",
|
||||
),
|
||||
(
|
||||
"Array",
|
||||
vec![
|
||||
("MAIN", "COMMON SHARED a%(2)\na%(1)=7\nCALL F\nEND"),
|
||||
("LIB", "COMMON SHARED a%(2)\nSUB F\nPRINT a%(1)\nEND SUB"),
|
||||
],
|
||||
" 7 \n",
|
||||
),
|
||||
] {
|
||||
match compile(&files) {
|
||||
Err(e) => failures.push(format!("{name}: {e:?}")),
|
||||
Ok(module) => {
|
||||
let actual = run(module);
|
||||
if actual != (RunEvent::Ended, want.into()) {
|
||||
failures.push(format!("{name}: {actual:?}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(failures.is_empty(), "{}", failures.join("\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v5_load_und_unload_loesen_den_expliziten_container_auf() {
|
||||
use tb_frontend::forms::ObjectClass;
|
||||
let mut catalog = FormCatalog::default();
|
||||
for name in ["Form1", "Form2"] {
|
||||
catalog.add(name, ObjectClass::Form, None, false);
|
||||
catalog.add("Text1", ObjectClass::TextBox, Some(name), true);
|
||||
}
|
||||
let source = units(&[("MAIN", "LOAD Form2!Text1(3)\nForm2!Text1(3).Text=\"b\"\nPRINT Form2!Text1(3).Text\nUNLOAD Form2!Text1(3)\nEND")]);
|
||||
let module = compile_project("APP", &source, &catalog, &[]).unwrap();
|
||||
assert_eq!(run(module), (RunEvent::Ended, "b\n".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v6_linkerdiagnose_nennt_den_urspruenglichen_dateiort() {
|
||||
let errors = compile(&[
|
||||
("MAIN", "DECLARE SUB F(x%)\nCALL F(1)\nEND"),
|
||||
("LIB", "SUB F(x$)\nEND SUB"),
|
||||
])
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
errors
|
||||
.iter()
|
||||
.any(|d| d.message.contains("Parameter type mismatch")),
|
||||
"{errors:?}"
|
||||
);
|
||||
assert!(
|
||||
errors
|
||||
.iter()
|
||||
.all(|d| d.file.is_some() && d.pos.line > 0 && d.pos.column > 0),
|
||||
"{errors:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modulbreakpoint_rundlauf_inspektion_und_numerische_erl_bleiben_getrennt() {
|
||||
let main = format!(
|
||||
"DIM a%(2)\nTYPE T\nx AS INTEGER\nEND TYPE\nDIM r AS T\na%(1)=7\nr.x=8\n{}CALL F\nEND",
|
||||
"\n\nn%=1\n"
|
||||
);
|
||||
let lib = format!("SUB F\n{}200 ERROR 6\nEND SUB", "\n".repeat(8));
|
||||
let module = compile(&[("MAIN", &main), ("LIB", &lib)]).unwrap();
|
||||
let bytes = module.to_tbc();
|
||||
let loaded = CompiledModule::from_tbc(&bytes).unwrap();
|
||||
assert_eq!(bytes, loaded.to_tbc());
|
||||
let mut vm = Vm::new(loaded);
|
||||
vm.add_module_breakpoint(1, 10);
|
||||
let mut host = CaptureHost::default();
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 10 });
|
||||
assert_eq!(vm.current_module(), 1);
|
||||
assert_eq!(vm.current_file(), "LIB.bas");
|
||||
assert!(matches!(
|
||||
vm.inspect_element("MAIN!a%", &[1]),
|
||||
Some(Value::Int(7))
|
||||
));
|
||||
assert!(matches!(
|
||||
vm.inspect_field("MAIN!r", &[0]),
|
||||
Some(Value::Int(8))
|
||||
));
|
||||
vm.remove_module_breakpoint(1, 10);
|
||||
vm.set_step(true);
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Stepped { line: 10 });
|
||||
assert_eq!(vm.current_source_pos().column, 5);
|
||||
vm.set_step(false);
|
||||
assert!(matches!(
|
||||
vm.run(&mut host),
|
||||
RunEvent::Error {
|
||||
code: 6,
|
||||
line: 10,
|
||||
..
|
||||
}
|
||||
));
|
||||
let source = format!("{}ERROR 6", "\n".repeat(41));
|
||||
assert!(matches!(
|
||||
run(compile(&[("MAIN", &source)]).unwrap()).0,
|
||||
RunEvent::Error {
|
||||
code: 6,
|
||||
line: 42,
|
||||
..
|
||||
}
|
||||
));
|
||||
let module = compile(&[(
|
||||
"MAIN",
|
||||
"ON ERROR RESUME NEXT\n200 ERROR 6\nPRINT ERR;ERL\nEND",
|
||||
)])
|
||||
.unwrap();
|
||||
assert_eq!(run(module), (RunEvent::Ended, " 6 200 \n".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn importkontext_bleibt_auch_bei_abweichenden_lokalen_definitionen_erhalten() {
|
||||
for (files, expected) in [
|
||||
(vec![("MAIN", "CONST A=99\nPRINT B\nEND"), ("LIB", "CONST A=3\nCONST B=A+1")], " 4 \n"),
|
||||
(vec![("MAIN", "PRINT C\nEND"), ("LIB", "CONST C=B+1"), ("BASE", "CONST B=3")], " 4 \n"),
|
||||
(vec![("MAIN", "TYPE Inner\nx AS STRING * 3\nEND TYPE\nDIM a AS Outer\na.i.x=3\nPRINT a.i.x\nEND"), ("LIB", "TYPE Inner\nx AS INTEGER\nEND TYPE\nTYPE Outer\ni AS Inner\nEND TYPE")], " 3 \n"),
|
||||
(vec![("MAIN", "DEFSTR A-Z\nx%=3\nCALL Twice(x%)\nPRINT x%\nPRINT Doppelt(3)\nEND"), ("LIB", "DEFINT A-Z\nSUB Twice(x)\nx=x*2\nEND SUB\nFUNCTION Doppelt(x)\nDoppelt=x*2\nEND FUNCTION")], " 6 \n 6 \n"),
|
||||
] {
|
||||
let actual = run(compile(&files).unwrap_or_else(|e| panic!("{files:?}: {e:?}")));
|
||||
assert_eq!(actual, (RunEvent::Ended, expected.into()), "{files:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn common_vertraege_und_linkfehler_haben_echte_quellorte() {
|
||||
for (left, right) in [
|
||||
("COMMON SHARED a%(2)", "COMMON SHARED a%(3)"),
|
||||
("COMMON SHARED a%(1 TO 2)", "COMMON SHARED a%(0 TO 2)"),
|
||||
("COMMON SHARED a%(2)", "COMMON SHARED a%(2,2)"),
|
||||
("COMMON SHARED a AS INTEGER", "COMMON SHARED a AS STRING"),
|
||||
("COMMON SHARED a%(2)", "COMMON SHARED a%"),
|
||||
] {
|
||||
let units = [
|
||||
SourceUnit::new("MAIN", "main.bas", &format!("{left}\nEND")),
|
||||
SourceUnit {
|
||||
name: "LIB".into(),
|
||||
segments: vec![SourceSegment {
|
||||
file: "shared.bi".into(),
|
||||
first_line: 42,
|
||||
text: format!(" {right}\n"),
|
||||
}],
|
||||
},
|
||||
];
|
||||
let errors = compile_project("APP", &units, &FormCatalog::default(), &[]).unwrap_err();
|
||||
assert!(
|
||||
errors.iter().any(|e| e.file.as_deref() == Some("shared.bi")
|
||||
&& e.pos.line == 42
|
||||
&& e.pos.column > 1
|
||||
&& e.message.contains("COMMON")),
|
||||
"{errors:?}"
|
||||
);
|
||||
}
|
||||
let files = [
|
||||
(
|
||||
"MAIN",
|
||||
"COMMON SHARED a%(2),a$(2)\na%(1)=7\na$(1)=\"ok\"\nCALL F\nEND",
|
||||
),
|
||||
(
|
||||
"LIB",
|
||||
"COMMON SHARED a%(2),a$(2)\nSUB F\nPRINT a%(1);a$(1)\nEND SUB",
|
||||
),
|
||||
];
|
||||
assert_eq!(
|
||||
run(compile(&files).unwrap()),
|
||||
(RunEvent::Ended, " 7 ok\n".into())
|
||||
);
|
||||
let errors =
|
||||
compile(&[("MAIN", "' header\nDECLARE SUB F(x$)\nSUB F(x%)\nEND SUB")]).unwrap_err();
|
||||
assert!(
|
||||
errors.iter().any(|e| e.pos.line == 3
|
||||
&& e.file.as_deref() == Some("MAIN.bas")
|
||||
&& e.message.contains("Parameter")),
|
||||
"{errors:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schleifengrenzen_tragen_breakpoint_include_und_resume() {
|
||||
for (source, line) in [
|
||||
("FOR i%=1 TO 2\nPRINT i%\nNEXT\nEND", 3),
|
||||
("DO\nPRINT 1\nLOOP UNTIL 1\nEND", 3),
|
||||
("i%=1\nWHILE i%\ni%=0\nWEND\nEND", 4),
|
||||
(
|
||||
"SELECT CASE 2\nCASE 1\nPRINT 1\nCASE 2\nPRINT 2\nEND SELECT",
|
||||
4,
|
||||
),
|
||||
] {
|
||||
let module = compile(&[("MAIN", source)]).unwrap();
|
||||
let mut vm = Vm::new(CompiledModule::from_tbc(&module.to_tbc()).unwrap());
|
||||
vm.add_module_breakpoint(0, line);
|
||||
assert_eq!(
|
||||
vm.run(&mut CaptureHost::default()),
|
||||
RunEvent::Breakpoint { line }
|
||||
);
|
||||
assert_eq!(vm.current_file(), "MAIN.bas");
|
||||
assert_eq!(vm.current_source_pos().column, 1);
|
||||
vm.remove_module_breakpoint(0, line);
|
||||
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
|
||||
}
|
||||
let actual = run(compile(&[("MAIN", "x%=1\nWHILE 1/x%\nx%=0\nWEND")]).unwrap());
|
||||
assert!(
|
||||
matches!(
|
||||
actual.0,
|
||||
RunEvent::Error {
|
||||
code: 11,
|
||||
line: 2,
|
||||
..
|
||||
}
|
||||
),
|
||||
"{actual:?}"
|
||||
);
|
||||
let actual = run(compile(&[(
|
||||
"MAIN",
|
||||
"ON ERROR GOTO H\nDO\nn%=n%+1\nLOOP UNTIL 1/x%\nPRINT n%\nEND\nH:\nx%=1\nRESUME",
|
||||
)])
|
||||
.unwrap());
|
||||
assert_eq!(actual, (RunEvent::Ended, " 1 \n".into()));
|
||||
let actual = run(compile(&[(
|
||||
"MAIN",
|
||||
"ON ERROR RESUME NEXT\nFOR i%=32767 TO 32767\nNEXT\nPRINT ERR\nEND",
|
||||
)])
|
||||
.unwrap());
|
||||
assert_eq!(actual, (RunEvent::Ended, " 6 \n".into()));
|
||||
let unit = SourceUnit {
|
||||
name: "MAIN".into(),
|
||||
segments: vec![
|
||||
SourceSegment {
|
||||
file: "main.bas".into(),
|
||||
first_line: 1,
|
||||
text: "DO\nPRINT 1\n".into(),
|
||||
},
|
||||
SourceSegment {
|
||||
file: "end.bi".into(),
|
||||
first_line: 42,
|
||||
text: " LOOP UNTIL 1/0\n".into(),
|
||||
},
|
||||
],
|
||||
};
|
||||
let module = compile_project("APP", &[unit], &FormCatalog::default(), &[]).unwrap();
|
||||
let mut vm = Vm::new(CompiledModule::from_tbc(&module.to_tbc()).unwrap());
|
||||
assert!(matches!(
|
||||
vm.run(&mut CaptureHost::default()),
|
||||
RunEvent::Error {
|
||||
code: 11,
|
||||
line: 42,
|
||||
..
|
||||
}
|
||||
));
|
||||
assert_eq!(vm.current_file(), "end.bi");
|
||||
assert_eq!(vm.current_source_pos().column, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debugger_unterscheidet_typisierte_scalar_und_arraynamen() {
|
||||
let mut vm = Vm::new(
|
||||
compile(&[(
|
||||
"MAIN",
|
||||
"COMMON SHARED c%,c$,a%(2),a$(2)\nc%=7\nc$=\"ok\"\na%(1)=8\na$(1)=\"array\"\nSTOP",
|
||||
)])
|
||||
.unwrap(),
|
||||
);
|
||||
assert!(matches!(
|
||||
vm.run(&mut CaptureHost::default()),
|
||||
RunEvent::Stopped { .. }
|
||||
));
|
||||
assert!(matches!(vm.inspect("c%"), Some(Value::Int(7))));
|
||||
assert!(matches!(vm.inspect("c$"),Some(Value::Str(s)) if s.as_ref()=="ok"));
|
||||
assert!(matches!(
|
||||
vm.inspect_element("a%", &[1]),
|
||||
Some(Value::Int(8))
|
||||
));
|
||||
assert!(matches!(vm.inspect_element("a$",&[1]),Some(Value::Str(s)) if s.as_ref()=="array"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn common_arrays_behalten_dynamische_grenzen_und_kompatible_offene_deklarationen() {
|
||||
for (left, right) in [
|
||||
("COMMON SHARED a%(2)", "COMMON SHARED a%()"),
|
||||
("COMMON SHARED a%(2.4)", "COMMON SHARED a%(2)"),
|
||||
("COMMON SHARED a%(n%+2)", "COMMON SHARED a%(n%+2)"),
|
||||
] {
|
||||
let files = [
|
||||
("MAIN", format!("{left}\na%(1)=7\nCALL F\nEND")),
|
||||
("LIB", format!("{right}\nSUB F\nPRINT a%(1)\nEND SUB")),
|
||||
];
|
||||
let files: Vec<_> = files.iter().map(|(n, s)| (*n, s.as_str())).collect();
|
||||
assert_eq!(
|
||||
run(compile(&files).unwrap()),
|
||||
(RunEvent::Ended, " 7 \n".into()),
|
||||
"{files:?}"
|
||||
);
|
||||
}
|
||||
let module = compile(&[
|
||||
("MAIN", "COMMON SHARED a%(n%+2)\nEND"),
|
||||
("LIB", "COMMON SHARED a%(n%+3)"),
|
||||
])
|
||||
.unwrap();
|
||||
let mut vm = Vm::new(CompiledModule::from_tbc(&module.to_tbc()).unwrap());
|
||||
assert!(matches!(
|
||||
vm.run(&mut CaptureHost::default()),
|
||||
RunEvent::Error {
|
||||
code: 13,
|
||||
line: 1,
|
||||
..
|
||||
}
|
||||
));
|
||||
assert_eq!(vm.current_file(), "LIB.bas");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prozedurlokaler_deftype_veraendert_keine_nachfolgende_signatur() {
|
||||
let files=[("MAIN","PRINT F(3)\nEND"),("LIB","DEFINT A-Z\nSUB Setup\nDEFSTR A-Z\nx=\"local\"\nEND SUB\nFUNCTION F(x)\nF=x*2\nEND FUNCTION")];
|
||||
assert_eq!(
|
||||
run(compile(&files).unwrap()),
|
||||
(RunEvent::Ended, " 6 \n".into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gemischte_frm_und_form_module_haben_getrennte_ereignisbindungen() {
|
||||
let form=tb_ui::frm::read_text("A.frm","VERSION 1.00\nBEGIN Form A\n Caption = \"design\"\nEND\nSUB Form_Load\nCaption=\"a\"\nEND SUB").unwrap();
|
||||
let sources = units(&[
|
||||
(
|
||||
"MAIN",
|
||||
"B.Show\na$=A.Caption\nb$=B.Caption\nA.Hide\nB.Hide\nCLS\nPRINT a$;b$\nEND",
|
||||
),
|
||||
("A", &form.code),
|
||||
("B", "'$FORM\nSUB Form_Load\nCaption=\"b\"\nEND SUB"),
|
||||
]);
|
||||
let module = compile_project("APP", &sources, &form.catalog(), &[form]).unwrap();
|
||||
assert_eq!(module.event_procs.len(), 2);
|
||||
assert_ne!(module.event_procs[0].object, module.event_procs[1].object);
|
||||
assert_eq!(run(module), (RunEvent::Ended, "ab\n".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diagnostik_bei_literalen_im_include_hat_die_exakte_spalte() {
|
||||
let unit = SourceUnit {
|
||||
name: "MAIN".into(),
|
||||
segments: vec![
|
||||
SourceSegment {
|
||||
file: "main.bas".into(),
|
||||
first_line: 1,
|
||||
text: "DECLARE SUB F(x%)\n".into(),
|
||||
},
|
||||
SourceSegment {
|
||||
file: "args.bi".into(),
|
||||
first_line: 42,
|
||||
text: " CALL F(\"wrong\")\n".into(),
|
||||
},
|
||||
],
|
||||
};
|
||||
let errors = compile_project("APP", &[unit], &FormCatalog::default(), &[]).unwrap_err();
|
||||
assert!(
|
||||
errors.iter().any(|e| e.file.as_deref() == Some("args.bi")
|
||||
&& e.pos.line == 42
|
||||
&& e.pos.column == 10
|
||||
&& e.message.contains("Parameter type mismatch")),
|
||||
"{errors:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spaetere_common_konflikte_und_doppelte_module_werden_am_ursprung_gemeldet() {
|
||||
let errors = compile(&[
|
||||
("MAIN", "COMMON a%()\nEND"),
|
||||
("LIB", "COMMON a%(2)"),
|
||||
("THIRD", "' head\nCOMMON a%(3)"),
|
||||
])
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
errors.iter().any(|e| e.file.as_deref() == Some("THIRD.bas")
|
||||
&& e.pos.line == 2
|
||||
&& e.message.contains("COMMON")),
|
||||
"{errors:?}"
|
||||
);
|
||||
let units = [
|
||||
SourceUnit::new("SAME", "first.bas", "END"),
|
||||
SourceUnit::new("SAME", "second.bas", "' head\n END"),
|
||||
];
|
||||
let errors = compile_project("APP", &units, &FormCatalog::default(), &[]).unwrap_err();
|
||||
assert!(
|
||||
errors
|
||||
.iter()
|
||||
.any(|e| e.file.as_deref() == Some("second.bas")
|
||||
&& e.pos.line == 2
|
||||
&& e.pos.column == 3
|
||||
&& e.message.contains("Duplicate")),
|
||||
"{errors:?}"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user