Phase 4: Formularmodell und Objektsprache
This commit is contained in:
@@ -14,6 +14,8 @@ pub enum TypeName {
|
||||
Currency,
|
||||
Str,
|
||||
FixedStr(i64),
|
||||
Form,
|
||||
Control,
|
||||
Udt(String),
|
||||
}
|
||||
|
||||
@@ -25,9 +27,24 @@ pub enum UnOp {
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BinOp {
|
||||
Pow, Mul, Div, IntDiv, Mod, Add, Sub,
|
||||
Eq, Ne, Lt, Le, Gt, Ge,
|
||||
And, Or, Xor, Eqv, Imp,
|
||||
Pow,
|
||||
Mul,
|
||||
Div,
|
||||
IntDiv,
|
||||
Mod,
|
||||
Add,
|
||||
Sub,
|
||||
Eq,
|
||||
Ne,
|
||||
Lt,
|
||||
Le,
|
||||
Gt,
|
||||
Ge,
|
||||
And,
|
||||
Or,
|
||||
Xor,
|
||||
Eqv,
|
||||
Imp,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@@ -46,11 +63,26 @@ pub enum Expr {
|
||||
args: Option<Vec<Expr>>,
|
||||
pos: SourcePos,
|
||||
},
|
||||
Unary { op: UnOp, operand: Box<Expr>, pos: SourcePos },
|
||||
Binary { op: BinOp, lhs: Box<Expr>, rhs: Box<Expr>, pos: SourcePos },
|
||||
Unary {
|
||||
op: UnOp,
|
||||
operand: Box<Expr>,
|
||||
pos: SourcePos,
|
||||
},
|
||||
Binary {
|
||||
op: BinOp,
|
||||
lhs: Box<Expr>,
|
||||
rhs: Box<Expr>,
|
||||
pos: SourcePos,
|
||||
},
|
||||
/// Geklammerter Ausdruck. Semantisch transparent, aber als Argument
|
||||
/// erzwingt die Klammer Wertübergabe (BYVAL) statt BYREF.
|
||||
Paren(Box<Expr>),
|
||||
/// `TYPEOF ausdruck IS klasse`.
|
||||
TypeOf {
|
||||
value: Box<Expr>,
|
||||
class: String,
|
||||
pos: SourcePos,
|
||||
},
|
||||
/// Ausgelassenes Argument (`LOCATE , 5`).
|
||||
Missing,
|
||||
}
|
||||
@@ -60,7 +92,8 @@ impl Expr {
|
||||
match self {
|
||||
Expr::Name { pos, .. }
|
||||
| Expr::Unary { pos, .. }
|
||||
| Expr::Binary { pos, .. } => *pos,
|
||||
| Expr::Binary { pos, .. }
|
||||
| Expr::TypeOf { pos, .. } => *pos,
|
||||
Expr::Paren(e) => e.pos(),
|
||||
_ => SourcePos::default(),
|
||||
}
|
||||
@@ -202,7 +235,11 @@ pub enum EventAction {
|
||||
pub enum Stmt {
|
||||
Label(String),
|
||||
LineNumber(u32),
|
||||
Assign { target: Expr, value: Expr, pos: SourcePos },
|
||||
Assign {
|
||||
target: Expr,
|
||||
value: Expr,
|
||||
pos: SourcePos,
|
||||
},
|
||||
Print {
|
||||
/// LPRINT (Druckerausgabe) statt PRINT.
|
||||
printer: bool,
|
||||
@@ -226,7 +263,11 @@ pub enum Stmt {
|
||||
else_body: Option<Vec<Stmt>>,
|
||||
pos: SourcePos,
|
||||
},
|
||||
Select { expr: Expr, arms: Vec<CaseArm>, pos: SourcePos },
|
||||
Select {
|
||||
expr: Expr,
|
||||
arms: Vec<CaseArm>,
|
||||
pos: SourcePos,
|
||||
},
|
||||
For {
|
||||
var: Expr,
|
||||
from: Expr,
|
||||
@@ -236,25 +277,57 @@ pub enum Stmt {
|
||||
pos: SourcePos,
|
||||
},
|
||||
DoLoop {
|
||||
pre: Option<(bool, Expr)>, // (ist UNTIL, Bedingung)
|
||||
pre: Option<(bool, Expr)>, // (ist UNTIL, Bedingung)
|
||||
post: Option<(bool, Expr)>,
|
||||
body: Vec<Stmt>,
|
||||
pos: SourcePos,
|
||||
},
|
||||
While { cond: Expr, body: Vec<Stmt>, pos: SourcePos },
|
||||
Goto { target: LabelRef, pos: SourcePos },
|
||||
Gosub { target: LabelRef, pos: SourcePos },
|
||||
OnGoto { expr: Expr, targets: Vec<LabelRef>, gosub: bool, pos: SourcePos },
|
||||
Return { target: Option<LabelRef>, pos: SourcePos },
|
||||
While {
|
||||
cond: Expr,
|
||||
body: Vec<Stmt>,
|
||||
pos: SourcePos,
|
||||
},
|
||||
Goto {
|
||||
target: LabelRef,
|
||||
pos: SourcePos,
|
||||
},
|
||||
Gosub {
|
||||
target: LabelRef,
|
||||
pos: SourcePos,
|
||||
},
|
||||
OnGoto {
|
||||
expr: Expr,
|
||||
targets: Vec<LabelRef>,
|
||||
gosub: bool,
|
||||
pos: SourcePos,
|
||||
},
|
||||
Return {
|
||||
target: Option<LabelRef>,
|
||||
pos: SourcePos,
|
||||
},
|
||||
End(SourcePos),
|
||||
StopStmt(SourcePos),
|
||||
System(SourcePos),
|
||||
Exit { kind: ExitKind, pos: SourcePos },
|
||||
Dim { shared: bool, redim: bool, decls: Vec<VarDecl>, pos: SourcePos },
|
||||
Exit {
|
||||
kind: ExitKind,
|
||||
pos: SourcePos,
|
||||
},
|
||||
Dim {
|
||||
shared: bool,
|
||||
redim: bool,
|
||||
decls: Vec<VarDecl>,
|
||||
pos: SourcePos,
|
||||
},
|
||||
/// `SHARED`-Anweisung in einer Prozedur (Zugriff auf Modulvariablen).
|
||||
SharedDecl { decls: Vec<VarDecl>, pos: SourcePos },
|
||||
SharedDecl {
|
||||
decls: Vec<VarDecl>,
|
||||
pos: SourcePos,
|
||||
},
|
||||
/// `STATIC`-Anweisung in einer Prozedur.
|
||||
StaticDecl { decls: Vec<VarDecl>, pos: SourcePos },
|
||||
StaticDecl {
|
||||
decls: Vec<VarDecl>,
|
||||
pos: SourcePos,
|
||||
},
|
||||
/// `COMMON [SHARED] [/block/] liste`.
|
||||
CommonDecl {
|
||||
shared: bool,
|
||||
@@ -262,20 +335,64 @@ pub enum Stmt {
|
||||
decls: Vec<VarDecl>,
|
||||
pos: SourcePos,
|
||||
},
|
||||
Erase { names: Vec<Expr>, pos: SourcePos },
|
||||
ConstDecl { items: Vec<(String, Option<Suffix>, Expr)>, pos: SourcePos },
|
||||
DefType { ty: TypeName, ranges: Vec<(char, char)>, pos: SourcePos },
|
||||
OptionStmt { kind: OptionKind, pos: SourcePos },
|
||||
TypeDecl { name: String, fields: Vec<(String, TypeName)>, pos: SourcePos },
|
||||
Declare { sig: ProcSig, pos: SourcePos },
|
||||
Erase {
|
||||
names: Vec<Expr>,
|
||||
pos: SourcePos,
|
||||
},
|
||||
ConstDecl {
|
||||
items: Vec<(String, Option<Suffix>, Expr)>,
|
||||
pos: SourcePos,
|
||||
},
|
||||
DefType {
|
||||
ty: TypeName,
|
||||
ranges: Vec<(char, char)>,
|
||||
pos: SourcePos,
|
||||
},
|
||||
OptionStmt {
|
||||
kind: OptionKind,
|
||||
pos: SourcePos,
|
||||
},
|
||||
TypeDecl {
|
||||
name: String,
|
||||
fields: Vec<(String, TypeName)>,
|
||||
pos: SourcePos,
|
||||
},
|
||||
Declare {
|
||||
sig: ProcSig,
|
||||
pos: SourcePos,
|
||||
},
|
||||
/// Expliziter oder impliziter Prozedur-/Builtin-Aufruf als Anweisung.
|
||||
Call { name: String, suffix: Option<Suffix>, args: Vec<Expr>, pos: SourcePos },
|
||||
OnError { local: bool, action: OnErrorAction, pos: SourcePos },
|
||||
Resume { kind: ResumeKind, pos: SourcePos },
|
||||
ErrorStmt { code: Expr, pos: SourcePos },
|
||||
Data { items: Vec<String>, pos: SourcePos },
|
||||
ReadStmt { vars: Vec<Expr>, pos: SourcePos },
|
||||
Restore { target: Option<LabelRef>, pos: SourcePos },
|
||||
Call {
|
||||
name: String,
|
||||
suffix: Option<Suffix>,
|
||||
args: Vec<Expr>,
|
||||
pos: SourcePos,
|
||||
},
|
||||
OnError {
|
||||
local: bool,
|
||||
action: OnErrorAction,
|
||||
pos: SourcePos,
|
||||
},
|
||||
Resume {
|
||||
kind: ResumeKind,
|
||||
pos: SourcePos,
|
||||
},
|
||||
ErrorStmt {
|
||||
code: Expr,
|
||||
pos: SourcePos,
|
||||
},
|
||||
Data {
|
||||
items: Vec<String>,
|
||||
pos: SourcePos,
|
||||
},
|
||||
ReadStmt {
|
||||
vars: Vec<Expr>,
|
||||
pos: SourcePos,
|
||||
},
|
||||
Restore {
|
||||
target: Option<LabelRef>,
|
||||
pos: SourcePos,
|
||||
},
|
||||
/// Einzeilige `DEF FNname(...) = ausdruck`-Definition.
|
||||
DefFn {
|
||||
name: String,
|
||||
@@ -312,8 +429,15 @@ pub enum Stmt {
|
||||
len: Option<Expr>,
|
||||
pos: SourcePos,
|
||||
},
|
||||
CloseStmt { files: Vec<Expr>, pos: SourcePos },
|
||||
FieldStmt { file: Expr, fields: Vec<(Expr, Expr)>, pos: SourcePos },
|
||||
CloseStmt {
|
||||
files: Vec<Expr>,
|
||||
pos: SourcePos,
|
||||
},
|
||||
FieldStmt {
|
||||
file: Expr,
|
||||
fields: Vec<(Expr, Expr)>,
|
||||
pos: SourcePos,
|
||||
},
|
||||
GetPut {
|
||||
put: bool,
|
||||
file: Expr,
|
||||
@@ -321,9 +445,22 @@ pub enum Stmt {
|
||||
var: Option<Expr>,
|
||||
pos: SourcePos,
|
||||
},
|
||||
LsetRset { rset: bool, target: Expr, value: Expr, pos: SourcePos },
|
||||
WriteStmt { file: Option<Expr>, items: Vec<Expr>, pos: SourcePos },
|
||||
SeekStmt { file: Expr, position: Expr, pos: SourcePos },
|
||||
LsetRset {
|
||||
rset: bool,
|
||||
target: Expr,
|
||||
value: Expr,
|
||||
pos: SourcePos,
|
||||
},
|
||||
WriteStmt {
|
||||
file: Option<Expr>,
|
||||
items: Vec<Expr>,
|
||||
pos: SourcePos,
|
||||
},
|
||||
SeekStmt {
|
||||
file: Expr,
|
||||
position: Expr,
|
||||
pos: SourcePos,
|
||||
},
|
||||
LockStmt {
|
||||
unlock: bool,
|
||||
file: Expr,
|
||||
@@ -362,9 +499,18 @@ pub enum Stmt {
|
||||
},
|
||||
// ---- Metabefehle ----
|
||||
/// `'$INCLUDE: 'datei''` — Auflösung übernimmt der Compile-Treiber.
|
||||
Include { path: String, pos: SourcePos },
|
||||
Include {
|
||||
path: String,
|
||||
pos: SourcePos,
|
||||
},
|
||||
/// `'$STATIC` / `'$DYNAMIC`.
|
||||
MetaArrays { static_arrays: bool, pos: SourcePos },
|
||||
MetaArrays {
|
||||
static_arrays: bool,
|
||||
pos: SourcePos,
|
||||
},
|
||||
MetaForm {
|
||||
pos: SourcePos,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
|
||||
631
crates/tb-frontend/src/forms.rs
Normal file
631
crates/tb-frontend/src/forms.rs
Normal file
@@ -0,0 +1,631 @@
|
||||
//! Statische Forms-Typinformation. Namen werden beim Übersetzen in diese
|
||||
//! Tabellenindizes aufgelöst; die Laufzeit muss keine Strings durchsuchen.
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum ObjectClass {
|
||||
Form,
|
||||
CheckBox,
|
||||
ComboBox,
|
||||
CommandButton,
|
||||
DirListBox,
|
||||
DriveListBox,
|
||||
FileListBox,
|
||||
Frame,
|
||||
HScrollBar,
|
||||
Label,
|
||||
ListBox,
|
||||
Menu,
|
||||
OptionButton,
|
||||
PictureBox,
|
||||
TextBox,
|
||||
Timer,
|
||||
VScrollBar,
|
||||
Screen,
|
||||
}
|
||||
|
||||
impl ObjectClass {
|
||||
pub const ALL: [Self; 18] = [
|
||||
Self::Form,
|
||||
Self::CheckBox,
|
||||
Self::ComboBox,
|
||||
Self::CommandButton,
|
||||
Self::DirListBox,
|
||||
Self::DriveListBox,
|
||||
Self::FileListBox,
|
||||
Self::Frame,
|
||||
Self::HScrollBar,
|
||||
Self::Label,
|
||||
Self::ListBox,
|
||||
Self::Menu,
|
||||
Self::OptionButton,
|
||||
Self::PictureBox,
|
||||
Self::TextBox,
|
||||
Self::Timer,
|
||||
Self::VScrollBar,
|
||||
Self::Screen,
|
||||
];
|
||||
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Form => "FORM",
|
||||
Self::CheckBox => "CHECKBOX",
|
||||
Self::ComboBox => "COMBOBOX",
|
||||
Self::CommandButton => "COMMANDBUTTON",
|
||||
Self::DirListBox => "DIRLISTBOX",
|
||||
Self::DriveListBox => "DRIVELISTBOX",
|
||||
Self::FileListBox => "FILELISTBOX",
|
||||
Self::Frame => "FRAME",
|
||||
Self::HScrollBar => "HSCROLLBAR",
|
||||
Self::Label => "LABEL",
|
||||
Self::ListBox => "LISTBOX",
|
||||
Self::Menu => "MENU",
|
||||
Self::OptionButton => "OPTIONBUTTON",
|
||||
Self::PictureBox => "PICTUREBOX",
|
||||
Self::TextBox => "TEXTBOX",
|
||||
Self::Timer => "TIMER",
|
||||
Self::VScrollBar => "VSCROLLBAR",
|
||||
Self::Screen => "SCREEN",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(name: &str) -> Option<Self> {
|
||||
Self::ALL
|
||||
.into_iter()
|
||||
.find(|c| c.name().eq_ignore_ascii_case(name))
|
||||
}
|
||||
|
||||
pub fn id(self) -> u8 {
|
||||
Self::ALL.iter().position(|c| *c == self).unwrap() as u8
|
||||
}
|
||||
|
||||
pub fn from_id(id: u8) -> Option<Self> {
|
||||
Self::ALL.get(id as usize).copied()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PropertyType {
|
||||
Integer,
|
||||
Single,
|
||||
String,
|
||||
Boolean,
|
||||
Object,
|
||||
IntegerArray,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum PropertyDefault {
|
||||
Integer(i32),
|
||||
Single(f32),
|
||||
String(&'static str),
|
||||
Boolean(bool),
|
||||
Empty,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct PropertySpec {
|
||||
pub name: &'static str,
|
||||
pub ty: PropertyType,
|
||||
pub default: PropertyDefault,
|
||||
pub min: Option<i32>,
|
||||
pub max: Option<i32>,
|
||||
pub writable: bool,
|
||||
}
|
||||
|
||||
const fn int(name: &'static str, default: i32) -> PropertySpec {
|
||||
PropertySpec {
|
||||
name,
|
||||
ty: PropertyType::Integer,
|
||||
default: PropertyDefault::Integer(default),
|
||||
min: None,
|
||||
max: None,
|
||||
writable: true,
|
||||
}
|
||||
}
|
||||
const fn range(name: &'static str, default: i32, min: i32, max: i32) -> PropertySpec {
|
||||
PropertySpec {
|
||||
name,
|
||||
ty: PropertyType::Integer,
|
||||
default: PropertyDefault::Integer(default),
|
||||
min: Some(min),
|
||||
max: Some(max),
|
||||
writable: true,
|
||||
}
|
||||
}
|
||||
const fn boolp(name: &'static str, default: bool) -> PropertySpec {
|
||||
PropertySpec {
|
||||
name,
|
||||
ty: PropertyType::Boolean,
|
||||
default: PropertyDefault::Boolean(default),
|
||||
min: None,
|
||||
max: None,
|
||||
writable: true,
|
||||
}
|
||||
}
|
||||
const fn string(name: &'static str, default: &'static str) -> PropertySpec {
|
||||
PropertySpec {
|
||||
name,
|
||||
ty: PropertyType::String,
|
||||
default: PropertyDefault::String(default),
|
||||
min: None,
|
||||
max: None,
|
||||
writable: true,
|
||||
}
|
||||
}
|
||||
const fn ro(name: &'static str, ty: PropertyType) -> PropertySpec {
|
||||
PropertySpec {
|
||||
name,
|
||||
ty,
|
||||
default: PropertyDefault::Empty,
|
||||
min: None,
|
||||
max: None,
|
||||
writable: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Eigenschaftstabelle in stabiler Reihenfolge. Der Vektor ist klein und wird
|
||||
/// nur beim Übersetzen bzw. Erzeugen eines Objekts aufgebaut.
|
||||
pub fn properties(class: ObjectClass) -> Vec<PropertySpec> {
|
||||
use ObjectClass::*;
|
||||
let mut p = Vec::new();
|
||||
if !matches!(class, Timer | Menu | Screen) {
|
||||
p.extend([
|
||||
range("BACKCOLOR", 7, 0, 15),
|
||||
boolp("ENABLED", true),
|
||||
range("HEIGHT", 1, 1, 254),
|
||||
range("LEFT", 0, 0, 254),
|
||||
range("MOUSEPOINTER", 0, 0, 12),
|
||||
ro("PARENT", PropertyType::Object),
|
||||
string("TAG", ""),
|
||||
range("TOP", 0, 0, 254),
|
||||
boolp("VISIBLE", true),
|
||||
range("WIDTH", 1, 1, 254),
|
||||
]);
|
||||
}
|
||||
if !matches!(
|
||||
class,
|
||||
Form | Frame | Label | PictureBox | Timer | Menu | Screen
|
||||
) {
|
||||
p.extend([int("INDEX", 0), int("TABINDEX", 0), boolp("TABSTOP", true)]);
|
||||
}
|
||||
if !matches!(class, Form | Timer | Menu | Screen) {
|
||||
p.push(string("CTLNAME", ""));
|
||||
}
|
||||
if matches!(
|
||||
class,
|
||||
Form | TextBox
|
||||
| ListBox
|
||||
| ComboBox
|
||||
| CheckBox
|
||||
| OptionButton
|
||||
| Label
|
||||
| HScrollBar
|
||||
| VScrollBar
|
||||
| PictureBox
|
||||
) {
|
||||
p.push(range("FORECOLOR", 0, 0, 15));
|
||||
}
|
||||
if matches!(
|
||||
class,
|
||||
Form | CommandButton
|
||||
| TextBox
|
||||
| ListBox
|
||||
| ComboBox
|
||||
| CheckBox
|
||||
| OptionButton
|
||||
| Frame
|
||||
| Label
|
||||
| HScrollBar
|
||||
| VScrollBar
|
||||
| PictureBox
|
||||
) {
|
||||
p.push(range("DRAGMODE", 0, 0, 1));
|
||||
}
|
||||
match class {
|
||||
Form => p.extend([
|
||||
boolp("AUTOREDRAW", false),
|
||||
range("BORDERSTYLE", 2, 0, 6),
|
||||
string("CAPTION", ""),
|
||||
boolp("CONTROLBOX", true),
|
||||
int("CURRENTX", 0),
|
||||
int("CURRENTY", 0),
|
||||
string("FORMNAME", ""),
|
||||
range("FORMTYPE", 0, 0, 1),
|
||||
boolp("MAXBUTTON", true),
|
||||
boolp("MINBUTTON", true),
|
||||
ro("SCALEHEIGHT", PropertyType::Integer),
|
||||
ro("SCALEWIDTH", PropertyType::Integer),
|
||||
range("WINDOWSTATE", 0, 0, 2),
|
||||
]),
|
||||
CommandButton => p.extend([
|
||||
boolp("CANCEL", false),
|
||||
string("CAPTION", ""),
|
||||
boolp("DEFAULT", false),
|
||||
boolp("VALUE", false),
|
||||
]),
|
||||
TextBox => p.extend([
|
||||
range("BORDERSTYLE", 1, 0, 1),
|
||||
boolp("MULTILINE", false),
|
||||
range("SCROLLBARS", 0, 0, 3),
|
||||
int("SELLENGTH", 0),
|
||||
int("SELSTART", 0),
|
||||
string("SELTEXT", ""),
|
||||
string("TEXT", ""),
|
||||
]),
|
||||
ListBox => p.extend([
|
||||
ro("LIST", PropertyType::String),
|
||||
ro("LISTCOUNT", PropertyType::Integer),
|
||||
int("LISTINDEX", -1),
|
||||
boolp("SORTED", false),
|
||||
string("TEXT", ""),
|
||||
]),
|
||||
ComboBox => p.extend([
|
||||
ro("LIST", PropertyType::String),
|
||||
ro("LISTCOUNT", PropertyType::Integer),
|
||||
int("LISTINDEX", -1),
|
||||
boolp("SORTED", false),
|
||||
int("SELLENGTH", 0),
|
||||
int("SELSTART", 0),
|
||||
string("SELTEXT", ""),
|
||||
range("STYLE", 0, 0, 2),
|
||||
string("TEXT", ""),
|
||||
]),
|
||||
CheckBox => p.extend([string("CAPTION", ""), range("VALUE", 0, 0, 2)]),
|
||||
OptionButton => p.extend([string("CAPTION", ""), range("VALUE", 0, -1, 0)]),
|
||||
Frame => p.push(string("CAPTION", "")),
|
||||
Label => p.extend([
|
||||
range("ALIGNMENT", 0, 0, 2),
|
||||
boolp("AUTOSIZE", false),
|
||||
range("BORDERSTYLE", 0, 0, 2),
|
||||
string("CAPTION", ""),
|
||||
]),
|
||||
HScrollBar | VScrollBar => p.extend([
|
||||
ro("ATTACHED", PropertyType::Boolean),
|
||||
range("LARGECHANGE", 1, 1, 32767),
|
||||
range("MIN", 0, -32768, 32767),
|
||||
range("MAX", 32767, -32768, 32767),
|
||||
range("SMALLCHANGE", 1, 1, 32767),
|
||||
range("VALUE", 0, -32768, 32767),
|
||||
]),
|
||||
PictureBox => p.extend([
|
||||
boolp("AUTOREDRAW", false),
|
||||
range("BORDERSTYLE", 1, 0, 2),
|
||||
int("CURRENTX", 0),
|
||||
int("CURRENTY", 0),
|
||||
ro("SCALEHEIGHT", PropertyType::Integer),
|
||||
ro("SCALEWIDTH", PropertyType::Integer),
|
||||
]),
|
||||
Timer => p.extend([
|
||||
string("CTLNAME", ""),
|
||||
boolp("ENABLED", true),
|
||||
int("INDEX", 0),
|
||||
range("INTERVAL", 0, 0, 65535),
|
||||
ro("PARENT", PropertyType::Object),
|
||||
string("TAG", ""),
|
||||
]),
|
||||
Menu => p.extend([
|
||||
string("CAPTION", ""),
|
||||
boolp("CHECKED", false),
|
||||
string("CTLNAME", ""),
|
||||
boolp("ENABLED", true),
|
||||
int("INDEX", 0),
|
||||
ro("PARENT", PropertyType::Object),
|
||||
boolp("SEPARATOR", false),
|
||||
string("TAG", ""),
|
||||
boolp("VISIBLE", true),
|
||||
]),
|
||||
DirListBox => p.extend([string("PATH", ""), string("TEXT", "")]),
|
||||
DriveListBox => p.extend([string("DRIVE", ""), string("TEXT", "")]),
|
||||
FileListBox => p.extend([
|
||||
string("FILENAME", ""),
|
||||
string("PATH", ""),
|
||||
string("PATTERN", "*.*"),
|
||||
boolp("ARCHIVE", true),
|
||||
boolp("HIDDEN", false),
|
||||
boolp("NORMAL", true),
|
||||
boolp("READONLY", true),
|
||||
boolp("SYSTEM", false),
|
||||
string("TEXT", ""),
|
||||
]),
|
||||
Screen => p.extend([
|
||||
ro("ACTIVECONTROL", PropertyType::Object),
|
||||
ro("ACTIVEFORM", PropertyType::Object),
|
||||
boolp("AUTOREDRAW", false),
|
||||
PropertySpec {
|
||||
name: "CONTROLPANEL",
|
||||
ty: PropertyType::IntegerArray,
|
||||
default: PropertyDefault::Integer(0),
|
||||
min: Some(0),
|
||||
max: Some(15),
|
||||
writable: true,
|
||||
},
|
||||
ro("HEIGHT", PropertyType::Integer),
|
||||
ro("WIDTH", PropertyType::Integer),
|
||||
range("MOUSEPOINTER", 0, 0, 12),
|
||||
]),
|
||||
}
|
||||
if class == Form {
|
||||
if let Some(visible) = p.iter_mut().find(|p| p.name == "VISIBLE") {
|
||||
visible.default = PropertyDefault::Boolean(false);
|
||||
}
|
||||
}
|
||||
p
|
||||
}
|
||||
|
||||
pub fn property(class: ObjectClass, name: &str) -> Option<(u16, PropertySpec)> {
|
||||
properties(class)
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.find(|(_, p)| p.name.eq_ignore_ascii_case(name))
|
||||
.map(|(i, p)| (i as u16, p))
|
||||
}
|
||||
|
||||
pub fn methods(class: ObjectClass) -> &'static [&'static str] {
|
||||
use ObjectClass::*;
|
||||
match class {
|
||||
Form => &[
|
||||
"CLS",
|
||||
"DRAG",
|
||||
"HIDE",
|
||||
"LOAD",
|
||||
"MOVE",
|
||||
"PRINT",
|
||||
"PRINTFORM",
|
||||
"REFRESH",
|
||||
"SHOW",
|
||||
"TEXTHEIGHT",
|
||||
"TEXTWIDTH",
|
||||
"UNLOAD",
|
||||
],
|
||||
ListBox | ComboBox => &[
|
||||
"ADDITEM",
|
||||
"DRAG",
|
||||
"MOVE",
|
||||
"REFRESH",
|
||||
"REMOVEITEM",
|
||||
"SETFOCUS",
|
||||
],
|
||||
CommandButton | TextBox | CheckBox | OptionButton => {
|
||||
&["DRAG", "MOVE", "REFRESH", "SETFOCUS"]
|
||||
}
|
||||
Frame | Label | HScrollBar | VScrollBar => &["DRAG", "MOVE", "REFRESH"],
|
||||
PictureBox => &[
|
||||
"CLS",
|
||||
"DRAG",
|
||||
"MOVE",
|
||||
"PRINT",
|
||||
"REFRESH",
|
||||
"SETFOCUS",
|
||||
"TEXTHEIGHT",
|
||||
"TEXTWIDTH",
|
||||
],
|
||||
Screen => &["HIDE", "SHOW"],
|
||||
_ => &[],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn method_is_implemented(class: ObjectClass, name: &str) -> bool {
|
||||
matches!(
|
||||
(class, name),
|
||||
(ObjectClass::Form, "HIDE" | "LOAD" | "SHOW" | "UNLOAD")
|
||||
| (ObjectClass::Screen, "HIDE" | "SHOW")
|
||||
)
|
||||
}
|
||||
|
||||
pub fn method_arity(class: ObjectClass, name: &str) -> Option<(usize, usize)> {
|
||||
match (class, name) {
|
||||
(ObjectClass::Form, "SHOW") => Some((0, 1)),
|
||||
(ObjectClass::Form, "HIDE" | "LOAD" | "UNLOAD")
|
||||
| (ObjectClass::Screen, "HIDE" | "SHOW") => Some((0, 0)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn events(class: ObjectClass) -> &'static [&'static str] {
|
||||
use ObjectClass::*;
|
||||
match class {
|
||||
Form => &[
|
||||
"CLICK",
|
||||
"DBLCLICK",
|
||||
"DRAGDROP",
|
||||
"DRAGOVER",
|
||||
"GOTFOCUS",
|
||||
"KEYDOWN",
|
||||
"KEYPRESS",
|
||||
"KEYUP",
|
||||
"LOAD",
|
||||
"LOSTFOCUS",
|
||||
"MOUSEDOWN",
|
||||
"MOUSEMOVE",
|
||||
"MOUSEUP",
|
||||
"PAINT",
|
||||
"RESIZE",
|
||||
"UNLOAD",
|
||||
],
|
||||
CommandButton => &[
|
||||
"CLICK",
|
||||
"DRAGDROP",
|
||||
"DRAGOVER",
|
||||
"GOTFOCUS",
|
||||
"KEYDOWN",
|
||||
"KEYPRESS",
|
||||
"KEYUP",
|
||||
"LOSTFOCUS",
|
||||
],
|
||||
TextBox => &[
|
||||
"CHANGE",
|
||||
"DRAGDROP",
|
||||
"DRAGOVER",
|
||||
"GOTFOCUS",
|
||||
"KEYDOWN",
|
||||
"KEYPRESS",
|
||||
"KEYUP",
|
||||
"LOSTFOCUS",
|
||||
],
|
||||
ListBox => &[
|
||||
"CLICK",
|
||||
"DBLCLICK",
|
||||
"DRAGDROP",
|
||||
"DRAGOVER",
|
||||
"GOTFOCUS",
|
||||
"KEYDOWN",
|
||||
"KEYPRESS",
|
||||
"KEYUP",
|
||||
"LOSTFOCUS",
|
||||
"MOUSEDOWN",
|
||||
"MOUSEMOVE",
|
||||
"MOUSEUP",
|
||||
],
|
||||
ComboBox => &[
|
||||
"CHANGE",
|
||||
"CLICK",
|
||||
"DBLCLICK",
|
||||
"DRAGDROP",
|
||||
"DRAGOVER",
|
||||
"DROPDOWN",
|
||||
"GOTFOCUS",
|
||||
"KEYDOWN",
|
||||
"KEYPRESS",
|
||||
"KEYUP",
|
||||
"LOSTFOCUS",
|
||||
"MOUSEDOWN",
|
||||
"MOUSEMOVE",
|
||||
"MOUSEUP",
|
||||
],
|
||||
CheckBox => &[
|
||||
"CLICK",
|
||||
"DRAGDROP",
|
||||
"DRAGOVER",
|
||||
"GOTFOCUS",
|
||||
"KEYDOWN",
|
||||
"KEYPRESS",
|
||||
"KEYUP",
|
||||
"LOSTFOCUS",
|
||||
],
|
||||
OptionButton => &[
|
||||
"CLICK",
|
||||
"DBLCLICK",
|
||||
"DRAGDROP",
|
||||
"DRAGOVER",
|
||||
"GOTFOCUS",
|
||||
"KEYDOWN",
|
||||
"KEYPRESS",
|
||||
"KEYUP",
|
||||
"LOSTFOCUS",
|
||||
],
|
||||
Frame => &["DRAGDROP", "DRAGOVER"],
|
||||
Label => &[
|
||||
"CHANGE",
|
||||
"CLICK",
|
||||
"DBLCLICK",
|
||||
"DRAGDROP",
|
||||
"DRAGOVER",
|
||||
"MOUSEDOWN",
|
||||
"MOUSEMOVE",
|
||||
"MOUSEUP",
|
||||
],
|
||||
HScrollBar | VScrollBar => &[
|
||||
"CHANGE",
|
||||
"DRAGDROP",
|
||||
"DRAGOVER",
|
||||
"GOTFOCUS",
|
||||
"KEYDOWN",
|
||||
"KEYPRESS",
|
||||
"KEYUP",
|
||||
"LOSTFOCUS",
|
||||
],
|
||||
PictureBox => &[
|
||||
"CLICK",
|
||||
"DBLCLICK",
|
||||
"DRAGDROP",
|
||||
"DRAGOVER",
|
||||
"GOTFOCUS",
|
||||
"KEYDOWN",
|
||||
"KEYPRESS",
|
||||
"KEYUP",
|
||||
"LOSTFOCUS",
|
||||
"MOUSEDOWN",
|
||||
"MOUSEMOVE",
|
||||
"MOUSEUP",
|
||||
"PAINT",
|
||||
],
|
||||
Timer => &["TIMER"],
|
||||
Menu => &["CLICK"],
|
||||
DirListBox => &["CHANGE", "CLICK", "DBLCLICK", "PATHCHANGE"],
|
||||
DriveListBox => &["CHANGE"],
|
||||
FileListBox => &["CLICK", "DBLCLICK", "PATTERNCHANGE", "PATHCHANGE"],
|
||||
Screen => &[],
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum EventParamType {
|
||||
Integer,
|
||||
Single,
|
||||
Control,
|
||||
}
|
||||
|
||||
pub fn event_params(event: &str) -> Option<&'static [(&'static str, EventParamType)]> {
|
||||
use EventParamType::*;
|
||||
Some(match event.to_ascii_uppercase().as_str() {
|
||||
"KEYDOWN" | "KEYUP" => &[("KEYCODE", Integer), ("SHIFT", Integer)],
|
||||
"KEYPRESS" => &[("KEYASCII", Integer)],
|
||||
"MOUSEDOWN" | "MOUSEMOVE" | "MOUSEUP" => &[
|
||||
("BUTTON", Integer),
|
||||
("SHIFT", Integer),
|
||||
("X", Single),
|
||||
("Y", Single),
|
||||
],
|
||||
"DRAGDROP" => &[("SOURCE", Control), ("X", Single), ("Y", Single)],
|
||||
"DRAGOVER" => &[
|
||||
("SOURCE", Control),
|
||||
("X", Single),
|
||||
("Y", Single),
|
||||
("STATE", Integer),
|
||||
],
|
||||
"UNLOAD" => &[("CANCEL", Integer)],
|
||||
"CLICK" | "DBLCLICK" | "CHANGE" | "DROPDOWN" | "GOTFOCUS" | "LOSTFOCUS" | "LOAD"
|
||||
| "PAINT" | "RESIZE" | "TIMER" | "PATHCHANGE" | "PATTERNCHANGE" => &[],
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FormObject {
|
||||
pub name: String,
|
||||
pub class: ObjectClass,
|
||||
pub parent_form: Option<String>,
|
||||
pub array: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct FormCatalog {
|
||||
pub objects: Vec<FormObject>,
|
||||
}
|
||||
|
||||
impl FormCatalog {
|
||||
pub fn add(
|
||||
&mut self,
|
||||
name: impl Into<String>,
|
||||
class: ObjectClass,
|
||||
parent_form: Option<&str>,
|
||||
array: bool,
|
||||
) -> u16 {
|
||||
let id = self.objects.len() as u16;
|
||||
self.objects.push(FormObject {
|
||||
name: name.into().to_uppercase(),
|
||||
class,
|
||||
parent_form: parent_form.map(str::to_uppercase),
|
||||
array,
|
||||
});
|
||||
id
|
||||
}
|
||||
|
||||
pub fn find(&self, name: &str) -> Option<(u16, &FormObject)> {
|
||||
self.objects
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, o)| o.name.eq_ignore_ascii_case(name))
|
||||
.map(|(i, o)| (i as u16, o))
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,8 @@ pub enum HTy {
|
||||
FixedStr(u32),
|
||||
/// Benutzerdefinierter Typ (Index in `HirModule::udts`).
|
||||
Udt(u16),
|
||||
Form,
|
||||
Control,
|
||||
}
|
||||
|
||||
impl HTy {
|
||||
@@ -131,6 +133,17 @@ pub struct HirModule {
|
||||
pub procs: Vec<HProc>,
|
||||
pub data: Vec<DataItem>,
|
||||
pub option_base: u8,
|
||||
/// Zur Übersetzungszeit bekannte Forms-Objekte in Indexreihenfolge.
|
||||
pub objects: Vec<crate::forms::FormObject>,
|
||||
/// Ereignisprozeduren: Objektindex, Ereignisname, Prozedurindex.
|
||||
pub event_procs: Vec<HEventProc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HEventProc {
|
||||
pub object: u16,
|
||||
pub event: String,
|
||||
pub proc: u16,
|
||||
}
|
||||
|
||||
// ---- Ausdrücke -------------------------------------------------------------
|
||||
@@ -367,6 +380,25 @@ pub enum HExpr {
|
||||
Cur(i64),
|
||||
Str(String),
|
||||
Load(Box<HPlace>),
|
||||
ObjectProperty {
|
||||
object: u16,
|
||||
index: Option<Box<HExpr>>,
|
||||
property: u16,
|
||||
ty: HTy,
|
||||
},
|
||||
DynamicObjectProperty {
|
||||
object: Box<HExpr>,
|
||||
property: String,
|
||||
},
|
||||
ObjectRef {
|
||||
object: u16,
|
||||
index: Option<Box<HExpr>>,
|
||||
class: crate::forms::ObjectClass,
|
||||
},
|
||||
TypeOf {
|
||||
value: Box<HExpr>,
|
||||
class: crate::forms::ObjectClass,
|
||||
},
|
||||
/// Numerische Konvertierung nach Matrix (Rundung/Überlauf).
|
||||
Conv {
|
||||
from: NumTy,
|
||||
@@ -473,6 +505,27 @@ pub enum HStmtKind {
|
||||
place: HPlace,
|
||||
value: HExpr,
|
||||
},
|
||||
SetObjectProperty {
|
||||
object: u16,
|
||||
index: Option<HExpr>,
|
||||
property: u16,
|
||||
value: HExpr,
|
||||
},
|
||||
SetDynamicObjectProperty {
|
||||
object: HExpr,
|
||||
property: String,
|
||||
value: HExpr,
|
||||
},
|
||||
ObjectMethod {
|
||||
object: u16,
|
||||
method: u16,
|
||||
args: Vec<HExpr>,
|
||||
},
|
||||
ObjectLoad {
|
||||
object: u16,
|
||||
index: Option<HExpr>,
|
||||
unload: bool,
|
||||
},
|
||||
Print {
|
||||
items: Vec<HPrintItem>,
|
||||
/// Endet die Anweisung mit `;`/`,` (kein Zeilenumbruch)?
|
||||
|
||||
@@ -62,43 +62,169 @@ pub enum NumValue {
|
||||
/// und -anweisungen sind KEINE Keywords, sondern Builtins der Semantik).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Kw {
|
||||
And, As, Call, Case, Common, Const, Data, Declare, Def, DefCur, DefDbl, DefInt,
|
||||
DefLng, DefSng, DefStr, Dim, Do, Double, Else, ElseIf, End, Eqv, Erase,
|
||||
Error, Exit, For, Function, Gosub, Goto, If, Imp, Input, Integer, Is,
|
||||
Let, Line, Local, Long, Loop, Mod, Next, Not, On, Option, Or, Print,
|
||||
Read, ReDim, Rem, Restore, Resume, Return, Select, Shared, Single,
|
||||
Static, Step, Stop, String, Sub, System, Then, To, Type, Until, Using,
|
||||
Wend, While, Xor, Currency,
|
||||
And,
|
||||
As,
|
||||
Call,
|
||||
Case,
|
||||
Common,
|
||||
Const,
|
||||
Data,
|
||||
Declare,
|
||||
Def,
|
||||
DefCur,
|
||||
DefDbl,
|
||||
DefInt,
|
||||
DefLng,
|
||||
DefSng,
|
||||
DefStr,
|
||||
Dim,
|
||||
Do,
|
||||
Double,
|
||||
Else,
|
||||
ElseIf,
|
||||
End,
|
||||
Eqv,
|
||||
Erase,
|
||||
Error,
|
||||
Exit,
|
||||
For,
|
||||
Function,
|
||||
Gosub,
|
||||
Goto,
|
||||
If,
|
||||
Imp,
|
||||
Input,
|
||||
Integer,
|
||||
Is,
|
||||
Let,
|
||||
Line,
|
||||
Local,
|
||||
Long,
|
||||
Loop,
|
||||
Mod,
|
||||
Next,
|
||||
Not,
|
||||
On,
|
||||
Option,
|
||||
Or,
|
||||
Print,
|
||||
Read,
|
||||
ReDim,
|
||||
Rem,
|
||||
Restore,
|
||||
Resume,
|
||||
Return,
|
||||
Select,
|
||||
Shared,
|
||||
Single,
|
||||
Static,
|
||||
Step,
|
||||
Stop,
|
||||
String,
|
||||
Sub,
|
||||
System,
|
||||
Then,
|
||||
To,
|
||||
Type,
|
||||
Until,
|
||||
Using,
|
||||
Wend,
|
||||
While,
|
||||
Xor,
|
||||
Currency,
|
||||
// Datei-E/A-Keywords: werden geparst, aber erst in Phase 3 implementiert
|
||||
Open, Close, Write, Field, Get, Put, Seek, Lset, Rset,
|
||||
Open,
|
||||
Close,
|
||||
Write,
|
||||
Field,
|
||||
Get,
|
||||
Put,
|
||||
Seek,
|
||||
Lset,
|
||||
Rset,
|
||||
}
|
||||
|
||||
fn keyword(upper: &str) -> Option<Kw> {
|
||||
use Kw::*;
|
||||
Some(match upper {
|
||||
"AND" => And, "AS" => As, "CALL" => Call, "CASE" => Case,
|
||||
"COMMON" => Common, "CONST" => Const, "CURRENCY" => Currency,
|
||||
"AND" => And,
|
||||
"AS" => As,
|
||||
"CALL" => Call,
|
||||
"CASE" => Case,
|
||||
"COMMON" => Common,
|
||||
"CONST" => Const,
|
||||
"CURRENCY" => Currency,
|
||||
"DATA" => Data,
|
||||
"DECLARE" => Declare, "DEF" => Def, "DEFCUR" => DefCur,
|
||||
"DEFDBL" => DefDbl, "DEFINT" => DefInt, "DEFLNG" => DefLng,
|
||||
"DEFSNG" => DefSng, "DEFSTR" => DefStr, "DIM" => Dim, "DO" => Do,
|
||||
"DOUBLE" => Double, "ELSE" => Else, "ELSEIF" => ElseIf, "END" => End,
|
||||
"EQV" => Eqv, "ERASE" => Erase, "ERROR" => Error, "EXIT" => Exit,
|
||||
"FIELD" => Field, "FOR" => For, "FUNCTION" => Function,
|
||||
"GET" => Get, "GOSUB" => Gosub, "GOTO" => Goto, "IF" => If,
|
||||
"IMP" => Imp, "INPUT" => Input, "INTEGER" => Integer, "IS" => Is,
|
||||
"LET" => Let, "LINE" => Line, "LOCAL" => Local, "LONG" => Long,
|
||||
"LOOP" => Loop, "LSET" => Lset, "MOD" => Mod, "NEXT" => Next,
|
||||
"NOT" => Not, "ON" => On, "OPEN" => Open, "OPTION" => Option,
|
||||
"OR" => Or, "PRINT" => Print, "PUT" => Put, "READ" => Read,
|
||||
"REDIM" => ReDim, "REM" => Rem, "RESTORE" => Restore,
|
||||
"RESUME" => Resume, "RETURN" => Return, "RSET" => Rset,
|
||||
"SEEK" => Seek, "SELECT" => Select, "SHARED" => Shared,
|
||||
"SINGLE" => Single, "STATIC" => Static, "STEP" => Step,
|
||||
"STOP" => Stop, "STRING" => String, "SUB" => Sub,
|
||||
"SYSTEM" => System, "THEN" => Then,
|
||||
"TO" => To, "TYPE" => Type, "UNTIL" => Until, "USING" => Using,
|
||||
"WEND" => Wend, "WHILE" => While, "WRITE" => Write, "XOR" => Xor,
|
||||
"DECLARE" => Declare,
|
||||
"DEF" => Def,
|
||||
"DEFCUR" => DefCur,
|
||||
"DEFDBL" => DefDbl,
|
||||
"DEFINT" => DefInt,
|
||||
"DEFLNG" => DefLng,
|
||||
"DEFSNG" => DefSng,
|
||||
"DEFSTR" => DefStr,
|
||||
"DIM" => Dim,
|
||||
"DO" => Do,
|
||||
"DOUBLE" => Double,
|
||||
"ELSE" => Else,
|
||||
"ELSEIF" => ElseIf,
|
||||
"END" => End,
|
||||
"EQV" => Eqv,
|
||||
"ERASE" => Erase,
|
||||
"ERROR" => Error,
|
||||
"EXIT" => Exit,
|
||||
"FIELD" => Field,
|
||||
"FOR" => For,
|
||||
"FUNCTION" => Function,
|
||||
"GET" => Get,
|
||||
"GOSUB" => Gosub,
|
||||
"GOTO" => Goto,
|
||||
"IF" => If,
|
||||
"IMP" => Imp,
|
||||
"INPUT" => Input,
|
||||
"INTEGER" => Integer,
|
||||
"IS" => Is,
|
||||
"LET" => Let,
|
||||
"LINE" => Line,
|
||||
"LOCAL" => Local,
|
||||
"LONG" => Long,
|
||||
"LOOP" => Loop,
|
||||
"LSET" => Lset,
|
||||
"MOD" => Mod,
|
||||
"NEXT" => Next,
|
||||
"NOT" => Not,
|
||||
"ON" => On,
|
||||
"OPEN" => Open,
|
||||
"OPTION" => Option,
|
||||
"OR" => Or,
|
||||
"PRINT" => Print,
|
||||
"PUT" => Put,
|
||||
"READ" => Read,
|
||||
"REDIM" => ReDim,
|
||||
"REM" => Rem,
|
||||
"RESTORE" => Restore,
|
||||
"RESUME" => Resume,
|
||||
"RETURN" => Return,
|
||||
"RSET" => Rset,
|
||||
"SEEK" => Seek,
|
||||
"SELECT" => Select,
|
||||
"SHARED" => Shared,
|
||||
"SINGLE" => Single,
|
||||
"STATIC" => Static,
|
||||
"STEP" => Step,
|
||||
"STOP" => Stop,
|
||||
"STRING" => String,
|
||||
"SUB" => Sub,
|
||||
"SYSTEM" => System,
|
||||
"THEN" => Then,
|
||||
"TO" => To,
|
||||
"TYPE" => Type,
|
||||
"UNTIL" => Until,
|
||||
"USING" => Using,
|
||||
"WEND" => Wend,
|
||||
"WHILE" => While,
|
||||
"WRITE" => Write,
|
||||
"XOR" => Xor,
|
||||
"CLOSE" => Close,
|
||||
_ => return None,
|
||||
})
|
||||
@@ -107,18 +233,39 @@ fn keyword(upper: &str) -> Option<Kw> {
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum TokenKind {
|
||||
/// Bezeichner; `name` ist bereits in Großschreibung normalisiert.
|
||||
Ident { name: String, suffix: Option<Suffix> },
|
||||
Ident {
|
||||
name: String,
|
||||
suffix: Option<Suffix>,
|
||||
},
|
||||
Kw(Kw),
|
||||
Num(NumValue),
|
||||
Str(String),
|
||||
Plus, Minus, Star, Slash, Backslash, Caret,
|
||||
Eq, Ne, Lt, Le, Gt, Ge,
|
||||
LParen, RParen, Comma, Semicolon, Colon, Hash,
|
||||
Plus,
|
||||
Minus,
|
||||
Star,
|
||||
Slash,
|
||||
Backslash,
|
||||
Caret,
|
||||
Eq,
|
||||
Ne,
|
||||
Lt,
|
||||
Le,
|
||||
Gt,
|
||||
Ge,
|
||||
LParen,
|
||||
RParen,
|
||||
Comma,
|
||||
Semicolon,
|
||||
Colon,
|
||||
Hash,
|
||||
Dot,
|
||||
/// Metabefehl `'$INCLUDE: 'datei''` (Pfad; leer = fehlerhafte Syntax).
|
||||
MetaInclude(String),
|
||||
/// Metabefehle `'$STATIC` / `'$DYNAMIC`.
|
||||
MetaStatic,
|
||||
MetaDynamic,
|
||||
/// Metabefehl `'$FORM`: das Modul ist ein Formularmodul.
|
||||
MetaForm,
|
||||
/// Rohtext einer `DATA`-Anweisung bis zum Anweisungsende. Er wird
|
||||
/// bewusst **nicht** zerlegt oder normalisiert: unquotierte Elemente
|
||||
/// behalten ihre Groß- und Kleinschreibung und ihren inneren Leerraum.
|
||||
@@ -139,6 +286,9 @@ fn meta_token(rest: &[char]) -> Option<TokenKind> {
|
||||
if up.starts_with("$DYNAMIC") {
|
||||
return Some(TokenKind::MetaDynamic);
|
||||
}
|
||||
if up.starts_with("$FORM") {
|
||||
return Some(TokenKind::MetaForm);
|
||||
}
|
||||
if up.starts_with("$INCLUDE") {
|
||||
if let Some(colon) = t.find(':') {
|
||||
let after = t[colon + 1..].trim();
|
||||
@@ -191,14 +341,15 @@ pub fn lex(source: &str) -> LexOutput {
|
||||
break;
|
||||
}
|
||||
let start = i;
|
||||
let pos = SourcePos { line: line_no, column: (start + 1) as u32 };
|
||||
let pos = SourcePos {
|
||||
line: line_no,
|
||||
column: (start + 1) as u32,
|
||||
};
|
||||
let c = chars[i];
|
||||
|
||||
// Zeilenfortsetzung: `_` nach Leerraum, danach nur noch Leerraum
|
||||
if c == '_'
|
||||
&& (start == 0
|
||||
|| chars[start - 1] == ' '
|
||||
|| chars[start - 1] == '\t')
|
||||
&& (start == 0 || chars[start - 1] == ' ' || chars[start - 1] == '\t')
|
||||
&& chars[start + 1..].iter().all(|&ch| ch == ' ' || ch == '\t')
|
||||
{
|
||||
line_continued = true;
|
||||
@@ -236,17 +387,16 @@ pub fn lex(source: &str) -> LexOutput {
|
||||
// Das Vorbild toleriert fehlende schließende
|
||||
// Anführungszeichen am Zeilenende.
|
||||
}
|
||||
tokens.push(Token { kind: TokenKind::Str(s), pos });
|
||||
tokens.push(Token {
|
||||
kind: TokenKind::Str(s),
|
||||
pos,
|
||||
});
|
||||
}
|
||||
'&' if i + 1 < chars.len()
|
||||
&& matches!(chars[i + 1], 'h' | 'H' | 'o' | 'O') =>
|
||||
{
|
||||
'&' if i + 1 < chars.len() && matches!(chars[i + 1], 'h' | 'H' | 'o' | 'O') => {
|
||||
let hex = matches!(chars[i + 1], 'h' | 'H');
|
||||
i += 2;
|
||||
let digit_start = i;
|
||||
while i < chars.len()
|
||||
&& chars[i].is_ascii_alphanumeric()
|
||||
{
|
||||
while i < chars.len() && chars[i].is_ascii_alphanumeric() {
|
||||
i += 1;
|
||||
}
|
||||
let digits: String = chars[digit_start..i].iter().collect();
|
||||
@@ -276,8 +426,8 @@ pub fn lex(source: &str) -> LexOutput {
|
||||
}),
|
||||
}
|
||||
}
|
||||
'0'..='9' | '.' if c != '.'
|
||||
|| (i + 1 < chars.len() && chars[i + 1].is_ascii_digit()) =>
|
||||
'0'..='9' | '.'
|
||||
if c != '.' || (i + 1 < chars.len() && chars[i + 1].is_ascii_digit()) =>
|
||||
{
|
||||
let int_start = i;
|
||||
while i < chars.len() && chars[i].is_ascii_digit() {
|
||||
@@ -298,9 +448,7 @@ pub fn lex(source: &str) -> LexOutput {
|
||||
// Exponent E/D
|
||||
let mut exp_kind: Option<char> = None;
|
||||
let mut exp_str = String::new();
|
||||
if i < chars.len()
|
||||
&& matches!(chars[i], 'e' | 'E' | 'd' | 'D')
|
||||
{
|
||||
if i < chars.len() && matches!(chars[i], 'e' | 'E' | 'd' | 'D') {
|
||||
let save = i;
|
||||
let k = chars[i].to_ascii_uppercase();
|
||||
let mut j = i + 1;
|
||||
@@ -403,22 +551,25 @@ pub fn lex(source: &str) -> LexOutput {
|
||||
}
|
||||
}
|
||||
};
|
||||
tokens.push(Token { kind: TokenKind::Num(value), pos });
|
||||
tokens.push(Token {
|
||||
kind: TokenKind::Num(value),
|
||||
pos,
|
||||
});
|
||||
}
|
||||
c if c.is_alphabetic() => {
|
||||
i += 1;
|
||||
while i < chars.len()
|
||||
&& (chars[i].is_alphanumeric()
|
||||
|| chars[i] == '.'
|
||||
|| chars[i] == '_')
|
||||
&& (chars[i].is_alphanumeric() || chars[i] == '.' || chars[i] == '_')
|
||||
{
|
||||
i += 1;
|
||||
}
|
||||
let mut name: String = chars[start..i]
|
||||
.iter()
|
||||
.collect::<String>()
|
||||
.to_uppercase();
|
||||
let suffix = if i < chars.len() {
|
||||
let mut name: String =
|
||||
chars[start..i].iter().collect::<String>().to_uppercase();
|
||||
let container_bang = i < chars.len()
|
||||
&& chars[i] == '!'
|
||||
&& i + 1 < chars.len()
|
||||
&& chars[i + 1].is_alphabetic();
|
||||
let suffix = if i < chars.len() && !container_bang {
|
||||
Suffix::from_char(chars[i])
|
||||
} else {
|
||||
None
|
||||
@@ -426,6 +577,16 @@ pub fn lex(source: &str) -> LexOutput {
|
||||
if suffix.is_some() {
|
||||
i += 1;
|
||||
}
|
||||
if container_bang {
|
||||
name.push('!');
|
||||
i += 1;
|
||||
while i < chars.len()
|
||||
&& (chars[i].is_alphanumeric() || chars[i] == '.' || chars[i] == '_')
|
||||
{
|
||||
name.push(chars[i].to_ascii_uppercase());
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
if suffix.is_none() {
|
||||
if let Some(kw) = keyword(&name) {
|
||||
if kw == Kw::Rem {
|
||||
@@ -435,7 +596,10 @@ pub fn lex(source: &str) -> LexOutput {
|
||||
}
|
||||
break 'line;
|
||||
}
|
||||
tokens.push(Token { kind: TokenKind::Kw(kw), pos });
|
||||
tokens.push(Token {
|
||||
kind: TokenKind::Kw(kw),
|
||||
pos,
|
||||
});
|
||||
if kw == Kw::Data {
|
||||
// Der Rest der Anweisung ist Rohtext: bis zum
|
||||
// `:` außerhalb von Anführungszeichen oder bis
|
||||
@@ -486,8 +650,9 @@ pub fn lex(source: &str) -> LexOutput {
|
||||
';' => TokenKind::Semicolon,
|
||||
':' => TokenKind::Colon,
|
||||
'#' => TokenKind::Hash,
|
||||
'.' => TokenKind::Dot,
|
||||
'?' => TokenKind::Kw(Kw::Print), // Editor-Kurzform
|
||||
'&' => TokenKind::Kw(Kw::Long), // isoliertes & (selten)
|
||||
'&' => TokenKind::Kw(Kw::Long), // isoliertes & (selten)
|
||||
'<' => {
|
||||
if i < chars.len() && chars[i] == '=' {
|
||||
i += 1;
|
||||
@@ -538,9 +703,15 @@ pub fn lex(source: &str) -> LexOutput {
|
||||
|
||||
tokens.push(Token {
|
||||
kind: TokenKind::Eof,
|
||||
pos: SourcePos { line: (source.lines().count() + 1) as u32, column: 1 },
|
||||
pos: SourcePos {
|
||||
line: (source.lines().count() + 1) as u32,
|
||||
column: 1,
|
||||
},
|
||||
});
|
||||
LexOutput { tokens, diagnostics }
|
||||
LexOutput {
|
||||
tokens,
|
||||
diagnostics,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -565,7 +736,10 @@ mod tests {
|
||||
let k = kinds("STRING$ STRING");
|
||||
assert_eq!(
|
||||
k[0],
|
||||
TokenKind::Ident { name: "STRING".into(), suffix: Some(Suffix::Str) }
|
||||
TokenKind::Ident {
|
||||
name: "STRING".into(),
|
||||
suffix: Some(Suffix::Str)
|
||||
}
|
||||
);
|
||||
assert_eq!(k[1], TokenKind::Kw(Kw::String));
|
||||
}
|
||||
@@ -620,4 +794,23 @@ mod tests {
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bang_ist_suffix_oder_containeroperator_nach_folgetoken() {
|
||||
let k = kinds("Wert! = 1.5\nForm1!Text1.Text = \"a\"");
|
||||
assert_eq!(
|
||||
k[0],
|
||||
TokenKind::Ident {
|
||||
name: "WERT".into(),
|
||||
suffix: Some(Suffix::Single)
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
k[4],
|
||||
TokenKind::Ident {
|
||||
name: "FORM1!TEXT1.TEXT".into(),
|
||||
suffix: None
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
//! übersetzt.
|
||||
|
||||
pub mod ast;
|
||||
pub mod forms;
|
||||
pub mod hir;
|
||||
pub mod lexer;
|
||||
pub mod parser;
|
||||
@@ -42,12 +43,24 @@ pub struct Analysis {
|
||||
|
||||
/// Komplette Pipeline: Lexen → Parsen → semantische Prüfung + Lowering.
|
||||
pub fn analyze_source(module_name: &str, source: &str) -> Analysis {
|
||||
analyze_source_with_forms(module_name, source, &forms::FormCatalog::default())
|
||||
}
|
||||
|
||||
pub fn analyze_source_with_forms(
|
||||
module_name: &str,
|
||||
source: &str,
|
||||
forms: &forms::FormCatalog,
|
||||
) -> Analysis {
|
||||
let lexed = lexer::lex(source);
|
||||
let mut diagnostics = lexed.diagnostics;
|
||||
let parsed = parser::parse(module_name, &lexed.tokens);
|
||||
diagnostics.extend(parsed.diagnostics);
|
||||
let (hir, sema_diags) = sema::lower(&parsed.module);
|
||||
let (hir, sema_diags) = sema::lower_with_forms(&parsed.module, forms);
|
||||
diagnostics.extend(sema_diags);
|
||||
diagnostics.sort_by_key(|d| (d.pos.line, d.pos.column));
|
||||
Analysis { module: parsed.module, diagnostics, hir }
|
||||
Analysis {
|
||||
module: parsed.module,
|
||||
diagnostics,
|
||||
hir,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,12 @@ pub struct ParseOutput {
|
||||
}
|
||||
|
||||
pub fn parse(module_name: &str, tokens: &[Token]) -> ParseOutput {
|
||||
let mut p = P { toks: tokens, i: 0, diags: Vec::new(), at_line_start: true };
|
||||
let mut p = P {
|
||||
toks: tokens,
|
||||
i: 0,
|
||||
diags: Vec::new(),
|
||||
at_line_start: true,
|
||||
};
|
||||
let mut body = Vec::new();
|
||||
let mut procs = Vec::new();
|
||||
|
||||
@@ -40,7 +45,11 @@ pub fn parse(module_name: &str, tokens: &[Token]) -> ParseOutput {
|
||||
}
|
||||
|
||||
ParseOutput {
|
||||
module: Module { name: module_name.to_string(), body, procs },
|
||||
module: Module {
|
||||
name: module_name.to_string(),
|
||||
body,
|
||||
procs,
|
||||
},
|
||||
diagnostics: p.diags,
|
||||
}
|
||||
}
|
||||
@@ -85,7 +94,10 @@ impl<'a> P<'a> {
|
||||
}
|
||||
fn err(&mut self, msg: impl Into<String>) {
|
||||
let pos = self.pos();
|
||||
self.diags.push(Diagnostic { pos, message: msg.into() });
|
||||
self.diags.push(Diagnostic {
|
||||
pos,
|
||||
message: msg.into(),
|
||||
});
|
||||
}
|
||||
fn expect_kw(&mut self, kw: Kw, what: &str) -> bool {
|
||||
if self.eat_kw(kw) {
|
||||
@@ -205,11 +217,21 @@ impl<'a> P<'a> {
|
||||
}
|
||||
TokenKind::MetaStatic => {
|
||||
self.advance();
|
||||
Some(Stmt::MetaArrays { static_arrays: true, pos })
|
||||
Some(Stmt::MetaArrays {
|
||||
static_arrays: true,
|
||||
pos,
|
||||
})
|
||||
}
|
||||
TokenKind::MetaDynamic => {
|
||||
self.advance();
|
||||
Some(Stmt::MetaArrays { static_arrays: false, pos })
|
||||
Some(Stmt::MetaArrays {
|
||||
static_arrays: false,
|
||||
pos,
|
||||
})
|
||||
}
|
||||
TokenKind::MetaForm => {
|
||||
self.advance();
|
||||
Some(Stmt::MetaForm { pos })
|
||||
}
|
||||
TokenKind::Kw(Kw::Print) => self.parse_print(pos, false),
|
||||
TokenKind::Kw(Kw::Input) => {
|
||||
@@ -368,8 +390,9 @@ impl<'a> P<'a> {
|
||||
}
|
||||
Some(Stmt::ConstDecl { items, pos })
|
||||
}
|
||||
TokenKind::Kw(k @ (Kw::DefInt | Kw::DefLng | Kw::DefSng
|
||||
| Kw::DefDbl | Kw::DefStr | Kw::DefCur)) => {
|
||||
TokenKind::Kw(
|
||||
k @ (Kw::DefInt | Kw::DefLng | Kw::DefSng | Kw::DefDbl | Kw::DefStr | Kw::DefCur),
|
||||
) => {
|
||||
self.advance();
|
||||
let ty = match k {
|
||||
Kw::DefInt => TypeName::Integer,
|
||||
@@ -382,9 +405,7 @@ impl<'a> P<'a> {
|
||||
let mut ranges = Vec::new();
|
||||
loop {
|
||||
let a = match self.k() {
|
||||
TokenKind::Ident { name, suffix: None }
|
||||
if name.len() == 1 =>
|
||||
{
|
||||
TokenKind::Ident { name, suffix: None } if name.len() == 1 => {
|
||||
self.advance();
|
||||
name.chars().next().unwrap()
|
||||
}
|
||||
@@ -396,9 +417,7 @@ impl<'a> P<'a> {
|
||||
};
|
||||
let b = if self.eat(&TokenKind::Minus) {
|
||||
match self.k() {
|
||||
TokenKind::Ident { name, suffix: None }
|
||||
if name.len() == 1 =>
|
||||
{
|
||||
TokenKind::Ident { name, suffix: None } if name.len() == 1 => {
|
||||
self.advance();
|
||||
name.chars().next().unwrap()
|
||||
}
|
||||
@@ -420,11 +439,12 @@ impl<'a> P<'a> {
|
||||
TokenKind::Kw(Kw::Option) => {
|
||||
self.advance();
|
||||
match self.k() {
|
||||
TokenKind::Ident { name, suffix: None }
|
||||
if name == "EXPLICIT" =>
|
||||
{
|
||||
TokenKind::Ident { name, suffix: None } if name == "EXPLICIT" => {
|
||||
self.advance();
|
||||
Some(Stmt::OptionStmt { kind: OptionKind::Explicit, pos })
|
||||
Some(Stmt::OptionStmt {
|
||||
kind: OptionKind::Explicit,
|
||||
pos,
|
||||
})
|
||||
}
|
||||
TokenKind::Ident { name, suffix: None } if name == "BASE" => {
|
||||
self.advance();
|
||||
@@ -439,7 +459,10 @@ impl<'a> P<'a> {
|
||||
0
|
||||
}
|
||||
};
|
||||
Some(Stmt::OptionStmt { kind: OptionKind::Base(base), pos })
|
||||
Some(Stmt::OptionStmt {
|
||||
kind: OptionKind::Base(base),
|
||||
pos,
|
||||
})
|
||||
}
|
||||
_ => {
|
||||
self.err("Expected: BASE or EXPLICIT");
|
||||
@@ -466,7 +489,12 @@ impl<'a> P<'a> {
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
Some(Stmt::Call { name, suffix, args, pos })
|
||||
Some(Stmt::Call {
|
||||
name,
|
||||
suffix,
|
||||
args,
|
||||
pos,
|
||||
})
|
||||
}
|
||||
_ => {
|
||||
self.err("Expected: identifier");
|
||||
@@ -510,7 +538,11 @@ impl<'a> P<'a> {
|
||||
_ => cur.push(c),
|
||||
}
|
||||
}
|
||||
items.push(if quotiert { cur } else { cur.trim().to_string() });
|
||||
items.push(if quotiert {
|
||||
cur
|
||||
} else {
|
||||
cur.trim().to_string()
|
||||
});
|
||||
Some(Stmt::Data { items, pos })
|
||||
}
|
||||
TokenKind::Kw(Kw::Read) => {
|
||||
@@ -594,7 +626,13 @@ impl<'a> P<'a> {
|
||||
var = Some(self.parse_name_ref()?);
|
||||
}
|
||||
}
|
||||
Some(Stmt::GetPut { put: k == Kw::Put, file, recnum, var, pos })
|
||||
Some(Stmt::GetPut {
|
||||
put: k == Kw::Put,
|
||||
file,
|
||||
recnum,
|
||||
var,
|
||||
pos,
|
||||
})
|
||||
}
|
||||
TokenKind::Kw(k @ (Kw::Lset | Kw::Rset)) => {
|
||||
self.advance();
|
||||
@@ -605,7 +643,12 @@ impl<'a> P<'a> {
|
||||
return None;
|
||||
}
|
||||
let value = self.parse_expr()?;
|
||||
Some(Stmt::LsetRset { rset: k == Kw::Rset, target, value, pos })
|
||||
Some(Stmt::LsetRset {
|
||||
rset: k == Kw::Rset,
|
||||
target,
|
||||
value,
|
||||
pos,
|
||||
})
|
||||
}
|
||||
TokenKind::Kw(Kw::Write) => {
|
||||
self.advance();
|
||||
@@ -636,7 +679,11 @@ impl<'a> P<'a> {
|
||||
self.err("Expected: ,");
|
||||
}
|
||||
let position = self.parse_expr()?;
|
||||
Some(Stmt::SeekStmt { file, position, pos })
|
||||
Some(Stmt::SeekStmt {
|
||||
file,
|
||||
position,
|
||||
pos,
|
||||
})
|
||||
}
|
||||
TokenKind::Kw(Kw::Common) => {
|
||||
self.advance();
|
||||
@@ -654,7 +701,12 @@ impl<'a> P<'a> {
|
||||
}
|
||||
}
|
||||
let decls = self.parse_var_decls();
|
||||
Some(Stmt::CommonDecl { shared, block, decls, pos })
|
||||
Some(Stmt::CommonDecl {
|
||||
shared,
|
||||
block,
|
||||
decls,
|
||||
pos,
|
||||
})
|
||||
}
|
||||
TokenKind::Kw(Kw::Shared) => {
|
||||
self.advance();
|
||||
@@ -681,19 +733,20 @@ impl<'a> P<'a> {
|
||||
}
|
||||
Some(Stmt::ViewPrint { top, bottom, pos })
|
||||
}
|
||||
TokenKind::Ident { ref name, suffix: None }
|
||||
if name == "NAME" && self.k_at(1) != TokenKind::Eq =>
|
||||
{
|
||||
TokenKind::Ident {
|
||||
ref name,
|
||||
suffix: None,
|
||||
} if name == "NAME" && self.k_at(1) != TokenKind::Eq => {
|
||||
self.advance();
|
||||
let old = self.parse_expr()?;
|
||||
self.expect_kw(Kw::As, "AS");
|
||||
let new = self.parse_expr()?;
|
||||
Some(Stmt::NameStmt { old, new, pos })
|
||||
}
|
||||
TokenKind::Ident { ref name, suffix: None }
|
||||
if (name == "LOCK" || name == "UNLOCK")
|
||||
&& self.k_at(1) != TokenKind::Eq =>
|
||||
{
|
||||
TokenKind::Ident {
|
||||
ref name,
|
||||
suffix: None,
|
||||
} if (name == "LOCK" || name == "UNLOCK") && self.k_at(1) != TokenKind::Eq => {
|
||||
let unlock = name == "UNLOCK";
|
||||
self.advance();
|
||||
self.eat(&TokenKind::Hash);
|
||||
@@ -707,7 +760,13 @@ impl<'a> P<'a> {
|
||||
to = Some(self.parse_expr()?);
|
||||
}
|
||||
}
|
||||
Some(Stmt::LockStmt { unlock, file, from, to, pos })
|
||||
Some(Stmt::LockStmt {
|
||||
unlock,
|
||||
file,
|
||||
from,
|
||||
to,
|
||||
pos,
|
||||
})
|
||||
}
|
||||
// ISAM-Anweisungen: `NAME [#]n [, arg …]`. Sie unterscheiden sich
|
||||
// von einem gewöhnlichen impliziten Aufruf nur durch das
|
||||
@@ -716,10 +775,12 @@ impl<'a> P<'a> {
|
||||
// `=` und `(` schließen die Anweisungsdeutung aus: dann ist der
|
||||
// Name eine Variable bzw. ein Array (`DELETE = 1`, `DELETE(2) = 1`).
|
||||
// Keine ISAM-Syntaxform beginnt mit einer Klammer.
|
||||
TokenKind::Ident { ref name, suffix: None }
|
||||
if ist_isam_anweisung(name)
|
||||
&& self.k_at(1) != TokenKind::Eq
|
||||
&& self.k_at(1) != TokenKind::LParen =>
|
||||
TokenKind::Ident {
|
||||
ref name,
|
||||
suffix: None,
|
||||
} if ist_isam_anweisung(name)
|
||||
&& self.k_at(1) != TokenKind::Eq
|
||||
&& self.k_at(1) != TokenKind::LParen =>
|
||||
{
|
||||
self.parse_isam(name.clone(), pos)
|
||||
}
|
||||
@@ -740,11 +801,20 @@ impl<'a> P<'a> {
|
||||
// Es senkt auf die Sentinel-Kennung ab, damit `ROLLBACK` eine
|
||||
// einzige Signatur behält.
|
||||
if name == "ROLLBACK" {
|
||||
if let TokenKind::Ident { name: ref w, suffix: None } = self.k() {
|
||||
if let TokenKind::Ident {
|
||||
name: ref w,
|
||||
suffix: None,
|
||||
} = self.k()
|
||||
{
|
||||
if w == "ALL" {
|
||||
self.advance();
|
||||
let args = vec![Expr::LongLit(ROLLBACK_ALL)];
|
||||
return Some(Stmt::Call { name, suffix: None, args, pos });
|
||||
return Some(Stmt::Call {
|
||||
name,
|
||||
suffix: None,
|
||||
args,
|
||||
pos,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -754,7 +824,12 @@ impl<'a> P<'a> {
|
||||
} else {
|
||||
self.parse_arg_list_to_stmt_end()
|
||||
};
|
||||
Some(Stmt::Call { name, suffix: None, args, pos })
|
||||
Some(Stmt::Call {
|
||||
name,
|
||||
suffix: None,
|
||||
args,
|
||||
pos,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_print(&mut self, pos: SourcePos, printer: bool) -> Option<Stmt> {
|
||||
@@ -793,7 +868,13 @@ impl<'a> P<'a> {
|
||||
},
|
||||
}
|
||||
}
|
||||
Some(Stmt::Print { printer, file, using, items, pos })
|
||||
Some(Stmt::Print {
|
||||
printer,
|
||||
file,
|
||||
using,
|
||||
items,
|
||||
pos,
|
||||
})
|
||||
}
|
||||
|
||||
/// `OPEN` in beiden Syntaxformen (FOR-Klausel und Kurzform).
|
||||
@@ -813,7 +894,13 @@ impl<'a> P<'a> {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
return Some(Stmt::OpenLegacy { mode: first, number, file, len, pos });
|
||||
return Some(Stmt::OpenLegacy {
|
||||
mode: first,
|
||||
number,
|
||||
file,
|
||||
len,
|
||||
pos,
|
||||
});
|
||||
}
|
||||
let mut mode = None;
|
||||
let mut isam = None;
|
||||
@@ -823,7 +910,10 @@ impl<'a> P<'a> {
|
||||
self.advance();
|
||||
OpenMode::Input
|
||||
}
|
||||
TokenKind::Ident { ref name, suffix: None } => {
|
||||
TokenKind::Ident {
|
||||
ref name,
|
||||
suffix: None,
|
||||
} => {
|
||||
let m = match name.as_str() {
|
||||
"OUTPUT" => OpenMode::Output,
|
||||
"APPEND" => OpenMode::Append,
|
||||
@@ -873,7 +963,11 @@ impl<'a> P<'a> {
|
||||
}
|
||||
}
|
||||
let mut access = None;
|
||||
if let TokenKind::Ident { ref name, suffix: None } = self.k() {
|
||||
if let TokenKind::Ident {
|
||||
ref name,
|
||||
suffix: None,
|
||||
} = self.k()
|
||||
{
|
||||
if name == "ACCESS" {
|
||||
self.advance();
|
||||
let read = self.eat_kw(Kw::Read);
|
||||
@@ -892,7 +986,11 @@ impl<'a> P<'a> {
|
||||
let mut lock = None;
|
||||
if self.eat_kw(Kw::Shared) {
|
||||
lock = Some(LockClause::Shared);
|
||||
} else if let TokenKind::Ident { ref name, suffix: None } = self.k() {
|
||||
} else if let TokenKind::Ident {
|
||||
ref name,
|
||||
suffix: None,
|
||||
} = self.k()
|
||||
{
|
||||
if name == "LOCK" {
|
||||
self.advance();
|
||||
let read = self.eat_kw(Kw::Read);
|
||||
@@ -912,7 +1010,11 @@ impl<'a> P<'a> {
|
||||
self.eat(&TokenKind::Hash);
|
||||
let number = self.parse_expr()?;
|
||||
let mut len = None;
|
||||
if let TokenKind::Ident { ref name, suffix: None } = self.k() {
|
||||
if let TokenKind::Ident {
|
||||
ref name,
|
||||
suffix: None,
|
||||
} = self.k()
|
||||
{
|
||||
if name == "LEN" {
|
||||
self.advance();
|
||||
if !self.eat(&TokenKind::Eq) {
|
||||
@@ -921,7 +1023,16 @@ impl<'a> P<'a> {
|
||||
len = Some(self.parse_expr()?);
|
||||
}
|
||||
}
|
||||
Some(Stmt::Open { file: first, mode, isam, access, lock, number, len, pos })
|
||||
Some(Stmt::Open {
|
||||
file: first,
|
||||
mode,
|
||||
isam,
|
||||
access,
|
||||
lock,
|
||||
number,
|
||||
len,
|
||||
pos,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_input(&mut self, line: bool, pos: SourcePos) -> Option<Stmt> {
|
||||
@@ -936,8 +1047,7 @@ impl<'a> P<'a> {
|
||||
let mut prompt = None;
|
||||
if file.is_none() {
|
||||
if let TokenKind::Str(s) = self.k() {
|
||||
if matches!(self.k_at(1), TokenKind::Semicolon | TokenKind::Comma)
|
||||
{
|
||||
if matches!(self.k_at(1), TokenKind::Semicolon | TokenKind::Comma) {
|
||||
self.advance();
|
||||
let with_question = self.k() == TokenKind::Semicolon;
|
||||
self.advance();
|
||||
@@ -959,7 +1069,14 @@ impl<'a> P<'a> {
|
||||
if vars.is_empty() {
|
||||
self.err("Expected: variable");
|
||||
}
|
||||
Some(Stmt::Input { line, file, keep_cursor, prompt, vars, pos })
|
||||
Some(Stmt::Input {
|
||||
line,
|
||||
file,
|
||||
keep_cursor,
|
||||
prompt,
|
||||
vars,
|
||||
pos,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_if(&mut self, pos: SourcePos) -> Option<Stmt> {
|
||||
@@ -991,7 +1108,13 @@ impl<'a> P<'a> {
|
||||
} else {
|
||||
self.err("Expected: END IF");
|
||||
}
|
||||
Some(Stmt::If { cond, then_body, elseifs, else_body, pos })
|
||||
Some(Stmt::If {
|
||||
cond,
|
||||
then_body,
|
||||
elseifs,
|
||||
else_body,
|
||||
pos,
|
||||
})
|
||||
} else {
|
||||
// Einzeilig
|
||||
let then_body = if let Some(n) = self.line_number_target() {
|
||||
@@ -1008,7 +1131,13 @@ impl<'a> P<'a> {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Some(Stmt::If { cond, then_body, elseifs: Vec::new(), else_body, pos })
|
||||
Some(Stmt::If {
|
||||
cond,
|
||||
then_body,
|
||||
elseifs: Vec::new(),
|
||||
else_body,
|
||||
pos,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1112,7 +1241,14 @@ impl<'a> P<'a> {
|
||||
} else {
|
||||
self.err("FOR without NEXT");
|
||||
}
|
||||
Some(Stmt::For { var, from, to, step, body, pos })
|
||||
Some(Stmt::For {
|
||||
var,
|
||||
from,
|
||||
to,
|
||||
step,
|
||||
body,
|
||||
pos,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_do(&mut self, pos: SourcePos) -> Option<Stmt> {
|
||||
@@ -1135,7 +1271,12 @@ impl<'a> P<'a> {
|
||||
} else {
|
||||
self.err("DO without LOOP");
|
||||
}
|
||||
Some(Stmt::DoLoop { pre, post, body, pos })
|
||||
Some(Stmt::DoLoop {
|
||||
pre,
|
||||
post,
|
||||
body,
|
||||
pos,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_while(&mut self, pos: SourcePos) -> Option<Stmt> {
|
||||
@@ -1252,13 +1393,23 @@ impl<'a> P<'a> {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Some(Stmt::OnGoto { expr, targets, gosub, pos })
|
||||
Some(Stmt::OnGoto {
|
||||
expr,
|
||||
targets,
|
||||
gosub,
|
||||
pos,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_dim(&mut self, redim: bool, pos: SourcePos) -> Option<Stmt> {
|
||||
let shared = self.eat_kw(Kw::Shared);
|
||||
let decls = self.parse_var_decls();
|
||||
Some(Stmt::Dim { shared, redim, decls, pos })
|
||||
Some(Stmt::Dim {
|
||||
shared,
|
||||
redim,
|
||||
decls,
|
||||
pos,
|
||||
})
|
||||
}
|
||||
|
||||
/// Deklarationsliste `name[suffix][(dims)] [AS typ], …` — gemeinsame
|
||||
@@ -1309,7 +1460,13 @@ impl<'a> P<'a> {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
decls.push(VarDecl { name, suffix, dims, as_type, pos: dpos });
|
||||
decls.push(VarDecl {
|
||||
name,
|
||||
suffix,
|
||||
dims,
|
||||
as_type,
|
||||
pos: dpos,
|
||||
});
|
||||
if !self.eat(&TokenKind::Comma) {
|
||||
break;
|
||||
}
|
||||
@@ -1362,7 +1519,11 @@ impl<'a> P<'a> {
|
||||
}
|
||||
TokenKind::Ident { name, suffix: None } => {
|
||||
self.advance();
|
||||
Some(TypeName::Udt(name))
|
||||
Some(match name.as_str() {
|
||||
"FORM" => TypeName::Form,
|
||||
"CONTROL" => TypeName::Control,
|
||||
_ => TypeName::Udt(name),
|
||||
})
|
||||
}
|
||||
_ => {
|
||||
self.err("Expected: type");
|
||||
@@ -1397,7 +1558,10 @@ impl<'a> P<'a> {
|
||||
break;
|
||||
}
|
||||
match self.k() {
|
||||
TokenKind::Ident { name: fname, suffix: None } => {
|
||||
TokenKind::Ident {
|
||||
name: fname,
|
||||
suffix: None,
|
||||
} => {
|
||||
self.advance();
|
||||
if self.expect_kw(Kw::As, "AS") {
|
||||
if let Some(t) = self.parse_type_name() {
|
||||
@@ -1442,7 +1606,10 @@ impl<'a> P<'a> {
|
||||
if !self.eat(&TokenKind::RParen) {
|
||||
loop {
|
||||
match self.k() {
|
||||
TokenKind::Ident { name: pname, suffix: psfx } => {
|
||||
TokenKind::Ident {
|
||||
name: pname,
|
||||
suffix: psfx,
|
||||
} => {
|
||||
self.advance();
|
||||
let array = if self.eat(&TokenKind::LParen) {
|
||||
self.eat(&TokenKind::RParen);
|
||||
@@ -1474,7 +1641,12 @@ impl<'a> P<'a> {
|
||||
self.eat(&TokenKind::RParen);
|
||||
}
|
||||
}
|
||||
Some(ProcSig { kind, name, suffix, params })
|
||||
Some(ProcSig {
|
||||
kind,
|
||||
name,
|
||||
suffix,
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_proc(&mut self) -> Option<Proc> {
|
||||
@@ -1495,14 +1667,19 @@ impl<'a> P<'a> {
|
||||
ProcKind::Function => "Expected: END FUNCTION",
|
||||
});
|
||||
}
|
||||
Some(Proc { sig, is_static, body, pos })
|
||||
Some(Proc {
|
||||
sig,
|
||||
is_static,
|
||||
body,
|
||||
pos,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_def_fn(&mut self, pos: SourcePos) -> Option<Stmt> {
|
||||
self.advance(); // DEF
|
||||
// `DEF SEG` ist Segmentadressierung und damit deklariertes
|
||||
// Non-Feature — an der Syntax erkennbar, weil `DEF` sonst nur
|
||||
// `DEF FNname` einleitet.
|
||||
// `DEF SEG` ist Segmentadressierung und damit deklariertes
|
||||
// Non-Feature — an der Syntax erkennbar, weil `DEF` sonst nur
|
||||
// `DEF FNname` einleitet.
|
||||
if matches!(self.k(), TokenKind::Ident { ref name, suffix: None } if name == "SEG") {
|
||||
self.err("Feature unavailable");
|
||||
self.sync();
|
||||
@@ -1524,7 +1701,10 @@ impl<'a> P<'a> {
|
||||
if !self.eat(&TokenKind::RParen) {
|
||||
loop {
|
||||
match self.k() {
|
||||
TokenKind::Ident { name: pname, suffix: psfx } => {
|
||||
TokenKind::Ident {
|
||||
name: pname,
|
||||
suffix: psfx,
|
||||
} => {
|
||||
self.advance();
|
||||
params.push(Param {
|
||||
name: pname,
|
||||
@@ -1547,7 +1727,13 @@ impl<'a> P<'a> {
|
||||
}
|
||||
if self.eat(&TokenKind::Eq) {
|
||||
let body = self.parse_expr()?;
|
||||
Some(Stmt::DefFn { name, suffix, params, body, pos })
|
||||
Some(Stmt::DefFn {
|
||||
name,
|
||||
suffix,
|
||||
params,
|
||||
body,
|
||||
pos,
|
||||
})
|
||||
} else {
|
||||
// Blockform: DEF FNname(...) … [EXIT DEF] … END DEF
|
||||
let body = self.parse_stmt_list(|p: &P| p.at_end_pair(Kw::Def));
|
||||
@@ -1557,7 +1743,13 @@ impl<'a> P<'a> {
|
||||
} else {
|
||||
self.err("Expected: END DEF");
|
||||
}
|
||||
Some(Stmt::DefFnBlock { name, suffix, params, body, pos })
|
||||
Some(Stmt::DefFnBlock {
|
||||
name,
|
||||
suffix,
|
||||
params,
|
||||
body,
|
||||
pos,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1612,7 +1804,10 @@ impl<'a> P<'a> {
|
||||
}
|
||||
}
|
||||
// Impliziter Aufruf: `name [arg [, arg …]]`
|
||||
if let Expr::Name { name, suffix, args, .. } = target {
|
||||
if let Expr::Name {
|
||||
name, suffix, args, ..
|
||||
} = target
|
||||
{
|
||||
// Ohne CALL-Keyword sind Klammern Wert-Klammern (BYVAL), keine
|
||||
// Argumentlisten-Klammern: `Foo (n%)` übergibt `(n%)`.
|
||||
let mut call_args: Vec<Expr> = args
|
||||
@@ -1626,7 +1821,12 @@ impl<'a> P<'a> {
|
||||
if !self.at_stmt_end() && call_args.is_empty() {
|
||||
call_args = self.parse_arg_list_to_stmt_end();
|
||||
}
|
||||
Some(Stmt::Call { name, suffix, args: call_args, pos })
|
||||
Some(Stmt::Call {
|
||||
name,
|
||||
suffix,
|
||||
args: call_args,
|
||||
pos,
|
||||
})
|
||||
} else {
|
||||
self.err("Syntax error");
|
||||
self.sync();
|
||||
@@ -1640,7 +1840,7 @@ impl<'a> P<'a> {
|
||||
fn parse_name_ref(&mut self) -> Option<Expr> {
|
||||
let pos = self.pos();
|
||||
match self.k() {
|
||||
TokenKind::Ident { name, suffix } => {
|
||||
TokenKind::Ident { mut name, suffix } => {
|
||||
self.advance();
|
||||
let args = if self.eat(&TokenKind::LParen) {
|
||||
let a = self.parse_arg_list(&TokenKind::RParen);
|
||||
@@ -1649,7 +1849,25 @@ impl<'a> P<'a> {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Some(Expr::Name { name, suffix, args, pos })
|
||||
if self.eat(&TokenKind::Dot) {
|
||||
match self.k() {
|
||||
TokenKind::Ident {
|
||||
name: member,
|
||||
suffix: None,
|
||||
} => {
|
||||
name.push('.');
|
||||
name.push_str(&member);
|
||||
self.advance();
|
||||
}
|
||||
_ => self.err("Expected: property"),
|
||||
}
|
||||
}
|
||||
Some(Expr::Name {
|
||||
name,
|
||||
suffix,
|
||||
args,
|
||||
pos,
|
||||
})
|
||||
}
|
||||
_ => {
|
||||
self.err("Expected: variable");
|
||||
@@ -1665,7 +1883,12 @@ impl<'a> P<'a> {
|
||||
match self.k() {
|
||||
TokenKind::Ident { name, suffix } => {
|
||||
self.advance();
|
||||
Some(Expr::Name { name, suffix, args: None, pos })
|
||||
Some(Expr::Name {
|
||||
name,
|
||||
suffix,
|
||||
args: None,
|
||||
pos,
|
||||
})
|
||||
}
|
||||
_ => {
|
||||
self.err("Expected: variable");
|
||||
@@ -1773,7 +1996,12 @@ impl<'a> P<'a> {
|
||||
let pos = self.pos();
|
||||
self.advance();
|
||||
let rhs = self.parse_bin(prec + 1)?;
|
||||
lhs = Expr::Binary { op, lhs: Box::new(lhs), rhs: Box::new(rhs), pos };
|
||||
lhs = Expr::Binary {
|
||||
op,
|
||||
lhs: Box::new(lhs),
|
||||
rhs: Box::new(rhs),
|
||||
pos,
|
||||
};
|
||||
}
|
||||
Some(lhs)
|
||||
}
|
||||
@@ -1809,7 +2037,11 @@ impl<'a> P<'a> {
|
||||
TokenKind::Minus => {
|
||||
self.advance();
|
||||
let operand = self.parse_bin(12)?;
|
||||
Some(Expr::Unary { op: UnOp::Neg, operand: Box::new(operand), pos })
|
||||
Some(Expr::Unary {
|
||||
op: UnOp::Neg,
|
||||
operand: Box::new(operand),
|
||||
pos,
|
||||
})
|
||||
}
|
||||
TokenKind::Plus => {
|
||||
self.advance();
|
||||
@@ -1818,7 +2050,11 @@ impl<'a> P<'a> {
|
||||
TokenKind::Kw(Kw::Not) => {
|
||||
self.advance();
|
||||
let operand = self.parse_bin(6)?;
|
||||
Some(Expr::Unary { op: UnOp::Not, operand: Box::new(operand), pos })
|
||||
Some(Expr::Unary {
|
||||
op: UnOp::Not,
|
||||
operand: Box::new(operand),
|
||||
pos,
|
||||
})
|
||||
}
|
||||
_ => self.parse_primary(),
|
||||
}
|
||||
@@ -1842,6 +2078,30 @@ impl<'a> P<'a> {
|
||||
pos,
|
||||
});
|
||||
}
|
||||
if matches!(self.k(), TokenKind::Ident { ref name, suffix: None } if name == "TYPEOF") {
|
||||
let pos = self.pos();
|
||||
self.advance();
|
||||
let value = self.parse_primary()?;
|
||||
if !self.eat_kw(Kw::Is) {
|
||||
self.err("Expected: IS");
|
||||
return None;
|
||||
}
|
||||
let class = match self.k() {
|
||||
TokenKind::Ident { name, suffix: None } => {
|
||||
self.advance();
|
||||
name
|
||||
}
|
||||
_ => {
|
||||
self.err("Expected: control class");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
return Some(Expr::TypeOf {
|
||||
value: Box::new(value),
|
||||
class,
|
||||
pos,
|
||||
});
|
||||
}
|
||||
match self.k() {
|
||||
TokenKind::Num(n) => {
|
||||
self.advance();
|
||||
@@ -1913,13 +2173,21 @@ mod tests {
|
||||
"IF a > 1 THEN\nPRINT 1\nELSEIF a > 0 THEN\nPRINT 2\nELSE\nPRINT 3\nEND IF\nIF b THEN PRINT 4 ELSE PRINT 5",
|
||||
);
|
||||
match &m.body[0] {
|
||||
Stmt::If { elseifs, else_body, .. } => {
|
||||
Stmt::If {
|
||||
elseifs, else_body, ..
|
||||
} => {
|
||||
assert_eq!(elseifs.len(), 1);
|
||||
assert!(else_body.is_some());
|
||||
}
|
||||
other => panic!("erwartet IF, war {other:?}"),
|
||||
}
|
||||
assert!(matches!(&m.body[1], Stmt::If { else_body: Some(_), .. }));
|
||||
assert!(matches!(
|
||||
&m.body[1],
|
||||
Stmt::If {
|
||||
else_body: Some(_),
|
||||
..
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1943,8 +2211,22 @@ mod tests {
|
||||
"FOR i = 1 TO 10 STEP 2\nNEXT i\nDO WHILE x\nLOOP\nDO\nLOOP UNTIL x\nWHILE x\nWEND",
|
||||
);
|
||||
assert!(matches!(&m.body[0], Stmt::For { step: Some(_), .. }));
|
||||
assert!(matches!(&m.body[1], Stmt::DoLoop { pre: Some((false, _)), post: None, .. }));
|
||||
assert!(matches!(&m.body[2], Stmt::DoLoop { pre: None, post: Some((true, _)), .. }));
|
||||
assert!(matches!(
|
||||
&m.body[1],
|
||||
Stmt::DoLoop {
|
||||
pre: Some((false, _)),
|
||||
post: None,
|
||||
..
|
||||
}
|
||||
));
|
||||
assert!(matches!(
|
||||
&m.body[2],
|
||||
Stmt::DoLoop {
|
||||
pre: None,
|
||||
post: Some((true, _)),
|
||||
..
|
||||
}
|
||||
));
|
||||
assert!(matches!(&m.body[3], Stmt::While { .. }));
|
||||
}
|
||||
|
||||
@@ -1963,9 +2245,29 @@ mod tests {
|
||||
let m = parse_ok(
|
||||
"ON ERROR GOTO Handler\nON LOCAL ERROR RESUME NEXT\nON ERROR GOTO 0\nHandler:\nRESUME NEXT",
|
||||
);
|
||||
assert!(matches!(&m.body[0], Stmt::OnError { local: false, action: OnErrorAction::Goto(_), .. }));
|
||||
assert!(matches!(&m.body[1], Stmt::OnError { local: true, action: OnErrorAction::ResumeNext, .. }));
|
||||
assert!(matches!(&m.body[2], Stmt::OnError { action: OnErrorAction::Disable, .. }));
|
||||
assert!(matches!(
|
||||
&m.body[0],
|
||||
Stmt::OnError {
|
||||
local: false,
|
||||
action: OnErrorAction::Goto(_),
|
||||
..
|
||||
}
|
||||
));
|
||||
assert!(matches!(
|
||||
&m.body[1],
|
||||
Stmt::OnError {
|
||||
local: true,
|
||||
action: OnErrorAction::ResumeNext,
|
||||
..
|
||||
}
|
||||
));
|
||||
assert!(matches!(
|
||||
&m.body[2],
|
||||
Stmt::OnError {
|
||||
action: OnErrorAction::Disable,
|
||||
..
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1973,7 +2275,15 @@ mod tests {
|
||||
let m = parse_ok("x = 1 + 2 * 3 ^ 2");
|
||||
// 1 + (2 * (3 ^ 2))
|
||||
match &m.body[0] {
|
||||
Stmt::Assign { value: Expr::Binary { op: BinOp::Add, rhs, .. }, .. } => {
|
||||
Stmt::Assign {
|
||||
value:
|
||||
Expr::Binary {
|
||||
op: BinOp::Add,
|
||||
rhs,
|
||||
..
|
||||
},
|
||||
..
|
||||
} => {
|
||||
assert!(matches!(**rhs, Expr::Binary { op: BinOp::Mul, .. }));
|
||||
}
|
||||
other => panic!("unerwartet: {other:?}"),
|
||||
@@ -1989,4 +2299,18 @@ mod tests {
|
||||
assert!(matches!(&m.body[1], Stmt::Dim { shared: true, decls, .. } if decls.len() == 2));
|
||||
assert!(matches!(&m.body[3], Stmt::DefType { ranges, .. } if ranges.len() == 2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn forms_syntax() {
|
||||
let m = parse_ok(
|
||||
"'$FORM\nDIM f AS FORM, c AS CONTROL\nIF TYPEOF c IS CommandButton THEN PRINT 1",
|
||||
);
|
||||
assert!(matches!(m.body[0], Stmt::MetaForm { .. }));
|
||||
assert!(matches!(&m.body[1], Stmt::Dim { decls, .. }
|
||||
if decls[0].as_type == Some(TypeName::Form) && decls[1].as_type == Some(TypeName::Control)));
|
||||
assert!(
|
||||
matches!(&m.body[2], Stmt::If { cond: Expr::TypeOf { class, .. }, .. }
|
||||
if class == "COMMANDBUTTON")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user