MVP: AILang-Sprache mit JSON-AST, Typchecker, LLVM-IR-Backend
Erste lauffähige Iteration. examples/sum.ail.json wird zu nativem Binary kompiliert und druckt 55 (Summe 1..10) als End-to-End-Test. Architektur: - ailang-core: hashbares JSON-AST + canonical-form + pretty-printer - ailang-check: monomorpher HM-Subset + Effekt-Set-Tracking - ailang-codegen: LLVM-IR-Text-Emitter (kein libllvm-link) - ail: CLI mit check/manifest/render/describe/emit-ir/build/builtins Designentscheidungen sind in docs/DESIGN.md dokumentiert; der Verlauf in docs/JOURNAL.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "ailang-codegen"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
ailang-core.workspace = true
|
||||
ailang-check.workspace = true
|
||||
thiserror.workspace = true
|
||||
indexmap.workspace = true
|
||||
@@ -0,0 +1,581 @@
|
||||
//! LLVM-IR-Text-Emitter für AILang (MVP).
|
||||
//!
|
||||
//! Strategie: Wir erzeugen LLVM-IR als String, schreiben sie als `.ll` und
|
||||
//! linken sie mit `clang`. Keine Bindung an eine bestimmte libllvm-Version.
|
||||
//!
|
||||
//! Typ-Mapping:
|
||||
//! - `Int` -> `i64`
|
||||
//! - `Bool` -> `i1`
|
||||
//! - `Unit` -> `i8` (Wert immer 0)
|
||||
//!
|
||||
//! Named-Mangling: AILang-Fn `foo` wird zu LLVM `@ail_foo`. Wenn ein Modul
|
||||
//! eine Funktion `main : () -> Unit !IO` hat, wird zusätzlich ein
|
||||
//! `define i32 @main()` Wrapper erzeugt, der `@ail_main` aufruft und 0
|
||||
//! zurückgibt — damit das Binary direkt ausführbar ist.
|
||||
|
||||
use ailang_core::ast::*;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum CodegenError {
|
||||
#[error("def `{0}`: {1}")]
|
||||
Def(String, Box<CodegenError>),
|
||||
|
||||
#[error("unsupported type: {0}")]
|
||||
UnsupportedType(String),
|
||||
|
||||
#[error("unknown variable: `{0}`")]
|
||||
UnknownVar(String),
|
||||
|
||||
#[error("expected fn type, got {0}")]
|
||||
NotFnType(String),
|
||||
|
||||
#[error("internal: {0}")]
|
||||
Internal(String),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, CodegenError>;
|
||||
|
||||
pub fn emit_ir(m: &Module) -> Result<String> {
|
||||
let mut emitter = Emitter::new(m);
|
||||
emitter.emit_module()?;
|
||||
Ok(emitter.finish())
|
||||
}
|
||||
|
||||
struct Emitter<'a> {
|
||||
module: &'a Module,
|
||||
header: String,
|
||||
body: String,
|
||||
/// String-Konstanten: content -> (global-name, llvm-typ-länge inkl. \0)
|
||||
strings: BTreeMap<String, (String, usize)>,
|
||||
/// Lokale Symboltabelle pro Funktion: name -> (ssa-name inkl `%`, llvm-typ).
|
||||
locals: Vec<(String, String, String)>,
|
||||
/// fortlaufender Zähler für SSA-Werte und Labels.
|
||||
counter: u64,
|
||||
/// fortlaufender Zähler für globale String-Namen.
|
||||
str_counter: u64,
|
||||
/// Liste aller user-definierten Top-Level-Funktionen (für call-resolution).
|
||||
user_fns: BTreeMap<String, FnSig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct FnSig {
|
||||
params: Vec<String>, // llvm types
|
||||
ret: String, // llvm type
|
||||
}
|
||||
|
||||
impl<'a> Emitter<'a> {
|
||||
fn new(module: &'a Module) -> Self {
|
||||
let mut user_fns = BTreeMap::new();
|
||||
for def in &module.defs {
|
||||
if let Def::Fn(f) = def {
|
||||
if let Type::Fn { params, ret, .. } = &f.ty {
|
||||
let psig: Result<Vec<String>> =
|
||||
params.iter().map(llvm_type).collect();
|
||||
let rsig = llvm_type(ret);
|
||||
if let (Ok(params), Ok(ret)) = (psig, rsig) {
|
||||
user_fns.insert(f.name.clone(), FnSig { params, ret });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Self {
|
||||
module,
|
||||
header: String::new(),
|
||||
body: String::new(),
|
||||
strings: BTreeMap::new(),
|
||||
locals: Vec::new(),
|
||||
counter: 0,
|
||||
str_counter: 0,
|
||||
user_fns,
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(self) -> String {
|
||||
let mut out = String::new();
|
||||
out.push_str("; AILang generated module: ");
|
||||
out.push_str(&self.module.name);
|
||||
out.push('\n');
|
||||
out.push_str("source_filename = \"");
|
||||
out.push_str(&self.module.name);
|
||||
out.push_str(".ail\"\n");
|
||||
out.push_str("target triple = \"");
|
||||
out.push_str(default_triple());
|
||||
out.push_str("\"\n\n");
|
||||
|
||||
// Globals first.
|
||||
for (content, (name, _)) in &self.strings {
|
||||
let escaped = ll_string_literal(content);
|
||||
let len = c_byte_len(content);
|
||||
out.push_str(&format!(
|
||||
"@{name} = private unnamed_addr constant [{len} x i8] c\"{escaped}\", align 1\n",
|
||||
));
|
||||
}
|
||||
if !self.strings.is_empty() {
|
||||
out.push('\n');
|
||||
}
|
||||
|
||||
out.push_str("declare i32 @printf(ptr, ...)\n\n");
|
||||
out.push_str(&self.header);
|
||||
out.push_str(&self.body);
|
||||
out
|
||||
}
|
||||
|
||||
fn emit_module(&mut self) -> Result<()> {
|
||||
let defs: Vec<&Def> = self.module.defs.iter().collect();
|
||||
for def in defs {
|
||||
match def {
|
||||
Def::Fn(f) => {
|
||||
self.emit_fn(f).map_err(|e| {
|
||||
CodegenError::Def(f.name.clone(), Box::new(e))
|
||||
})?;
|
||||
}
|
||||
Def::Const(c) => {
|
||||
self.emit_const(c).map_err(|e| {
|
||||
CodegenError::Def(c.name.clone(), Box::new(e))
|
||||
})?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// main-Wrapper, falls vorhanden.
|
||||
if let Some(Def::Fn(main_fn)) = self
|
||||
.module
|
||||
.defs
|
||||
.iter()
|
||||
.find(|d| d.name() == "main")
|
||||
{
|
||||
if let Type::Fn { params, ret, .. } = &main_fn.ty {
|
||||
if params.is_empty() && matches!(ret.as_ref(), Type::Con { name } if name == "Unit")
|
||||
{
|
||||
self.body.push_str(
|
||||
"\ndefine i32 @main() {\n call i8 @ail_main()\n ret i32 0\n}\n",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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()),
|
||||
},
|
||||
_ => {
|
||||
return Err(CodegenError::Internal(
|
||||
"MVP: const muss Literal sein".into(),
|
||||
));
|
||||
}
|
||||
};
|
||||
if val_ty != lty {
|
||||
return Err(CodegenError::Internal(format!(
|
||||
"const type mismatch: {} vs {}",
|
||||
lty, val_ty
|
||||
)));
|
||||
}
|
||||
self.header.push_str(&format!(
|
||||
"@ail_{name} = constant {ty} {val}\n",
|
||||
name = c.name,
|
||||
ty = lty,
|
||||
val = val,
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn emit_fn(&mut self, f: &FnDef) -> Result<()> {
|
||||
let (param_tys, ret_ty) = match &f.ty {
|
||||
Type::Fn { params, ret, .. } => (params.clone(), (**ret).clone()),
|
||||
other => {
|
||||
return Err(CodegenError::NotFnType(
|
||||
ailang_core::pretty::type_to_string(other),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let llvm_param_tys: Vec<String> =
|
||||
param_tys.iter().map(llvm_type).collect::<Result<_>>()?;
|
||||
let llvm_ret = llvm_type(&ret_ty)?;
|
||||
|
||||
self.locals.clear();
|
||||
self.counter = 0;
|
||||
|
||||
let mut sig = format!("define {ret} @ail_{name}(", ret = llvm_ret, name = f.name);
|
||||
for (i, (pname, pty)) in f.params.iter().zip(llvm_param_tys.iter()).enumerate() {
|
||||
if i > 0 {
|
||||
sig.push_str(", ");
|
||||
}
|
||||
// SSA-Argumentname: %arg_<name>
|
||||
sig.push_str(&format!("{} %arg_{}", pty, pname));
|
||||
self.locals.push((
|
||||
pname.clone(),
|
||||
format!("%arg_{}", pname),
|
||||
pty.clone(),
|
||||
));
|
||||
}
|
||||
sig.push_str(") {\n");
|
||||
self.body.push_str(&sig);
|
||||
self.body.push_str("entry:\n");
|
||||
|
||||
let (val, val_ty) = self.lower_term(&f.body)?;
|
||||
if val_ty != llvm_ret {
|
||||
return Err(CodegenError::Internal(format!(
|
||||
"fn `{}`: body type {val_ty} != return type {llvm_ret}",
|
||||
f.name
|
||||
)));
|
||||
}
|
||||
self.body
|
||||
.push_str(&format!(" ret {val_ty} {val}\n}}\n\n"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Lowert einen Term zu (SSA-Value-String, LLVM-Typ).
|
||||
fn lower_term(&mut self, t: &Term) -> Result<(String, String)> {
|
||||
match t {
|
||||
Term::Lit { lit } => Ok(match lit {
|
||||
Literal::Int { value } => (value.to_string(), "i64".into()),
|
||||
Literal::Bool { value } => (
|
||||
if *value { "true".into() } else { "false".into() },
|
||||
"i1".into(),
|
||||
),
|
||||
Literal::Unit => ("0".into(), "i8".into()),
|
||||
}),
|
||||
Term::Var { name } => {
|
||||
if let Some((_, ssa, ty)) = self.locals.iter().rev().find(|(n, _, _)| n == name) {
|
||||
Ok((ssa.clone(), ty.clone()))
|
||||
} else {
|
||||
Err(CodegenError::UnknownVar(name.clone()))
|
||||
}
|
||||
}
|
||||
Term::Let { name, value, body } => {
|
||||
let (val_ssa, val_ty) = self.lower_term(value)?;
|
||||
self.locals.push((name.clone(), val_ssa, val_ty));
|
||||
let r = self.lower_term(body);
|
||||
self.locals.pop();
|
||||
r
|
||||
}
|
||||
Term::If { cond, then, else_ } => {
|
||||
let (cond_v, cond_ty) = self.lower_term(cond)?;
|
||||
if cond_ty != "i1" {
|
||||
return Err(CodegenError::Internal(format!(
|
||||
"if cond not i1: {cond_ty}"
|
||||
)));
|
||||
}
|
||||
let id = self.fresh_id();
|
||||
let then_lbl = format!("then.{id}");
|
||||
let else_lbl = format!("else.{id}");
|
||||
let join_lbl = format!("join.{id}");
|
||||
|
||||
self.body.push_str(&format!(
|
||||
" br i1 {cond_v}, label %{then_lbl}, label %{else_lbl}\n"
|
||||
));
|
||||
|
||||
self.body.push_str(&format!("{then_lbl}:\n"));
|
||||
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"));
|
||||
|
||||
self.body.push_str(&format!("{else_lbl}:\n"));
|
||||
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"));
|
||||
|
||||
self.body.push_str(&format!("{join_lbl}:\n"));
|
||||
let phi = self.fresh_ssa();
|
||||
self.body.push_str(&format!(
|
||||
" {phi} = phi {ty} [ {tv}, %{tlbl} ], [ {ev}, %{elbl} ]\n",
|
||||
ty = then_ty,
|
||||
tv = then_v,
|
||||
tlbl = then_block_end,
|
||||
ev = else_v,
|
||||
elbl = else_block_end,
|
||||
));
|
||||
Ok((phi, then_ty))
|
||||
}
|
||||
Term::App { callee, args } => {
|
||||
let name = match callee.as_ref() {
|
||||
Term::Var { name } => name.clone(),
|
||||
_ => {
|
||||
return Err(CodegenError::Internal(
|
||||
"MVP: callee muss Variable sein".into(),
|
||||
));
|
||||
}
|
||||
};
|
||||
self.lower_app(&name, args)
|
||||
}
|
||||
Term::Do { op, args } => self.lower_effect_op(op, args),
|
||||
}
|
||||
}
|
||||
|
||||
fn lower_app(&mut self, name: &str, args: &[Term]) -> Result<(String, String)> {
|
||||
// Built-in arithmetic / comparison.
|
||||
if let Some((instr, ret_ty)) = builtin_binop(name) {
|
||||
if args.len() != 2 {
|
||||
return Err(CodegenError::Internal(format!(
|
||||
"builtin `{name}` expected 2 args"
|
||||
)));
|
||||
}
|
||||
let (a, _) = self.lower_term(&args[0])?;
|
||||
let (b, _) = self.lower_term(&args[1])?;
|
||||
let dst = self.fresh_ssa();
|
||||
self.body.push_str(&format!(
|
||||
" {dst} = {instr} i64 {a}, {b}\n"
|
||||
));
|
||||
return Ok((dst, ret_ty.into()));
|
||||
}
|
||||
if name == "not" {
|
||||
if args.len() != 1 {
|
||||
return Err(CodegenError::Internal("not arity".into()));
|
||||
}
|
||||
let (a, _) = self.lower_term(&args[0])?;
|
||||
let dst = self.fresh_ssa();
|
||||
self.body
|
||||
.push_str(&format!(" {dst} = xor i1 {a}, true\n"));
|
||||
return Ok((dst, "i1".into()));
|
||||
}
|
||||
|
||||
// User-Funktion?
|
||||
if let Some(sig) = self.user_fns.get(name).cloned() {
|
||||
let mut compiled_args = Vec::new();
|
||||
for (a, exp_ty) in args.iter().zip(sig.params.iter()) {
|
||||
let (v, vty) = self.lower_term(a)?;
|
||||
if &vty != exp_ty {
|
||||
return Err(CodegenError::Internal(format!(
|
||||
"call `{name}` arg type mismatch: expected {exp_ty}, got {vty}"
|
||||
)));
|
||||
}
|
||||
compiled_args.push((v, vty));
|
||||
}
|
||||
let arglist = compiled_args
|
||||
.iter()
|
||||
.map(|(v, t)| format!("{t} {v}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
let dst = self.fresh_ssa();
|
||||
self.body.push_str(&format!(
|
||||
" {dst} = call {ret} @ail_{name}({arglist})\n",
|
||||
ret = sig.ret,
|
||||
));
|
||||
return Ok((dst, sig.ret));
|
||||
}
|
||||
|
||||
Err(CodegenError::Internal(format!(
|
||||
"unknown callee: `{name}`"
|
||||
)))
|
||||
}
|
||||
|
||||
fn lower_effect_op(&mut self, op: &str, args: &[Term]) -> Result<(String, String)> {
|
||||
match op {
|
||||
"io/print_int" => {
|
||||
if args.len() != 1 {
|
||||
return Err(CodegenError::Internal(
|
||||
"io/print_int arity".into(),
|
||||
));
|
||||
}
|
||||
let (v, vty) = self.lower_term(&args[0])?;
|
||||
if vty != "i64" {
|
||||
return Err(CodegenError::Internal(
|
||||
"io/print_int needs i64".into(),
|
||||
));
|
||||
}
|
||||
let fmt = self.intern_string("fmt_int", "%lld\n");
|
||||
self.body.push_str(&format!(
|
||||
" call i32 (ptr, ...) @printf(ptr @{fmt}, i64 {v})\n"
|
||||
));
|
||||
Ok(("0".into(), "i8".into()))
|
||||
}
|
||||
"io/print_bool" => {
|
||||
if args.len() != 1 {
|
||||
return Err(CodegenError::Internal(
|
||||
"io/print_bool arity".into(),
|
||||
));
|
||||
}
|
||||
let (v, vty) = self.lower_term(&args[0])?;
|
||||
if vty != "i1" {
|
||||
return Err(CodegenError::Internal(
|
||||
"io/print_bool needs i1".into(),
|
||||
));
|
||||
}
|
||||
// Drucke "true\n" oder "false\n".
|
||||
let fmt_t = self.intern_string("fmt_true", "true\n");
|
||||
let fmt_f = self.intern_string("fmt_false", "false\n");
|
||||
let id = self.fresh_id();
|
||||
let then_lbl = format!("ptbl_t.{id}");
|
||||
let else_lbl = format!("ptbl_f.{id}");
|
||||
let join_lbl = format!("ptbl_j.{id}");
|
||||
self.body.push_str(&format!(
|
||||
" br i1 {v}, label %{then_lbl}, label %{else_lbl}\n"
|
||||
));
|
||||
self.body.push_str(&format!("{then_lbl}:\n"));
|
||||
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.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"));
|
||||
Ok(("0".into(), "i8".into()))
|
||||
}
|
||||
other => Err(CodegenError::Internal(format!(
|
||||
"unknown effect op: {other}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
fn fresh_ssa(&mut self) -> String {
|
||||
self.counter += 1;
|
||||
format!("%v{}", self.counter)
|
||||
}
|
||||
fn fresh_id(&mut self) -> u64 {
|
||||
self.counter += 1;
|
||||
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();
|
||||
}
|
||||
let name = format!(".str_{}_{}", hint, self.str_counter);
|
||||
self.str_counter += 1;
|
||||
let len = c_byte_len(content);
|
||||
self.strings
|
||||
.insert(content.to_string(), (name.clone(), len));
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
fn llvm_type(t: &Type) -> Result<String> {
|
||||
match t {
|
||||
Type::Con { name } => match name.as_str() {
|
||||
"Int" => Ok("i64".into()),
|
||||
"Bool" => Ok("i1".into()),
|
||||
"Unit" => Ok("i8".into()),
|
||||
other => Err(CodegenError::UnsupportedType(other.into())),
|
||||
},
|
||||
other => Err(CodegenError::UnsupportedType(
|
||||
ailang_core::pretty::type_to_string(other),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn builtin_binop(name: &str) -> Option<(&'static str, &'static str)> {
|
||||
Some(match name {
|
||||
"+" => ("add", "i64"),
|
||||
"-" => ("sub", "i64"),
|
||||
"*" => ("mul", "i64"),
|
||||
"/" => ("sdiv", "i64"),
|
||||
"%" => ("srem", "i64"),
|
||||
"==" => ("icmp eq", "i1"),
|
||||
"!=" => ("icmp ne", "i1"),
|
||||
"<" => ("icmp slt", "i1"),
|
||||
"<=" => ("icmp sle", "i1"),
|
||||
">" => ("icmp sgt", "i1"),
|
||||
">=" => ("icmp sge", "i1"),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
fn c_byte_len(s: &str) -> usize {
|
||||
s.as_bytes().len() + 1 // + NUL
|
||||
}
|
||||
|
||||
/// Escapt einen String für LLVM IR `c"..."`. Alle Bytes außerhalb von
|
||||
/// 0x20..0x7E werden als `\HH` escapt; `"` und `\` ebenfalls. Endet mit `\00`.
|
||||
fn default_triple() -> &'static str {
|
||||
// Im MVP fragen wir den Compile-Host. Für Cross-Compilation müsste man das
|
||||
// konfigurierbar machen — kein Bedarf jetzt.
|
||||
if cfg!(target_os = "linux") && cfg!(target_arch = "x86_64") {
|
||||
"x86_64-pc-linux-gnu"
|
||||
} else if cfg!(target_os = "macos") && cfg!(target_arch = "aarch64") {
|
||||
"arm64-apple-darwin"
|
||||
} else if cfg!(target_os = "macos") && cfg!(target_arch = "x86_64") {
|
||||
"x86_64-apple-darwin"
|
||||
} else if cfg!(target_arch = "aarch64") {
|
||||
"aarch64-unknown-linux-gnu"
|
||||
} else {
|
||||
"x86_64-pc-linux-gnu"
|
||||
}
|
||||
}
|
||||
|
||||
fn ll_string_literal(s: &str) -> String {
|
||||
let mut out = String::new();
|
||||
for &b in s.as_bytes() {
|
||||
match b {
|
||||
b'"' => out.push_str("\\22"),
|
||||
b'\\' => out.push_str("\\5C"),
|
||||
0x20..=0x7E => out.push(b as char),
|
||||
_ => out.push_str(&format!("\\{:02X}", b)),
|
||||
}
|
||||
}
|
||||
out.push_str("\\00");
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use ailang_core::SCHEMA;
|
||||
|
||||
#[test]
|
||||
fn emits_arith_fn() {
|
||||
let m = Module {
|
||||
schema: SCHEMA.into(),
|
||||
name: "t".into(),
|
||||
imports: vec![],
|
||||
defs: vec![Def::Fn(FnDef {
|
||||
name: "add".into(),
|
||||
ty: Type::Fn {
|
||||
params: vec![Type::int(), Type::int()],
|
||||
ret: Box::new(Type::int()),
|
||||
effects: vec![],
|
||||
},
|
||||
params: vec!["a".into(), "b".into()],
|
||||
body: Term::App {
|
||||
callee: Box::new(Term::Var { name: "+".into() }),
|
||||
args: vec![
|
||||
Term::Var { name: "a".into() },
|
||||
Term::Var { name: "b".into() },
|
||||
],
|
||||
},
|
||||
doc: None,
|
||||
})],
|
||||
};
|
||||
let ir = emit_ir(&m).unwrap();
|
||||
assert!(ir.contains("define i64 @ail_add(i64 %arg_a, i64 %arg_b)"));
|
||||
assert!(ir.contains("add i64 %arg_a, %arg_b"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user