///|
/// A lexical word preserved from a G-code block.
pub struct BlockWord {
letter_value : Int
value_value : Double
raw_value : String
column_value : Int
} derive(Eq, Debug)
///|
pub fn BlockWord::letter(self : BlockWord) -> Int {
self.letter_value
}
///|
pub fn BlockWord::value(self : BlockWord) -> Double {
self.value_value
}
///|
pub fn BlockWord::raw(self : BlockWord) -> String {
self.raw_value
}
///|
pub fn BlockWord::column(self : BlockWord) -> Int {
self.column_value
}
///|
pub fn BlockWord::letter_text(self : BlockWord) -> String {
self.letter_value.unsafe_to_char().to_string()
}
///|
/// One source block with comments and lexical metadata preserved.
pub struct GCodeBlock {
source_value : String
body_value : String
source_line_value : Int
sequence_value : Int?
words_value : Array[BlockWord]
comments_value : Array[String]
checksum_value : Int?
computed_checksum_value : Int?
} derive(Eq, Debug)
///|
pub fn GCodeBlock::source(self : GCodeBlock) -> String {
self.source_value
}
///|
pub fn GCodeBlock::body(self : GCodeBlock) -> String {
self.body_value
}
///|
pub fn GCodeBlock::source_line(self : GCodeBlock) -> Int {
self.source_line_value
}
///|
pub fn GCodeBlock::sequence(self : GCodeBlock) -> Int? {
self.sequence_value
}
///|
pub fn GCodeBlock::words(self : GCodeBlock) -> Array[BlockWord] {
self.words_value.copy()
}
///|
pub fn GCodeBlock::comments(self : GCodeBlock) -> Array[String] {
self.comments_value.copy()
}
///|
pub fn GCodeBlock::checksum(self : GCodeBlock) -> Int? {
self.checksum_value
}
///|
pub fn GCodeBlock::computed_checksum(self : GCodeBlock) -> Int? {
self.computed_checksum_value
}
///|
pub fn GCodeBlock::checksum_valid(self : GCodeBlock) -> Bool {
match (self.checksum_value, self.computed_checksum_value) {
(Some(expected), Some(actual)) => expected == actual
(None, None) => true
_ => false
}
}
///|
pub fn GCodeBlock::is_comment_only(self : GCodeBlock) -> Bool {
self.words_value.length() == 0 && self.comments_value.length() > 0
}
///|
pub fn GCodeBlock::word_count(self : GCodeBlock) -> Int {
self.words_value.length()
}
///|
pub fn GCodeBlock::has_letter(self : GCodeBlock, letter : Int) -> Bool {
let upper = ascii_upper(letter)
for word in self.words_value {
if word.letter() == upper {
return true
}
}
false
}
///|
pub fn GCodeBlock::values_for(self : GCodeBlock, letter : Int) -> Array[Double] {
let values : Array[Double] = []
let upper = ascii_upper(letter)
for word in self.words_value {
if word.letter() == upper {
values.push(word.value())
}
}
values
}
///|
pub fn GCodeBlock::first_value(self : GCodeBlock, letter : Int) -> Double? {
let upper = ascii_upper(letter)
for word in self.words_value {
if word.letter() == upper {
return Some(word.value())
}
}
None
}
///|
pub fn GCodeBlock::duplicate_letters(self : GCodeBlock) -> Array[Int] {
let duplicates : Array[Int] = []
let seen : Array[Int] = []
for word in self.words_value {
let letter = word.letter()
let mut already_seen = false
for value in seen {
if value == letter {
already_seen = true
}
}
if already_seen {
let mut already_reported = false
for value in duplicates {
if value == letter {
already_reported = true
}
}
if !already_reported {
duplicates.push(letter)
}
} else {
seen.push(letter)
}
}
duplicates
}
///|
/// A complete lexical document. Empty physical lines are intentionally kept.
pub struct GCodeDocument {
blocks_value : Array[GCodeBlock]
physical_lines_value : Int
} derive(Eq, Debug)
///|
pub fn GCodeDocument::blocks(self : GCodeDocument) -> Array[GCodeBlock] {
self.blocks_value.copy()
}
///|
pub fn GCodeDocument::physical_lines(self : GCodeDocument) -> Int {
self.physical_lines_value
}
///|
pub fn GCodeDocument::block_count(self : GCodeDocument) -> Int {
self.blocks_value.length()
}
///|
pub fn GCodeDocument::executable_block_count(self : GCodeDocument) -> Int {
let mut count = 0
for block in self.blocks_value {
if block.word_count() > 0 {
count += 1
}
}
count
}
///|
pub fn GCodeDocument::comment_count(self : GCodeDocument) -> Int {
let mut count = 0
for block in self.blocks_value {
count += block.comments().length()
}
count
}
///|
pub fn GCodeDocument::checksum_count(self : GCodeDocument) -> Int {
let mut count = 0
for block in self.blocks_value {
if block.checksum() is Some(_) {
count += 1
}
}
count
}
///|
pub fn GCodeDocument::invalid_checksum_lines(
self : GCodeDocument,
) -> Array[Int] {
let lines : Array[Int] = []
for block in self.blocks_value {
if !block.checksum_valid() {
lines.push(block.source_line())
}
}
lines
}
///|
fn ascii_upper(code : Int) -> Int {
if code >= 97 && code <= 122 {
code - 32
} else {
code
}
}
///|
fn is_ascii_letter(code : Int) -> Bool {
(code >= 65 && code <= 90) || (code >= 97 && code <= 122)
}
///|
fn is_space(code : Int) -> Bool {
code == 32 || code == 9 || code == 13
}
///|
fn trim_ascii(text : String) -> String {
text.trim().to_owned()
}
///|
fn parse_integer_word(text : String) -> Int? {
match parse_number(text) {
Some(value) => {
let integer = value.to_int()
if integer.to_double() == value {
Some(integer)
} else {
None
}
}
None => None
}
}
///|
fn checksum_for_text(text : String) -> Int {
let mut value = 0
for character in text {
value = value ^ character.to_int()
}
value & 0xFF
}
///|
struct BlockScan {
body : String
checksum_body : String
comments : Array[String]
checksum_text : String?
} derive(Eq, Debug)
///|
fn scan_block_source(raw : String, line : Int) -> Result[BlockScan, GCodeError] {
let mut body = ""
let mut checksum_body = ""
let comments : Array[String] = []
let mut parenthesized = ""
let mut in_parentheses = false
let mut semicolon_comment = false
let mut after_checksum = false
let mut checksum_text = ""
for character in raw {
let code = character.to_int()
if semicolon_comment {
checksum_body = checksum_body + character.to_string()
parenthesized = parenthesized + character.to_string()
} else if in_parentheses {
checksum_body = checksum_body + character.to_string()
if code == 41 {
comments.push(trim_ascii(parenthesized))
parenthesized = ""
in_parentheses = false
} else if code == 40 {
return Err(make_error(InvalidWord, "nested comment", line))
} else {
parenthesized = parenthesized + character.to_string()
}
} else if code == 40 {
checksum_body = checksum_body + character.to_string()
in_parentheses = true
} else if code == 41 {
return Err(make_error(InvalidWord, "unmatched closing comment", line))
} else if code == 59 {
checksum_body = checksum_body + character.to_string()
semicolon_comment = true
} else if code == 42 {
if after_checksum {
return Err(make_error(InvalidWord, "multiple checksum markers", line))
}
after_checksum = true
} else if after_checksum {
if !is_space(code) {
checksum_text = checksum_text + character.to_string()
}
} else {
body = body + character.to_string()
checksum_body = checksum_body + character.to_string()
}
}
if in_parentheses {
return Err(make_error(InvalidWord, "unterminated comment", line))
}
if semicolon_comment {
comments.push(trim_ascii(parenthesized))
}
Ok({
body: trim_ascii(body),
checksum_body,
comments,
checksum_text: if after_checksum {
Some(checksum_text)
} else {
None
},
})
}
///|
fn lexical_words(
body : String,
line : Int,
) -> Result[Array[BlockWord], GCodeError] {
let words : Array[BlockWord] = []
let mut current = ""
let mut current_column = 0
for index, character in body {
let code = character.to_int()
if is_ascii_letter(code) {
if current != "" {
let value = match word_number(current) {
Some(number) => number
None =>
return Err(make_error(InvalidNumber, "invalid block word", line))
}
words.push({
letter_value: ascii_upper(current[0].to_int()),
value_value: value,
raw_value: current,
column_value: current_column,
})
}
current = character.to_string()
current_column = index + 1
} else if is_space(code) {
if current != "" {
let value = match word_number(current) {
Some(number) => number
None =>
return Err(make_error(InvalidNumber, "invalid block word", line))
}
words.push({
letter_value: ascii_upper(current[0].to_int()),
value_value: value,
raw_value: current,
column_value: current_column,
})
current = ""
}
} else if current == "" {
return Err(
make_error(InvalidWord, "value appears before a word letter", line),
)
} else {
current = current + character.to_string()
}
}
if current != "" {
let value = match word_number(current) {
Some(number) => number
None => return Err(make_error(InvalidNumber, "invalid block word", line))
}
words.push({
letter_value: ascii_upper(current[0].to_int()),
value_value: value,
raw_value: current,
column_value: current_column,
})
}
Ok(words)
}
///|
fn block_sequence(words : Array[BlockWord]) -> Int? {
for word in words {
if word.letter() == 78 {
let value = word.value().to_int()
if value.to_double() == word.value() {
return Some(value)
}
}
}
None
}
///|
/// Parse source blocks without interpreting machine motion.
pub fn parse_gcode_document(
text : String,
max_lines? : Int = 100000,
) -> Result[GCodeDocument, GCodeError] {
let blocks : Array[GCodeBlock] = []
let mut line_number = 0
for raw in text.split("\n") {
line_number += 1
if line_number > max_lines {
return Err(make_error(InvalidWord, "line limit exceeded", line_number))
}
let scan = match scan_block_source(raw.to_owned(), line_number) {
Ok(value) => value
Err(error) => return Err(error)
}
let words = match lexical_words(scan.body, line_number) {
Ok(value) => value
Err(error) => return Err(error)
}
let checksum = match scan.checksum_text {
Some(text) =>
match parse_integer_word(text) {
Some(value) if value >= 0 && value <= 255 => Some(value)
_ =>
return Err(
make_error(InvalidNumber, "invalid checksum", line_number),
)
}
None => None
}
blocks.push({
source_value: raw.to_owned(),
body_value: scan.body,
source_line_value: line_number,
sequence_value: block_sequence(words),
words_value: words,
comments_value: scan.comments,
checksum_value: checksum,
computed_checksum_value: match checksum {
Some(_) => Some(checksum_for_text(scan.checksum_body))
None => None
},
})
}
Ok({ blocks_value: blocks, physical_lines_value: line_number })
}
///|
pub fn render_block_structure(document : GCodeDocument) -> String {
let mut output = "lines=" + document.physical_lines().to_string()
output = output +
", executable=" +
document.executable_block_count().to_string()
output = output + ", comments=" + document.comment_count().to_string()
output = output +
", checksums=" +
document.checksum_count().to_string() +
"\n"
for block in document.blocks() {
output = output + "line " + block.source_line().to_string()
match block.sequence() {
Some(value) => output = output + " N" + value.to_string()
None => ()
}
output = output + " words=" + block.word_count().to_string()
output = output + " comments=" + block.comments().length().to_string()
if block.checksum() is Some(_) {
output = output +
" checksum=" +
(if block.checksum_valid() { "valid" } else { "invalid" })
}
output = output + "\n"
}
output
}