Phase 4: Formularmodell und Objektsprache

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff