use alloc::boxed::Box;
use alloc::string::{String, ToString};
use miette::{Diagnostic, LabeledSpan, SourceSpan};
fn slice_at_span(src: &str, span: (usize, usize)) -> &str {
let start = span.0.min(src.len());
let end = span.0.saturating_add(span.1).min(src.len());
src.get(start..end).unwrap_or("")
}
fn lex_error_summary(src: &str, span: (usize, usize)) -> String {
let snippet = slice_at_span(src, span);
let rest = src.get(span.0.min(src.len())..).unwrap_or("");
if rest.starts_with('#') {
"Failed to parse token '#': comments in .fasm start with '//', not '#'".to_string()
} else if rest.starts_with("/*") || rest.starts_with("*/") {
"Failed to parse token '/*': block comments are not supported in .fasm (use '//' instead)"
.to_string()
} else if rest.starts_with('\'') {
alloc::format!(
"Failed to parse token '{}': single quotes are not used for text strings in .fasm (use double quotes \"...\" instead)",
snippet
)
} else if rest.starts_with('/')
&& rest.as_bytes().get(1).is_some_and(|b| b.is_ascii_digit())
{
alloc::format!(
"Failed to parse token '{}': subnet masks (CIDR notation) are not used in HOST or IP declarations",
snippet
)
} else if rest.starts_with('=') || rest.starts_with(':') {
let ch = rest.chars().next().unwrap_or('=');
alloc::format!(
"Failed to parse token '{}': assignment symbol '{}' is not used between keywords and values in .fasm",
ch,
ch
)
} else if rest.starts_with('[') || rest.starts_with(']') {
let ch = rest.chars().next().unwrap_or('[');
alloc::format!(
"Failed to parse token '{}': square brackets '[' and ']' are not used in .fasm",
ch
)
} else if rest.starts_with(';') {
"Failed to parse token ';': semicolons are not needed at the end of lines in .fasm"
.to_string()
} else if rest.starts_with('+') {
"Failed to parse token '+': relative delay prefixes like '+5ms' are not used in .fasm"
.to_string()
} else if snippet.is_empty() {
"Failed to parse token".to_string()
} else {
alloc::format!("Failed to parse token '{}'", snippet)
}
}
pub fn detect_foreign_fasm(src: &str) -> Option<&'static str> {
let has_compile = src.lines().any(|l| {
let trimmed = l.trim();
trimmed.to_ascii_uppercase().starts_with("COMPILE")
});
let has_flow = src.lines().any(|l| {
let trimmed = l.trim();
trimmed.to_ascii_uppercase().starts_with("FLOW")
});
let has_host = src.lines().any(|l| {
let trimmed = l.trim();
trimmed.to_ascii_uppercase().starts_with("HOST")
});
if has_compile && (has_flow || has_host) {
return None;
}
let mut has_semicolon_comment = false;
let mut has_x86_directive = false;
let mut has_hash_comment = false;
let mut has_fpga_tile = false;
let mut has_dot_tile_feature = false;
let fpga_tiles = ["CLB", "LUT", "IOB", "INT_L", "INT_R", "BRAM", "DSP", "SLICE"];
for line in src.lines() {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if trimmed.starts_with(';') {
has_semicolon_comment = true;
}
if trimmed.starts_with('#') {
has_hash_comment = true;
}
let upper = trimmed.to_ascii_uppercase();
if upper.starts_with("FORMAT ")
|| upper.starts_with("SECTION ")
|| upper.starts_with("SEGMENT ")
|| upper.starts_with("USE16")
|| upper.starts_with("USE32")
|| upper.starts_with("USE64")
|| upper.starts_with("ORG ")
|| upper.starts_with("ENTRY ")
|| (upper.starts_with("INCLUDE ") && (trimmed.contains('\'') || trimmed.contains('"')))
{
has_x86_directive = true;
}
for tile in &fpga_tiles {
if upper.contains(tile) {
has_fpga_tile = true;
if trimmed.contains('.') && (trimmed.contains('[') || trimmed.contains('=')) {
has_dot_tile_feature = true;
}
break;
}
}
}
if has_x86_directive || (has_semicolon_comment && !has_flow && !has_compile && !has_host) {
return Some("an x86 Flat Assembler");
}
if has_fpga_tile
&& (has_hash_comment || has_dot_tile_feature)
&& !has_flow
&& !has_compile
&& !has_host
{
return Some("an FPGA FASM");
}
None
}
fn lex_error_help(src: &str, span: (usize, usize)) -> String {
if let Some(kind) = detect_foreign_fasm(src) {
return alloc::format!(
"This file appears to be {} file, not a FrameAssembly network packet specification. FrameAssembly files declare HOST {{ IP ... }}, FLOW(...), and COMPILE {{ ... }} blocks to compile network traffic into .pcap/.pcapng captures. See https://github.com/SteveWinschel/FrameAssembly for syntax and examples.",
kind
);
}
let rest = src.get(span.0.min(src.len())..).unwrap_or("");
if rest.starts_with('#') {
"Replace '#' with '//' to write a comment (for example: // Define client and server hosts)."
.to_string()
} else if rest.starts_with("/*") || rest.starts_with("*/") {
"Start each comment line with '//' instead of wrapping comments in '/* ... */'.".to_string()
} else if rest.starts_with('\'') {
"Wrap text strings in double quotes instead of single quotes (for example: URI \"/index.html\" or USER \"admin\").".to_string()
} else if rest.starts_with('/')
&& rest.as_bytes().get(1).is_some_and(|b| b.is_ascii_digit())
{
"Remove the CIDR prefix length and write a plain IP address (for example: IP 10.0.0.1 instead of IP 10.0.0.1/24).".to_string()
} else if rest.starts_with('=') || rest.starts_with(':') {
"Separate keywords and values with a space instead of '=' or ':' (for example: IP 10.0.0.1 or DSTPORT 443).".to_string()
} else if rest.starts_with('[') || rest.starts_with(']') {
"Write TCP flags as space-separated words without brackets (for example: TCP SYN ACK) and place delays at the end of the line (WAIT 5ms).".to_string()
} else if rest.starts_with(';') {
"Remove the ';' at the end of the line — statements end automatically at the newline."
.to_string()
} else if rest.starts_with('+') {
"Put 'WAIT <duration>' at the end of the packet statement instead (for example: c -> s TCP SYN ACK WAIT 5ms).".to_string()
} else {
"Check for unsupported punctuation or typos. See docs/getting_started.md for valid .fasm syntax and examples.".to_string()
}
}
fn parse_error_help(message: &str, src: &str, span: (usize, usize)) -> String {
if let Some(kind) = detect_foreign_fasm(src) {
return alloc::format!(
"This file appears to be {} file, not a FrameAssembly network packet specification. FrameAssembly files declare HOST {{ IP ... }}, FLOW(...), and COMPILE {{ ... }} blocks to compile network traffic into .pcap/.pcapng captures. See https://github.com/SteveWinschel/FrameAssembly for syntax and examples.",
kind
);
}
let snippet = slice_at_span(src, span).to_ascii_lowercase();
if matches!(snippet.as_str(), "sport" | "src_port" | "source_port") {
"Replace 'sport' with 'SRCPORT <port>' (for example: SRCPORT 51234).".to_string()
} else if matches!(
snippet.as_str(),
"dport" | "dst_port" | "dest_port" | "destination_port"
) {
"Replace 'dport' with 'DSTPORT <port>' (for example: DSTPORT 443).".to_string()
} else if message.contains("ACKNUM") {
"Use 'ACK' alone as a TCP flag, and use 'ACKNUM <number>' to set the TCP acknowledgment number (for example: TCP ACK ACKNUM 100).".to_string()
} else if matches!(snippet.as_str(), "hoplimit" | "hop_limit" | "hlim")
|| message.contains("Hop Limit")
{
"Use 'TTL <value>' for both IPv4 Time-to-Live and IPv6 Hop Limit (for example: TTL 64 or TTL 255).".to_string()
} else if matches!(
snippet.as_str(),
"tos" | "traffic_class" | "trafficclass" | "tclass"
) {
"Use 'DSCP <0..63>' and optional 'ECN <0..3>' to set IPv4 TOS / IPv6 Traffic Class bits (for example: DSCP 46).".to_string()
} else if matches!(snippet.as_str(), "cos" | "dot1p") {
"Use 'VLAN <id> PCP <0..7>' to set the Layer 2 802.1p Class of Service priority.".to_string()
} else if matches!(
snippet.as_str(),
"vlan_id" | "vlanid" | "vid" | "svlan" | "cvlan" | "outer_vlan"
) || message.contains("INNER_VLAN")
{
"Use 'VLAN 100' for single 802.1Q tagging, or 'QINQ 100 INNER_VLAN 200' for 802.1ad double tagging.".to_string()
} else if message.contains("HOST_HDR") {
"Inside an HTTP statement, use 'HOST_HDR \"example.com\"' to set the HTTP Host header ('HOST' is reserved for declaring network endpoints).".to_string()
} else if message.contains("missing a required 'IP")
|| message.contains("must specify at least an 'IP <address>' or 'MAC <address>'")
{
"Add 'IP <address>' and/or 'MAC <address>' inside the HOST block — for example: HOST client { IP 10.0.0.1 MAC 00:11:22:33:44:55 }.".to_string()
} else if message.contains("missing its endpoint parameter list") {
"Add endpoint parameter names in parentheses after the flow name — for example: FLOW my_flow(c, s) { c -> s TCP SYN }.".to_string()
} else if message.contains("missing 'COMPILE") || message.contains("duplicate 'COMPILE'") {
"Every .fasm file must contain exactly one COMPILE { ... } block at the end that calls your flows — for example: COMPILE { my_flow(client, server) }.".to_string()
} else if message.contains("outside a FLOW block") {
"Move packet statements ('a -> b ...') inside a FLOW name(a, b) { ... } block (then call name(host1, host2) inside COMPILE { ... }) or place inline packet statements directly inside COMPILE { ... }.".to_string()
} else {
"Ensure the syntax matches the expected grammar. See docs/getting_started.md for ready-to-use .fasm examples.".to_string()
}
}
fn semantic_error_help(message: &str) -> String {
if message.contains("Mixed IPv4 and IPv6") {
"Use either two IPv4 hosts or two IPv6 hosts in the same flow call — a single packet cannot have an IPv4 source and an IPv6 destination.".to_string()
} else if message.contains("requires an 'IP <address>'") {
"Add 'IP <address>' to the HOST declaration (for example: HOST r1 { IP 10.0.0.1 MAC 00:11:22:33:44:55 }), or use a pure Layer 2 protocol (ARP, VLAN, QINQ, MPLS, LLDP, STP, LACP, EAPOL).".to_string()
} else if message.contains("Undefined host") {
"Declare the host at the top of the file with 'HOST <name> { IP <address> }' before passing it into a flow call in COMPILE.".to_string()
} else if message.contains("Undefined flow") {
"Declare the flow with 'FLOW <name>(a, b) { ... }' before calling it inside COMPILE.".to_string()
} else if message.contains("Undefined endpoint") {
"Make sure the endpoint name on the packet line matches one of the parameter names listed in 'FLOW name(param1, param2)'.".to_string()
} else if message.contains("expects") && message.contains("arguments") {
"Pass the exact number of host names in 'COMPILE { flow_name(host1, host2) }' to match the parameters in 'FLOW flow_name(a, b)'.".to_string()
} else {
"Check the highlighted field value or endpoint name against protocol limits.".to_string()
}
}
#[derive(Debug)]
pub enum CompilerError {
LexError {
src: String,
span: (usize, usize),
},
ParseError {
message: String,
src: String,
span: (usize, usize),
},
SemanticError {
message: String,
src: String,
span: (usize, usize),
},
BackendError(String),
}
impl Diagnostic for CompilerError {
fn code<'a>(&'a self) -> Option<Box<dyn core::fmt::Display + 'a>> {
let code_str = match self {
Self::LexError { .. } => "frameassembly::lex_error",
Self::ParseError { .. } => "frameassembly::parse_error",
Self::SemanticError { .. } => "frameassembly::semantic_error",
Self::BackendError(_) => "frameassembly::backend_error",
};
Some(Box::new(code_str))
}
fn help<'a>(&'a self) -> Option<Box<dyn core::fmt::Display + 'a>> {
match self {
Self::LexError { src, span } => Some(Box::new(lex_error_help(src, *span))),
Self::ParseError { message, src, span } => {
Some(Box::new(parse_error_help(message, src, *span)))
}
Self::SemanticError { message, .. } => Some(Box::new(semantic_error_help(message))),
Self::BackendError(_) => None,
}
}
fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
match self {
Self::LexError { span, .. } => Some(Box::new(core::iter::once(
LabeledSpan::new_with_span(
Some("Invalid token".to_string()),
SourceSpan::from(*span),
),
))),
Self::ParseError { span, .. } => Some(Box::new(core::iter::once(
LabeledSpan::new_with_span(Some("Here".to_string()), SourceSpan::from(*span)),
))),
Self::SemanticError { span, .. } => Some(Box::new(core::iter::once(
LabeledSpan::new_with_span(
Some("Error occurred here".to_string()),
SourceSpan::from(*span),
),
))),
Self::BackendError(_) => None,
}
}
}
impl core::fmt::Display for CompilerError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::LexError { src, span } => write!(f, "{}", lex_error_summary(src, *span)),
Self::ParseError { message, .. } => write!(f, "Failed to parse input: {}", message),
Self::SemanticError { message, .. } => write!(f, "Semantic error: {}", message),
Self::BackendError(msg) => write!(f, "Backend generation error: {}", msg),
}
}
}
impl std::error::Error for CompilerError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_x86_flat_assembler() {
let sample1 = "format PE GUI 4.0\nentry start\nsection '.text' code readable executable\nstart:\n ret\n";
assert_eq!(detect_foreign_fasm(sample1), Some("an x86 Flat Assembler"));
let sample2 = "; Flat Assembler source\nuse32\norg 0x100\nmov ax, 0x4c00\nint 0x21\n";
assert_eq!(detect_foreign_fasm(sample2), Some("an x86 Flat Assembler"));
let sample3 = "format ELF64 executable\nsegment readable executable\nentry $\n";
assert_eq!(detect_foreign_fasm(sample3), Some("an x86 Flat Assembler"));
}
#[test]
fn test_detect_fpga_fasm() {
let sample1 = "# Generated by Project X-Ray\nCLBLL_L_X12Y34.SLICEL_X0.ALUT.INIT[31:0] = 32'h00000000\nIOB_X0Y1.ENABLE 1\n";
assert_eq!(detect_foreign_fasm(sample1), Some("an FPGA FASM"));
let sample2 = "CLBLL_L_X12Y34.SLICEL_X0.ALUT.INIT[31:0] = 32'h00000000\n";
assert_eq!(detect_foreign_fasm(sample2), Some("an FPGA FASM"));
}
#[test]
fn test_valid_frameassembly_is_not_foreign() {
let sample = "HOST client { IP 10.0.0.1 }\nFLOW ping(c, s) { c -> s ICMP }\nCOMPILE { ping(client, client) }\n";
assert_eq!(detect_foreign_fasm(sample), None);
}
#[test]
fn test_foreign_fasm_lex_error_help_hint() {
let sample = "; x86 assembly file\nformat PE\n";
let help = lex_error_help(sample, (0, 1));
assert!(help.contains("x86 Flat Assembler"));
assert!(help.contains("FrameAssembly files declare HOST { IP ... }, FLOW(...), and COMPILE { ... } blocks"));
assert!(help.contains("https://github.com/SteveWinschel/FrameAssembly"));
let fpga_sample = "# FPGA bitstream\nCLBLL_L_X0Y0.ENABLE 1\n";
let fpga_help = lex_error_help(fpga_sample, (0, 1));
assert!(fpga_help.contains("FPGA FASM"));
}
#[test]
fn test_foreign_fasm_parse_error_help_hint() {
let sample = "format PE GUI 4.0\nentry start\n";
let help = parse_error_help("unexpected token", sample, (0, 6));
assert!(help.contains("x86 Flat Assembler"));
}
}