Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 84 additions & 117 deletions src/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use syn::{Expr, Ident, LitStr, spanned::Spanned};

/// Generate parsing code from tokens.
///
/// Returns `(code, anon_count)` or error for consecutive placeholders / missing args.
/// Returns `(code, anon_count)` or error for consecutive placeholders/missing args.
fn generate_parsing_code(
tokens: &[FormatToken],
explicit_args: &[&Expr],
Expand Down Expand Up @@ -87,157 +87,126 @@ fn generate_parsing_code(
Ok((generated, anon_index))
}

/// Generate code for named placeholder with separator.
fn generate_named_placeholder_with_separator(
name: &str,
/// Generate code for placeholder with separator (named or anonymous).
fn generate_placeholder_with_separator(
assignment_stmt: &proc_macro2::TokenStream,
var_desc: &str,
separator: &LitStr,
) -> proc_macro2::TokenStream {
let ident = Ident::new(name, Span::call_site());

quote! {
if let Some(pos) = remaining.find(#separator) {
{
let pos = remaining.find(#separator).ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"Expected separator {:?} for {} not found in remaining input: {:?}",
#separator,
#var_desc,
remaining
)
)
})?;
let slice = &remaining[..pos];
match slice.parse() {
Ok(parsed) => {
#ident = parsed;
}
Err(error) => {
result = result.and(Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("Failed to parse variable '{}' from {:?}: {}", #name, slice, error)
)));
}
}
remaining = &remaining[pos + #separator.len()..];
} else {
result = result.and(Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"Expected separator {:?} for variable '{}' not found in remaining input: {:?}",
#separator,
#name,
remaining
let parsed = slice.parse().map_err(|error| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("Failed to parse {} from {:?}: {}", #var_desc, slice, error)
)
)));
})?;
#assignment_stmt;
Copy link

Copilot AI Nov 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The semicolon after #assignment_stmt should be removed. This line expands a token stream that already includes proper statement termination (either #ident = parsed or *#arg_expr = parsed), and adding a semicolon creates = parsed;; which is a syntax error.

Suggested change
#assignment_stmt;
#assignment_stmt

Copilot uses AI. Check for mistakes.
remaining = &remaining[pos + #separator.len()..];
}
}
}

/// Generate code for named placeholder with separator.
///
/// Note: `assignment_stmt` contains the expression `#ident = parsed` WITHOUT a trailing
/// semicolon. The semicolon is added explicitly at the insertion point to form a complete
/// statement within the generated block.
fn generate_named_placeholder_with_separator(
name: &str,
separator: &LitStr,
) -> proc_macro2::TokenStream {
let ident = Ident::new(name, Span::call_site());
let assignment_stmt = quote! { #ident = parsed }; // No trailing semicolon
let var_desc = format!("variable '{name}'");
generate_placeholder_with_separator(&assignment_stmt, &var_desc, separator)
}

/// Generate code for anonymous placeholder with separator.
///
/// Note: `assignment_stmt` contains the expression `*#arg_expr = parsed` WITHOUT a trailing
/// semicolon. The semicolon is added explicitly at the insertion point to form a complete
/// statement within the generated block.
fn generate_anonymous_placeholder_with_separator(
arg_expr: &Expr,
placeholder_num: usize,
separator: &LitStr,
) -> proc_macro2::TokenStream {
quote! {
if let Some(pos) = remaining.find(#separator) {
let slice = &remaining[..pos];
match slice.parse() {
Ok(parsed) => {
*#arg_expr = parsed;
}
Err(error) => {
result = result.and(Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"Failed to parse anonymous placeholder #{} from {:?}: {}",
#placeholder_num,
slice,
error
)
)));
}
}
remaining = &remaining[pos + #separator.len()..];
} else {
result = result.and(Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"Expected separator {:?} for anonymous placeholder #{} not found in remaining input: {:?}",
#separator,
#placeholder_num,
remaining
)
)));
}
}
let assignment_stmt = quote! { *#arg_expr = parsed }; // No trailing semicolon
let var_desc = format!("anonymous placeholder #{placeholder_num}");
generate_placeholder_with_separator(&assignment_stmt, &var_desc, separator)
}

/// Generate code for fixed text matching at current position.
fn generate_fixed_text_match(text: &LitStr) -> proc_macro2::TokenStream {
quote! {
if let Some(pos) = remaining.find(#text) {
if pos == 0 {
remaining = &remaining[#text.len()..];
} else {
result = result.and(Err(std::io::Error::new(
{
if !remaining.starts_with(#text) {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"Expected text {:?} at current position, but found it at offset {}. \
Remaining input: {:?}",
"Expected text {:?} at current position. Remaining input: {:?}",
#text,
pos,
remaining
)
)));
));
}
} else {
result = result.and(Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"Required text separator {:?} not found. Remaining input: {:?}",
#text,
remaining
)
)));
remaining = &remaining[#text.len()..];
}
}
}

