add: for sample, fix bugs, and make test harness

This commit is contained in:
2026-03-12 22:39:30 -07:00
parent 944326f114
commit 6c4118c489
11 changed files with 311 additions and 22 deletions

View File

@@ -692,10 +692,13 @@ impl<'ctx> Codegen<'ctx> {
self.builder.position_at_end(bb);
// Call the RPG entry procedure.
// Try the bare name first (CTL-OPT MAIN procedures are not renamed),
// then the `rpg_` prefix used for EXPORT-ed procedures.
let callee = self.module.get_function(rpg_entry)
.or_else(|| self.module.get_function(&format!("rpg_{}", rpg_entry)));
// Try the `rpg_` prefix first (used for EXPORT-ed procedures) so that
// a procedure named "main" resolves to @rpg_main rather than the @main
// wrapper we just created (which would cause infinite recursion).
// Fall back to the bare name for CTL-OPT MAIN procedures, which are
// not exported and therefore not prefixed.
let callee = self.module.get_function(&format!("rpg_{}", rpg_entry))
.or_else(|| self.module.get_function(rpg_entry));
if let Some(rpg_fn) = callee {
self.builder.build_call(rpg_fn, &[], "call_rpg").ok();
}
@@ -1011,14 +1014,33 @@ impl<'ctx> Codegen<'ctx> {
let func = state.function;
let i64_t = self.context.i64_type();
// Allocate loop variable.
let loop_var = self.builder.build_alloca(i64_t, &f.var).unwrap();
// Reuse the existing i64 alloca when the variable has already been
// upgraded by a prior FOR loop (registered as Int(20), 8 bytes).
// If the variable comes from a DCL-S INT(10) declaration it only has a
// 4-byte slot, which is too small to hold an i64 — in that case (and
// when there is no existing local at all) allocate a fresh 8-byte slot.
// Both cases update state.locals so that the rest of the procedure
// reads/writes through the same pointer for the rest of its lifetime.
let loop_var = match state.locals.get(&f.var) {
Some((ptr, TypeSpec::Int(n))) => {
// Already upgraded to Int(20) (8 bytes) by a previous FOR —
// reuse it directly so all loops share the same variable.
if matches!(n.as_ref(), Expression::Literal(Literal::Integer(20))) {
*ptr
} else {
// Declared as a narrower int (e.g. INT(10) = 4 bytes).
// Allocate a fresh 8-byte slot; locals will be updated below.
self.builder.build_alloca(i64_t, &f.var).unwrap()
}
}
_ => self.builder.build_alloca(i64_t, &f.var).unwrap(),
};
let start = self.gen_expression(&f.start, state)?;
let start_i = self.coerce_to_i64(start);
self.builder.build_store(loop_var, start_i).ok();
// Store the loop variable with Int(20) so that byte_size() returns 8,
// matching the i64 alloca above. (Int(10) would give 4 bytes, causing
// a 32-bit load from an 8-byte slot.)
// Update the type to Int(20) so that byte_size() returns 8 and all
// subsequent loads/stores use a full i64 slot. (Int(10) gives 4 bytes,
// which would cause a width mismatch with the i64 alloca above.)
state.locals.insert(f.var.clone(), (loop_var, TypeSpec::Int(Box::new(Expression::Literal(Literal::Integer(20))))));
let cond_bb = self.context.append_basic_block(func, "for_cond");
@@ -1830,7 +1852,7 @@ mod tests {
#[test]
fn ir_hello_world() {
let src = include_str!("../hello.rpg");
let src = include_str!("../samples/hello.rpg");
let prog = lower(src).expect("lower hello.rpg");
let ir = emit_ir(&prog).expect("emit_ir hello.rpg");
// The IR must contain the dsply call and both main functions.
@@ -1853,6 +1875,71 @@ END-PROC;
assert!(ir.contains("for_cond") || ir.contains("br"), "FOR loop should emit branches:\n{}", ir);
}
/// Two FOR loops on the same DCL-S variable (one ascending, one downto)
/// must share a single alloca — matching the semantics of for.rpg.
#[test]
fn ir_for_two_loops_same_var() {
let src = r#"
**FREE
Ctl-Opt Main(For);
Dcl-Proc For;
dcl-s num int(10);
for num = 1 to 3;
dsply ('i = ' + %char(num));
endfor;
for num = 5 downto 1 by 1;
dsply ('i = ' + %char(num));
endfor;
End-Proc For;
"#;
let ir = get_ir(src);
// Both loops must be present (two for_cond labels).
let for_cond_count = ir.matches("for_cond").count();
assert!(
for_cond_count >= 2,
"expected at least two for_cond blocks (one per loop), got {}:\n{}",
for_cond_count,
&ir[..ir.len().min(3000)]
);
// The ascending loop uses sle (signed less-or-equal).
assert!(
ir.contains("icmp sle"),
"ascending loop should use 'icmp sle':\n{}",
&ir[..ir.len().min(3000)]
);
// The downto loop uses sge (signed greater-or-equal).
assert!(
ir.contains("icmp sge"),
"downto loop should use 'icmp sge':\n{}",
&ir[..ir.len().min(3000)]
);
// The decrement instruction must appear for the downto loop.
assert!(
ir.contains("sub i64"),
"downto loop should emit 'sub i64' for decrement:\n{}",
&ir[..ir.len().min(3000)]
);
// There must be exactly one i64 alloca for 'num' — both loops share it.
// Count lines that both allocate i64 and mention 'num'.
let num_i64_allocas = ir
.lines()
.filter(|l| l.contains("alloca i64") && l.contains("num"))
.count();
assert_eq!(
num_i64_allocas, 1,
"both FOR loops must share a single i64 alloca for 'num', found {}:\n{}",
num_i64_allocas,
&ir[..ir.len().min(3000)]
);
}
#[test]
fn ir_if_stmt() {
let src = r#"

View File

@@ -1105,12 +1105,11 @@ impl Parser {
fn parse_paren_ident(&mut self) -> Option<String> {
if self.peek() != &Token::LParen { return None; }
self.advance(); // (
let name = if let Token::Identifier(s) = self.peek().clone() {
self.advance();
Some(s)
} else {
None
};
// Use try_parse_ident_or_name so that names which collide with keywords
// (e.g. `For` in `Ctl-Opt Main(For)`) are accepted. Lowercase the
// result so it matches how procedure names are stored by token_as_name /
// expect_name (which always return lowercase strings for keyword tokens).
let name = self.try_parse_ident_or_name().map(|s| s.to_lowercase());
self.eat(&Token::RParen);
name
}
@@ -2500,7 +2499,7 @@ impl Parser {
fn expect_ident(&mut self) -> Result<String, LowerError> {
match self.advance() {
Token::Identifier(s) => Ok(s),
Token::Identifier(s) => Ok(s.to_lowercase()),
tok => Err(LowerError::new(format!("expected identifier, got {:?}", tok))),
}
}
@@ -2572,7 +2571,7 @@ impl Parser {
/// lowercase or mixed-case spelling that the source would have used.
fn token_as_name(tok: &Token) -> Option<String> {
match tok {
Token::Identifier(s) => Some(s.clone()),
Token::Identifier(s) => Some(s.to_lowercase()),
// Statement / declaration keywords that are commonly used as names.
Token::KwMain => Some("main".into()),
@@ -2814,7 +2813,7 @@ mod tests {
fn lower_dcl_c() {
let p = lower_ok("DCL-C MAX_SIZE CONST(100);");
if let Declaration::Constant(c) = &p.declarations[0] {
assert_eq!(c.name, "MAX_SIZE");
assert_eq!(c.name, "max_size");
}
}
@@ -2840,7 +2839,7 @@ mod tests {
#[test]
fn lower_hello_rpg() {
let hello = include_str!("../hello.rpg");
let hello = include_str!("../samples/hello.rpg");
let p = lower_ok(hello);
assert!(!p.procedures.is_empty(), "should have at least one procedure");
let proc = p.procedures.iter().find(|p| p.name == "main").expect("main proc");

View File

@@ -326,7 +326,7 @@ mod tests {
/// to LLVM IR without errors.
#[test]
fn hello_rpg_emits_ir() {
let src = include_str!("../hello.rpg");
let src = include_str!("../samples/hello.rpg");
let prog = lower(src.trim()).expect("lower hello.rpg");
let ir = emit_ir(&prog).expect("emit_ir hello.rpg");