Phase 1 vollstaendig abgeschlossen; neue Vorgaben verankert

Frontend-Vervollstaendigung (keine offenen Punkte mehr in Phase 1):
- MID$-Anweisung, DEF-FN-Blockform, COMMON/SHARED/STATIC,
  LPRINT, VIEW PRINT, NAME AS, LOCK/UNLOCK, Ereignissteuerung
  (TIMER/KEY(n)/UEVENT ON|OFF|STOP), Metabefehle $INCLUDE/$STATIC/$DYNAMIC
- Komplette Datei-E/A-Grammatik: OPEN (beide Syntaxen, inkl. ISAM- und
  ACCESS/LOCK-Klauseln), CLOSE, FIELD, GET/PUT, LSET/RSET, WRITE, SEEK
- Semantik: UDT-Feldtypen, SHARED-Import von Modulvariablen,
  Konstantenfaltung (Invalid constant), OPTION BASE erfasst
- Non-Features konsistent zur Compile-Zeit abgewiesen: Hardware-Naehe,
  CHAIN, Grafik-Anweisungen, SOUND/PLAY ("Feature unavailable")
- 33 Frontend-Tests, Korpus-Meilenstein weiter gruen

Neue Vorgaben:
- Stufe-2-Ideen: SQLite/Embedded-SQL mit Record-Buffern; Runtime-
  Bibliotheken (z.B. crossterm) als BASIC-Bibliotheken
- Erster Kompatibilitaetstest: github.com/cout/vbdos (extern, wird nicht
  einvendort); binaer-.FRM-Konverter als Phase-4-Aufgabe (tbc convert-frm)
- Doku als Definition of Done: docs/ = Ist-Dokumentation, IDE-Hilfe
  rendert Markdown dynamisch zur Fenstergroesse (Phase-5-Aufgabe)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-02 09:30:44 +02:00
parent 333e794540
commit d77574d9de
6 changed files with 1059 additions and 165 deletions

View File

