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

@@ -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
}
);
}
}