Phase 4: Formularmodell und Objektsprache

This commit is contained in:
2026-09-04 17:30:32 +02:00
parent 5cf5a6582c
commit 54488b5b67
35 changed files with 7195 additions and 627 deletions

View File

@@ -14,6 +14,8 @@ pub enum TypeName {
Currency,
Str,
FixedStr(i64),
Form,
Control,
Udt(String),
}
@@ -25,9 +27,24 @@ pub enum UnOp {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BinOp {
Pow, Mul, Div, IntDiv, Mod, Add, Sub,
Eq, Ne, Lt, Le, Gt, Ge,
And, Or, Xor, Eqv, Imp,
Pow,
Mul,
Div,
IntDiv,
Mod,
Add,
Sub,
Eq,
Ne,
Lt,
Le,
Gt,
Ge,
And,
Or,
Xor,
Eqv,
Imp,
}
#[derive(Debug, Clone, PartialEq)]
@@ -46,11 +63,26 @@ pub enum Expr {
args: Option<Vec<Expr>>,
pos: SourcePos,
},
Unary { op: UnOp, operand: Box<Expr>, pos: SourcePos },
Binary { op: BinOp, lhs: Box<Expr>, rhs: Box<Expr>, pos: SourcePos },
Unary {
op: UnOp,
operand: Box<Expr>,
pos: SourcePos,
},
Binary {
op: BinOp,
lhs: Box<Expr>,
rhs: Box<Expr>,
pos: SourcePos,
},
/// Geklammerter Ausdruck. Semantisch transparent, aber als Argument
/// erzwingt die Klammer Wertübergabe (BYVAL) statt BYREF.
Paren(Box<Expr>),
/// `TYPEOF ausdruck IS klasse`.
TypeOf {
value: Box<Expr>,
class: String,
pos: SourcePos,
},
/// Ausgelassenes Argument (`LOCATE , 5`).
Missing,
}
@@ -60,7 +92,8 @@ impl Expr {
match self {
Expr::Name { pos, .. }
| Expr::Unary { pos, .. }
| Expr::Binary { pos, .. } => *pos,
| Expr::Binary { pos, .. }
| Expr::TypeOf { pos, .. } => *pos,
Expr::Paren(e) => e.pos(),
_ => SourcePos::default(),
}
@@ -202,7 +235,11 @@ pub enum EventAction {
pub enum Stmt {
Label(String),
LineNumber(u32),
Assign { target: Expr, value: Expr, pos: SourcePos },
Assign {
target: Expr,
value: Expr,
pos: SourcePos,
},
Print {
/// LPRINT (Druckerausgabe) statt PRINT.
printer: bool,
@@ -226,7 +263,11 @@ pub enum Stmt {
else_body: Option<Vec<Stmt>>,
pos: SourcePos,
},
Select { expr: Expr, arms: Vec<CaseArm>, pos: SourcePos },
Select {
expr: Expr,
arms: Vec<CaseArm>,
pos: SourcePos,
},
For {
var: Expr,
from: Expr,
@@ -236,25 +277,57 @@ pub enum Stmt {
pos: SourcePos,
},
DoLoop {
pre: Option<(bool, Expr)>, // (ist UNTIL, Bedingung)
pre: Option<(bool, Expr)>, // (ist UNTIL, Bedingung)
post: Option<(bool, Expr)>,
body: Vec<Stmt>,
pos: SourcePos,
},
While { cond: Expr, body: Vec<Stmt>, pos: SourcePos },
Goto { target: LabelRef, pos: SourcePos },
Gosub { target: LabelRef, pos: SourcePos },
OnGoto { expr: Expr, targets: Vec<LabelRef>, gosub: bool, pos: SourcePos },
Return { target: Option<LabelRef>, pos: SourcePos },
While {
cond: Expr,
body: Vec<Stmt>,
pos: SourcePos,
},
Goto {
target: LabelRef,
pos: SourcePos,
},
Gosub {
target: LabelRef,
pos: SourcePos,
},
OnGoto {
expr: Expr,
targets: Vec<LabelRef>,
gosub: bool,
pos: SourcePos,
},
Return {
target: Option<LabelRef>,
pos: SourcePos,
},
End(SourcePos),
StopStmt(SourcePos),
System(SourcePos),
Exit { kind: ExitKind, pos: SourcePos },
Dim { shared: bool, redim: bool, decls: Vec<VarDecl>, pos: SourcePos },
Exit {
kind: ExitKind,
pos: SourcePos,
},
Dim {
shared: bool,
redim: bool,
decls: Vec<VarDecl>,
pos: SourcePos,
},
/// `SHARED`-Anweisung in einer Prozedur (Zugriff auf Modulvariablen).
SharedDecl { decls: Vec<VarDecl>, pos: SourcePos },
SharedDecl {
decls: Vec<VarDecl>,
pos: SourcePos,
},
/// `STATIC`-Anweisung in einer Prozedur.
StaticDecl { decls: Vec<VarDecl>, pos: SourcePos },
StaticDecl {
decls: Vec<VarDecl>,
pos: SourcePos,
},
/// `COMMON [SHARED] [/block/] liste`.
CommonDecl {
shared: bool,
@@ -262,20 +335,64 @@ pub enum Stmt {
decls: Vec<VarDecl>,
pos: SourcePos,
},
Erase { names: Vec<Expr>, pos: SourcePos },
ConstDecl { items: Vec<(String, Option<Suffix>, Expr)>, pos: SourcePos },
DefType { ty: TypeName, ranges: Vec<(char, char)>, pos: SourcePos },
OptionStmt { kind: OptionKind, pos: SourcePos },
TypeDecl { name: String, fields: Vec<(String, TypeName)>, pos: SourcePos },
Declare { sig: ProcSig, pos: SourcePos },
Erase {
names: Vec<Expr>,
pos: SourcePos,
},
ConstDecl {
items: Vec<(String, Option<Suffix>, Expr)>,
pos: SourcePos,
},
DefType {
ty: TypeName,
ranges: Vec<(char, char)>,
pos: SourcePos,
},
OptionStmt {
kind: OptionKind,
pos: SourcePos,
},
TypeDecl {
name: String,
fields: Vec<(String, TypeName)>,
pos: SourcePos,
},
Declare {
sig: ProcSig,
pos: SourcePos,
},
/// Expliziter oder impliziter Prozedur-/Builtin-Aufruf als Anweisung.
Call { name: String, suffix: Option<Suffix>, args: Vec<Expr>, pos: SourcePos },
OnError { local: bool, action: OnErrorAction, pos: SourcePos },
Resume { kind: ResumeKind, pos: SourcePos },
ErrorStmt { code: Expr, pos: SourcePos },
Data { items: Vec<String>, pos: SourcePos },
ReadStmt { vars: Vec<Expr>, pos: SourcePos },
Restore { target: Option<LabelRef>, pos: SourcePos },
Call {
name: String,
suffix: Option<Suffix>,
args: Vec<Expr>,
pos: SourcePos,
},
OnError {
local: bool,
action: OnErrorAction,
pos: SourcePos,
},
Resume {
kind: ResumeKind,
pos: SourcePos,
},
ErrorStmt {
code: Expr,
pos: SourcePos,
},
Data {
items: Vec<String>,
pos: SourcePos,
},
ReadStmt {
vars: Vec<Expr>,
pos: SourcePos,
},
Restore {
target: Option<LabelRef>,
pos: SourcePos,
},
/// Einzeilige `DEF FNname(...) = ausdruck`-Definition.
DefFn {
name: String,
@@ -312,8 +429,15 @@ pub enum Stmt {
len: Option<Expr>,
pos: SourcePos,
},
CloseStmt { files: Vec<Expr>, pos: SourcePos },
FieldStmt { file: Expr, fields: Vec<(Expr, Expr)>, pos: SourcePos },
CloseStmt {
files: Vec<Expr>,
pos: SourcePos,
},
FieldStmt {
file: Expr,
fields: Vec<(Expr, Expr)>,
pos: SourcePos,
},
GetPut {
put: bool,
file: Expr,
@@ -321,9 +445,22 @@ pub enum Stmt {
var: Option<Expr>,
pos: SourcePos,
},
LsetRset { rset: bool, target: Expr, value: Expr, pos: SourcePos },
WriteStmt { file: Option<Expr>, items: Vec<Expr>, pos: SourcePos },
SeekStmt { file: Expr, position: Expr, pos: SourcePos },
LsetRset {
rset: bool,
target: Expr,
value: Expr,
pos: SourcePos,
},
WriteStmt {
file: Option<Expr>,
items: Vec<Expr>,
pos: SourcePos,
},
SeekStmt {
file: Expr,
position: Expr,
pos: SourcePos,
},
LockStmt {
unlock: bool,
file: Expr,
@@ -362,9 +499,18 @@ pub enum Stmt {
},
// ---- Metabefehle ----
/// `'$INCLUDE: 'datei''` — Auflösung übernimmt der Compile-Treiber.
Include { path: String, pos: SourcePos },
Include {
path: String,
pos: SourcePos,
},
/// `'$STATIC` / `'$DYNAMIC`.
MetaArrays { static_arrays: bool, pos: SourcePos },
MetaArrays {
static_arrays: bool,
pos: SourcePos,
},
MetaForm {
pos: SourcePos,
},
}
#[derive(Debug, Clone, PartialEq)]

View File

@@ -0,0 +1,631 @@
//! Statische Forms-Typinformation. Namen werden beim Übersetzen in diese
//! Tabellenindizes aufgelöst; die Laufzeit muss keine Strings durchsuchen.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ObjectClass {
Form,
CheckBox,
ComboBox,
CommandButton,
DirListBox,
DriveListBox,
FileListBox,
Frame,
HScrollBar,
Label,
ListBox,
Menu,
OptionButton,
PictureBox,
TextBox,
Timer,
VScrollBar,
Screen,
}
impl ObjectClass {
pub const ALL: [Self; 18] = [
Self::Form,
Self::CheckBox,
Self::ComboBox,
Self::CommandButton,
Self::DirListBox,
Self::DriveListBox,
Self::FileListBox,
Self::Frame,
Self::HScrollBar,
Self::Label,
Self::ListBox,
Self::Menu,
Self::OptionButton,
Self::PictureBox,
Self::TextBox,
Self::Timer,
Self::VScrollBar,
Self::Screen,
];
pub fn name(self) -> &'static str {
match self {
Self::Form => "FORM",
Self::CheckBox => "CHECKBOX",
Self::ComboBox => "COMBOBOX",
Self::CommandButton => "COMMANDBUTTON",
Self::DirListBox => "DIRLISTBOX",
Self::DriveListBox => "DRIVELISTBOX",
Self::FileListBox => "FILELISTBOX",
Self::Frame => "FRAME",
Self::HScrollBar => "HSCROLLBAR",
Self::Label => "LABEL",
Self::ListBox => "LISTBOX",
Self::Menu => "MENU",
Self::OptionButton => "OPTIONBUTTON",
Self::PictureBox => "PICTUREBOX",
Self::TextBox => "TEXTBOX",
Self::Timer => "TIMER",
Self::VScrollBar => "VSCROLLBAR",
Self::Screen => "SCREEN",
}
}
pub fn parse(name: &str) -> Option<Self> {
Self::ALL
.into_iter()
.find(|c| c.name().eq_ignore_ascii_case(name))
}
pub fn id(self) -> u8 {
Self::ALL.iter().position(|c| *c == self).unwrap() as u8
}
pub fn from_id(id: u8) -> Option<Self> {
Self::ALL.get(id as usize).copied()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PropertyType {
Integer,
Single,
String,
Boolean,
Object,
IntegerArray,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PropertyDefault {
Integer(i32),
Single(f32),
String(&'static str),
Boolean(bool),
Empty,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PropertySpec {
pub name: &'static str,
pub ty: PropertyType,
pub default: PropertyDefault,
pub min: Option<i32>,
pub max: Option<i32>,
pub writable: bool,
}
const fn int(name: &'static str, default: i32) -> PropertySpec {
PropertySpec {
name,
ty: PropertyType::Integer,
default: PropertyDefault::Integer(default),
min: None,
max: None,
writable: true,
}
}
const fn range(name: &'static str, default: i32, min: i32, max: i32) -> PropertySpec {
PropertySpec {
name,
ty: PropertyType::Integer,
default: PropertyDefault::Integer(default),
min: Some(min),
max: Some(max),
writable: true,
}
}
const fn boolp(name: &'static str, default: bool) -> PropertySpec {
PropertySpec {
name,
ty: PropertyType::Boolean,
default: PropertyDefault::Boolean(default),
min: None,
max: None,
writable: true,
}
}
const fn string(name: &'static str, default: &'static str) -> PropertySpec {
PropertySpec {
name,
ty: PropertyType::String,
default: PropertyDefault::String(default),
min: None,
max: None,
writable: true,
}
}
const fn ro(name: &'static str, ty: PropertyType) -> PropertySpec {
PropertySpec {
name,
ty,
default: PropertyDefault::Empty,
min: None,
max: None,
writable: false,
}
}
/// Eigenschaftstabelle in stabiler Reihenfolge. Der Vektor ist klein und wird
/// nur beim Übersetzen bzw. Erzeugen eines Objekts aufgebaut.
pub fn properties(class: ObjectClass) -> Vec<PropertySpec> {
use ObjectClass::*;
let mut p = Vec::new();
if !matches!(class, Timer | Menu | Screen) {
p.extend([
range("BACKCOLOR", 7, 0, 15),
boolp("ENABLED", true),
range("HEIGHT", 1, 1, 254),
range("LEFT", 0, 0, 254),
range("MOUSEPOINTER", 0, 0, 12),
ro("PARENT", PropertyType::Object),
string("TAG", ""),
range("TOP", 0, 0, 254),
boolp("VISIBLE", true),
range("WIDTH", 1, 1, 254),
]);
}
if !matches!(
class,
Form | Frame | Label | PictureBox | Timer | Menu | Screen
) {
p.extend([int("INDEX", 0), int("TABINDEX", 0), boolp("TABSTOP", true)]);
}
if !matches!(class, Form | Timer | Menu | Screen) {
p.push(string("CTLNAME", ""));
}
if matches!(
class,
Form | TextBox
| ListBox
| ComboBox
| CheckBox
| OptionButton
| Label
| HScrollBar
| VScrollBar
| PictureBox
) {
p.push(range("FORECOLOR", 0, 0, 15));
}
if matches!(
class,
Form | CommandButton
| TextBox
| ListBox
| ComboBox
| CheckBox
| OptionButton
| Frame
| Label
| HScrollBar
| VScrollBar
| PictureBox
) {
p.push(range("DRAGMODE", 0, 0, 1));
}
match class {
Form => p.extend([
boolp("AUTOREDRAW", false),
range("BORDERSTYLE", 2, 0, 6),
string("CAPTION", ""),
boolp("CONTROLBOX", true),
int("CURRENTX", 0),
int("CURRENTY", 0),
string("FORMNAME", ""),
range("FORMTYPE", 0, 0, 1),
boolp("MAXBUTTON", true),
boolp("MINBUTTON", true),
ro("SCALEHEIGHT", PropertyType::Integer),
ro("SCALEWIDTH", PropertyType::Integer),
range("WINDOWSTATE", 0, 0, 2),
]),
CommandButton => p.extend([
boolp("CANCEL", false),
string("CAPTION", ""),
boolp("DEFAULT", false),
boolp("VALUE", false),
]),
TextBox => p.extend([
range("BORDERSTYLE", 1, 0, 1),
boolp("MULTILINE", false),
range("SCROLLBARS", 0, 0, 3),
int("SELLENGTH", 0),
int("SELSTART", 0),
string("SELTEXT", ""),
string("TEXT", ""),
]),
ListBox => p.extend([
ro("LIST", PropertyType::String),
ro("LISTCOUNT", PropertyType::Integer),
int("LISTINDEX", -1),
boolp("SORTED", false),
string("TEXT", ""),
]),
ComboBox => p.extend([
ro("LIST", PropertyType::String),
ro("LISTCOUNT", PropertyType::Integer),
int("LISTINDEX", -1),
boolp("SORTED", false),
int("SELLENGTH", 0),
int("SELSTART", 0),
string("SELTEXT", ""),
range("STYLE", 0, 0, 2),
string("TEXT", ""),
]),
CheckBox => p.extend([string("CAPTION", ""), range("VALUE", 0, 0, 2)]),
OptionButton => p.extend([string("CAPTION", ""), range("VALUE", 0, -1, 0)]),
Frame => p.push(string("CAPTION", "")),
Label => p.extend([
range("ALIGNMENT", 0, 0, 2),
boolp("AUTOSIZE", false),
range("BORDERSTYLE", 0, 0, 2),
string("CAPTION", ""),
]),
HScrollBar | VScrollBar => p.extend([
ro("ATTACHED", PropertyType::Boolean),
range("LARGECHANGE", 1, 1, 32767),
range("MIN", 0, -32768, 32767),
range("MAX", 32767, -32768, 32767),
range("SMALLCHANGE", 1, 1, 32767),
range("VALUE", 0, -32768, 32767),
]),
PictureBox => p.extend([
boolp("AUTOREDRAW", false),
range("BORDERSTYLE", 1, 0, 2),
int("CURRENTX", 0),
int("CURRENTY", 0),
ro("SCALEHEIGHT", PropertyType::Integer),
ro("SCALEWIDTH", PropertyType::Integer),
]),
Timer => p.extend([
string("CTLNAME", ""),
boolp("ENABLED", true),
int("INDEX", 0),
range("INTERVAL", 0, 0, 65535),
ro("PARENT", PropertyType::Object),
string("TAG", ""),
]),
Menu => p.extend([
string("CAPTION", ""),
boolp("CHECKED", false),
string("CTLNAME", ""),
boolp("ENABLED", true),
int("INDEX", 0),
ro("PARENT", PropertyType::Object),
boolp("SEPARATOR", false),
string("TAG", ""),
boolp("VISIBLE", true),
]),
DirListBox => p.extend([string("PATH", ""), string("TEXT", "")]),
DriveListBox => p.extend([string("DRIVE", ""), string("TEXT", "")]),
FileListBox => p.extend([
string("FILENAME", ""),
string("PATH", ""),
string("PATTERN", "*.*"),
boolp("ARCHIVE", true),
boolp("HIDDEN", false),
boolp("NORMAL", true),
boolp("READONLY", true),
boolp("SYSTEM", false),
string("TEXT", ""),
]),
Screen => p.extend([
ro("ACTIVECONTROL", PropertyType::Object),
ro("ACTIVEFORM", PropertyType::Object),
boolp("AUTOREDRAW", false),
PropertySpec {
name: "CONTROLPANEL",
ty: PropertyType::IntegerArray,
default: PropertyDefault::Integer(0),
min: Some(0),
max: Some(15),
writable: true,
},
ro("HEIGHT", PropertyType::Integer),
ro("WIDTH", PropertyType::Integer),
range("MOUSEPOINTER", 0, 0, 12),
]),
}
if class == Form {
if let Some(visible) = p.iter_mut().find(|p| p.name == "VISIBLE") {
visible.default = PropertyDefault::Boolean(false);
}
}
p
}
pub fn property(class: ObjectClass, name: &str) -> Option<(u16, PropertySpec)> {
properties(class)
.into_iter()
.enumerate()
.find(|(_, p)| p.name.eq_ignore_ascii_case(name))
.map(|(i, p)| (i as u16, p))
}
pub fn methods(class: ObjectClass) -> &'static [&'static str] {
use ObjectClass::*;
match class {
Form => &[
"CLS",
"DRAG",
"HIDE",
"LOAD",
"MOVE",
"PRINT",
"PRINTFORM",
"REFRESH",
"SHOW",
"TEXTHEIGHT",
"TEXTWIDTH",
"UNLOAD",
],
ListBox | ComboBox => &[
"ADDITEM",
"DRAG",
"MOVE",
"REFRESH",
"REMOVEITEM",
"SETFOCUS",
],
CommandButton | TextBox | CheckBox | OptionButton => {
&["DRAG", "MOVE", "REFRESH", "SETFOCUS"]
}
Frame | Label | HScrollBar | VScrollBar => &["DRAG", "MOVE", "REFRESH"],
PictureBox => &[
"CLS",
"DRAG",
"MOVE",
"PRINT",
"REFRESH",
"SETFOCUS",
"TEXTHEIGHT",
"TEXTWIDTH",
],
Screen => &["HIDE", "SHOW"],
_ => &[],
}
}
pub fn method_is_implemented(class: ObjectClass, name: &str) -> bool {
matches!(
(class, name),
(ObjectClass::Form, "HIDE" | "LOAD" | "SHOW" | "UNLOAD")
| (ObjectClass::Screen, "HIDE" | "SHOW")
)
}
pub fn method_arity(class: ObjectClass, name: &str) -> Option<(usize, usize)> {
match (class, name) {
(ObjectClass::Form, "SHOW") => Some((0, 1)),
(ObjectClass::Form, "HIDE" | "LOAD" | "UNLOAD")
| (ObjectClass::Screen, "HIDE" | "SHOW") => Some((0, 0)),
_ => None,
}
}
pub fn events(class: ObjectClass) -> &'static [&'static str] {
use ObjectClass::*;
match class {
Form => &[
"CLICK",
"DBLCLICK",
"DRAGDROP",
"DRAGOVER",
"GOTFOCUS",
"KEYDOWN",
"KEYPRESS",
"KEYUP",
"LOAD",
"LOSTFOCUS",
"MOUSEDOWN",
"MOUSEMOVE",
"MOUSEUP",
"PAINT",
"RESIZE",
"UNLOAD",
],
CommandButton => &[
"CLICK",
"DRAGDROP",
"DRAGOVER",
"GOTFOCUS",
"KEYDOWN",
"KEYPRESS",
"KEYUP",
"LOSTFOCUS",
],
TextBox => &[
"CHANGE",
"DRAGDROP",
"DRAGOVER",
"GOTFOCUS",
"KEYDOWN",
"KEYPRESS",
"KEYUP",
"LOSTFOCUS",
],
ListBox => &[
"CLICK",
"DBLCLICK",
"DRAGDROP",
"DRAGOVER",
"GOTFOCUS",
"KEYDOWN",
"KEYPRESS",
"KEYUP",
"LOSTFOCUS",
"MOUSEDOWN",
"MOUSEMOVE",
"MOUSEUP",
],
ComboBox => &[
"CHANGE",
"CLICK",
"DBLCLICK",
"DRAGDROP",
"DRAGOVER",
"DROPDOWN",
"GOTFOCUS",
"KEYDOWN",
"KEYPRESS",
"KEYUP",
"LOSTFOCUS",
"MOUSEDOWN",
"MOUSEMOVE",
"MOUSEUP",
],
CheckBox => &[
"CLICK",
"DRAGDROP",
"DRAGOVER",
"GOTFOCUS",
"KEYDOWN",
"KEYPRESS",
"KEYUP",
"LOSTFOCUS",
],
OptionButton => &[
"CLICK",
"DBLCLICK",
"DRAGDROP",
"DRAGOVER",
"GOTFOCUS",
"KEYDOWN",
"KEYPRESS",
"KEYUP",
"LOSTFOCUS",
],
Frame => &["DRAGDROP", "DRAGOVER"],
Label => &[
"CHANGE",
"CLICK",
"DBLCLICK",
"DRAGDROP",
"DRAGOVER",
"MOUSEDOWN",
"MOUSEMOVE",
"MOUSEUP",
],
HScrollBar | VScrollBar => &[
"CHANGE",
"DRAGDROP",
"DRAGOVER",
"GOTFOCUS",
"KEYDOWN",
"KEYPRESS",
"KEYUP",
"LOSTFOCUS",
],
PictureBox => &[
"CLICK",
"DBLCLICK",
"DRAGDROP",
"DRAGOVER",
"GOTFOCUS",
"KEYDOWN",
"KEYPRESS",
"KEYUP",
"LOSTFOCUS",
"MOUSEDOWN",
"MOUSEMOVE",
"MOUSEUP",
"PAINT",
],
Timer => &["TIMER"],
Menu => &["CLICK"],
DirListBox => &["CHANGE", "CLICK", "DBLCLICK", "PATHCHANGE"],
DriveListBox => &["CHANGE"],
FileListBox => &["CLICK", "DBLCLICK", "PATTERNCHANGE", "PATHCHANGE"],
Screen => &[],
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EventParamType {
Integer,
Single,
Control,
}
pub fn event_params(event: &str) -> Option<&'static [(&'static str, EventParamType)]> {
use EventParamType::*;
Some(match event.to_ascii_uppercase().as_str() {
"KEYDOWN" | "KEYUP" => &[("KEYCODE", Integer), ("SHIFT", Integer)],
"KEYPRESS" => &[("KEYASCII", Integer)],
"MOUSEDOWN" | "MOUSEMOVE" | "MOUSEUP" => &[
("BUTTON", Integer),
("SHIFT", Integer),
("X", Single),
("Y", Single),
],
"DRAGDROP" => &[("SOURCE", Control), ("X", Single), ("Y", Single)],
"DRAGOVER" => &[
("SOURCE", Control),
("X", Single),
("Y", Single),
("STATE", Integer),
],
"UNLOAD" => &[("CANCEL", Integer)],
"CLICK" | "DBLCLICK" | "CHANGE" | "DROPDOWN" | "GOTFOCUS" | "LOSTFOCUS" | "LOAD"
| "PAINT" | "RESIZE" | "TIMER" | "PATHCHANGE" | "PATTERNCHANGE" => &[],
_ => return None,
})
}
#[derive(Debug, Clone)]
pub struct FormObject {
pub name: String,
pub class: ObjectClass,
pub parent_form: Option<String>,
pub array: bool,
}
#[derive(Debug, Clone, Default)]
pub struct FormCatalog {
pub objects: Vec<FormObject>,
}
impl FormCatalog {
pub fn add(
&mut self,
name: impl Into<String>,
class: ObjectClass,
parent_form: Option<&str>,
array: bool,
) -> u16 {
let id = self.objects.len() as u16;
self.objects.push(FormObject {
name: name.into().to_uppercase(),
class,
parent_form: parent_form.map(str::to_uppercase),
array,
});
id
}
pub fn find(&self, name: &str) -> Option<(u16, &FormObject)> {
self.objects
.iter()
.enumerate()
.find(|(_, o)| o.name.eq_ignore_ascii_case(name))
.map(|(i, o)| (i as u16, o))
}
}

View File

@@ -41,6 +41,8 @@ pub enum HTy {
FixedStr(u32),
/// Benutzerdefinierter Typ (Index in `HirModule::udts`).
Udt(u16),
Form,
Control,
}
impl HTy {
@@ -131,6 +133,17 @@ pub struct HirModule {
pub procs: Vec<HProc>,
pub data: Vec<DataItem>,
pub option_base: u8,
/// Zur Übersetzungszeit bekannte Forms-Objekte in Indexreihenfolge.
pub objects: Vec<crate::forms::FormObject>,
/// Ereignisprozeduren: Objektindex, Ereignisname, Prozedurindex.
pub event_procs: Vec<HEventProc>,
}
#[derive(Debug, Clone)]
pub struct HEventProc {
pub object: u16,
pub event: String,
pub proc: u16,
}
// ---- Ausdrücke -------------------------------------------------------------
@@ -367,6 +380,25 @@ pub enum HExpr {
Cur(i64),
Str(String),
Load(Box<HPlace>),
ObjectProperty {
object: u16,
index: Option<Box<HExpr>>,
property: u16,
ty: HTy,
},
DynamicObjectProperty {
object: Box<HExpr>,
property: String,
},
ObjectRef {
object: u16,
index: Option<Box<HExpr>>,
class: crate::forms::ObjectClass,
},
TypeOf {
value: Box<HExpr>,
class: crate::forms::ObjectClass,
},
/// Numerische Konvertierung nach Matrix (Rundung/Überlauf).
Conv {
from: NumTy,
@@ -473,6 +505,27 @@ pub enum HStmtKind {
place: HPlace,
value: HExpr,
},
SetObjectProperty {
object: u16,
index: Option<HExpr>,
property: u16,
value: HExpr,
},
SetDynamicObjectProperty {
object: HExpr,
property: String,
value: HExpr,
},
ObjectMethod {
object: u16,
method: u16,
args: Vec<HExpr>,
},
ObjectLoad {
object: u16,
index: Option<HExpr>,
unload: bool,
},
Print {
items: Vec<HPrintItem>,
/// Endet die Anweisung mit `;`/`,` (kein Zeilenumbruch)?

View File

@@ -62,43 +62,169 @@ pub enum NumValue {
/// und -anweisungen sind KEINE Keywords, sondern Builtins der Semantik).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Kw {
And, As, Call, Case, Common, Const, Data, Declare, Def, DefCur, DefDbl, DefInt,
DefLng, DefSng, DefStr, Dim, Do, Double, Else, ElseIf, End, Eqv, Erase,
Error, Exit, For, Function, Gosub, Goto, If, Imp, Input, Integer, Is,
Let, Line, Local, Long, Loop, Mod, Next, Not, On, Option, Or, Print,
Read, ReDim, Rem, Restore, Resume, Return, Select, Shared, Single,
Static, Step, Stop, String, Sub, System, Then, To, Type, Until, Using,
Wend, While, Xor, Currency,
And,
As,
Call,
Case,
Common,
Const,
Data,
Declare,
Def,
DefCur,
DefDbl,
DefInt,
DefLng,
DefSng,
DefStr,
Dim,
Do,
Double,
Else,
ElseIf,
End,
Eqv,
Erase,
Error,
Exit,
For,
Function,
Gosub,
Goto,
If,
Imp,
Input,
Integer,
Is,
Let,
Line,
Local,
Long,
Loop,
Mod,
Next,
Not,
On,
Option,
Or,
Print,
Read,
ReDim,
Rem,
Restore,
Resume,
Return,
Select,
Shared,
Single,
Static,
Step,
Stop,
String,
Sub,
System,
Then,
To,
Type,
Until,
Using,
Wend,
While,
Xor,
Currency,
// Datei-E/A-Keywords: werden geparst, aber erst in Phase 3 implementiert
Open, Close, Write, Field, Get, Put, Seek, Lset, Rset,
Open,
Close,
Write,
Field,
Get,
Put,
Seek,
Lset,
Rset,
}
fn keyword(upper: &str) -> Option<Kw> {
use Kw::*;
Some(match upper {
"AND" => And, "AS" => As, "CALL" => Call, "CASE" => Case,
"COMMON" => Common, "CONST" => Const, "CURRENCY" => Currency,
"AND" => And,
"AS" => As,
"CALL" => Call,
"CASE" => Case,
"COMMON" => Common,
"CONST" => Const,
"CURRENCY" => Currency,
"DATA" => Data,
"DECLARE" => Declare, "DEF" => Def, "DEFCUR" => DefCur,
"DEFDBL" => DefDbl, "DEFINT" => DefInt, "DEFLNG" => DefLng,
"DEFSNG" => DefSng, "DEFSTR" => DefStr, "DIM" => Dim, "DO" => Do,
"DOUBLE" => Double, "ELSE" => Else, "ELSEIF" => ElseIf, "END" => End,
"EQV" => Eqv, "ERASE" => Erase, "ERROR" => Error, "EXIT" => Exit,
"FIELD" => Field, "FOR" => For, "FUNCTION" => Function,
"GET" => Get, "GOSUB" => Gosub, "GOTO" => Goto, "IF" => If,
"IMP" => Imp, "INPUT" => Input, "INTEGER" => Integer, "IS" => Is,
"LET" => Let, "LINE" => Line, "LOCAL" => Local, "LONG" => Long,
"LOOP" => Loop, "LSET" => Lset, "MOD" => Mod, "NEXT" => Next,
"NOT" => Not, "ON" => On, "OPEN" => Open, "OPTION" => Option,
"OR" => Or, "PRINT" => Print, "PUT" => Put, "READ" => Read,
"REDIM" => ReDim, "REM" => Rem, "RESTORE" => Restore,
"RESUME" => Resume, "RETURN" => Return, "RSET" => Rset,
"SEEK" => Seek, "SELECT" => Select, "SHARED" => Shared,
"SINGLE" => Single, "STATIC" => Static, "STEP" => Step,
"STOP" => Stop, "STRING" => String, "SUB" => Sub,
"SYSTEM" => System, "THEN" => Then,
"TO" => To, "TYPE" => Type, "UNTIL" => Until, "USING" => Using,
"WEND" => Wend, "WHILE" => While, "WRITE" => Write, "XOR" => Xor,
"DECLARE" => Declare,
"DEF" => Def,
"DEFCUR" => DefCur,
"DEFDBL" => DefDbl,
"DEFINT" => DefInt,
"DEFLNG" => DefLng,
"DEFSNG" => DefSng,
"DEFSTR" => DefStr,
"DIM" => Dim,
"DO" => Do,
"DOUBLE" => Double,
"ELSE" => Else,
"ELSEIF" => ElseIf,
"END" => End,
"EQV" => Eqv,
"ERASE" => Erase,
"ERROR" => Error,
"EXIT" => Exit,
"FIELD" => Field,
"FOR" => For,
"FUNCTION" => Function,
"GET" => Get,
"GOSUB" => Gosub,
"GOTO" => Goto,
"IF" => If,
"IMP" => Imp,
"INPUT" => Input,
"INTEGER" => Integer,
"IS" => Is,
"LET" => Let,
"LINE" => Line,
"LOCAL" => Local,
"LONG" => Long,
"LOOP" => Loop,
"LSET" => Lset,
"MOD" => Mod,
"NEXT" => Next,
"NOT" => Not,
"ON" => On,
"OPEN" => Open,
"OPTION" => Option,
"OR" => Or,
"PRINT" => Print,
"PUT" => Put,
"READ" => Read,
"REDIM" => ReDim,
"REM" => Rem,
"RESTORE" => Restore,
"RESUME" => Resume,
"RETURN" => Return,
"RSET" => Rset,
"SEEK" => Seek,
"SELECT" => Select,
"SHARED" => Shared,
"SINGLE" => Single,
"STATIC" => Static,
"STEP" => Step,
"STOP" => Stop,
"STRING" => String,
"SUB" => Sub,
"SYSTEM" => System,
"THEN" => Then,
"TO" => To,
"TYPE" => Type,
"UNTIL" => Until,
"USING" => Using,
"WEND" => Wend,
"WHILE" => While,
"WRITE" => Write,
"XOR" => Xor,
"CLOSE" => Close,
_ => return None,
})
@@ -107,18 +233,39 @@ fn keyword(upper: &str) -> Option<Kw> {
#[derive(Debug, Clone, PartialEq)]
pub enum TokenKind {
/// Bezeichner; `name` ist bereits in Großschreibung normalisiert.
Ident { name: String, suffix: Option<Suffix> },
Ident {
name: String,
suffix: Option<Suffix>,
},
Kw(Kw),
Num(NumValue),
Str(String),
Plus, Minus, Star, Slash, Backslash, Caret,
Eq, Ne, Lt, Le, Gt, Ge,
LParen, RParen, Comma, Semicolon, Colon, Hash,
Plus,
Minus,
Star,
Slash,
Backslash,
Caret,
Eq,
Ne,
Lt,
Le,
Gt,
Ge,
LParen,
RParen,
Comma,
Semicolon,
Colon,
Hash,
Dot,
/// Metabefehl `'$INCLUDE: 'datei''` (Pfad; leer = fehlerhafte Syntax).
MetaInclude(String),
/// Metabefehle `'$STATIC` / `'$DYNAMIC`.
MetaStatic,
MetaDynamic,
/// Metabefehl `'$FORM`: das Modul ist ein Formularmodul.
MetaForm,
/// Rohtext einer `DATA`-Anweisung bis zum Anweisungsende. Er wird
/// bewusst **nicht** zerlegt oder normalisiert: unquotierte Elemente
/// behalten ihre Groß- und Kleinschreibung und ihren inneren Leerraum.
@@ -139,6 +286,9 @@ fn meta_token(rest: &[char]) -> Option<TokenKind> {
if up.starts_with("$DYNAMIC") {
return Some(TokenKind::MetaDynamic);
}
if up.starts_with("$FORM") {
return Some(TokenKind::MetaForm);
}
if up.starts_with("$INCLUDE") {
if let Some(colon) = t.find(':') {
let after = t[colon + 1..].trim();
@@ -191,14 +341,15 @@ pub fn lex(source: &str) -> LexOutput {
break;
}
let start = i;
let pos = SourcePos { line: line_no, column: (start + 1) as u32 };
let pos = SourcePos {
line: line_no,
column: (start + 1) as u32,
};
let c = chars[i];
// Zeilenfortsetzung: `_` nach Leerraum, danach nur noch Leerraum
if c == '_'
&& (start == 0
|| chars[start - 1] == ' '
|| chars[start - 1] == '\t')
&& (start == 0 || chars[start - 1] == ' ' || chars[start - 1] == '\t')
&& chars[start + 1..].iter().all(|&ch| ch == ' ' || ch == '\t')
{
line_continued = true;
@@ -236,17 +387,16 @@ pub fn lex(source: &str) -> LexOutput {
// Das Vorbild toleriert fehlende schließende
// Anführungszeichen am Zeilenende.
}
tokens.push(Token { kind: TokenKind::Str(s), pos });
tokens.push(Token {
kind: TokenKind::Str(s),
pos,
});
}
'&' if i + 1 < chars.len()
&& matches!(chars[i + 1], 'h' | 'H' | 'o' | 'O') =>
{
'&' if i + 1 < chars.len() && matches!(chars[i + 1], 'h' | 'H' | 'o' | 'O') => {
let hex = matches!(chars[i + 1], 'h' | 'H');
i += 2;
let digit_start = i;
while i < chars.len()
&& chars[i].is_ascii_alphanumeric()
{
while i < chars.len() && chars[i].is_ascii_alphanumeric() {
i += 1;
}
let digits: String = chars[digit_start..i].iter().collect();
@@ -276,8 +426,8 @@ pub fn lex(source: &str) -> LexOutput {
}),
}
}
'0'..='9' | '.' if c != '.'
|| (i + 1 < chars.len() && chars[i + 1].is_ascii_digit()) =>
'0'..='9' | '.'
if c != '.' || (i + 1 < chars.len() && chars[i + 1].is_ascii_digit()) =>
{
let int_start = i;
while i < chars.len() && chars[i].is_ascii_digit() {
@@ -298,9 +448,7 @@ pub fn lex(source: &str) -> LexOutput {
// Exponent E/D
let mut exp_kind: Option<char> = None;
let mut exp_str = String::new();
if i < chars.len()
&& matches!(chars[i], 'e' | 'E' | 'd' | 'D')
{
if i < chars.len() && matches!(chars[i], 'e' | 'E' | 'd' | 'D') {
let save = i;
let k = chars[i].to_ascii_uppercase();
let mut j = i + 1;
@@ -403,22 +551,25 @@ pub fn lex(source: &str) -> LexOutput {
}
}
};
tokens.push(Token { kind: TokenKind::Num(value), pos });
tokens.push(Token {
kind: TokenKind::Num(value),
pos,
});
}
c if c.is_alphabetic() => {
i += 1;
while i < chars.len()
&& (chars[i].is_alphanumeric()
|| chars[i] == '.'
|| chars[i] == '_')
&& (chars[i].is_alphanumeric() || chars[i] == '.' || chars[i] == '_')
{
i += 1;
}
let mut name: String = chars[start..i]
.iter()
.collect::<String>()
.to_uppercase();
let suffix = if i < chars.len() {
let mut name: String =
chars[start..i].iter().collect::<String>().to_uppercase();
let container_bang = i < chars.len()
&& chars[i] == '!'
&& i + 1 < chars.len()
&& chars[i + 1].is_alphabetic();
let suffix = if i < chars.len() && !container_bang {
Suffix::from_char(chars[i])
} else {
None
@@ -426,6 +577,16 @@ pub fn lex(source: &str) -> LexOutput {
if suffix.is_some() {
i += 1;
}
if container_bang {
name.push('!');
i += 1;
while i < chars.len()
&& (chars[i].is_alphanumeric() || chars[i] == '.' || chars[i] == '_')
{
name.push(chars[i].to_ascii_uppercase());
i += 1;
}
}
if suffix.is_none() {
if let Some(kw) = keyword(&name) {
if kw == Kw::Rem {
@@ -435,7 +596,10 @@ pub fn lex(source: &str) -> LexOutput {
}
break 'line;
}
tokens.push(Token { kind: TokenKind::Kw(kw), pos });
tokens.push(Token {
kind: TokenKind::Kw(kw),
pos,
});
if kw == Kw::Data {
// Der Rest der Anweisung ist Rohtext: bis zum
// `:` außerhalb von Anführungszeichen oder bis
@@ -486,8 +650,9 @@ pub fn lex(source: &str) -> LexOutput {
';' => TokenKind::Semicolon,
':' => TokenKind::Colon,
'#' => TokenKind::Hash,
'.' => TokenKind::Dot,
'?' => TokenKind::Kw(Kw::Print), // Editor-Kurzform
'&' => TokenKind::Kw(Kw::Long), // isoliertes & (selten)
'&' => TokenKind::Kw(Kw::Long), // isoliertes & (selten)
'<' => {
if i < chars.len() && chars[i] == '=' {
i += 1;
@@ -538,9 +703,15 @@ pub fn lex(source: &str) -> LexOutput {
tokens.push(Token {
kind: TokenKind::Eof,
pos: SourcePos { line: (source.lines().count() + 1) as u32, column: 1 },
pos: SourcePos {
line: (source.lines().count() + 1) as u32,
column: 1,
},
});
LexOutput { tokens, diagnostics }
LexOutput {
tokens,
diagnostics,
}
}
#[cfg(test)]
@@ -565,7 +736,10 @@ mod tests {
let k = kinds("STRING$ STRING");
assert_eq!(
k[0],
TokenKind::Ident { name: "STRING".into(), suffix: Some(Suffix::Str) }
TokenKind::Ident {
name: "STRING".into(),
suffix: Some(Suffix::Str)
}
);
assert_eq!(k[1], TokenKind::Kw(Kw::String));
}
@@ -620,4 +794,23 @@ mod tests {
}
);
}
#[test]
fn bang_ist_suffix_oder_containeroperator_nach_folgetoken() {
let k = kinds("Wert! = 1.5\nForm1!Text1.Text = \"a\"");
assert_eq!(
k[0],
TokenKind::Ident {
name: "WERT".into(),
suffix: Some(Suffix::Single)
}
);
assert_eq!(
k[4],
TokenKind::Ident {
name: "FORM1!TEXT1.TEXT".into(),
suffix: None
}
);
}
}

View File

@@ -6,6 +6,7 @@
//! übersetzt.
pub mod ast;
pub mod forms;
pub mod hir;
pub mod lexer;
pub mod parser;
@@ -42,12 +43,24 @@ pub struct Analysis {
/// Komplette Pipeline: Lexen → Parsen → semantische Prüfung + Lowering.
pub fn analyze_source(module_name: &str, source: &str) -> Analysis {
analyze_source_with_forms(module_name, source, &forms::FormCatalog::default())
}
pub fn analyze_source_with_forms(
module_name: &str,
source: &str,
forms: &forms::FormCatalog,
) -> Analysis {
let lexed = lexer::lex(source);
let mut diagnostics = lexed.diagnostics;
let parsed = parser::parse(module_name, &lexed.tokens);
diagnostics.extend(parsed.diagnostics);
let (hir, sema_diags) = sema::lower(&parsed.module);
let (hir, sema_diags) = sema::lower_with_forms(&parsed.module, forms);
diagnostics.extend(sema_diags);
diagnostics.sort_by_key(|d| (d.pos.line, d.pos.column));
Analysis { module: parsed.module, diagnostics, hir }
Analysis {
module: parsed.module,
diagnostics,
hir,
}
}

View File

@@ -14,7 +14,12 @@ pub struct ParseOutput {
}
pub fn parse(module_name: &str, tokens: &[Token]) -> ParseOutput {
let mut p = P { toks: tokens, i: 0, diags: Vec::new(), at_line_start: true };
let mut p = P {
toks: tokens,
i: 0,
diags: Vec::new(),
at_line_start: true,
};
let mut body = Vec::new();
let mut procs = Vec::new();
@@ -40,7 +45,11 @@ pub fn parse(module_name: &str, tokens: &[Token]) -> ParseOutput {
}
ParseOutput {
module: Module { name: module_name.to_string(), body, procs },
module: Module {
name: module_name.to_string(),
body,
procs,
},
diagnostics: p.diags,
}
}
@@ -85,7 +94,10 @@ impl<'a> P<'a> {
}
fn err(&mut self, msg: impl Into<String>) {
let pos = self.pos();
self.diags.push(Diagnostic { pos, message: msg.into() });
self.diags.push(Diagnostic {
pos,
message: msg.into(),
});
}
fn expect_kw(&mut self, kw: Kw, what: &str) -> bool {
if self.eat_kw(kw) {
@@ -205,11 +217,21 @@ impl<'a> P<'a> {
}
TokenKind::MetaStatic => {
self.advance();
Some(Stmt::MetaArrays { static_arrays: true, pos })
Some(Stmt::MetaArrays {
static_arrays: true,
pos,
})
}
TokenKind::MetaDynamic => {
self.advance();
Some(Stmt::MetaArrays { static_arrays: false, pos })
Some(Stmt::MetaArrays {
static_arrays: false,
pos,
})
}
TokenKind::MetaForm => {
self.advance();
Some(Stmt::MetaForm { pos })
}
TokenKind::Kw(Kw::Print) => self.parse_print(pos, false),
TokenKind::Kw(Kw::Input) => {
@@ -368,8 +390,9 @@ impl<'a> P<'a> {
}
Some(Stmt::ConstDecl { items, pos })
}
TokenKind::Kw(k @ (Kw::DefInt | Kw::DefLng | Kw::DefSng
| Kw::DefDbl | Kw::DefStr | Kw::DefCur)) => {
TokenKind::Kw(
k @ (Kw::DefInt | Kw::DefLng | Kw::DefSng | Kw::DefDbl | Kw::DefStr | Kw::DefCur),
) => {
self.advance();
let ty = match k {
Kw::DefInt => TypeName::Integer,
@@ -382,9 +405,7 @@ impl<'a> P<'a> {
let mut ranges = Vec::new();
loop {
let a = match self.k() {
TokenKind::Ident { name, suffix: None }
if name.len() == 1 =>
{
TokenKind::Ident { name, suffix: None } if name.len() == 1 => {
self.advance();
name.chars().next().unwrap()
}
@@ -396,9 +417,7 @@ impl<'a> P<'a> {
};
let b = if self.eat(&TokenKind::Minus) {
match self.k() {
TokenKind::Ident { name, suffix: None }
if name.len() == 1 =>
{
TokenKind::Ident { name, suffix: None } if name.len() == 1 => {
self.advance();
name.chars().next().unwrap()
}
@@ -420,11 +439,12 @@ impl<'a> P<'a> {
TokenKind::Kw(Kw::Option) => {
self.advance();
match self.k() {
TokenKind::Ident { name, suffix: None }
if name == "EXPLICIT" =>
{
TokenKind::Ident { name, suffix: None } if name == "EXPLICIT" => {
self.advance();
Some(Stmt::OptionStmt { kind: OptionKind::Explicit, pos })
Some(Stmt::OptionStmt {
kind: OptionKind::Explicit,
pos,
})
}
TokenKind::Ident { name, suffix: None } if name == "BASE" => {
self.advance();
@@ -439,7 +459,10 @@ impl<'a> P<'a> {
0
}
};
Some(Stmt::OptionStmt { kind: OptionKind::Base(base), pos })
Some(Stmt::OptionStmt {
kind: OptionKind::Base(base),
pos,
})
}
_ => {
self.err("Expected: BASE or EXPLICIT");
@@ -466,7 +489,12 @@ impl<'a> P<'a> {
} else {
Vec::new()
};
Some(Stmt::Call { name, suffix, args, pos })
Some(Stmt::Call {
name,
suffix,
args,
pos,
})
}
_ => {
self.err("Expected: identifier");
@@ -510,7 +538,11 @@ impl<'a> P<'a> {
_ => cur.push(c),
}
}
items.push(if quotiert { cur } else { cur.trim().to_string() });
items.push(if quotiert {
cur
} else {
cur.trim().to_string()
});
Some(Stmt::Data { items, pos })
}
TokenKind::Kw(Kw::Read) => {
@@ -594,7 +626,13 @@ impl<'a> P<'a> {
var = Some(self.parse_name_ref()?);
}
}
Some(Stmt::GetPut { put: k == Kw::Put, file, recnum, var, pos })
Some(Stmt::GetPut {
put: k == Kw::Put,
file,
recnum,
var,
pos,
})
}
TokenKind::Kw(k @ (Kw::Lset | Kw::Rset)) => {
self.advance();
@@ -605,7 +643,12 @@ impl<'a> P<'a> {
return None;
}
let value = self.parse_expr()?;
Some(Stmt::LsetRset { rset: k == Kw::Rset, target, value, pos })
Some(Stmt::LsetRset {
rset: k == Kw::Rset,
target,
value,
pos,
})
}
TokenKind::Kw(Kw::Write) => {
self.advance();
@@ -636,7 +679,11 @@ impl<'a> P<'a> {
self.err("Expected: ,");
}
let position = self.parse_expr()?;
Some(Stmt::SeekStmt { file, position, pos })
Some(Stmt::SeekStmt {
file,
position,
pos,
})
}
TokenKind::Kw(Kw::Common) => {
self.advance();
@@ -654,7 +701,12 @@ impl<'a> P<'a> {
}
}
let decls = self.parse_var_decls();
Some(Stmt::CommonDecl { shared, block, decls, pos })
Some(Stmt::CommonDecl {
shared,
block,
decls,
pos,
})
}
TokenKind::Kw(Kw::Shared) => {
self.advance();
@@ -681,19 +733,20 @@ impl<'a> P<'a> {
}
Some(Stmt::ViewPrint { top, bottom, pos })
}
TokenKind::Ident { ref name, suffix: None }
if name == "NAME" && self.k_at(1) != TokenKind::Eq =>
{
TokenKind::Ident {
ref name,
suffix: None,
} if name == "NAME" && self.k_at(1) != TokenKind::Eq => {
self.advance();
let old = self.parse_expr()?;
self.expect_kw(Kw::As, "AS");
let new = self.parse_expr()?;
Some(Stmt::NameStmt { old, new, pos })
}
TokenKind::Ident { ref name, suffix: None }
if (name == "LOCK" || name == "UNLOCK")
&& self.k_at(1) != TokenKind::Eq =>
{
TokenKind::Ident {
ref name,
suffix: None,
} if (name == "LOCK" || name == "UNLOCK") && self.k_at(1) != TokenKind::Eq => {
let unlock = name == "UNLOCK";
self.advance();
self.eat(&TokenKind::Hash);
@@ -707,7 +760,13 @@ impl<'a> P<'a> {
to = Some(self.parse_expr()?);
}
}
Some(Stmt::LockStmt { unlock, file, from, to, pos })
Some(Stmt::LockStmt {
unlock,
file,
from,
to,
pos,
})
}
// ISAM-Anweisungen: `NAME [#]n [, arg …]`. Sie unterscheiden sich
// von einem gewöhnlichen impliziten Aufruf nur durch das
@@ -716,10 +775,12 @@ impl<'a> P<'a> {
// `=` und `(` schließen die Anweisungsdeutung aus: dann ist der
// Name eine Variable bzw. ein Array (`DELETE = 1`, `DELETE(2) = 1`).
// Keine ISAM-Syntaxform beginnt mit einer Klammer.
TokenKind::Ident { ref name, suffix: None }
if ist_isam_anweisung(name)
&& self.k_at(1) != TokenKind::Eq
&& self.k_at(1) != TokenKind::LParen =>
TokenKind::Ident {
ref name,
suffix: None,
} if ist_isam_anweisung(name)
&& self.k_at(1) != TokenKind::Eq
&& self.k_at(1) != TokenKind::LParen =>
{
self.parse_isam(name.clone(), pos)
}
@@ -740,11 +801,20 @@ impl<'a> P<'a> {
// Es senkt auf die Sentinel-Kennung ab, damit `ROLLBACK` eine
// einzige Signatur behält.
if name == "ROLLBACK" {
if let TokenKind::Ident { name: ref w, suffix: None } = self.k() {
if let TokenKind::Ident {
name: ref w,
suffix: None,
} = self.k()
{
if w == "ALL" {
self.advance();
let args = vec![Expr::LongLit(ROLLBACK_ALL)];
return Some(Stmt::Call { name, suffix: None, args, pos });
return Some(Stmt::Call {
name,
suffix: None,
args,
pos,
});
}
}
}
@@ -754,7 +824,12 @@ impl<'a> P<'a> {
} else {
self.parse_arg_list_to_stmt_end()
};
Some(Stmt::Call { name, suffix: None, args, pos })
Some(Stmt::Call {
name,
suffix: None,
args,
pos,
})
}
fn parse_print(&mut self, pos: SourcePos, printer: bool) -> Option<Stmt> {
@@ -793,7 +868,13 @@ impl<'a> P<'a> {
},
}
}
Some(Stmt::Print { printer, file, using, items, pos })
Some(Stmt::Print {
printer,
file,
using,
items,
pos,
})
}
/// `OPEN` in beiden Syntaxformen (FOR-Klausel und Kurzform).
@@ -813,7 +894,13 @@ impl<'a> P<'a> {
} else {
None
};
return Some(Stmt::OpenLegacy { mode: first, number, file, len, pos });
return Some(Stmt::OpenLegacy {
mode: first,
number,
file,
len,
pos,
});
}
let mut mode = None;
let mut isam = None;
@@ -823,7 +910,10 @@ impl<'a> P<'a> {
self.advance();
OpenMode::Input
}
TokenKind::Ident { ref name, suffix: None } => {
TokenKind::Ident {
ref name,
suffix: None,
} => {
let m = match name.as_str() {
"OUTPUT" => OpenMode::Output,
"APPEND" => OpenMode::Append,
@@ -873,7 +963,11 @@ impl<'a> P<'a> {
}
}
let mut access = None;
if let TokenKind::Ident { ref name, suffix: None } = self.k() {
if let TokenKind::Ident {
ref name,
suffix: None,
} = self.k()
{
if name == "ACCESS" {
self.advance();
let read = self.eat_kw(Kw::Read);
@@ -892,7 +986,11 @@ impl<'a> P<'a> {
let mut lock = None;
if self.eat_kw(Kw::Shared) {
lock = Some(LockClause::Shared);
} else if let TokenKind::Ident { ref name, suffix: None } = self.k() {
} else if let TokenKind::Ident {
ref name,
suffix: None,
} = self.k()
{
if name == "LOCK" {
self.advance();
let read = self.eat_kw(Kw::Read);
@@ -912,7 +1010,11 @@ impl<'a> P<'a> {
self.eat(&TokenKind::Hash);
let number = self.parse_expr()?;
let mut len = None;
if let TokenKind::Ident { ref name, suffix: None } = self.k() {
if let TokenKind::Ident {
ref name,
suffix: None,
} = self.k()
{
if name == "LEN" {
self.advance();
if !self.eat(&TokenKind::Eq) {
@@ -921,7 +1023,16 @@ impl<'a> P<'a> {
len = Some(self.parse_expr()?);
}
}
Some(Stmt::Open { file: first, mode, isam, access, lock, number, len, pos })
Some(Stmt::Open {
file: first,
mode,
isam,
access,
lock,
number,
len,
pos,
})
}
fn parse_input(&mut self, line: bool, pos: SourcePos) -> Option<Stmt> {
@@ -936,8 +1047,7 @@ impl<'a> P<'a> {
let mut prompt = None;
if file.is_none() {
if let TokenKind::Str(s) = self.k() {
if matches!(self.k_at(1), TokenKind::Semicolon | TokenKind::Comma)
{
if matches!(self.k_at(1), TokenKind::Semicolon | TokenKind::Comma) {
self.advance();
let with_question = self.k() == TokenKind::Semicolon;
self.advance();
@@ -959,7 +1069,14 @@ impl<'a> P<'a> {
if vars.is_empty() {
self.err("Expected: variable");
}
Some(Stmt::Input { line, file, keep_cursor, prompt, vars, pos })
Some(Stmt::Input {
line,
file,
keep_cursor,
prompt,
vars,
pos,
})
}
fn parse_if(&mut self, pos: SourcePos) -> Option<Stmt> {
@@ -991,7 +1108,13 @@ impl<'a> P<'a> {
} else {
self.err("Expected: END IF");
}
Some(Stmt::If { cond, then_body, elseifs, else_body, pos })
Some(Stmt::If {
cond,
then_body,
elseifs,
else_body,
pos,
})
} else {
// Einzeilig
let then_body = if let Some(n) = self.line_number_target() {
@@ -1008,7 +1131,13 @@ impl<'a> P<'a> {
} else {
None
};
Some(Stmt::If { cond, then_body, elseifs: Vec::new(), else_body, pos })
Some(Stmt::If {
cond,
then_body,
elseifs: Vec::new(),
else_body,
pos,
})
}
}
@@ -1112,7 +1241,14 @@ impl<'a> P<'a> {
} else {
self.err("FOR without NEXT");
}
Some(Stmt::For { var, from, to, step, body, pos })
Some(Stmt::For {
var,
from,
to,
step,
body,
pos,
})
}
fn parse_do(&mut self, pos: SourcePos) -> Option<Stmt> {
@@ -1135,7 +1271,12 @@ impl<'a> P<'a> {
} else {
self.err("DO without LOOP");
}
Some(Stmt::DoLoop { pre, post, body, pos })
Some(Stmt::DoLoop {
pre,
post,
body,
pos,
})
}
fn parse_while(&mut self, pos: SourcePos) -> Option<Stmt> {
@@ -1252,13 +1393,23 @@ impl<'a> P<'a> {
break;
}
}
Some(Stmt::OnGoto { expr, targets, gosub, pos })
Some(Stmt::OnGoto {
expr,
targets,
gosub,
pos,
})
}
fn parse_dim(&mut self, redim: bool, pos: SourcePos) -> Option<Stmt> {
let shared = self.eat_kw(Kw::Shared);
let decls = self.parse_var_decls();
Some(Stmt::Dim { shared, redim, decls, pos })
Some(Stmt::Dim {
shared,
redim,
decls,
pos,
})
}
/// Deklarationsliste `name[suffix][(dims)] [AS typ], …` — gemeinsame
@@ -1309,7 +1460,13 @@ impl<'a> P<'a> {
} else {
None
};
decls.push(VarDecl { name, suffix, dims, as_type, pos: dpos });
decls.push(VarDecl {
name,
suffix,
dims,
as_type,
pos: dpos,
});
if !self.eat(&TokenKind::Comma) {
break;
}
@@ -1362,7 +1519,11 @@ impl<'a> P<'a> {
}
TokenKind::Ident { name, suffix: None } => {
self.advance();
Some(TypeName::Udt(name))
Some(match name.as_str() {
"FORM" => TypeName::Form,
"CONTROL" => TypeName::Control,
_ => TypeName::Udt(name),
})
}
_ => {
self.err("Expected: type");
@@ -1397,7 +1558,10 @@ impl<'a> P<'a> {
break;
}
match self.k() {
TokenKind::Ident { name: fname, suffix: None } => {
TokenKind::Ident {
name: fname,
suffix: None,
} => {
self.advance();
if self.expect_kw(Kw::As, "AS") {
if let Some(t) = self.parse_type_name() {
@@ -1442,7 +1606,10 @@ impl<'a> P<'a> {
if !self.eat(&TokenKind::RParen) {
loop {
match self.k() {
TokenKind::Ident { name: pname, suffix: psfx } => {
TokenKind::Ident {
name: pname,
suffix: psfx,
} => {
self.advance();
let array = if self.eat(&TokenKind::LParen) {
self.eat(&TokenKind::RParen);
@@ -1474,7 +1641,12 @@ impl<'a> P<'a> {
self.eat(&TokenKind::RParen);
}
}
Some(ProcSig { kind, name, suffix, params })
Some(ProcSig {
kind,
name,
suffix,
params,
})
}
fn parse_proc(&mut self) -> Option<Proc> {
@@ -1495,14 +1667,19 @@ impl<'a> P<'a> {
ProcKind::Function => "Expected: END FUNCTION",
});
}
Some(Proc { sig, is_static, body, pos })
Some(Proc {
sig,
is_static,
body,
pos,
})
}
fn parse_def_fn(&mut self, pos: SourcePos) -> Option<Stmt> {
self.advance(); // DEF
// `DEF SEG` ist Segmentadressierung und damit deklariertes
// Non-Feature — an der Syntax erkennbar, weil `DEF` sonst nur
// `DEF FNname` einleitet.
// `DEF SEG` ist Segmentadressierung und damit deklariertes
// Non-Feature — an der Syntax erkennbar, weil `DEF` sonst nur
// `DEF FNname` einleitet.
if matches!(self.k(), TokenKind::Ident { ref name, suffix: None } if name == "SEG") {
self.err("Feature unavailable");
self.sync();
@@ -1524,7 +1701,10 @@ impl<'a> P<'a> {
if !self.eat(&TokenKind::RParen) {
loop {
match self.k() {
TokenKind::Ident { name: pname, suffix: psfx } => {
TokenKind::Ident {
name: pname,
suffix: psfx,
} => {
self.advance();
params.push(Param {
name: pname,
@@ -1547,7 +1727,13 @@ impl<'a> P<'a> {
}
if self.eat(&TokenKind::Eq) {
let body = self.parse_expr()?;
Some(Stmt::DefFn { name, suffix, params, body, pos })
Some(Stmt::DefFn {
name,
suffix,
params,
body,
pos,
})
} else {
// Blockform: DEF FNname(...) … [EXIT DEF] … END DEF
let body = self.parse_stmt_list(|p: &P| p.at_end_pair(Kw::Def));
@@ -1557,7 +1743,13 @@ impl<'a> P<'a> {
} else {
self.err("Expected: END DEF");
}
Some(Stmt::DefFnBlock { name, suffix, params, body, pos })
Some(Stmt::DefFnBlock {
name,
suffix,
params,
body,
pos,
})
}
}
@@ -1612,7 +1804,10 @@ impl<'a> P<'a> {
}
}
// Impliziter Aufruf: `name [arg [, arg …]]`
if let Expr::Name { name, suffix, args, .. } = target {
if let Expr::Name {
name, suffix, args, ..
} = target
{
// Ohne CALL-Keyword sind Klammern Wert-Klammern (BYVAL), keine
// Argumentlisten-Klammern: `Foo (n%)` übergibt `(n%)`.
let mut call_args: Vec<Expr> = args
@@ -1626,7 +1821,12 @@ impl<'a> P<'a> {
if !self.at_stmt_end() && call_args.is_empty() {
call_args = self.parse_arg_list_to_stmt_end();
}
Some(Stmt::Call { name, suffix, args: call_args, pos })
Some(Stmt::Call {
name,
suffix,
args: call_args,
pos,
})
} else {
self.err("Syntax error");
self.sync();
@@ -1640,7 +1840,7 @@ impl<'a> P<'a> {
fn parse_name_ref(&mut self) -> Option<Expr> {
let pos = self.pos();
match self.k() {
TokenKind::Ident { name, suffix } => {
TokenKind::Ident { mut name, suffix } => {
self.advance();
let args = if self.eat(&TokenKind::LParen) {
let a = self.parse_arg_list(&TokenKind::RParen);
@@ -1649,7 +1849,25 @@ impl<'a> P<'a> {
} else {
None
};
Some(Expr::Name { name, suffix, args, pos })
if self.eat(&TokenKind::Dot) {
match self.k() {
TokenKind::Ident {
name: member,
suffix: None,
} => {
name.push('.');
name.push_str(&member);
self.advance();
}
_ => self.err("Expected: property"),
}
}
Some(Expr::Name {
name,
suffix,
args,
pos,
})
}
_ => {
self.err("Expected: variable");
@@ -1665,7 +1883,12 @@ impl<'a> P<'a> {
match self.k() {
TokenKind::Ident { name, suffix } => {
self.advance();
Some(Expr::Name { name, suffix, args: None, pos })
Some(Expr::Name {
name,
suffix,
args: None,
pos,
})
}
_ => {
self.err("Expected: variable");
@@ -1773,7 +1996,12 @@ impl<'a> P<'a> {
let pos = self.pos();
self.advance();
let rhs = self.parse_bin(prec + 1)?;
lhs = Expr::Binary { op, lhs: Box::new(lhs), rhs: Box::new(rhs), pos };
lhs = Expr::Binary {
op,
lhs: Box::new(lhs),
rhs: Box::new(rhs),
pos,
};
}
Some(lhs)
}
@@ -1809,7 +2037,11 @@ impl<'a> P<'a> {
TokenKind::Minus => {
self.advance();
let operand = self.parse_bin(12)?;
Some(Expr::Unary { op: UnOp::Neg, operand: Box::new(operand), pos })
Some(Expr::Unary {
op: UnOp::Neg,
operand: Box::new(operand),
pos,
})
}
TokenKind::Plus => {
self.advance();
@@ -1818,7 +2050,11 @@ impl<'a> P<'a> {
TokenKind::Kw(Kw::Not) => {
self.advance();
let operand = self.parse_bin(6)?;
Some(Expr::Unary { op: UnOp::Not, operand: Box::new(operand), pos })
Some(Expr::Unary {
op: UnOp::Not,
operand: Box::new(operand),
pos,
})
}
_ => self.parse_primary(),
}
@@ -1842,6 +2078,30 @@ impl<'a> P<'a> {
pos,
});
}
if matches!(self.k(), TokenKind::Ident { ref name, suffix: None } if name == "TYPEOF") {
let pos = self.pos();
self.advance();
let value = self.parse_primary()?;
if !self.eat_kw(Kw::Is) {
self.err("Expected: IS");
return None;
}
let class = match self.k() {
TokenKind::Ident { name, suffix: None } => {
self.advance();
name
}
_ => {
self.err("Expected: control class");
return None;
}
};
return Some(Expr::TypeOf {
value: Box::new(value),
class,
pos,
});
}
match self.k() {
TokenKind::Num(n) => {
self.advance();
@@ -1913,13 +2173,21 @@ mod tests {
"IF a > 1 THEN\nPRINT 1\nELSEIF a > 0 THEN\nPRINT 2\nELSE\nPRINT 3\nEND IF\nIF b THEN PRINT 4 ELSE PRINT 5",
);
match &m.body[0] {
Stmt::If { elseifs, else_body, .. } => {
Stmt::If {
elseifs, else_body, ..
} => {
assert_eq!(elseifs.len(), 1);
assert!(else_body.is_some());
}
other => panic!("erwartet IF, war {other:?}"),
}
assert!(matches!(&m.body[1], Stmt::If { else_body: Some(_), .. }));
assert!(matches!(
&m.body[1],
Stmt::If {
else_body: Some(_),
..
}
));
}
#[test]
@@ -1943,8 +2211,22 @@ mod tests {
"FOR i = 1 TO 10 STEP 2\nNEXT i\nDO WHILE x\nLOOP\nDO\nLOOP UNTIL x\nWHILE x\nWEND",
);
assert!(matches!(&m.body[0], Stmt::For { step: Some(_), .. }));
assert!(matches!(&m.body[1], Stmt::DoLoop { pre: Some((false, _)), post: None, .. }));
assert!(matches!(&m.body[2], Stmt::DoLoop { pre: None, post: Some((true, _)), .. }));
assert!(matches!(
&m.body[1],
Stmt::DoLoop {
pre: Some((false, _)),
post: None,
..
}
));
assert!(matches!(
&m.body[2],
Stmt::DoLoop {
pre: None,
post: Some((true, _)),
..
}
));
assert!(matches!(&m.body[3], Stmt::While { .. }));
}
@@ -1963,9 +2245,29 @@ mod tests {
let m = parse_ok(
"ON ERROR GOTO Handler\nON LOCAL ERROR RESUME NEXT\nON ERROR GOTO 0\nHandler:\nRESUME NEXT",
);
assert!(matches!(&m.body[0], Stmt::OnError { local: false, action: OnErrorAction::Goto(_), .. }));
assert!(matches!(&m.body[1], Stmt::OnError { local: true, action: OnErrorAction::ResumeNext, .. }));
assert!(matches!(&m.body[2], Stmt::OnError { action: OnErrorAction::Disable, .. }));
assert!(matches!(
&m.body[0],
Stmt::OnError {
local: false,
action: OnErrorAction::Goto(_),
..
}
));
assert!(matches!(
&m.body[1],
Stmt::OnError {
local: true,
action: OnErrorAction::ResumeNext,
..
}
));
assert!(matches!(
&m.body[2],
Stmt::OnError {
action: OnErrorAction::Disable,
..
}
));
}
#[test]
@@ -1973,7 +2275,15 @@ mod tests {
let m = parse_ok("x = 1 + 2 * 3 ^ 2");
// 1 + (2 * (3 ^ 2))
match &m.body[0] {
Stmt::Assign { value: Expr::Binary { op: BinOp::Add, rhs, .. }, .. } => {
Stmt::Assign {
value:
Expr::Binary {
op: BinOp::Add,
rhs,
..
},
..
} => {
assert!(matches!(**rhs, Expr::Binary { op: BinOp::Mul, .. }));
}
other => panic!("unerwartet: {other:?}"),
@@ -1989,4 +2299,18 @@ mod tests {
assert!(matches!(&m.body[1], Stmt::Dim { shared: true, decls, .. } if decls.len() == 2));
assert!(matches!(&m.body[3], Stmt::DefType { ranges, .. } if ranges.len() == 2));
}
#[test]
fn forms_syntax() {
let m = parse_ok(
"'$FORM\nDIM f AS FORM, c AS CONTROL\nIF TYPEOF c IS CommandButton THEN PRINT 1",
);
assert!(matches!(m.body[0], Stmt::MetaForm { .. }));
assert!(matches!(&m.body[1], Stmt::Dim { decls, .. }
if decls[0].as_type == Some(TypeName::Form) && decls[1].as_type == Some(TypeName::Control)));
assert!(
matches!(&m.body[2], Stmt::If { cond: Expr::TypeOf { class, .. }, .. }
if class == "COMMANDBUTTON")
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -31,6 +31,7 @@ struct Eintrag {
art: String,
status: String,
fundstelle: String,
quelle: String,
zeile: usize,
}
@@ -61,6 +62,7 @@ fn inventar() -> Vec<Eintrag> {
art: spalten[1].to_string(),
status: spalten[3].to_string(),
fundstelle: spalten[4].to_string(),
quelle: spalten[5].to_string(),
zeile: i + 1,
});
}
@@ -155,7 +157,10 @@ fn stand() -> Stand {
signaturen,
verboten: literale(ban),
nicht_verfuegbar,
syntaktisch: literale(syn).into_iter().map(|s| s.to_uppercase()).collect(),
syntaktisch: literale(syn)
.into_iter()
.map(|s| s.to_uppercase())
.collect(),
}
}
@@ -167,7 +172,15 @@ fn statusvokabular_ist_beschraenkt() {
assert!(
matches!(
e.art.as_str(),
"Anweisung" | "Funktion" | "Metabefehl" | "Routine"
"Anweisung"
| "Funktion"
| "Metabefehl"
| "Routine"
| "Eigenschaft"
| "Methode"
| "Ereignis"
| "Datentyp"
| "Operator"
),
"Zeile {}: `{}` trägt unzulässige Art `{}`",
e.zeile,
@@ -185,6 +198,84 @@ fn statusvokabular_ist_beschraenkt() {
}
}
fn pruefe_forms(inv: &[Eintrag]) -> Vec<String> {
use tb_frontend::forms::{events, method_is_implemented, methods, properties, ObjectClass};
let mut erwartet = BTreeMap::new();
for class in ObjectClass::ALL {
for p in properties(class) {
erwartet.insert(
format!("{}.{}", class.name(), p.name),
("Eigenschaft", IMPLEMENTIERT, "tb-ui::forms"),
);
}
for m in methods(class) {
erwartet.insert(
format!("{}.{m}", class.name()),
if method_is_implemented(class, m) {
("Methode", IMPLEMENTIERT, "tb-ui::forms")
} else {
("Methode", OFFEN, "-")
},
);
}
for e in events(class) {
erwartet.insert(
format!("{}_{e}", class.name()),
("Ereignis", IMPLEMENTIERT, "tb-vm::interp"),
);
}
}
let gefunden: BTreeMap<_, _> = inv
.iter()
.filter(|e| matches!(e.art.as_str(), "Eigenschaft" | "Methode" | "Ereignis"))
.map(|e| (e.name.to_uppercase(), e))
.collect();
let mut fehler = Vec::new();
for (name, (art, status, fundstelle)) in &erwartet {
match gefunden.get(name) {
None => fehler.push(format!("`{name}` fehlt")),
Some(e)
if e.art != *art
|| e.status != *status
|| e.fundstelle != *fundstelle
|| e.quelle != "forms-referenz" =>
{
fehler.push(format!("`{name}`: Art/Status/Fundstelle/Quelle falsch"))
}
Some(_) => {}
}
}
for name in gefunden.keys() {
if !erwartet.contains_key(name) {
fehler.push(format!("`{name}` ist nicht in der Klassentabelle"));
}
}
fehler
}
#[test]
fn forms_inventar_stimmt_mit_der_klassentabelle_ueberein() {
let fehler = pruefe_forms(&inventar());
assert!(
fehler.is_empty(),
"Forms-Inventar weicht ab:\n {}",
fehler.join("\n ")
);
}
#[test]
fn falsch_gesetzte_eigenschaft_wird_erkannt() {
let mut inv = inventar();
let e = inv
.iter_mut()
.find(|e| e.name == "TEXTBOX.SELSTART")
.unwrap();
e.status = OFFEN.into();
assert!(pruefe_forms(&inv)
.iter()
.any(|f| f.contains("TEXTBOX.SELSTART")));
}
#[test]
fn non_features_nennen_eine_fundstelle_in_der_sprachreferenz() {
for e in inventar() {
@@ -343,11 +434,7 @@ fn inventar_stimmt_mit_code_ueberein() {
fn abdeckungsstand_wird_ausgewiesen() {
let inv = inventar();
let zaehle = |s: &str| inv.iter().filter(|e| e.status == s).count();
let (i, o, n) = (
zaehle(IMPLEMENTIERT),
zaehle(OFFEN),
zaehle(NON_FEATURE),
);
let (i, o, n) = (zaehle(IMPLEMENTIERT), zaehle(OFFEN), zaehle(NON_FEATURE));
println!(
"Abdeckung: implementiert {i} · offen {o} · Non-Feature {n} · gesamt {}",
inv.len()
@@ -384,10 +471,22 @@ fn dokumentierte_elemente_werden_namentlich_abgewiesen() {
("CALLS Foo", "CALLS", "Feature unavailable"),
// Non-Feature über die Syntax (teilt das Schlüsselwort)
("LINE (1, 1)-(2, 2)", "LINE", "Feature unavailable"),
("GET (1, 1)-(2, 2), A", "GET (Grafik)", "Feature unavailable"),
(r#"OPEN "COM1:9600,N,8,1" AS #1"#, "OPEN COM", "Feature unavailable"),
(
"GET (1, 1)-(2, 2), A",
"GET (Grafik)",
"Feature unavailable",
),
(
r#"OPEN "COM1:9600,N,8,1" AS #1"#,
"OPEN COM",
"Feature unavailable",
),
// Ereignisgerät als Funktionsform
("ON PEN GOSUB Ziel\nZiel:\nRETURN", "ON PEN", "Feature unavailable"),
(
"ON PEN GOSUB Ziel\nZiel:\nRETURN",
"ON PEN",
"Feature unavailable",
),
];
for (quelle, element, erwartet) in faelle {
@@ -411,7 +510,12 @@ fn dokumentierte_elemente_werden_namentlich_abgewiesen() {
/// generischen Syntaxfehler.
#[test]
fn offene_elemente_erzeugen_keinen_syntaxfehler() {
for quelle in ["CLS", "LOCATE 5, 10", "COLOR 14, 1", r#"OPEN "d.txt" FOR OUTPUT AS #1"#] {
for quelle in [
"CLS",
"LOCATE 5, 10",
"COLOR 14, 1",
r#"OPEN "d.txt" FOR OUTPUT AS #1"#,
] {
let a = tb_frontend::analyze_source("probe.bas", quelle);
assert!(
a.diagnostics.is_empty(),

File diff suppressed because it is too large Load Diff

View File

@@ -70,6 +70,11 @@ impl RuntimeError {
pub const INVALID_SCREEN_MODE: Self = Self(271);
pub const INVALID_WHEN_FORMS_SHOWING: Self = Self(272);
pub const INVALID_PROPERTY_VALUE: Self = Self(380);
pub const DESIGN_TIME_CONTROL: Self = Self(362);
pub const FORM_ALREADY_DISPLAYED: Self = Self(400);
pub const NON_MODAL_WHILE_MODAL: Self = Self(401);
pub const MODAL_NOT_TOPMOST: Self = Self(402);
pub const MDI_FORM_MODAL: Self = Self(403);
pub const NO_ACTIVE_CONTROL: Self = Self(430);
pub const NO_ACTIVE_FORM: Self = Self(431);
@@ -219,7 +224,10 @@ mod tests {
"Invalid when forms are showing"
);
assert_eq!(RuntimeError::NO_ACTIVE_FORM.code(), 431);
assert_eq!(RuntimeError(403).message(), "MDI form cannot be shown modally");
assert_eq!(
RuntimeError(403).message(),
"MDI form cannot be shown modally"
);
}
#[test]

View File

@@ -18,6 +18,8 @@ pub enum Value {
Dbl(f64),
Cur(i64),
Str(Rc<str>),
/// Formular-/Control-Objekt mit optionalem Index eines Control-Arrays.
Obj(u16, Option<i32>),
Arr(Rc<RefCell<ArrayObj>>),
Rec(Rc<RefCell<RecordObj>>),
/// Referenz (BYREF-Parameter-Slot).
@@ -91,7 +93,11 @@ pub struct ArrayObj {
}
impl ArrayObj {
pub fn new(elem: TypeInit, dims: Vec<(i32, i32)>, udts: &[UdtLayout]) -> Result<Self, RuntimeError> {
pub fn new(
elem: TypeInit,
dims: Vec<(i32, i32)>,
udts: &[UdtLayout],
) -> Result<Self, RuntimeError> {
let mut len: usize = 1;
for (lo, hi) in &dims {
if hi < lo {

View File

@@ -8,6 +8,7 @@ authors.workspace = true
[dependencies]
tb-runtime.workspace = true
tb-frontend.workspace = true
ratatui.workspace = true
crossterm.workspace = true
signal-hook.workspace = true

View File

@@ -3,4 +3,592 @@
//! Ziel ist volle Kompatibilität zum Forms-Modell des Vorbilds inklusive
//! des textbasierten Formular-Dateiformats (`.FRM`).
// Platzhalter — wird in Phase 4 ausgearbeitet (siehe PLAN.md)
use std::collections::{BTreeMap, VecDeque};
use tb_frontend::forms::{self, FormObject, ObjectClass, PropertyDefault, PropertyType};
use tb_runtime::errors::RuntimeError;
#[derive(Debug, Clone, PartialEq)]
pub enum PropertyValue {
Integer(i32),
Single(f32),
String(String),
Boolean(bool),
Object(Option<(u16, Option<i32>)>),
IntegerArray(Vec<i32>),
}
impl PropertyValue {
fn from_default(default: PropertyDefault, ty: PropertyType) -> Self {
match default {
PropertyDefault::Integer(v) if ty == PropertyType::IntegerArray => {
Self::IntegerArray(vec![v; 18])
}
PropertyDefault::Integer(v) => Self::Integer(v),
PropertyDefault::Single(v) => Self::Single(v),
PropertyDefault::String(v) => Self::String(v.into()),
PropertyDefault::Boolean(v) => Self::Boolean(v),
PropertyDefault::Empty if ty == PropertyType::Object => Self::Object(None),
PropertyDefault::Empty if ty == PropertyType::String => Self::String(String::new()),
PropertyDefault::Empty => Self::Integer(0),
}
}
}
#[derive(Debug, Clone)]
pub struct ObjectInstance {
pub description: FormObject,
properties: Vec<PropertyValue>,
pub loaded: bool,
pub visible: bool,
pub design_time: bool,
pub array_index: Option<i32>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct FormEvent {
pub object: u16,
pub array_index: Option<i32>,
pub name: String,
pub args: Vec<PropertyValue>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ShowResult {
Modeless,
ModalWait,
}
/// Reines Zustandsmodell. Zeichnen und Bedienung kommen im Folge-Change;
/// diese Schicht hält Objekte, Lebenszyklus, Modalität und Ereignisqueue.
pub struct FormsModel {
pub objects: Vec<ObjectInstance>,
dynamic: BTreeMap<(u16, i32), ObjectInstance>,
pub events: VecDeque<FormEvent>,
modal: Vec<u16>,
visible_forms: Vec<u16>,
active_form: Option<u16>,
active_control: Option<(u16, Option<i32>)>,
width: usize,
height: usize,
screen_visible: bool,
}
impl FormsModel {
fn set_visible_property(obj: &mut ObjectInstance, visible: bool) {
if let Some((property, _)) = forms::property(obj.description.class, "VISIBLE") {
obj.properties[property as usize] = PropertyValue::Boolean(visible);
}
}
fn clear_active_control_for_form(&mut self, form: u16) {
let Some((control, _)) = self.active_control else {
return;
};
let Some(form_name) = self
.objects
.get(form as usize)
.map(|o| o.description.name.as_str())
else {
return;
};
if self
.objects
.get(control as usize)
.and_then(|o| o.description.parent_form.as_deref())
.is_some_and(|parent| parent.eq_ignore_ascii_case(form_name))
{
self.active_control = None;
}
}
pub fn new(objects: Vec<FormObject>, width: usize, height: usize) -> Self {
let objects = objects
.into_iter()
.map(|description| {
let properties = forms::properties(description.class)
.into_iter()
.map(|p| PropertyValue::from_default(p.default, p.ty))
.collect();
let loaded = description.class == ObjectClass::Screen;
ObjectInstance {
description,
properties,
loaded,
visible: false,
design_time: true,
array_index: None,
}
})
.collect();
Self {
objects,
dynamic: BTreeMap::new(),
events: VecDeque::new(),
modal: Vec::new(),
visible_forms: Vec::new(),
active_form: None,
active_control: None,
width,
height,
screen_visible: true,
}
}
pub fn resize(&mut self, width: usize, height: usize) {
self.width = width;
self.height = height;
}
pub fn get(&mut self, object: u16, property: u16) -> Result<PropertyValue, RuntimeError> {
self.get_at(object, None, property)
}
pub fn get_at(
&mut self,
object: u16,
index: Option<i32>,
property: u16,
) -> Result<PropertyValue, RuntimeError> {
self.ensure_loaded_at(object, index)?;
let obj = if index.is_some_and(|i| i != 0) {
self.dynamic
.get(&(object, index.unwrap()))
.ok_or(RuntimeError(340))?
} else {
self.objects.get(object as usize).ok_or(RuntimeError(420))?
};
let spec = forms::properties(obj.description.class)
.get(property as usize)
.copied()
.ok_or(RuntimeError(422))?;
if obj.description.class == ObjectClass::Screen {
return Ok(match spec.name {
"WIDTH" => PropertyValue::Integer(self.width as i32),
"HEIGHT" => PropertyValue::Integer(self.height as i32),
"ACTIVEFORM" => PropertyValue::Object(self.active_form.map(|id| (id, None))),
"ACTIVECONTROL" => PropertyValue::Object(self.active_control),
_ => obj.properties[property as usize].clone(),
});
}
Ok(obj.properties[property as usize].clone())
}
pub fn set(
&mut self,
object: u16,
property: u16,
value: PropertyValue,
) -> Result<(), RuntimeError> {
self.set_at(object, None, property, value)
}
pub fn set_at(
&mut self,
object: u16,
index: Option<i32>,
property: u16,
value: PropertyValue,
) -> Result<(), RuntimeError> {
self.ensure_loaded_at(object, index)?;
let obj = if index.is_some_and(|i| i != 0) {
self.dynamic
.get_mut(&(object, index.unwrap()))
.ok_or(RuntimeError(340))?
} else {
self.objects
.get_mut(object as usize)
.ok_or(RuntimeError(420))?
};
let spec = forms::properties(obj.description.class)
.get(property as usize)
.copied()
.ok_or(RuntimeError(422))?;
if !spec.writable {
return Err(RuntimeError(383));
}
let type_ok = matches!(
(&value, spec.ty),
(PropertyValue::Integer(_), PropertyType::Integer)
| (PropertyValue::Single(_), PropertyType::Single)
| (PropertyValue::String(_), PropertyType::String)
| (PropertyValue::Boolean(_), PropertyType::Boolean)
| (PropertyValue::Object(_), PropertyType::Object)
| (PropertyValue::IntegerArray(_), PropertyType::IntegerArray)
);
if !type_ok {
return Err(RuntimeError::TYPE_MISMATCH);
}
if let PropertyValue::Integer(v) = value {
if spec.min.is_some_and(|min| v < min) || spec.max.is_some_and(|max| v > max) {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
}
obj.properties[property as usize] = PropertyValue::Integer(v);
} else {
obj.properties[property as usize] = value;
}
if spec.name == "VISIBLE" {
obj.visible = matches!(
obj.properties[property as usize],
PropertyValue::Boolean(true)
);
}
Ok(())
}
pub fn ensure_loaded(&mut self, object: u16) -> Result<(), RuntimeError> {
let obj = self
.objects
.get_mut(object as usize)
.ok_or(RuntimeError(420))?;
if obj.loaded {
return Ok(());
}
obj.loaded = true;
if obj.description.class == ObjectClass::Form {
self.events.push_back(FormEvent {
object,
array_index: None,
name: "LOAD".into(),
args: Vec::new(),
});
}
Ok(())
}
pub fn ensure_loaded_at(
&mut self,
object: u16,
index: Option<i32>,
) -> Result<(), RuntimeError> {
if index.is_some_and(|i| i != 0) {
self.dynamic
.get(&(object, index.unwrap()))
.map(|_| ())
.ok_or(RuntimeError(340))
} else {
self.ensure_loaded(object)
}
}
pub fn show(&mut self, object: u16, modal: bool) -> Result<ShowResult, RuntimeError> {
self.ensure_loaded(object)?;
let obj = self
.objects
.get_mut(object as usize)
.ok_or(RuntimeError(420))?;
if obj.description.class != ObjectClass::Form {
return Err(RuntimeError(421));
}
if modal {
if obj.visible {
return Err(RuntimeError(400));
}
let form_type = forms::property(ObjectClass::Form, "FORMTYPE").unwrap().0 as usize;
if obj.properties.get(form_type) == Some(&PropertyValue::Integer(1)) {
return Err(RuntimeError(403));
}
self.modal.push(object);
} else if !self.modal.is_empty() {
return Err(RuntimeError(401));
}
obj.visible = true;
Self::set_visible_property(obj, true);
self.visible_forms.retain(|id| *id != object);
self.visible_forms.push(object);
self.active_form = Some(object);
Ok(if modal {
ShowResult::ModalWait
} else {
ShowResult::Modeless
})
}
pub fn hide(&mut self, object: u16) -> Result<(), RuntimeError> {
if self.modal.contains(&object) && self.modal.last().copied() != Some(object) {
return Err(RuntimeError(402));
}
let obj = self
.objects
.get_mut(object as usize)
.ok_or(RuntimeError(420))?;
obj.visible = false;
Self::set_visible_property(obj, false);
self.visible_forms.retain(|id| *id != object);
if self.modal.last().copied() == Some(object) {
self.modal.pop();
}
if self.active_form == Some(object) {
self.active_form = self.visible_forms.last().copied();
}
self.clear_active_control_for_form(object);
Ok(())
}
pub fn unload_with(
&mut self,
object: u16,
mut handler: impl FnMut(&mut i16),
) -> Result<bool, RuntimeError> {
if self.modal.contains(&object) && self.modal.last().copied() != Some(object) {
return Err(RuntimeError(402));
}
let class = self
.objects
.get(object as usize)
.ok_or(RuntimeError(420))?
.description
.class;
if class != ObjectClass::Form {
return Err(RuntimeError(361));
}
let mut cancel = 0i16;
handler(&mut cancel);
if cancel != 0 {
return Ok(false);
}
let obj = &mut self.objects[object as usize];
obj.visible = false;
Self::set_visible_property(obj, false);
obj.loaded = false;
self.visible_forms.retain(|id| *id != object);
if self.modal.last().copied() == Some(object) {
self.modal.pop();
}
if self.active_form == Some(object) {
self.active_form = self.visible_forms.last().copied();
}
self.clear_active_control_for_form(object);
Ok(true)
}
pub fn load_array(&mut self, base: u16, index: i32) -> Result<(), RuntimeError> {
let template = self.objects.get(base as usize).ok_or(RuntimeError(420))?;
if !template.description.array {
return Err(RuntimeError(343));
}
if index == 0 {
return Err(RuntimeError(360));
}
let key = (base, index);
if self.dynamic.contains_key(&key) {
return Err(RuntimeError(360));
}
let mut obj = template.clone();
obj.loaded = true;
obj.visible = false;
obj.design_time = false;
obj.array_index = Some(index);
self.dynamic.insert(key, obj);
Ok(())
}
pub fn unload_array(&mut self, base: u16, index: i32) -> Result<(), RuntimeError> {
let template = self.objects.get(base as usize).ok_or(RuntimeError(420))?;
if !template.description.array {
return Err(RuntimeError(343));
}
if template.array_index == Some(index) || (template.array_index.is_none() && index == 0) {
return Err(RuntimeError(362));
}
self.dynamic
.remove(&(base, index))
.map(|_| ())
.ok_or(RuntimeError(340))
}
pub fn queue(&mut self, event: FormEvent) {
self.events.push_back(event);
}
pub fn next_event(&mut self) -> Option<FormEvent> {
self.events.pop_front()
}
pub fn is_visible(&self, object: u16) -> bool {
self.objects.get(object as usize).is_some_and(|o| o.visible)
}
pub fn is_loaded(&self, object: u16) -> bool {
self.objects.get(object as usize).is_some_and(|o| o.loaded)
}
pub fn is_loaded_at(&self, object: u16, index: Option<i32>) -> bool {
if index.is_some_and(|i| i != 0) {
self.dynamic.contains_key(&(object, index.unwrap()))
} else {
self.is_loaded(object)
}
}
pub fn active_form(&self) -> Option<u16> {
self.active_form
}
pub fn set_active_control(
&mut self,
object: u16,
index: Option<i32>,
) -> Result<(), RuntimeError> {
self.ensure_loaded_at(object, index)?;
let description = if index.is_some_and(|i| i != 0) {
&self
.dynamic
.get(&(object, index.unwrap()))
.unwrap()
.description
} else {
&self
.objects
.get(object as usize)
.ok_or(RuntimeError(420))?
.description
};
if matches!(description.class, ObjectClass::Form | ObjectClass::Screen) {
return Err(RuntimeError(421));
}
let parent = description
.parent_form
.as_deref()
.ok_or(RuntimeError(421))?;
self.active_form = self
.objects
.iter()
.position(|o| {
o.description.class == ObjectClass::Form
&& o.description.name.eq_ignore_ascii_case(parent)
})
.map(|id| id as u16);
self.active_control = Some((object, index));
Ok(())
}
pub fn modal_top(&self) -> Option<u16> {
self.modal.last().copied()
}
pub fn screen_show(&mut self, visible: bool) {
self.screen_visible = visible;
}
pub fn screen_visible(&self) -> bool {
self.screen_visible
}
}
#[cfg(test)]
mod tests {
use super::*;
fn model() -> FormsModel {
let mut c = forms::FormCatalog::default();
c.add("Form1", ObjectClass::Form, None, false);
c.add("Check1", ObjectClass::CheckBox, Some("Form1"), false);
c.add("Command1", ObjectClass::CommandButton, Some("Form1"), true);
c.add("SCREEN", ObjectClass::Screen, None, false);
FormsModel::new(c.objects, 80, 25)
}
#[test]
fn defaults_bereich_und_implizites_laden() {
let mut m = model();
let value = forms::property(ObjectClass::CheckBox, "VALUE").unwrap().0;
assert_eq!(m.get(1, value).unwrap(), PropertyValue::Integer(0));
assert!(m.objects[1].loaded);
assert_eq!(
m.set(1, value, PropertyValue::Integer(3)),
Err(RuntimeError(5))
);
}
#[test]
fn hide_entlaedt_nicht_und_unload_ist_abbrechbar() {
let mut m = model();
m.show(0, false).unwrap();
m.hide(0).unwrap();
assert!(m.objects[0].loaded);
assert!(!m.unload_with(0, |cancel| *cancel = 1).unwrap());
assert!(m.objects[0].loaded);
assert!(m.unload_with(0, |_| {}).unwrap());
}
#[test]
fn arrays_und_designzeitfehler() {
let mut m = model();
let caption = forms::property(ObjectClass::CommandButton, "CAPTION")
.unwrap()
.0;
m.set(2, caption, PropertyValue::String("Vorlage".into()))
.unwrap();
assert_eq!(m.load_array(2, 0), Err(RuntimeError(360)));
m.load_array(2, 3).unwrap();
assert_eq!(
m.get_at(2, Some(3), caption).unwrap(),
PropertyValue::String("Vorlage".into())
);
m.set_at(2, Some(3), caption, PropertyValue::String("Klon".into()))
.unwrap();
assert_eq!(
m.get(2, caption).unwrap(),
PropertyValue::String("Vorlage".into())
);
assert_eq!(m.unload_array(2, 0), Err(RuntimeError(362)));
m.unload_array(2, 3).unwrap();
}
#[test]
fn screen_meldet_oberstes_formular_und_aktives_control() {
let mut c = forms::FormCatalog::default();
c.add("A", ObjectClass::Form, None, false);
c.add("B", ObjectClass::Form, None, false);
c.add("Text1", ObjectClass::TextBox, Some("B"), false);
c.add("SCREEN", ObjectClass::Screen, None, false);
let mut m = FormsModel::new(c.objects, 80, 25);
m.show(0, false).unwrap();
m.show(1, false).unwrap();
m.set_active_control(2, None).unwrap();
let active_form = forms::property(ObjectClass::Screen, "ACTIVEFORM")
.unwrap()
.0;
let active_control = forms::property(ObjectClass::Screen, "ACTIVECONTROL")
.unwrap()
.0;
assert_eq!(
m.get(3, active_form).unwrap(),
PropertyValue::Object(Some((1, None)))
);
assert_eq!(
m.get(3, active_control).unwrap(),
PropertyValue::Object(Some((2, None)))
);
m.hide(1).unwrap();
assert_eq!(
m.get(3, active_form).unwrap(),
PropertyValue::Object(Some((0, None)))
);
assert_eq!(
m.get(3, active_control).unwrap(),
PropertyValue::Object(None)
);
}
#[test]
fn screen_folgt_resize() {
let mut m = model();
m.resize(120, 40);
let width = forms::property(ObjectClass::Screen, "WIDTH").unwrap().0;
let height = forms::property(ObjectClass::Screen, "HEIGHT").unwrap().0;
assert_eq!(m.get(3, width).unwrap(), PropertyValue::Integer(120));
assert_eq!(m.get(3, height).unwrap(), PropertyValue::Integer(40));
}
#[test]
fn modalfehler_400_bis_403() {
let mut m = model();
m.show(0, false).unwrap();
assert_eq!(m.show(0, true), Err(RuntimeError(400)));
m.hide(0).unwrap();
m.show(0, true).unwrap();
assert_eq!(m.show(0, false), Err(RuntimeError(401)));
let mut c = forms::FormCatalog::default();
c.add("A", ObjectClass::Form, None, false);
c.add("B", ObjectClass::Form, None, false);
let mut stacked = FormsModel::new(c.objects, 80, 25);
stacked.show(0, true).unwrap();
stacked.show(1, true).unwrap();
assert_eq!(stacked.hide(0), Err(RuntimeError(402)));
let ft = forms::property(ObjectClass::Form, "FORMTYPE").unwrap().0;
let mut mdi = model();
mdi.set(0, ft, PropertyValue::Integer(1)).unwrap();
assert_eq!(mdi.show(0, true), Err(RuntimeError(403)));
}
}

View File

@@ -11,6 +11,7 @@ tb-frontend.workspace = true
thiserror.workspace = true
log.workspace = true
tb-runtime.workspace = true
tb-ui.workspace = true
[[bench]]
name = "compile"

View File

@@ -8,10 +8,12 @@
use std::fmt;
use std::rc::Rc;
use tb_frontend::forms::{FormObject, ObjectClass};
use tb_frontend::hir::HEventProc;
use tb_runtime::value::{TypeInit, UdtLayout};
pub const TBC_MAGIC: &[u8; 4] = b"TBC\0";
pub const TBC_VERSION: u16 = 1;
pub const TBC_VERSION: u16 = 3;
/// Vergleichsoperator (Operand der `Cmp*`-Instruktionen).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -52,7 +54,10 @@ impl fmt::Display for LoadError {
match self {
LoadError::BadMagic => write!(f, "Keine .tbc-Datei (Magic fehlt)"),
LoadError::Version(v) => {
write!(f, "Unbekannte .tbc-Formatversion {v} (unterstützt: {TBC_VERSION})")
write!(
f,
"Unbekannte .tbc-Formatversion {v} (unterstützt: {TBC_VERSION})"
)
}
LoadError::Corrupt(what) => write!(f, "Beschädigte .tbc-Datei ({what})"),
}
@@ -303,6 +308,14 @@ instrs! {
0xB1 RetProc;
0xB2 RetFn;
0xB3 CallBuiltin(a: u16, b: u8);
0xB4 LoadObjectProperty(a: u16, b: u16, c: bool);
0xB5 StoreObjectProperty(a: u16, b: u16, c: bool);
0xB6 PushObject(a: u16, b: bool);
0xB7 TypeOf(a: u8);
0xB8 ObjectMethod(a: u16, b: u16, c: u8);
0xB9 ObjectLoad(a: u16, b: bool, c: bool); // unload?, Index liegt auf Stack?
0xBA LoadDynamicObjectProperty(a: u16);
0xBB StoreDynamicObjectProperty(a: u16);
// 0xE0 — Ereignis-Traps (Sprachreferenz §8); Kennung vom Stack
0xE0 TrapDefine(a: u8, b: u32); // Quellenart, Sprungziel
@@ -375,6 +388,8 @@ pub struct CompiledModule {
pub data: Vec<DataItem>,
/// Sprungtabellen für `ON n GOTO/GOSUB`.
pub jump_tables: Vec<Vec<u32>>,
pub objects: Vec<FormObject>,
pub event_procs: Vec<HEventProc>,
}
fn w_string(out: &mut Vec<u8>, s: &str) {
@@ -458,6 +473,22 @@ impl CompiledModule {
}
sections.push((*b"JMPT", jmpt));
let mut objs = Vec::new();
objs.extend_from_slice(&(self.objects.len() as u32).to_le_bytes());
for o in &self.objects {
w_string(&mut objs, &o.name);
objs.push(o.class.id());
w_string(&mut objs, o.parent_form.as_deref().unwrap_or(""));
objs.push(o.array as u8);
}
objs.extend_from_slice(&(self.event_procs.len() as u32).to_le_bytes());
for e in &self.event_procs {
objs.extend_from_slice(&e.object.to_le_bytes());
w_string(&mut objs, &e.event);
objs.extend_from_slice(&e.proc.to_le_bytes());
}
sections.push((*b"OBJS", objs));
// Header + Abschnittstabelle
let mut out = Vec::new();
out.extend_from_slice(TBC_MAGIC);
@@ -567,7 +598,13 @@ impl CompiledModule {
for _ in 0..n_instr {
code.push(Instr::decode(&mut cr)?);
}
procs.push(ProcCode { name, n_params, locals_init, local_names, code });
procs.push(ProcCode {
name,
n_params,
locals_init,
local_names,
code,
});
}
let mut r = section(b"DATA")?;
@@ -591,6 +628,31 @@ impl CompiledModule {
jump_tables.push(t);
}
let mut r = section(b"OBJS")?;
let n = r.u32()? as usize;
let mut objects = Vec::with_capacity(n);
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;
objects.push(FormObject {
name,
class,
parent_form: (!parent.is_empty()).then_some(parent),
array,
});
}
let n = r.u32()? as usize;
let mut event_procs = Vec::with_capacity(n);
for _ in 0..n {
event_procs.push(HEventProc {
object: r.u16()?,
event: r.string()?,
proc: r.u16()?,
});
}
Ok(CompiledModule {
name,
option_base,
@@ -601,6 +663,8 @@ impl CompiledModule {
procs,
data,
jump_tables,
objects,
event_procs,
})
}
}
@@ -660,8 +724,13 @@ mod tests {
local_names: vec![],
code: vec![Instr::Stmt(1), Instr::PushStr(0), Instr::End],
}],
data: vec![DataItem { text: "1.5".into(), line: 3 }],
data: vec![DataItem {
text: "1.5".into(),
line: 3,
}],
jump_tables: vec![vec![4, 9]],
objects: vec![],
event_procs: vec![],
};
let bytes = m.to_tbc();
let back = CompiledModule::from_tbc(&bytes).unwrap();
@@ -687,6 +756,8 @@ mod tests {
procs: vec![],
data: vec![],
jump_tables: vec![],
objects: vec![],
event_procs: vec![],
};
let mut bytes = m.to_tbc();
bytes[4] = 0xFF; // Version hochsetzen

View File

@@ -43,9 +43,14 @@ pub fn compile(hir: &HirModule) -> CompiledModule {
data: hir
.data
.iter()
.map(|d| DataItem { text: d.text.clone(), line: d.line })
.map(|d| DataItem {
text: d.text.clone(),
line: d.line,
})
.collect(),
jump_tables: cg.jump_tables,
objects: hir.objects.clone(),
event_procs: hir.event_procs.clone(),
}
}
@@ -68,6 +73,7 @@ fn type_init(t: &HTy) -> TypeInit {
HTy::Str => TypeInit::Str,
HTy::FixedStr(n) => TypeInit::FixedStr(*n),
HTy::Udt(id) => TypeInit::Udt(*id),
HTy::Form | HTy::Control => TypeInit::Empty,
}
}
@@ -169,8 +175,8 @@ impl Codegen {
for (idx, label) in std::mem::take(&mut ctx.fixups) {
// Modulweites `ON ERROR GOTO` in einer Prozedur: das Label lebt
// im Modulrumpf, nicht im eigenen.
let modulweit = proc.kind != hir::HProcKind::Main
&& matches!(ctx.code[idx], Instr::OnErrorGoto(_));
let modulweit =
proc.kind != hir::HProcKind::Main && matches!(ctx.code[idx], Instr::OnErrorGoto(_));
let pc = if modulweit {
self.modul_label_pc
.get(label as usize)
@@ -241,6 +247,52 @@ impl Codegen {
HStmtKind::Assign { place, value } => {
self.store_place(ctx, place, |cg, ctx| cg.expr(ctx, value));
}
HStmtKind::SetObjectProperty {
object,
index,
property,
value,
} => {
if let Some(index) = index {
self.expr(ctx, index);
}
self.expr(ctx, value);
ctx.emit(Instr::StoreObjectProperty(
*object,
*property,
index.is_some(),
));
}
HStmtKind::SetDynamicObjectProperty {
object,
property,
value,
} => {
self.expr(ctx, object);
self.expr(ctx, value);
let property = self.pool(property);
ctx.emit(Instr::StoreDynamicObjectProperty(property));
}
HStmtKind::ObjectMethod {
object,
method,
args,
} => {
for arg in args {
self.expr(ctx, arg);
}
ctx.emit(Instr::ObjectMethod(*object, *method, args.len() as u8));
}
HStmtKind::ObjectLoad {
object,
index,
unload,
} => {
if let Some(index) = index {
self.expr(ctx, index);
}
ctx.emit(Instr::ObjectLoad(*object, *unload, index.is_some()));
}
HStmtKind::Print { items, trailing } => {
for item in items {
match item {
@@ -277,12 +329,21 @@ impl Codegen {
}
ctx.emit(Instr::Field(fields.len() as u8));
}
HStmtKind::LsetRset { rset, target, value } => {
HStmtKind::LsetRset {
rset,
target,
value,
} => {
self.make_ref(ctx, target);
self.expr(ctx, value);
ctx.emit(Instr::LsetRset(*rset));
}
HStmtKind::GetPut { put, file, recnum, var } => {
HStmtKind::GetPut {
put,
file,
recnum,
var,
} => {
self.expr(ctx, file);
if let Some(r) = recnum {
self.expr(ctx, r);
@@ -301,12 +362,19 @@ impl Codegen {
HTy::Udt(i) => (7, *i),
// Variable Strings: Länge erst zur Laufzeit.
HTy::Str => (8, 0),
HTy::Form | HTy::Control => (8, 0),
}
}
};
ctx.emit(Instr::GetPut(*put, recnum.is_some(), art, zusatz));
}
HStmtKind::Input { file, line_mode, prompt, question, targets } => {
HStmtKind::Input {
file,
line_mode,
prompt,
question,
targets,
} => {
if let Some(f) = file {
// Dateinummer zuerst, dann die Referenzen darüber.
self.expr(ctx, f);
@@ -506,7 +574,12 @@ impl Codegen {
}
}
HStmtKind::Restore(idx) => ctx.emit(Instr::Restore(*idx)),
HStmtKind::Dim { slot, elem, dims, redim } => {
HStmtKind::Dim {
slot,
elem,
dims,
redim,
} => {
for (lo, hi) in dims {
self.expr(ctx, lo);
self.expr(ctx, hi);
@@ -637,6 +710,36 @@ impl Codegen {
ctx.emit(Instr::PushStr(idx));
}
HExpr::Load(p) => self.load_place(ctx, p),
HExpr::ObjectProperty {
object,
index,
property,
..
} => {
if let Some(index) = index {
self.expr(ctx, index);
}
ctx.emit(Instr::LoadObjectProperty(
*object,
*property,
index.is_some(),
));
}
HExpr::DynamicObjectProperty { object, property } => {
self.expr(ctx, object);
let property = self.pool(property);
ctx.emit(Instr::LoadDynamicObjectProperty(property));
}
HExpr::ObjectRef { object, index, .. } => {
if let Some(index) = index {
self.expr(ctx, index);
}
ctx.emit(Instr::PushObject(*object, index.is_some()));
}
HExpr::TypeOf { value, class } => {
self.expr(ctx, value);
ctx.emit(Instr::TypeOf(class.id()));
}
HExpr::Conv { from, to, arg } => {
self.expr(ctx, arg);
emit_conv(ctx, *from, *to);
@@ -1276,7 +1379,12 @@ mod tests {
let m = compile_src("SUB Inc (x%)\nx% = x% + 1\nEND SUB\nn% = 1\nInc n%\nInc (n%)");
let code = main_code(&m);
assert!(code.contains(&Instr::MakeRefGlobal(0)));
assert!(code.iter().filter(|i| matches!(i, Instr::Call(1, 1))).count() == 2);
assert!(
code.iter()
.filter(|i| matches!(i, Instr::Call(1, 1)))
.count()
== 2
);
// Prozedurrumpf liest/schreibt über Referenz
let sub = &m.procs[1].code;
assert!(sub.contains(&Instr::LoadRef(0)));
@@ -1309,7 +1417,12 @@ mod tests {
let m = compile_src("DATA 1, 2\nREAD a%, b%\nRESTORE\nREAD c%");
let code = main_code(&m);
assert_eq!(m.data.len(), 2);
assert!(code.iter().filter(|i| matches!(i, Instr::ReadData(1))).count() == 3);
assert!(
code.iter()
.filter(|i| matches!(i, Instr::ReadData(1)))
.count()
== 3
);
assert!(code.contains(&Instr::Restore(0)));
assert!(code.contains(&Instr::ConvR8I2));
}
@@ -1318,7 +1431,9 @@ mod tests {
fn fehlerbehandlung_emit() {
let m = compile_src("ON ERROR GOTO H\nERROR 5\nEND\nH:\nRESUME NEXT");
let code = main_code(&m);
assert!(code.iter().any(|i| matches!(i, Instr::OnErrorGoto(t) if *t > 0)));
assert!(code
.iter()
.any(|i| matches!(i, Instr::OnErrorGoto(t) if *t > 0)));
assert!(code.contains(&Instr::RaiseError));
assert!(code.contains(&Instr::ResumeNext));
}

View File

@@ -17,6 +17,7 @@ use tb_runtime::errors::RuntimeError;
use tb_runtime::host::Host;
use tb_runtime::traps::Quelle;
use tb_runtime::value::{self, default_value, ArrayObj, RecordObj, TypeInit, Value, VarRef};
use tb_ui::forms::{FormEvent, FormsModel, PropertyValue, ShowResult};
/// Warum die VM die Kontrolle abgibt.
#[derive(Debug, Clone, PartialEq)]
@@ -24,12 +25,24 @@ pub enum RunEvent {
/// `END`, `SYSTEM` oder Programmende.
Ended,
/// `STOP` — VM-Zustand bleibt fortsetzbar (IDE: CONT).
Stopped { line: u32 },
Breakpoint { line: u32 },
Stepped { line: u32 },
Interrupted { line: u32 },
Stopped {
line: u32,
},
Breakpoint {
line: u32,
},
Stepped {
line: u32,
},
Interrupted {
line: u32,
},
/// Unbehandelter Laufzeitfehler.
Error { code: u16, line: u32, message: String },
Error {
code: u16,
line: u32,
message: String,
},
}
const F_STEP: u32 = 1;
@@ -58,6 +71,15 @@ struct Frame {
/// Satz würde dem Handler leere Modulvariablen zeigen. Sein `RETURN`
/// beendet den Handler (design.md, D2).
trap: Option<Quelle>,
waiting_form: Option<u16>,
pending_show: Option<u16>,
form_event: Option<FormEventReturn>,
}
#[derive(Clone, Copy)]
enum FormEventReturn {
Normal,
Unload(u16),
}
pub struct Vm {
@@ -84,6 +106,7 @@ pub struct Vm {
tick_zaehler: u32,
breakpoints: HashSet<u32>,
data_ptr: usize,
pub forms: FormsModel,
}
/// Quellenarten, wie der Codegenerator sie kodiert.
@@ -126,6 +149,7 @@ impl Vm {
.iter()
.map(|t| default_value(t, &module.udts))
.collect();
let forms = FormsModel::new(module.objects.clone(), 80, 25);
let mut vm = Vm {
globals,
locals: Vec::new(),
@@ -142,6 +166,7 @@ impl Vm {
tick_zaehler: 0,
breakpoints: HashSet::new(),
data_ptr: 0,
forms,
module,
};
// ISAM leitet das Satzlayout aus dem Typ der `OPEN`-Anweisung ab und
@@ -173,9 +198,156 @@ impl Vm {
last_stmt_pc: 0,
line: 0,
trap: None,
waiting_form: None,
pending_show: None,
form_event: None,
});
}
pub fn queue_form_event(&mut self, event: FormEvent) {
self.forms.queue(event);
}
fn form_value(v: PropertyValue) -> Value {
match v {
PropertyValue::Integer(v) => Value::Int(v as i16),
PropertyValue::Single(v) => Value::Sng(v),
PropertyValue::String(v) => Value::Str(Rc::from(v)),
PropertyValue::Boolean(v) => Value::Int(if v { -1 } else { 0 }),
PropertyValue::Object(v) => v
.map(|(object, index)| Value::Obj(object, index))
.unwrap_or(Value::Empty),
PropertyValue::IntegerArray(_) => Value::Empty,
}
}
fn property_value(
&self,
object: u16,
property: u16,
v: Value,
) -> Result<PropertyValue, RuntimeError> {
let class = self
.module
.objects
.get(object as usize)
.ok_or(RuntimeError(420))?
.class;
let spec = tb_frontend::forms::properties(class)
.get(property as usize)
.copied()
.ok_or(RuntimeError(422))?;
Ok(match (spec.ty, v) {
(tb_frontend::forms::PropertyType::Integer, Value::Int(v)) => {
PropertyValue::Integer(v as i32)
}
(tb_frontend::forms::PropertyType::Integer, Value::Lng(v)) => PropertyValue::Integer(v),
(tb_frontend::forms::PropertyType::Boolean, Value::Int(v)) => {
PropertyValue::Boolean(v != 0)
}
(tb_frontend::forms::PropertyType::Boolean, Value::Lng(v)) => {
PropertyValue::Boolean(v != 0)
}
(tb_frontend::forms::PropertyType::Single, Value::Sng(v)) => PropertyValue::Single(v),
(tb_frontend::forms::PropertyType::String, Value::Str(v)) => {
PropertyValue::String(v.to_string())
}
(tb_frontend::forms::PropertyType::Object, Value::Obj(object, index)) => {
PropertyValue::Object(Some((object, index)))
}
_ => return Err(RuntimeError::TYPE_MISMATCH),
})
}
fn dispatch_form_event(&mut self, event: FormEvent, on_return: FormEventReturn) -> bool {
if self
.module
.objects
.get(event.object as usize)
.is_some_and(|o| {
!matches!(
o.class,
tb_frontend::forms::ObjectClass::Form | tb_frontend::forms::ObjectClass::Screen
)
})
{
let _ = self
.forms
.set_active_control(event.object, event.array_index);
}
let Some(binding) = self
.module
.event_procs
.iter()
.find(|e| e.object == event.object && e.event.eq_ignore_ascii_case(&event.name))
.cloned()
else {
return false;
};
if let Some(index) = event.array_index {
self.push(Value::Int(index as i16));
}
for arg in event.args {
self.push(Self::form_value(arg));
}
self.push_frame(
binding.proc as usize,
self.module.procs[binding.proc as usize].n_params as usize,
);
if let Some(frame) = self.frames.last_mut() {
frame.form_event = Some(on_return);
}
true
}
fn dispatch_next_form_event(&mut self) -> bool {
while let Some(event) = self.forms.next_event() {
if self.dispatch_form_event(event, FormEventReturn::Normal) {
return true;
}
}
false
}
fn request_unload(&mut self, object: u16) -> Result<(), RuntimeError> {
let event = FormEvent {
object,
array_index: None,
name: "UNLOAD".into(),
args: vec![PropertyValue::Integer(0)],
};
if !self.dispatch_form_event(event, FormEventReturn::Unload(object)) {
self.forms.unload_with(object, |_| {})?;
}
Ok(())
}
fn forms_zustellen(&mut self) -> bool {
self.forms
.resize(self.rt.screen.cols(), self.rt.screen.rows());
while let Some(m) = self.rt.maus.pop_front() {
if let Some(object) = self.forms.active_form() {
let name = match m.art {
tb_runtime::host::MausArt::Druck => "MOUSEDOWN",
tb_runtime::host::MausArt::Loslassen => "MOUSEUP",
tb_runtime::host::MausArt::Bewegung => "MOUSEMOVE",
};
self.forms.queue(FormEvent {
object,
array_index: None,
name: name.into(),
args: vec![
PropertyValue::Integer(m.taste as i32),
PropertyValue::Integer(m.shift as i32),
PropertyValue::Single(m.spalte as f32),
PropertyValue::Single(m.zeile as f32),
],
});
}
}
self.dispatch_next_form_event()
}
/// Anstehendes Ereignis zustellen, falls eines fällig ist. Liefert
/// `true`, wenn ein Handler aufgesetzt wurde.
fn zustellen(&mut self, host: &mut dyn Host) -> bool {
@@ -243,6 +415,9 @@ impl Vm {
last_stmt_pc: 0,
line: 0,
trap: Some(q),
waiting_form: None,
pending_show: None,
form_event: None,
});
}
@@ -358,6 +533,19 @@ impl Vm {
pub fn run(&mut self, host: &mut dyn Host) -> RunEvent {
loop {
if let Some(form) = self.frames.last().and_then(|f| f.waiting_form) {
if !self.forms.is_visible(form) {
self.frames.last_mut().unwrap().waiting_form = None;
} else {
self.rt.tick(host);
self.forms_zustellen();
if self.rt.ende {
return RunEvent::Ended;
}
std::thread::yield_now();
continue;
}
}
let frame = self.frames.last_mut().expect("kein Frame");
let proc = frame.proc;
let pc = frame.pc;
@@ -642,6 +830,9 @@ impl Vm {
self.rt.tick(host);
self.rt.screen.veraenderung_quittieren();
}
if self.forms_zustellen() {
return Ok(Flow::Normal);
}
// Ereigniszustellung (design.md, D1): nur wenn überhaupt
// ein Trap definiert ist — sonst kostet die Grenze nichts.
if self.rt.traps.aktiv() {
@@ -711,6 +902,7 @@ impl Vm {
// lässt den Wert unberührt.
self.stack.push(Value::Int(0));
self.rt.tick(host);
self.forms_zustellen();
self.zustellen(host);
Ok(Flow::Normal)
}
@@ -757,6 +949,202 @@ impl Vm {
self.push(Value::Str(self.module.strings[i as usize].clone()));
Ok(Flow::Normal)
}
I::LoadObjectProperty(object, property, has_index) => {
let index = if has_index {
Some(self.pop_i32()?)
} else {
None
};
if !self.forms.is_loaded_at(object, index) {
self.forms.ensure_loaded_at(object, index)?;
if index.is_none() && self.dispatch_next_form_event() {
let caller = self.frames.len() - 2;
self.frames[caller].pc = self.frames[caller].pc.saturating_sub(1);
return Ok(Flow::Normal);
}
}
let value = self.forms.get_at(object, index, property)?;
self.push(Self::form_value(value));
Ok(Flow::Normal)
}
I::StoreObjectProperty(object, property, has_index) => {
let value = self.pop()?;
let index = if has_index {
Some(self.pop_i32()?)
} else {
None
};
if !self.forms.is_loaded_at(object, index) {
self.forms.ensure_loaded_at(object, index)?;
if index.is_none() {
self.push(value.clone());
}
if index.is_none() && self.dispatch_next_form_event() {
let caller = self.frames.len() - 2;
self.frames[caller].pc = self.frames[caller].pc.saturating_sub(1);
return Ok(Flow::Normal);
}
if index.is_none() {
let value = self.pop()?;
let value = self.property_value(object, property, value)?;
self.forms.set_at(object, index, property, value)?;
return Ok(Flow::Normal);
}
}
let value = self.property_value(object, property, value)?;
self.forms.set_at(object, index, property, value)?;
Ok(Flow::Normal)
}
I::PushObject(object, has_index) => {
let index = if has_index {
Some(self.pop_i32()?)
} else {
None
};
self.forms.ensure_loaded_at(object, index)?;
self.push(Value::Obj(object, index));
self.dispatch_next_form_event();
Ok(Flow::Normal)
}
I::LoadDynamicObjectProperty(property) => {
let Value::Obj(object, index) = self.pop()? else {
return Err(RuntimeError::TYPE_MISMATCH);
};
let class = self
.module
.objects
.get(object as usize)
.ok_or(RuntimeError(420))?
.class;
let name = self
.module
.strings
.get(property as usize)
.ok_or(RuntimeError(422))?;
let property = tb_frontend::forms::property(class, name)
.map(|(property, _)| property)
.ok_or(RuntimeError(422))?;
let value = self.forms.get_at(object, index, property)?;
self.push(Self::form_value(value));
Ok(Flow::Normal)
}
I::StoreDynamicObjectProperty(property) => {
let value = self.pop()?;
let Value::Obj(object, index) = self.pop()? else {
return Err(RuntimeError::TYPE_MISMATCH);
};
let class = self
.module
.objects
.get(object as usize)
.ok_or(RuntimeError(420))?
.class;
let name = self
.module
.strings
.get(property as usize)
.ok_or(RuntimeError(422))?;
let property = tb_frontend::forms::property(class, name)
.map(|(property, _)| property)
.ok_or(RuntimeError(422))?;
let value = self.property_value(object, property, value)?;
self.forms.set_at(object, index, property, value)?;
Ok(Flow::Normal)
}
I::TypeOf(class) => {
let matches = match self.pop()? {
Value::Obj(object, _) => self
.module
.objects
.get(object as usize)
.is_some_and(|o| o.class.id() == class),
_ => false,
};
self.push(Value::Int(if matches { -1 } else { 0 }));
Ok(Flow::Normal)
}
I::ObjectMethod(object, method, argc) => {
let class = self
.module
.objects
.get(object as usize)
.ok_or(RuntimeError(420))?
.class;
let name = *tb_frontend::forms::methods(class)
.get(method as usize)
.ok_or(RuntimeError(421))?;
let resumed_show = self.frames.last().unwrap().pending_show == Some(object);
if resumed_show {
self.frames.last_mut().unwrap().pending_show = None;
} else if class == tb_frontend::forms::ObjectClass::Form
&& name == "SHOW"
&& !self.forms.is_loaded(object)
{
self.forms.ensure_loaded(object)?;
if self.dispatch_next_form_event() {
let caller = self.frames.len() - 2;
self.frames[caller].pending_show = Some(object);
self.frames[caller].pc = self.frames[caller].pc.saturating_sub(1);
return Ok(Flow::Normal);
}
}
let mut args = Vec::with_capacity(argc as usize);
for _ in 0..argc {
args.push(self.pop()?);
}
args.reverse();
if resumed_show && !self.forms.is_loaded(object) {
return Ok(Flow::Normal);
}
match (class, name) {
(tb_frontend::forms::ObjectClass::Form, "SHOW") => {
let style = match args.first() {
None => 0,
Some(Value::Int(v)) => *v as i32,
Some(Value::Lng(v)) => *v,
_ => return Err(RuntimeError::TYPE_MISMATCH),
};
if !matches!(style, 0 | 1) {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
}
if self.forms.show(object, style == 1)? == ShowResult::ModalWait {
self.frames.last_mut().unwrap().waiting_form = Some(object);
}
}
(tb_frontend::forms::ObjectClass::Form, "HIDE") => self.forms.hide(object)?,
(tb_frontend::forms::ObjectClass::Form, "LOAD") => {
self.forms.ensure_loaded(object)?
}
(tb_frontend::forms::ObjectClass::Form, "UNLOAD") => {
self.request_unload(object)?
}
(tb_frontend::forms::ObjectClass::Screen, "SHOW") => {
self.forms.screen_show(true)
}
(tb_frontend::forms::ObjectClass::Screen, "HIDE") => {
self.forms.screen_show(false)
}
_ => return Err(RuntimeError::FEATURE_UNAVAILABLE),
}
self.dispatch_next_form_event();
Ok(Flow::Normal)
}
I::ObjectLoad(object, unload, has_index) => {
if has_index {
let index = self.pop_i32()?;
if unload {
self.forms.unload_array(object, index)?;
} else {
self.forms.load_array(object, index)?;
}
} else if unload {
self.request_unload(object)?;
} else {
self.forms.ensure_loaded(object)?;
self.dispatch_next_form_event();
}
Ok(Flow::Normal)
}
I::Dup => {
let v = self.stack.last().cloned().ok_or(RuntimeError(51))?;
self.push(v);
@@ -906,8 +1294,7 @@ impl Vm {
let dst = self.pop_rec()?;
let src = self.pop_rec()?;
if !Rc::ptr_eq(&dst, &src) {
let copied: Vec<Value> =
src.borrow().fields.iter().map(deep_copy).collect();
let copied: Vec<Value> = src.borrow().fields.iter().map(deep_copy).collect();
dst.borrow_mut().fields = copied;
}
Ok(Flow::Normal)
@@ -1334,7 +1721,25 @@ impl Vm {
Ok(Flow::Normal)
}
I::RetProc => {
let event = self.frames.last().and_then(|f| f.form_event);
let cancel = self
.frames
.last()
.and_then(|f| {
let base = f.locals_base;
match self.locals.get(base) {
Some(Value::Int(v)) => Some(*v),
_ => None,
}
})
.unwrap_or(0);
self.pop_frame();
if let Some(FormEventReturn::Unload(object)) = event {
self.forms.unload_with(object, |c| *c = cancel)?;
}
if event.is_some() {
self.dispatch_next_form_event();
}
Ok(Flow::Normal)
}
I::RetFn => {
@@ -1616,13 +2021,7 @@ impl Vm {
match treffer {
Some((nummer, start, laenge)) => {
let datei = self.rt.dateien.get(nummer)?;
tb_runtime::fileio::feld_setzen(
&mut datei.puffer,
start,
laenge,
text,
rset,
);
tb_runtime::fileio::feld_setzen(&mut datei.puffer, start, laenge, text, rset);
let neu = tb_runtime::fileio::feld_lesen(&datei.puffer, start, laenge);
self.write_ref(&ziel, Value::Str(Rc::from(neu.as_str())))?;
}
@@ -1918,7 +2317,10 @@ fn input_value(target: &Value, text: &str) -> Option<Value> {
let t = text.trim();
match target {
Value::Str(_) => {
let s = t.strip_prefix('"').and_then(|s| s.strip_suffix('"')).unwrap_or(t);
let s = t
.strip_prefix('"')
.and_then(|s| s.strip_suffix('"'))
.unwrap_or(t);
Some(Value::Str(Rc::from(s)))
}
Value::Int(_) => {
@@ -1946,4 +2348,3 @@ fn strict_number(t: &str) -> Option<f64> {
let cleaned = t.replace(['d', 'D'], "E").replace('e', "E");
cleaned.parse::<f64>().ok()
}

View File

@@ -27,3 +27,17 @@ pub fn compile_source(
let hir = analysis.hir.expect("diagnose-frei, aber kein HIR");
Ok(codegen::compile(&hir))
}
pub fn compile_source_with_forms(
module_name: &str,
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"),
))
}

View File

@@ -99,7 +99,10 @@ fn gemischte_arithmetik() {
#[test]
fn logik_bitweise() {
assert_eq!(out("PRINT 6 AND 3; 6 OR 3; 6 XOR 3; NOT 0"), " 2 7 5 -1 \n");
assert_eq!(
out("PRINT 6 AND 3; 6 OR 3; 6 XOR 3; NOT 0"),
" 2 7 5 -1 \n"
);
// Operanden werden gerundet: 1.5 AND 1 → 2 AND 1 = 0
assert_eq!(out("PRINT 1.5 AND 1"), " 0 \n");
}
@@ -122,12 +125,18 @@ fn stringvergleich_und_verkettung() {
#[test]
fn for_ohne_durchlauf() {
// Spec-Szenario: FOR i% = 3 TO 1 → Körper wird nicht betreten
assert_eq!(out("FOR i% = 3 TO 1\nPRINT i%\nNEXT\nPRINT \"ende\""), "ende\n");
assert_eq!(
out("FOR i% = 3 TO 1\nPRINT i%\nNEXT\nPRINT \"ende\""),
"ende\n"
);
}
#[test]
fn for_mit_negativem_step() {
assert_eq!(out("FOR i% = 3 TO 1 STEP -1\nPRINT i%;\nNEXT\nPRINT"), " 3 2 1 \n");
assert_eq!(
out("FOR i% = 3 TO 1 STEP -1\nPRINT i%;\nNEXT\nPRINT"),
" 3 2 1 \n"
);
}
#[test]
@@ -172,11 +181,13 @@ fn exit_for_und_do() {
fn kontrollfluss_korpusdatei() {
// 5.2-Verifikation: kontrollfluss.bas byte-genau korrekt.
let src = std::fs::read_to_string(
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/compat/kontrollfluss.bas"),
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../tests/compat/kontrollfluss.bas"),
)
.unwrap();
let want = std::fs::read_to_string(
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/compat/kontrollfluss.out"),
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../tests/compat/kontrollfluss.out"),
)
.unwrap();
assert_eq!(out(&src), want);
@@ -268,7 +279,8 @@ fn data_read_restore() {
// Anmerkung: unquotierte DATA-Texte verlieren derzeit die
// Groß-/Kleinschreibung (Lexer normalisiert Bezeichner);
// Rohtext-Erhalt ist als Aufgabe in PLAN.md Phase 3 eingeplant.
let src = "DATA 1, 2.5, \"hallo\"\nREAD a%, b!, c$\nPRINT a%; b!; c$\nRESTORE\nREAD x%\nPRINT x%";
let src =
"DATA 1, 2.5, \"hallo\"\nREAD a%, b!, c$\nPRINT a%; b!; c$\nRESTORE\nREAD x%\nPRINT x%";
assert_eq!(out(src), " 1 2.5 hallo\n 1 \n");
}
@@ -444,10 +456,7 @@ fn end_und_system() {
#[test]
fn input_mit_redo() {
let (ev, output) = run_with_input(
"INPUT \"Zahl\"; n%\nPRINT n% * 2",
&["abc", "21"],
);
let (ev, output) = run_with_input("INPUT \"Zahl\"; n%\nPRINT n% * 2", &["abc", "21"]);
assert_eq!(ev, RunEvent::Ended);
assert!(output.contains("Redo from start"));
assert!(output.ends_with(" 42 \n"), "{output}");
@@ -464,7 +473,10 @@ fn line_input_liest_ganze_zeile() {
#[test]
fn mid_anweisung_mutiert() {
assert_eq!(out("s$ = \"hallo\"\nMID$(s$, 2, 2) = \"EY\"\nPRINT s$"), "hEYlo\n");
assert_eq!(
out("s$ = \"hallo\"\nMID$(s$, 2, 2) = \"EY\"\nPRINT s$"),
"hEYlo\n"
);
}
#[test]
@@ -495,12 +507,10 @@ fn unsupported_feature_fehler_73() {
#[test]
fn open_for_isam_endet_nicht_mehr_mit_fehler_73() {
let _dir = TempVerzeichnis::neu("open_isam");
let (ev, ausgabe) = run(
"TYPE T\n f AS INTEGER\nEND TYPE\n\
let (ev, ausgabe) = run("TYPE T\n f AS INTEGER\nEND TYPE\n\
OPEN \"db.isam\" FOR ISAM T \"Tab\" AS #1\n\
PRINT \"offen\"\n\
CLOSE #1",
);
CLOSE #1");
assert_eq!(ev, RunEvent::Ended, "unerwartetes Ende: {ev:?}\n{ausgabe}");
assert!(ausgabe.contains("offen"), "{ausgabe}");
}
@@ -552,7 +562,10 @@ fn groessenaenderung_wirkt_zur_laufzeit() {
let module = tb_vm::compile_source("TEST", "PRINT \"a\"\nFOR i% = 1 TO 50\nNEXT i%\n").unwrap();
let mut vm = Vm::new(module);
let mut host = CaptureHost::default();
host.ereignis(Ereignis::Groesse { cols: 120, rows: 40 });
host.ereignis(Ereignis::Groesse {
cols: 120,
rows: 40,
});
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert_eq!((vm.rt.screen.cols(), vm.rt.screen.rows()), (120, 40));
// Inhalt bleibt oben links erhalten.
@@ -639,10 +652,7 @@ fn cls_setzt_cursor_zurueck() {
#[test]
fn width_hebt_die_groesse_an() {
assert_eq!(
out("WIDTH 120, 40\nPRINT SCREEN(40, 120)"),
" 32 \n"
);
assert_eq!(out("WIDTH 120, 40\nPRINT SCREEN(40, 120)"), " 32 \n");
}
// ---- ON ERROR-Scoping (Spec sprach-frontend) -------------------------------
@@ -736,8 +746,14 @@ fn time_und_now_stimmen_ueberein() {
#[test]
fn fester_versatz_verschiebt_die_uhr() {
let src = "PRINT INT(TIMER)";
let ohne: f64 = out_mit_zone(src, Zeitzone::Unbekannt).trim().parse().unwrap();
let mit: f64 = out_mit_zone(src, Zeitzone::Fest(3600)).trim().parse().unwrap();
let ohne: f64 = out_mit_zone(src, Zeitzone::Unbekannt)
.trim()
.parse()
.unwrap();
let mit: f64 = out_mit_zone(src, Zeitzone::Fest(3600))
.trim()
.parse()
.unwrap();
// Modulo Tageslänge, damit ein Mitternachtsübergang nichts kaputt macht.
let diff = (mit - ohne).rem_euclid(86_400.0);
assert!((diff - 3600.0).abs() <= 2.0, "Differenz {diff}");
@@ -746,7 +762,10 @@ fn fester_versatz_verschiebt_die_uhr() {
/// `TIMER` zählt ab der lokalen Mitternacht, nicht ab der UTC-Mitternacht.
#[test]
fn timer_zaehlt_ab_lokaler_mitternacht() {
let t = out_mit_zone("PRINT INT(TIMER / 3600); VAL(LEFT$(TIME$, 2))", Zeitzone::Fest(-18000));
let t = out_mit_zone(
"PRINT INT(TIMER / 3600); VAL(LEFT$(TIME$, 2))",
Zeitzone::Fest(-18000),
);
let zahlen: Vec<f64> = t
.split_whitespace()
.filter_map(|w| w.parse().ok())
@@ -1093,3 +1112,169 @@ fn erl_nennt_die_fehlerzeile_nicht_die_handlerzeile() {
110 RESUME NEXT\n");
assert_eq!(out.trim(), "20");
}
fn forms_catalog() -> tb_frontend::forms::FormCatalog {
use tb_frontend::forms::ObjectClass;
let mut c = tb_frontend::forms::FormCatalog::default();
c.add("Form1", ObjectClass::Form, None, false);
c.add("Text1", ObjectClass::TextBox, Some("Form1"), false);
c.add("Command1", ObjectClass::CommandButton, Some("Form1"), true);
c
}
fn form_vm(src: &str) -> Vm {
let module = tb_vm::compile_source_with_forms("FORM1", src, &forms_catalog())
.unwrap_or_else(|d| panic!("Compile-Fehler: {d:?}"));
Vm::new(module)
}
#[test]
fn objektzugriff_laeuft_ueber_objekt_und_eigenschaftsindex() {
let mut vm = form_vm("Form1!Text1.Text = \"hallo\"\nPRINT Form1!Text1.Text");
let ev = vm.run(&mut CaptureHost::default());
assert_eq!(ev, RunEvent::Ended);
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen).trim(), "hallo");
}
#[test]
fn steuerarray_eigenschaften_und_objektparameter_funktionieren() {
let mut vm = form_vm(
"LOAD Command1(3)\nCommand1(3).Caption = \"drei\"\nPRINT Command1(3).Caption\n\
CALL Aus(Text1)\nPRINT Text1.Visible\nCALL FormAus(Form1)\nPRINT Form1.Caption\nEND\n\
SUB Aus(c AS CONTROL)\nPRINT TYPEOF c IS TextBox\nc.Visible = 0\nEND SUB\n\
SUB FormAus(f AS FORM)\nPRINT TYPEOF f IS Form\nf.Caption = \"Formular\"\nEND SUB",
);
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
assert_eq!(
tb_runtime::snapshot::text(&vm.rt.screen).trim(),
"drei\n-1 \n 0 \n-1 \nFormular"
);
}
#[test]
fn modales_show_setzt_nach_unload_fort() {
let mut vm = form_vm(
"DIM SHARED gesehen%\nForm1.Show 1\nPRINT gesehen%\nEND\n\
SUB Form_MouseDown(Button AS INTEGER, Shift AS INTEGER, X AS SINGLE, Y AS SINGLE)\n\
SHARED gesehen%\ngesehen% = 1\nUNLOAD Form1\nEND SUB",
);
let mut host = CaptureHost::default();
host.ereignis_nach(
2,
tb_runtime::host::Ereignis::Maus(tb_runtime::host::MausEreignis {
art: tb_runtime::host::MausArt::Druck,
taste: 1,
shift: 0,
zeile: 1,
spalte: 1,
}),
);
let ev = vm.run(&mut host);
assert_eq!(ev, RunEvent::Ended);
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen).trim(), "1");
assert!(host.presents >= 2);
}
#[test]
fn fehlende_ereignisprozedur_verfaellt_und_typeof_prueft_klasse() {
let mut vm = form_vm("Form1.Show\nIF TYPEOF Text1 IS TextBox THEN PRINT \"ja\"");
vm.queue_form_event(tb_ui::forms::FormEvent {
object: 0,
array_index: None,
name: "CLICK".into(),
args: vec![],
});
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen).trim(), "ja");
}
#[test]
fn mausargumente_kommen_in_zellen_an() {
use tb_runtime::host::{Ereignis, MausArt, MausEreignis};
let mut vm = form_vm(
"DIM SHARED b%, s%, gx!, gy!\nForm1.Show\nSTOP\nDOEVENTS\nEND\n\
SUB Form_MouseDown(Button AS INTEGER, Shift AS INTEGER, X AS SINGLE, Y AS SINGLE)\n\
SHARED b%, s%, gx!, gy!\nb% = Button: s% = Shift: gx! = X: gy! = Y\nEND SUB",
);
let mut host = CaptureHost::default();
assert!(matches!(vm.run(&mut host), RunEvent::Stopped { .. }));
host.ereignis(Ereignis::Maus(MausEreignis {
art: MausArt::Druck,
taste: 1,
shift: 2,
zeile: 5,
spalte: 7,
}));
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert!(matches!(vm.inspect("b"), Some(Value::Int(1))));
assert!(matches!(vm.inspect("s"), Some(Value::Int(2))));
assert!(matches!(vm.inspect("gx"), Some(Value::Sng(7.0))));
assert!(matches!(vm.inspect("gy"), Some(Value::Sng(5.0))));
}
#[test]
fn form_load_laeuft_vor_dem_sichtbarsetzen() {
let mut vm = form_vm(
"Form1.Show\nEND\n\
SUB Form_Load()\nPRINT Form1.Visible\nEND SUB",
);
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen).trim(), "0");
}
#[test]
fn steuerarray_ereignis_stellt_index_voran() {
let mut vm = form_vm(
"DIM SHARED seen%\nSTOP\nDOEVENTS\nEND\n\
SUB Command1_Click(Index AS INTEGER)\nSHARED seen%\nseen% = Index\nEND SUB",
);
let mut host = CaptureHost::default();
assert!(matches!(vm.run(&mut host), RunEvent::Stopped { .. }));
vm.forms.load_array(2, 3).unwrap();
vm.queue_form_event(tb_ui::forms::FormEvent {
object: 2,
array_index: Some(3),
name: "CLICK".into(),
args: vec![],
});
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert!(matches!(vm.inspect("seen"), Some(Value::Int(3))));
let active =
tb_frontend::forms::property(tb_frontend::forms::ObjectClass::Screen, "ACTIVECONTROL")
.unwrap()
.0;
assert_eq!(
vm.forms.get(3, active).unwrap(),
tb_ui::forms::PropertyValue::Object(Some((2, Some(3))))
);
}
#[test]
fn form_load_laeuft_einmal_und_unload_cancel_verhindert_entladen() {
let mut vm = form_vm(
"DIM SHARED loads%\nx$ = Form1.Caption\nForm1.Hide\nx$ = Form1.Caption\n\
Form1.Show\nUNLOAD Form1\nEND\n\
SUB Form_Load()\nSHARED loads%\nloads% = loads% + 1\nEND SUB\n\
SUB Form_Unload(Cancel AS INTEGER)\nCancel = 1\nEND SUB",
);
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
assert!(matches!(vm.inspect("loads"), Some(Value::Int(1))));
assert!(vm.forms.objects[0].loaded);
assert!(vm.forms.objects[0].visible);
}
#[test]
fn objektcode_erzeugt_keine_zusaetzlichen_zustellopcodes() {
let module = tb_vm::compile_source_with_forms(
"FORM1",
"Form1.Caption = \"x\"\nDOEVENTS\nx$ = Form1.Caption",
&forms_catalog(),
)
.unwrap();
let n = module.procs[0]
.code
.iter()
.filter(|i| matches!(i, tb_vm::bytecode::Instr::Doevents))
.count();
assert_eq!(n, 1, "nur das explizite DOEVENTS ist ein Zustellopcode");
}