Iter 2: verschachteltes if, Strings, JSON-Output, deps

- Codegen: current_block-Tracking ersetzt die Heuristik im phi-Lowering;
  verschachtelte if-Ausdrücke produzieren jetzt korrekte LLVM IR.
  examples/max3.ail.json + Test schützt gegen Regression.
- Strings: Lit::Str / Type Str / io/print_str Effekt-Op; Strings sind im
  MVP immutable Konstanten. examples/hello.ail.json als zweiter E2E-Test.
- CLI: --json für manifest und builtins; neuer deps-Subcommand listet
  statische Symbol-Referenzen pro Definition. Effekt-Ops mit Prefix
  effect: markiert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-07 10:28:16 +02:00
parent 2fbcdba0b1
commit 6e6b6a14fb
10 changed files with 431 additions and 49 deletions
+62 -41
View File
@@ -56,6 +56,9 @@ struct Emitter<'a> {
str_counter: u64,
/// Liste aller user-definierten Top-Level-Funktionen (für call-resolution).
user_fns: BTreeMap<String, FnSig>,
/// Aktuelles Basic-Block-Label. Wird von `start_block` gesetzt und ist
/// die einzige Quelle der Wahrheit für `phi`-Operanden.
current_block: String,
}
#[derive(Debug, Clone)]
@@ -88,9 +91,16 @@ impl<'a> Emitter<'a> {
counter: 0,
str_counter: 0,
user_fns,
current_block: String::new(),
}
}
fn start_block(&mut self, label: &str) {
self.body.push_str(label);
self.body.push_str(":\n");
self.current_block = label.to_string();
}
fn finish(self) -> String {
let mut out = String::new();
out.push_str("; AILang generated module: ");
@@ -115,7 +125,8 @@ impl<'a> Emitter<'a> {
out.push('\n');
}
out.push_str("declare i32 @printf(ptr, ...)\n\n");
out.push_str("declare i32 @printf(ptr, ...)\n");
out.push_str("declare i32 @puts(ptr)\n\n");
out.push_str(&self.header);
out.push_str(&self.body);
out
@@ -159,20 +170,26 @@ impl<'a> Emitter<'a> {
fn emit_const(&mut self, c: &ConstDef) -> Result<()> {
let lty = llvm_type(&c.ty)?;
let (val_ty, val) = match &c.value {
Term::Lit { lit } => match lit {
Literal::Int { value } => ("i64".to_string(), value.to_string()),
Literal::Bool { value } => {
("i1".to_string(), if *value { "true".into() } else { "false".into() })
}
Literal::Unit => ("i8".to_string(), "0".to_string()),
},
let lit = match &c.value {
Term::Lit { lit } => lit,
_ => {
return Err(CodegenError::Internal(
"MVP: const muss Literal sein".into(),
));
}
};
let (val_ty, val) = match lit {
Literal::Int { value } => ("i64".to_string(), value.to_string()),
Literal::Bool { value } => (
"i1".to_string(),
if *value { "true".into() } else { "false".into() },
),
Literal::Unit => ("i8".to_string(), "0".to_string()),
Literal::Str { value } => {
let g = self.intern_string("str", value);
("ptr".to_string(), format!("@{g}"))
}
};
if val_ty != lty {
return Err(CodegenError::Internal(format!(
"const type mismatch: {} vs {}",
@@ -220,7 +237,7 @@ impl<'a> Emitter<'a> {
}
sig.push_str(") {\n");
self.body.push_str(&sig);
self.body.push_str("entry:\n");
self.start_block("entry");
let (val, val_ty) = self.lower_term(&f.body)?;
if val_ty != llvm_ret {
@@ -243,6 +260,12 @@ impl<'a> Emitter<'a> {
if *value { "true".into() } else { "false".into() },
"i1".into(),
),
Literal::Str { value } => {
// Globale Konstante anlegen; in opaque-pointer-LLVM
// ist `@name` direkt ein gültiger `ptr`.
let g = self.intern_string("str", value);
(format!("@{g}"), "ptr".into())
}
Literal::Unit => ("0".into(), "i8".into()),
}),
Term::Var { name } => {
@@ -275,24 +298,24 @@ impl<'a> Emitter<'a> {
" br i1 {cond_v}, label %{then_lbl}, label %{else_lbl}\n"
));
self.body.push_str(&format!("{then_lbl}:\n"));
self.start_block(&then_lbl);
let (then_v, then_ty) = self.lower_term(then)?;
let then_block_end = self.current_block_label_for_phi(&then_lbl);
self.body
.push_str(&format!(" br label %{join_lbl}\n"));
// Verschachtelter Code im `then`-Body kann das Block-Label
// verändert haben — phi muss den letzten tatsächlichen Block sehen.
let then_block_end = self.current_block.clone();
self.body.push_str(&format!(" br label %{join_lbl}\n"));
self.body.push_str(&format!("{else_lbl}:\n"));
self.start_block(&else_lbl);
let (else_v, else_ty) = self.lower_term(else_)?;
if then_ty != else_ty {
return Err(CodegenError::Internal(format!(
"if branches type mismatch: {then_ty} vs {else_ty}"
)));
}
let else_block_end = self.current_block_label_for_phi(&else_lbl);
self.body
.push_str(&format!(" br label %{join_lbl}\n"));
let else_block_end = self.current_block.clone();
self.body.push_str(&format!(" br label %{join_lbl}\n"));
self.body.push_str(&format!("{join_lbl}:\n"));
self.start_block(&join_lbl);
let phi = self.fresh_ssa();
self.body.push_str(&format!(
" {phi} = phi {ty} [ {tv}, %{tlbl} ], [ {ev}, %{elbl} ]\n",
@@ -396,6 +419,22 @@ impl<'a> Emitter<'a> {
));
Ok(("0".into(), "i8".into()))
}
"io/print_str" => {
if args.len() != 1 {
return Err(CodegenError::Internal(
"io/print_str arity".into(),
));
}
let (v, vty) = self.lower_term(&args[0])?;
if vty != "ptr" {
return Err(CodegenError::Internal(
"io/print_str needs ptr".into(),
));
}
self.body
.push_str(&format!(" call i32 @puts(ptr {v})\n"));
Ok(("0".into(), "i8".into()))
}
"io/print_bool" => {
if args.len() != 1 {
return Err(CodegenError::Internal(
@@ -418,17 +457,17 @@ impl<'a> Emitter<'a> {
self.body.push_str(&format!(
" br i1 {v}, label %{then_lbl}, label %{else_lbl}\n"
));
self.body.push_str(&format!("{then_lbl}:\n"));
self.start_block(&then_lbl);
self.body.push_str(&format!(
" call i32 (ptr, ...) @printf(ptr @{fmt_t})\n"
));
self.body.push_str(&format!(" br label %{join_lbl}\n"));
self.body.push_str(&format!("{else_lbl}:\n"));
self.start_block(&else_lbl);
self.body.push_str(&format!(
" call i32 (ptr, ...) @printf(ptr @{fmt_f})\n"
));
self.body.push_str(&format!(" br label %{join_lbl}\n"));
self.body.push_str(&format!("{join_lbl}:\n"));
self.start_block(&join_lbl);
Ok(("0".into(), "i8".into()))
}
other => Err(CodegenError::Internal(format!(
@@ -446,25 +485,6 @@ impl<'a> Emitter<'a> {
self.counter
}
/// Im MVP haben wir keinen verschachtelten Control-Flow innerhalb von
/// `then`/`else` von `if`, also entspricht das End-Label dem Block-Anfang.
/// Das ändert sich, sobald `if` rekursiv andere `if`s enthält — dann muss
/// das tatsächlich aktuelle Label am Phi-Punkt verwendet werden.
fn current_block_label_for_phi(&self, fallback: &str) -> String {
// Heuristik: scanne self.body rückwärts nach dem letzten Label-Header.
// Das ist robuster als anzunehmen, dass der ursprüngliche Block-Header
// noch der aktuelle ist.
for line in self.body.lines().rev() {
let line = line.trim_end();
if let Some(s) = line.strip_suffix(':') {
if !s.starts_with(' ') && !s.is_empty() && !s.contains(' ') {
return s.to_string();
}
}
}
fallback.to_string()
}
fn intern_string(&mut self, hint: &str, content: &str) -> String {
if let Some((name, _)) = self.strings.get(content) {
return name.clone();
@@ -484,6 +504,7 @@ fn llvm_type(t: &Type) -> Result<String> {
"Int" => Ok("i64".into()),
"Bool" => Ok("i1".into()),
"Unit" => Ok("i8".into()),
"Str" => Ok("ptr".into()),
other => Err(CodegenError::UnsupportedType(other.into())),
},
other => Err(CodegenError::UnsupportedType(