Phase 4: Formularmodell und Objektsprache
This commit is contained in:
@@ -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)]
|
||||
|
||||
631
crates/tb-frontend/src/forms.rs
Normal file
631
crates/tb-frontend/src/forms.rs
Normal 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))
|
||||
}
|
||||
}
|
||||
@@ -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)?
|
||||
|
||||
@@ -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
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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]
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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"),
|
||||
))
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user