stufffffffffff

This commit is contained in:
Janis 2024-08-18 14:52:24 +02:00
parent ddd65e8c4d
commit 00359a306c
7 changed files with 396 additions and 53 deletions

View file

@ -6,7 +6,7 @@ use crate::string_table::{self, ImmOrIndex};
pub type Node = NonZero<u32>; pub type Node = NonZero<u32>;
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Tag { pub enum Tag {
Undefined, Undefined,
Root, Root,
@ -46,14 +46,6 @@ pub enum Tag {
Ident { Ident {
name: string_table::Index, name: string_table::Index,
}, },
IntegralConstant {
bits: string_table::Index,
ty: Option<IntegralType>,
},
FloatingConstant {
bits: u64,
ty: FloatingType,
},
Constant { Constant {
bytes: ImmOrIndex, bytes: ImmOrIndex,
ty: Type, ty: Type,
@ -197,7 +189,7 @@ pub enum Tag {
}, },
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum LetOrVar { pub enum LetOrVar {
Let, Let,
Var, Var,
@ -298,14 +290,12 @@ impl PartialEq for Type {
(Self::Floating(l0), Self::Floating(r0)) => l0 == r0, (Self::Floating(l0), Self::Floating(r0)) => l0 == r0,
( (
Self::Pointer { Self::Pointer {
constness: l_constness, pointee: l_pointee, ..
pointee: l_pointee,
}, },
Self::Pointer { Self::Pointer {
constness: r_constness, pointee: r_pointee, ..
pointee: r_pointee,
}, },
) => l_constness == r_constness && l_pointee == r_pointee, ) => l_pointee == r_pointee,
( (
Self::Fn { Self::Fn {
parameter_types: l_parameter_types, parameter_types: l_parameter_types,
@ -322,12 +312,41 @@ impl PartialEq for Type {
} }
impl Type { impl Type {
pub fn as_primitive_type(&self) -> Option<PrimitiveType> {
match self {
Type::Void => Some(PrimitiveType::Void),
Type::Bool => Some(PrimitiveType::Bool),
Type::Integer(t) => Some(PrimitiveType::IntegralType(*t)),
Type::Floating(t) => Some(PrimitiveType::FloatingType(*t)),
_ => None,
}
}
pub fn equal_type(&self, rhs: &Self) -> Option<Type> { pub fn equal_type(&self, rhs: &Self) -> Option<Type> {
match (self, rhs) { match (self, rhs) {
(Self::ComptimeNumber, Self::Floating(_)) (Self::ComptimeNumber, Self::Floating(_))
| (Self::ComptimeNumber, Self::Integer(_)) => Some(rhs.clone()), | (Self::ComptimeNumber, Self::Integer(_)) => Some(rhs.clone()),
(Self::Integer(_), Self::ComptimeNumber) (Self::Integer(_), Self::ComptimeNumber)
| (Self::Floating(_), Self::ComptimeNumber) => Some(self.clone()), | (Self::Floating(_), Self::ComptimeNumber) => Some(self.clone()),
(
Self::Pointer {
constness: a_const,
pointee: a_ptr,
},
Self::Pointer {
constness: b_const,
pointee: b_ptr,
},
) => {
if a_ptr == b_ptr {
Some(Self::Pointer {
constness: *a_const || *b_const,
pointee: a_ptr.clone(),
})
} else {
None
}
}
_ => { _ => {
if self.eq(rhs) { if self.eq(rhs) {
Some(self.clone()) Some(self.clone())
@ -343,6 +362,9 @@ impl Type {
pub fn bool() -> Type { pub fn bool() -> Type {
Self::Void Self::Void
} }
pub fn comptime_number() -> Type {
Self::ComptimeNumber
}
pub fn any() -> Type { pub fn any() -> Type {
Self::Any Self::Any
} }
@ -353,6 +375,22 @@ impl Type {
} }
} }
pub fn bit_width(&self) -> u16 {
match self {
Type::Any => 0,
Type::Void => 0,
Type::Bool => 1,
Type::ComptimeNumber => u16::MAX,
Type::Integer(i) => i.bits,
Type::Floating(f) => match f {
FloatingType::Binary32 => 32,
FloatingType::Binary64 => 64,
},
Type::Pointer { .. } => 64,
Type::Fn { .. } => 64,
}
}
pub fn can_logical_and_or(&self) -> bool { pub fn can_logical_and_or(&self) -> bool {
match self { match self {
Type::Bool => true, Type::Bool => true,
@ -452,7 +490,7 @@ impl Type {
} }
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PrimitiveType { pub enum PrimitiveType {
FloatingType(FloatingType), FloatingType(FloatingType),
IntegralType(IntegralType), IntegralType(IntegralType),

26
src/error.rs Normal file
View file

@ -0,0 +1,26 @@
use crate::ast::Type;
#[derive(Debug, PartialEq, Eq, thiserror::Error)]
pub enum AnalysisErrorTag {
#[error("Mismatching types in function return.")]
MismatchingTypesFunctionReturn,
#[error("Insufficient bits in type {1} for constant with {0} bits")]
InsufficientBitsInTypeForConstant(u32, Type),
}
#[derive(Debug, PartialEq, Eq, thiserror::Error)]
pub struct AnalysisError {
inner: AnalysisErrorTag,
}
impl AnalysisError {
pub fn new(inner: AnalysisErrorTag) -> Self {
Self { inner }
}
}
impl core::fmt::Display for AnalysisError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
core::fmt::Debug::fmt(&self.inner, f)
}
}

View file

@ -593,18 +593,7 @@ pub mod bigint {
} }
pub fn bit_width(&self) -> u32 { pub fn bit_width(&self) -> u32 {
let mut bits = self.0.len() as u32; count_bits(&self.0)
for d in self.0.iter().rev() {
if *d == 0 {
bits -= u32::BITS;
} else {
bits -= d.leading_zeros();
break;
}
}
bits
} }
pub fn from_bytes_le(bytes: &[u8]) -> BigInt { pub fn from_bytes_le(bytes: &[u8]) -> BigInt {
@ -677,6 +666,33 @@ pub mod bigint {
} }
} }
/// counts used bits in a u32 slice, discards leading zeros in MSB.
/// `[0xff,0xff,0x00,0x00]` -> 16
/// `[0xff,0xff,0x00]` -> 16
/// `[0xff,0xff,0x0f]` -> 20
pub fn count_bits(bytes: &[u32]) -> u32 {
let mut bits = bytes.len() as u32;
for d in bytes.iter().rev() {
if *d == 0 {
bits -= u32::BITS;
} else {
bits -= d.leading_zeros();
break;
}
}
bits
}
#[test]
fn test_count_bits() {
assert_eq!(count_bits(&[0xffffffff, 0x00, 0x00]), 32);
assert_eq!(count_bits(&[0xffffffff, 0xff, 0x00]), 40);
assert_eq!(count_bits(&[0xffffffff, 0xff]), 40);
assert_eq!(count_bits(&[0xffffffff, 0xff, 0xffff]), 64 + 16);
}
#[allow(unused)] #[allow(unused)]
/// lhs must be bigger than rhs /// lhs must be bigger than rhs
fn sub_bigint(lhs: &mut [u32], rhs: &[u32]) { fn sub_bigint(lhs: &mut [u32], rhs: &[u32]) {

View file

@ -10,6 +10,7 @@
pub mod ast; pub mod ast;
pub mod codegen; pub mod codegen;
pub mod common; pub mod common;
pub mod error;
pub mod lexer; pub mod lexer;
pub mod parser; pub mod parser;
pub mod string_table; pub mod string_table;

View file

@ -5,8 +5,9 @@ use itertools::Itertools;
use crate::{ use crate::{
ast::{FloatingType, IntegralType, LetOrVar, Node, PrimitiveType, Tag, Type}, ast::{FloatingType, IntegralType, LetOrVar, Node, PrimitiveType, Tag, Type},
common::NextIf, common::NextIf,
error::{AnalysisError, AnalysisErrorTag},
lexer::{bigint::BigInt, Radix, TokenIterator}, lexer::{bigint::BigInt, Radix, TokenIterator},
string_table::{Index, StringTable}, string_table::{ImmOrIndex, Index, StringTable},
symbol_table::{SymbolKind, SymbolTable}, symbol_table::{SymbolKind, SymbolTable},
tokens::Token, tokens::Token,
}; };
@ -41,6 +42,11 @@ impl core::ops::Index<Node> for Nodes {
&self.inner[index.get() as usize] &self.inner[index.get() as usize]
} }
} }
impl core::ops::IndexMut<Node> for Nodes {
fn index_mut(&mut self, index: Node) -> &mut Self::Output {
&mut self.inner[index.get() as usize]
}
}
impl Nodes { impl Nodes {
fn new() -> Nodes { fn new() -> Nodes {
@ -126,6 +132,25 @@ impl Tree {
} }
} }
pub fn global_decls(&self) -> impl Iterator<Item = (Node, String)> {
self.global_decls.iter().map(|decl| {
let name = match self.nodes.get_node(*decl) {
Tag::FunctionDecl { proto, body } => {
let Tag::FunctionProto { name, .. } = self.nodes.get_node(*proto) else {
unreachable!()
};
self.get_ident_str(*name).unwrap().to_owned()
}
Tag::GlobalDecl { name, .. } => self.get_ident_str(*name).unwrap().to_owned(),
_ => {
unreachable!()
}
};
(*decl, name)
})
}
#[allow(unused)] #[allow(unused)]
fn is_integral_type(lexeme: &str) -> Option<()> { fn is_integral_type(lexeme: &str) -> Option<()> {
let mut iter = lexeme.chars(); let mut iter = lexeme.chars();
@ -822,16 +847,35 @@ impl Tree {
| Token::IntegerConstant => { | Token::IntegerConstant => {
_ = tokens.next(); _ = tokens.next();
let (bits, ty) = Self::parse_integral_constant(token.token(), token.lexeme()); let (bits, ty) = Self::parse_integral_constant(token.token(), token.lexeme());
let index = self.strings.insert(bits.into_bytes_le()); let bytes = bits.into_bytes_le();
const BUF_SIZE: usize = core::mem::size_of::<u64>();
let mut buf = [0u8; BUF_SIZE];
buf[..bytes.len().min(BUF_SIZE)]
.copy_from_slice(&bytes[..bytes.len().min(BUF_SIZE)]);
let bytes = match bytes.len() {
0..2 => {
let (buf, _) = buf.split_at(core::mem::size_of::<u32>());
let dw = u32::from_le_bytes(buf.try_into().unwrap());
ImmOrIndex::U32(dw)
}
0..4 => {
let (buf, _) = buf.split_at(core::mem::size_of::<u64>());
let qw = u64::from_le_bytes(buf.try_into().unwrap());
ImmOrIndex::U64(qw)
}
0.. => {
let idx = self.strings.insert(bytes);
ImmOrIndex::Index(idx)
}
};
let ty = match ty { let ty = match ty {
Some(int) => Type::Integer(int), Some(int) => Type::Integer(int),
None => Type::ComptimeNumber, None => Type::ComptimeNumber,
}; };
Ok(self.nodes.push_tag(Tag::Constant { Ok(self.nodes.push_tag(Tag::Constant { bytes, ty }))
bytes: crate::string_table::ImmOrIndex::Index(index),
ty,
}))
} }
Token::FloatingConstant Token::FloatingConstant
| Token::FloatingExpConstant | Token::FloatingExpConstant
@ -840,8 +884,13 @@ impl Tree {
_ = tokens.next(); _ = tokens.next();
let (bits, ty) = Self::parse_floating_constant(token.token(), token.lexeme()); let (bits, ty) = Self::parse_floating_constant(token.token(), token.lexeme());
let bytes = match ty {
FloatingType::Binary32 => ImmOrIndex::U32(bits as u32),
FloatingType::Binary64 => ImmOrIndex::U64(bits as u64),
};
Ok(self.nodes.push_tag(Tag::Constant { Ok(self.nodes.push_tag(Tag::Constant {
bytes: crate::string_table::ImmOrIndex::U64(bits), bytes,
ty: Type::Floating(ty), ty: Type::Floating(ty),
})) }))
} }
@ -906,7 +955,9 @@ impl Tree {
_ => None, _ => None,
} }
} }
}
impl Tree {
fn render_node<W: core::fmt::Write>( fn render_node<W: core::fmt::Write>(
&mut self, &mut self,
writer: &mut W, writer: &mut W,
@ -1360,17 +1411,13 @@ impl Tree {
) )
} }
Tag::Constant { bytes, ty } => { Tag::Constant { bytes, ty } => {
let bytes = match bytes {
crate::string_table::ImmOrIndex::U64(i) => &i.to_le_bytes()[..],
crate::string_table::ImmOrIndex::U32(i) => &i.to_le_bytes()[..],
crate::string_table::ImmOrIndex::Index(idx) => self.strings.get_bytes(idx),
};
writeln_indented!( writeln_indented!(
indent, indent,
writer, writer,
"%{} = constant{{ ty: {}, bytes: {bytes:?}}}", "%{} = constant{{ ty: {}, bytes: {}}}",
node.get(), node.get(),
ty ty,
self.strings.display_idx(bytes)
) )
} }
_ => unreachable!(), _ => unreachable!(),
@ -1487,6 +1534,146 @@ impl Tree {
} }
} }
impl Tree {
/// type-checks and inserts appropriate explicit-cast nodes.
pub fn typecheck(&mut self) {
let mut errors = Vec::new();
for decl in self.global_decls.clone() {
self.typecheck_node(&mut errors, decl);
}
}
// TODO: inline types into the AST proper before tackling this.
// for now, comptime_number is not supported in IR gen, then.
fn typecheck_node(&mut self, errors: &mut Vec<AnalysisError>, node: Node) {
#[allow(unused_variables)]
match self.nodes[node].clone() {
Tag::FunctionProto { .. } => {}
Tag::FunctionDecl { proto, body } => {
let Tag::FunctionProto { return_type, .. } = self.nodes[proto] else {
unreachable!()
};
let body_t = self.type_of_node(body);
let ret_t = self.type_of_node(return_type);
if let Some(peer_t) = body_t.equal_type(&ret_t) {
if body_t == Type::comptime_number() {
let Tag::Block { trailing_expr, .. } = self.nodes[body] else {
unreachable!()
};
if let Some(expr) = trailing_expr {
let ty = self.nodes.push_tag(Tag::PrimitiveType(
peer_t
.as_primitive_type()
.expect("comptime cannot be cast into a non-primitive type"),
));
let expr = self.nodes.push_tag(Tag::ExplicitCast {
lhs: expr,
typename: ty,
});
let Tag::Block { trailing_expr, .. } = &mut self.nodes[body] else {
unreachable!()
};
*trailing_expr = Some(expr)
}
}
} else {
errors.push(AnalysisError::new(
AnalysisErrorTag::MismatchingTypesFunctionReturn,
));
}
}
Tag::Constant { bytes, ty } => {
let bits = self.strings.count_bits(bytes);
if bits < ty.bit_width() as u32 {
errors.push(AnalysisError::new(
AnalysisErrorTag::InsufficientBitsInTypeForConstant(bits, ty.clone()),
));
}
}
Tag::Block {
statements,
trailing_expr,
} => {
for statement in statements {
self.typecheck_node(errors, statement);
}
if let Some(expr) = trailing_expr {
self.typecheck_node(errors, expr);
}
}
Tag::ReturnStmt { expr } => {
if let Some(expr) = expr {
self.typecheck_node(errors, expr);
}
}
Tag::ExprStmt { expr } => {
self.typecheck_node(errors, expr);
}
Tag::VarDecl {
explicit_type,
assignment,
..
} => {
assignment.map(|t| self.typecheck_node(errors, t));
let explicit_t = explicit_type.map(|t| self.type_of_node(t));
let assignment_t = assignment.map(|t| self.type_of_node(t));
match (explicit_t, assignment_t) {
(None, None) => unreachable!(),
(Some(explicit_t), None) => {}
(Some(explicit_t), Some(assignment_t)) => {
// TODO: ensure types match, explicit-cast comptime_number
}
(None, Some(assignment_t)) => {
// TODO: set explicit_type to assignment_t
}
}
}
Tag::GlobalDecl {
name,
explicit_type,
assignment,
} => todo!(),
Tag::DeclRef(_) => todo!(),
Tag::GlobalRef(_) => todo!(),
Tag::CallExpr { lhs, rhs } => todo!(),
Tag::ArgumentList { parameters } => todo!(),
Tag::Argument { name, expr } => todo!(),
Tag::ExplicitCast { lhs, typename } => todo!(),
Tag::Deref { lhs } => todo!(),
Tag::Ref { lhs } => todo!(),
Tag::Not { lhs } => todo!(),
Tag::Negate { lhs } => todo!(),
Tag::Or { lhs, rhs } => todo!(),
Tag::And { lhs, rhs } => todo!(),
Tag::BitOr { lhs, rhs } => todo!(),
Tag::BitAnd { lhs, rhs } => todo!(),
Tag::BitXOr { lhs, rhs } => todo!(),
Tag::Eq { lhs, rhs } => todo!(),
Tag::NEq { lhs, rhs } => todo!(),
Tag::Lt { lhs, rhs } => todo!(),
Tag::Gt { lhs, rhs } => todo!(),
Tag::Le { lhs, rhs } => todo!(),
Tag::Ge { lhs, rhs } => todo!(),
Tag::Shl { lhs, rhs } => todo!(),
Tag::Shr { lhs, rhs } => todo!(),
Tag::Add { lhs, rhs } => todo!(),
Tag::Sub { lhs, rhs } => todo!(),
Tag::Mul { lhs, rhs } => todo!(),
Tag::Rem { lhs, rhs } => todo!(),
Tag::Div { lhs, rhs } => todo!(),
Tag::Assign { lhs, rhs } => todo!(),
_ => {
unreachable!()
}
}
}
}
static PRECEDENCE_MAP: std::sync::LazyLock<HashMap<Token, u32>> = std::sync::LazyLock::new(|| { static PRECEDENCE_MAP: std::sync::LazyLock<HashMap<Token, u32>> = std::sync::LazyLock::new(|| {
HashMap::from([ HashMap::from([
(Token::PipePipe, 10), (Token::PipePipe, 10),
@ -1518,7 +1705,7 @@ mod tests {
#[test] #[test]
fn render_ast() { fn render_ast() {
let src = "let a: u21 = 3;"; let src = "let a: u21 = 3u32;";
let tokens = Tokenizer::new(src.as_bytes()).unwrap(); let tokens = Tokenizer::new(src.as_bytes()).unwrap();
let mut tree = Tree::new(); let mut tree = Tree::new();
@ -1532,8 +1719,8 @@ mod tests {
fn render_ast2() { fn render_ast2() {
let src = " let src = "
fn main() -> void { fn main() -> void {
let a: u32 = 0; let a: u32 = 0u32;
a == 1 a == 1u32
} }
fn square(x: u32) -> u32 { fn square(x: u32) -> u32 {
x * x x * x
@ -1553,10 +1740,10 @@ x * x
fn render_ast3() { fn render_ast3() {
let src = " let src = "
fn main() -> void { fn main() -> void {
let a: u32 = 0; let a: u32 = 0u32;
a == global a == global
} }
const global: u32 = 42; const global: u32 = 42u32;
"; ";
let tokens = Tokenizer::new(src.as_bytes()).unwrap(); let tokens = Tokenizer::new(src.as_bytes()).unwrap();

View file

@ -1,12 +1,12 @@
use std::{collections::BTreeMap, hash::Hasher}; use std::{collections::BTreeMap, hash::Hasher};
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Index { pub struct Index {
pub start: u32, pub start: u32,
pub end: u32, pub end: u32,
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ImmOrIndex { pub enum ImmOrIndex {
U64(u64), U64(u64),
U32(u32), U32(u32),
@ -19,7 +19,6 @@ impl Index {
} }
} }
#[derive(Debug)]
pub struct StringTable { pub struct StringTable {
bytes: Vec<u8>, bytes: Vec<u8>,
indices: BTreeMap<u64, Index>, indices: BTreeMap<u64, Index>,
@ -41,6 +40,24 @@ impl StringTable {
} }
} }
pub fn display_idx(&self, idx: ImmOrIndex) -> ImmOrIndexDisplay {
ImmOrIndexDisplay::new(self, idx)
}
pub fn count_bits(&self, idx: ImmOrIndex) -> u32 {
match idx {
ImmOrIndex::U64(v) => u64::BITS - v.leading_zeros(),
ImmOrIndex::U32(v) => u32::BITS - v.leading_zeros(),
ImmOrIndex::Index(idx) => {
let bytes = self.get_bytes(idx);
let ints = unsafe {
core::slice::from_raw_parts(bytes.as_ptr().cast::<u32>(), bytes.len() / 4)
};
crate::lexer::bigint::count_bits(ints)
}
}
}
pub fn get_str(&self, idx: Index) -> &str { pub fn get_str(&self, idx: Index) -> &str {
unsafe { core::str::from_utf8_unchecked(&self[idx]) } unsafe { core::str::from_utf8_unchecked(&self[idx]) }
} }
@ -73,3 +90,60 @@ impl StringTable {
index index
} }
} }
mod display {
use core::{fmt::Debug, str};
use super::*;
impl Debug for StringTable {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_list()
.entries(self.indices.iter().map(|(_, idx)| {
struct Test<'a> {
bytes: &'a [u8],
str: Option<&'a str>,
}
impl<'a> Debug for Test<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{{ bytes: {:x?}", self.bytes)?;
if let Some(str) = self.str {
write!(f, ", str: {}", str)?;
}
write!(f, " }}")
}
}
let bytes = self.get_bytes(*idx);
let str = str::from_utf8(bytes).ok();
Test { bytes, str }
}))
.finish()
}
}
pub struct ImmOrIndexDisplay<'table> {
table: &'table StringTable,
idx: ImmOrIndex,
}
impl<'table> ImmOrIndexDisplay<'table> {
pub fn new(table: &'table StringTable, idx: ImmOrIndex) -> Self {
Self { table, idx }
}
}
impl<'table> core::fmt::Display for ImmOrIndexDisplay<'table> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.idx {
ImmOrIndex::U64(i) => write!(f, "0x{i:0>16x}"),
ImmOrIndex::U32(i) => write!(f, "0x{i:0>8x}"),
ImmOrIndex::Index(idx) => {
let bytes = self.table.get_bytes(idx);
write!(f, "{bytes:?}")
}
}
}
}
}
pub use display::ImmOrIndexDisplay;

View file

@ -3,7 +3,7 @@
use std::collections::{hash_map::Entry, HashMap}; use std::collections::{hash_map::Entry, HashMap};
use crate::{ use crate::{
ast::{FloatingType, IntegralType, Node as AstNode, Tag, Type}, ast::{Node as AstNode, Tag, Type},
parser::Tree, parser::Tree,
string_table::{ImmOrIndex, Index as StringsIndex}, string_table::{ImmOrIndex, Index as StringsIndex},
writeln_indented, writeln_indented,
@ -533,7 +533,7 @@ mod tests {
fn ir() { fn ir() {
let src = " let src = "
fn main() -> u32 { fn main() -> u32 {
let a: u32 = 0 + 3u32; let a: u32 = 0u32 + 3u32;
let ptr_a = &a; let ptr_a = &a;
return *ptr_a * global; return *ptr_a * global;
} }
@ -542,7 +542,7 @@ fn square(x: u32) -> u32 {
x * x x * x
} }
const global: u32 = 42; const global: u32 = 42u32;
"; ";
let tokens = Tokenizer::new(src.as_bytes()).unwrap(); let tokens = Tokenizer::new(src.as_bytes()).unwrap();
@ -552,6 +552,7 @@ const global: u32 = 42;
let mut buf = String::new(); let mut buf = String::new();
tree.render(&mut buf).unwrap(); tree.render(&mut buf).unwrap();
println!("{buf}"); println!("{buf}");
println!("{:#?}", tree.strings);
let mut ir = IR::new(); let mut ir = IR::new();
ir.build(&mut tree); ir.build(&mut tree);