///|
priv struct CheckerState {
mut format_seen_count : Int
mut unit_seen_count : Int
mut first_operation_seen : Bool
apertures : Map[Int, ApertureDefinition]
aperture_macros : Map[String, Bool]
mut current_aperture : Int?
mut plot_mode : PlotMode?
mut g75_seen : Bool
mut current_x_defined : Bool
mut current_y_defined : Bool
mut region_open : Bool
mut end_seen : Bool
mut end_count : Int
file_attributes : Map[String, String]
mut file_function : String?
mut file_polarity : String?
mut generation_software : String?
mut unit : GerberUnit?
mut coordinate_format : CoordinateFormat?
mut statistics : GerberStatistics
issues : Array[Issue]
mut region_geometry_warned : Bool
mut arc_geometry_warned : Bool
}
///|
fn new_checker_state() -> CheckerState {
{
format_seen_count: 0,
unit_seen_count: 0,
first_operation_seen: false,
apertures: Map([]),
aperture_macros: Map([]),
current_aperture: None,
plot_mode: None,
g75_seen: false,
current_x_defined: false,
current_y_defined: false,
region_open: false,
end_seen: false,
end_count: 0,
file_attributes: Map([]),
file_function: None,
file_polarity: None,
generation_software: None,
unit: None,
coordinate_format: None,
statistics: empty_statistics(),
issues: [],
region_geometry_warned: false,
arc_geometry_warned: false,
}
}
///|
fn add_error(
state : CheckerState,
code : String,
message : String,
line : Int?,
) -> Unit {
state.issues.push({ code, severity: Error, message, line })
}
///|
fn add_warning(
state : CheckerState,
code : String,
message : String,
line : Int?,
) -> Unit {
state.issues.push({ code, severity: Warning, message, line })
}
///|
fn check_commands(
commands : Array[LocatedCommand],
extra_issues : Array[Issue],
issue_command_indices : Array[Int],
) -> GerberReport {
let state = new_checker_state()
let mut extra_index = 0
let mut command_index = 0
while command_index < commands.length() {
let cmd = commands[command_index]
while extra_index < extra_issues.length() &&
issue_command_indices[extra_index] == command_index {
state.issues.push(extra_issues[extra_index])
extra_index = extra_index + 1
}
if state.end_seen {
// Any command after M02
add_error(state, "G131", "Data found after M02.", Some(cmd.line))
// continue checking lightly
}
check_one(state, cmd)
command_index = command_index + 1
}
while extra_index < extra_issues.length() {
state.issues.push(extra_issues[extra_index])
extra_index = extra_index + 1
}
finalize_report(state)
}
///|
fn check_one(state : CheckerState, located : LocatedCommand) -> Unit {
let line = located.line
match located.command {
Comment(_) => {
check_region_disallowed(state, line)
state.statistics = {
..state.statistics,
comments: state.statistics.comments + 1,
}
}
Format(fmt) => check_format(state, fmt, line)
Unit(unit) => check_unit(state, unit, line)
ApertureMacro(name, _) => check_am(state, name, line)
ApertureDefinition(def) => check_ad(state, def, line)
SelectAperture(code) => check_select(state, code, line)
SetPlotMode(mode) =>
// G01/G02/G03 allowed inside region
state.plot_mode = Some(mode)
MultiQuadrant => {
check_region_disallowed(state, line)
state.g75_seen = true
}
Move(fields) => check_move(state, fields, line)
Draw(fields) => check_draw(state, fields, line)
Flash(fields) => check_flash(state, fields, line)
BeginRegion => check_begin_region(state, line)
EndRegion => check_end_region(state, line)
SetPolarity(_) => check_region_disallowed(state, line)
Attribute(attr) => check_attribute(state, attr, line)
DeleteAttribute(_) => {
check_region_disallowed(state, line)
state.statistics = {
..state.statistics,
attributes: state.statistics.attributes + 1,
}
}
EndFile => check_end_file(state, line)
UnknownWord(raw) => check_unknown(state, raw, line, false)
UnknownExtended(raw) => check_unknown(state, raw, line, true)
Malformed(_) =>
// Issues already emitted by parser when applicable
check_region_disallowed_if_open(state, line)
}
}
///|
fn check_region_disallowed(state : CheckerState, line : Int) -> Unit {
if state.region_open {
add_error(
state,
"G223",
"Command is not allowed inside a region.",
Some(line),
)
}
}
///|
fn check_region_disallowed_if_open(state : CheckerState, line : Int) -> Unit {
if state.region_open {
add_error(
state,
"G223",
"Command is not allowed inside a region.",
Some(line),
)
}
}
///|
fn check_format(
state : CheckerState,
fmt : CoordinateFormat,
line : Int,
) -> Unit {
check_region_disallowed(state, line)
state.format_seen_count = state.format_seen_count + 1
if state.format_seen_count > 1 {
add_error(state, "G121", "Duplicate format specification.", Some(line))
} else {
state.coordinate_format = Some(fmt)
}
if state.first_operation_seen {
add_error(
state,
"G122",
"Format specification appears after first operation.",
Some(line),
)
}
}
///|
fn check_unit(state : CheckerState, unit : GerberUnit, line : Int) -> Unit {
check_region_disallowed(state, line)
state.unit_seen_count = state.unit_seen_count + 1
if state.unit_seen_count > 1 {
add_error(state, "G111", "Duplicate unit statement.", Some(line))
} else {
state.unit = Some(unit)
}
if state.first_operation_seen {
add_error(
state,
"G112",
"Unit statement appears after first operation.",
Some(line),
)
}
}
///|
fn check_am(state : CheckerState, name : String, line : Int) -> Unit {
check_region_disallowed(state, line)
state.aperture_macros.set(name, true)
state.statistics = {
..state.statistics,
aperture_macros: state.statistics.aperture_macros + 1,
}
add_warning(
state,
"G303",
"Aperture macro geometry was not analyzed.",
Some(line),
)
}
///|
fn check_ad(state : CheckerState, def : ApertureDefinition, line : Int) -> Unit {
check_region_disallowed(state, line)
state.statistics = {
..state.statistics,
apertures: state.statistics.apertures + 1,
}
// Standard templates C,R,O,P don't need macro; others need macro defined
let std = def.template == "C" ||
def.template == "R" ||
def.template == "O" ||
def.template == "P"
if !std {
match state.aperture_macros.get(def.template) {
None =>
add_error(
state,
"G205",
"Undefined aperture macro " + def.template + ".",
Some(line),
)
Some(_) => ()
}
}
match state.apertures.get(def.code) {
Some(_) =>
add_error(
state,
"G202",
"Aperture D" + def.code.to_string() + " was defined more than once.",
Some(line),
)
None => state.apertures.set(def.code, def)
}
}
///|
fn check_select(state : CheckerState, code : Int, line : Int) -> Unit {
check_region_disallowed(state, line)
match state.apertures.get(code) {
None =>
add_error(
state,
"G201",
"Undefined aperture D" + code.to_string() + ".",
Some(line),
)
Some(_) => state.current_aperture = Some(code)
}
}
///|
fn mark_first_operation(state : CheckerState) -> Unit {
state.first_operation_seen = true
}
///|
fn validate_coordinate_precision(
state : CheckerState,
fields : CoordinateFields,
line : Int,
) -> Unit {
match state.coordinate_format {
None => ()
Some(fmt) => {
let max_digits = fmt.integer_digits + fmt.decimal_digits
fn check_one_coord(v : String?) -> Unit {
match v {
None => ()
Some(s) =>
if digit_count_of_coordinate(s) > max_digits {
add_error(
state,
"G216",
"Coordinate exceeds declared FS precision.",
Some(line),
)
}
}
}
check_one_coord(fields.x)
check_one_coord(fields.y)
check_one_coord(fields.i)
check_one_coord(fields.j)
}
}
}
///|
fn check_move(
state : CheckerState,
fields : CoordinateFields,
line : Int,
) -> Unit {
// D02 allowed in region
mark_first_operation(state)
validate_coordinate_precision(state, fields, line)
// Resolve point: need complete X/Y via new or old
let x_ok = match fields.x {
Some(_) => true
None => state.current_x_defined
}
let y_ok = match fields.y {
Some(_) => true
None => state.current_y_defined
}
if !(x_ok && y_ok) {
add_error(state, "G217", "Current point cannot be resolved.", Some(line))
}
match fields.x {
Some(_) => state.current_x_defined = true
None => ()
}
match fields.y {
Some(_) => state.current_y_defined = true
None => ()
}
state.statistics = { ..state.statistics, moves: state.statistics.moves + 1 }
}
///|
fn check_flash(
state : CheckerState,
fields : CoordinateFields,
line : Int,
) -> Unit {
mark_first_operation(state)
if state.region_open {
add_error(
state,
"G223",
"Command is not allowed inside a region.",
Some(line),
)
}
validate_coordinate_precision(state, fields, line)
match state.current_aperture {
None =>
add_error(state, "G206", "No current aperture selected.", Some(line))
Some(_) => ()
}
let x_ok = match fields.x {
Some(_) => true
None => state.current_x_defined
}
let y_ok = match fields.y {
Some(_) => true
None => state.current_y_defined
}
if !(x_ok && y_ok) {
add_error(state, "G217", "Current point cannot be resolved.", Some(line))
}
match fields.x {
Some(_) => state.current_x_defined = true
None => ()
}
match fields.y {
Some(_) => state.current_y_defined = true
None => ()
}
state.statistics = {
..state.statistics,
flashes: state.statistics.flashes + 1,
}
}
///|
fn check_draw(
state : CheckerState,
fields : CoordinateFields,
line : Int,
) -> Unit {
mark_first_operation(state)
// Region: D01 allowed, no aperture required
if !state.region_open {
match state.current_aperture {
None =>
add_error(state, "G206", "No current aperture selected.", Some(line))
Some(_) => ()
}
}
validate_coordinate_precision(state, fields, line)
if !(state.current_x_defined && state.current_y_defined) {
add_error(
state,
"G211",
"Draw operation has no valid current point.",
Some(line),
)
}
match state.plot_mode {
None =>
add_error(
state,
"G212",
"Draw operation has no active plot mode.",
Some(line),
)
Some(Linear) =>
if fields.i is Some(_) || fields.j is Some(_) {
add_error(
state,
"G214",
"I/J offset is invalid in linear mode.",
Some(line),
)
}
Some(Clockwise) | Some(CounterClockwise) => {
if !state.g75_seen {
add_error(state, "G213", "Circular draw before G75.", Some(line))
}
if !(fields.i is Some(_) && fields.j is Some(_)) {
add_error(
state,
"G215",
"Circular draw requires both I and J offsets.",
Some(line),
)
}
if !state.arc_geometry_warned {
add_warning(state, "G306", "Arc geometry was not verified.", Some(line))
state.arc_geometry_warned = true
}
state.statistics = {
..state.statistics,
arc_draws: state.statistics.arc_draws + 1,
}
}
}
// Update point after draw using end coords
match fields.x {
Some(_) => state.current_x_defined = true
None => ()
}
match fields.y {
Some(_) => state.current_y_defined = true
None => ()
}
state.statistics = { ..state.statistics, draws: state.statistics.draws + 1 }
}
///|
fn check_begin_region(state : CheckerState, line : Int) -> Unit {
if state.region_open {
add_error(state, "G220", "Nested region is not allowed.", Some(line))
} else {
state.region_open = true
state.statistics = {
..state.statistics,
regions: state.statistics.regions + 1,
}
if !state.region_geometry_warned {
add_warning(
state,
"G305",
"Region geometry was not verified.",
Some(line),
)
state.region_geometry_warned = true
}
}
}
///|
fn check_end_region(state : CheckerState, line : Int) -> Unit {
if !state.region_open {
add_error(state, "G221", "Region end without active region.", Some(line))
} else {
state.region_open = false
}
}
///|
fn check_attribute(
state : CheckerState,
attr : GerberAttribute,
line : Int,
) -> Unit {
check_region_disallowed(state, line)
state.statistics = {
..state.statistics,
attributes: state.statistics.attributes + 1,
}
if attr.scope == File {
if attr.name == ".FileFunction" && state.first_operation_seen {
add_error(
state,
"G230",
"FileFunction must be defined in the header.",
Some(line),
)
}
match state.file_attributes.get(attr.name) {
Some(_) =>
add_error(state, "G231", "Duplicate file attribute.", Some(line))
None => {
let value = match attr.raw_value {
Some(v) => v
None => ""
}
state.file_attributes.set(attr.name, value)
if attr.name == ".FileFunction" {
if state.file_function is None {
state.file_function = attr.raw_value
}
} else if attr.name == ".FilePolarity" {
if state.file_polarity is None {
state.file_polarity = attr.raw_value
}
} else if attr.name == ".GenerationSoftware" {
if state.generation_software is None {
state.generation_software = attr.raw_value
}
}
}
}
}
}
///|
fn check_end_file(state : CheckerState, line : Int) -> Unit {
if state.region_open {
// M02 inside region is disallowed
add_error(
state,
"G223",
"Command is not allowed inside a region.",
Some(line),
)
}
state.end_count = state.end_count + 1
if state.end_count > 1 {
add_error(state, "G132", "Duplicate M02.", Some(line))
}
state.end_seen = true
}
///|
fn check_unknown(
state : CheckerState,
_raw : String,
line : Int,
_extended : Bool,
) -> Unit {
add_warning(state, "G302", "Unsupported Gerber command.", Some(line))
state.statistics = {
..state.statistics,
unknown_commands: state.statistics.unknown_commands + 1,
}
if state.region_open {
add_error(
state,
"G223",
"Command is not allowed inside a region.",
Some(line),
)
}
}
///|
fn finalize_report(state : CheckerState) -> GerberReport {
// Global checks in fixed order: G110, G120, G130, G222, G301
if state.unit_seen_count == 0 {
add_error(state, "G110", "Missing unit statement.", None)
}
if state.format_seen_count == 0 {
add_error(state, "G120", "Missing format specification.", None)
}
if state.end_count == 0 {
add_error(state, "G130", "Missing M02 end-of-file command.", None)
}
if state.region_open {
add_error(state, "G222", "Unclosed region.", None)
}
if state.file_function is None {
add_warning(state, "G301", "X2 FileFunction attribute was not found.", None)
}
let issues = state.issues
{
status: compute_status(issues),
unit: state.unit,
coordinate_format: state.coordinate_format,
file_function: state.file_function,
file_polarity: state.file_polarity,
generation_software: state.generation_software,
statistics: state.statistics,
issues,
}
}
///|
pub fn analyze(source : String) -> GerberReport {
let scan = scan_source(source)
if scan.fatal {
return {
status: Fail,
unit: None,
coordinate_format: None,
file_function: None,
file_polarity: None,
generation_software: None,
statistics: empty_statistics(),
issues: scan.issues,
}
}
let parsed = parse_tokens(scan.tokens)
check_commands(parsed.commands, parsed.issues, parsed.issue_command_indices)
}
///|
pub fn input_too_large_report() -> GerberReport {
let issues : Array[Issue] = [
{
code: "G001",
severity: Error,
message: "Input file exceeds 64 MiB limit.",
line: None,
},
]
{
status: Fail,
unit: None,
coordinate_format: None,
file_function: None,
file_polarity: None,
generation_software: None,
statistics: empty_statistics(),
issues,
}
}