diff --git a/Cargo.lock b/Cargo.lock index 5ce653a..999a414 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -637,6 +637,7 @@ dependencies = [ "log", "ratatui", "signal-hook", + "tb-frontend", "tb-runtime", "thiserror", ] @@ -648,6 +649,7 @@ dependencies = [ "log", "tb-frontend", "tb-runtime", + "tb-ui", "thiserror", ] diff --git a/PLAN.md b/PLAN.md index daf8974..a78b900 100644 --- a/PLAN.md +++ b/PLAN.md @@ -399,7 +399,7 @@ Maßgeblich ist seit 2026-09-03 das Inventar, nicht diese Liste. Ereignisprozedur) Befund: Die VM stellt klassische Traps über einen eigenen Frame zu; Forms-Ereignisprozeduren verwenden später denselben Rückrufweg. -- [ ] Formular-Modell: Eigenschaften, Lade-/Entladezyklus, `SHOW`/`HIDE` +- [x] Formular-Modell: Eigenschaften, Lade-/Entladezyklus, `SHOW`/`HIDE` (modal/nicht-modal) - [ ] Steuerelemente: CommandButton, TextBox, ListBox, ComboBox, CheckBox, OptionButton, Frame, Label, HScrollBar/VScrollBar, PictureBox (Text), @@ -416,7 +416,7 @@ Maßgeblich ist seit 2026-09-03 das Inventar, nicht diese Liste. - [ ] Meilenstein (erster Kompatibilitätstest): die Programme aus https://github.com/cout/vbdos ohne Non-Features kompilieren und sind nutzbar (Konsolenprogramme bereits ab Phase 3) -- [ ] Ereignisdispatch: Event-Queue ↔ VM (Ereignisprozeduren `Name_Ereignis`) +- [x] Ereignisdispatch: Event-Queue ↔ VM (Ereignisprozeduren `Name_Ereignis`) - [x] **Klassische Ereignis-Traps** (ohne Forms, Sprachreferenz §8) — `ON TIMER(n)`, `ON KEY(n)`, `ON UEVENT` und `ON SIGNAL(n)` je mit `GOSUB label`, dazu die Steueranweisungen `TIMER`, `KEY(n)`, diff --git a/crates/tb-frontend/src/ast.rs b/crates/tb-frontend/src/ast.rs index 19a8d52..8bf421c 100644 --- a/crates/tb-frontend/src/ast.rs +++ b/crates/tb-frontend/src/ast.rs @@ -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>, pos: SourcePos, }, - Unary { op: UnOp, operand: Box, pos: SourcePos }, - Binary { op: BinOp, lhs: Box, rhs: Box, pos: SourcePos }, + Unary { + op: UnOp, + operand: Box, + pos: SourcePos, + }, + Binary { + op: BinOp, + lhs: Box, + rhs: Box, + pos: SourcePos, + }, /// Geklammerter Ausdruck. Semantisch transparent, aber als Argument /// erzwingt die Klammer Wertübergabe (BYVAL) statt BYREF. Paren(Box), + /// `TYPEOF ausdruck IS klasse`. + TypeOf { + value: Box, + 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>, pos: SourcePos, }, - Select { expr: Expr, arms: Vec, pos: SourcePos }, + Select { + expr: Expr, + arms: Vec, + 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, pos: SourcePos, }, - While { cond: Expr, body: Vec, pos: SourcePos }, - Goto { target: LabelRef, pos: SourcePos }, - Gosub { target: LabelRef, pos: SourcePos }, - OnGoto { expr: Expr, targets: Vec, gosub: bool, pos: SourcePos }, - Return { target: Option, pos: SourcePos }, + While { + cond: Expr, + body: Vec, + pos: SourcePos, + }, + Goto { + target: LabelRef, + pos: SourcePos, + }, + Gosub { + target: LabelRef, + pos: SourcePos, + }, + OnGoto { + expr: Expr, + targets: Vec, + gosub: bool, + pos: SourcePos, + }, + Return { + target: Option, + pos: SourcePos, + }, End(SourcePos), StopStmt(SourcePos), System(SourcePos), - Exit { kind: ExitKind, pos: SourcePos }, - Dim { shared: bool, redim: bool, decls: Vec, pos: SourcePos }, + Exit { + kind: ExitKind, + pos: SourcePos, + }, + Dim { + shared: bool, + redim: bool, + decls: Vec, + pos: SourcePos, + }, /// `SHARED`-Anweisung in einer Prozedur (Zugriff auf Modulvariablen). - SharedDecl { decls: Vec, pos: SourcePos }, + SharedDecl { + decls: Vec, + pos: SourcePos, + }, /// `STATIC`-Anweisung in einer Prozedur. - StaticDecl { decls: Vec, pos: SourcePos }, + StaticDecl { + decls: Vec, + pos: SourcePos, + }, /// `COMMON [SHARED] [/block/] liste`. CommonDecl { shared: bool, @@ -262,20 +335,64 @@ pub enum Stmt { decls: Vec, pos: SourcePos, }, - Erase { names: Vec, pos: SourcePos }, - ConstDecl { items: Vec<(String, Option, 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, + pos: SourcePos, + }, + ConstDecl { + items: Vec<(String, Option, 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, args: Vec, pos: SourcePos }, - OnError { local: bool, action: OnErrorAction, pos: SourcePos }, - Resume { kind: ResumeKind, pos: SourcePos }, - ErrorStmt { code: Expr, pos: SourcePos }, - Data { items: Vec, pos: SourcePos }, - ReadStmt { vars: Vec, pos: SourcePos }, - Restore { target: Option, pos: SourcePos }, + Call { + name: String, + suffix: Option, + args: Vec, + pos: SourcePos, + }, + OnError { + local: bool, + action: OnErrorAction, + pos: SourcePos, + }, + Resume { + kind: ResumeKind, + pos: SourcePos, + }, + ErrorStmt { + code: Expr, + pos: SourcePos, + }, + Data { + items: Vec, + pos: SourcePos, + }, + ReadStmt { + vars: Vec, + pos: SourcePos, + }, + Restore { + target: Option, + pos: SourcePos, + }, /// Einzeilige `DEF FNname(...) = ausdruck`-Definition. DefFn { name: String, @@ -312,8 +429,15 @@ pub enum Stmt { len: Option, pos: SourcePos, }, - CloseStmt { files: Vec, pos: SourcePos }, - FieldStmt { file: Expr, fields: Vec<(Expr, Expr)>, pos: SourcePos }, + CloseStmt { + files: Vec, + 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, pos: SourcePos, }, - LsetRset { rset: bool, target: Expr, value: Expr, pos: SourcePos }, - WriteStmt { file: Option, items: Vec, pos: SourcePos }, - SeekStmt { file: Expr, position: Expr, pos: SourcePos }, + LsetRset { + rset: bool, + target: Expr, + value: Expr, + pos: SourcePos, + }, + WriteStmt { + file: Option, + items: Vec, + 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)] diff --git a/crates/tb-frontend/src/forms.rs b/crates/tb-frontend/src/forms.rs new file mode 100644 index 0000000..3a459af --- /dev/null +++ b/crates/tb-frontend/src/forms.rs @@ -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::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::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, + pub max: Option, + 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 { + 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, + pub array: bool, +} + +#[derive(Debug, Clone, Default)] +pub struct FormCatalog { + pub objects: Vec, +} + +impl FormCatalog { + pub fn add( + &mut self, + name: impl Into, + 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)) + } +} diff --git a/crates/tb-frontend/src/hir.rs b/crates/tb-frontend/src/hir.rs index 68159b0..d5fa5b3 100644 --- a/crates/tb-frontend/src/hir.rs +++ b/crates/tb-frontend/src/hir.rs @@ -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, pub data: Vec, pub option_base: u8, + /// Zur Übersetzungszeit bekannte Forms-Objekte in Indexreihenfolge. + pub objects: Vec, + /// Ereignisprozeduren: Objektindex, Ereignisname, Prozedurindex. + pub event_procs: Vec, +} + +#[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), + ObjectProperty { + object: u16, + index: Option>, + property: u16, + ty: HTy, + }, + DynamicObjectProperty { + object: Box, + property: String, + }, + ObjectRef { + object: u16, + index: Option>, + class: crate::forms::ObjectClass, + }, + TypeOf { + value: Box, + 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, + property: u16, + value: HExpr, + }, + SetDynamicObjectProperty { + object: HExpr, + property: String, + value: HExpr, + }, + ObjectMethod { + object: u16, + method: u16, + args: Vec, + }, + ObjectLoad { + object: u16, + index: Option, + unload: bool, + }, Print { items: Vec, /// Endet die Anweisung mit `;`/`,` (kein Zeilenumbruch)? diff --git a/crates/tb-frontend/src/lexer.rs b/crates/tb-frontend/src/lexer.rs index 6f8bc89..20ea751 100644 --- a/crates/tb-frontend/src/lexer.rs +++ b/crates/tb-frontend/src/lexer.rs @@ -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 { 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 { #[derive(Debug, Clone, PartialEq)] pub enum TokenKind { /// Bezeichner; `name` ist bereits in Großschreibung normalisiert. - Ident { name: String, suffix: Option }, + Ident { + name: String, + suffix: Option, + }, 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 { 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 = 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::() - .to_uppercase(); - let suffix = if i < chars.len() { + let mut name: String = + chars[start..i].iter().collect::().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 + } + ); + } } diff --git a/crates/tb-frontend/src/lib.rs b/crates/tb-frontend/src/lib.rs index 6aa6726..8157640 100644 --- a/crates/tb-frontend/src/lib.rs +++ b/crates/tb-frontend/src/lib.rs @@ -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, + } } diff --git a/crates/tb-frontend/src/parser.rs b/crates/tb-frontend/src/parser.rs index dedc8d1..752c57c 100644 --- a/crates/tb-frontend/src/parser.rs +++ b/crates/tb-frontend/src/parser.rs @@ -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) { 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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { 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 { @@ -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 { 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 = 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 { 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") + ); + } } diff --git a/crates/tb-frontend/src/sema.rs b/crates/tb-frontend/src/sema.rs index e5c91ee..770d745 100644 --- a/crates/tb-frontend/src/sema.rs +++ b/crates/tb-frontend/src/sema.rs @@ -10,7 +10,11 @@ //! bei Fehlern werden betroffene Konstrukte bestmöglich übersprungen. use crate::ast::*; -use crate::hir::{self, Builtin, HArg, HExpr, HPlace, HPrintItem, HStmt, HStmtKind, HTy, IntKind, LabelId, NumTy, VarSlot}; +use crate::forms::{self, FormCatalog, ObjectClass, PropertyType}; +use crate::hir::{ + self, Builtin, HArg, HExpr, HPlace, HPrintItem, HStmt, HStmtKind, HTy, IntKind, LabelId, NumTy, + VarSlot, +}; use crate::lexer::Suffix; use crate::{Diagnostic, SourcePos}; use std::collections::HashMap; @@ -25,11 +29,16 @@ pub enum Ty { Str, FixedStr(u32), Udt(String), + Form, + Control, Unknown, } fn is_num(t: &Ty) -> bool { - matches!(t, Ty::Int | Ty::Lng | Ty::Cur | Ty::Sng | Ty::Dbl | Ty::Unknown) + matches!( + t, + Ty::Int | Ty::Lng | Ty::Cur | Ty::Sng | Ty::Dbl | Ty::Unknown + ) } fn is_str(t: &Ty) -> bool { matches!(t, Ty::Str | Ty::FixedStr(_) | Ty::Unknown) @@ -101,6 +110,8 @@ fn type_name_ty(t: &TypeName) -> Ty { TypeName::Currency => Ty::Cur, TypeName::Str => Ty::Str, TypeName::FixedStr(n) => Ty::FixedStr((*n).max(0) as u32), + TypeName::Form => Ty::Form, + TypeName::Control => Ty::Control, TypeName::Udt(n) => Ty::Udt(n.clone()), } } @@ -310,8 +321,13 @@ fn banned_feature(name: &str) -> bool { /// der Syntax erkannt: `GET`/`PUT` mit `(` (Grafik) und `LINE` ohne `INPUT` /// im Parser, `OPEN` mit einem `COMn:`-Gerätenamen hier in der Semantik. /// Der Inventar-Abgleich liest diese Liste. -pub const SYNTAKTISCH_ABGEWIESEN: &[&str] = - &["GET (Grafik)", "PUT (Grafik)", "LINE", "OPEN COM", "DEF SEG"]; +pub const SYNTAKTISCH_ABGEWIESEN: &[&str] = &[ + "GET (Grafik)", + "PUT (Grafik)", + "LINE", + "OPEN COM", + "DEF SEG", +]; /// Ist `s` ein serieller Gerätename (`COM1:` …)? Nur für Stringliterale. fn ist_com_geraet(s: &str) -> bool { @@ -382,6 +398,25 @@ pub fn check(module: &Module) -> Vec { /// Analyse + Lowering: liefert das HIR (vollständig nur bei leeren /// Diagnosen) und alle Diagnosen. pub fn lower(module: &Module) -> (Option, Vec) { + lower_with_forms(module, &FormCatalog::default()) +} + +pub fn lower_with_forms( + module: &Module, + catalog: &FormCatalog, +) -> (Option, Vec) { + let mut catalog = catalog.clone(); + if catalog.find("SCREEN").is_none() { + catalog.add("SCREEN", ObjectClass::Screen, None, false); + } + if module + .body + .iter() + .any(|s| matches!(s, Stmt::MetaForm { .. })) + && catalog.find(&module.name).is_none() + { + catalog.add(&module.name, ObjectClass::Form, None, false); + } let mut s = Sema { diags: Vec::new(), deftypes: [const { None }; 26], @@ -401,6 +436,8 @@ pub fn lower(module: &Module) -> (Option, Vec) { data_marks_line: HashMap::new(), hir_procs: Vec::new(), isam_typ: HashMap::new(), + forms: catalog, + event_procs: Vec::new(), }; let hir = s.run(module); (hir, s.diags) @@ -438,11 +475,16 @@ struct Sema { data_marks_line: HashMap, /// Fertige HIR-Prozeduren nach Id (0 = Hauptprogramm). hir_procs: Vec>, + forms: FormCatalog, + event_procs: Vec, } impl Sema { fn err(&mut self, pos: SourcePos, msg: impl Into) { - self.diags.push(Diagnostic { pos, message: msg.into() }); + self.diags.push(Diagnostic { + pos, + message: msg.into(), + }); } fn run(&mut self, module: &Module) -> Option { @@ -463,8 +505,12 @@ impl Sema { if self.udt_ids.contains_key(name) { self.err(*pos, "Duplicate definition"); } else { - self.udt_ids.insert(name.clone(), self.udt_defs.len() as u16); - self.udt_defs.push(hir::HUdt { name: name.clone(), fields: hfields }); + self.udt_ids + .insert(name.clone(), self.udt_defs.len() as u16); + self.udt_defs.push(hir::HUdt { + name: name.clone(), + fields: hfields, + }); } } _ => {} @@ -473,12 +519,19 @@ impl Sema { for proc in &module.procs { self.register_proc(&proc.sig); } + for proc in &module.procs { + self.register_event_proc(proc); + } self.hir_procs = Vec::new(); - self.hir_procs.resize_with(self.next_proc_id as usize, || None); + self.hir_procs + .resize_with(self.next_proc_id as usize, || None); // Pass 2: Modulrumpf. Labels, Zeilennummern und DATA-Positionen // werden vorab eingesammelt (RESTORE nach vorn, statisches DATA). - let mut scope = Scope { is_module: true, ..Scope::default() }; + let mut scope = Scope { + is_module: true, + ..Scope::default() + }; self.prescan(&module.body, &mut scope, true); let body = self.lower_body(&module.body, &mut scope); let main = hir::HProc { @@ -491,7 +544,8 @@ impl Sema { body, label_count: scope.next_label, }; - self.hir_procs.resize_with(self.next_proc_id as usize, || None); + self.hir_procs + .resize_with(self.next_proc_id as usize, || None); self.hir_procs[0] = Some(main); self.module_labels = scope.labels.clone(); self.module_line_labels = scope.line_labels.clone(); @@ -527,6 +581,8 @@ impl Sema { procs, data: std::mem::take(&mut self.data), option_base: self.option_base, + objects: self.forms.objects.clone(), + event_procs: std::mem::take(&mut self.event_procs), }) } @@ -544,13 +600,29 @@ impl Sema { let key = self.var_key(&p.name, &p.suffix, p.as_type.is_some()); let hty = self.h_ty(&ty); let slot = scope.locals.len() as u16; - scope.locals.push(hir::HVar { name: p.name.clone(), ty: hty.clone(), array: p.array }); + scope.locals.push(hir::HVar { + name: p.name.clone(), + ty: hty.clone(), + array: p.array, + }); let by_ref = !p.array && !matches!(ty, Ty::Udt(_)); scope.vars.insert( key, - VarInfo { ty: ty.clone(), array: p.array, explicit: true, slot, global: false, by_ref }, + VarInfo { + ty: ty.clone(), + array: p.array, + explicit: true, + slot, + global: false, + by_ref, + }, ); - params.push(hir::HParam { name: p.name.clone(), ty: hty, array: p.array, by_ref }); + params.push(hir::HParam { + name: p.name.clone(), + ty: hty, + array: p.array, + by_ref, + }); } // Funktionsname als Rückgabe-Variable (immer frame-lokal). let mut ret_slot = None; @@ -566,7 +638,14 @@ impl Sema { }); scope.vars.insert( key, - VarInfo { ty: ret.clone(), array: false, explicit: true, slot, global: false, by_ref: false }, + VarInfo { + ty: ret.clone(), + array: false, + explicit: true, + slot, + global: false, + by_ref: false, + }, ); ret_slot = Some(VarSlot::Local(slot)); ret_ty = Some(self.h_ty(&ret)); @@ -601,7 +680,8 @@ impl Sema { let id = scope.new_label(); scope.labels.insert(n.clone(), id); if module_data { - self.data_marks_name.insert(n.clone(), self.data.len() as u32); + self.data_marks_name + .insert(n.clone(), self.data.len() as u32); } } Stmt::LineNumber(n) => { @@ -614,11 +694,19 @@ impl Sema { Stmt::Data { items, pos } => { if module_data { for it in items { - self.data.push(hir::DataItem { text: it.clone(), line: pos.line }); + self.data.push(hir::DataItem { + text: it.clone(), + line: pos.line, + }); } } } - Stmt::If { then_body, elseifs, else_body, .. } => { + Stmt::If { + then_body, + elseifs, + else_body, + .. + } => { self.prescan(then_body, scope, module_data); for (_, b) in elseifs { self.prescan(b, scope, module_data); @@ -653,7 +741,11 @@ impl Sema { ProcKind::Function => self.name_ty(&sig.name, &sig.suffix), ProcKind::Sub => Ty::Unknown, }; - let params = sig.params.iter().map(|p| (self.param_ty(p), p.array)).collect(); + let params = sig + .params + .iter() + .map(|p| (self.param_ty(p), p.array)) + .collect(); let id = match self.procs.get(&sig.name) { Some(p) => p.id, None => { @@ -664,7 +756,13 @@ impl Sema { }; self.procs.insert( sig.name.clone(), - ProcInfo { kind: sig.kind, ret, params, id, def_fn: false }, + ProcInfo { + kind: sig.kind, + ret, + params, + id, + def_fn: false, + }, ); } @@ -678,10 +776,250 @@ impl Sema { Ty::Str => HTy::Str, Ty::FixedStr(n) => HTy::FixedStr(*n), Ty::Udt(name) => HTy::Udt(*self.udt_ids.get(name).unwrap_or(&0)), + Ty::Form => HTy::Form, + Ty::Control => HTy::Control, Ty::Unknown => HTy::Num(NumTy::Sng), } } + fn register_event_proc(&mut self, proc: &Proc) { + let Some((object, class, event)) = self.event_binding(&proc.sig.name) else { + return; + }; + if !forms::events(class) + .iter() + .any(|e| e.eq_ignore_ascii_case(&event)) + { + return; + } + let mut expected: Vec<(&str, forms::EventParamType)> = Vec::new(); + if self.forms.objects[object as usize].array { + expected.push(("INDEX", forms::EventParamType::Integer)); + } + expected.extend(forms::event_params(&event).unwrap_or(&[]).iter().copied()); + let valid = proc.sig.kind == ProcKind::Sub + && proc.sig.params.len() == expected.len() + && proc.sig.params.iter().zip(&expected).all(|(p, (name, t))| { + let actual = self.param_ty(p); + p.name.eq_ignore_ascii_case(name) + && matches!( + (actual, t), + (Ty::Int, forms::EventParamType::Integer) + | (Ty::Sng, forms::EventParamType::Single) + | (Ty::Control, forms::EventParamType::Control) + ) + }); + if !valid { + let args = expected + .iter() + .map(|(n, t)| { + let ty = match t { + forms::EventParamType::Integer => "INTEGER", + forms::EventParamType::Single => "SINGLE", + forms::EventParamType::Control => "CONTROL", + }; + format!("{n} AS {ty}") + }) + .collect::>() + .join(", "); + self.err( + proc.pos, + format!("Event procedure {} expects ({args})", proc.sig.name), + ); + } + if let Some(info) = self.procs.get(&proc.sig.name) { + self.event_procs.push(hir::HEventProc { + object, + event, + proc: info.id, + }); + } + } + + fn event_binding(&self, name: &str) -> Option<(u16, ObjectClass, String)> { + let (base, event) = name.rsplit_once('_')?; + let found = if base == "FORM" { + self.forms + .objects + .iter() + .enumerate() + .find(|(_, o)| o.class == ObjectClass::Form) + .map(|(i, o)| (i as u16, o)) + } else { + self.forms.find(base) + }?; + Some((found.0, found.1.class, event.to_string())) + } + + fn property_ty(spec: forms::PropertySpec) -> Ty { + match spec.ty { + PropertyType::String => Ty::Str, + PropertyType::Single => Ty::Sng, + PropertyType::Object => Ty::Control, + PropertyType::Integer | PropertyType::Boolean | PropertyType::IntegerArray => Ty::Int, + } + } + + fn object_by_name( + &mut self, + name: &str, + pos: SourcePos, + ) -> Option<(u16, crate::forms::FormObject)> { + if let Some((id, object)) = self.forms.find(name) { + return Some((id, object.clone())); + } + self.err(pos, format!("Unknown object '{name}'")); + None + } + + fn object_property( + &mut self, + path: &str, + pos: SourcePos, + ) -> Option<(u16, u16, forms::PropertySpec, ObjectClass)> { + let (object_name, member, parent) = if let Some((form, tail)) = path.split_once('!') { + let (object, member) = tail.split_once('.').unwrap_or((tail, "")); + (object, member, Some(form)) + } else { + let (object, member) = path.split_once('.')?; + (object, member, None) + }; + let (id, object) = self.object_by_name(object_name, pos)?; + if let Some(parent) = parent { + let parent_ok = object + .parent_form + .as_deref() + .is_some_and(|p| p.eq_ignore_ascii_case(parent)); + if !parent_ok { + self.err( + pos, + format!("Control '{object_name}' not found in form '{parent}'"), + ); + return None; + } + } + let Some((property, spec)) = forms::property(object.class, member) else { + self.err( + pos, + format!("Unknown property '{member}' of {}", object.class.name()), + ); + return None; + }; + Some((id, property, spec, object.class)) + } + + fn object_member_target( + &mut self, + path: &str, + pos: SourcePos, + ) -> Option<(u16, crate::forms::FormObject, String)> { + let (object_name, member, parent) = if let Some((form, tail)) = path.split_once('!') { + let (object, member) = tail.split_once('.').unwrap_or((tail, "")); + (object, member, Some(form)) + } else { + let (object, member) = path.split_once('.')?; + (object, member, None) + }; + let (id, object) = self.object_by_name(object_name, pos)?; + if let Some(parent) = parent { + if !object + .parent_form + .as_deref() + .is_some_and(|p| p.eq_ignore_ascii_case(parent)) + { + self.err( + pos, + format!("Control '{object_name}' not found in form '{parent}'"), + ); + return None; + } + } + Some((id, object, member.to_string())) + } + + fn is_udt_path(&self, scope: &Scope, path: &str) -> bool { + let base = path.split('.').next().unwrap_or(path); + let as_key = format!("{base}\u{1}AS"); + let plain = self.var_key(base, &None, false); + self.vars_of(scope) + .get(&as_key) + .or_else(|| self.vars_of(scope).get(&plain)) + .is_some_and(|v| matches!(v.ty, Ty::Udt(_))) + } + + fn declared_var(&self, scope: &Scope, name: &str) -> Option { + let as_key = format!("{name}\u{1}AS"); + let plain = self.var_key(name, &None, false); + self.vars_of(scope) + .get(&as_key) + .or_else(|| self.vars_of(scope).get(&plain)) + .cloned() + .or_else(|| { + if scope.is_def_fn { + self.module_vars + .get(&as_key) + .or_else(|| self.module_vars.get(&plain)) + .cloned() + } else { + None + } + }) + } + + fn dynamic_property( + &mut self, + scope: &mut Scope, + path: &str, + pos: SourcePos, + ) -> Result, ()> { + let Some((base, member)) = path.split_once('.') else { + return Ok(None); + }; + let Some(info) = self.declared_var(scope, base) else { + return Ok(None); + }; + let spec = match info.ty { + Ty::Form => forms::property(ObjectClass::Form, member).map(|(_, spec)| spec), + Ty::Control => ObjectClass::ALL + .into_iter() + .filter(|class| !matches!(class, ObjectClass::Form | ObjectClass::Screen)) + .find_map(|class| forms::property(class, member).map(|(_, spec)| spec)), + _ => return Ok(None), + }; + let Some(spec) = spec else { + self.err(pos, format!("Unknown property '{member}'")); + return Err(()); + }; + let base = Expr::Name { + name: base.to_string(), + suffix: None, + args: None, + pos, + }; + let (place, _) = self.lower_place(&base, scope); + Ok(place.map(|place| (HExpr::Load(Box::new(place)), spec))) + } + + fn object_index( + &mut self, + object: u16, + args: &Option>, + scope: &mut Scope, + pos: SourcePos, + ) -> Result, ()> { + let Some(args) = args else { return Ok(None) }; + let info = &self.forms.objects[object as usize]; + if !info.array { + self.err(pos, format!("Object '{}' is not an array", info.name)); + return Err(()); + } + if args.len() != 1 { + self.err(pos, "Argument-count mismatch"); + return Err(()); + } + Ok(Some(self.lower_num_as(&args[0], scope, NumTy::Lng))) + } + /// Standardtyp eines Namens ohne Suffix (DEFtype bzw. SINGLE). fn default_ty(&self, name: &str) -> Ty { let first = name.chars().next().unwrap_or('A'); @@ -724,7 +1062,11 @@ impl Sema { fn alloc_global(&mut self, name: &str, ty: &Ty, array: bool) -> u16 { let slot = self.globals.len() as u16; - self.globals.push(hir::HVar { name: name.to_string(), ty: self.h_ty(ty), array }); + self.globals.push(hir::HVar { + name: name.to_string(), + ty: self.h_ty(ty), + array, + }); slot } @@ -734,10 +1076,17 @@ impl Sema { if scope.is_module { (self.alloc_global(name, ty, array), true) } else if scope.is_static { - (self.alloc_global(&format!("STATIC.{name}"), ty, array), true) + ( + self.alloc_global(&format!("STATIC.{name}"), ty, array), + true, + ) } else { let slot = scope.locals.len() as u16; - scope.locals.push(hir::HVar { name: name.to_string(), ty: self.h_ty(ty), array }); + scope.locals.push(hir::HVar { + name: name.to_string(), + ty: self.h_ty(ty), + array, + }); (slot, false) } } @@ -769,12 +1118,7 @@ impl Sema { } /// Feldpfad eines UDT-Zugriffs auflösen: liefert Typ und Feldindizes. - fn member_path( - &mut self, - base_ty: &Ty, - path: &[&str], - pos: SourcePos, - ) -> (Ty, Vec) { + fn member_path(&mut self, base_ty: &Ty, path: &[&str], pos: SourcePos) -> (Ty, Vec) { let mut cur = base_ty.clone(); let mut idxs = Vec::new(); for seg in path { @@ -795,6 +1139,8 @@ impl Sema { HTy::Str => Ty::Str, HTy::FixedStr(n) => Ty::FixedStr(n), HTy::Udt(id) => Ty::Udt(self.udt_defs[id as usize].name.clone()), + HTy::Form => Ty::Form, + HTy::Control => Ty::Control, }; } None => { @@ -854,12 +1200,26 @@ impl Sema { // DEF FN: freie Namen binden an Modulvariablen (Vorbild-Semantik). let info = if scope.is_def_fn { let slot = self.alloc_global(name, &ty, array); - let info = VarInfo { ty, array, explicit: false, slot, global: true, by_ref: false }; + let info = VarInfo { + ty, + array, + explicit: false, + slot, + global: true, + by_ref: false, + }; self.module_vars.insert(key, info.clone()); info } else { let (slot, global) = self.alloc_var(scope, name, &ty, array); - let info = VarInfo { ty, array, explicit: false, slot, global, by_ref: false }; + let info = VarInfo { + ty, + array, + explicit: false, + slot, + global, + by_ref: false, + }; self.insert_var(scope, key, info.clone()); info }; @@ -942,11 +1302,21 @@ impl Sema { if f == to { return e; } - HExpr::Conv { from: f, to, arg: Box::new(e) } + HExpr::Conv { + from: f, + to, + arg: Box::new(e), + } } /// Wert an Zieltyp anpassen (Zuweisung, BYVAL-Argument). fn coerce(&mut self, e: HExpr, from: &Ty, to: &Ty, pos: SourcePos) -> HExpr { + if matches!( + (from, to), + (Ty::Control, Ty::Control) | (Ty::Form, Ty::Form) + ) { + return e; + } match (is_num(to), is_str(to)) { (true, _) if is_num(from) => { if matches!(to, Ty::Unknown) || matches!(from, Ty::Unknown) { @@ -956,7 +1326,10 @@ impl Sema { } } (_, true) if is_str(from) => match to { - Ty::FixedStr(n) => HExpr::FixStr { len: *n, arg: Box::new(e) }, + Ty::FixedStr(n) => HExpr::FixStr { + len: *n, + arg: Box::new(e), + }, _ => e, }, _ => { @@ -979,7 +1352,10 @@ impl Sema { } fn push(&mut self, out: &mut Vec, scope: &Scope, kind: HStmtKind) { - out.push(HStmt { line: scope.current_line, kind }); + out.push(HStmt { + line: scope.current_line, + kind, + }); } fn lower_stmt(&mut self, stmt: &Stmt, scope: &mut Scope, out: &mut Vec) { @@ -989,7 +1365,10 @@ impl Sema { scope.current_line = pos.line; } match stmt { - Stmt::Data { .. } | Stmt::Include { .. } | Stmt::MetaArrays { .. } => {} + Stmt::Data { .. } + | Stmt::Include { .. } + | Stmt::MetaArrays { .. } + | Stmt::MetaForm { .. } => {} Stmt::Label(n) => { if let Some(id) = scope.labels.get(n).copied() { self.push(out, scope, HStmtKind::Label(id)); @@ -1006,7 +1385,12 @@ impl Sema { Stmt::System(_) => self.push(out, scope, HStmtKind::System), Stmt::Exit { kind, pos } => match kind { ExitKind::For => { - match scope.loop_exits.iter().rev().find(|(k, _)| *k == LoopKind::For) { + match scope + .loop_exits + .iter() + .rev() + .find(|(k, _)| *k == LoopKind::For) + { Some((_, id)) => { let id = *id; self.push(out, scope, HStmtKind::Goto(id)); @@ -1015,7 +1399,12 @@ impl Sema { } } ExitKind::Do => { - match scope.loop_exits.iter().rev().find(|(k, _)| *k == LoopKind::Do) { + match scope + .loop_exits + .iter() + .rev() + .find(|(k, _)| *k == LoopKind::Do) + { Some((_, id)) => { let id = *id; self.push(out, scope, HStmtKind::Goto(id)); @@ -1029,6 +1418,111 @@ impl Sema { }, Stmt::Assign { target, value, pos } => { + if let Expr::Name { + name, + suffix: None, + args, + .. + } = target + { + if (name.contains('.') || name.contains('!')) && !self.is_udt_path(scope, name) + { + if name.contains('!') && !name.contains('.') { + let object_name = name.split_once('!').unwrap().1; + self.err( + *pos, + format!("Object '{object_name}' has no default property"), + ); + self.lower_expr(value, scope); + return; + } + if args.is_none() { + match self.dynamic_property(scope, name, *pos) { + Ok(Some((object_expr, spec))) => { + if !spec.writable { + self.err( + *pos, + format!( + "Property '{}' is read-only at run time", + spec.name + ), + ); + self.lower_expr(value, scope); + return; + } + let (value, actual) = self.lower_expr(value, scope); + let expected = Self::property_ty(spec); + if !((is_num(&expected) && is_num(&actual)) + || (is_str(&expected) && is_str(&actual))) + { + self.err( + *pos, + format!("Type mismatch for property '{}'", spec.name), + ); + } + let value = self.coerce_silent(value, &actual, &expected); + self.push( + out, + scope, + HStmtKind::SetDynamicObjectProperty { + object: object_expr, + property: spec.name.to_string(), + value, + }, + ); + return; + } + Err(()) => { + self.lower_expr(value, scope); + return; + } + Ok(None) => {} + } + } + if let Some((object, property, spec, _)) = self.object_property(name, *pos) + { + let Ok(index) = self.object_index(object, args, scope, *pos) else { + self.lower_expr(value, scope); + return; + }; + if !spec.writable { + self.err( + *pos, + format!("Property '{}' is read-only at run time", spec.name), + ); + self.lower_expr(value, scope); + return; + } + let (value, actual) = self.lower_expr(value, scope); + let expected = Self::property_ty(spec); + if !((is_num(&expected) && is_num(&actual)) + || (is_str(&expected) && is_str(&actual))) + { + self.err( + *pos, + format!("Type mismatch for property '{}'", spec.name), + ); + } + let value = self.coerce_silent(value, &actual, &expected); + self.push( + out, + scope, + HStmtKind::SetObjectProperty { + object, + index, + property, + value, + }, + ); + } + return; + } + if self.forms.find(name).is_some() { + self.err(*pos, format!("Object '{name}' has no default property")); + self.lower_expr(value, scope); + return; + } + } // MID$-Anweisung: MID$(s$, start [, laenge]) = ausdruck if let Expr::Name { name, @@ -1076,7 +1570,13 @@ impl Sema { // Zuweisungen an eine Variable. // `ERR = n` setzt den Fehlercode, ohne einen Fehler // auszulösen (Anweisungsform von ERR). - if let Expr::Name { name, suffix: None, args: None, .. } = target { + if let Expr::Name { + name, + suffix: None, + args: None, + .. + } = target + { if name == "ERR" { let (e, t) = self.want_num(value, scope); let e = self.conv_num(e, &t, NumTy::Lng); @@ -1084,7 +1584,13 @@ impl Sema { return; } } - if let Expr::Name { name, suffix: Some(Suffix::Str), args: None, .. } = target { + if let Expr::Name { + name, + suffix: Some(Suffix::Str), + args: None, + .. + } = target + { if name == "DATE" || name == "TIME" { let (ve, _) = self.want_str(value, scope); let bt = if name == "DATE" { @@ -1095,7 +1601,10 @@ impl Sema { self.push( out, scope, - HStmtKind::BuiltinStmt { b: bt, args: vec![ve] }, + HStmtKind::BuiltinStmt { + b: bt, + args: vec![ve], + }, ); return; } @@ -1108,7 +1617,13 @@ impl Sema { self.push(out, scope, HStmtKind::Assign { place, value }); } } - Stmt::Print { printer, file, using, items, .. } => { + Stmt::Print { + printer, + file, + using, + items, + .. + } => { if let Some(f) = file { self.want_num(f, scope); } @@ -1123,7 +1638,13 @@ impl Sema { PrintItem::Expr(e) => { trailing = false; // TAB(n)/SPC(n) sind Positionssteuerungen. - if let Expr::Name { name, suffix: None, args: Some(a), .. } = e { + if let Expr::Name { + name, + suffix: None, + args: Some(a), + .. + } = e + { if (name == "TAB" || name == "SPC") && a.len() == 1 { let n = self.lower_num_as(&a[0], scope, NumTy::Lng); hitems.push(if name == "TAB" { @@ -1175,23 +1696,43 @@ impl Sema { self.push( out, scope, - HStmtKind::BuiltinStmt { b: Builtin::PrintUsing, args }, + HStmtKind::BuiltinStmt { + b: Builtin::PrintUsing, + args, + }, ); if !trailing { self.push( out, scope, - HStmtKind::BuiltinStmt { b: Builtin::PrintNewline, args: vec![] }, + HStmtKind::BuiltinStmt { + b: Builtin::PrintNewline, + args: vec![], + }, ); } } else { - self.push(out, scope, HStmtKind::Print { items: hitems, trailing }); + self.push( + out, + scope, + HStmtKind::Print { + items: hitems, + trailing, + }, + ); } if ziel.is_some() { self.push_ziel(out, scope, HExpr::Lng(-1)); } } - Stmt::Input { line, file, keep_cursor: _, prompt, vars, .. } => { + Stmt::Input { + line, + file, + keep_cursor: _, + prompt, + vars, + .. + } => { let datei = file.as_ref().map(|f| { let (e, t) = self.want_num(f, scope); self.conv_num(e, &t, NumTy::Lng) @@ -1220,7 +1761,13 @@ impl Sema { }, ); } - Stmt::If { cond, then_body, elseifs, else_body, .. } => { + Stmt::If { + cond, + then_body, + elseifs, + else_body, + .. + } => { let c = self.lower_cond(cond, scope); let then = self.lower_body(then_body, scope); // ELSEIF-Kette zu verschachteltem If absenken (von hinten). @@ -1233,7 +1780,11 @@ impl Sema { let body2 = self.lower_body(eb, scope); let stmt = HStmt { line: scope.current_line, - kind: HStmtKind::If { cond: c2, then: body2, els }, + kind: HStmtKind::If { + cond: c2, + then: body2, + els, + }, }; els = vec![stmt]; } @@ -1242,7 +1793,14 @@ impl Sema { Stmt::Select { expr, arms, .. } => { self.lower_select(expr, arms, scope, out); } - Stmt::For { var, from, to, step, body, pos } => { + Stmt::For { + var, + from, + to, + step, + body, + pos, + } => { let (place, vt) = self.lower_place(var, scope); if !is_num(&vt) { self.err(*pos, "Type mismatch"); @@ -1287,21 +1845,24 @@ impl Sema { ); } } - Stmt::DoLoop { pre, post, body, .. } => { - let pre_c = pre - .as_ref() - .map(|(u, c)| (*u, self.lower_cond(c, scope))); + Stmt::DoLoop { + pre, post, body, .. + } => { + let pre_c = pre.as_ref().map(|(u, c)| (*u, self.lower_cond(c, scope))); let exit_label = scope.new_label(); scope.loop_exits.push((LoopKind::Do, exit_label)); let hbody = self.lower_body(body, scope); scope.loop_exits.pop(); - let post_c = post - .as_ref() - .map(|(u, c)| (*u, self.lower_cond(c, scope))); + let post_c = post.as_ref().map(|(u, c)| (*u, self.lower_cond(c, scope))); self.push( out, scope, - HStmtKind::Loop { pre: pre_c, post: post_c, body: hbody, exit_label }, + HStmtKind::Loop { + pre: pre_c, + post: post_c, + body: hbody, + exit_label, + }, ); } Stmt::While { cond, body, .. } => { @@ -1329,7 +1890,12 @@ impl Sema { self.push(out, scope, HStmtKind::Gosub(id)); } } - Stmt::OnGoto { expr, targets, gosub, pos } => { + Stmt::OnGoto { + expr, + targets, + gosub, + pos, + } => { let sel = self.lower_num_as(expr, scope, NumTy::Int); let mut ids = Vec::new(); for t in targets { @@ -1337,7 +1903,15 @@ impl Sema { ids.push(id); } } - self.push(out, scope, HStmtKind::OnGoto { sel, gosub: *gosub, targets: ids }); + self.push( + out, + scope, + HStmtKind::OnGoto { + sel, + gosub: *gosub, + targets: ids, + }, + ); } Stmt::Return { target, pos } => { let id = match target { @@ -1363,11 +1937,25 @@ impl Sema { self.module_label_id(t, *pos) }; if let Some(id) = id { - self.push(out, scope, HStmtKind::OnError { local, target: Some(id) }); + self.push( + out, + scope, + HStmtKind::OnError { + local, + target: Some(id), + }, + ); } } OnErrorAction::Disable => { - self.push(out, scope, HStmtKind::OnError { local, target: None }); + self.push( + out, + scope, + HStmtKind::OnError { + local, + target: None, + }, + ); } OnErrorAction::ResumeNext => { self.push(out, scope, HStmtKind::OnErrorResumeNext { local }); @@ -1448,7 +2036,10 @@ impl Sema { Stmt::Erase { names, .. } => { let mut slots = Vec::new(); for n in names { - if let Expr::Name { name, suffix, pos, .. } = n { + if let Expr::Name { + name, suffix, pos, .. + } = n + { if let Some(v) = self.resolve_var(scope, name, suffix, true, *pos) { slots.push(if v.global { VarSlot::Global(v.slot) @@ -1483,8 +2074,7 @@ impl Sema { let (a, b) = (a.to_ascii_uppercase(), b.to_ascii_uppercase()); for c in a..=b { if c.is_ascii_uppercase() { - self.deftypes[(c as u8 - b'A') as usize] = - Some(type_name_ty(ty)); + self.deftypes[(c as u8 - b'A') as usize] = Some(type_name_ty(ty)); } } } @@ -1495,7 +2085,9 @@ impl Sema { }, Stmt::TypeDecl { .. } => {} // bereits in Pass 1 registriert Stmt::Declare { .. } => {} // bereits in Pass 1 registriert - Stmt::Call { name, args, pos, .. } => { + Stmt::Call { + name, args, pos, .. + } => { self.lower_call_stmt(name, args, scope, *pos, out); } Stmt::ReadStmt { vars, .. } => { @@ -1613,7 +2205,14 @@ impl Sema { }, ); } - Stmt::OpenLegacy { mode, number, file, len, pos, .. } => { + Stmt::OpenLegacy { + mode, + number, + file, + len, + pos, + .. + } => { let (me, _) = self.want_str(mode, scope); let (ne, nt) = self.want_num(number, scope); let ne = self.conv_num(ne, &nt, NumTy::Lng); @@ -1640,7 +2239,10 @@ impl Sema { self.push( out, scope, - HStmtKind::BuiltinStmt { b: Builtin::CloseAll, args: vec![] }, + HStmtKind::BuiltinStmt { + b: Builtin::CloseAll, + args: vec![], + }, ); } for f in files { @@ -1649,7 +2251,10 @@ impl Sema { self.push( out, scope, - HStmtKind::BuiltinStmt { b: Builtin::Close, args: vec![e] }, + HStmtKind::BuiltinStmt { + b: Builtin::Close, + args: vec![e], + }, ); } } @@ -1668,9 +2273,22 @@ impl Sema { hf.push((we, p)); } } - self.push(out, scope, HStmtKind::Field { file: fe, fields: hf }); + self.push( + out, + scope, + HStmtKind::Field { + file: fe, + fields: hf, + }, + ); } - Stmt::GetPut { put, file, recnum, var, .. } => { + Stmt::GetPut { + put, + file, + recnum, + var, + .. + } => { let (fe, ft) = self.want_num(file, scope); let fe = self.conv_num(fe, &ft, NumTy::Lng); let re = recnum.as_ref().map(|r| { @@ -1681,10 +2299,20 @@ impl Sema { self.push( out, scope, - HStmtKind::GetPut { put: *put, file: fe, recnum: re, var: ve }, + HStmtKind::GetPut { + put: *put, + file: fe, + recnum: re, + var: ve, + }, ); } - Stmt::LsetRset { rset, target, value, pos } => { + Stmt::LsetRset { + rset, + target, + value, + pos, + } => { let (_, tt) = self.lower_place(target, scope); let (_, vt) = self.lower_expr(value, scope); let ok = (is_str(&tt) && is_str(&vt)) @@ -1700,7 +2328,11 @@ impl Sema { self.push( out, scope, - HStmtKind::LsetRset { rset: *rset, target: place, value: ve }, + HStmtKind::LsetRset { + rset: *rset, + target: place, + value: ve, + }, ); } } @@ -1722,14 +2354,20 @@ impl Sema { self.push( out, scope, - HStmtKind::BuiltinStmt { b: Builtin::WriteFile, args }, + HStmtKind::BuiltinStmt { + b: Builtin::WriteFile, + args, + }, ); self.push_ziel(out, scope, HExpr::Lng(-1)); } else { self.push( out, scope, - HStmtKind::BuiltinStmt { b: Builtin::WriteFile, args }, + HStmtKind::BuiltinStmt { + b: Builtin::WriteFile, + args, + }, ); } } @@ -1743,7 +2381,10 @@ impl Sema { self.push( out, scope, - HStmtKind::BuiltinStmt { b: Builtin::SeekStmt, args: vec![fe, pe] }, + HStmtKind::BuiltinStmt { + b: Builtin::SeekStmt, + args: vec![fe, pe], + }, ); } Stmt::LockStmt { file, from, to, .. } => { @@ -1759,7 +2400,10 @@ impl Sema { self.push( out, scope, - HStmtKind::BuiltinStmt { b: Builtin::LockStmt, args: vec![fe] }, + HStmtKind::BuiltinStmt { + b: Builtin::LockStmt, + args: vec![fe], + }, ); } Stmt::NameStmt { old, new, .. } => { @@ -1770,7 +2414,10 @@ impl Sema { self.push( out, scope, - HStmtKind::BuiltinStmt { b: Builtin::NameStmt, args: vec![a, b2] }, + HStmtKind::BuiltinStmt { + b: Builtin::NameStmt, + args: vec![a, b2], + }, ); } // ---- Bildschirm/Ereignisse ---- @@ -1941,7 +2588,11 @@ impl Sema { ) { let (se, st) = self.lower_expr(expr, scope); // Selektor einmal auswerten (versteckter Temp-Slot). - let sel_ty = if st == Ty::Unknown { Ty::Sng } else { st.clone() }; + let sel_ty = if st == Ty::Unknown { + Ty::Sng + } else { + st.clone() + }; let temp = self.alloc_temp(scope, &sel_ty); let temp_place = HPlace { base: temp, @@ -1954,7 +2605,10 @@ impl Sema { self.push( out, scope, - HStmtKind::Assign { place: temp_place.clone(), value: se }, + HStmtKind::Assign { + place: temp_place.clone(), + value: se, + }, ); // Arme in Bedingungs-/Rumpf-Paare übersetzen; CASE ELSE gesondert. @@ -1986,7 +2640,11 @@ impl Sema { for (cond, body) in cases.into_iter().rev() { let stmt = HStmt { line: scope.current_line, - kind: HStmtKind::If { cond, then: body, els }, + kind: HStmtKind::If { + cond, + then: body, + els, + }, }; els = vec![stmt]; } @@ -2090,7 +2748,10 @@ impl Sema { // Rumpf-Scope: Parameter (BYVAL) und Rückgabevariable lokal, // freie Namen binden an Modulvariablen. - let mut fscope = Scope { is_def_fn: true, ..Scope::default() }; + let mut fscope = Scope { + is_def_fn: true, + ..Scope::default() + }; let mut hparams = Vec::new(); for p in params { let ty = self.param_ty(p); @@ -2103,7 +2764,14 @@ impl Sema { }); fscope.vars.insert( key, - VarInfo { ty: ty.clone(), array: false, explicit: true, slot, global: false, by_ref: false }, + VarInfo { + ty: ty.clone(), + array: false, + explicit: true, + slot, + global: false, + by_ref: false, + }, ); hparams.push(hir::HParam { name: p.name.clone(), @@ -2205,7 +2873,12 @@ impl Sema { self.push( out, scope, - HStmtKind::Dim { slot, elem: self.h_ty(&ty), dims: hdims, redim: true }, + HStmtKind::Dim { + slot, + elem: self.h_ty(&ty), + dims: hdims, + redim: true, + }, ); return; } @@ -2214,14 +2887,30 @@ impl Sema { self.insert_var( scope, key, - VarInfo { ty: ty.clone(), array: is_array, explicit: true, slot, global, by_ref: false }, + VarInfo { + ty: ty.clone(), + array: is_array, + explicit: true, + slot, + global, + by_ref: false, + }, ); if is_array && !hdims.is_empty() { - let vslot = if global { VarSlot::Global(slot) } else { VarSlot::Local(slot) }; + let vslot = if global { + VarSlot::Global(slot) + } else { + VarSlot::Local(slot) + }; self.push( out, scope, - HStmtKind::Dim { slot: vslot, elem: self.h_ty(&ty), dims: hdims, redim }, + HStmtKind::Dim { + slot: vslot, + elem: self.h_ty(&ty), + dims: hdims, + redim, + }, ); } } @@ -2270,6 +2959,94 @@ impl Sema { pos: SourcePos, out: &mut Vec, ) { + if name.contains('.') || name.contains('!') { + if let Some((object, info, member)) = self.object_member_target(name, pos) { + let Some(method) = forms::methods(info.class) + .iter() + .position(|m| m.eq_ignore_ascii_case(&member)) + else { + self.err( + pos, + format!("Unknown method '{member}' of {}", info.class.name()), + ); + return; + }; + if !forms::method_is_implemented(info.class, forms::methods(info.class)[method]) { + self.err( + pos, + format!("Feature unavailable: {}.{member}", info.class.name()), + ); + return; + } + let member = forms::methods(info.class)[method]; + let (min, max) = forms::method_arity(info.class, member).unwrap(); + if !(min..=max).contains(&args.len()) { + self.err( + pos, + format!("Argument-count mismatch for {}.{member}", info.class.name()), + ); + for arg in args { + self.lower_expr(arg, scope); + } + return; + } + let hargs = if info.class == ObjectClass::Form && member == "SHOW" { + args.iter() + .map(|a| self.lower_num_as(a, scope, NumTy::Lng)) + .collect() + } else { + args.iter().map(|a| self.lower_expr(a, scope).0).collect() + }; + self.push( + out, + scope, + HStmtKind::ObjectMethod { + object, + method: method as u16, + args: hargs, + }, + ); + return; + } + return; + } + if matches!(name, "LOAD" | "UNLOAD") && args.len() == 1 { + if let Expr::Name { + name: object_name, + suffix: None, + args: index, + pos: object_pos, + } = &args[0] + { + if let Some((object, info)) = + self.forms.find(object_name).map(|(i, o)| (i, o.clone())) + { + if index.is_some() && !info.array { + self.err( + *object_pos, + format!("Object '{object_name}' is not an array"), + ); + return; + } + let index = index + .as_ref() + .and_then(|a| a.first()) + .map(|e| self.lower_num_as(e, scope, NumTy::Lng)); + self.push( + out, + scope, + HStmtKind::ObjectLoad { + object, + index, + unload: name == "UNLOAD", + }, + ); + return; + } + self.err(*object_pos, format!("Unknown object '{object_name}'")); + return; + } + } if let Some(info) = self.procs.get(name).cloned() { if info.kind != ProcKind::Sub { self.err(pos, "Duplicate definition"); @@ -2279,7 +3056,14 @@ impl Sema { self.err(pos, "Argument-count mismatch"); } let hargs = self.lower_call_args(&info, args, scope); - self.push(out, scope, HStmtKind::CallSub { proc: info.id, args: hargs }); + self.push( + out, + scope, + HStmtKind::CallSub { + proc: info.id, + args: hargs, + }, + ); return; } if let Some((min, max, spec)) = builtin_stmt(name) { @@ -2309,7 +3093,13 @@ impl Sema { let force_byval = info.def_fn; // Array-Parameter: ganzes Array als Referenz. if p_array { - if let Expr::Name { name, suffix, args: idx, pos } = a { + if let Expr::Name { + name, + suffix, + args: idx, + pos, + } = a + { if idx.as_ref().map(|v| v.is_empty()).unwrap_or(true) { if let Some(v) = self.resolve_var(scope, name, suffix, true, *pos) { let base = if v.global { @@ -2339,6 +3129,10 @@ impl Sema { let kind_ok = (is_num(&pt) && is_num(&at)) || (is_str(&pt) && is_str(&at)) || matches!((&pt, &at), (Ty::Udt(x), Ty::Udt(y)) if x == y) + || matches!( + (&pt, &at), + (Ty::Control, Ty::Control) | (Ty::Form, Ty::Form) + ) || pt == Ty::Unknown || at == Ty::Unknown; if !kind_ok { @@ -2369,6 +3163,10 @@ impl Sema { let (e, at) = self.lower_expr(a, scope); let kind_ok = (is_num(&pt) && is_num(&at)) || (is_str(&pt) && is_str(&at)) + || matches!( + (&pt, &at), + (Ty::Control, Ty::Control) | (Ty::Form, Ty::Form) + ) || pt == Ty::Unknown || at == Ty::Unknown; if !kind_ok { @@ -2385,10 +3183,17 @@ impl Sema { /// Konstante, Funktion oder Klammerung? fn is_place_expr(&self, e: &Expr, scope: &Scope) -> bool { match e { - Expr::Name { name, suffix, args, .. } => { + Expr::Name { + name, suffix, args, .. + } => { if self.consts.contains_key(name) { return false; } + if self.forms.find(name).is_some() + || (name.contains('!') && !self.is_udt_path(scope, name)) + { + return false; + } match args { None => { // Variable oder parameterlose Funktion/Builtin? @@ -2477,7 +3282,14 @@ impl Sema { vec![conv] }) .unwrap_or_default(); - self.push(out, scope, HStmtKind::BuiltinStmt { b: Builtin::Randomize, args: a }); + self.push( + out, + scope, + HStmtKind::BuiltinStmt { + b: Builtin::Randomize, + args: a, + }, + ); } "SLEEP" => { let a = lowered @@ -2488,7 +3300,14 @@ impl Sema { vec![conv] }) .unwrap_or_default(); - self.push(out, scope, HStmtKind::BuiltinStmt { b: Builtin::Sleep, args: a }); + self.push( + out, + scope, + HStmtKind::BuiltinStmt { + b: Builtin::Sleep, + args: a, + }, + ); } "SWAP" => { // SWAP a, b → t = a : a = b : b = t (versteckter Temp). @@ -2519,9 +3338,30 @@ impl Sema { let b_conv = self.coerce_silent(load_b, &tb, &ta); let t_load = HExpr::Load(Box::new(tmp_place.clone())); let t_conv = self.coerce_silent(t_load, &ta, &tb); - self.push(out, scope, HStmtKind::Assign { place: tmp_place, value: load_a }); - self.push(out, scope, HStmtKind::Assign { place: pa, value: b_conv }); - self.push(out, scope, HStmtKind::Assign { place: pb, value: t_conv }); + self.push( + out, + scope, + HStmtKind::Assign { + place: tmp_place, + value: load_a, + }, + ); + self.push( + out, + scope, + HStmtKind::Assign { + place: pa, + value: b_conv, + }, + ); + self.push( + out, + scope, + HStmtKind::Assign { + place: pb, + value: t_conv, + }, + ); } } } @@ -2554,7 +3394,10 @@ impl Sema { "RESET" => self.push( out, scope, - HStmtKind::BuiltinStmt { b: Builtin::CloseAll, args: vec![] }, + HStmtKind::BuiltinStmt { + b: Builtin::CloseAll, + args: vec![], + }, ), "SETFORMATCC" => { let (e, t) = lowered[0].clone(); @@ -2562,7 +3405,10 @@ impl Sema { self.push( out, scope, - HStmtKind::BuiltinStmt { b: Builtin::SetFormatCc, args: vec![n] }, + HStmtKind::BuiltinStmt { + b: Builtin::SetFormatCc, + args: vec![n], + }, ); } "KEY" => { @@ -2578,7 +3424,10 @@ impl Sema { self.push( out, scope, - HStmtKind::BuiltinStmt { b: Builtin::KeyList, args: vec![] }, + HStmtKind::BuiltinStmt { + b: Builtin::KeyList, + args: vec![], + }, ); } else { if lowered.len() != 2 { @@ -2590,7 +3439,10 @@ impl Sema { self.push( out, scope, - HStmtKind::BuiltinStmt { b: Builtin::KeyAssign, args: vec![n, t] }, + HStmtKind::BuiltinStmt { + b: Builtin::KeyAssign, + args: vec![n, t], + }, ); } } @@ -2679,9 +3531,10 @@ impl Sema { // literal bekannt sind (Aufgabe 2.3). Ist die Nummer erst zur // Laufzeit bekannt, prüft die Laufzeit gegen das Tabellenlayout. if matches!(name, "INSERT" | "RETRIEVE" | "UPDATE") { - if let (Some(ConstVal::Num(n)), Some((_, Ty::Udt(hat)))) = - (args.first().and_then(|a| self.fold_const(a)), lowered.get(1)) - { + if let (Some(ConstVal::Num(n)), Some((_, Ty::Udt(hat)))) = ( + args.first().and_then(|a| self.fold_const(a)), + lowered.get(1), + ) { if let Some(soll) = self.isam_typ.get(&(n as i32)) { if soll != hat { self.err(args[1].pos(), "Type mismatch"); @@ -2709,7 +3562,10 @@ impl Sema { self.push( out, scope, - HStmtKind::BuiltinStmt { b: Builtin::PrintZiel, args: vec![n] }, + HStmtKind::BuiltinStmt { + b: Builtin::PrintZiel, + args: vec![n], + }, ); } @@ -2746,8 +3602,10 @@ impl Sema { scope: &mut Scope, pos: SourcePos, ) -> Vec<(HExpr, Ty)> { - let real: Vec<&Expr> = - args.iter().filter(|a| !matches!(a, Expr::Missing)).collect(); + let real: Vec<&Expr> = args + .iter() + .filter(|a| !matches!(a, Expr::Missing)) + .collect(); if real.len() < min as usize || args.len() > max as usize { // Katalogtext des Vorbilds, ergänzt um das Element: eine Diagnose // zu einem dokumentierten Element muss es benennen (Guiding @@ -2862,6 +3720,10 @@ impl Sema { let ok = (is_num(target) && is_num(value)) || (is_str(target) && is_str(value)) || matches!((target, value), (Ty::Udt(a), Ty::Udt(b)) if a == b) + || matches!( + (target, value), + (Ty::Control, Ty::Control) | (Ty::Form, Ty::Form) + ) || *target == Ty::Unknown || *value == Ty::Unknown; if !ok { @@ -2876,7 +3738,10 @@ impl Sema { } if let Ty::FixedStr(n) = to { if is_str(from) { - return HExpr::FixStr { len: *n, arg: Box::new(e) }; + return HExpr::FixStr { + len: *n, + arg: Box::new(e), + }; } } e @@ -2885,7 +3750,12 @@ impl Sema { /// L-Wert absenken: liefert Platz und Typ. fn lower_place(&mut self, e: &Expr, scope: &mut Scope) -> (Option, Ty) { match e { - Expr::Name { name, suffix, args, pos } => { + Expr::Name { + name, + suffix, + args, + pos, + } => { let indices = match args { Some(idx) => { let mut v = Vec::new(); @@ -2987,6 +3857,24 @@ impl Sema { Expr::StrLit(s) => (HExpr::Str(s.clone()), Ty::Str), Expr::Paren(inner) => self.lower_expr(inner, scope), Expr::Missing => (HExpr::Int(0), Ty::Unknown), + Expr::TypeOf { value, class, pos } => { + let Some(class) = ObjectClass::parse(class) else { + self.err(*pos, format!("Unknown control class '{class}'")); + self.lower_expr(value, scope); + return (HExpr::Int(0), Ty::Int); + }; + let (value, ty) = self.lower_expr(value, scope); + if !matches!(ty, Ty::Form | Ty::Control | Ty::Unknown) { + self.err(*pos, "Object required"); + } + ( + HExpr::TypeOf { + value: Box::new(value), + class, + }, + Ty::Int, + ) + } Expr::Unary { op, operand, pos } => { let (he, t) = self.lower_expr(operand, scope); if !is_num(&t) { @@ -3005,7 +3893,13 @@ impl Sema { } (HExpr::Sng(v), NumTy::Sng) => (HExpr::Sng(-v), t.clone()), (HExpr::Dbl(v), NumTy::Dbl) => (HExpr::Dbl(-v), t.clone()), - _ => (HExpr::Neg { ty: nt, arg: Box::new(he) }, t.clone()), + _ => ( + HExpr::Neg { + ty: nt, + arg: Box::new(he), + }, + t.clone(), + ), } } UnOp::Not => { @@ -3014,11 +3908,26 @@ impl Sema { } else { IntKind::I4 }; - let target = if kind == IntKind::I2 { NumTy::Int } else { NumTy::Lng }; - let conv = if t == Ty::Unknown { he } else { self.conv_num(he, &t, target) }; + let target = if kind == IntKind::I2 { + NumTy::Int + } else { + NumTy::Lng + }; + let conv = if t == Ty::Unknown { + he + } else { + self.conv_num(he, &t, target) + }; ( - HExpr::Not { ty: kind, arg: Box::new(conv) }, - if kind == IntKind::I2 { Ty::Int } else { Ty::Lng }, + HExpr::Not { + ty: kind, + arg: Box::new(conv), + }, + if kind == IntKind::I2 { + Ty::Int + } else { + Ty::Lng + }, ) } } @@ -3063,7 +3972,11 @@ impl Sema { self.err(pos, "Type mismatch"); return (HExpr::Int(0), Ty::Unknown); } - let a = if op == BinOp::Sub { HArith::Sub } else { HArith::Mul }; + let a = if op == BinOp::Sub { + HArith::Sub + } else { + HArith::Mul + }; self.arith(a, le, <, re, &rt) } BinOp::Div => { @@ -3082,7 +3995,12 @@ impl Sema { let l = self.conv_num(le, <, out); let r = self.conv_num(re, &rt, out); ( - HExpr::Bin { op: HArith::Div, ty: out, l: Box::new(l), r: Box::new(r) }, + HExpr::Bin { + op: HArith::Div, + ty: out, + l: Box::new(l), + r: Box::new(r), + }, ty_of_num(out), ) } @@ -3114,7 +4032,11 @@ impl Sema { (p, Ty::Dbl) } else { ( - HExpr::Conv { from: NumTy::Dbl, to: NumTy::Sng, arg: Box::new(p) }, + HExpr::Conv { + from: NumTy::Dbl, + to: NumTy::Sng, + arg: Box::new(p), + }, Ty::Sng, ) } @@ -3133,9 +4055,18 @@ impl Sema { }; let l = self.conv_num(le, <, out); let r = self.conv_num(re, &rt, out); - let a = if op == BinOp::IntDiv { HArith::IDiv } else { HArith::Mod }; + let a = if op == BinOp::IntDiv { + HArith::IDiv + } else { + HArith::Mod + }; ( - HExpr::Bin { op: a, ty: out, l: Box::new(l), r: Box::new(r) }, + HExpr::Bin { + op: a, + ty: out, + l: Box::new(l), + r: Box::new(r), + }, ty_of_num(out), ) } @@ -3194,26 +4125,32 @@ impl Sema { } else { IntKind::I4 }; - let target = if kind == IntKind::I2 { NumTy::Int } else { NumTy::Lng }; + let target = if kind == IntKind::I2 { + NumTy::Int + } else { + NumTy::Lng + }; let l = self.conv_num(le, <, target); let r = self.conv_num(re, &rt, target); ( - HExpr::Logic { op: hop, ty: kind, l: Box::new(l), r: Box::new(r) }, - if kind == IntKind::I2 { Ty::Int } else { Ty::Lng }, + HExpr::Logic { + op: hop, + ty: kind, + l: Box::new(l), + r: Box::new(r), + }, + if kind == IntKind::I2 { + Ty::Int + } else { + Ty::Lng + }, ) } } } /// Arithmetik `+ - *` im promoteten Typ. - fn arith( - &mut self, - op: hir::HArith, - le: HExpr, - lt: &Ty, - re: HExpr, - rt: &Ty, - ) -> (HExpr, Ty) { + fn arith(&mut self, op: hir::HArith, le: HExpr, lt: &Ty, re: HExpr, rt: &Ty) -> (HExpr, Ty) { if *lt == Ty::Unknown || *rt == Ty::Unknown { return (le, Ty::Unknown); } @@ -3221,7 +4158,12 @@ impl Sema { let l = self.conv_num(le, lt, out); let r = self.conv_num(re, rt, out); ( - HExpr::Bin { op, ty: out, l: Box::new(l), r: Box::new(r) }, + HExpr::Bin { + op, + ty: out, + l: Box::new(l), + r: Box::new(r), + }, ty_of_num(out), ) } @@ -3234,6 +4176,84 @@ impl Sema { pos: SourcePos, scope: &mut Scope, ) -> (HExpr, Ty) { + if (name.contains('.') || name.contains('!')) && !self.is_udt_path(scope, name) { + if name.contains('!') && !name.contains('.') { + return match self.object_member_target(name, pos) { + Some((object, info, _)) => { + let Ok(index) = self.object_index(object, args, scope, pos) else { + return (HExpr::Int(0), Ty::Unknown); + }; + let ty = if info.class == ObjectClass::Form { + Ty::Form + } else { + Ty::Control + }; + ( + HExpr::ObjectRef { + object, + index: index.map(Box::new), + class: info.class, + }, + ty, + ) + } + None => (HExpr::Int(0), Ty::Unknown), + }; + } + if args.is_none() { + match self.dynamic_property(scope, name, pos) { + Ok(Some((object, spec))) => { + let ty = Self::property_ty(spec); + return ( + HExpr::DynamicObjectProperty { + object: Box::new(object), + property: spec.name.to_string(), + }, + ty, + ); + } + Err(()) => return (HExpr::Int(0), Ty::Unknown), + Ok(None) => {} + } + } + return match self.object_property(name, pos) { + Some((object, property, spec, _)) => { + let Ok(index) = self.object_index(object, args, scope, pos) else { + return (HExpr::Int(0), Ty::Unknown); + }; + ( + HExpr::ObjectProperty { + object, + index: index.map(Box::new), + property, + ty: self.h_ty(&Self::property_ty(spec)), + }, + Self::property_ty(spec), + ) + } + None => (HExpr::Int(0), Ty::Unknown), + }; + } + if let Some((object, info)) = self.forms.find(name).map(|(id, info)| (id, info.clone())) { + if args.is_none() || info.array { + let Ok(index) = self.object_index(object, args, scope, pos) else { + return (HExpr::Int(0), Ty::Unknown); + }; + let ty = if info.class == ObjectClass::Form { + Ty::Form + } else { + Ty::Control + }; + return ( + HExpr::ObjectRef { + object, + index: index.map(Box::new), + class: info.class, + }, + ty, + ); + } + } let full_name = match suffix { Some(s) => format!("{name}{}", s.as_char()), None => name.to_string(), @@ -3400,7 +4420,12 @@ impl Sema { } // 3. Variable (ggf. implizit deklarieren) let (place, ty) = self.lower_place( - &Expr::Name { name: name.to_string(), suffix: *suffix, args: None, pos }, + &Expr::Name { + name: name.to_string(), + suffix: *suffix, + args: None, + pos, + }, scope, ); let e = match place { @@ -3428,20 +4453,27 @@ impl Sema { self.err(pos, "Argument-count mismatch"); return (HExpr::Lng(0), Ty::Lng); } - let place = if let Expr::Name { name, suffix, args: idx, pos } = &args[0] { + let place = if let Expr::Name { + name, + suffix, + args: idx, + pos, + } = &args[0] + { if idx.as_ref().map(|v| v.is_empty()).unwrap_or(true) { - self.resolve_var(scope, name, suffix, true, *pos).map(|v| HPlace { - base: if v.global { - VarSlot::Global(v.slot) - } else { - VarSlot::Local(v.slot) - }, - base_is_ref: false, - indices: Vec::new(), - fields: Vec::new(), - ty: self.h_ty(&v.ty), - array_elem: None, - }) + self.resolve_var(scope, name, suffix, true, *pos) + .map(|v| HPlace { + base: if v.global { + VarSlot::Global(v.slot) + } else { + VarSlot::Local(v.slot) + }, + base_is_ref: false, + indices: Vec::new(), + fields: Vec::new(), + ty: self.h_ty(&v.ty), + array_elem: None, + }) } else { None } @@ -3470,7 +4502,8 @@ impl Sema { } } - let lowered = self.check_and_lower_builtin_args(full_name, args, min, max, spec, scope, pos); + let lowered = + self.check_and_lower_builtin_args(full_name, args, min, max, spec, scope, pos); let get = |i: usize| -> Option<&(HExpr, Ty)> { lowered.get(i) }; let take = |l: &[(HExpr, Ty)], i: usize| -> (HExpr, Ty) { l.get(i).cloned().unwrap_or((HExpr::Int(0), Ty::Unknown)) @@ -3583,7 +4616,11 @@ impl Sema { "CDBL" => NumTy::Dbl, _ => NumTy::Cur, }; - let e = if t == Ty::Unknown { e } else { self.conv_num(e, &t, to) }; + let e = if t == Ty::Unknown { + e + } else { + self.conv_num(e, &t, to) + }; (e, ty_of_num(to)) } // --- Mathematik --- @@ -3595,7 +4632,11 @@ impl Sema { _ => Builtin::IntF, }; // Ergebnistyp = Operandentyp (Vorbild). - let rt = if t == Ty::Unknown { Ty::Sng } else { ty_of_num(num_ty(&t)) }; + let rt = if t == Ty::Unknown { + Ty::Sng + } else { + ty_of_num(num_ty(&t)) + }; b(bt, vec![e], rt, self) } "SGN" => b(Builtin::Sgn, vec![take(&lowered, 0).0], Ty::Int, self), @@ -3880,6 +4921,7 @@ fn stmt_pos(stmt: &Stmt) -> SourcePos { | Stmt::EventControl { pos, .. } | Stmt::Include { pos, .. } | Stmt::MetaArrays { pos, .. } + | Stmt::MetaForm { pos } | Stmt::End(pos) | Stmt::StopStmt(pos) | Stmt::System(pos) => *pos, @@ -3891,7 +4933,8 @@ use crate::hir::literal_value; #[cfg(test)] mod tests { - use crate::analyze_source; + use crate::forms::{FormCatalog, ObjectClass}; + use crate::{analyze_source, analyze_source_with_forms}; fn diags(src: &str) -> Vec { analyze_source("TEST", src) @@ -3911,8 +4954,7 @@ mod tests { #[test] fn option_explicit() { - assert!(diags("OPTION EXPLICIT\nx = 1") - .contains(&"Variable not defined".to_string())); + assert!(diags("OPTION EXPLICIT\nx = 1").contains(&"Variable not defined".to_string())); assert!(diags("OPTION EXPLICIT\nDIM x AS INTEGER\nx = 1").is_empty()); } @@ -3930,8 +4972,7 @@ mod tests { #[test] fn argumentanzahl() { - assert!(diags("PRINT LEFT$(\"a\")") - .contains(&"Argument-count mismatch: LEFT$".to_string())); + assert!(diags("PRINT LEFT$(\"a\")").contains(&"Argument-count mismatch: LEFT$".to_string())); assert!(diags("SUB Foo (a%, b%)\nEND SUB\nFoo 1") .contains(&"Argument-count mismatch".to_string())); } @@ -3964,8 +5005,7 @@ mod tests { fn arrays_implizit_und_explizit() { assert!(diags("DIM a(10)\na(1) = 2\nPRINT a(1)").is_empty()); assert!(diags("b(3) = 1").is_empty()); // implizites Array - assert!(diags("DIM c(5)\nDIM c(5)") - .contains(&"Duplicate definition".to_string())); + assert!(diags("DIM c(5)\nDIM c(5)").contains(&"Duplicate definition".to_string())); assert!(diags("REDIM d(5)\nREDIM d(9)").is_empty()); } @@ -3981,15 +5021,12 @@ mod tests { #[test] fn mid_anweisung() { assert!(diags("s$ = \"hallo\"\nMID$(s$, 2, 2) = \"EY\"").is_empty()); - assert!(diags("s$ = \"hallo\"\nMID$(s$, 2) = 5") - .contains(&"Type mismatch".to_string())); + assert!(diags("s$ = \"hallo\"\nMID$(s$, 2) = 5").contains(&"Type mismatch".to_string())); } #[test] fn def_fn_blockform() { - let d = diags( - "DEF FNquad (x)\nFNquad = x * x\nEND DEF\ny = FNQUAD(3)", - ); + let d = diags("DEF FNquad (x)\nFNquad = x * x\nEND DEF\ny = FNQUAD(3)"); assert!(d.is_empty(), "{d:?}"); } @@ -4005,7 +5042,8 @@ mod tests { #[test] fn shared_und_common() { - let src = "DIM SHARED zaehler%\nSUB Hoch\nSHARED zaehler%\nzaehler% = zaehler% + 1\nEND SUB"; + let src = + "DIM SHARED zaehler%\nSUB Hoch\nSHARED zaehler%\nzaehler% = zaehler% + 1\nEND SUB"; assert!(diags(src).is_empty(), "{:?}", diags(src)); assert!(diags("COMMON SHARED /blk/ a%, b$()").is_empty()); } @@ -4013,8 +5051,7 @@ mod tests { #[test] fn konstantenfaltung() { assert!(diags("CONST PI = 3.14159, ZWEI.PI = PI * 2").is_empty()); - assert!(diags("x = 1\nCONST K = x + 1") - .contains(&"Invalid constant".to_string())); + assert!(diags("x = 1\nCONST K = x + 1").contains(&"Invalid constant".to_string())); } #[test] @@ -4024,6 +5061,122 @@ mod tests { assert!(diags("PEN ON").contains(&"Feature unavailable".to_string())); } + fn form_catalog() -> FormCatalog { + let mut c = 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_diags(src: &str) -> Vec { + analyze_source_with_forms("FORM1", src, &form_catalog()) + .diagnostics + .into_iter() + .map(|d| d.message) + .collect() + } + + #[test] + fn objektpfade_werden_namentlich_geprueft_und_udt_bleibt_unveraendert() { + assert!(diags("Text9.Text = \"a\"") + .iter() + .any(|d| d.contains("TEXT9"))); + assert!(form_diags("Text1.Farbe = 3") + .iter() + .any(|d| d.contains("FARBE"))); + assert!(form_diags("Text1.Text = 5") + .iter() + .any(|d| d.contains("TEXT"))); + assert!(form_diags("x = Text1.ListCount") + .iter() + .any(|d| d.contains("LISTCOUNT"))); + assert!(diags("TYPE T\nF AS INTEGER\nEND TYPE\nDIM x AS T\nx.F = 1").is_empty()); + } + + #[test] + fn eigenschaftstyp_schreibbarkeit_bang_und_hir_index() { + let d = form_diags("Text1.Text = 5\nx = Text1.SelLength"); + assert!( + d.iter() + .any(|m| m.contains("Type mismatch for property 'TEXT'")), + "{d:?}" + ); + let d = form_diags("SCREEN.Width = 80"); + assert!(d.iter().any(|m| m.contains("read-only")), "{d:?}"); + assert!(form_diags("Form1!Text1.Text = \"a\"").is_empty()); + let d = form_diags("Text1 = \"x\""); + assert!( + d.iter().any(|m| m.contains("Text1") || m.contains("TEXT1")), + "{d:?}" + ); + assert!(form_diags("Command1(3).Caption = \"drei\"").is_empty()); + assert!( + form_diags("CALL Aus(Text1)\nSUB Aus(c AS CONTROL)\nc.Visible = 0\nEND SUB").is_empty() + ); + let a = analyze_source_with_forms("FORM1", "Text1.Text = \"a\"", &form_catalog()); + let h = a.hir.unwrap(); + assert!(matches!( + h.procs[0].body[0].kind, + HStmtKind::SetObjectProperty { object: 1, .. } + )); + let d = form_diags("Text1.Move 1, 2"); + assert!( + d.iter() + .any(|m| m.contains("Feature unavailable: TEXTBOX.MOVE")), + "{d:?}" + ); + for src in ["Form1.Hide 1", "Form1.Show 1, 2", "SCREEN.Show 1"] { + let d = form_diags(src); + assert!( + d.iter().any(|m| m.contains("Argument-count mismatch")), + "{src}: {d:?}" + ); + } + let d = form_diags("Form1.Show \"modal\""); + assert!(d.iter().any(|m| m.contains("Type mismatch")), "{d:?}"); + } + + #[test] + fn typeof_form_control_und_form_metabefehl() { + let d = form_diags("DIM Ziel AS CONTROL\nIF TYPEOF Ziel IS CommandButton THEN PRINT 1"); + assert!(d.is_empty(), "{d:?}"); + let a = analyze_source("MeinForm", "'$FORM\nMeinForm.Caption = \"x\""); + assert!(a.diagnostics.is_empty(), "{:?}", a.diagnostics); + assert!(a + .hir + .unwrap() + .objects + .iter() + .any(|o| o.name == "MEINFORM" && o.class == ObjectClass::Form)); + } + + #[test] + fn ereignisprozedur_signatur_und_normale_unterstrich_sub() { + let ok = "SUB Form_MouseDown(Button AS INTEGER, Shift AS INTEGER, X AS SINGLE, Y AS SINGLE)\nEND SUB"; + assert!(form_diags(ok).is_empty(), "{:?}", form_diags(ok)); + let bad = form_diags("SUB Form_MouseDown(X AS SINGLE)\nEND SUB"); + assert!( + bad.iter().any(|d| d.contains("BUTTON AS INTEGER")), + "{bad:?}" + ); + assert!(form_diags("SUB Zins_Berechnen(n AS INTEGER)\nEND SUB").is_empty()); + let array_bad = form_diags("SUB Command1_Click()\nEND SUB"); + assert!( + array_bad.iter().any(|d| d.contains("INDEX AS INTEGER")), + "{array_bad:?}" + ); + } + + #[test] + fn dreizeiler_wird_namentlich_statt_falsch_abgewiesen() { + let d = diags("Text1.Text = \"hallo\"\nForm1.Show 1\nUNLOAD Form1"); + assert_eq!(d.len(), 3, "{d:?}"); + assert!(d[0].contains("TEXT1")); + assert!(d[1].contains("FORM1")); + assert!(d[2].contains("FORM1")); + } + // ---- HIR-Lowering ------------------------------------------------------ use crate::hir::{HExpr, HStmtKind, NumTy}; @@ -4048,7 +5201,14 @@ mod tests { }; assert_eq!(*ty, NumTy::Dbl); assert!( - matches!(**l, HExpr::Conv { from: NumTy::Int, to: NumTy::Dbl, .. }), + matches!( + **l, + HExpr::Conv { + from: NumTy::Int, + to: NumTy::Dbl, + .. + } + ), "Conv INTEGER→DOUBLE erwartet: {l:?}" ); } @@ -4197,15 +5357,16 @@ mod tests { /// nennt keine Obergrenze, also darf die Signatur auch keine setzen. #[test] fn createindex_ohne_obergrenze_der_spaltenzahl() { - let felder: String = - (1..=12).map(|i| format!(" F{i} AS LONG\n")).collect(); - let spalten: String = - (1..=12).map(|i| format!(", \"F{i}\"")).collect(); + let felder: String = (1..=12).map(|i| format!(" F{i} AS LONG\n")).collect(); + let spalten: String = (1..=12).map(|i| format!(", \"F{i}\"")).collect(); let quelle = format!( "TYPE Breit\n{felder}END TYPE\n DIM b AS Breit\n OPEN \"db\" FOR ISAM Breit \"Tab\" AS #1\n CREATEINDEX #1, \"Viel\", 0{spalten}" ); let d = diags(&quelle); - assert!(d.is_empty(), "zwölfspaltiger Index muss zulässig sein: {d:?}"); + assert!( + d.is_empty(), + "zwölfspaltiger Index muss zulässig sein: {d:?}" + ); // Ein Spaltenname muss trotzdem ein String sein. let quelle = format!( "TYPE Breit\n{felder}END TYPE\n DIM b AS Breit\n OPEN \"db\" FOR ISAM Breit \"Tab\" AS #1\n CREATEINDEX #1, \"Viel\", 0, \"F1\", 42" diff --git a/crates/tb-frontend/tests/inventar.rs b/crates/tb-frontend/tests/inventar.rs index d84fd9d..9b681f6 100644 --- a/crates/tb-frontend/tests/inventar.rs +++ b/crates/tb-frontend/tests/inventar.rs @@ -31,6 +31,7 @@ struct Eintrag { art: String, status: String, fundstelle: String, + quelle: String, zeile: usize, } @@ -61,6 +62,7 @@ fn inventar() -> Vec { 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 { + 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(), diff --git a/crates/tb-runtime/src/builtins.rs b/crates/tb-runtime/src/builtins.rs index 8a574d3..d18ab8c 100644 --- a/crates/tb-runtime/src/builtins.rs +++ b/crates/tb-runtime/src/builtins.rs @@ -33,6 +33,9 @@ pub struct RtState { /// wird nur für die benutzerdefinierten Trap-Tasten gebraucht; /// `INKEY$` und `INPUT` sehen ihn nicht. pub tasten: VecDeque<(String, u8)>, + /// Mausereignisse für den Forms-Dispatch; sie werden erst an den + /// bestehenden Zustellpunkten von der VM entnommen. + pub maus: VecDeque, /// Abbruchwunsch (Strg+Untbr) wurde gemeldet. pub abbruch: bool, /// Eingabeende wurde gemeldet. @@ -70,6 +73,7 @@ impl Default for RtState { isam: Default::default(), print: PrintState::default(), tasten: VecDeque::new(), + maus: VecDeque::new(), abbruch: false, ende: false, // Das Vorbild belegt die Makros im Interpreter vor; ein @@ -115,10 +119,7 @@ impl RtState { self.abbruch = true; } } - // ponytail: Mausereignisse bekommen ihren Verbraucher erst - // mit dem Forms-Objektmodell. Bis dahin werden sie hier - // verworfen, damit die Warteschlange nicht wächst. - Ereignis::Maus(_) => {} + Ereignis::Maus(m) => self.maus.push_back(m), } if blockierend { return; @@ -629,13 +630,21 @@ fn s_ok(s: String) -> Result, RuntimeError> { // ---- Strings ---------------------------------------------------------------- -fn bi_len(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result, RuntimeError> { +fn bi_len( + _: &mut RtState, + _: &mut dyn Host, + a: &mut [Value], +) -> Result, RuntimeError> { let s = arg_str(a, 0)?; let n = s.chars().count(); Ok(Some(Value::Int(i16::try_from(n).unwrap_or(i16::MAX)))) } -fn bi_left(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result, RuntimeError> { +fn bi_left( + _: &mut RtState, + _: &mut dyn Host, + a: &mut [Value], +) -> Result, RuntimeError> { let s = arg_str(a, 0)?; let n = arg_i32(a, 1)?; if n < 0 { @@ -644,7 +653,11 @@ fn bi_left(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result Result, RuntimeError> { +fn bi_right( + _: &mut RtState, + _: &mut dyn Host, + a: &mut [Value], +) -> Result, RuntimeError> { let s = arg_str(a, 0)?; let n = arg_i32(a, 1)?; if n < 0 { @@ -655,7 +668,11 @@ fn bi_right(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result