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,151 @@
|
||||
//! AST-Knoten. Serde-Layout entspricht dem JSON-Schema in `docs/DESIGN.md`.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Module {
|
||||
pub schema: String,
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub imports: Vec<Import>,
|
||||
pub defs: Vec<Def>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Import {
|
||||
pub module: String,
|
||||
#[serde(rename = "as", default, skip_serializing_if = "Option::is_none")]
|
||||
pub alias: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "lowercase")]
|
||||
pub enum Def {
|
||||
Fn(FnDef),
|
||||
Const(ConstDef),
|
||||
}
|
||||
|
||||
impl Def {
|
||||
pub fn name(&self) -> &str {
|
||||
match self {
|
||||
Def::Fn(f) => &f.name,
|
||||
Def::Const(c) => &c.name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FnDef {
|
||||
pub name: String,
|
||||
#[serde(rename = "type")]
|
||||
pub ty: Type,
|
||||
pub params: Vec<String>,
|
||||
pub body: Term,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub doc: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ConstDef {
|
||||
pub name: String,
|
||||
#[serde(rename = "type")]
|
||||
pub ty: Type,
|
||||
pub value: Term,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub doc: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "t", rename_all = "lowercase")]
|
||||
pub enum Term {
|
||||
Lit { lit: Literal },
|
||||
Var { name: String },
|
||||
App {
|
||||
#[serde(rename = "fn")]
|
||||
callee: Box<Term>,
|
||||
args: Vec<Term>,
|
||||
},
|
||||
Let {
|
||||
name: String,
|
||||
value: Box<Term>,
|
||||
body: Box<Term>,
|
||||
},
|
||||
If {
|
||||
cond: Box<Term>,
|
||||
then: Box<Term>,
|
||||
#[serde(rename = "else")]
|
||||
else_: Box<Term>,
|
||||
},
|
||||
Do {
|
||||
op: String,
|
||||
args: Vec<Term>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "lowercase")]
|
||||
pub enum Literal {
|
||||
Int { value: i64 },
|
||||
Bool { value: bool },
|
||||
Unit,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "k", rename_all = "lowercase")]
|
||||
pub enum Type {
|
||||
Con {
|
||||
name: String,
|
||||
},
|
||||
Fn {
|
||||
params: Vec<Type>,
|
||||
ret: Box<Type>,
|
||||
#[serde(default)]
|
||||
effects: Vec<String>,
|
||||
},
|
||||
Var {
|
||||
name: String,
|
||||
},
|
||||
Forall {
|
||||
vars: Vec<String>,
|
||||
body: Box<Type>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Type {
|
||||
pub fn int() -> Type {
|
||||
Type::Con { name: "Int".into() }
|
||||
}
|
||||
pub fn bool_() -> Type {
|
||||
Type::Con { name: "Bool".into() }
|
||||
}
|
||||
pub fn unit() -> Type {
|
||||
Type::Con { name: "Unit".into() }
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Type {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Type::Con { name: a }, Type::Con { name: b }) => a == b,
|
||||
(
|
||||
Type::Fn { params: ap, ret: ar, effects: ae },
|
||||
Type::Fn { params: bp, ret: br, effects: be },
|
||||
) => {
|
||||
ap == bp && ar == br && {
|
||||
let mut a = ae.clone();
|
||||
let mut b = be.clone();
|
||||
a.sort();
|
||||
b.sort();
|
||||
a == b
|
||||
}
|
||||
}
|
||||
(Type::Var { name: a }, Type::Var { name: b }) => a == b,
|
||||
(
|
||||
Type::Forall { vars: a, body: ab },
|
||||
Type::Forall { vars: b, body: bb },
|
||||
) => a == b && ab == bb,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Eq for Type {}
|
||||
@@ -0,0 +1,85 @@
|
||||
//! Kanonische JSON-Serialisierung.
|
||||
//!
|
||||
//! Schreibt JSON ohne Whitespace und mit lexikographisch sortierten Object-Keys.
|
||||
//! Damit ist die Repräsentation deterministisch und für Hashing geeignet.
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
/// Kanonische Bytes für ein beliebiges Serializable-Objekt.
|
||||
pub fn to_bytes<T: serde::Serialize>(value: &T) -> Vec<u8> {
|
||||
let v = serde_json::to_value(value).expect("serializable");
|
||||
let mut out = Vec::new();
|
||||
write_value(&v, &mut out).expect("write to Vec");
|
||||
out
|
||||
}
|
||||
|
||||
fn write_value(v: &serde_json::Value, out: &mut Vec<u8>) -> std::io::Result<()> {
|
||||
use serde_json::Value;
|
||||
match v {
|
||||
Value::Null => out.write_all(b"null"),
|
||||
Value::Bool(true) => out.write_all(b"true"),
|
||||
Value::Bool(false) => out.write_all(b"false"),
|
||||
Value::Number(n) => out.write_all(n.to_string().as_bytes()),
|
||||
Value::String(s) => {
|
||||
let escaped = serde_json::to_string(s).expect("string serializable");
|
||||
out.write_all(escaped.as_bytes())
|
||||
}
|
||||
Value::Array(arr) => {
|
||||
out.write_all(b"[")?;
|
||||
for (i, item) in arr.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.write_all(b",")?;
|
||||
}
|
||||
write_value(item, out)?;
|
||||
}
|
||||
out.write_all(b"]")
|
||||
}
|
||||
Value::Object(map) => {
|
||||
let mut keys: Vec<&String> = map.keys().collect();
|
||||
keys.sort();
|
||||
out.write_all(b"{")?;
|
||||
for (i, k) in keys.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.write_all(b",")?;
|
||||
}
|
||||
let kj = serde_json::to_string(k).expect("key serializable");
|
||||
out.write_all(kj.as_bytes())?;
|
||||
out.write_all(b":")?;
|
||||
write_value(&map[*k], out)?;
|
||||
}
|
||||
out.write_all(b"}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn sorts_keys() {
|
||||
let v = json!({ "b": 1, "a": 2 });
|
||||
let bytes = to_bytes(&v);
|
||||
assert_eq!(std::str::from_utf8(&bytes).unwrap(), r#"{"a":2,"b":1}"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_keys_sorted() {
|
||||
let v = json!({ "z": { "y": 1, "x": [3, { "b": 2, "a": 1 }] } });
|
||||
let bytes = to_bytes(&v);
|
||||
assert_eq!(
|
||||
std::str::from_utf8(&bytes).unwrap(),
|
||||
r#"{"z":{"x":[3,{"a":1,"b":2}],"y":1}}"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_whitespace() {
|
||||
let v = json!({ "a": 1, "b": [1, 2, 3] });
|
||||
let bytes = to_bytes(&v);
|
||||
let s = std::str::from_utf8(&bytes).unwrap();
|
||||
assert!(!s.contains(' '));
|
||||
assert!(!s.contains('\n'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
//! Content-addressed Hashing für Definitionen.
|
||||
//!
|
||||
//! Hash = BLAKE3 über die kanonische JSON-Form (siehe `canonical`).
|
||||
//! Das `hash`-Feld in der Eingabe wird vor dem Hashen entfernt.
|
||||
|
||||
use crate::ast::Def;
|
||||
use crate::canonical;
|
||||
|
||||
/// 16-Hex-Zeichen (64 bit) Prefix des BLAKE3-Hashes.
|
||||
/// Reicht für Eindeutigkeit innerhalb realistischer Codebases und ist
|
||||
/// kompakt genug für visuelle Inspektion.
|
||||
pub fn def_hash(def: &Def) -> String {
|
||||
let bytes = canonical::to_bytes(def);
|
||||
let h = blake3::hash(&bytes);
|
||||
let hex = h.to_hex();
|
||||
hex.as_str()[..16].to_string()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ast::*;
|
||||
|
||||
fn sample_fn() -> Def {
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hash_is_stable() {
|
||||
let h1 = def_hash(&sample_fn());
|
||||
let h2 = def_hash(&sample_fn());
|
||||
assert_eq!(h1, h2);
|
||||
assert_eq!(h1.len(), 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hash_changes_with_content() {
|
||||
let mut def = sample_fn();
|
||||
let h1 = def_hash(&def);
|
||||
if let Def::Fn(ref mut f) = def {
|
||||
f.name = "mul".into();
|
||||
}
|
||||
let h2 = def_hash(&def);
|
||||
assert_ne!(h1, h2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
//! AILang Kerndatenmodell.
|
||||
//!
|
||||
//! Quelle einer AILang-Übersetzungseinheit ist ein `Module` als JSON. Dieses
|
||||
//! Crate definiert das Schema, Serialisierung und content-addressed Hashing.
|
||||
|
||||
pub mod ast;
|
||||
pub mod canonical;
|
||||
pub mod hash;
|
||||
pub mod pretty;
|
||||
|
||||
pub use ast::{ConstDef, Def, FnDef, Import, Literal, Module, Term, Type};
|
||||
pub use hash::def_hash;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("schema mismatch: expected {expected:?}, got {got:?}")]
|
||||
SchemaMismatch { expected: String, got: String },
|
||||
|
||||
#[error("json: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
|
||||
#[error("io: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
pub const SCHEMA: &str = "ailang/v0";
|
||||
|
||||
pub fn load_module(path: &std::path::Path) -> Result<Module> {
|
||||
let bytes = std::fs::read(path)?;
|
||||
let module: Module = serde_json::from_slice(&bytes)?;
|
||||
if module.schema != SCHEMA {
|
||||
return Err(Error::SchemaMismatch {
|
||||
expected: SCHEMA.to_string(),
|
||||
got: module.schema,
|
||||
});
|
||||
}
|
||||
Ok(module)
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
//! Pretty-Printer: AST → menschenlesbare Textform.
|
||||
//!
|
||||
//! Die Textform ist als Diff- und Review-Werkzeug gedacht. Die
|
||||
//! kanonische Quelle bleibt die JSON-Form. Jede pretty-Ausgabe ist
|
||||
//! deterministisch.
|
||||
|
||||
use crate::ast::*;
|
||||
use std::fmt::Write;
|
||||
|
||||
pub fn module(m: &Module) -> String {
|
||||
let mut s = String::new();
|
||||
writeln!(s, "(module {}", m.name).unwrap();
|
||||
if !m.imports.is_empty() {
|
||||
for imp in &m.imports {
|
||||
match &imp.alias {
|
||||
Some(a) => writeln!(s, " (import {} as {})", imp.module, a).unwrap(),
|
||||
None => writeln!(s, " (import {})", imp.module).unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
for (i, def) in m.defs.iter().enumerate() {
|
||||
if i > 0 {
|
||||
s.push('\n');
|
||||
}
|
||||
let body = def_block(def, 2);
|
||||
s.push_str(&body);
|
||||
s.push('\n');
|
||||
}
|
||||
s.push(')');
|
||||
s.push('\n');
|
||||
s
|
||||
}
|
||||
|
||||
pub fn manifest(m: &Module) -> String {
|
||||
let mut s = String::new();
|
||||
writeln!(s, "module {}", m.name).unwrap();
|
||||
let max_name = m.defs.iter().map(|d| d.name().len()).max().unwrap_or(0);
|
||||
for def in &m.defs {
|
||||
let h = crate::hash::def_hash(def);
|
||||
let (kw, ty) = match def {
|
||||
Def::Fn(f) => ("fn", type_to_string(&f.ty)),
|
||||
Def::Const(c) => ("const", type_to_string(&c.ty)),
|
||||
};
|
||||
writeln!(
|
||||
s,
|
||||
" {kw:5} {name:<width$} :: {ty} [{h}]",
|
||||
kw = kw,
|
||||
name = def.name(),
|
||||
width = max_name,
|
||||
ty = ty,
|
||||
h = h,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
fn def_block(def: &Def, indent: usize) -> String {
|
||||
let pad = " ".repeat(indent);
|
||||
match def {
|
||||
Def::Fn(f) => {
|
||||
let params = if f.params.is_empty() {
|
||||
"[]".to_string()
|
||||
} else {
|
||||
format!("[{}]", f.params.join(" "))
|
||||
};
|
||||
let mut s = format!(
|
||||
"{pad}(fn {name} :: {ty} {params}\n",
|
||||
pad = pad,
|
||||
name = f.name,
|
||||
ty = type_to_string(&f.ty),
|
||||
params = params,
|
||||
);
|
||||
s.push_str(&term_block(&f.body, indent + 2));
|
||||
s.push(')');
|
||||
s
|
||||
}
|
||||
Def::Const(c) => {
|
||||
let mut s = format!(
|
||||
"{pad}(const {name} :: {ty}\n",
|
||||
pad = pad,
|
||||
name = c.name,
|
||||
ty = type_to_string(&c.ty),
|
||||
);
|
||||
s.push_str(&term_block(&c.value, indent + 2));
|
||||
s.push(')');
|
||||
s
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn term_block(t: &Term, indent: usize) -> String {
|
||||
let pad = " ".repeat(indent);
|
||||
match t {
|
||||
Term::Lit { lit } => format!("{pad}{}", lit_to_string(lit)),
|
||||
Term::Var { name } => format!("{pad}{name}"),
|
||||
Term::App { callee, args } => {
|
||||
let mut s = format!("{pad}(");
|
||||
s.push_str(&term_inline(callee));
|
||||
for a in args {
|
||||
s.push(' ');
|
||||
s.push_str(&term_inline(a));
|
||||
}
|
||||
s.push(')');
|
||||
s
|
||||
}
|
||||
Term::Let { name, value, body } => {
|
||||
let mut s = format!("{pad}(let {name}\n");
|
||||
s.push_str(&term_block(value, indent + 2));
|
||||
s.push('\n');
|
||||
s.push_str(&term_block(body, indent + 2));
|
||||
s.push(')');
|
||||
s
|
||||
}
|
||||
Term::If { cond, then, else_ } => {
|
||||
let mut s = format!("{pad}(if\n");
|
||||
s.push_str(&term_block(cond, indent + 2));
|
||||
s.push('\n');
|
||||
s.push_str(&term_block(then, indent + 2));
|
||||
s.push('\n');
|
||||
s.push_str(&term_block(else_, indent + 2));
|
||||
s.push(')');
|
||||
s
|
||||
}
|
||||
Term::Do { op, args } => {
|
||||
let mut s = format!("{pad}(do {op}");
|
||||
for a in args {
|
||||
s.push(' ');
|
||||
s.push_str(&term_inline(a));
|
||||
}
|
||||
s.push(')');
|
||||
s
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn term_inline(t: &Term) -> String {
|
||||
match t {
|
||||
Term::Lit { lit } => lit_to_string(lit),
|
||||
Term::Var { name } => name.clone(),
|
||||
Term::App { callee, args } => {
|
||||
let mut s = String::from("(");
|
||||
s.push_str(&term_inline(callee));
|
||||
for a in args {
|
||||
s.push(' ');
|
||||
s.push_str(&term_inline(a));
|
||||
}
|
||||
s.push(')');
|
||||
s
|
||||
}
|
||||
Term::Do { op, args } => {
|
||||
let mut s = format!("(do {op}");
|
||||
for a in args {
|
||||
s.push(' ');
|
||||
s.push_str(&term_inline(a));
|
||||
}
|
||||
s.push(')');
|
||||
s
|
||||
}
|
||||
// Strukturelle Terms in Inline-Form rekursiv schwer; fallback:
|
||||
Term::Let { name, value, body } => {
|
||||
format!(
|
||||
"(let {name} {} {})",
|
||||
term_inline(value),
|
||||
term_inline(body)
|
||||
)
|
||||
}
|
||||
Term::If { cond, then, else_ } => {
|
||||
format!(
|
||||
"(if {} {} {})",
|
||||
term_inline(cond),
|
||||
term_inline(then),
|
||||
term_inline(else_)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn lit_to_string(l: &Literal) -> String {
|
||||
match l {
|
||||
Literal::Int { value } => value.to_string(),
|
||||
Literal::Bool { value } => value.to_string(),
|
||||
Literal::Unit => "()".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn type_to_string(t: &Type) -> String {
|
||||
match t {
|
||||
Type::Con { name } => name.clone(),
|
||||
Type::Var { name } => name.clone(),
|
||||
Type::Fn { params, ret, effects } => {
|
||||
let p = params
|
||||
.iter()
|
||||
.map(type_to_string)
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
let eff = if effects.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" !{}", effects.join(","))
|
||||
};
|
||||
format!("({p}) -> {ret}{eff}", ret = type_to_string(ret))
|
||||
}
|
||||
Type::Forall { vars, body } => {
|
||||
format!("forall {}. {}", vars.join(" "), type_to_string(body))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn sample_module() -> Module {
|
||||
Module {
|
||||
schema: crate::SCHEMA.into(),
|
||||
name: "sample".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,
|
||||
}),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pretty_print_does_not_panic() {
|
||||
let s = module(&sample_module());
|
||||
assert!(s.contains("(module sample"));
|
||||
assert!(s.contains("(fn add"));
|
||||
assert!(s.contains("(+ a b)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manifest_contains_type_and_hash() {
|
||||
let s = manifest(&sample_module());
|
||||
assert!(s.contains("add"));
|
||||
assert!(s.contains("(Int, Int) -> Int"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user