use ratatui::{
buffer::Buffer,
layout::Rect,
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Paragraph, Widget, Wrap},
};
const LLVM_KEYWORDS: &[&str] = &[
"define", "declare", "tailcc", "fastcc", "ccc", "private", "internal", "external", "global",
"constant", "align", "to", "null", "true", "false", "undef", "nuw", "nsw", "exact", "inbounds",
];
const LLVM_INSTRUCTIONS: &[&str] = &[
"ret",
"br",
"switch",
"invoke",
"resume",
"unreachable",
"add",
"sub",
"mul",
"udiv",
"sdiv",
"urem",
"srem",
"and",
"or",
"xor",
"shl",
"lshr",
"ashr",
"fadd",
"fsub",
"fmul",
"fdiv",
"frem",
"alloca",
"load",
"store",
"getelementptr",
"fence",
"cmpxchg",
"atomicrmw",
"trunc",
"zext",
"sext",
"fptrunc",
"fpext",
"fptoui",
"fptosi",
"uitofp",
"sitofp",
"ptrtoint",
"inttoptr",
"bitcast",
"addrspacecast",
"icmp",
"fcmp",
"phi",
"select",
"call",
"va_arg",
"extractelement",
"insertelement",
"shufflevector",
"extractvalue",
"insertvalue",
];
const LLVM_TYPES: &[&str] = &[
"void", "i1", "i8", "i16", "i32", "i64", "i128", "half", "float", "double", "fp128", "ptr",
"label", "metadata", "type",
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) enum IrViewMode {
#[default]
StackArt,
TypedAst,
LlvmIr,
}
impl IrViewMode {
pub(crate) fn next(self) -> Self {
match self {
Self::StackArt => Self::TypedAst,
Self::TypedAst => Self::LlvmIr,
Self::LlvmIr => Self::StackArt,
}
}
pub(crate) fn name(&self) -> &'static str {
match self {
Self::StackArt => "Stack Effects",
Self::TypedAst => "Typed AST",
Self::LlvmIr => "LLVM IR",
}
}
}
#[derive(Debug, Clone, Default)]
pub(crate) struct IrContent {
pub(crate) stack_art: Vec<String>,
pub(crate) typed_ast: Vec<String>,
pub(crate) llvm_ir: Vec<String>,
pub(crate) errors: Vec<String>,
}
impl IrContent {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn content_for(&self, mode: IrViewMode) -> &[String] {
match mode {
IrViewMode::StackArt => &self.stack_art,
IrViewMode::TypedAst => &self.typed_ast,
IrViewMode::LlvmIr => &self.llvm_ir,
}
}
pub(crate) fn has_errors(&self) -> bool {
!self.errors.is_empty()
}
}
pub(crate) struct IrPane<'a> {
mode: IrViewMode,
content: &'a IrContent,
scroll: u16,
}
impl<'a> IrPane<'a> {
pub(crate) fn new(content: &'a IrContent) -> Self {
Self {
mode: IrViewMode::default(),
content,
scroll: 0,
}
}
pub(crate) fn mode(mut self, mode: IrViewMode) -> Self {
self.mode = mode;
self
}
fn style_content(&self, lines: &[String]) -> Vec<Line<'a>> {
match self.mode {
IrViewMode::StackArt => self.style_stack_art(lines),
IrViewMode::TypedAst => self.style_ast(lines),
IrViewMode::LlvmIr => self.style_llvm(lines),
}
}
fn style_stack_art(&self, lines: &[String]) -> Vec<Line<'a>> {
lines
.iter()
.map(|line| self.style_stack_art_line(line))
.collect()
}
fn style_stack_art_line(&self, line: &str) -> Line<'a> {
let mut spans = Vec::new();
let chars: Vec<char> = line.chars().collect();
let mut i = 0;
while i < chars.len() {
let ch = chars[i];
if "┌┐└┘├┤─│".contains(ch) {
spans.push(Span::styled(
ch.to_string(),
Style::default().fg(Color::Cyan),
));
i += 1;
}
else if ch == '→' {
spans.push(Span::styled(
"→",
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
));
i += 1;
}
else if ch.is_uppercase() {
let start = i;
while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
i += 1;
}
let word: String = chars[start..i].iter().collect();
spans.push(Span::styled(word, Style::default().fg(Color::Green)));
}
else if ch == '.' && i + 1 < chars.len() && chars[i + 1] == '.' {
let start = i;
i += 2; while i < chars.len() && chars[i].is_alphanumeric() {
i += 1;
}
let word: String = chars[start..i].iter().collect();
spans.push(Span::styled(word, Style::default().fg(Color::Magenta)));
}
else {
spans.push(Span::raw(ch.to_string()));
i += 1;
}
}
Line::from(spans)
}
fn style_ast(&self, lines: &[String]) -> Vec<Line<'a>> {
lines
.iter()
.map(|line| {
Line::from(Span::styled(
line.clone(),
Style::default().fg(Color::White),
))
})
.collect()
}
fn style_llvm(&self, lines: &[String]) -> Vec<Line<'a>> {
lines
.iter()
.map(|line| {
let trimmed = line.trim_start();
if trimmed.starts_with(';') {
return Line::from(Span::styled(
line.clone(),
Style::default().fg(Color::DarkGray),
));
}
if trimmed.ends_with(':') && !trimmed.contains(' ') {
return Line::from(Span::styled(
line.clone(),
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
));
}
Line::from(self.tokenize_llvm_line(line))
})
.collect()
}
fn tokenize_llvm_line(&self, line: &str) -> Vec<Span<'a>> {
let mut spans = Vec::new();
let chars: Vec<char> = line.chars().collect();
let mut i = 0;
while i < chars.len() {
let ch = chars[i];
if ch.is_whitespace() {
let start = i;
while i < chars.len() && chars[i].is_whitespace() {
i += 1;
}
spans.push(Span::raw(chars[start..i].iter().collect::<String>()));
continue;
}
if ch == '%' {
let start = i;
i += 1;
while i < chars.len()
&& (chars[i].is_alphanumeric() || chars[i] == '_' || chars[i] == '.')
{
i += 1;
}
spans.push(Span::styled(
chars[start..i].iter().collect::<String>(),
Style::default().fg(Color::Magenta),
));
continue;
}
if ch == '@' {
let start = i;
i += 1;
while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
i += 1;
}
spans.push(Span::styled(
chars[start..i].iter().collect::<String>(),
Style::default().fg(Color::Yellow),
));
continue;
}
if ch.is_ascii_digit()
|| (ch == '-' && i + 1 < chars.len() && chars[i + 1].is_ascii_digit())
{
let start = i;
if ch == '-' {
i += 1;
}
while i < chars.len() && chars[i].is_ascii_digit() {
i += 1;
}
spans.push(Span::styled(
chars[start..i].iter().collect::<String>(),
Style::default().fg(Color::Blue),
));
continue;
}
if ch.is_alphabetic() || ch == '_' {
let start = i;
while i < chars.len()
&& (chars[i].is_alphanumeric() || chars[i] == '_' || chars[i] == '.')
{
i += 1;
}
let word: String = chars[start..i].iter().collect();
let style = self.llvm_word_style(&word);
spans.push(Span::styled(word, style));
continue;
}
spans.push(Span::raw(ch.to_string()));
i += 1;
}
spans
}
fn llvm_word_style(&self, word: &str) -> Style {
if LLVM_KEYWORDS.contains(&word) {
Style::default().fg(Color::Yellow)
} else if LLVM_INSTRUCTIONS.contains(&word) {
Style::default().fg(Color::Green)
} else if LLVM_TYPES.contains(&word)
|| word.starts_with('i') && word[1..].chars().all(|c| c.is_ascii_digit())
{
Style::default().fg(Color::Cyan)
} else {
Style::default().fg(Color::White)
}
}
fn width_adapted_lines(&self, available_width: usize) -> Vec<Line<'a>> {
if self.content.has_errors() {
return self
.content
.errors
.iter()
.map(|e| Line::from(Span::styled(e.clone(), Style::default().fg(Color::Red))))
.collect();
}
let lines = self.content.content_for(self.mode);
if lines.is_empty() {
return vec![Line::from(Span::styled(
format!("No {} available", self.mode.name().to_lowercase()),
Style::default().fg(Color::DarkGray),
))];
}
let max_line_width = lines.iter().map(|l| l.chars().count()).max().unwrap_or(0);
if max_line_width <= available_width {
self.style_content(lines)
} else {
self.compact_content(lines, available_width)
}
}
fn compact_content(&self, lines: &[String], available_width: usize) -> Vec<Line<'a>> {
lines
.iter()
.filter_map(|line| self.compact_line(line, available_width))
.collect()
}
fn compact_line(&self, line: &str, available_width: usize) -> Option<Line<'a>> {
if ['╭', '╮', '╰', '╯'].iter().any(|c| line.contains(*c)) {
return None;
}
if line.starts_with('│') && line.ends_with('│') {
let inner = line.trim_start_matches('│').trim_end_matches('│').trim();
if !inner.is_empty() {
return Some(Line::from(Span::styled(
inner.to_string(),
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
)));
}
return None;
}
let has_box_chars = ['┌', '┐', '└', '┘', '├', '┤']
.iter()
.any(|c| line.contains(*c));
if has_box_chars && line.chars().count() > available_width {
if line.contains('(') && line.contains(')') {
return Some(Line::from(Span::styled(
line.to_string(),
Style::default().fg(Color::Yellow),
)));
}
return None;
}
let display = if line.chars().count() > available_width {
let truncated: String = line
.chars()
.take(available_width.saturating_sub(1))
.collect();
format!("{}…", truncated)
} else {
line.to_string()
};
Some(Line::from(Span::styled(
display,
Style::default().fg(Color::White),
)))
}
}
impl Widget for &IrPane<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
let title = format!(" {} ", self.mode.name());
let block = Block::default()
.title(title)
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::DarkGray));
let inner = block.inner(area);
block.render(area, buf);
let available_width = inner.width as usize;
let lines = self.width_adapted_lines(available_width);
let paragraph = Paragraph::new(lines)
.scroll((self.scroll, 0))
.wrap(Wrap { trim: false });
paragraph.render(inner, buf);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_view_mode_cycling() {
let mode = IrViewMode::StackArt;
assert_eq!(mode.next(), IrViewMode::TypedAst);
assert_eq!(mode.next().next(), IrViewMode::LlvmIr);
assert_eq!(mode.next().next().next(), IrViewMode::StackArt);
}
#[test]
fn test_view_mode_names() {
assert_eq!(IrViewMode::StackArt.name(), "Stack Effects");
assert_eq!(IrViewMode::TypedAst.name(), "Typed AST");
assert_eq!(IrViewMode::LlvmIr.name(), "LLVM IR");
}
#[test]
fn test_ir_content_empty() {
let content = IrContent::new();
assert!(!content.has_errors());
assert!(content.content_for(IrViewMode::StackArt).is_empty());
}
#[test]
fn test_ir_pane_creation() {
let content = IrContent::new();
let pane = IrPane::new(&content).mode(IrViewMode::LlvmIr);
assert_eq!(pane.mode, IrViewMode::LlvmIr);
}
#[test]
fn test_ir_pane_render() -> Result<(), String> {
let content = IrContent {
stack_art: vec![
"┌───┐".to_string(),
"│ 5 │".to_string(),
"└───┘".to_string(),
],
..Default::default()
};
let pane = IrPane::new(&content);
let area = Rect::new(0, 0, 20, 10);
let mut buf = Buffer::empty(area);
(&pane).render(area, &mut buf);
let title_cell = buf.cell((1, 0)).ok_or("cell (1,0) should exist")?;
assert!(title_cell.symbol().chars().next().is_some());
Ok(())
}
}