add: fib sample

This commit is contained in:
2026-03-12 22:19:42 -07:00
parent 073c86d784
commit 31a6c8b91b
7 changed files with 756 additions and 46 deletions

View File

@@ -45,6 +45,24 @@ pub fn lower(source: &str) -> Result<Program, LowerError> {
Ok(program)
}
/// Strip RPG IV compiler directives that start with `**` (e.g. `**FREE`,
/// `**CTDATA`) by blanking out those lines before tokenization.
fn strip_star_star_directives(source: &str) -> String {
source
.lines()
.map(|line| {
let trimmed = line.trim_start();
if trimmed.starts_with("**") {
// Replace with an empty line so line numbers stay consistent.
""
} else {
line
}
})
.collect::<Vec<_>>()
.join("\n")
}
// ─────────────────────────────────────────────────────────────────────────────
// Error type
// ─────────────────────────────────────────────────────────────────────────────
@@ -52,11 +70,17 @@ pub fn lower(source: &str) -> Result<Program, LowerError> {
#[derive(Debug)]
pub struct LowerError {
pub message: String,
/// 1-based source line where the error was detected, if known.
pub line: Option<usize>,
}
impl std::fmt::Display for LowerError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "lower error: {}", self.message)
if let Some(ln) = self.line {
write!(f, "lower error (line {}): {}", ln, self.message)
} else {
write!(f, "lower error: {}", self.message)
}
}
}
@@ -64,7 +88,11 @@ impl std::error::Error for LowerError {}
impl LowerError {
fn new(msg: impl Into<String>) -> Self {
LowerError { message: msg.into() }
LowerError { message: msg.into(), line: None }
}
fn at(line: usize, msg: impl Into<String>) -> Self {
LowerError { message: msg.into(), line: Some(line) }
}
}
@@ -385,12 +413,22 @@ enum Token {
// ─────────────────────────────────────────────────────────────────────────────
fn tokenize(source: &str) -> Result<Vec<Token>, LowerError> {
let chars: Vec<char> = source.chars().collect();
// Strip **FREE / **CTDATA / any **word compiler directives first.
let cleaned = strip_star_star_directives(source);
let chars: Vec<char> = cleaned.chars().collect();
let mut pos = 0;
let mut tokens = Vec::new();
let mut line: usize = 1;
while pos < chars.len() {
// Skip whitespace
// Track line numbers.
if chars[pos] == '\n' {
line += 1;
pos += 1;
continue;
}
// Skip other whitespace
if chars[pos].is_whitespace() {
pos += 1;
continue;
@@ -490,6 +528,14 @@ fn tokenize(source: &str) -> Result<Vec<Token>, LowerError> {
'=' => { tokens.push(Token::OpEq); pos += 1; continue; }
'*' => {
if pos + 1 < chars.len() && chars[pos + 1] == '*' {
// `**word` — a compiler directive that escaped pre-processing;
// treat the rest of the line as a comment and skip it.
if pos + 2 < chars.len() && chars[pos + 2].is_alphabetic() {
while pos < chars.len() && chars[pos] != '\n' {
pos += 1;
}
continue;
}
tokens.push(Token::OpStar2);
pos += 2;
} else {
@@ -704,6 +750,7 @@ fn tokenize(source: &str) -> Result<Vec<Token>, LowerError> {
}
tokens.push(Token::Eof);
let _ = line; // line tracking available for future per-token storage
Ok(tokens)
}
@@ -873,11 +920,12 @@ fn keyword_or_ident(upper: &str, original: &str) -> Token {
struct Parser {
tokens: Vec<Token>,
pos: usize,
_line: usize,
}
impl Parser {
fn new(tokens: Vec<Token>) -> Self {
Parser { tokens, pos: 0 }
Parser { tokens, pos: 0, _line: 1 }
}
fn peek(&self) -> &Token {
@@ -901,7 +949,10 @@ impl Parser {
if &tok == expected {
Ok(())
} else {
Err(LowerError::new(format!("expected {:?}, got {:?}", expected, tok)))
Err(LowerError::new(format!(
"expected {:?}, got {:?} (token index {})",
expected, tok, self.pos
)))
}
}
@@ -927,12 +978,21 @@ impl Parser {
fn parse_program(&mut self) -> Result<Program, LowerError> {
let mut declarations = Vec::new();
let mut procedures = Vec::new();
let mut skipped_tokens: Vec<String> = Vec::new();
while !self.is_eof() {
match self.peek() {
Token::KwDclProc => {
if let Ok(p) = self.parse_procedure() {
procedures.push(p);
if !skipped_tokens.is_empty() {
skipped_tokens.clear();
}
match self.parse_procedure() {
Ok(p) => procedures.push(p),
Err(e) => {
eprintln!("warning: skipping procedure due to parse error: {}", e);
// Recover by advancing past the current token.
self.advance();
}
}
}
Token::KwCtlOpt |
@@ -941,17 +1001,34 @@ impl Parser {
Token::KwDclDs |
Token::KwDclF |
Token::KwBegSr => {
if let Ok(d) = self.parse_declaration() {
declarations.push(d);
if !skipped_tokens.is_empty() {
skipped_tokens.clear();
}
match self.parse_declaration() {
Ok(d) => declarations.push(d),
Err(e) => {
eprintln!("warning: skipping declaration due to parse error: {}", e);
self.advance();
}
}
}
_ => {
// Skip unrecognised top-level tokens
tok => {
// Accumulate unrecognised top-level tokens so we can report
// them as a meaningful diagnostic.
skipped_tokens.push(format!("{:?}", tok));
self.advance();
}
}
}
if !skipped_tokens.is_empty() {
eprintln!(
"warning: {} unrecognised top-level token(s) were skipped: {}",
skipped_tokens.len(),
skipped_tokens.join(", ")
);
}
Ok(Program { declarations, procedures })
}
@@ -965,7 +1042,11 @@ impl Parser {
Token::KwDclDs => self.parse_dcl_ds(),
Token::KwDclF => self.parse_dcl_f(),
Token::KwBegSr => self.parse_subroutine(),
tok => Err(LowerError::new(format!("unexpected token in declaration: {:?}", tok))),
tok => Err(LowerError::new(format!(
"unexpected token in declaration: {:?}\
expected one of CTL-OPT, DCL-S, DCL-C, DCL-DS, DCL-F, BEG-SR",
tok
))),
}
}
@@ -1256,6 +1337,18 @@ impl Parser {
fn parse_var_keyword(&mut self) -> VarKeyword {
match self.peek().clone() {
Token::KwDim => {
self.advance(); // KwDim
if self.peek() == &Token::LParen {
self.advance(); // (
if let Ok(expr) = self.parse_expression() {
self.eat(&Token::RParen);
return VarKeyword::Dim(expr);
}
self.eat(&Token::RParen);
}
VarKeyword::Other("DIM".to_string())
}
Token::KwInz => {
self.advance();
if self.peek() == &Token::LParen {
@@ -1342,6 +1435,10 @@ impl Parser {
// Body statements until END-PROC
let body = self.parse_statement_list(&[Token::KwEndProc]);
self.eat(&Token::KwEndProc);
// RPG IV allows an optional procedure name after END-PROC:
// End-Proc Perform_Fibonacci_Sequence;
// Consume it (any name-like token) so it doesn't leak to parse_program.
let _ = self.try_parse_name();
self.eat_semicolon();
Ok(Procedure { name, exported, pi, locals, body })
@@ -1893,6 +1990,8 @@ impl Parser {
if self.peek() == &Token::LParen {
// Peek ahead to decide: call or subscript-assignment?
// If after the matching ')' we see '=' it's an assignment, else call.
// NOTE: `name` is already consumed, so we save pos at '(' and scan
// forward without rewinding past the name.
let saved = self.pos;
self.advance(); // (
let mut depth = 1;
@@ -1904,11 +2003,22 @@ impl Parser {
}
}
let is_assign = self.peek() == &Token::OpEq;
self.pos = saved; // rewind
self.pos = saved; // rewind to '('
if is_assign {
// subscript assignment: `name(idx) = expr;`
let lv = self.parse_lvalue()?;
// Build LValue directly using the already-consumed `name`
// instead of calling parse_lvalue() (which would try to
// re-consume the name from the current position which is '(').
let qname = QualifiedName::simple(name.clone());
let mut indices = Vec::new();
self.advance(); // consume '('
indices.push(self.parse_expression()?);
while self.eat(&Token::Colon) {
indices.push(self.parse_expression()?);
}
self.eat(&Token::RParen);
let lv = LValue::Index(qname, indices);
self.expect(&Token::OpEq)?;
let value = self.parse_expression()?;
self.eat_semicolon();
@@ -2221,7 +2331,9 @@ impl Parser {
fn parse_builtin_expr(&mut self) -> Result<Expression, LowerError> {
let bif_tok = self.advance();
self.expect(&Token::LParen)?;
self.expect(&Token::LParen).map_err(|e| LowerError::new(format!(
"built-in function {:?}: {}", bif_tok, e.message
)))?;
let bif = match bif_tok {
Token::BifLen => {
let e = self.parse_expression()?;
@@ -2277,6 +2389,11 @@ impl Parser {
self.eat(&Token::RParen);
BuiltIn::Error
}
Token::BifElem => {
let e = self.parse_expression()?;
self.eat(&Token::RParen);
BuiltIn::Elem(Box::new(e))
}
Token::BifSize => {
let e = self.parse_expression()?;
self.eat(&Token::RParen);