@@ -1,15 +1,14 @@
//! Semantische Analyse: Symboltabellen, implizite Deklaration,
//! `DEFtype`-Regeln, `OPTION EXPLICIT`/`BASE`, Typprüfung, Auflösung
//! Array-Index vs. Funktionsaufruf, Label-Prüfung, Builtin-Signaturen.
//!
//! Erste Ausbaustufe (Phase 1): bewusst nachsichtig — lieber eine Prüfung
//! auslassen als falsche Fehler melden. `COMMON`, `SHARED`-Sichtbarkeit
//! über Prozedurgrenzen und UDT-Feldtypen folgen mit Phase 2/3.
//! `DEFtype`-Regeln, `OPTION EXPLICIT`/`BASE`, Typprüfung inkl.
//! UDT-Feldtypen, Konstantenfaltung, Auflösung Array-Index vs.
//! Funktionsaufruf, Label-Prüfung, Builtin-Signaturen sowie die
//! Compile-Zeit-Abweisung deklarierter Non-Features.
use crate::ast::*;
use crate::lexer::Suffix;
use crate::{Diagnostic, SourcePos};
use std::collections::{HashMap, HashSet};
use std::collections::HashMap;
use std::collections::HashSet;
#[derive(Debug, Clone, PartialEq)]
pub enum Ty {
@@ -68,6 +67,13 @@ fn type_name_ty(t: &TypeName) -> Ty {
}
}
/// Gefalteter Konstantenwert (`CONST`).
#[derive(Debug, Clone, PartialEq)]
pub enum ConstVal {
Num(f64),
Str(String),
}
#[derive(Debug, Clone)]
struct VarInfo {
ty: Ty,
@@ -150,6 +156,7 @@ fn builtin_fn(name: &str) -> Option<(u8, u8, &'static [ArgK], RetK)> {
"LBOUND" | "UBOUND" => (1, 2, &[A, N], L),
"CSRLIN" => (0, 0, &[], I),
"POS" => (1, 1, &[N], I),
"SCREEN" => (2, 3, &[N, N, N], I),
"TAB" | "SPC" => (1, 1, &[N], St),
"DATE$" | "TIME$" => (0, 0, &[], St),
"TIMER" => (0, 0, &[], Sg),
@@ -165,6 +172,7 @@ fn builtin_fn(name: &str) -> Option<(u8, u8, &'static [ArgK], RetK)> {
"EOF" => (1, 1, &[N], I),
"LOF" | "LOC" | "SEEK" => (1, 1, &[N], L),
"FREEFILE" => (0, 0, &[], I),
"FILEATTR" => (2, 2, &[N, N], L),
"ENVIRON$" => (1, 1, &[A], St),
"COMMAND$" => (0, 0, &[], St),
"DOEVENTS" => (0, 0, &[], I),
@@ -174,16 +182,23 @@ fn builtin_fn(name: &str) -> Option<(u8, u8, &'static [ArgK], RetK)> {
})
}
/// Hardware-nahe Namen des Vorbilds, die Terminal Basic bewusst nicht
/// unterstützt (Entscheidung 2026-09-02): Ablehnung bereits zur
/// Compile-Zeit statt Laufzeitfehler 73.
/// Deklarierte Non-Features (siehe Sprachreferenz „Abweichungen"):
/// Hardware-Nähe, CHAIN/Overlays, Grafik, PLAY/SOUND. Ablehnung erfolgt
/// zur Compile-Zeit mit der Meldung „Feature unavailable".
fn banned_feature(name: &str) -> bool {
matches!(
name,
// Hardware-Nähe
"PEEK" | "POKE" | "INP" | "OUT" | "WAIT" | "BLOAD" | "BSAVE"
| "VARPTR" | "VARSEG" | "SADD" | "VARPTR$" | "ABSOLUTE"
| "INTERRUPT" | "INTERRUPTX" | "IOCTL" | "IOCTL$"
| "DEF.SEG" | "FRE.SEG"
// Overlay-Mechanismus
| "CHAIN"
// Grafik
| "PSET" | "PRESET" | "CIRCLE" | "PAINT" | "DRAW" | "PALETTE"
| "PCOPY" | "PMAP" | "WINDOW"
// Klang (außer BEEP)
| "SOUND" | "PLAY"
)
}
@@ -192,19 +207,23 @@ fn builtin_stmt(name: &str) -> Option<(u8, u8, &'static [ArgK])> {
use ArgK::*;
Some(match name {
"CLS" => (0, 1, &[N]),
"BEEP" | "DOEVENTS" => (0, 0, &[]),
"BEEP" | "DOEVENTS" | "TRON" | "TROFF" | "RESET" => (0, 0, &[]),
"COLOR" => (0, 3, &[N, N, N]),
"LOCATE" => (0, 5, &[N, N, N, N, N]),
"RANDOMIZE" => (0, 1, &[N]),
"SLEEP" => (0, 1, &[N]),
"WIDTH" => (0, 2, &[N, N]),
"SOUND" => (2, 2, &[N, N]),
"SCREEN" => (1, 4, &[N, N, N, N]),
"SWAP" => (2, 2, &[A, A]),
"KILL" | "CHDIR" | "MKDIR" | "RMDIR" => (1, 1, &[S]),
"FILES" => (0, 1, &[S]),
"SHELL" => (0, 1, &[S]),
"RUN" => (0, 1, &[A]),
"CLEAR" => (0, 3, &[N, N, N]),
"KEY" => (1, 2, &[A, S]),
"ENVIRON" => (1, 1, &[S]),
"MSGBOX" => (1, 3, &[S, N, S]),
"RESET" => (0, 0, &[]),
"SETUEVENT" => (0, 0, &[]),
_ => return None,
})
}
@@ -216,9 +235,11 @@ pub fn check(module: &Module) -> Vec<Diagnostic> {
diags: Vec::new(),
deftypes: [const { None }; 26],
explicit: false,
option_base: 0,
procs: HashMap::new(),
udts: HashSet::new(),
udts: HashMap::new(),
consts: HashMap::new(),
module_vars: HashMap::new(),
};
s.run(module);
s.diags
@@ -229,9 +250,15 @@ struct Sema {
/// `DEFtype`-Zuordnung je Anfangsbuchstabe; None = Standard (SINGLE).
deftypes: [Option<Ty>; 26],
explicit: bool,
/// `OPTION BASE` (0 oder 1) — relevant für die Codegenerierung.
option_base: u8,
procs: HashMap<String, ProcInfo>,
udts: HashSet<String>,
consts: HashMap<String, Ty>,
/// Benutzerdefinierte Typen mit Feldtypen.
udts: HashMap<String, HashMap<String, Ty>>,
/// Konstanten: Typ und gefalteter Wert.
consts: HashMap<String, (Ty, Option<ConstVal>)>,
/// Modulvariablen nach dem Modulrumpf-Pass (für SHARED in Prozeduren).
module_vars: HashMap<String, VarInfo>,
}
impl Sema {
@@ -240,12 +267,24 @@ impl Sema {
}
fn run(&mut self, module: &Module) {
// Pass 1: Prozeduren, DECLAREs und TYPEs registrieren
// Pass 1: Prozeduren, DECLAREs und TYPEs registrieren (in Reihenfolge,
// damit TYPE-Verweise „definiert vor Verwendung" geprüft werden).
for stmt in &module.body {
match stmt {
Stmt::Declare { sig, .. } => self.register_proc(sig),
Stmt::TypeDecl { name, .. } => {
self.udts.insert(name.clone());
Stmt::TypeDecl { name, fields, pos } => {
let mut map = HashMap::new();
for (fname, ftype) in fields {
if let TypeName::Udt(n) = ftype {
if !self.udts.contains_key(n) {
self.err(*pos, "Type not defined");
}
}
map.insert(fname.clone(), type_name_ty(ftype));
}
if self.udts.insert(name.clone(), map).is_some() {
self.err(*pos, "Duplicate definition");
}
}
_ => {}
}
@@ -257,10 +296,10 @@ impl Sema {
// Pass 2: Modulrumpf
let mut scope = Scope::default();
collect_labels(&module.body, &mut scope);
let body_ref: Vec<&Stmt> = module.body.iter().collect();
for stmt in body_ref {
self.check_stmt(stmt, &mut scope, None);
for stmt in &module.body {
self.check_stmt(stmt, &mut scope);
}
self.module_vars = scope.vars.clone();
// Pass 3: Prozedurrümpfe
for proc in &module.procs {
@@ -269,23 +308,20 @@ impl Sema {
for p in &proc.sig.params {
let ty = self.param_ty(p);
let key = self.var_key(&p.name, &p.suffix, p.as_type.is_some());
pscope.vars.insert(
key,
VarInfo { ty, array: p.array, explicit: true },
);
pscope
.vars
.insert(key, VarInfo { ty, array: p.array, explicit: true });
}
// Funktionsname als Rückgabe-Variable
if proc.sig.kind == ProcKind::Function {
let ret = self.name_ty(&proc.sig.name, &proc.sig.suffix);
let key = self.var_key(&proc.sig.name, &proc.sig.suffix, false);
pscope.vars.insert(key, VarInfo { ty: ret, array: false, explicit: true });
pscope
.vars
.insert(key, VarInfo { ty: ret, array: false, explicit: true });
}
let fn_name = match proc.sig.kind {
ProcKind::Function => Some(proc.sig.name.clone()),
ProcKind::Sub => None,
};
for stmt in &proc.body {
self.check_stmt(stmt, &mut pscope, fn_name.as_deref());
self.check_stmt(stmt, &mut pscope);
}
}
}
@@ -303,15 +339,9 @@ impl Sema {
ProcKind::Function => self.name_ty(&sig.name, &sig.suffix),
ProcKind::Sub => Ty::Unknown,
};
let params = sig
.params
.iter()
.map(|p| (self.param_ty(p), p.array))
.collect();
self.procs.insert(
sig.name.clone(),
ProcInfo { kind: sig.kind, ret, params },
);
let params = sig.params.iter().map(|p| (self.param_ty(p), p.array)).collect();
self.procs
.insert(sig.name.clone(), ProcInfo { kind: sig.kind, ret, params });
}
/// Standardtyp eines Namens ohne Suffix (DEFtype bzw. SINGLE).
@@ -352,7 +382,32 @@ impl Sema {
}
}
/// Variable nachschlagen; `declare_if_missing` deklariert implizit.
/// Feldtyp eines UDT-Zugriffs `basisvar.feld[.feld …]`.
fn member_ty(&mut self, base_ty: &Ty, path: &[&str], pos: SourcePos) -> Ty {
let mut cur = base_ty.clone();
for seg in path {
match &cur {
Ty::Udt(udt_name) => {
match self.udts.get(udt_name).and_then(|f| f.get(*seg)) {
Some(t) => cur = t.clone(),
None => {
self.err(pos, "Element not defined");
return Ty::Unknown;
}
}
}
Ty::Unknown => return Ty::Unknown,
_ => {
self.err(pos, "Type mismatch");
return Ty::Unknown;
}
}
}
cur
}
/// Variable nachschlagen bzw. implizit deklarieren; löst auch
/// UDT-Feldzugriffe (`a.b.c`) auf.
fn resolve_var(
&mut self,
scope: &mut Scope,
@@ -375,12 +430,19 @@ impl Sema {
if let Some(v) = scope.vars.get(&key) {
return v.ty.clone();
}
// UDT-Feldzugriff `a.b`: Basis auflösen
if let Some(base) = name.split('.').next() {
if base != name {
let base_as = format!("{base}\u{1}AS");
if scope.vars.contains_key(&base_as) {
return Ty::Unknown; // Feldtypen: Phase 2
// UDT-Feldzugriff: Basis vor dem ersten Punkt auflösen
if name.contains('.') {
let parts: Vec<&str> = name.split('.').collect();
let base = parts[0];
let base_as = format!("{base}\u{1}AS");
let base_info = scope
.vars
.get(&base_as)
.or_else(|| scope.vars.get(&self.var_key(base, &None, false)))
.cloned();
if let Some(info) = base_info {
if matches!(info.ty, Ty::Udt(_)) {
return self.member_ty(&info.ty, &parts[1..], pos);
}
}
}
@@ -389,22 +451,109 @@ impl Sema {
return self.name_ty(name, suffix);
}
let ty = self.name_ty(name, suffix);
scope.vars.insert(
key,
VarInfo { ty: ty.clone(), array, explicit: false },
);
scope
.vars
.insert(key, VarInfo { ty: ty.clone(), array, explicit: false });
ty
}
/// Konstantenfaltung für `CONST`-Ausdrücke.
fn fold_const(&self, e: &Expr) -> Option<ConstVal> {
match e {
Expr::IntLit(v) => Some(ConstVal::Num(*v as f64)),
Expr::LongLit(v) => Some(ConstVal::Num(*v as f64)),
Expr::SingleLit(v) => Some(ConstVal::Num(*v as f64)),
Expr::DoubleLit(v) => Some(ConstVal::Num(*v)),
Expr::CurrencyLit(v) => Some(ConstVal::Num(*v as f64 / 10_000.0)),
Expr::StrLit(s) => Some(ConstVal::Str(s.clone())),
Expr::Name { name, args: None, .. } => {
self.consts.get(name).and_then(|(_, v)| v.clone())
}
Expr::Unary { op: UnOp::Neg, operand, .. } => {
match self.fold_const(operand)? {
ConstVal::Num(n) => Some(ConstVal::Num(-n)),
ConstVal::Str(_) => None,
}
}
Expr::Unary { op: UnOp::Not, operand, .. } => {
match self.fold_const(operand)? {
ConstVal::Num(n) => Some(ConstVal::Num(!(n as i64) as f64)),
ConstVal::Str(_) => None,
}
}
Expr::Binary { op, lhs, rhs, .. } => {
let l = self.fold_const(lhs)?;
let r = self.fold_const(rhs)?;
match (l, r) {
(ConstVal::Num(a), ConstVal::Num(b)) => {
let v = match op {
BinOp::Add => a + b,
BinOp::Sub => a - b,
BinOp::Mul => a * b,
BinOp::Div => a / b,
BinOp::Pow => a.powf(b),
BinOp::IntDiv => {
let bi = b as i64;
if bi == 0 {
return None;
}
((a as i64) / bi) as f64
}
BinOp::Mod => {
let bi = b as i64;
if bi == 0 {
return None;
}
((a as i64) % bi) as f64
}
_ => return None,
};
Some(ConstVal::Num(v))
}
(ConstVal::Str(a), ConstVal::Str(b)) if *op == BinOp::Add => {
Some(ConstVal::Str(format!("{a}{b}")))
}
_ => None,
}
}
_ => None,
}
}
// ---- Anweisungen -------------------------------------------------------
fn check_stmt(&mut self, stmt: &Stmt, scope: &mut Scope, fn_name: Option<&str>) {
fn check_stmt(&mut self, stmt: &Stmt, scope: &mut Scope) {
match stmt {
Stmt::Label(_) | Stmt::LineNumber(_) | Stmt::End | Stmt::StopStmt
| Stmt::System | Stmt::Data { .. } | Stmt::Exit { .. }
| Stmt::NotYetImplemented { .. } => {}
| Stmt::Include { .. } | Stmt::MetaArrays { .. } => {}
Stmt::Assign { target, value, pos } => {
// MID$-Anweisung: MID$(s$, start [, laenge]) = ausdruck
if let Expr::Name {
name,
suffix: Some(Suffix::Str),
args: Some(args),
..
} = target
{
if name == "MID" {
if args.is_empty() || args.len() > 3 {
self.err(*pos, "Argument-count mismatch");
}
if let Some(sv) = args.first() {
let t = self.lvalue_ty(sv, scope);
if !is_str(&t) {
self.err(sv.pos(), "Type mismatch");
}
}
for a in args.iter().skip(1) {
self.want_num(a, scope);
}
self.want_str(value, scope);
return;
}
}
let tt = self.lvalue_ty(target, scope);
let vt = self.expr_ty(value, scope);
self.check_assign(&tt, &vt, *pos);
@@ -433,21 +582,21 @@ impl Sema {
Stmt::If { cond, then_body, elseifs, else_body, .. } => {
self.want_num(cond, scope);
for s in then_body {
self.check_stmt(s, scope, fn_name);
self.check_stmt(s, scope);
}
for (c, b) in elseifs {
self.want_num(c, scope);
for s in b {
self.check_stmt(s, scope, fn_name);
self.check_stmt(s, scope);
}
}
if let Some(b) = else_body {
for s in b {
self.check_stmt(s, scope, fn_name);
self.check_stmt(s, scope);
}
}
}
Stmt::Select { expr, arms, pos: _ } => {
Stmt::Select { expr, arms, .. } => {
let st = self.expr_ty(expr, scope);
for arm in arms {
for spec in &arm.specs {
@@ -466,7 +615,7 @@ impl Sema {
}
}
for s in &arm.body {
self.check_stmt(s, scope, fn_name);
self.check_stmt(s, scope);
}
}
}
@@ -481,7 +630,7 @@ impl Sema {
self.want_num(s, scope);
}
for s in body {
self.check_stmt(s, scope, fn_name);
self.check_stmt(s, scope);
}
}
Stmt::DoLoop { pre, post, body, .. } => {
@@ -492,13 +641,13 @@ impl Sema {
self.want_num(c, scope);
}
for s in body {
self.check_stmt(s, scope, fn_name);
self.check_stmt(s, scope);
}
}
Stmt::While { cond, body, .. } => {
self.want_num(cond, scope);
for s in body {
self.check_stmt(s, scope, fn_name);
self.check_stmt(s, scope);
}
}
Stmt::Goto { target, pos } | Stmt::Gosub { target, pos } => {
@@ -531,6 +680,36 @@ impl Sema {
self.declare(d, *redim, scope);
}
}
Stmt::StaticDecl { decls, .. } | Stmt::CommonDecl { decls, .. } => {
for d in decls {
self.declare(d, false, scope);
}
}
Stmt::SharedDecl { decls, .. } => {
// Zugriff auf Modulvariablen aus einer Prozedur heraus:
// vorhandene Modulvariable importieren, sonst dort anlegen.
for d in decls {
let key = self.var_key(&d.name, &d.suffix, d.as_type.is_some());
let info = match self.module_vars.get(&key) {
Some(v) => v.clone(),
None => {
let ty = if let Some(t) = &d.as_type {
type_name_ty(t)
} else {
self.name_ty(&d.name, &d.suffix)
};
let v = VarInfo {
ty,
array: d.dims.is_some(),
explicit: true,
};
self.module_vars.insert(key.clone(), v.clone());
v
}
};
scope.vars.insert(key, info);
}
}
Stmt::Erase { names, .. } => {
for n in names {
if let Expr::Name { name, suffix, pos, .. } = n {
@@ -538,10 +717,22 @@ impl Sema {
}
}
}
Stmt::ConstDecl { items, .. } => {
for (name, _suffix, value) in items {
let ty = self.expr_ty(value, scope);
self.consts.insert(name.clone(), ty);
Stmt::ConstDecl { items, pos } => {
for (name, suffix, value) in items {
let ty = match suffix {
Some(s) => suffix_ty(*s),
None => match self.fold_const(value) {
Some(ConstVal::Str(_)) => Ty::Str,
_ => Ty::Dbl,
},
};
let folded = self.fold_const(value);
if folded.is_none() {
self.err(*pos, "Invalid constant");
}
if self.consts.insert(name.clone(), (ty, folded)).is_some() {
self.err(*pos, "Duplicate definition");
}
}
}
Stmt::DefType { ty, ranges, .. } => {
@@ -555,22 +746,12 @@ impl Sema {
}
}
}
Stmt::OptionStmt { kind, .. } => {
if *kind == OptionKind::Explicit {
self.explicit = true;
}
// OPTION BASE: relevant erst für die Array-Semantik (Phase 2)
}
Stmt::TypeDecl { fields, pos, .. } => {
for (_, t) in fields {
if let TypeName::Udt(n) = t {
if !self.udts.contains(n) {
self.err(*pos, "Type not defined");
}
}
}
}
Stmt::Declare { .. } => {} // bereits in Pass 1 registriert
Stmt::OptionStmt { kind, .. } => match kind {
OptionKind::Explicit => self.explicit = true,
OptionKind::Base(b) => self.option_base = *b,
},
Stmt::TypeDecl { .. } => {} // bereits in Pass 1 registriert
Stmt::Declare { .. } => {} // bereits in Pass 1 registriert
Stmt::Call { name, args, pos, .. } => {
self.check_call(name, args, scope, *pos);
}
@@ -584,33 +765,150 @@ impl Sema {
self.check_label(t, scope, *pos);
}
}
Stmt::DefFn { name, suffix, params, body, pos: _ } => {
let ret = self.name_ty(name, suffix);
let param_infos = params
.iter()
.map(|p| (self.param_ty(p), p.array))
.collect();
self.procs.insert(
name.clone(),
ProcInfo { kind: ProcKind::Function, ret, params: param_infos },
);
// Rumpf in Mini-Scope mit den Parametern prüfen
let mut fscope = Scope::default();
for p in params {
let ty = self.param_ty(p);
let key = self.var_key(&p.name, &p.suffix, false);
fscope.vars.insert(key, VarInfo { ty, array: false, explicit: true });
}
Stmt::DefFn { name, suffix, params, body, .. } => {
self.register_def_fn(name, suffix, params);
let mut fscope = self.def_fn_scope(name, suffix, params);
self.expr_ty(body, &mut fscope);
}
Stmt::DefFnBlock { name, suffix, params, body, .. } => {
self.register_def_fn(name, suffix, params);
let mut fscope = self.def_fn_scope(name, suffix, params);
collect_labels(body, &mut fscope);
// Modul-Labels bleiben aus DEF FN heraus ansprechbar
fscope.labels.extend(scope.labels.iter().cloned());
fscope.line_labels.extend(scope.line_labels.iter().copied());
for s in body {
self.check_stmt(s, &mut fscope);
}
}
// ---- Datei-E/A ----
Stmt::Open { file, isam, number, len, pos, .. } => {
self.want_str(file, scope);
self.want_num(number, scope);
if let Some(l) = len {
self.want_num(l, scope);
}
if let Some((ty_name, _table)) = isam {
if !ty_name.is_empty() && !self.udts.contains_key(ty_name) {
self.err(*pos, "Type not defined");
}
}
}
Stmt::OpenLegacy { mode, number, file, len, .. } => {
self.want_str(mode, scope);
self.want_num(number, scope);
self.want_str(file, scope);
if let Some(l) = len {
self.want_num(l, scope);
}
}
Stmt::CloseStmt { files, .. } => {
for f in files {
self.want_num(f, scope);
}
}
Stmt::FieldStmt { file, fields, .. } => {
self.want_num(file, scope);
for (width, var) in fields {
self.want_num(width, scope);
let t = self.lvalue_ty(var, scope);
if !is_str(&t) {
self.err(var.pos(), "Type mismatch");
}
}
}
Stmt::GetPut { file, recnum, var, .. } => {
self.want_num(file, scope);
if let Some(r) = recnum {
self.want_num(r, scope);
}
if let Some(v) = var {
self.lvalue_ty(v, scope);
}
}
Stmt::LsetRset { target, value, pos, .. } => {
let tt = self.lvalue_ty(target, scope);
let vt = self.expr_ty(value, scope);
let ok = (is_str(&tt) && is_str(&vt))
|| matches!((&tt, &vt), (Ty::Udt(a), Ty::Udt(b)) if a == b)
|| tt == Ty::Unknown
|| vt == Ty::Unknown;
if !ok {
self.err(*pos, "Type mismatch");
}
}
Stmt::WriteStmt { file, items, .. } => {
if let Some(f) = file {
self.want_num(f, scope);
}
for e in items {
self.expr_ty(e, scope);
}
}
Stmt::SeekStmt { file, position, .. } => {
self.want_num(file, scope);
self.want_num(position, scope);
}
Stmt::LockStmt { file, from, to, .. } => {
self.want_num(file, scope);
if let Some(f) = from {
self.want_num(f, scope);
}
if let Some(t) = to {
self.want_num(t, scope);
}
}
Stmt::NameStmt { old, new, .. } => {
self.want_str(old, scope);
self.want_str(new, scope);
}
// ---- Bildschirm/Ereignisse ----
Stmt::ViewPrint { top, bottom, .. } => {
if let Some(t) = top {
self.want_num(t, scope);
}
if let Some(b) = bottom {
self.want_num(b, scope);
}
}
Stmt::EventControl { device, index, pos, .. } => {
match device.as_str() {
"TIMER" | "KEY" | "UEVENT" | "EVENT" => {}
_ => self.err(*pos, "Feature unavailable"),
}
if let Some(i) = index {
self.want_num(i, scope);
}
}
}
let _ = fn_name; // Rückgabezuweisung läuft über die Scope-Variable
}
fn register_def_fn(&mut self, name: &str, suffix: &Option<Suffix>, params: &[Param]) {
let ret = self.name_ty(name, suffix);
let param_infos = params.iter().map(|p| (self.param_ty(p), p.array)).collect();
self.procs.insert(
name.to_string(),
ProcInfo { kind: ProcKind::Function, ret, params: param_infos },
);
}
fn def_fn_scope(&mut self, name: &str, suffix: &Option<Suffix>, params: &[Param]) -> Scope {
let mut fscope = Scope::default();
for p in params {
let ty = self.param_ty(p);
let key = self.var_key(&p.name, &p.suffix, false);
fscope.vars.insert(key, VarInfo { ty, array: false, explicit: true });
}
let ret = self.name_ty(name, suffix);
let key = self.var_key(name, suffix, false);
fscope.vars.insert(key, VarInfo { ty: ret, array: false, explicit: true });
fscope
}
fn declare(&mut self, d: &VarDecl, redim: bool, scope: &mut Scope) {
let ty = if let Some(t) = &d.as_type {
if let TypeName::Udt(n) = t {
if !self.udts.contains(n) {
if !self.udts.contains_key(n) {
self.err(d.pos, "Type not defined");
}
}
@@ -707,13 +1005,13 @@ impl Sema {
self.err(pos, "Argument-count mismatch");
}
// Sonderfall INSTR([start%,] s$, such$)
let insts_with_start = name == "INSTR" && args.len() == 3;
let instr_with_start = name == "INSTR" && args.len() == 3;
for (i, a) in args.iter().enumerate() {
if matches!(a, Expr::Missing) {
continue;
}
let at = self.expr_ty(a, scope);
let kind = if insts_with_start {
let kind = if instr_with_start {
match i {
0 => ArgK::N,
_ => ArgK::S,
@@ -868,7 +1166,7 @@ impl Sema {
};
// 1. Konstante
if let Some(t) = self.consts.get(name).cloned() {
if let Some((t, _)) = self.consts.get(name).cloned() {
if args.is_some() {
self.err(pos, "Syntax error");
}
@@ -895,7 +1193,7 @@ impl Sema {
self.err(pos, "Duplicate definition");
return Ty::Unknown;
}
// 3. Nicht unterstützte Hardware-Features → Compile-Fehler
// 3. Nicht unterstützte Features → Compile-Fehler
if banned_feature(&full_name) {
for a in idx {
self.expr_ty(a, scope);
@@ -908,7 +1206,7 @@ impl Sema {
self.check_builtin_args(&full_name, idx, min, max, spec, scope, pos);
return ret_ty(ret);
}
// 4. FUNCTION / DEF FN
// 5. FUNCTION / DEF FN
if let Some(info) = self.procs.get(name).cloned() {
if info.kind == ProcKind::Function {
if idx.len() != info.params.len() {
@@ -922,7 +1220,7 @@ impl Sema {
self.err(pos, "Duplicate definition");
return Ty::Unknown;
}
// 5. Implizites Array (klassisch: DIM x(10) implizit)
// 6. Implizites Array (klassisch: DIM x(10) implizit)
for a in idx {
self.want_num(a, scope);
}
@@ -932,10 +1230,9 @@ impl Sema {
}
let ty = self.name_ty(name, suffix);
let key = self.var_key(name, suffix, false);
scope.vars.insert(
key,
VarInfo { ty: ty.clone(), array: true, explicit: false },
);
scope
.vars
.insert(key, VarInfo { ty: ty.clone(), array: true, explicit: false });
ty
}
None => {
@@ -945,6 +1242,10 @@ impl Sema {
let declared = scope.vars.contains_key(&key)
|| (suffix.is_none() && scope.vars.contains_key(&as_key));
if !declared {
if banned_feature(&full_name) {
self.err(pos, "Feature unavailable");
return Ty::Unknown;
}
if let Some((0, _, _, ret)) = builtin_fn(&full_name) {
return ret_ty(ret);
}
@@ -1023,7 +1324,6 @@ mod tests {
#[test]
fn deftype_regeln() {
// DEFSTR macht s zum String
assert!(diags("DEFSTR S\ns = 5").contains(&"Type mismatch".to_string()));
assert!(diags("DEFINT I\ni = 5").is_empty());
}
@@ -1047,6 +1347,10 @@ mod tests {
assert!(diags("POKE 100, 1").contains(&"Feature unavailable".to_string()));
assert!(diags("x = PEEK(100)").contains(&"Feature unavailable".to_string()));
assert!(diags("p = VARPTR(a%)").contains(&"Feature unavailable".to_string()));
assert!(diags("SOUND 440, 10").contains(&"Feature unavailable".to_string()));
assert!(diags("CHAIN \"prog\"").contains(&"Feature unavailable".to_string()));
assert!(diags("PLAY \"cde\"").contains(&"Feature unavailable".to_string()));
assert!(diags("CIRCLE 1, 2").contains(&"Feature unavailable".to_string()));
}
#[test]
@@ -1058,9 +1362,7 @@ mod tests {
#[test]
fn funktionsaufruf_und_rueckgabe() {
let d = diags(
"FUNCTION Quad (x)\nQuad = x * x\nEND FUNCTION\ny = Quad(3)",
);
let d = diags("FUNCTION Quad (x)\nQuad = x * x\nEND FUNCTION\ny = Quad(3)");
assert!(d.is_empty(), "{d:?}");
}
@@ -1081,4 +1383,50 @@ mod tests {
assert!(diags("x$ = INKEY$").is_empty());
assert!(diags("t! = TIMER").is_empty());
}
#[test]
fn mid_anweisung() {
assert!(diags("s$ = \"hallo\"\nMID$(s$, 2, 2) = \"EY\"").is_empty());
assert!(diags("s$ = \"hallo\"\nMID$(s$, 2) = 5")
.contains(&"Type mismatch".to_string()));
}
#[test]
fn def_fn_blockform() {
let d = diags(
"DEF FNquad (x)\nFNquad = x * x\nEND DEF\ny = FNQUAD(3)",
);
assert!(d.is_empty(), "{d:?}");
}
#[test]
fn udt_feldtypen() {
let src = "TYPE Kunde\nName AS STRING * 30\nUmsatz AS DOUBLE\nEND TYPE\nDIM k AS Kunde\nk.Umsatz = 5\nk.Name = \"x\"";
assert!(diags(src).is_empty(), "{:?}", diags(src));
let bad = "TYPE Kunde\nUmsatz AS DOUBLE\nEND TYPE\nDIM k AS Kunde\nk.Gibtsnicht = 1";
assert!(diags(bad).contains(&"Element not defined".to_string()));
let bad2 = "TYPE Kunde\nName AS STRING * 30\nEND TYPE\nDIM k AS Kunde\nk.Name = 5";
assert!(diags(bad2).contains(&"Type mismatch".to_string()));
}
#[test]
fn shared_und_common() {
let src = "DIM SHARED zaehler%\nSUB Hoch\nSHARED zaehler%\nzaehler% = zaehler% + 1\nEND SUB";
assert!(diags(src).is_empty(), "{:?}", diags(src));
assert!(diags("COMMON SHARED /blk/ a%, b$()").is_empty());
}
#[test]
fn konstantenfaltung() {
assert!(diags("CONST PI = 3.14159, ZWEI.PI = PI * 2").is_empty());
assert!(diags("x = 1\nCONST K = x + 1")
.contains(&"Invalid constant".to_string()));
}
#[test]
fn datei_ea_und_events() {
let src = "OPEN \"test.dat\" FOR RANDOM AS #1 LEN = 64\nCLOSE #1\nOPEN \"o\", #2, \"f.txt\"\nCLOSE\nTIMER ON\nKEY(5) OFF";
assert!(diags(src).is_empty(), "{:?}", diags(src));
assert!(diags("PEN ON").contains(&"Feature unavailable".to_string()));
}
}