Skip to main content

inline_csharp_macros/
lib.rs

1//! Proc-macro implementation for `inline_csharp`.
2//!
3//! Provides three proc macros for embedding C# code in Rust:
4//!
5//! | Macro           | When it runs    |
6//! |-----------------|-----------------|
7//! | [`csharp!`]     | program runtime |
8//! | [`csharp_fn!`]  | program runtime |
9//! | [`ct_csharp!`]  | compile time    |
10//!
11//! All macros require the user to write a `static <T> Run(...)` method
12//! where `T` is one of: `sbyte`, `byte`, `short`, `ushort`, `int`, `uint`,
13//! `long`, `ulong`, `float`, `double`, `bool`, `char`, `string`, `T[]`,
14//! `List<T>`, or `T?` — including arbitrarily nested types.
15//!
16//! # Wire format (C# → Rust, stdout)
17//!
18//! The macro generates a `Main()` that binary-serialises `Run()`'s return
19//! value to stdout via `BinaryWriter` (raw UTF-8 for top-level `string`).
20//!
21//! Encoding per type (all little-endian):
22//! - `string` at top level: raw UTF-8 (no length prefix)
23//! - `string` inside a container: 4-byte LE `u32` length + UTF-8 bytes
24//! - scalar: fixed-width little-endian bytes via `BinaryWriter`
25//! - `T[]` / `List<T>`: 4-byte LE `u32` count + N × encode(T)
26//! - `T?` (Nullable): 1-byte tag (0=null, 1=present) + encode(T) if present
27//!
28//! # Wire format (Rust → C#, stdin)
29//!
30//! Parameters declared in `Run(...)` are serialised by Rust and piped to the
31//! child process's stdin. C# reads them with `BinaryReader`.
32//!
33//! # Options
34//!
35//! All three macros accept zero or more `key = "value"` pairs before the C# body,
36//! comma-separated.  Recognised keys:
37//!
38//! - `build = "<args>"` — extra arguments passed verbatim to `dotnet build`
39//! - `run   = "<args>"` — extra arguments passed verbatim to `dotnet run`
40//! - `reference = "<path>"` — add a reference assembly (repeatable)
41
42use proc_macro::TokenStream;
43use proc_macro2::TokenTree;
44use quote::{format_ident, quote};
45use std::collections::hash_map::DefaultHasher;
46use std::fmt::Write as FmtWrite;
47use std::hash::{Hash, Hasher};
48use std::str::FromStr;
49
50// ── CsharpType ───────────────────────────────────────────────────────────────
51
52/// Recursive composable C# type system.
53#[derive(Clone, PartialEq)]
54enum CsharpType {
55	// Signed integers
56	Sbyte,
57	Short,
58	Int,
59	Long,
60	// Unsigned integers
61	Byte,
62	Ushort,
63	Uint,
64	Ulong,
65	// Floats
66	Float,
67	Double,
68	// Other scalars
69	Bool,
70	Char,
71	Str,
72	// Composites
73	/// `T[]` — returned as `Vec<T>`
74	Array(Box<CsharpType>),
75	/// `List<T>` — same wire format / Rust type as `Array`
76	List(Box<CsharpType>),
77	/// `T?` — returned as `Option<T>`
78	Nullable(Box<CsharpType>),
79}
80
81impl CsharpType {
82	/// Parse a scalar type name to a `CsharpType`.
83	fn from_name(s: &str) -> Option<Self> {
84		match s {
85			"sbyte" | "SByte" => Some(Self::Sbyte),
86			"byte" | "Byte" => Some(Self::Byte),
87			"short" | "Int16" => Some(Self::Short),
88			"ushort" | "UInt16" => Some(Self::Ushort),
89			"int" | "Int32" => Some(Self::Int),
90			"uint" | "UInt32" => Some(Self::Uint),
91			"long" | "Int64" => Some(Self::Long),
92			"ulong" | "UInt64" => Some(Self::Ulong),
93			"float" | "Single" => Some(Self::Float),
94			"double" | "Double" => Some(Self::Double),
95			"bool" | "Boolean" => Some(Self::Bool),
96			"char" | "Char" => Some(Self::Char),
97			"string" | "String" => Some(Self::Str),
98			_ => None,
99		}
100	}
101
102	/// The C# type name for use in generated code.
103	fn csharp_type_name(&self) -> String {
104		match self {
105			Self::Sbyte => "sbyte".to_string(),
106			Self::Byte => "byte".to_string(),
107			Self::Short => "short".to_string(),
108			Self::Ushort => "ushort".to_string(),
109			Self::Int => "int".to_string(),
110			Self::Uint => "uint".to_string(),
111			Self::Long => "long".to_string(),
112			Self::Ulong => "ulong".to_string(),
113			Self::Float => "float".to_string(),
114			Self::Double => "double".to_string(),
115			Self::Bool => "bool".to_string(),
116			Self::Char => "char".to_string(),
117			Self::Str => "string".to_string(),
118			Self::Array(inner) => format!("{}[]", inner.csharp_type_name()),
119			Self::List(inner) => {
120				format!(
121					"System.Collections.Generic.List<{}>",
122					inner.csharp_type_name()
123				)
124			}
125			Self::Nullable(inner) => format!("{}?", inner.csharp_type_name()),
126		}
127	}
128
129	/// Returns true for C# value types (scalars).
130	/// Value-type nullables (`int?`, `bool?`, …) use `.HasValue`/`.Value`.
131	/// Reference-type nullables (`string?`, `T[]?`, `List<T>?`) use `!= null`.
132	fn is_value_type(&self) -> bool {
133		matches!(
134			self,
135			Self::Sbyte
136				| Self::Byte | Self::Short
137				| Self::Ushort
138				| Self::Int | Self::Uint
139				| Self::Long | Self::Ulong
140				| Self::Float
141				| Self::Double
142				| Self::Bool | Self::Char
143		)
144	}
145
146	/// Returns the Rust return type token stream for this C# type.
147	fn rust_return_type_ts(&self) -> proc_macro2::TokenStream {
148		match self {
149			Self::Sbyte => quote! { i8 },
150			Self::Byte => quote! { u8 },
151			Self::Short => quote! { i16 },
152			Self::Ushort => quote! { u16 },
153			Self::Int => quote! { i32 },
154			Self::Uint => quote! { u32 },
155			Self::Long => quote! { i64 },
156			Self::Ulong => quote! { u64 },
157			Self::Float => quote! { f32 },
158			Self::Double => quote! { f64 },
159			Self::Bool => quote! { bool },
160			Self::Char => quote! { char },
161			Self::Str => quote! { ::std::string::String },
162			Self::Array(inner) | Self::List(inner) => {
163				let inner_ts = inner.rust_return_type_ts();
164				quote! { ::std::vec::Vec<#inner_ts> }
165			}
166			Self::Nullable(inner) => {
167				let inner_ts = inner.rust_return_type_ts();
168				quote! { ::std::option::Option<#inner_ts> }
169			}
170		}
171	}
172
173	/// Returns the Rust parameter type token stream.
174	/// `Str` leaf → `&str`; `Array`/`List` → `&[T]` (slice reference).
175	fn rust_param_type_ts(&self) -> proc_macro2::TokenStream {
176		match self {
177			Self::Str => quote! { &str },
178			Self::Array(inner) | Self::List(inner) => {
179				let inner_ts = inner.rust_param_type_ts();
180				quote! { &[#inner_ts] }
181			}
182			Self::Nullable(inner) => {
183				let inner_ts = inner.rust_param_type_ts();
184				quote! { ::std::option::Option<#inner_ts> }
185			}
186			// All scalar types: same as return type
187			_ => self.rust_return_type_ts(),
188		}
189	}
190
191	/// Generates Rust code to serialize a parameter value into `_stdin_bytes`.
192	fn rust_ser_ts(
193		&self,
194		param_ident: &proc_macro2::TokenStream,
195		depth: usize,
196	) -> proc_macro2::TokenStream {
197		match self {
198			Self::Sbyte => quote! {
199				_stdin_bytes.extend_from_slice(&(#param_ident as i8).to_le_bytes());
200			},
201			Self::Byte | Self::Bool => quote! {
202				_stdin_bytes.push(#param_ident as u8);
203			},
204			Self::Short => quote! {
205				_stdin_bytes.extend_from_slice(&(#param_ident as i16).to_le_bytes());
206			},
207			Self::Ushort => quote! {
208				_stdin_bytes.extend_from_slice(&(#param_ident as u16).to_le_bytes());
209			},
210			Self::Int => quote! {
211				_stdin_bytes.extend_from_slice(&(#param_ident as i32).to_le_bytes());
212			},
213			Self::Uint => quote! {
214				_stdin_bytes.extend_from_slice(&(#param_ident as u32).to_le_bytes());
215			},
216			Self::Long => quote! {
217				_stdin_bytes.extend_from_slice(&(#param_ident as i64).to_le_bytes());
218			},
219			Self::Ulong => quote! {
220				_stdin_bytes.extend_from_slice(&(#param_ident as u64).to_le_bytes());
221			},
222			Self::Float => quote! {
223				_stdin_bytes.extend_from_slice(&(#param_ident as f32).to_bits().to_le_bytes());
224			},
225			Self::Double => quote! {
226				_stdin_bytes.extend_from_slice(&(#param_ident as f64).to_bits().to_le_bytes());
227			},
228			Self::Char => quote! {
229				{
230					let _c = #param_ident as u32;
231					assert!(_c <= 0xFFFF, "inline_csharp: char value exceeds u16 range");
232					_stdin_bytes.extend_from_slice(&(_c as u16).to_le_bytes());
233				}
234			},
235			Self::Str => quote! {
236				{
237					let _b = #param_ident.as_bytes();
238					let _len = _b.len() as u32;
239					_stdin_bytes.extend_from_slice(&_len.to_le_bytes());
240					_stdin_bytes.extend_from_slice(_b);
241				}
242			},
243			Self::Array(inner) | Self::List(inner) => {
244				let item_var = format_ident!("_item{}", depth);
245				let item_expr = quote! { #item_var };
246				let inner_ser = inner.rust_ser_ts(&item_expr, depth + 1);
247				quote! {
248					{
249						_stdin_bytes.extend_from_slice(&(#param_ident.len() as u32).to_le_bytes());
250						for &#item_var in #param_ident {
251							#inner_ser
252						}
253					}
254				}
255			}
256			Self::Nullable(inner) => {
257				let inner_var = format_ident!("_inner{}", depth);
258				let inner_expr = quote! { #inner_var };
259				let inner_ser = inner.rust_ser_ts(&inner_expr, depth + 1);
260				quote! {
261					match #param_ident {
262						::std::option::Option::None => _stdin_bytes.push(0u8),
263						::std::option::Option::Some(#inner_var) => {
264							_stdin_bytes.push(1u8);
265							#inner_ser
266						}
267					}
268				}
269			}
270		}
271	}
272
273	/// Returns a Rust expression that deserialises raw stdout bytes `_raw: Vec<u8>`
274	/// into the corresponding Rust type. Used by `csharp!` and `csharp_fn!` at runtime.
275	fn rust_deser(&self) -> proc_macro2::TokenStream {
276		match self {
277			Self::Str => {
278				// Top-level string: raw UTF-8, no length prefix
279				quote! { ::std::string::String::from_utf8(_raw)? }
280			}
281			Self::Sbyte => quote! { i8::from_le_bytes([_raw[0]]) },
282			Self::Byte => quote! { _raw[0] },
283			Self::Short => quote! { i16::from_le_bytes([_raw[0], _raw[1]]) },
284			Self::Ushort => quote! { u16::from_le_bytes([_raw[0], _raw[1]]) },
285			Self::Int => quote! { i32::from_le_bytes([_raw[0], _raw[1], _raw[2], _raw[3]]) },
286			Self::Uint => quote! { u32::from_le_bytes([_raw[0], _raw[1], _raw[2], _raw[3]]) },
287			Self::Long => {
288				quote! {
289					i64::from_le_bytes([
290						_raw[0], _raw[1], _raw[2], _raw[3],
291						_raw[4], _raw[5], _raw[6], _raw[7],
292					])
293				}
294			}
295			Self::Ulong => {
296				quote! {
297					u64::from_le_bytes([
298						_raw[0], _raw[1], _raw[2], _raw[3],
299						_raw[4], _raw[5], _raw[6], _raw[7],
300					])
301				}
302			}
303			Self::Float => {
304				quote! { f32::from_bits(u32::from_le_bytes([_raw[0], _raw[1], _raw[2], _raw[3]])) }
305			}
306			Self::Double => {
307				quote! {
308					f64::from_bits(u64::from_le_bytes([
309						_raw[0], _raw[1], _raw[2], _raw[3],
310						_raw[4], _raw[5], _raw[6], _raw[7],
311					]))
312				}
313			}
314			Self::Bool => quote! { _raw[0] != 0 },
315			Self::Char => {
316				quote! {
317					::std::char::from_u32(u16::from_le_bytes([_raw[0], _raw[1]]) as u32)
318						.ok_or(::inline_csharp::CsharpError::InvalidChar)?
319				}
320			}
321			_ => {
322				// Container types: set up shared _cur and call recursive reader
323				let rust_type = self.rust_return_type_ts();
324				let read_expr = rust_read_element(self, 0);
325				quote! {
326					{
327						let mut _cur = 0usize;
328						let _result: #rust_type = #read_expr;
329						_result
330					}
331				}
332			}
333		}
334	}
335
336	/// Converts raw stdout bytes produced by the generated `Main()` into a
337	/// Rust literal / expression token stream to splice at the `ct_csharp!` call site.
338	fn ct_csharp_tokens(&self, bytes: Vec<u8>) -> Result<proc_macro2::TokenStream, String> {
339		match self {
340			Self::Str => {
341				// Top-level string: raw UTF-8, no length prefix
342				let s = String::from_utf8(bytes)
343					.map_err(|_| "ct_csharp: C# string is not valid UTF-8".to_string())?;
344				let lit = format!("{s:?}");
345				proc_macro2::TokenStream::from_str(&lit)
346					.map_err(|e| format!("ct_csharp: produced invalid Rust token: {e}"))
347			}
348			Self::Sbyte
349			| Self::Byte
350			| Self::Short
351			| Self::Ushort
352			| Self::Int
353			| Self::Uint
354			| Self::Long
355			| Self::Ulong
356			| Self::Float
357			| Self::Double
358			| Self::Bool
359			| Self::Char => {
360				let (lit, _) = scalar_ct_lit(self, &bytes, 0)?;
361				proc_macro2::TokenStream::from_str(&lit)
362					.map_err(|e| format!("ct_csharp: produced invalid Rust token: {e}"))
363			}
364			_ => {
365				let mut cur = 0usize;
366				let ts = ct_csharp_tokens_recursive(self, &bytes, &mut cur)?;
367				Ok(ts)
368			}
369		}
370	}
371
372	/// Generates the complete `static void Main()` method that binary-serialises
373	/// `Run()`'s return value to stdout. `params` lists the parameters declared
374	/// in `Run(...)` so the generated `Main` can read them from stdin and forward
375	/// them to `Run`.
376	fn csharp_main(&self, params: &[(CsharpType, String)]) -> String {
377		let param_reads = if params.is_empty() {
378			String::new()
379		} else {
380			let mut s = String::from(
381				"\t\tSystem.IO.BinaryReader _br = new System.IO.BinaryReader(System.Console.OpenStandardInput());\n",
382			);
383			for (ty, name) in params {
384				writeln!(s, "\t\t{}", csharp_br_read(ty, name, 0)).unwrap();
385			}
386			s
387		};
388
389		let run_args: String = params
390			.iter()
391			.map(|(_, name)| name.as_str())
392			.collect::<Vec<_>>()
393			.join(", ");
394
395		let result_ty = self.csharp_type_name();
396		let serialize = csharp_bw_write(self, "_result", 0);
397
398		format!(
399			"\tstatic void Main() {{\n\
400			 {param_reads}\t\t{result_ty} _result = Run({run_args});\n\
401			 \t\t{serialize}\n\
402			 \t}}"
403		)
404	}
405}
406
407// ── C# BinaryReader param reading ────────────────────────────────────────────
408
409/// Generates C# statement(s) to read a parameter from `BinaryReader _br`.
410fn csharp_br_read(ty: &CsharpType, name: &str, depth: usize) -> String {
411	match ty {
412		CsharpType::Sbyte => format!("sbyte {name} = _br.ReadSByte();"),
413		CsharpType::Byte => format!("byte {name} = _br.ReadByte();"),
414		CsharpType::Short => format!("short {name} = _br.ReadInt16();"),
415		CsharpType::Ushort => format!("ushort {name} = _br.ReadUInt16();"),
416		CsharpType::Int => format!("int {name} = _br.ReadInt32();"),
417		CsharpType::Uint => format!("uint {name} = _br.ReadUInt32();"),
418		CsharpType::Long => format!("long {name} = _br.ReadInt64();"),
419		CsharpType::Ulong => format!("ulong {name} = _br.ReadUInt64();"),
420		CsharpType::Float => format!("float {name} = _br.ReadSingle();"),
421		CsharpType::Double => format!("double {name} = _br.ReadDouble();"),
422		CsharpType::Bool => format!("bool {name} = _br.ReadBoolean();"),
423		CsharpType::Char => format!("char {name} = (char)_br.ReadUInt16();"),
424		CsharpType::Str => {
425			format!(
426				"uint _len_{name} = _br.ReadUInt32();\n\
427				 \t\tbyte[] _b_{name} = _br.ReadBytes((int)_len_{name});\n\
428				 \t\tstring {name} = System.Text.Encoding.UTF8.GetString(_b_{name});"
429			)
430		}
431		CsharpType::Array(inner) => {
432			let count_var = format!("_count_{name}_{depth}");
433			let i_var = format!("_i_{name}_{depth}");
434			let elem_var = format!("_elem_{name}_{depth}");
435			let inner_cs_type = inner.csharp_type_name();
436			let inner_read = csharp_br_read(inner, &elem_var, depth + 1);
437			// For jagged arrays (e.g. inner_cs_type = "string[]"), C# requires
438			// `new string[count][]` not `new string[][count]`. Strip trailing []
439			// from inner_cs_type and re-append after the size bracket.
440			let mut base = inner_cs_type.as_str();
441			let mut trailing = "";
442			if base.ends_with("[]") {
443				let end = base.len()
444					- base
445						.chars()
446						.rev()
447						.take_while(|&c| c == '[' || c == ']')
448						.count();
449				trailing = &base[end..];
450				base = &base[..end];
451			}
452			format!(
453				"uint {count_var} = _br.ReadUInt32();\n\
454				 \t\t{inner_cs_type}[] {name} = new {base}[{count_var}]{trailing};\n\
455				 \t\tfor (int {i_var} = 0; {i_var} < {count_var}; {i_var}++) {{\n\
456				 \t\t\t{inner_read}\n\
457				 \t\t\t{name}[{i_var}] = {elem_var};\n\
458				 \t\t}}"
459			)
460		}
461		CsharpType::List(inner) => {
462			let count_var = format!("_count_{name}_{depth}");
463			let i_var = format!("_i_{name}_{depth}");
464			let elem_var = format!("_elem_{name}_{depth}");
465			let inner_cs_type = inner.csharp_type_name();
466			let inner_read = csharp_br_read(inner, &elem_var, depth + 1);
467			format!(
468				"uint {count_var} = _br.ReadUInt32();\n\
469				 \t\tSystem.Collections.Generic.List<{inner_cs_type}> {name} = new();\n\
470				 \t\tfor (int {i_var} = 0; {i_var} < {count_var}; {i_var}++) {{\n\
471				 \t\t\t{inner_read}\n\
472				 \t\t\t{name}.Add({elem_var});\n\
473				 \t\t}}"
474			)
475		}
476		CsharpType::Nullable(inner) => {
477			let tag_var = format!("_tag_{name}_{depth}");
478			let inner_var = format!("_inner_{name}_{depth}");
479			let inner_cs_type = inner.csharp_type_name();
480			let inner_read = csharp_br_read(inner, &inner_var, depth + 1);
481			format!(
482				"byte {tag_var} = _br.ReadByte();\n\
483				 \t\t{inner_cs_type}? {name};\n\
484				 \t\tif ({tag_var} != 0) {{\n\
485				 \t\t\t{inner_read}\n\
486				 \t\t\t{name} = {inner_var};\n\
487				 \t\t}} else {{\n\
488				 \t\t\t{name} = null;\n\
489				 \t\t}}"
490			)
491		}
492	}
493}
494
495// ── C# BinaryWriter result writing ───────────────────────────────────────────
496
497/// Generates C# statement(s) to write `var` of type `ty` to stdout.
498/// For the top-level `_result` this is called from `csharp_main`.
499fn csharp_bw_write(ty: &CsharpType, var: &str, _depth: usize) -> String {
500	match ty {
501		CsharpType::Str => {
502			// Top-level string: raw UTF-8, no length prefix
503			format!(
504				"byte[] _b = System.Text.Encoding.UTF8.GetBytes({var}); \
505				 System.Console.OpenStandardOutput().Write(_b, 0, _b.Length);"
506			)
507		}
508		CsharpType::Char => {
509			format!(
510				"System.IO.BinaryWriter _bw = new System.IO.BinaryWriter(System.Console.OpenStandardOutput());\n\
511				 \t\t_bw.Write((ushort){var});\n\
512				 \t\t_bw.Flush();"
513			)
514		}
515		CsharpType::Array(inner) => {
516			let inner_cs_type = inner.csharp_type_name();
517			let ser_body = csharp_ser_element(inner, "_e0", "_bw", 1);
518			format!(
519				"System.IO.BinaryWriter _bw = new System.IO.BinaryWriter(System.Console.OpenStandardOutput());\n\
520				 \t\t_bw.Write((uint){var}.Length);\n\
521				 \t\tforeach ({inner_cs_type} _e0 in {var}) {{\n\
522				 \t\t\t{ser_body}\n\
523				 \t\t}}\n\
524				 \t\t_bw.Flush();"
525			)
526		}
527		CsharpType::List(inner) => {
528			let inner_cs_type = inner.csharp_type_name();
529			let ser_body = csharp_ser_element(inner, "_e0", "_bw", 1);
530			format!(
531				"System.IO.BinaryWriter _bw = new System.IO.BinaryWriter(System.Console.OpenStandardOutput());\n\
532				 \t\t_bw.Write((uint){var}.Count);\n\
533				 \t\tforeach ({inner_cs_type} _e0 in {var}) {{\n\
534				 \t\t\t{ser_body}\n\
535				 \t\t}}\n\
536				 \t\t_bw.Flush();"
537			)
538		}
539		CsharpType::Nullable(inner) => {
540			let inner_cs_type = inner.csharp_type_name();
541			if inner.is_value_type() {
542				// Nullable<T> value type: .HasValue / .Value
543				let ser_body = csharp_ser_element(inner, &format!("{var}.Value"), "_bw", 1);
544				format!(
545					"System.IO.BinaryWriter _bw = new System.IO.BinaryWriter(System.Console.OpenStandardOutput());\n\
546					 \t\tif ({var}.HasValue) {{\n\
547					 \t\t\t_bw.Write((byte)1);\n\
548					 \t\t\t{inner_cs_type} _opt_val = {var}.Value;\n\
549					 \t\t\t{ser_body}\n\
550					 \t\t}} else {{\n\
551					 \t\t\t_bw.Write((byte)0);\n\
552					 \t\t}}\n\
553					 \t\t_bw.Flush();"
554				)
555			} else {
556				// Nullable reference type (string?, T[]?, List<T>?): != null check
557				let ser_body = csharp_ser_element(inner, "_opt_val", "_bw", 1);
558				format!(
559					"System.IO.BinaryWriter _bw = new System.IO.BinaryWriter(System.Console.OpenStandardOutput());\n\
560					 \t\tif ({var} != null) {{\n\
561					 \t\t\t_bw.Write((byte)1);\n\
562					 \t\t\t{inner_cs_type} _opt_val = {var};\n\
563					 \t\t\t{ser_body}\n\
564					 \t\t}} else {{\n\
565					 \t\t\t_bw.Write((byte)0);\n\
566					 \t\t}}\n\
567					 \t\t_bw.Flush();"
568				)
569			}
570		}
571		// All other scalars: BinaryWriter.Write with cast
572		_ => {
573			let cs_type = ty.csharp_type_name();
574			format!(
575				"System.IO.BinaryWriter _bw = new System.IO.BinaryWriter(System.Console.OpenStandardOutput());\n\
576				 \t\t_bw.Write(({cs_type}){var});\n\
577				 \t\t_bw.Flush();"
578			)
579		}
580	}
581}
582
583/// Generates C# code to serialize `var` of type `ty` to `BinaryWriter` named `bw_name`.
584/// Used for element serialization inside containers.
585fn csharp_ser_element(ty: &CsharpType, var: &str, bw_name: &str, depth: usize) -> String {
586	match ty {
587		CsharpType::Char => {
588			format!("{bw_name}.Write((ushort){var});")
589		}
590		CsharpType::Str => {
591			format!(
592				"{{ byte[] _b{depth} = System.Text.Encoding.UTF8.GetBytes({var}); \
593				 {bw_name}.Write((uint)_b{depth}.Length); \
594				 {bw_name}.Write(_b{depth}); }}"
595			)
596		}
597		CsharpType::Array(inner) => {
598			let inner_cs_type = inner.csharp_type_name();
599			let elem_var = format!("_e{depth}");
600			let inner_ser = csharp_ser_element(inner, &elem_var, bw_name, depth + 1);
601			format!(
602				"{bw_name}.Write((uint)({var}).Length);\n\
603				 \t\t\tforeach ({inner_cs_type} {elem_var} in ({var})) {{\n\
604				 \t\t\t\t{inner_ser}\n\
605				 \t\t\t}}"
606			)
607		}
608		CsharpType::List(inner) => {
609			let inner_cs_type = inner.csharp_type_name();
610			let elem_var = format!("_e{depth}");
611			let inner_ser = csharp_ser_element(inner, &elem_var, bw_name, depth + 1);
612			format!(
613				"{bw_name}.Write((uint)({var}).Count);\n\
614				 \t\t\tforeach ({inner_cs_type} {elem_var} in ({var})) {{\n\
615				 \t\t\t\t{inner_ser}\n\
616				 \t\t\t}}"
617			)
618		}
619		CsharpType::Nullable(inner) => {
620			let inner_cs_type = inner.csharp_type_name();
621			let opt_inner_var = format!("_opt_inner{depth}");
622			let inner_ser = csharp_ser_element(inner, &opt_inner_var, bw_name, depth + 1);
623			if inner.is_value_type() {
624				format!(
625					"if (({var}).HasValue) {{\n\
626					 \t\t\t\t{bw_name}.Write((byte)1);\n\
627					 \t\t\t\t{inner_cs_type} {opt_inner_var} = ({var}).Value;\n\
628					 \t\t\t\t{inner_ser}\n\
629					 \t\t\t}} else {{\n\
630					 \t\t\t\t{bw_name}.Write((byte)0);\n\
631					 \t\t\t}}"
632				)
633			} else {
634				format!(
635					"if (({var}) != null) {{\n\
636					 \t\t\t\t{bw_name}.Write((byte)1);\n\
637					 \t\t\t\t{inner_cs_type} {opt_inner_var} = {var};\n\
638					 \t\t\t\t{inner_ser}\n\
639					 \t\t\t}} else {{\n\
640					 \t\t\t\t{bw_name}.Write((byte)0);\n\
641					 \t\t\t}}"
642				)
643			}
644		}
645		// Non-string scalars
646		_ => {
647			let cs_type = ty.csharp_type_name();
648			format!("{bw_name}.Write(({cs_type}){var});")
649		}
650	}
651}
652
653// ── Recursive Rust deserialization helper ─────────────────────────────────────
654
655/// Generates a Rust expression block that reads one value of type `ty` from `_raw`
656/// using the shared mutable cursor `_cur`. All levels share the same `_cur` and `_raw`.
657#[allow(clippy::too_many_lines)]
658fn rust_read_element(ty: &CsharpType, depth: usize) -> proc_macro2::TokenStream {
659	match ty {
660		CsharpType::Sbyte => quote! {{
661			let _val = i8::from_le_bytes([_raw[_cur]]);
662			_cur += 1;
663			_val
664		}},
665		CsharpType::Byte => quote! {{
666			let _val = _raw[_cur];
667			_cur += 1;
668			_val
669		}},
670		CsharpType::Short => quote! {{
671			let _val = i16::from_le_bytes([_raw[_cur], _raw[_cur + 1]]);
672			_cur += 2;
673			_val
674		}},
675		CsharpType::Ushort => quote! {{
676			let _val = u16::from_le_bytes([_raw[_cur], _raw[_cur + 1]]);
677			_cur += 2;
678			_val
679		}},
680		CsharpType::Int => quote! {{
681			let _val = i32::from_le_bytes([_raw[_cur], _raw[_cur + 1], _raw[_cur + 2], _raw[_cur + 3]]);
682			_cur += 4;
683			_val
684		}},
685		CsharpType::Uint => quote! {{
686			let _val = u32::from_le_bytes([_raw[_cur], _raw[_cur + 1], _raw[_cur + 2], _raw[_cur + 3]]);
687			_cur += 4;
688			_val
689		}},
690		CsharpType::Long => quote! {{
691			let _val = i64::from_le_bytes([
692				_raw[_cur], _raw[_cur + 1], _raw[_cur + 2], _raw[_cur + 3],
693				_raw[_cur + 4], _raw[_cur + 5], _raw[_cur + 6], _raw[_cur + 7],
694			]);
695			_cur += 8;
696			_val
697		}},
698		CsharpType::Ulong => quote! {{
699			let _val = u64::from_le_bytes([
700				_raw[_cur], _raw[_cur + 1], _raw[_cur + 2], _raw[_cur + 3],
701				_raw[_cur + 4], _raw[_cur + 5], _raw[_cur + 6], _raw[_cur + 7],
702			]);
703			_cur += 8;
704			_val
705		}},
706		CsharpType::Float => quote! {{
707			let _val = f32::from_bits(u32::from_le_bytes([_raw[_cur], _raw[_cur + 1], _raw[_cur + 2], _raw[_cur + 3]]));
708			_cur += 4;
709			_val
710		}},
711		CsharpType::Double => quote! {{
712			let _val = f64::from_bits(u64::from_le_bytes([
713				_raw[_cur], _raw[_cur + 1], _raw[_cur + 2], _raw[_cur + 3],
714				_raw[_cur + 4], _raw[_cur + 5], _raw[_cur + 6], _raw[_cur + 7],
715			]));
716			_cur += 8;
717			_val
718		}},
719		CsharpType::Bool => quote! {{
720			let _val = _raw[_cur] != 0;
721			_cur += 1;
722			_val
723		}},
724		CsharpType::Char => quote! {{
725			let _val = ::std::char::from_u32(u16::from_le_bytes([_raw[_cur], _raw[_cur + 1]]) as u32)
726				.ok_or(::inline_csharp::CsharpError::InvalidChar)?;
727			_cur += 2;
728			_val
729		}},
730		// String inside container: u32 length prefix + UTF-8 bytes
731		CsharpType::Str => quote! {{
732			let _slen = u32::from_le_bytes([_raw[_cur], _raw[_cur + 1], _raw[_cur + 2], _raw[_cur + 3]]) as usize;
733			_cur += 4;
734			let _val = ::std::string::String::from_utf8(_raw[_cur.._cur + _slen].to_vec())?;
735			_cur += _slen;
736			_val
737		}},
738		CsharpType::Array(inner) | CsharpType::List(inner) => {
739			let n_var = format_ident!("_n{}", depth);
740			let v_var = format_ident!("_v{}", depth);
741			let inner_rust_type = inner.rust_return_type_ts();
742			let inner_read = rust_read_element(inner, depth + 1);
743			quote! {{
744				let #n_var = u32::from_le_bytes([_raw[_cur], _raw[_cur + 1], _raw[_cur + 2], _raw[_cur + 3]]) as usize;
745				_cur += 4;
746				let mut #v_var: ::std::vec::Vec<#inner_rust_type> = ::std::vec::Vec::with_capacity(#n_var);
747				for _ in 0..#n_var {
748					let _item = #inner_read;
749					#v_var.push(_item);
750				}
751				#v_var
752			}}
753		}
754		CsharpType::Nullable(inner) => {
755			let inner_rust_type = inner.rust_return_type_ts();
756			let inner_read = rust_read_element(inner, depth + 1);
757			quote! {{
758				let _tag = _raw[_cur];
759				_cur += 1;
760				if _tag == 0 {
761					::std::option::Option::None::<#inner_rust_type>
762				} else {
763					::std::option::Option::Some(#inner_read)
764				}
765			}}
766		}
767	}
768}
769
770// ── Compile-time literal generation ──────────────────────────────────────────
771
772/// Deserialise one scalar element from `bytes[offset..]` and return a
773/// `(rust_literal_string, bytes_consumed)` pair for `ct_csharp_tokens`.
774#[allow(clippy::too_many_lines)]
775fn scalar_ct_lit(ty: &CsharpType, bytes: &[u8], offset: usize) -> Result<(String, usize), String> {
776	let b = &bytes[offset..];
777	match ty {
778		CsharpType::Sbyte => {
779			if b.is_empty() {
780				return Err("ct_csharp: truncated sbyte element".to_string());
781			}
782			Ok((format!("{}", i8::from_le_bytes([b[0]])), 1))
783		}
784		CsharpType::Byte => {
785			if b.is_empty() {
786				return Err("ct_csharp: truncated byte element".to_string());
787			}
788			Ok((format!("{}", b[0]), 1))
789		}
790		CsharpType::Short => {
791			if b.len() < 2 {
792				return Err("ct_csharp: truncated short element".to_string());
793			}
794			Ok((format!("{}", i16::from_le_bytes([b[0], b[1]])), 2))
795		}
796		CsharpType::Ushort => {
797			if b.len() < 2 {
798				return Err("ct_csharp: truncated ushort element".to_string());
799			}
800			Ok((format!("{}", u16::from_le_bytes([b[0], b[1]])), 2))
801		}
802		CsharpType::Int => {
803			let arr: [u8; 4] = b[..4]
804				.try_into()
805				.map_err(|_| "ct_csharp: truncated int element")?;
806			Ok((format!("{}", i32::from_le_bytes(arr)), 4))
807		}
808		CsharpType::Uint => {
809			let arr: [u8; 4] = b[..4]
810				.try_into()
811				.map_err(|_| "ct_csharp: truncated uint element")?;
812			Ok((format!("{}", u32::from_le_bytes(arr)), 4))
813		}
814		CsharpType::Long => {
815			let arr: [u8; 8] = b[..8]
816				.try_into()
817				.map_err(|_| "ct_csharp: truncated long element")?;
818			Ok((format!("{}", i64::from_le_bytes(arr)), 8))
819		}
820		CsharpType::Ulong => {
821			let arr: [u8; 8] = b[..8]
822				.try_into()
823				.map_err(|_| "ct_csharp: truncated ulong element")?;
824			Ok((format!("{}", u64::from_le_bytes(arr)), 8))
825		}
826		CsharpType::Float => {
827			let arr: [u8; 4] = b[..4]
828				.try_into()
829				.map_err(|_| "ct_csharp: truncated float element")?;
830			let bits = u32::from_le_bytes(arr);
831			Ok((format!("f32::from_bits(0x{bits:08x}_u32)"), 4))
832		}
833		CsharpType::Double => {
834			let arr: [u8; 8] = b[..8]
835				.try_into()
836				.map_err(|_| "ct_csharp: truncated double element")?;
837			let bits = u64::from_le_bytes(arr);
838			Ok((format!("f64::from_bits(0x{bits:016x}_u64)"), 8))
839		}
840		CsharpType::Bool => {
841			if b.is_empty() {
842				return Err("ct_csharp: truncated bool element".to_string());
843			}
844			Ok((
845				if b[0] != 0 {
846					"true".to_string()
847				} else {
848					"false".to_string()
849				},
850				1,
851			))
852		}
853		CsharpType::Char => {
854			if b.len() < 2 {
855				return Err("ct_csharp: truncated char element".to_string());
856			}
857			let code_unit = u16::from_le_bytes([b[0], b[1]]);
858			let c = char::from_u32(u32::from(code_unit))
859				.ok_or("ct_csharp: C# char is not a valid Unicode scalar value")?;
860			Ok((format!("{c:?}"), 2))
861		}
862		CsharpType::Str => {
863			// String inside container: u32 length prefix
864			if b.len() < 4 {
865				return Err("ct_csharp: truncated String length prefix".to_string());
866			}
867			let len = u32::from_le_bytes(b[..4].try_into().unwrap()) as usize;
868			if b.len() < 4 + len {
869				return Err(format!(
870					"ct_csharp: truncated String element (expected {len} bytes)"
871				));
872			}
873			let s = String::from_utf8(b[4..4 + len].to_vec())
874				.map_err(|_| "ct_csharp: String element is not valid UTF-8".to_string())?;
875			Ok((format!("{s:?}"), 4 + len))
876		}
877		_ => Err("ct_csharp: scalar_ct_lit called on non-scalar type".to_string()),
878	}
879}
880
881/// Recursively decode one value of `ty` from `bytes[*cur..]`, advance `*cur`,
882/// and return a Rust literal/expression token stream.
883fn ct_csharp_tokens_recursive(
884	ty: &CsharpType,
885	bytes: &[u8],
886	cur: &mut usize,
887) -> Result<proc_macro2::TokenStream, String> {
888	match ty {
889		CsharpType::Array(inner) | CsharpType::List(inner) => {
890			if bytes[*cur..].len() < 4 {
891				return Err("ct_csharp: array/list output too short (missing length)".to_string());
892			}
893			let n = u32::from_le_bytes(bytes[*cur..*cur + 4].try_into().unwrap()) as usize;
894			*cur += 4;
895			let mut lits: Vec<proc_macro2::TokenStream> = Vec::with_capacity(n);
896			for _ in 0..n {
897				lits.push(ct_csharp_tokens_recursive(inner, bytes, cur)?);
898			}
899			Ok(quote! { [#(#lits),*] })
900		}
901		CsharpType::Nullable(inner) => {
902			if bytes[*cur..].is_empty() {
903				return Err("ct_csharp: nullable output is empty".to_string());
904			}
905			let tag = bytes[*cur];
906			*cur += 1;
907			if tag == 0 {
908				proc_macro2::TokenStream::from_str("::std::option::Option::None")
909					.map_err(|e| format!("ct_csharp: produced invalid Rust token: {e}"))
910			} else {
911				let inner_ts = ct_csharp_tokens_recursive(inner, bytes, cur)?;
912				Ok(quote! { ::std::option::Option::Some(#inner_ts) })
913			}
914		}
915		CsharpType::Str => {
916			// String inside container: u32 length prefix
917			let (lit, consumed) = scalar_ct_lit(ty, bytes, *cur)?;
918			*cur += consumed;
919			proc_macro2::TokenStream::from_str(&lit)
920				.map_err(|e| format!("ct_csharp: produced invalid Rust token: {e}"))
921		}
922		_ => {
923			// Scalar types
924			let (lit, consumed) = scalar_ct_lit(ty, bytes, *cur)?;
925			*cur += consumed;
926			proc_macro2::TokenStream::from_str(&lit)
927				.map_err(|e| format!("ct_csharp: produced invalid Rust token: {e}"))
928		}
929	}
930}
931
932// ── ParsedCsharp + source parser ──────────────────────────────────────────────
933
934/// Output of the unified C# source parser.
935struct ParsedCsharp {
936	/// The `using` directives verbatim from the original source.
937	usings: String,
938	/// The `namespace` declaration (e.g. `"namespace MyNs;"`) or empty string.
939	namespace_decl: String,
940	/// Any class/interface/enum declarations written before `Run()`.
941	outer: String,
942	/// The `Run()` method and everything after it, verbatim from the original source.
943	body: String,
944	/// Parameters declared in `Run(...)`, in order.
945	params: Vec<(CsharpType, String)>,
946	/// Return type of the `static T Run(...)` method.
947	csharp_type: CsharpType,
948}
949
950/// Recursively parse a `CsharpType` from `tts` starting at index 0.
951/// Returns `(csharp_type, tokens_consumed)` on success.
952///
953/// Recognises:
954/// - Scalar: `T` where T is a C# type name
955/// - Array: `T[]`, `T[][]`, … (Ident + one or more empty Bracket groups)
956/// - List: `List<T>`
957/// - Nullable suffix: `T?` wraps the whole type in `Nullable`
958fn parse_csharp_type(tts: &[TokenTree]) -> Result<(CsharpType, usize), String> {
959	if tts.is_empty() {
960		return Err("inline_csharp: unexpected end of tokens while parsing C# type".to_string());
961	}
962
963	match tts.first() {
964		Some(TokenTree::Ident(id)) => {
965			let name = id.to_string();
966			let (base_ty, consumed) = if name == "List" {
967				// Expect `<` inner_type `>`
968				if !matches!(tts.get(1), Some(TokenTree::Punct(p)) if p.as_char() == '<') {
969					return Err("inline_csharp: expected `<` after `List`".to_string());
970				}
971				let (inner_ty, inner_consumed) = parse_csharp_type_inner(&tts[2..])?;
972				let close_idx = 2 + inner_consumed;
973				if !matches!(tts.get(close_idx), Some(TokenTree::Punct(p)) if p.as_char() == '>') {
974					return Err("inline_csharp: expected `>` to close `List<...>`".to_string());
975				}
976				(CsharpType::List(Box::new(inner_ty)), close_idx + 1)
977			} else if let Some(scalar) = CsharpType::from_name(&name) {
978				(scalar, 1)
979			} else {
980				return Err(format!(
981					"inline_csharp: `{name}` is not a supported C# type; \
982					 scalar types: sbyte byte short ushort int uint long ulong float double bool char string"
983				));
984			};
985
986			// Consume trailing `[]` bracket groups, each wraps in Array.
987			let mut ty = base_ty;
988			let mut total_consumed = consumed;
989			while matches!(
990				tts.get(total_consumed),
991				Some(TokenTree::Group(g))
992					if g.delimiter() == proc_macro2::Delimiter::Bracket
993					   && g.stream().is_empty()
994			) {
995				ty = CsharpType::Array(Box::new(ty));
996				total_consumed += 1;
997			}
998
999			// Consume optional `?` suffix — wraps the outermost type in Nullable.
1000			if matches!(tts.get(total_consumed), Some(TokenTree::Punct(p)) if p.as_char() == '?') {
1001				ty = CsharpType::Nullable(Box::new(ty));
1002				total_consumed += 1;
1003			}
1004
1005			Ok((ty, total_consumed))
1006		}
1007		_ => Err("inline_csharp: expected a C# type name".to_string()),
1008	}
1009}
1010
1011/// Like `parse_csharp_type` but for use inside `<>` generics.
1012fn parse_csharp_type_inner(tts: &[TokenTree]) -> Result<(CsharpType, usize), String> {
1013	if tts.is_empty() {
1014		return Err(
1015			"inline_csharp: unexpected end of tokens while parsing C# type argument".to_string(),
1016		);
1017	}
1018
1019	match tts.first() {
1020		Some(TokenTree::Ident(id)) => {
1021			let name = id.to_string();
1022			let (base_ty, consumed) = if name == "List" {
1023				if !matches!(tts.get(1), Some(TokenTree::Punct(p)) if p.as_char() == '<') {
1024					return Err("inline_csharp: expected `<` after `List`".to_string());
1025				}
1026				let (inner_ty, inner_consumed) = parse_csharp_type_inner(&tts[2..])?;
1027				let close_idx = 2 + inner_consumed;
1028				if !matches!(tts.get(close_idx), Some(TokenTree::Punct(p)) if p.as_char() == '>') {
1029					return Err("inline_csharp: expected `>` to close `List<...>`".to_string());
1030				}
1031				(CsharpType::List(Box::new(inner_ty)), close_idx + 1)
1032			} else if let Some(scalar) = CsharpType::from_name(&name) {
1033				(scalar, 1)
1034			} else {
1035				return Err(format!(
1036					"inline_csharp: `{name}` is not a supported C# type argument; \
1037					 supported: sbyte byte short ushort int uint long ulong float double bool char string"
1038				));
1039			};
1040
1041			// Consume trailing `[]` bracket groups.
1042			let mut ty = base_ty;
1043			let mut total_consumed = consumed;
1044			while matches!(
1045				tts.get(total_consumed),
1046				Some(TokenTree::Group(g))
1047					if g.delimiter() == proc_macro2::Delimiter::Bracket
1048					   && g.stream().is_empty()
1049			) {
1050				ty = CsharpType::Array(Box::new(ty));
1051				total_consumed += 1;
1052			}
1053
1054			// Consume optional `?` suffix.
1055			if matches!(tts.get(total_consumed), Some(TokenTree::Punct(p)) if p.as_char() == '?') {
1056				ty = CsharpType::Nullable(Box::new(ty));
1057				total_consumed += 1;
1058			}
1059
1060			Ok((ty, total_consumed))
1061		}
1062		_ => Err("inline_csharp: expected a C# type name inside `<>`".to_string()),
1063	}
1064}
1065
1066/// Scan `tts` for the first `[visibility] static <T> Run` pattern and return the
1067/// corresponding `CsharpType` together with the index of the method declaration
1068/// start and the index of the `Run` identifier token.
1069///
1070/// Returns `(csharp_type, method_start_idx, run_idx)`.
1071fn parse_run_return_type(tts: &[TokenTree]) -> Result<(CsharpType, usize, usize), String> {
1072	for i in 0..tts.len().saturating_sub(2) {
1073		if !matches!(&tts[i], TokenTree::Ident(id) if id == "static") {
1074			continue;
1075		}
1076
1077		// Include an optional preceding visibility modifier in the returned start index.
1078		let start = if i > 0
1079			&& matches!(&tts[i - 1], TokenTree::Ident(id)
1080				if matches!(id.to_string().as_str(), "public" | "private" | "protected"))
1081		{
1082			i - 1
1083		} else {
1084			i
1085		};
1086
1087		let type_start = i + 1;
1088		if type_start >= tts.len() {
1089			continue;
1090		}
1091
1092		if let Ok((csharp_type, consumed)) = parse_csharp_type(&tts[type_start..]) {
1093			let run_idx = type_start + consumed;
1094			if matches!(tts.get(run_idx), Some(TokenTree::Ident(id)) if id == "Run") {
1095				return Ok((csharp_type, start, run_idx));
1096			}
1097		}
1098	}
1099	Err("inline_csharp: could not find `static <type> Run()` in C# body".to_string())
1100}
1101
1102/// Parse the parameter list from the `Group(Parenthesis)` token immediately
1103/// after the `Run` identifier. Returns `Vec<(CsharpType, param_name)>`.
1104fn parse_run_params(tts: &[TokenTree]) -> Result<Vec<(CsharpType, String)>, String> {
1105	let group = match tts.first() {
1106		Some(TokenTree::Group(g)) if g.delimiter() == proc_macro2::Delimiter::Parenthesis => g,
1107		_ => return Ok(vec![]),
1108	};
1109
1110	let inner: Vec<TokenTree> = group.stream().into_iter().collect();
1111	if inner.is_empty() {
1112		return Ok(vec![]);
1113	}
1114
1115	let mut params = Vec::new();
1116	let mut segments: Vec<Vec<TokenTree>> = Vec::new();
1117	let mut current: Vec<TokenTree> = Vec::new();
1118	let mut angle_depth = 0i32;
1119	for tt in inner {
1120		if matches!(&tt, TokenTree::Punct(p) if p.as_char() == '<') {
1121			angle_depth += 1;
1122			current.push(tt);
1123		} else if matches!(&tt, TokenTree::Punct(p) if p.as_char() == '>') {
1124			angle_depth -= 1;
1125			current.push(tt);
1126		} else if matches!(&tt, TokenTree::Punct(p) if p.as_char() == ',') && angle_depth == 0 {
1127			segments.push(std::mem::take(&mut current));
1128		} else {
1129			current.push(tt);
1130		}
1131	}
1132	if !current.is_empty() {
1133		segments.push(current);
1134	}
1135
1136	for seg in segments {
1137		if seg.is_empty() {
1138			continue;
1139		}
1140
1141		let param_name = match seg.last() {
1142			Some(TokenTree::Ident(id)) => id.to_string(),
1143			_ => {
1144				return Err(
1145					"inline_csharp: unexpected token in Run() parameter list: expected a parameter name"
1146						.to_string(),
1147				);
1148			}
1149		};
1150
1151		let type_tts = &seg[..seg.len() - 1];
1152		if type_tts.is_empty() {
1153			return Err(format!(
1154				"inline_csharp: missing type for parameter `{param_name}`"
1155			));
1156		}
1157
1158		let (csharp_type, consumed) = parse_csharp_type(type_tts).map_err(|e| {
1159			format!("inline_csharp: error parsing type of parameter `{param_name}`: {e}")
1160		})?;
1161
1162		if consumed != type_tts.len() {
1163			return Err(format!(
1164				"inline_csharp: unexpected tokens after type of parameter `{param_name}`"
1165			));
1166		}
1167
1168		params.push((csharp_type, param_name));
1169	}
1170
1171	Ok(params)
1172}
1173
1174/// Unified parser: walks the token stream once to separate `using` directives
1175/// from the method body, identify the `Run()` return type and parameters.
1176fn parse_csharp_source(stream: proc_macro2::TokenStream) -> Result<ParsedCsharp, String> {
1177	let tts: Vec<TokenTree> = stream.into_iter().collect();
1178
1179	// Separate usings from body: collect leading `using ... ;` sequences.
1180	// Skip `using static` and `using var` (treated as non-using).
1181	let mut first_using_idx: Option<usize> = None;
1182	let mut last_using_end_idx: Option<usize> = None;
1183	let mut first_body_idx: Option<usize> = None;
1184	let mut in_usings = true;
1185	let mut i = 0usize;
1186
1187	while i < tts.len() && in_usings {
1188		match &tts[i] {
1189			TokenTree::Ident(id) if id == "using" => {
1190				// Check for `using static` or `using var` — not a namespace using
1191				let is_namespace_using = !matches!(tts.get(i + 1), Some(TokenTree::Ident(next))
1192						if next == "static" || next == "var");
1193				if is_namespace_using {
1194					first_using_idx.get_or_insert(i);
1195					// Scan forward for the terminating ';'.
1196					let semi = tts[i + 1..]
1197						.iter()
1198						.position(|t| matches!(t, TokenTree::Punct(p) if p.as_char() == ';'))
1199						.map(|rel| i + 1 + rel);
1200					if let Some(semi_idx) = semi {
1201						last_using_end_idx = Some(semi_idx);
1202						i = semi_idx + 1;
1203					} else {
1204						in_usings = false;
1205						first_body_idx = Some(i);
1206					}
1207				} else {
1208					in_usings = false;
1209					first_body_idx = Some(i);
1210				}
1211			}
1212			_ => {
1213				in_usings = false;
1214				first_body_idx = Some(i);
1215			}
1216		}
1217	}
1218	if first_body_idx.is_none() && i < tts.len() {
1219		first_body_idx = Some(i);
1220	}
1221	let body_start = first_body_idx.unwrap_or(tts.len());
1222
1223	// Parse return type and Run index from body tokens.
1224	let (csharp_type, run_rel_idx, run_rel_run_idx) = parse_run_return_type(&tts[body_start..])?;
1225	let run_abs_idx = body_start + run_rel_idx;
1226	let run_token_abs_idx = body_start + run_rel_run_idx;
1227
1228	// Parse Run() parameters.
1229	let params = parse_run_params(&tts[run_token_abs_idx + 1..])?;
1230
1231	// Helper: get source text for a contiguous slice of tts, with fallback.
1232	let slice_text = |lo: usize, hi: usize| -> String {
1233		if lo >= hi {
1234			return String::new();
1235		}
1236		tts[lo]
1237			.span()
1238			.join(tts[hi - 1].span())
1239			.and_then(|s| s.source_text())
1240			.unwrap_or_else(|| {
1241				tts[lo..hi]
1242					.iter()
1243					.map(std::string::ToString::to_string)
1244					.collect::<Vec<_>>()
1245					.join(" ")
1246			})
1247	};
1248
1249	// usings: span from first using keyword to last ';'
1250	let usings = match (first_using_idx, last_using_end_idx) {
1251		(Some(fi), Some(le)) => slice_text(fi, le + 1),
1252		_ => String::new(),
1253	};
1254
1255	// outer: any tokens between end of usings and the `Run` method declaration
1256	let outer = slice_text(body_start, run_abs_idx);
1257
1258	// body: from the `Run` method declaration to end (verbatim).
1259	let body = if run_abs_idx < tts.len() {
1260		let start_span = tts[run_abs_idx].span();
1261		let end_span = tts.last().unwrap().span();
1262		match start_span.join(end_span).and_then(|s| s.source_text()) {
1263			Some(raw) => raw,
1264			None => tts[run_abs_idx..]
1265				.iter()
1266				.map(std::string::ToString::to_string)
1267				.collect::<Vec<_>>()
1268				.join(" "),
1269		}
1270	} else {
1271		String::new()
1272	};
1273
1274	// Extract namespace declaration from outer section (substring search),
1275	// then strip it from `outer` so it doesn't appear twice in the generated file.
1276	// We strip using the raw indices from `outer` rather than re-searching for the
1277	// normalised `namespace_decl` string, because the fallback token serialisation
1278	// may insert spaces before ';' (e.g. "namespace MyNamespace ;") which would
1279	// prevent an exact string match.
1280	let (namespace_decl, outer) = strip_namespace_decl(outer);
1281
1282	Ok(ParsedCsharp {
1283		usings,
1284		namespace_decl,
1285		outer,
1286		body,
1287		params,
1288		csharp_type,
1289	})
1290}
1291
1292// ── Option extraction ─────────────────────────────────────────────────────────
1293
1294#[derive(Default)]
1295struct DotnetOpts {
1296	build_args: String,
1297	run_args: String,
1298	references: Vec<String>,
1299}
1300
1301/// Consume leading `build = "…"` / `run = "…"` / `reference = "…"` option pairs
1302/// (comma-separated, trailing comma optional) and return the remaining token stream.
1303fn extract_opts(input: proc_macro2::TokenStream) -> (DotnetOpts, proc_macro2::TokenStream) {
1304	let mut tts: Vec<TokenTree> = input.into_iter().collect();
1305	let mut opts = DotnetOpts::default();
1306	let mut cursor = 0;
1307
1308	loop {
1309		match try_parse_opt(&tts[cursor..]) {
1310			None => break,
1311			Some((key, val, consumed)) => {
1312				match key.as_str() {
1313					"build" => opts.build_args = val,
1314					"run" => opts.run_args = val,
1315					"reference" => opts.references.push(val),
1316					_ => break,
1317				}
1318				cursor += consumed;
1319				if let Some(TokenTree::Punct(p)) = tts.get(cursor)
1320					&& p.as_char() == ','
1321				{
1322					cursor += 1;
1323				}
1324			}
1325		}
1326	}
1327
1328	let rest = tts.drain(cursor..).collect();
1329	(opts, rest)
1330}
1331
1332/// Try to parse `Ident("build"|"run"|"reference") Punct("=") Literal(string)` at the
1333/// start of `tts`. Returns `(key, unquoted_value, tokens_consumed)` or `None`.
1334fn try_parse_opt(tts: &[TokenTree]) -> Option<(String, String, usize)> {
1335	let key = match tts.first() {
1336		Some(TokenTree::Ident(id)) => id.to_string(),
1337		_ => return None,
1338	};
1339	let Some(TokenTree::Punct(eq)) = tts.get(1) else {
1340		return None;
1341	};
1342	if eq.as_char() != '=' {
1343		return None;
1344	}
1345	let Some(TokenTree::Literal(lit)) = tts.get(2) else {
1346		return None;
1347	};
1348	let value = litrs::StringLit::try_from(lit).ok()?.value().to_owned();
1349	Some((key, value, 3))
1350}
1351
1352// ── Shared helpers ────────────────────────────────────────────────────────────
1353
1354/// Compute a deterministic class name by hashing the source and options.
1355fn make_class_name(
1356	prefix: &str,
1357	usings: &str,
1358	outer: &str,
1359	body: &str,
1360	opts: &DotnetOpts,
1361) -> String {
1362	let mut h = DefaultHasher::new();
1363	usings.hash(&mut h);
1364	outer.hash(&mut h);
1365	body.hash(&mut h);
1366	opts.build_args.hash(&mut h);
1367	opts.run_args.hash(&mut h);
1368	opts.references.hash(&mut h);
1369	format!("{prefix}_{:016x}", h.finish())
1370}
1371
1372/// Extract and strip the namespace declaration from `outer`.
1373///
1374/// Returns `(namespace_decl, stripped_outer)`.  Unlike the previous approach of
1375/// re-searching for the *normalised* `"namespace Foo;"` string, this function
1376/// locates the declaration by raw byte offsets so it works even when the
1377/// fallback token serialiser inserts spaces before `';'` (e.g.
1378/// `"namespace MyNamespace ;"`).
1379fn strip_namespace_decl(outer: String) -> (String, String) {
1380	let marker = "namespace ";
1381	let Some(i) = outer.find(marker) else {
1382		return (String::new(), outer);
1383	};
1384	if i > 0 && !outer[..i].ends_with(|c: char| c.is_whitespace()) {
1385		return (String::new(), outer);
1386	}
1387	let after_marker = i + marker.len();
1388	let rest = outer[after_marker..].trim_start();
1389	let offset_trim = outer[after_marker..].len() - rest.len();
1390	let Some(semi_in_rest) = rest.find(';') else {
1391		return (String::new(), outer);
1392	};
1393	let ns = rest[..semi_in_rest]
1394		.trim()
1395		.replace(|c: char| c.is_whitespace(), "");
1396	if ns.is_empty() {
1397		return (String::new(), outer);
1398	}
1399	// Byte index of ';' in `outer`.
1400	let semi_abs = after_marker + offset_trim + semi_in_rest;
1401	let stripped = format!("{}{}", &outer[..i], &outer[semi_abs + 1..]);
1402	(format!("namespace {ns};"), stripped.trim().to_string())
1403}
1404
1405/// Render the complete C# source file.
1406fn format_csharp_source(
1407	usings: &str,
1408	namespace_decl: &str,
1409	class_name: &str,
1410	outer: &str,
1411	body: &str,
1412	main_method: &str,
1413) -> String {
1414	format!(
1415		"{usings}\n{namespace_decl}\nclass {class_name} {{\n\n{outer}\n\n{body}\n\n{main_method}\n}}\n"
1416	)
1417}
1418
1419/// Compile and run C# at compile time, returning raw stdout bytes.
1420fn compile_run_csharp_now(
1421	class_name: &str,
1422	csharp_source: &str,
1423	build_raw: Option<&str>,
1424	run_raw: Option<&str>,
1425	references: &[&str],
1426) -> Result<Vec<u8>, String> {
1427	inline_csharp_core::run_csharp(
1428		class_name,
1429		csharp_source,
1430		build_raw.unwrap_or(""),
1431		run_raw.unwrap_or(""),
1432		references,
1433		&[],
1434	)
1435	.map_err(|e| e.to_string())
1436}
1437
1438// ── make_runner_fn ─────────────────────────────────────────────────────────────
1439
1440/// Generate a `fn __csharp_runner(...) -> Result<T, CsharpError>` token stream
1441/// used by both `csharp!` and `csharp_fn!`.
1442#[allow(clippy::similar_names)]
1443fn make_runner_fn(
1444	parsed: ParsedCsharp,
1445	opts: DotnetOpts,
1446	prefix: &str,
1447) -> proc_macro2::TokenStream {
1448	let ParsedCsharp {
1449		usings,
1450		namespace_decl,
1451		outer,
1452		body,
1453		params,
1454		csharp_type,
1455	} = parsed;
1456
1457	let class_name = make_class_name(prefix, &usings, &outer, &body, &opts);
1458	let main_method = csharp_type.csharp_main(&params);
1459	let csharp_source = format_csharp_source(
1460		&usings,
1461		&namespace_decl,
1462		&class_name,
1463		&outer,
1464		&body,
1465		&main_method,
1466	);
1467
1468	let build_raw = opts.build_args;
1469	let run_raw = opts.run_args;
1470	let reference_strs: Vec<proc_macro2::TokenStream> = opts
1471		.references
1472		.iter()
1473		.map(|r| {
1474			let lit = proc_macro2::Literal::string(r);
1475			quote! { #lit }
1476		})
1477		.collect();
1478
1479	let deser = csharp_type.rust_deser();
1480	let ret_ty = csharp_type.rust_return_type_ts();
1481
1482	let fn_params: Vec<proc_macro2::TokenStream> = params
1483		.iter()
1484		.map(|(ty, name)| {
1485			let ident = proc_macro2::Ident::new(name, proc_macro2::Span::call_site());
1486			let param_ty = ty.rust_param_type_ts();
1487			quote! { #ident: #param_ty }
1488		})
1489		.collect();
1490
1491	let ser_stmts: Vec<proc_macro2::TokenStream> = params
1492		.iter()
1493		.map(|(ty, name)| {
1494			let ident = proc_macro2::Ident::new(name, proc_macro2::Span::call_site());
1495			let ident_ts = quote! { #ident };
1496			ty.rust_ser_ts(&ident_ts, 0)
1497		})
1498		.collect();
1499
1500	quote! {
1501		fn __csharp_runner(#(#fn_params),*) -> ::std::result::Result<#ret_ty, ::inline_csharp::CsharpError> {
1502			let mut _stdin_bytes: ::std::vec::Vec<u8> = ::std::vec::Vec::new();
1503			#(#ser_stmts)*
1504			let _raw = ::inline_csharp::run_csharp(
1505				#class_name,
1506				#csharp_source,
1507				#build_raw,
1508				#run_raw,
1509				&[#(#reference_strs),*],
1510				&_stdin_bytes,
1511			)?;
1512			::std::result::Result::Ok(#deser)
1513		}
1514	}
1515}
1516
1517// ── ct_csharp_impl ────────────────────────────────────────────────────────────
1518
1519fn ct_csharp_impl(input: proc_macro2::TokenStream) -> Result<proc_macro2::TokenStream, String> {
1520	let (opts, input) = extract_opts(input);
1521
1522	let ParsedCsharp {
1523		usings,
1524		namespace_decl,
1525		outer,
1526		body,
1527		csharp_type,
1528		..
1529	} = parse_csharp_source(input)?;
1530
1531	let class_name = make_class_name("CtCsharp", &usings, &outer, &body, &opts);
1532	let main_method = csharp_type.csharp_main(&[]);
1533	let csharp_source = format_csharp_source(
1534		&usings,
1535		&namespace_decl,
1536		&class_name,
1537		&outer,
1538		&body,
1539		&main_method,
1540	);
1541
1542	let refs: Vec<&str> = opts.references.iter().map(String::as_str).collect();
1543	let bytes = compile_run_csharp_now(
1544		&class_name,
1545		&csharp_source,
1546		Some(&opts.build_args),
1547		Some(&opts.run_args),
1548		&refs,
1549	)?;
1550	csharp_type.ct_csharp_tokens(bytes)
1551}
1552
1553// ── Public proc macros ────────────────────────────────────────────────────────
1554
1555/// Compile and run zero-argument C# code at *program runtime*.
1556///
1557/// Wraps the provided C# body in a generated class, compiles it with `dotnet build`,
1558/// and executes it with `dotnet run`. The return value of `static T Run()` is
1559/// binary-serialised by the generated `Main()` and deserialised to the inferred
1560/// Rust type.
1561///
1562/// Expands to `Result<T, inline_csharp::CsharpError>`.
1563///
1564/// For `Run()` methods that take parameters, use [`csharp_fn!`] instead.
1565///
1566/// # Options
1567///
1568/// Optional `key = "value"` pairs may appear before the C# body, separated by commas:
1569///
1570/// - `build = "<args>"` — extra arguments for `dotnet build`.
1571/// - `run   = "<args>"` — extra arguments for `dotnet run`.
1572/// - `reference = "<path>"` — add a reference assembly (repeatable).
1573///
1574/// # Examples
1575///
1576/// ```text
1577/// use inline_csharp::csharp;
1578///
1579/// let x: i32 = csharp! {
1580///     static int Run() {
1581///         return 42;
1582///     }
1583/// }.unwrap();
1584/// ```
1585#[proc_macro]
1586#[allow(clippy::similar_names)]
1587pub fn csharp(input: TokenStream) -> TokenStream {
1588	let input2 = proc_macro2::TokenStream::from(input);
1589	let (opts, input2) = extract_opts(input2);
1590
1591	let parsed = match parse_csharp_source(input2) {
1592		Ok(p) => p,
1593		Err(msg) => return quote! { compile_error!(#msg) }.into(),
1594	};
1595
1596	let runner_fn = make_runner_fn(parsed, opts, "InlineCsharp");
1597
1598	let generated = quote! {
1599		{
1600			#runner_fn
1601			__csharp_runner()
1602		}
1603	};
1604
1605	generated.into()
1606}
1607
1608/// Return a typed Rust function that compiles and runs C# at *program runtime*.
1609///
1610/// Like [`csharp!`], but supports parameters. The parameters declared in the
1611/// C# `Run(P1 p1, P2 p2, ...)` method become the Rust function's parameters.
1612/// Arguments are serialised by Rust and piped to the C# process via stdin;
1613/// C# reads them with `BinaryReader`.
1614///
1615/// Expands to a function value of type `fn(P1, P2, ...) -> Result<T, CsharpError>`.
1616///
1617/// # Examples
1618///
1619/// ```text
1620/// use inline_csharp::csharp_fn;
1621///
1622/// let double_it = csharp_fn! {
1623///     static int Run(int n) {
1624///         return n * 2;
1625///     }
1626/// };
1627/// let result: i32 = double_it(21).unwrap();
1628/// assert_eq!(result, 42);
1629/// ```
1630#[proc_macro]
1631#[allow(clippy::similar_names)]
1632pub fn csharp_fn(input: TokenStream) -> TokenStream {
1633	let input2 = proc_macro2::TokenStream::from(input);
1634	let (opts, input2) = extract_opts(input2);
1635
1636	let parsed = match parse_csharp_source(input2) {
1637		Ok(p) => p,
1638		Err(msg) => return quote! { compile_error!(#msg) }.into(),
1639	};
1640
1641	let runner_fn = make_runner_fn(parsed, opts, "InlineCsharp");
1642
1643	let generated = quote! {
1644		{
1645			#runner_fn
1646			__csharp_runner
1647		}
1648	};
1649
1650	generated.into()
1651}
1652
1653/// Run C# at *compile time* and splice its return value as a Rust literal.
1654///
1655/// Accepts optional `build = "..."`, `run = "..."`, and `reference = "..."` key-value
1656/// pairs before the C# body. The user provides a `static <T> Run()` method; its
1657/// binary-serialised return value is decoded and emitted as a Rust literal at
1658/// the call site.
1659///
1660/// C# compilation/runtime errors become Rust `compile_error!` diagnostics.
1661///
1662/// # Examples
1663///
1664/// ```text
1665/// use inline_csharp::ct_csharp;
1666///
1667/// const PI_APPROX: f64 = ct_csharp! {
1668///     static double Run() {
1669///         return Math.PI;
1670///     }
1671/// };
1672/// ```
1673#[proc_macro]
1674pub fn ct_csharp(input: TokenStream) -> TokenStream {
1675	match ct_csharp_impl(proc_macro2::TokenStream::from(input)) {
1676		Ok(ts) => ts.into(),
1677		Err(msg) => quote! { compile_error!(#msg) }.into(),
1678	}
1679}