/// Generate code for final named placeholder (consumes rest of input).
fn generate_final_named_placeholder(name: &str) -> proc_macro2::TokenStream {
let ident = Ident::new(name, Span::call_site());

/// Generate code for final placeholder (consumes rest of input).
fn generate_final_placeholder(
assignment_stmt: &proc_macro2::TokenStream,
var_desc: &str,
) -> proc_macro2::TokenStream {
quote! {
match remaining.parse() {
Ok(parsed) => {
#ident = parsed;
}
Err(error) => {
result = result.and(Err(std::io::Error::new(
{
let parsed = remaining.parse().map_err(|error| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("Failed to parse variable '{}' from remaining input {:?}: {}", #name, remaining, error)
)));
}
format!("Failed to parse {} from remaining input {:?}: {}", #var_desc, remaining, error)
)
})?;
#assignment_stmt;
Copy link

Copilot AI Nov 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The semicolon after #assignment_stmt should be removed. This line expands a token stream that already includes proper statement termination (either #ident = parsed or *#arg_expr = parsed), and adding a semicolon creates = parsed;; which is a syntax error.

Suggested change
#assignment_stmt;
#assignment_stmt

Copilot uses AI. Check for mistakes.
remaining = "";
}
remaining = "";
}
}

/// Generate code for final named placeholder (consumes rest of input).
///
/// Note: `assignment_stmt` contains the expression WITHOUT a trailing semicolon.
fn generate_final_named_placeholder(name: &str) -> proc_macro2::TokenStream {
let ident = Ident::new(name, Span::call_site());
let assignment_stmt = quote! { #ident = parsed }; // No trailing semicolon
let var_desc = format!("variable '{name}'");
generate_final_placeholder(&assignment_stmt, &var_desc)
}

/// Generate code for final anonymous placeholder (consumes rest of input).
///
/// Note: `assignment_stmt` contains the expression WITHOUT a trailing semicolon.
fn generate_final_anonymous_placeholder(
arg_expr: &Expr,
placeholder_num: usize,
) -> proc_macro2::TokenStream {
quote! {
match remaining.parse() {
Ok(parsed) => {
*#arg_expr = parsed;
}
Err(error) => {
result = result.and(Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"Failed to parse anonymous placeholder #{} from remaining input {:?}: {}",
#placeholder_num,
remaining,
error
)
)));
}
}
remaining = "";
}
let assignment_stmt = quote! { *#arg_expr = parsed }; // No trailing semicolon
let var_desc = format!("anonymous placeholder #{placeholder_num}");
generate_final_placeholder(&assignment_stmt, &var_desc)
}

/// Create error for missing anonymous placeholder argument.
Expand All @@ -250,9 +219,8 @@ fn make_missing_argument_error(
syn::Error::new(
format_lit.span(),
format!(
"{}anonymous placeholder '{{}}' at position {} has no corresponding argument. \
Provide a mutable reference argument (e.g., &mut var) or use a named placeholder (e.g., '{{var}}')",
prefix, position
"{prefix}anonymous placeholder '{{}}' at position {position} has no corresponding argument. \
Provide a mutable reference argument (e.g., &mut var) or use a named placeholder (e.g., '{{var}}')"
),
)
.to_compile_error()
Expand Down Expand Up @@ -295,9 +263,8 @@ pub fn generate_scanf_implementation(
return Err(syn::Error::new(
explicit_args[anon_index].span(),
format!(
"Too many arguments: {} unused argument(s) provided. \
The format string only has {} anonymous placeholder(s)",
unused_count, anon_index
"Too many arguments: {unused_count} unused argument(s) provided. \
The format string only has {anon_index} anonymous placeholder(s)"
),
)
.to_compile_error()
Expand Down
36 changes: 17 additions & 19 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
//! # Architecture
//!
//! Compile-time: `tokenization` → `codegen` → expansion
//! Runtime: Generated code parses input with `.find()` and `.parse()`
//! Runtime: Generated code uses `.find()` and `.parse()`
//!
//! Modules: `constants`, `types`, `validation`, `parsing`, `tokenization`, `codegen`
//!
//! # Hygiene
//!
//! Generated code uses isolated scopes `{ }` (block expressions) - no prefix pollution.
//! Generated code uses isolated scopes - no variable pollution.
//!
//! # Limitations
//!
Expand All @@ -23,7 +23,7 @@
//!
//! # Security
//!
//! **DoS limits:** 10K bytes format, 256 tokens, 128 char identifiers
//! **`DoS` limits:** 10K bytes format, 256 tokens, 128 char identifiers
//! **Memory:** `#![forbid(unsafe_code)]`, `Box<str>`, bounds-checked
//! **Validation:** Rejects empty formats, keywords, invalid identifiers

Expand Down Expand Up @@ -88,10 +88,11 @@ pub fn sscanf(input: TokenStream) -> TokenStream {

// Scope isolation ensures macro hygiene
let expanded = quote! {{
let mut result: std::io::Result<()> = Ok(());
let mut remaining = #input_expr;
#(#generated)*
result
(|| -> std::io::Result<()> {
let mut remaining = #input_expr;
#(#generated)*
Ok(())
})()
}};

TokenStream::from(expanded)
Expand Down Expand Up @@ -132,18 +133,15 @@ pub fn scanf(input: TokenStream) -> TokenStream {

// Scope isolation ensures macro hygiene
let expanded = quote! {{
let mut result: std::io::Result<()> = Ok(());
let mut buffer = String::new();
let _ = std::io::Write::flush(&mut std::io::stdout());
match std::io::stdin().read_line(&mut buffer) {
Ok(_) => {
let input = buffer.trim_end_matches('\n').trim_end_matches('\r');
let mut remaining: &str = input;
#(#generated)*
result
}
Err(e) => Err(e)
}
(|| -> std::io::Result<()> {
let mut buffer = String::new();
let _ = std::io::Write::flush(&mut std::io::stdout());
std::io::stdin().read_line(&mut buffer)?;
let input = buffer.trim_end_matches('\n').trim_end_matches('\r');
let mut remaining: &str = input;
#(#generated)*
Ok(())
})()
}};
TokenStream::from(expanded)
}
2 changes: 1 addition & 1 deletion src/parsing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ impl Parse for SscanfArgs {
Punctuated::parse_terminated(input)?
};

Ok(SscanfArgs {
Ok(Self {
input: input_expr,
format,
args,
Expand Down
Loading