add: fib sample
This commit is contained in:
149
src/lower.rs
149
src/lower.rs
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user