use std::collections::BTreeSet;
use crate::lex;
use crate::session::Session;
pub const M_NEW: &str = "\x01\x02c-shell/new\x02\x01";
pub const M_VAL: &str = "\x01\x02c-shell/val\x02\x01";
pub const M_DONE: &str = "\x01\x02c-shell/done\x02\x01";
pub const M_STDIN: &str = "\x01\x02c-shell/stdin\x02\x01";
pub const HEADERS: &str = "\
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <inttypes.h>
#include <stddef.h>
#include <limits.h>
#include <ctype.h>
#include <stdarg.h>
#include <time.h>
";
const RUNTIME: &str = r##"
#define CS_VAL() fputs(CS_M_VAL, stdout)
static inline void cs_p_b (_Bool v) { CS_VAL(); printf("%s\n", v ? "true" : "false"); }
static inline void cs_p_c (char v) { CS_VAL(); printf("'%c' (%d)\n", (v >= 32 && v < 127) ? v : '?', (int)v); }
static inline void cs_p_i (long long v) { CS_VAL(); printf("%lld\n", v); }
static inline void cs_p_u (unsigned long long v) { CS_VAL(); printf("%llu\n", v); }
static inline void cs_p_d (double v) { CS_VAL(); printf("%.17g\n", v); }
static inline void cs_p_ld(long double v) { CS_VAL(); printf("%.21Lg\n", v); }
static inline void cs_p_s (const char *v) { CS_VAL(); if (v) printf("\"%s\"\n", v); else printf("NULL\n"); }
static inline void cs_p_p (const void *v) { CS_VAL(); if (v) printf("0x%" PRIxPTR "\n", (uintptr_t)v); else printf("NULL\n"); }
/* Struct-member formatting is deliberately different from top-level value
formatting: a char pointer member is an address, never an implicit %s
dereference. Generated struct printers extend the two empty association
lists below after the user's type definitions are visible. */
static inline void cs_m_b (const volatile _Bool *v) { printf("%s", *v ? "true" : "false"); }
static inline void cs_m_c (const volatile char *v) { printf("%d", (int)*v); }
static inline void cs_m_sc(const volatile signed char *v) { printf("%lld", (long long)*v); }
static inline void cs_m_uc(const volatile unsigned char *v) { printf("%llu", (unsigned long long)*v); }
static inline void cs_m_s (const volatile short *v) { printf("%lld", (long long)*v); }
static inline void cs_m_us(const volatile unsigned short *v) { printf("%llu", (unsigned long long)*v); }
static inline void cs_m_i (const volatile int *v) { printf("%lld", (long long)*v); }
static inline void cs_m_ui(const volatile unsigned int *v) { printf("%llu", (unsigned long long)*v); }
static inline void cs_m_l (const volatile long *v) { printf("%lld", (long long)*v); }
static inline void cs_m_ul(const volatile unsigned long *v) { printf("%llu", (unsigned long long)*v); }
static inline void cs_m_ll(const volatile long long *v) { printf("%lld", (long long)*v); }
static inline void cs_m_ull(const volatile unsigned long long *v){ printf("%llu", (unsigned long long)*v); }
static inline void cs_m_f (const volatile float *v) { printf("%.17g", (double)*v); }
static inline void cs_m_d (const volatile double *v) { printf("%.17g", *v); }
static inline void cs_m_ld(const volatile long double *v) { printf("%.21Lg", *v); }
static inline void cs_m_unknown(const volatile void *v) { (void)v; fputs("<unprintable>", stdout); }
#define CS_MEMBER_REF_TYPES(T, F) \
T *: F, const T *: F, volatile T *: F, const volatile T *: F
#define CS_AGG_PRINT_ASSOCIATIONS
#define CS_AGG_MEMBER_ASSOCIATIONS
#define CS_MEMBER_REF(x) _Generic(&(x), \
CS_MEMBER_REF_TYPES(_Bool, cs_m_b), \
CS_MEMBER_REF_TYPES(char, cs_m_c), \
CS_MEMBER_REF_TYPES(signed char, cs_m_sc), \
CS_MEMBER_REF_TYPES(unsigned char, cs_m_uc), \
CS_MEMBER_REF_TYPES(short, cs_m_s), \
CS_MEMBER_REF_TYPES(unsigned short, cs_m_us), \
CS_MEMBER_REF_TYPES(int, cs_m_i), \
CS_MEMBER_REF_TYPES(unsigned int, cs_m_ui), \
CS_MEMBER_REF_TYPES(long, cs_m_l), \
CS_MEMBER_REF_TYPES(unsigned long, cs_m_ul), \
CS_MEMBER_REF_TYPES(long long, cs_m_ll), \
CS_MEMBER_REF_TYPES(unsigned long long, cs_m_ull), \
CS_MEMBER_REF_TYPES(float, cs_m_f), \
CS_MEMBER_REF_TYPES(double, cs_m_d), \
CS_MEMBER_REF_TYPES(long double, cs_m_ld), \
CS_AGG_MEMBER_ASSOCIATIONS \
default: cs_m_unknown)(&(x))
#define CS_MEMBER_PTR(x) do { \
if ((x) == 0) fputs("NULL", stdout); \
else printf("(void *)0x%" PRIxPTR, (uintptr_t)(x)); \
} while (0)
#define CS_PRINT(x) _Generic((x), \
_Bool: cs_p_b, char: cs_p_c, \
signed char: cs_p_i, unsigned char: cs_p_u, \
short: cs_p_i, unsigned short: cs_p_u, \
int: cs_p_i, unsigned int: cs_p_u, \
long: cs_p_i, unsigned long: cs_p_u, \
long long: cs_p_i, unsigned long long: cs_p_u, \
float: cs_p_d, double: cs_p_d, \
long double: cs_p_ld, \
char *: cs_p_s, const char *: cs_p_s, \
CS_AGG_PRINT_ASSOCIATIONS \
default: cs_p_p)(x)
/* An array decays to a pointer before _Generic can see it, so array-ness is
settled at run time instead: an array object begins at its first element,
while a pointer object is stored somewhere else entirely. Both branches
type-check for either category, so one program serves both. The element
count comes from sizeof, so no declarator has to be parsed. */
#define CS_ARRAY_LIMIT 100
#define CS_IS_ARRAY(x) \
((const volatile void *)&(x) == (const volatile void *)&(x)[0])
#define CS_ARRAY_ELIDE(n) printf("... (%llu more)", (unsigned long long)(n))
#define CS_PRINT_ARRAY1(x) do { \
if (!CS_IS_ARRAY(x)) { CS_PRINT(x); break; } \
CS_VAL(); \
{ size_t cs_n0 = sizeof(x) / sizeof((x)[0]), cs_i0; \
fputc('{', stdout); \
for (cs_i0 = 0; cs_i0 < cs_n0; ++cs_i0) { \
if (cs_i0) fputs(", ", stdout); \
if (cs_i0 == CS_ARRAY_LIMIT) { CS_ARRAY_ELIDE(cs_n0 - cs_i0); break; } \
CS_MEMBER_REF((x)[cs_i0]); \
} \
fputc('}', stdout); } \
putchar('\n'); \
} while (0)
/* Nesting depth is a static property, so it cannot be chosen at run time the
way array-ness can. Indexing twice does still type-check for an array of
pointers, where the inner subscript would walk off into whatever each
pointer happens to address, so the element gets the same run-time array
test as the whole and falls back to the member printer when it fails. */
#define CS_PRINT_ARRAY2(x) do { \
if (!CS_IS_ARRAY(x)) { CS_PRINT(x); break; } \
CS_VAL(); \
{ size_t cs_n0 = sizeof(x) / sizeof((x)[0]), cs_i0; \
fputc('{', stdout); \
for (cs_i0 = 0; cs_i0 < cs_n0; ++cs_i0) { \
if (cs_i0) fputs(", ", stdout); \
if (cs_i0 == CS_ARRAY_LIMIT) { CS_ARRAY_ELIDE(cs_n0 - cs_i0); break; } \
if (!CS_IS_ARRAY((x)[cs_i0])) { CS_MEMBER_REF((x)[cs_i0]); continue; } \
{ size_t cs_n1 = sizeof((x)[cs_i0]) / sizeof((x)[cs_i0][0]), cs_i1; \
fputc('{', stdout); \
for (cs_i1 = 0; cs_i1 < cs_n1; ++cs_i1) { \
if (cs_i1) fputs(", ", stdout); \
if (cs_i1 == CS_ARRAY_LIMIT) { CS_ARRAY_ELIDE(cs_n1 - cs_i1); break; } \
CS_MEMBER_REF((x)[cs_i0][cs_i1]); \
} \
fputc('}', stdout); } \
} \
fputc('}', stdout); } \
putchar('\n'); \
} while (0)
/* C has no general type reflection. This portable _Generic table reports
scalar types and scalar pointers. The controlling expression is not
evaluated; its normal lvalue/array/function conversions still apply. */
#define CS_PTR_TYPES(T, N) \
T *: N " *", const T *: "const " N " *", \
volatile T *: "volatile " N " *", \
const volatile T *: "const volatile " N " *"
#define CS_TYPE_ASSOCIATIONS \
_Bool: "_Bool", char: "char", \
signed char: "signed char", unsigned char: "unsigned char", \
short: "short", unsigned short: "unsigned short", \
int: "int", unsigned int: "unsigned int", \
long: "long", unsigned long: "unsigned long", \
long long: "long long", unsigned long long: "unsigned long long", \
float: "float", double: "double", \
long double: "long double", \
CS_PTR_TYPES(void, "void"), \
CS_PTR_TYPES(_Bool, "_Bool"), \
CS_PTR_TYPES(char, "char"), \
CS_PTR_TYPES(signed char, "signed char"), \
CS_PTR_TYPES(unsigned char, "unsigned char"), \
CS_PTR_TYPES(short, "short"), \
CS_PTR_TYPES(unsigned short, "unsigned short"), \
CS_PTR_TYPES(int, "int"), \
CS_PTR_TYPES(unsigned int, "unsigned int"), \
CS_PTR_TYPES(long, "long"), \
CS_PTR_TYPES(unsigned long, "unsigned long"), \
CS_PTR_TYPES(long long, "long long"), \
CS_PTR_TYPES(unsigned long long, "unsigned long long"), \
CS_PTR_TYPES(float, "float"), \
CS_PTR_TYPES(double, "double"), \
CS_PTR_TYPES(long double, "long double")
#define CS_TYPE_NAME(x) _Generic((x), CS_TYPE_ASSOCIATIONS, \
default: "<unrecognized type>")
#define CS_MARK(m) do { \
fputs(m, stdout); fflush(stdout); \
fputs(m, stderr); fflush(stderr); \
} while (0)
/* scanf is routed through a line-request marker. The parent supplies recorded
bytes while replaying and reads one fresh terminal line only for the newest
input. vscanf keeps format conversion and destination writes in libc. */
static int cs_taped_scanf(const char *format, ...)
{
int result;
va_list args;
fputs(CS_M_STDIN, stderr);
fflush(stderr);
va_start(args, format);
result = vscanf(format, args);
va_end(args);
return result;
}
#ifdef scanf
#undef scanf
#endif
#define scanf cs_taped_scanf
static inline uint64_t cs_timeit_now_ns(void) {
#if defined(_WIN32)
return (uint64_t)clock() * 1000000ULL;
#else
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (uint64_t)ts.tv_sec * 1000000000ULL + (uint64_t)ts.tv_nsec;
#endif
}
"##;
pub const MAX_ARRAY_DEPTH: usize = 2;
pub const UNPRINTABLE: &str = "<unprintable>";
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Slot {
Expr,
Stmt,
FileScope,
}
pub struct Program {
pub src: String,
pub new_start_line: usize,
pub new_line_count: usize,
pub session_line_ranges: Vec<(usize, usize)>,
pub uses_stdin_tape: bool,
pub wrapped: bool,
}
#[derive(Clone, Copy)]
enum BuildFlavor {
Normal(Slot),
SilentExpr,
TypeProbe,
TimeitProbe(u64),
ArrayExpr(usize),
ScopedStmt,
ReplaceFile(usize),
}
impl BuildFlavor {
fn slot(self) -> Slot {
match self {
Self::Normal(slot) => slot,
Self::SilentExpr | Self::TypeProbe | Self::TimeitProbe(_) | Self::ArrayExpr(_) => {
Slot::Expr
}
Self::ScopedStmt => Slot::Stmt,
Self::ReplaceFile(_) => Slot::FileScope,
}
}
}
pub fn build_user_view(session: &Session) -> String {
let mut src = String::new();
for item in &session.file_items {
src.push_str(item);
src.push('\n');
}
if !session.file_items.is_empty() {
src.push('\n');
}
src.push_str("int main(void)\n{\n");
for (index, stmt) in session.stmts.iter().enumerate() {
let events = session.stmt_stdin_event_count(index);
if events > 0 {
src.push_str(&format!(
" /* stdin tape: {events} captured request(s); contents hidden */\n"
));
}
src.push_str(stmt);
src.push('\n');
}
for _ in 0..session.scope_depth {
src.push_str(" }\n");
}
src.push_str("}\n");
src
}
pub fn build(session: &Session, input: &str, slot: Slot) -> Program {
build_inner(session, input, BuildFlavor::Normal(slot))
}
pub fn build_scoped_stmt(session: &Session, input: &str) -> Program {
build_inner(session, input, BuildFlavor::ScopedStmt)
}
pub fn build_file_replacement(session: &Session, input: &str, index: usize) -> Program {
build_inner(session, input, BuildFlavor::ReplaceFile(index))
}
pub fn build_expr_probe(session: &Session, input: &str) -> Program {
build_inner(session, input, BuildFlavor::SilentExpr)
}
pub fn build_type_probe(session: &Session, input: &str) -> Program {
build_inner(session, input, BuildFlavor::TypeProbe)
}
pub fn build_array_expr(session: &Session, input: &str, depth: usize) -> Program {
build_inner(session, input, BuildFlavor::ArrayExpr(depth))
}
pub fn build_timeit_probe(session: &Session, input: &str, loops: u64) -> Program {
build_inner(session, input, BuildFlavor::TimeitProbe(loops))
}
fn build_inner(session: &Session, input: &str, flavor: BuildFlavor) -> Program {
let slot = flavor.slot();
let silent_expr = matches!(flavor, BuildFlavor::SilentExpr);
let type_probe = matches!(flavor, BuildFlavor::TypeProbe);
let scoped_stmt = matches!(flavor, BuildFlavor::ScopedStmt);
let array_depth = match flavor {
BuildFlavor::ArrayExpr(depth) => Some(depth),
_ => None,
};
let timeit_loops = match flavor {
BuildFlavor::TimeitProbe(loops) => Some(loops),
_ => None,
};
let file_replacement = match flavor {
BuildFlavor::ReplaceFile(index) => Some(index),
_ => None,
};
let uses_stdin_tape = session
.file_items
.iter()
.chain(session.stmts.iter())
.map(String::as_str)
.chain(std::iter::once(input))
.any(|source| lex::contains_code_identifier(source, "scanf"));
let mut src = String::with_capacity(4096);
let mut new_start_line = 1usize;
let mut session_line_ranges = Vec::new();
src.push_str(HEADERS);
src.push_str(&format!("#define CS_M_NEW \"{}\"\n", escape(M_NEW)));
src.push_str(&format!("#define CS_M_VAL \"{}\"\n", escape(M_VAL)));
src.push_str(&format!("#define CS_M_DONE \"{}\"\n", escape(M_DONE)));
src.push_str(&format!("#define CS_M_STDIN \"{}\"\n", escape(M_STDIN)));
src.push_str(RUNTIME);
for (index, item) in session.file_items.iter().enumerate() {
if file_replacement == Some(index) {
new_start_line = src.lines().count() + 1;
src.push_str(input);
} else {
let start = src.lines().count() + 1;
src.push_str(item);
session_line_ranges.push((start, item.lines().count().max(1)));
}
src.push('\n');
}
if slot == Slot::FileScope && file_replacement.is_none() {
new_start_line = src.lines().count() + 1;
src.push_str(input);
src.push('\n');
}
let aggregate_printers = build_aggregate_printers(&src);
src.push_str(&aggregate_printers);
src.push_str("\nint main(void)\n{\n");
for stmt in &session.stmts {
let start = src.lines().count() + 1;
src.push_str(stmt);
session_line_ranges.push((start, stmt.lines().count().max(1)));
src.push('\n');
}
src.push_str(" CS_MARK(CS_M_NEW);\n");
match slot {
Slot::FileScope => {}
Slot::Stmt => {
if scoped_stmt {
src.push_str(" {\n");
}
new_start_line = src.lines().count() + 1;
src.push_str(input);
src.push('\n');
}
Slot::Expr => {
new_start_line = src.lines().count() + if type_probe { 3 } else { 2 };
if type_probe {
let aggregates = aggregate_types(session, input);
src.push_str(" CS_VAL();\n");
if aggregates.is_empty() {
src.push_str(&format!(" puts(CS_TYPE_NAME((\n{input}\n )));\n"));
} else {
src.push_str(&format!(" puts(_Generic((\n{input}\n ),\n"));
src.push_str(" CS_TYPE_ASSOCIATIONS,\n");
for (ty, category) in aggregates {
let name = ty
.strip_prefix("struct ")
.or_else(|| ty.strip_prefix("union "))
.unwrap_or(&ty);
src.push_str(&format!(" {ty}: \"{category} {name}\",\n"));
}
src.push_str(" default: \"<unrecognized type>\"));\n");
}
} else if let Some(loops) = timeit_loops {
src.push_str(" CS_VAL();\n");
src.push_str(" {\n");
src.push_str(&format!(" uint64_t _cs_loops = {loops}ULL;\n"));
src.push_str(" uint64_t _cs_start = cs_timeit_now_ns();\n");
src.push_str(" for (uint64_t _cs_i = 0; _cs_i < _cs_loops; ++_cs_i) {\n");
if crate::eval::should_try_expr(input) {
src.push_str(&format!(" (void)(\n{input}\n );\n"));
} else {
src.push_str(&format!(" {{\n{input}\n }}\n"));
}
src.push_str(" }\n");
src.push_str(" uint64_t _cs_end = cs_timeit_now_ns();\n");
src.push_str(
" printf(\"%llu\\n\", (unsigned long long)(_cs_end - _cs_start));\n",
);
src.push_str(" }\n");
} else if silent_expr {
src.push_str(&format!(" (void)(\n{input}\n );\n"));
} else if let Some(depth) = array_depth {
src.push_str(&format!(" CS_PRINT_ARRAY{depth}((\n{input}\n ));\n"));
} else {
src.push_str(&format!(" CS_PRINT((\n{input}\n ));\n"));
}
}
}
src.push_str(" CS_MARK(CS_M_DONE);\n");
for _ in 0..session.scope_depth + usize::from(scoped_stmt) {
src.push_str(" }\n");
}
src.push_str(" return 0;\n}\n");
let new_line_count = input.lines().count().max(1);
Program {
src,
new_start_line,
new_line_count,
session_line_ranges,
uses_stdin_tape,
wrapped: slot == Slot::Expr && !silent_expr && timeit_loops.is_none(),
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum AggregateToken {
Ident(String),
Punct(char),
Directive,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum AggregateMemberKind {
Value,
Pointer,
Array {
dimensions: usize,
pointer_elements: bool,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct AggregateMember {
name: String,
kind: AggregateMemberKind,
type_hint: Option<String>,
visibly_nested: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct AggregatePrinter {
ty: String,
members: Vec<AggregateMember>,
byte_fallback: bool,
}
fn build_aggregate_printers(source: &str) -> String {
let printers = collect_aggregate_printers(source);
if printers.is_empty() {
return String::new();
}
let mut out = String::from("\n/* c-shell aggregate printers */\n");
out.push_str("static unsigned cs_aggregate_depth;\n");
out.push_str(
"static void cs_aggregate_indent(void) { unsigned i; for (i = 0; i < cs_aggregate_depth; ++i) fputs(\" \", stdout); }\n",
);
for (index, printer) in printers.iter().enumerate() {
out.push_str(&format!(
"static void cs_m_ag_{index}(const volatile {} *);\n",
printer.ty
));
out.push_str(&format!("static void cs_p_ag_{index}({});\n", printer.ty));
}
out.push_str("#undef CS_AGG_PRINT_ASSOCIATIONS\n");
out.push_str("#define CS_AGG_PRINT_ASSOCIATIONS ");
for (index, printer) in printers.iter().enumerate() {
out.push_str(&format!("{}: cs_p_ag_{index}, ", printer.ty));
}
out.push('\n');
out.push_str("#undef CS_AGG_MEMBER_ASSOCIATIONS\n");
out.push_str("#define CS_AGG_MEMBER_ASSOCIATIONS ");
for (index, printer) in printers.iter().enumerate() {
out.push_str(&format!(
"{} *: cs_m_ag_{index}, const {} *: cs_m_ag_{index}, volatile {} *: cs_m_ag_{index}, const volatile {} *: cs_m_ag_{index}, ",
printer.ty, printer.ty, printer.ty, printer.ty
));
}
out.push('\n');
for (index, printer) in printers.iter().enumerate() {
if printer.byte_fallback {
emit_byte_fallback(&mut out, index, printer);
} else {
emit_struct_formatter(&mut out, index, printer);
}
out.push_str(&format!(
"static void cs_p_ag_{index}({ty} v) {{ CS_VAL(); cs_aggregate_depth = 0; cs_m_ag_{index}(&v); putchar('\\n'); }}\n",
ty = printer.ty
));
}
out
}
fn emit_byte_fallback(out: &mut String, index: usize, printer: &AggregatePrinter) {
let label = escape(&printer.ty);
out.push_str(&format!(
"static void cs_m_ag_{index}(const volatile {ty} *v) {{\n\
const volatile unsigned char *p = (const volatile unsigned char *)v;\n\
size_t i; fputs(\"<{label} raw bytes:\", stdout);\n\
for (i = 0; i < sizeof(*v); ++i) printf(\" %02x\", (unsigned)p[i]);\n\
fputc('>', stdout);\n\
}}\n",
ty = printer.ty
));
}
fn emit_struct_formatter(out: &mut String, index: usize, printer: &AggregatePrinter) {
let multiline = printer.members.len() > 4
|| printer.members.iter().any(|member| {
member.visibly_nested || matches!(member.kind, AggregateMemberKind::Array { .. })
});
out.push_str(&format!(
"static void cs_m_ag_{index}(const volatile {} *v) {{\n",
printer.ty
));
if multiline {
out.push_str("fputs(\"{\\n\", stdout); ++cs_aggregate_depth;\n");
} else {
out.push_str("fputs(\"{ \", stdout);\n");
}
for (member_index, member) in printer.members.iter().enumerate() {
if multiline {
if member_index > 0 {
out.push_str("fputs(\",\\n\", stdout);\n");
}
out.push_str("cs_aggregate_indent();\n");
} else if member_index > 0 {
out.push_str("fputs(\", \", stdout);\n");
}
out.push_str(&format!("fputs(\".{} = \", stdout);\n", member.name));
let expr = format!("v->{}", member.name);
match member.kind {
AggregateMemberKind::Value => {
out.push_str(&format!("CS_MEMBER_REF({expr});\n"));
}
AggregateMemberKind::Pointer => {
out.push_str(&format!("CS_MEMBER_PTR({expr});\n"));
}
AggregateMemberKind::Array {
dimensions,
pointer_elements,
} => emit_array_formatter(out, &expr, dimensions, pointer_elements, member_index, 0),
}
}
if multiline {
out.push_str("--cs_aggregate_depth; fputc('\\n', stdout); cs_aggregate_indent(); fputc('}', stdout);\n");
} else {
out.push_str("fputs(\" }\", stdout);\n");
}
out.push_str("}\n");
}
fn emit_array_formatter(
out: &mut String,
expr: &str,
dimensions: usize,
pointer_elements: bool,
member_index: usize,
level: usize,
) {
let index = format!("cs_i_{member_index}_{level}");
let count = format!("cs_n_{member_index}_{level}");
out.push_str("{ ");
out.push_str(&format!(
"size_t {count} = sizeof({expr}) / sizeof(({expr})[0]); size_t {index}; fputc('{{', stdout); "
));
out.push_str(&format!(
"for ({index} = 0; {index} < {count}; ++{index}) {{ if ({index}) fputs(\", \", stdout); \
if ({index} == CS_ARRAY_LIMIT) {{ CS_ARRAY_ELIDE({count} - {index}); break; }} "
));
let element = format!("({expr})[{index}]");
if level + 1 < dimensions {
emit_array_formatter(
out,
&element,
dimensions,
pointer_elements,
member_index,
level + 1,
);
} else if pointer_elements {
out.push_str(&format!("CS_MEMBER_PTR({element}); "));
} else {
out.push_str(&format!("CS_MEMBER_REF({element}); "));
}
out.push_str("} fputc('}', stdout); }\n");
}
fn collect_aggregate_printers(source: &str) -> Vec<AggregatePrinter> {
let tokens = aggregate_tokens(source);
let mut printers = Vec::new();
let mut seen = BTreeSet::new();
let mut brace_depth = 0usize;
for (i, token) in tokens.iter().enumerate() {
let AggregateToken::Ident(kind) = token else {
match token {
AggregateToken::Punct('{') => brace_depth += 1,
AggregateToken::Punct('}') => brace_depth = brace_depth.saturating_sub(1),
_ => {}
}
continue;
};
if brace_depth != 0 || !matches!(kind.as_str(), "struct" | "union") {
continue;
}
let (tag, open) = match (tokens.get(i + 1), tokens.get(i + 2)) {
(Some(AggregateToken::Ident(tag)), Some(AggregateToken::Punct('{'))) => {
(Some(tag.clone()), i + 2)
}
(Some(AggregateToken::Punct('{')), _) => (None, i + 1),
_ => continue,
};
let Some(close) = matching_brace(&tokens, open) else {
continue;
};
let ty = if let Some(tag) = tag {
format!("{kind} {tag}")
} else {
let preceded_by_typedef = i
.checked_sub(1)
.and_then(|j| tokens.get(j))
.is_some_and(|t| matches!(t, AggregateToken::Ident(word) if word == "typedef"));
if !preceded_by_typedef {
continue;
}
match (tokens.get(close + 1), tokens.get(close + 2)) {
(Some(AggregateToken::Ident(alias)), Some(AggregateToken::Punct(';'))) => {
alias.clone()
}
_ => continue,
}
};
if !seen.insert(ty.clone()) {
continue;
}
let members = parse_aggregate_members(&tokens[open + 1..close]);
let (members, byte_fallback) = match members {
Ok(members) if kind == "struct" && !members.is_empty() => (members, false),
_ => (Vec::new(), true),
};
printers.push(AggregatePrinter {
ty,
members,
byte_fallback,
});
}
let known: BTreeSet<String> = printers.iter().map(|printer| printer.ty.clone()).collect();
for printer in &mut printers {
for member in &mut printer.members {
member.visibly_nested |= matches!(member.kind, AggregateMemberKind::Value)
&& member
.type_hint
.as_ref()
.is_some_and(|hint| known.contains(hint));
}
}
printers
}
fn matching_brace(tokens: &[AggregateToken], open: usize) -> Option<usize> {
let mut depth = 0usize;
for (i, token) in tokens.iter().enumerate().skip(open) {
match token {
AggregateToken::Punct('{') => depth += 1,
AggregateToken::Punct('}') => {
depth -= 1;
if depth == 0 {
return Some(i);
}
}
_ => {}
}
}
None
}
fn parse_aggregate_members(tokens: &[AggregateToken]) -> Result<Vec<AggregateMember>, ()> {
if tokens
.iter()
.any(|token| matches!(token, AggregateToken::Directive))
{
return Err(());
}
let mut members = Vec::new();
let mut start = 0usize;
let (mut braces, mut parens, mut brackets) = (0usize, 0usize, 0usize);
for (i, token) in tokens.iter().enumerate() {
match token {
AggregateToken::Punct('{') => braces += 1,
AggregateToken::Punct('}') => braces = braces.saturating_sub(1),
AggregateToken::Punct('(') => parens += 1,
AggregateToken::Punct(')') => parens = parens.saturating_sub(1),
AggregateToken::Punct('[') => brackets += 1,
AggregateToken::Punct(']') => brackets = brackets.saturating_sub(1),
AggregateToken::Punct(';') if braces == 0 && parens == 0 && brackets == 0 => {
let decl = &tokens[start..i];
if !decl.is_empty()
&& !matches!(decl.first(), Some(AggregateToken::Ident(word)) if word == "_Static_assert" || word == "static_assert")
{
members.push(parse_aggregate_member(decl)?);
}
start = i + 1;
}
_ => {}
}
}
if tokens[start..]
.iter()
.any(|token| !matches!(token, AggregateToken::Punct(';')))
{
return Err(());
}
Ok(members)
}
fn parse_aggregate_member(tokens: &[AggregateToken]) -> Result<AggregateMember, ()> {
let mut top_identifiers = Vec::new();
let (mut braces, mut parens, mut brackets) = (0usize, 0usize, 0usize);
let mut stars = 0usize;
let mut dimensions = 0usize;
let mut flexible = false;
let mut inline_aggregate = false;
let mut saw_parentheses = false;
for (i, token) in tokens.iter().enumerate() {
match token {
AggregateToken::Punct('{') => {
if braces == 0 && parens == 0 && brackets == 0 {
inline_aggregate = true;
}
braces += 1;
}
AggregateToken::Punct('}') => braces = braces.saturating_sub(1),
AggregateToken::Punct('(') => {
if braces == 0 && brackets == 0 {
saw_parentheses = true;
}
parens += 1;
}
AggregateToken::Punct(')') => parens = parens.saturating_sub(1),
AggregateToken::Punct('[') if braces == 0 && parens == 0 && brackets == 0 => {
dimensions += 1;
flexible |= matches!(tokens.get(i + 1), Some(AggregateToken::Punct(']')));
brackets += 1;
}
AggregateToken::Punct('[') => brackets += 1,
AggregateToken::Punct(']') => brackets = brackets.saturating_sub(1),
AggregateToken::Punct(',') if braces == 0 && parens == 0 && brackets == 0 => {
return Err(());
}
AggregateToken::Punct(':') if braces == 0 && parens == 0 && brackets == 0 => {
return Err(());
}
AggregateToken::Punct('*') if braces == 0 && brackets == 0 => {
if parens > 0 {
return Err(());
}
stars += 1;
}
AggregateToken::Ident(word) if braces == 0 && parens == 0 && brackets == 0 => {
if matches!(word.as_str(), "__attribute__" | "__declspec") {
return Err(());
}
top_identifiers.push((i, word.clone()));
}
_ => {}
}
}
if flexible || top_identifiers.len() < 2 {
return Err(());
}
let (_, name) = top_identifiers.last().ok_or(())?;
if is_type_word(name) {
return Err(());
}
let first = top_identifiers.first().map(|(_, word)| word.as_str());
let type_prefix = &top_identifiers[..top_identifiers.len() - 1];
let has_tag_keyword = type_prefix
.iter()
.any(|(_, word)| matches!(word.as_str(), "struct" | "union" | "enum"));
let unknown_type_words = type_prefix
.iter()
.filter(|(_, word)| !is_type_word(word))
.count();
let mixes_typedef_with_builtin = unknown_type_words > 0
&& type_prefix
.iter()
.any(|(_, word)| is_concrete_type_word(word));
if !has_tag_keyword && (unknown_type_words > 1 || mixes_typedef_with_builtin) {
return Err(());
}
if saw_parentheses && !matches!(first, Some("_Atomic" | "_Alignas" | "alignas")) {
return Err(());
}
if matches!(first, Some("struct" | "union" | "enum"))
&& top_identifiers.len() == 2
&& !inline_aggregate
{
return Err(());
}
if inline_aggregate && stars == 0 {
return Err(());
}
let visibly_nested = stars == 0 && matches!(first, Some("struct" | "union"));
let type_hint = if matches!(first, Some("struct" | "union")) {
top_identifiers
.get(1)
.map(|(_, tag)| format!("{} {tag}", first.expect("matched aggregate keyword")))
} else {
top_identifiers
.iter()
.rev()
.skip(1)
.find(|(_, word)| !is_type_qualifier(word))
.map(|(_, word)| word.clone())
};
let kind = if dimensions > 0 {
AggregateMemberKind::Array {
dimensions,
pointer_elements: stars > 0,
}
} else if stars > 0 {
AggregateMemberKind::Pointer
} else {
AggregateMemberKind::Value
};
Ok(AggregateMember {
name: name.clone(),
kind,
type_hint,
visibly_nested,
})
}
fn is_type_qualifier(word: &str) -> bool {
matches!(word, "const" | "volatile" | "restrict" | "_Atomic")
}
fn is_concrete_type_word(word: &str) -> bool {
matches!(
word,
"void"
| "_Bool"
| "char"
| "short"
| "int"
| "long"
| "float"
| "double"
| "signed"
| "unsigned"
| "_Complex"
)
}
fn is_type_word(word: &str) -> bool {
is_concrete_type_word(word)
|| matches!(
word,
"const"
| "volatile"
| "restrict"
| "_Atomic"
| "_Alignas"
| "alignas"
| "struct"
| "union"
| "enum"
)
}
fn aggregate_tokens(src: &str) -> Vec<AggregateToken> {
let scan = lex::scan(src);
let bytes = src.as_bytes();
let mut out = Vec::new();
let mut i = 0usize;
let mut at_line_start = true;
let mut in_directive = false;
let mut conditional_depth = 0usize;
while i < bytes.len() {
if bytes[i] == b'\n' {
let continued = i > 0 && bytes[i - 1] == b'\\';
if !continued {
in_directive = false;
}
at_line_start = !in_directive;
i += 1;
continue;
}
if at_line_start && bytes[i].is_ascii_whitespace() {
i += 1;
continue;
}
if at_line_start && scan.code[i] && bytes[i] == b'#' {
let end = bytes[i..]
.iter()
.position(|byte| *byte == b'\n')
.map_or(bytes.len(), |offset| i + offset);
let directive = src[i + 1..end].trim_start();
let word = directive
.split(|c: char| c.is_ascii_whitespace())
.next()
.unwrap_or("");
match word {
"if" | "ifdef" | "ifndef" => conditional_depth += 1,
"endif" => conditional_depth = conditional_depth.saturating_sub(1),
_ => {}
}
out.push(AggregateToken::Directive);
in_directive = true;
at_line_start = false;
} else if at_line_start && scan.code[i] {
at_line_start = false;
}
if in_directive || conditional_depth > 0 || !scan.code[i] || bytes[i].is_ascii_whitespace()
{
i += 1;
continue;
}
if bytes[i] == b'_' || bytes[i].is_ascii_alphabetic() {
let start = i;
i += 1;
while i < bytes.len()
&& scan.code[i]
&& (bytes[i] == b'_' || bytes[i].is_ascii_alphanumeric())
{
i += 1;
}
out.push(AggregateToken::Ident(src[start..i].to_string()));
continue;
}
out.push(AggregateToken::Punct(bytes[i] as char));
i += 1;
}
out
}
#[derive(Debug, PartialEq, Eq)]
enum TypeToken {
Ident(String),
LBrace,
RBrace,
Semi,
Other,
}
fn aggregate_types(session: &Session, input: &str) -> Vec<(String, &'static str)> {
let mut found = BTreeSet::new();
for src in session
.file_items
.iter()
.chain(session.stmts.iter())
.map(String::as_str)
.chain(std::iter::once(input))
{
collect_aggregate_types(src, &mut found);
}
found.into_iter().collect()
}
fn collect_aggregate_types(src: &str, found: &mut BTreeSet<(String, &'static str)>) {
let tokens = type_tokens(src);
for (i, token) in tokens.iter().enumerate() {
let TypeToken::Ident(keyword) = token else {
continue;
};
let category = match keyword.as_str() {
"struct" => "Struct",
"union" => "Union",
_ => continue,
};
if let (Some(TypeToken::Ident(name)), Some(TypeToken::LBrace)) =
(tokens.get(i + 1), tokens.get(i + 2))
{
found.insert((format!("{keyword} {name}"), category));
continue;
}
let preceded_by_typedef = i
.checked_sub(1)
.and_then(|j| tokens.get(j))
.is_some_and(|t| matches!(t, TypeToken::Ident(word) if word == "typedef"));
if !preceded_by_typedef || tokens.get(i + 1) != Some(&TypeToken::LBrace) {
continue;
}
let mut depth = 0usize;
let mut close = None;
for (j, t) in tokens.iter().enumerate().skip(i + 1) {
match t {
TypeToken::LBrace => depth += 1,
TypeToken::RBrace => {
depth -= 1;
if depth == 0 {
close = Some(j);
break;
}
}
_ => {}
}
}
let Some(close) = close else { continue };
if let (Some(TypeToken::Ident(alias)), Some(TypeToken::Semi)) =
(tokens.get(close + 1), tokens.get(close + 2))
{
found.insert((alias.clone(), category));
}
}
}
fn type_tokens(src: &str) -> Vec<TypeToken> {
let sc = lex::scan(src);
let b = src.as_bytes();
let mut out = Vec::new();
let mut i = 0;
while i < b.len() {
if !sc.code[i] {
i += 1;
continue;
}
if b[i] == b'_' || b[i].is_ascii_alphabetic() {
let start = i;
i += 1;
while i < b.len() && sc.code[i] && (b[i] == b'_' || b[i].is_ascii_alphanumeric()) {
i += 1;
}
out.push(TypeToken::Ident(src[start..i].to_string()));
continue;
}
match b[i] {
b'{' => out.push(TypeToken::LBrace),
b'}' => out.push(TypeToken::RBrace),
b';' => out.push(TypeToken::Semi),
c if !c.is_ascii_whitespace() => out.push(TypeToken::Other),
_ => {}
}
i += 1;
}
out
}
fn escape(s: &str) -> String {
s.bytes().map(|b| format!("\\x{b:02x}")).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn finds_reusable_struct_and_union_type_spellings() {
let mut found = BTreeSet::new();
collect_aggregate_types(
"struct Point { int x; int y; };\n\
union Value { int i; double d; };\n\
typedef struct { int code; } Result;\n\
typedef union { int code; } *ResultPtr;\n\
struct Forward; struct Forward *ptr;\n\
/* union Fake { int no; }; */",
&mut found,
);
assert!(found.contains(&("struct Point".into(), "Struct")));
assert!(found.contains(&("union Value".into(), "Union")));
assert!(found.contains(&("Result".into(), "Struct")));
assert!(!found.iter().any(|(ty, _)| ty.contains("ResultPtr")));
assert!(!found.iter().any(|(ty, _)| ty.contains("Forward")));
assert!(!found.iter().any(|(ty, _)| ty.contains("Fake")));
}
#[test]
fn type_probe_adds_session_aggregate_associations() {
let mut session = Session::default();
session.file_items.push("struct Pair { int x, y; };".into());
session.stmts.push("struct Pair pair = { 1, 2 };".into());
let program = build_type_probe(&session, "pair");
assert!(program.src.contains("struct Pair: \"Struct Pair\""));
}
#[test]
fn runtime_backs_every_advertised_array_constant() {
assert!(
RUNTIME.contains(&format!("fputs(\"{UNPRINTABLE}\", stdout)")),
"the member fallback no longer prints UNPRINTABLE"
);
for depth in 1..=MAX_ARRAY_DEPTH {
assert!(
RUNTIME.contains(&format!("#define CS_PRINT_ARRAY{depth}(")),
"no runtime printer for array depth {depth}"
);
}
assert!(
!RUNTIME.contains(&format!("#define CS_PRINT_ARRAY{}(", MAX_ARRAY_DEPTH + 1)),
"a deeper printer exists but MAX_ARRAY_DEPTH does not reach it"
);
}
#[test]
fn array_expression_builds_select_the_matching_depth_wrapper() {
let session = Session::default();
let one = build_array_expr(&session, "values", 1);
assert!(one.src.contains("CS_PRINT_ARRAY1((\nvalues\n ));"));
assert!(one.wrapped, "the array wrapper must remap like CS_PRINT");
let two = build_array_expr(&session, "grid", 2);
assert!(two.src.contains("CS_PRINT_ARRAY2((\ngrid\n ));"));
}
#[test]
fn aggregate_printers_are_safe_and_conservative_about_declarators() {
let printers = collect_aggregate_printers(
"#define FAKE_TYPE struct Fake { int hidden; }\n\
#define FIELDS int injected;\n\
#if 0\nstruct Conditional { int hidden; };\n#endif\n\
struct Person { char *name; int age; int scores[3]; struct Person *next; };\n\
struct Pair { int x, y; };\n\
struct MacroFields { FIELDS int visible; };\n\
struct Hooks { int (*callback)(void); int enabled; };\n\
struct Anonymous { struct { int value; }; int other; };",
);
assert!(
printers.iter().all(|printer| {
printer.ty != "struct Fake" && printer.ty != "struct Conditional"
}),
"preprocessor-controlled text is not a stable visible type definition"
);
let person = printers
.iter()
.find(|printer| printer.ty == "struct Person")
.expect("Person printer");
assert!(!person.byte_fallback);
assert_eq!(
person
.members
.iter()
.map(|member| (member.name.as_str(), member.kind))
.collect::<Vec<_>>(),
vec![
("name", AggregateMemberKind::Pointer),
("age", AggregateMemberKind::Value),
(
"scores",
AggregateMemberKind::Array {
dimensions: 1,
pointer_elements: false
}
),
("next", AggregateMemberKind::Pointer),
]
);
for ty in [
"struct Pair",
"struct MacroFields",
"struct Hooks",
"struct Anonymous",
] {
assert!(
printers
.iter()
.find(|printer| printer.ty == ty)
.is_some_and(|printer| printer.byte_fallback),
"{ty} should use the conservative byte fallback"
);
}
let generated = build_aggregate_printers(
"struct Child { int value; }; struct Parent { struct Child child; char *text; };",
);
assert!(generated.contains("CS_AGG_MEMBER_ASSOCIATIONS"));
assert!(generated.contains("CS_MEMBER_PTR(v->text)"));
assert!(!generated.contains("CS_PRINT(v->text)"));
}
#[test]
fn user_source_view_omits_runtime_scaffolding() {
let mut session = Session::default();
session.commit("int twice(int x) { return x * 2; }", Slot::FileScope);
session.commit("int value = 21;", Slot::Stmt);
session.attach_stdin_events(vec![crate::proc::StdinEvent {
bytes: b"secret input\n".to_vec(),
eof: false,
}]);
let src = build_user_view(&session);
assert!(src.contains("int twice(int x)"));
assert!(src.contains("int main(void)"));
assert!(src.contains("int value = 21;"));
assert!(src.contains("stdin tape: 1 captured request(s); contents hidden"));
assert!(!src.contains("secret input"));
assert!(!src.contains("CS_PRINT"));
assert!(!src.contains("CS_MARK"));
}
#[test]
fn scoped_statements_and_file_replacements_preserve_program_shape() {
let mut session = Session::default();
session.file_items.push("int f(void) { return 1; }".into());
session
.file_items
.push("int g(void) { return f(); }".into());
let replacement = build_file_replacement(&session, "int f(void) { return 2; }", 0).src;
let new_f = replacement.find("return 2").expect("replacement present");
let dependent_g = replacement.find("int g(void)").expect("dependent present");
assert!(new_f < dependent_g);
assert!(!replacement.contains("return 1"));
session.commit_scoped("int x = 2;");
let future = build(&session, "x", Slot::Expr).src;
let declaration = future.find("int x = 2;").expect("scoped declaration");
let query = future.rfind("\nx\n").expect("future query");
let close = future.rfind(" }\n return 0;").expect("scope close");
assert!(declaration < query && query < close);
}
}