///|
priv struct ParseResult {
commands : Array[LocatedCommand]
issues : Array[Issue]
issue_command_indices : Array[Int]
}
///|
fn empty_parse_result() -> ParseResult {
{ commands: [], issues: [], issue_command_indices: [] }
}
///|
fn push_cmd(result : ParseResult, command : GerberCommand, line : Int) -> Unit {
result.commands.push({ command, line })
}
///|
fn push_parse_issue(
result : ParseResult,
code : String,
severity : Severity,
message : String,
line : Int,
) -> Unit {
result.issues.push({ code, severity, message, line: Some(line) })
result.issue_command_indices.push(result.commands.length() - 1)
}
///|
fn parse_tokens(tokens : Array[RawToken]) -> ParseResult {
let result = empty_parse_result()
for token in tokens {
match token.kind {
Word => parse_word(result, token)
ExtendedBlock => parse_extended(result, token)
}
}
result
}
///|
fn parse_word(result : ParseResult, token : RawToken) -> Unit {
let body = trim_spaces(strip_word_star(token.raw))
let line = token.line
if body.length() == 0 {
push_cmd(result, Malformed(""), line)
push_parse_issue(
result,
"G302",
Warning,
"Unsupported Gerber command.",
line,
)
return
}
if has_prefix(body, "G04") {
let content = trim_spaces(slice_str(body, 3, body.length()))
push_cmd(result, Comment(content), line)
return
}
if body == "G01" {
push_cmd(result, SetPlotMode(Linear), line)
return
}
if body == "G02" {
push_cmd(result, SetPlotMode(Clockwise), line)
return
}
if body == "G03" {
push_cmd(result, SetPlotMode(CounterClockwise), line)
return
}
if body == "G75" {
push_cmd(result, MultiQuadrant, line)
return
}
if body == "G36" {
push_cmd(result, BeginRegion, line)
return
}
if body == "G37" {
push_cmd(result, EndRegion, line)
return
}
if body == "M02" {
push_cmd(result, EndFile, line)
return
}
// Deprecated combined G/D syntax: G01X...D01 etc.
if is_deprecated_combined_gd(body) {
push_cmd(result, Malformed(body), line)
push_parse_issue(
result,
"G304",
Warning,
"Deprecated combined G/D syntax is not supported.",
line,
)
return
}
// Deprecated styles G1, g01, G001
if is_deprecated_g_style(body) {
push_cmd(result, Malformed(body), line)
push_parse_issue(
result,
"G304",
Warning,
"Deprecated syntax not supported.",
line,
)
return
}
// Dnn aperture selection nn >= 10, or D01/D02/D03 operations
if code_unit_at(body, 0) == 68 {
// 'D'
match try_parse_operation_or_select(body) {
Some(cmd) => {
push_cmd(result, cmd, line)
return
}
None => ()
}
if looks_like_invalid_aperture_code(body) {
push_cmd(result, Malformed(body), line)
push_parse_issue(result, "G203", Error, "Invalid aperture code.", line)
return
}
}
// Coordinate + D0x operation
if looks_like_operation(body) {
match parse_operation(body) {
Ok(cmd) => {
push_cmd(result, cmd, line)
return
}
Err(msg) => {
push_cmd(result, Malformed(body), line)
push_parse_issue(result, "G210", Error, msg, line)
return
}
}
}
// Modal operation without D code: X100Y100
if is_modal_coordinate_only(body) {
push_cmd(result, Malformed(body), line)
push_parse_issue(
result,
"G304",
Warning,
"Deprecated modal operation syntax is not supported.",
line,
)
return
}
push_cmd(result, UnknownWord(body), line)
}
///|
fn parse_extended(result : ParseResult, token : RawToken) -> Unit {
let inner = strip_extended(token.raw)
let body = trim_spaces(inner)
let line = token.line
if body.length() == 0 {
push_cmd(result, Malformed("%"), line)
push_parse_issue(
result,
"G302",
Warning,
"Unsupported Gerber command.",
line,
)
return
}
if has_prefix(body, "MO") {
parse_unit(result, body, line)
return
}
if has_prefix(body, "FS") {
parse_format(result, body, line)
return
}
if has_prefix(body, "ADD") {
parse_ad(result, body, line)
return
}
if has_prefix(body, "AM") {
parse_am(result, inner, body, line)
return
}
if has_prefix(body, "LP") {
parse_lp(result, body, line)
return
}
if has_prefix(body, "TF") || has_prefix(body, "TA") || has_prefix(body, "TO") {
parse_attribute(result, body, line)
return
}
if has_prefix(body, "TD") {
parse_td(result, body, line)
return
}
push_cmd(result, UnknownExtended(body), line)
}
///|
fn parse_unit(result : ParseResult, body : String, line : Int) -> Unit {
if body == "MOMM" {
push_cmd(result, Unit(Millimeter), line)
} else if body == "MOIN" {
push_cmd(result, Unit(Inch), line)
} else {
push_cmd(result, Malformed(body), line)
push_parse_issue(result, "G113", Error, "Malformed unit statement.", line)
}
}
///|
fn parse_format(result : ParseResult, body : String, line : Int) -> Unit {
if has_prefix(body, "FSTA") || has_prefix(body, "FSLI") {
push_cmd(result, Malformed(body), line)
push_parse_issue(
result,
"G124",
Error,
"Unsupported or deprecated format variant.",
line,
)
return
}
// Expect FSLAXn6Yn6
if !has_prefix(body, "FSLA") {
push_cmd(result, Malformed(body), line)
push_parse_issue(
result,
"G123",
Error,
"Malformed format specification.",
line,
)
return
}
let rest = slice_str(body, 4, body.length())
// XnnYnn
if rest.length() != 6 {
push_cmd(result, Malformed(body), line)
push_parse_issue(
result,
"G123",
Error,
"Malformed format specification.",
line,
)
return
}
if code_unit_at(rest, 0) != 88 || code_unit_at(rest, 3) != 89 {
// X ... Y
push_cmd(result, Malformed(body), line)
push_parse_issue(
result,
"G123",
Error,
"Malformed format specification.",
line,
)
return
}
let xi = code_unit_at(rest, 1)
let xd = code_unit_at(rest, 2)
let yi = code_unit_at(rest, 4)
let yd = code_unit_at(rest, 5)
if !(is_ascii_digit(xi) &&
is_ascii_digit(xd) &&
is_ascii_digit(yi) &&
is_ascii_digit(yd)) {
push_cmd(result, Malformed(body), line)
push_parse_issue(
result,
"G123",
Error,
"Malformed format specification.",
line,
)
return
}
let x_int = xi - 48
let x_dec = xd - 48
let y_int = yi - 48
let y_dec = yd - 48
if x_int < 1 ||
x_int > 6 ||
y_int < 1 ||
y_int > 6 ||
x_dec != 6 ||
y_dec != 6 ||
x_int != y_int ||
x_dec != y_dec {
push_cmd(result, Malformed(body), line)
push_parse_issue(
result,
"G123",
Error,
"Malformed format specification.",
line,
)
return
}
push_cmd(
result,
Format({ integer_digits: x_int, decimal_digits: x_dec }),
line,
)
}
///|
fn parse_am(
result : ParseResult,
inner : String,
header_and_body : String,
line : Int,
) -> Unit {
// AMNAME*body... (inner may contain * and newlines)
let after_am = slice_str(header_and_body, 2, header_and_body.length())
let mut name_end = 0
while name_end < after_am.length() {
let c = code_unit_at(after_am, name_end)
if c == 42 {
break
}
name_end = name_end + 1
}
let name = slice_str(after_am, 0, name_end)
if name.length() == 0 || !is_valid_macro_name(name) {
push_cmd(result, Malformed(header_and_body), line)
push_parse_issue(
result,
"G204",
Error,
"Malformed aperture definition.",
line,
)
return
}
// Prefer raw body from original inner after AMNAME*
let raw_body = extract_am_body(inner, name)
push_cmd(result, ApertureMacro(name, raw_body), line)
}
///|
fn extract_am_body(inner : String, name : String) -> String {
// inner like: AMNAME*\n1,1,$1...\n or without newlines
let prefix = "AM" + name
let mut header_start = 0
while header_start < inner.length() {
let c = code_unit_at(inner, header_start)
if c == 32 || c == 9 || c == 10 || c == 13 {
header_start = header_start + 1
} else {
break
}
}
if header_start + prefix.length() <= inner.length() &&
slice_str(inner, header_start, header_start + prefix.length()) == prefix {
let start_search = header_start + prefix.length()
let mut i = start_search
while i < inner.length() {
if code_unit_at(inner, i) == 42 {
return slice_str(inner, i + 1, inner.length())
}
i = i + 1
}
}
""
}
///|
fn is_valid_macro_name(name : String) -> Bool {
if name.length() == 0 {
return false
}
let mut i = 0
while i < name.length() {
let c = code_unit_at(name, i)
let ok = (c >= 65 && c <= 90) ||
(c >= 97 && c <= 122) ||
(c >= 48 && c <= 57) ||
c == 95 ||
c == 46 ||
c == 36
if !ok {
return false
}
i = i + 1
}
true
}
///|
fn parse_ad(result : ParseResult, body : String, line : Int) -> Unit {
// ADD[,]
let rest = slice_str(body, 3, body.length())
let mut i = 0
while i < rest.length() && is_ascii_digit(code_unit_at(rest, i)) {
i = i + 1
}
if i == 0 {
push_cmd(result, Malformed(body), line)
if rest.length() > 0 &&
(code_unit_at(rest, 0) == 43 || code_unit_at(rest, 0) == 45) {
push_parse_issue(result, "G203", Error, "Invalid aperture code.", line)
} else {
push_parse_issue(
result,
"G204",
Error,
"Malformed aperture definition.",
line,
)
}
return
}
let code_str = slice_str(rest, 0, i)
match safe_parse_aperture_code(code_str) {
None => {
push_cmd(result, Malformed(body), line)
push_parse_issue(result, "G203", Error, "Invalid aperture code.", line)
}
Some(code) => {
let after = slice_str(rest, i, rest.length())
if after.length() == 0 {
push_cmd(result, Malformed(body), line)
push_parse_issue(
result,
"G204",
Error,
"Malformed aperture definition.",
line,
)
return
}
let mut t_end = 0
while t_end < after.length() && code_unit_at(after, t_end) != 44 {
t_end = t_end + 1
}
let template = slice_str(after, 0, t_end)
if template.length() == 0 || !is_valid_macro_name(template) {
push_cmd(result, Malformed(body), line)
push_parse_issue(
result,
"G204",
Error,
"Malformed aperture definition.",
line,
)
return
}
let params = if t_end < after.length() {
Some(slice_str(after, t_end + 1, after.length()))
} else {
None
}
if params == Some("") {
push_cmd(result, Malformed(body), line)
push_parse_issue(
result,
"G204",
Error,
"Malformed aperture definition.",
line,
)
return
}
push_cmd(
result,
ApertureDefinition({ code, template, parameters_raw: params, line }),
line,
)
}
}
}
///|
fn parse_lp(result : ParseResult, body : String, line : Int) -> Unit {
if body == "LPD" {
push_cmd(result, SetPolarity(Dark), line)
} else if body == "LPC" {
push_cmd(result, SetPolarity(Clear), line)
} else {
push_cmd(result, Malformed(body), line)
push_parse_issue(
result,
"G302",
Warning,
"Unsupported Gerber command.",
line,
)
}
}
///|
fn parse_attribute(result : ParseResult, body : String, line : Int) -> Unit {
let scope = if has_prefix(body, "TF") {
File
} else if has_prefix(body, "TA") {
Aperture
} else {
Object
}
let rest = slice_str(body, 2, body.length())
let mut comma = -1
let mut i = 0
while i < rest.length() {
if code_unit_at(rest, i) == 44 {
comma = i
break
}
i = i + 1
}
let name = if comma >= 0 { slice_str(rest, 0, comma) } else { rest }
let value = if comma >= 0 {
Some(slice_str(rest, comma + 1, rest.length()))
} else {
None
}
if name.length() == 0 || code_unit_at(name, 0) != 46 {
push_cmd(result, Malformed(body), line)
push_parse_issue(
result,
"G302",
Warning,
"Unsupported Gerber command.",
line,
)
return
}
push_cmd(result, Attribute({ scope, name, raw_value: value, line }), line)
}
///|
fn parse_td(result : ParseResult, body : String, line : Int) -> Unit {
if body == "TD" {
push_cmd(result, DeleteAttribute(None), line)
} else if body.length() > 3 && code_unit_at(body, 2) == 46 {
let name = slice_str(body, 2, body.length())
push_cmd(result, DeleteAttribute(Some(name)), line)
} else {
push_cmd(result, Malformed(body), line)
push_parse_issue(
result,
"G302",
Warning,
"Unsupported Gerber command.",
line,
)
}
}
///|
fn try_parse_operation_or_select(body : String) -> GerberCommand? {
// Exact Dnn or D01/D02/D03, or Dnn with coords is handled elsewhere
if body == "D01" {
return Some(Draw({ x: None, y: None, i: None, j: None }))
}
if body == "D02" {
return Some(Move({ x: None, y: None, i: None, j: None }))
}
if body == "D03" {
return Some(Flash({ x: None, y: None, i: None, j: None }))
}
// Pure Dnn selection: D followed by digits only, nn >= 10
let mut i = 1
while i < body.length() && is_ascii_digit(code_unit_at(body, i)) {
i = i + 1
}
if i == body.length() && i > 1 {
let code_str = slice_str(body, 1, body.length())
// D01/D02/D03 already handled; D04-D09 invalid as selection
match safe_parse_aperture_code(code_str) {
Some(code) => return Some(SelectAperture(code))
None =>
// Could be D9 or overflow - leave for malformed path
if code_str == "01" || code_str == "02" || code_str == "03" {
// unreachable
None
} else if code_str.length() > 0 && is_all_digits(code_str) {
// Invalid aperture code like D9
None
} else {
None
}
}
} else {
None
}
}
///|
fn is_all_digits(s : String) -> Bool {
if s.length() == 0 {
return false
}
let mut i = 0
while i < s.length() {
if !is_ascii_digit(code_unit_at(s, i)) {
return false
}
i = i + 1
}
true
}
///|
fn looks_like_invalid_aperture_code(body : String) -> Bool {
if body.length() <= 1 || code_unit_at(body, 0) != 68 {
return false
}
let first = code_unit_at(body, 1)
first == 43 || first == 45 || is_ascii_digit(first)
}
///|
fn looks_like_operation(body : String) -> Bool {
// Ends with D01/D02/D03 and starts with X/Y/I/J/D
if body.length() < 3 {
return false
}
let last3 = slice_str(body, body.length() - 3, body.length())
if !(last3 == "D01" || last3 == "D02" || last3 == "D03") {
return false
}
let first = code_unit_at(body, 0)
first == 88 || first == 89 || first == 73 || first == 74 || first == 68
}
///|
fn is_modal_coordinate_only(body : String) -> Bool {
if body.length() == 0 {
return false
}
let first = code_unit_at(body, 0)
if !(first == 88 || first == 89 || first == 73 || first == 74) {
return false
}
// Does not end with D0x
if body.length() >= 3 {
let last3 = slice_str(body, body.length() - 3, body.length())
if last3 == "D01" || last3 == "D02" || last3 == "D03" {
return false
}
}
true
}
///|
fn is_deprecated_combined_gd(body : String) -> Bool {
// G01X...D01 or G02... etc.
if !(has_prefix(body, "G01") ||
has_prefix(body, "G02") ||
has_prefix(body, "G03")) {
return false
}
if body == "G01" || body == "G02" || body == "G03" {
return false
}
true
}
///|
fn is_deprecated_g_style(body : String) -> Bool {
if body == "G1" || body == "G2" || body == "G3" {
return true
}
if body == "g01" || body == "g02" || body == "g03" {
return true
}
if body == "G001" || body == "G002" || body == "G003" {
return true
}
false
}
///|
fn parse_operation(body : String) -> Result[GerberCommand, String] {
let op = slice_str(body, body.length() - 3, body.length())
let coords = slice_str(body, 0, body.length() - 3)
match parse_coordinate_fields(coords) {
Err(msg) => Err(msg)
Ok(fields) =>
if op != "D01" && (fields.i is Some(_) || fields.j is Some(_)) {
Err("Malformed operation.")
} else if op == "D01" {
Ok(Draw(fields))
} else if op == "D02" {
Ok(Move(fields))
} else if op == "D03" {
Ok(Flash(fields))
} else {
Err("Malformed operation.")
}
}
}
///|
fn parse_coordinate_fields(s : String) -> Result[CoordinateFields, String] {
let mut x : String? = None
let mut y : String? = None
let mut i : String? = None
let mut j : String? = None
let mut expect = 0
// 0=X, 1=Y, 2=I, 3=J, 4=done order
let mut pos = 0
while pos < s.length() {
let c = code_unit_at(s, pos)
let field_kind = if c == 88 {
0
} else if c == 89 {
1
} else if c == 73 {
2
} else if c == 74 {
3
} else {
return Err("Malformed operation.")
}
if field_kind < expect {
return Err("Malformed operation.")
}
// no duplicates
if field_kind == 0 && x is Some(_) {
return Err("Malformed operation.")
}
if field_kind == 1 && y is Some(_) {
return Err("Malformed operation.")
}
if field_kind == 2 && i is Some(_) {
return Err("Malformed operation.")
}
if field_kind == 3 && j is Some(_) {
return Err("Malformed operation.")
}
pos = pos + 1
let value_start = pos
if pos < s.length() {
let sign = code_unit_at(s, pos)
if sign == 43 || sign == 45 {
pos = pos + 1
}
}
let digit_start = pos
while pos < s.length() && is_ascii_digit(code_unit_at(s, pos)) {
pos = pos + 1
}
if pos == digit_start {
return Err("Malformed operation.")
}
// Reject if next is letter that's not X/Y/I/J - handled by loop
let value = slice_str(s, value_start, pos)
if !is_coordinate_integer(value) {
return Err("Malformed operation.")
}
match field_kind {
0 => x = Some(value)
1 => y = Some(value)
2 => i = Some(value)
3 => j = Some(value)
_ => return Err("Malformed operation.")
}
expect = field_kind + 1
}
Ok({ x, y, i, j })
}