From 0ff00584175154bd65a4229df55d56825f967abd Mon Sep 17 00:00:00 2001 From: cnc Date: Fri, 22 May 2026 15:11:28 +0800 Subject: [PATCH] Expand LinuxCNC smoke coverage --- README.zh-CN.md | 9 + core/src/linuxcnc_canon_bridge.cpp | 14 +- core/src/linuxcnc_rs274_backend.cpp | 203 +- core/src/smoke_gcode_parser.cpp | 1848 ++++++++++++++++- core/src/smoke_gcode_parser.h | 13 +- .../cnc_sim_api_linuxcnc_rs274_smoke.cpp | 25 + core/tests/cnc_sim_api_smoke.cpp | 1181 +++++++++++ core/tests/linuxcnc_canon_bridge_smoke.cpp | 10 + core/tools/linuxcnc_rs274_dump.cpp | 84 +- docs/linuxcnc-porting.md | 8 +- test-linuxcnc-rs274-native.sh | 81 + test-linuxcnc-source-link.sh | 65 + test-native.sh | 4 + .../gcode/linuxcnc_canned_cycles_extended.ngc | 6 + tests/gcode/linuxcnc_tool_length.ngc | 4 + tests/gcode/smoke_oword_subprogram.ngc | 12 + tests/gcode/smoke_subprogram_m98.ngc | 10 + 17 files changed, 3507 insertions(+), 70 deletions(-) create mode 100644 tests/gcode/linuxcnc_canned_cycles_extended.ngc create mode 100644 tests/gcode/linuxcnc_tool_length.ngc create mode 100644 tests/gcode/smoke_oword_subprogram.ngc create mode 100644 tests/gcode/smoke_subprogram_m98.ngc diff --git a/README.zh-CN.md b/README.zh-CN.md index 23266c8..c087b3a 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -66,6 +66,11 @@ The first useful target is: - 已有冷却液状态回归语料,`M7/M8/M9` 会通过 `comment.reserved` 临时输出 `10/11=mist off/on`、`20/21=flood off/on`。 - 已有进给模式回归语料,`G93/G94/G95` 会通过 `comment.reserved` 临时输出 `93/94/95`,并通过 `set-feed` 记录 Canon feed mode 回调。 - 已有运动控制模式回归语料,`G61/G61.1/G64` 会通过 `comment.reserved` 临时输出 `611/612/640`,`feed` 暂存 G64 tolerance。 +- smoke parser 已有固定循环第一版展开,覆盖 `G73` 高速啄钻、`G81` 普通钻、`G82` 孔底暂停钻、`G83` 啄钻、`G85` 镗孔进给退刀、`G86` 镗孔停主轴快速退刀、`G89` 镗孔暂停后进给退刀,以及 `G80` 取消、`G98/G99` 返回模式和 `L` 重复次数。 +- LinuxCNC native/source 对照已扩展到 `G82` 孔底暂停和 `G83` 啄钻关键事件,覆盖孔底 dwell、啄钻深度序列和 R 平面退刀。 +- smoke parser 已有 Fanuc 风格 `M98 P... L...` / `M99` 子程序调用第一版,支持同文件 `O...` 子程序块和重复调用展开;未知子程序、裸 `M99` 和过深嵌套会返回明确错误。 +- smoke parser 已有 LinuxCNC 风格 O-word 子程序第一版,覆盖数字 `O100 ...` 和命名 `O ...` 的 `sub` / `call [args]` / `return` / `endsub`,支持简单数字实参、`#1 = ...` 编号参数赋值、`# = ...` 命名参数赋值、`X#1` / `X#` 参数引用、`+ - * /` 可嵌套方括号表达式,以及 `ABS[]`、`SQRT[]`、`EXP[]`、`LN[]`、角度制 `SIN[]` / `COS[]` / `TAN[]` / `ASIN[]` / `ACOS[]`、LinuxCNC 双参数 `ATAN[]/[]`、`FIX[]` / `FUP[]` / `ROUND[]`,并拒绝和 `M98/M99` 风格混用。 +- LinuxCNC native/source runner 已支持显式文件模式执行,O-word 子程序语料会通过 LinuxCNC 自身 `open/read/execute` 路径验证,而不是逐行模拟。 - LinuxCNC Canon bridge 会把当前解释行号补到没有自带 `lineno` 的回调事件上,方便 Web 端诊断和源码高亮。 - 下一步是继续替换源码级链接中残留的 Python、INI/HAL、文件系统和动态加载依赖,并把 RTCP 内核接入 canon 事件流和机型配置。 @@ -92,6 +97,10 @@ The first useful target is: - `M7` / `M8` / `M9` 冷却液状态回归 - `G93` / `G94` / `G95` 进给模式回归 - `G61` / `G61.1` / `G64` 运动控制模式回归 +- smoke parser `G73` / `G81` / `G82` / `G83` / `G85` / `G86` / `G89` / `G80` / `G98` / `G99` / `L` 固定循环展开回归 +- LinuxCNC native/source `G82` / `G83` 固定循环对照回归 +- smoke parser `M98 P... L...` / `M99` 子程序调用回归 +- smoke parser LinuxCNC 风格 `O... sub/call/return/endsub` 子程序调用回归 - `G10 L2` / `G10 L20` / `G92.1` 坐标系事件回归 - `G38.3` 探针事件回归 diff --git a/core/src/linuxcnc_canon_bridge.cpp b/core/src/linuxcnc_canon_bridge.cpp index b4cd7f2..1d903a1 100644 --- a/core/src/linuxcnc_canon_bridge.cpp +++ b/core/src/linuxcnc_canon_bridge.cpp @@ -439,7 +439,19 @@ void USE_NO_SPINDLE_FORCE() { void SET_TOOL_TABLE_ENTRY(int, int, const EmcPose &, double, double, double, int) { } -void USE_TOOL_LENGTH_OFFSET(const EmcPose &) { +void USE_TOOL_LENGTH_OFFSET(const EmcPose &offset) { + if (!active_sink) { + return; + } + CncSimEvent event{}; + event.version = 1; + event.type = CNC_SIM_EVENT_COMMENT; + event.line = event_line(); + event.reserved = 430; + event.start = make_pose(offset.tran.x, offset.tran.y, offset.tran.z, + offset.a, offset.b, offset.c, + offset.u, offset.v, offset.w); + active_sink->emit_raw(event); } void CHANGE_TOOL_NUMBER(int number) { diff --git a/core/src/linuxcnc_rs274_backend.cpp b/core/src/linuxcnc_rs274_backend.cpp index bf443f4..e441af8 100644 --- a/core/src/linuxcnc_rs274_backend.cpp +++ b/core/src/linuxcnc_rs274_backend.cpp @@ -15,6 +15,8 @@ #include #include #include +#include +#include int _task = 0; char _parameter_file_name[LINELEN]; @@ -88,6 +90,92 @@ std::string interp_error(InterpBase *interp, int status, const char *stage) { return result; } +bool file_mode_enabled() { + return std::getenv("CNC_SIM_RS274_FILE_MODE") != nullptr; +} + +bool write_temp_program(const char *program, + size_t program_len, + std::string *path, + std::string *error) { + char tmpl[] = "/tmp/cnc_sim_api_XXXXXX.ngc"; + const int fd = mkstemps(tmpl, 4); + if (fd < 0) { + if (error) { + *error = "failed to create temporary ngc file"; + } + return false; + } + + bool ok = true; + size_t written = 0; + while (written < program_len) { + const ssize_t rc = write(fd, program + written, program_len - written); + if (rc <= 0) { + ok = false; + break; + } + written += static_cast(rc); + } + if (close(fd) != 0) { + ok = false; + } + if (!ok) { + unlink(tmpl); + if (error) { + *error = "failed to write temporary ngc file"; + } + return false; + } + *path = tmpl; + return true; +} + +int execute_file_mode(InterpBase *interp, + CanonEventSink &sink, + const std::string &path, + std::string *error) { + int status = interp->open(path.c_str()); + if (status != INTERP_OK) { + if (error) { + *error = interp_error(interp, status, "open"); + } + return -1; + } + + bool program_done = false; + while (!program_done) { + status = interp->read(); + if (status == INTERP_EXIT || status == INTERP_ENDFILE) { + break; + } + if (!normal_read_status(status)) { + if (error) { + *error = interp_error(interp, status, "read"); + } + interp->close(); + return -1; + } + + cnc_sim_linuxcnc_set_current_line(interp->line()); + status = interp->execute(); + if (!normal_execute_status(status, &program_done)) { + if (error) { + *error = interp_error(interp, status, "execute"); + } + interp->close(); + return -1; + } + if (stop_if_callback_aborted(sink, error)) { + interp->close(); + return -1; + } + } + + interp->close(); + return 0; +} + } // namespace int parse_linuxcnc_rs274_backend(CanonEventSink &sink, @@ -119,57 +207,82 @@ int parse_linuxcnc_rs274_backend(CanonEventSink &sink, return -1; } - std::string source(program, program + program_len); - std::istringstream input(source); - std::string line; - bool program_done = false; - int line_number = 0; - while (!program_done && std::getline(input, line)) { - ++line_number; - cnc_sim_linuxcnc_set_current_line(line_number); - std::vector control_actions; - if (parse_simulator_gcode_control_line(line, &control_actions)) { - for (const auto &action : control_actions) { - emit_simulator_gcode_control_action(sink, action, line_number); - if (stop_if_callback_aborted(sink, error)) { - interp->exit(); - delete interp; - cnc_sim_linuxcnc_set_canon_sink(nullptr); - return -1; + if (file_mode_enabled()) { + std::string temp_path; + if (!write_temp_program(program, program_len, &temp_path, error)) { + interp->exit(); + delete interp; + cnc_sim_linuxcnc_set_canon_sink(nullptr); + return -1; + } + const int rc = execute_file_mode(interp, sink, temp_path, error); + unlink(temp_path.c_str()); + if (rc != 0) { + interp->exit(); + delete interp; + cnc_sim_linuxcnc_set_canon_sink(nullptr); + return -1; + } + } else { + std::string source(program, program + program_len); + std::istringstream input(source); + std::string line; + bool program_done = false; + int line_number = 0; + while (!program_done && std::getline(input, line)) { + ++line_number; + cnc_sim_linuxcnc_set_current_line(line_number); + std::vector control_actions; + if (parse_simulator_gcode_control_line(line, &control_actions)) { + bool simulator_only_line = true; + for (const auto &action : control_actions) { + emit_simulator_gcode_control_action(sink, action, line_number); + if (action.kind == SimulatorGcodeControlKind::RtcpState && + !action.rtcp_enabled) { + simulator_only_line = false; + } + if (stop_if_callback_aborted(sink, error)) { + interp->exit(); + delete interp; + cnc_sim_linuxcnc_set_canon_sink(nullptr); + return -1; + } + } + if (simulator_only_line) { + continue; } } - continue; - } - status = interp->read(line.c_str()); - if (!normal_read_status(status)) { - if (error) { - *error = interp_error(interp, status, "read"); + status = interp->read(line.c_str()); + if (!normal_read_status(status)) { + if (error) { + *error = interp_error(interp, status, "read"); + } + interp->exit(); + delete interp; + cnc_sim_linuxcnc_set_canon_sink(nullptr); + return -1; + } + if (status == INTERP_EXIT || status == INTERP_ENDFILE) { + break; } - interp->exit(); - delete interp; - cnc_sim_linuxcnc_set_canon_sink(nullptr); - return -1; - } - if (status == INTERP_EXIT || status == INTERP_ENDFILE) { - break; - } - status = interp->execute(); - if (!normal_execute_status(status, &program_done)) { - if (error) { - *error = interp_error(interp, status, "execute"); + status = interp->execute(); + if (!normal_execute_status(status, &program_done)) { + if (error) { + *error = interp_error(interp, status, "execute"); + } + interp->exit(); + delete interp; + cnc_sim_linuxcnc_set_canon_sink(nullptr); + return -1; + } + if (stop_if_callback_aborted(sink, error)) { + interp->exit(); + delete interp; + cnc_sim_linuxcnc_set_canon_sink(nullptr); + return -1; } - interp->exit(); - delete interp; - cnc_sim_linuxcnc_set_canon_sink(nullptr); - return -1; - } - if (stop_if_callback_aborted(sink, error)) { - interp->exit(); - delete interp; - cnc_sim_linuxcnc_set_canon_sink(nullptr); - return -1; } } diff --git a/core/src/smoke_gcode_parser.cpp b/core/src/smoke_gcode_parser.cpp index c04876e..7ba791a 100644 --- a/core/src/smoke_gcode_parser.cpp +++ b/core/src/smoke_gcode_parser.cpp @@ -1,6 +1,7 @@ #include "smoke_gcode_parser.h" #include +#include #include #include #include @@ -16,6 +17,61 @@ struct Word { double value; }; +struct SourceLine { + int number; + std::string raw; +}; + +struct Subprogram { + size_t header; + size_t start; + size_t end; + bool oword; +}; + +struct CallFrame { + size_t return_pc; + size_t sub_start; + int remaining; + bool oword; + std::array params; + std::unordered_map named_params; +}; + +struct ConditionalFrame { + bool branch_taken; + size_t start_pc; +}; + +struct LoopFrame { + int iterations; + int remaining; + bool repeat_initialized; +}; + +enum class OwordKind { + None, + If, + ElseIf, + Else, + EndIf, + Do, + While, + EndWhile, + Repeat, + EndRepeat, + Break, + Continue, + Call, + Return, + Sub, + EndSub +}; + +constexpr size_t kMaxSubprogramDepth = 16; +constexpr int kMaxOwordLoopIterations = 100000; +constexpr int kMaxNumberedParameter = 5602; + std::string trim(const std::string &value) { auto first = std::find_if_not(value.begin(), value.end(), [](unsigned char ch) { return std::isspace(ch); }); auto last = std::find_if_not(value.rbegin(), value.rend(), [](unsigned char ch) { return std::isspace(ch); }).base(); @@ -92,6 +148,1052 @@ std::unordered_map last_words_by_letter(const std::vector &w return words; } +int rounded_value(double value) { + return static_cast(std::lround(value)); +} + +std::vector split_source_lines(const std::string &source) { + std::vector lines; + std::istringstream input(source); + std::string raw_line; + int line_number = 0; + while (std::getline(input, raw_line)) { + ++line_number; + lines.push_back({line_number, raw_line}); + } + return lines; +} + +bool first_o_number(const std::vector &word_list, int *number) { + for (const Word &word : word_list) { + if (word.letter == 'O') { + *number = rounded_value(word.value); + return true; + } + } + return false; +} + +std::string numeric_o_identifier(int number) { + return "N:" + std::to_string(number); +} + +std::string first_o_identifier(const std::string &line, const std::vector &word_list) { + for (size_t i = 0; i < line.size(); ++i) { + if (line[i] != 'O') { + continue; + } + size_t pos = i + 1; + while (pos < line.size() && std::isspace(static_cast(line[pos]))) { + ++pos; + } + if (pos >= line.size() || line[pos] != '<') { + continue; + } + const size_t close = line.find('>', pos + 1); + if (close == std::string::npos) { + continue; + } + const std::string name = trim(line.substr(pos + 1, close - pos - 1)); + if (!name.empty()) { + return "S:" + name; + } + } + + int number = 0; + if (first_o_number(word_list, &number)) { + return numeric_o_identifier(number); + } + return {}; +} + +bool has_m_code(const std::vector &word_list, int code) { + for (const Word &word : word_list) { + if (word.letter == 'M' && rounded_value(word.value) == code) { + return true; + } + } + return false; +} + +std::string lower_compact(const std::string &value) { + std::string out; + out.reserve(value.size()); + for (char ch : value) { + if (!std::isspace(static_cast(ch))) { + out.push_back(static_cast(std::tolower(static_cast(ch)))); + } + } + return out; +} + +bool has_o_code_keyword(const std::string &line, const char *keyword) { + return lower_compact(line).find(keyword) != std::string::npos; +} + +OwordKind oword_kind(const std::string &line) { + for (size_t i = 0; i < line.size(); ++i) { + if (line[i] != 'O') { + continue; + } + size_t pos = i + 1; + while (pos < line.size() && std::isspace(static_cast(line[pos]))) { + ++pos; + } + if (pos < line.size() && line[pos] == '<') { + const size_t close = line.find('>', pos + 1); + if (close == std::string::npos) { + return OwordKind::None; + } + pos = close + 1; + } else { + bool saw_digit = false; + while (pos < line.size() && std::isdigit(static_cast(line[pos]))) { + saw_digit = true; + ++pos; + } + if (!saw_digit) { + return OwordKind::None; + } + } + while (pos < line.size() && std::isspace(static_cast(line[pos]))) { + ++pos; + } + const size_t start = pos; + while (pos < line.size() && std::isalpha(static_cast(line[pos]))) { + ++pos; + } + const std::string keyword = lower_compact(line.substr(start, pos - start)); + if (keyword == "if") { + return OwordKind::If; + } + if (keyword == "elseif") { + return OwordKind::ElseIf; + } + if (keyword == "else") { + return OwordKind::Else; + } + if (keyword == "endif") { + return OwordKind::EndIf; + } + if (keyword == "do") { + return OwordKind::Do; + } + if (keyword == "while") { + return OwordKind::While; + } + if (keyword == "endwhile") { + return OwordKind::EndWhile; + } + if (keyword == "repeat") { + return OwordKind::Repeat; + } + if (keyword == "endrepeat") { + return OwordKind::EndRepeat; + } + if (keyword == "break") { + return OwordKind::Break; + } + if (keyword == "continue") { + return OwordKind::Continue; + } + if (keyword == "call") { + return OwordKind::Call; + } + if (keyword == "return") { + return OwordKind::Return; + } + if (keyword == "sub") { + return OwordKind::Sub; + } + if (keyword == "endsub") { + return OwordKind::EndSub; + } + return OwordKind::None; + } + return OwordKind::None; +} + +double parameter_value(const std::vector &call_stack, + const std::unordered_map ¶meters, + int index) { + if (index > 0 && index <= 30 && !call_stack.empty() && call_stack.back().oword) { + return call_stack.back().params[static_cast(index)]; + } + const auto it = parameters.find(index); + if (it != parameters.end()) { + return it->second; + } + return 0.0; +} + +double named_parameter_value(const std::vector &call_stack, + const std::unordered_map &named_parameters, + const std::string &name) { + if (!call_stack.empty() && call_stack.back().oword) { + const auto local = call_stack.back().named_params.find(name); + if (local != call_stack.back().named_params.end()) { + return local->second; + } + } + const auto global = named_parameters.find(name); + if (global != named_parameters.end()) { + return global->second; + } + return 0.0; +} + +size_t find_matching_square_bracket(const std::string &text, size_t open) { + if (open >= text.size() || text[open] != '[') { + return std::string::npos; + } + int depth = 0; + for (size_t i = open; i < text.size(); ++i) { + if (text[i] == '[') { + ++depth; + } else if (text[i] == ']') { + --depth; + if (depth == 0) { + return i; + } + } + } + return std::string::npos; +} + +double degrees_to_radians(double degrees) { + static const double pi = std::acos(-1.0); + return degrees * pi / 180.0; +} + +double radians_to_degrees(double radians) { + static const double pi = std::acos(-1.0); + return radians * 180.0 / pi; +} + +double round_away_from_zero(double value) { + return static_cast(static_cast(value + (value < 0.0 ? -0.5 : 0.5))); +} + +bool truthy(double value) { + return value != 0.0; +} + +class ExpressionParser { +public: + ExpressionParser(const std::string &text, + const std::vector &call_stack, + const std::unordered_map ¶meters, + const std::unordered_map &named_parameters) + : text_(text), call_stack_(call_stack), parameters_(parameters), named_parameters_(named_parameters) { + } + + bool parse(double *value) { + *value = parse_logical(); + skip_space(); + return ok_ && pos_ == text_.size(); + } + +private: + double parse_logical() { + double value = parse_comparison(); + while (ok_) { + skip_space(); + if (consume_keyword("AND")) { + const double rhs = parse_comparison(); + value = truthy(value) && truthy(rhs) ? 1.0 : 0.0; + } else if (consume_keyword("XOR")) { + const double rhs = parse_comparison(); + value = truthy(value) != truthy(rhs) ? 1.0 : 0.0; + } else if (consume_keyword("OR")) { + const double rhs = parse_comparison(); + value = truthy(value) || truthy(rhs) ? 1.0 : 0.0; + } else { + break; + } + } + return value; + } + + double parse_comparison() { + constexpr double kToleranceEqual = 1e-6; + double value = parse_additive(); + while (ok_) { + skip_space(); + if (consume_keyword("EQ")) { + const double rhs = parse_additive(); + value = std::fabs(value - rhs) < kToleranceEqual ? 1.0 : 0.0; + } else if (consume_keyword("NE")) { + const double rhs = parse_additive(); + value = std::fabs(value - rhs) >= kToleranceEqual ? 1.0 : 0.0; + } else if (consume_keyword("LE")) { + const double rhs = parse_additive(); + const double diff = std::fabs(value - rhs); + value = (diff < kToleranceEqual || value <= rhs) ? 1.0 : 0.0; + } else if (consume_keyword("GE")) { + const double rhs = parse_additive(); + const double diff = std::fabs(value - rhs); + value = (diff < kToleranceEqual || value >= rhs) ? 1.0 : 0.0; + } else if (consume_keyword("LT")) { + value = value < parse_additive() ? 1.0 : 0.0; + } else if (consume_keyword("GT")) { + value = value > parse_additive() ? 1.0 : 0.0; + } else { + break; + } + } + return value; + } + + double parse_additive() { + double value = parse_multiplicative(); + while (ok_) { + skip_space(); + if (consume('+')) { + value += parse_multiplicative(); + } else if (consume('-')) { + value -= parse_multiplicative(); + } else { + break; + } + } + return value; + } + + double parse_multiplicative() { + double value = parse_power(); + while (ok_) { + skip_space(); + if (consume_keyword("MOD")) { + const double rhs = parse_power(); + if (rhs == 0.0) { + ok_ = false; + return 0.0; + } + value = std::fmod(value, rhs); + if (value < 0.0) { + value += std::fabs(rhs); + } + } else if (consume('*')) { + value *= parse_power(); + } else if (consume('/')) { + const double rhs = parse_power(); + if (rhs == 0.0) { + ok_ = false; + return 0.0; + } + value /= rhs; + } else { + break; + } + } + return value; + } + + double parse_power() { + double value = parse_unary(); + while (ok_) { + skip_space(); + if (!consume("**")) { + break; + } + const double rhs = parse_unary(); + if (value < 0.0 && std::floor(rhs) != rhs) { + ok_ = false; + return 0.0; + } + value = std::pow(value, rhs); + if (!std::isfinite(value)) { + ok_ = false; + return 0.0; + } + } + return value; + } + + double parse_unary() { + skip_space(); + if (consume('+')) { + return parse_unary(); + } + if (consume('-')) { + return -parse_unary(); + } + return parse_primary(); + } + + double parse_primary() { + skip_space(); + if (consume('(')) { + const double value = parse_logical(); + if (!consume(')')) { + ok_ = false; + } + return value; + } + if (consume('[')) { + const double value = parse_logical(); + if (!consume(']')) { + ok_ = false; + } + return value; + } + if (consume('#')) { + if (consume('<')) { + const size_t start = pos_; + while (pos_ < text_.size() && text_[pos_] != '>') { + ++pos_; + } + if (pos_ >= text_.size() || text_[pos_] != '>') { + ok_ = false; + return 0.0; + } + const std::string name = trim(text_.substr(start, pos_ - start)); + ++pos_; + if (name.empty()) { + ok_ = false; + return 0.0; + } + return named_parameter_value(call_stack_, named_parameters_, name); + } + const int index = parse_integer(); + if (index <= 0 || index >= kMaxNumberedParameter) { + ok_ = false; + return 0.0; + } + return parameter_value(call_stack_, parameters_, index); + } + if (pos_ < text_.size() && std::isalpha(static_cast(text_[pos_]))) { + const std::string name = parse_identifier(); + const double argument = parse_primary(); + if (!ok_) { + return 0.0; + } + if (name == "ABS") { + return std::fabs(argument); + } + if (name == "ACOS") { + if (argument < -1.0 || argument > 1.0) { + ok_ = false; + return 0.0; + } + return radians_to_degrees(std::acos(argument)); + } + if (name == "ASIN") { + if (argument < -1.0 || argument > 1.0) { + ok_ = false; + return 0.0; + } + return radians_to_degrees(std::asin(argument)); + } + if (name == "ATAN") { + if (!consume('/') || !consume('[')) { + ok_ = false; + return 0.0; + } + const double argument2 = parse_logical(); + if (!consume(']')) { + ok_ = false; + return 0.0; + } + return radians_to_degrees(std::atan2(argument, argument2)); + } + if (name == "SQRT") { + return std::sqrt(argument); + } + if (name == "EXP") { + return std::exp(argument); + } + if (name == "LN") { + if (argument <= 0.0) { + ok_ = false; + return 0.0; + } + return std::log(argument); + } + if (name == "FIX") { + return std::floor(argument); + } + if (name == "FUP") { + return std::ceil(argument); + } + if (name == "ROUND") { + return round_away_from_zero(argument); + } + if (name == "SIN") { + return std::sin(degrees_to_radians(argument)); + } + if (name == "COS") { + return std::cos(degrees_to_radians(argument)); + } + if (name == "TAN") { + return std::tan(degrees_to_radians(argument)); + } + ok_ = false; + return 0.0; + } + + char *end = nullptr; + const double value = std::strtod(text_.c_str() + pos_, &end); + if (end == text_.c_str() + pos_) { + ok_ = false; + return 0.0; + } + pos_ = static_cast(end - text_.c_str()); + return value; + } + + std::string parse_identifier() { + skip_space(); + const size_t start = pos_; + while (pos_ < text_.size() && std::isalpha(static_cast(text_[pos_]))) { + ++pos_; + } + return text_.substr(start, pos_ - start); + } + + int parse_integer() { + skip_space(); + int value = 0; + bool saw_digit = false; + while (pos_ < text_.size() && std::isdigit(static_cast(text_[pos_]))) { + saw_digit = true; + value = value * 10 + (text_[pos_] - '0'); + ++pos_; + } + if (!saw_digit) { + ok_ = false; + } + return value; + } + + bool consume(char ch) { + skip_space(); + if (pos_ < text_.size() && text_[pos_] == ch) { + ++pos_; + return true; + } + return false; + } + + bool consume(const char *token) { + skip_space(); + const size_t start = pos_; + for (size_t i = 0; token[i] != '\0'; ++i) { + if (start + i >= text_.size() || text_[start + i] != token[i]) { + return false; + } + } + pos_ = start + std::char_traits::length(token); + return true; + } + + bool consume_keyword(const char *keyword) { + skip_space(); + const size_t start = pos_; + for (size_t i = 0; keyword[i] != '\0'; ++i) { + if (start + i >= text_.size() || + std::toupper(static_cast(text_[start + i])) != keyword[i]) { + return false; + } + } + const size_t end = start + std::char_traits::length(keyword); + if (end < text_.size() && std::isalpha(static_cast(text_[end]))) { + return false; + } + pos_ = end; + return true; + } + + void skip_space() { + while (pos_ < text_.size() && std::isspace(static_cast(text_[pos_]))) { + ++pos_; + } + } + + const std::string &text_; + const std::vector &call_stack_; + const std::unordered_map ¶meters_; + const std::unordered_map &named_parameters_; + size_t pos_ = 0; + bool ok_ = true; +}; + +bool evaluate_expression(const std::string &text, + const std::vector &call_stack, + const std::unordered_map ¶meters, + const std::unordered_map &named_parameters, + double *value) { + ExpressionParser parser(text, call_stack, parameters, named_parameters); + return parser.parse(value); +} + +bool evaluate_oword_expression(const std::string &line, + const std::vector &call_stack, + const std::unordered_map ¶meters, + const std::unordered_map &named_parameters, + double *result, + std::string *error) { + const size_t open = line.find('['); + if (open == std::string::npos) { + if (error) { + *error = "O-word expression requires a bracket expression"; + } + return false; + } + const size_t close = find_matching_square_bracket(line, open); + if (close == std::string::npos) { + if (error) { + *error = "unterminated O-word expression"; + } + return false; + } + double value = 0.0; + if (!evaluate_expression(line.substr(open + 1, close - open - 1), + call_stack, + parameters, + named_parameters, + &value)) { + if (error) { + *error = "unsupported O-word expression"; + } + return false; + } + *result = value; + return true; +} + +bool evaluate_oword_condition(const std::string &line, + const std::vector &call_stack, + const std::unordered_map ¶meters, + const std::unordered_map &named_parameters, + bool *result, + std::string *error) { + double value = 0.0; + if (!evaluate_oword_expression(line, call_stack, parameters, named_parameters, &value, error)) { + return false; + } + *result = truthy(value); + return true; +} + +size_t find_next_conditional_branch(const std::vector &lines, size_t start) { + int nested = 0; + for (size_t i = start; i < lines.size(); ++i) { + const std::string stripped = strip_comments(lines[i].raw, nullptr); + const OwordKind kind = oword_kind(stripped); + if (kind == OwordKind::If) { + ++nested; + } else if (kind == OwordKind::EndIf) { + if (nested == 0) { + return i; + } + --nested; + } else if (nested == 0 && (kind == OwordKind::ElseIf || kind == OwordKind::Else)) { + return i; + } + } + return lines.size(); +} + +size_t find_matching_endif(const std::vector &lines, size_t start) { + int nested = 0; + for (size_t i = start; i < lines.size(); ++i) { + const std::string stripped = strip_comments(lines[i].raw, nullptr); + const OwordKind kind = oword_kind(stripped); + if (kind == OwordKind::If) { + ++nested; + } else if (kind == OwordKind::EndIf) { + if (nested == 0) { + return i; + } + --nested; + } + } + return lines.size(); +} + +std::string oword_identifier_for_line(const std::string &line) { + return first_o_identifier(line, parse_word_list(line)); +} + +bool is_loop_start(OwordKind kind) { + return kind == OwordKind::Do || kind == OwordKind::While || kind == OwordKind::Repeat; +} + +bool is_loop_end(OwordKind kind) { + return kind == OwordKind::EndWhile || kind == OwordKind::EndRepeat; +} + +OwordKind matching_loop_end_kind(OwordKind kind) { + if (kind == OwordKind::While) { + return OwordKind::EndWhile; + } + if (kind == OwordKind::Repeat) { + return OwordKind::EndRepeat; + } + return OwordKind::None; +} + +OwordKind matching_loop_start_kind(OwordKind kind) { + if (kind == OwordKind::EndWhile) { + return OwordKind::While; + } + if (kind == OwordKind::EndRepeat) { + return OwordKind::Repeat; + } + return OwordKind::None; +} + +size_t find_matching_loop_end(const std::vector &lines, + size_t start, + const std::string &identifier, + OwordKind start_kind) { + const OwordKind end_kind = matching_loop_end_kind(start_kind); + std::vector nested; + for (size_t i = start; i < lines.size(); ++i) { + const std::string stripped = strip_comments(lines[i].raw, nullptr); + const OwordKind kind = oword_kind(stripped); + if (kind == OwordKind::Do) { + nested.push_back(kind); + } else if (kind == OwordKind::While) { + if (!nested.empty() && nested.back() == OwordKind::Do) { + nested.pop_back(); + } else { + nested.push_back(kind); + } + } else if (kind == OwordKind::Repeat) { + nested.push_back(kind); + } else if (kind == OwordKind::EndWhile || kind == OwordKind::EndRepeat) { + if (nested.empty()) { + return kind == end_kind && oword_identifier_for_line(stripped) == identifier ? i : lines.size(); + } + if ((kind == OwordKind::EndWhile && nested.back() == OwordKind::While) || + (kind == OwordKind::EndRepeat && nested.back() == OwordKind::Repeat)) { + nested.pop_back(); + } + } + } + return lines.size(); +} + +size_t find_matching_loop_start(const std::vector &lines, + size_t loop_end_index, + const std::string &identifier, + OwordKind end_kind) { + const OwordKind start_kind = matching_loop_start_kind(end_kind); + int nested = 0; + for (size_t i = loop_end_index; i > 0; --i) { + const size_t candidate = i - 1; + const std::string stripped = strip_comments(lines[candidate].raw, nullptr); + const OwordKind kind = oword_kind(stripped); + if (is_loop_end(kind)) { + ++nested; + } else if (is_loop_start(kind)) { + if (nested == 0) { + return kind == start_kind && oword_identifier_for_line(stripped) == identifier ? candidate : lines.size(); + } + --nested; + } + } + return lines.size(); +} + +size_t find_matching_do_while(const std::vector &lines, + size_t start, + const std::string &identifier) { + std::vector nested; + for (size_t i = start; i < lines.size(); ++i) { + const std::string stripped = strip_comments(lines[i].raw, nullptr); + const OwordKind kind = oword_kind(stripped); + if (kind == OwordKind::Do) { + nested.push_back(kind); + } else if (kind == OwordKind::While) { + if (nested.empty()) { + return oword_identifier_for_line(stripped) == identifier ? i : lines.size(); + } + if (nested.back() == OwordKind::Do) { + nested.pop_back(); + } else { + nested.push_back(kind); + } + } else if (kind == OwordKind::Repeat) { + nested.push_back(kind); + } else if (kind == OwordKind::EndWhile || kind == OwordKind::EndRepeat) { + if (!nested.empty() && + ((kind == OwordKind::EndWhile && nested.back() == OwordKind::While) || + (kind == OwordKind::EndRepeat && nested.back() == OwordKind::Repeat))) { + nested.pop_back(); + } + } + } + return lines.size(); +} + +size_t find_matching_do_start(const std::vector &lines, + size_t while_index, + const std::string &identifier) { + for (size_t i = 0; i < while_index; ++i) { + const std::string stripped = strip_comments(lines[i].raw, nullptr); + if (oword_kind(stripped) == OwordKind::Do && + oword_identifier_for_line(stripped) == identifier && + find_matching_do_while(lines, i + 1, identifier) == while_index) { + return i; + } + } + return lines.size(); +} + +size_t find_matching_endwhile(const std::vector &lines, + size_t start, + const std::string &identifier) { + return find_matching_loop_end(lines, start, identifier, OwordKind::While); +} + +size_t find_matching_while(const std::vector &lines, + size_t endwhile_index, + const std::string &identifier) { + return find_matching_loop_start(lines, endwhile_index, identifier, OwordKind::EndWhile); +} + +std::string number_to_string(double value) { + std::ostringstream out; + out.precision(17); + out << value; + return out.str(); +} + +std::string expand_expressions_and_parameters(const std::string &line, + const std::vector &call_stack, + const std::unordered_map ¶meters, + const std::unordered_map &named_parameters, + std::string *error) { + std::string out; + out.reserve(line.size()); + for (size_t i = 0; i < line.size();) { + if (line[i] == '[') { + const size_t close = find_matching_square_bracket(line, i); + if (close == std::string::npos) { + if (error) { + *error = "unterminated bracket expression"; + } + return {}; + } + double value = 0.0; + if (!evaluate_expression(line.substr(i + 1, close - i - 1), call_stack, parameters, named_parameters, &value)) { + if (error) { + *error = "unsupported bracket expression"; + } + return {}; + } + out += number_to_string(value); + i = close + 1; + continue; + } + + if (line[i] != '#') { + out.push_back(line[i]); + ++i; + continue; + } + + size_t j = i + 1; + if (j < line.size() && line[j] == '<') { + const size_t close = line.find('>', j + 1); + if (close == std::string::npos) { + if (error) { + *error = "unterminated named parameter expression"; + } + return {}; + } + const std::string name = trim(line.substr(j + 1, close - j - 1)); + if (name.empty()) { + if (error) { + *error = "empty named parameter expression"; + } + return {}; + } + out += number_to_string(named_parameter_value(call_stack, named_parameters, name)); + i = close + 1; + continue; + } + if (j >= line.size() || !std::isdigit(static_cast(line[j]))) { + if (error) { + *error = "unsupported parameter expression"; + } + return {}; + } + while (j < line.size() && std::isdigit(static_cast(line[j]))) { + ++j; + } + const int index = std::atoi(line.c_str() + i + 1); + if (index <= 0 || index >= kMaxNumberedParameter) { + if (error) { + *error = "numbered parameter is out of range"; + } + return {}; + } + out += number_to_string(parameter_value(call_stack, parameters, index)); + i = j; + } + return out; +} + +std::vector parse_oword_call_arguments(const std::string &line, + const std::vector &call_stack, + const std::unordered_map ¶meters, + const std::unordered_map &named_parameters, + std::string *error) { + std::vector args; + for (size_t i = 0; i < line.size();) { + if (line[i] != '[') { + ++i; + continue; + } + const size_t close = find_matching_square_bracket(line, i); + if (close == std::string::npos) { + break; + } + const std::string text = trim(line.substr(i + 1, close - i - 1)); + if (!text.empty()) { + double value = 0.0; + if (!evaluate_expression(text, call_stack, parameters, named_parameters, &value)) { + if (error) { + *error = "unsupported O-word call argument expression"; + } + return {}; + } + args.push_back(value); + } + i = close + 1; + } + return args; +} + +bool parse_parameter_assignment(const std::string &line, + const std::vector &call_stack, + const std::unordered_map ¶meters, + const std::unordered_map &named_parameters, + int *index, + std::string *name, + double *value, + std::string *error) { + const std::string text = trim(line); + if (text.empty() || text[0] != '#') { + return false; + } + + size_t pos = 1; + std::string parsed_name; + if (pos < text.size() && text[pos] == '<') { + const size_t close = text.find('>', pos + 1); + if (close == std::string::npos) { + if (error) { + *error = "unterminated named parameter assignment"; + } + return true; + } + parsed_name = trim(text.substr(pos + 1, close - pos - 1)); + if (parsed_name.empty()) { + if (error) { + *error = "empty named parameter assignment"; + } + return true; + } + pos = close + 1; + } + + if (pos >= text.size() || !std::isdigit(static_cast(text[pos]))) { + if (parsed_name.empty()) { + return false; + } + } + int parsed_index = 0; + if (parsed_name.empty()) { + while (pos < text.size() && std::isdigit(static_cast(text[pos]))) { + parsed_index = parsed_index * 10 + (text[pos] - '0'); + ++pos; + } + if (parsed_index <= 0 || parsed_index >= kMaxNumberedParameter) { + if (error) { + *error = "numbered parameter assignment is out of range"; + } + return true; + } + } + while (pos < text.size() && std::isspace(static_cast(text[pos]))) { + ++pos; + } + if (pos >= text.size() || text[pos] != '=') { + return false; + } + ++pos; + std::string rhs = trim(text.substr(pos)); + if (rhs.empty()) { + if (error) { + *error = "parameter assignment requires a value"; + } + return true; + } + if (rhs.front() == '[' && rhs.back() == ']') { + rhs = rhs.substr(1, rhs.size() - 2); + } + + double parsed_value = 0.0; + if (!evaluate_expression(rhs, call_stack, parameters, named_parameters, &parsed_value)) { + if (error) { + *error = "unsupported parameter assignment expression"; + } + return true; + } + *index = parsed_index; + *name = parsed_name; + *value = parsed_value; + return true; +} + +std::unordered_map discover_subprograms(const std::vector &lines, + std::unordered_map *by_header) { + std::unordered_map subprograms; + for (size_t i = 0; i < lines.size(); ++i) { + const std::string stripped = strip_comments(lines[i].raw, nullptr); + const auto words = parse_word_list(stripped); + const std::string id = first_o_identifier(stripped, words); + if (id.empty()) { + continue; + } + + if (has_o_code_keyword(stripped, "sub")) { + for (size_t j = i + 1; j < lines.size(); ++j) { + const std::string end_line = strip_comments(lines[j].raw, nullptr); + const auto end_words = parse_word_list(end_line); + const std::string end_id = first_o_identifier(end_line, end_words); + if (end_id == id && + has_o_code_keyword(end_line, "endsub")) { + Subprogram subprogram{i, i + 1, j, true}; + subprograms[id] = subprogram; + (*by_header)[i] = subprogram; + i = j; + break; + } + } + continue; + } + + for (size_t j = i + 1; j < lines.size(); ++j) { + const std::string end_line = strip_comments(lines[j].raw, nullptr); + if (has_m_code(parse_word_list(end_line), 99)) { + Subprogram subprogram{i, i + 1, j, false}; + subprograms[id] = subprogram; + (*by_header)[i] = subprogram; + i = j; + break; + } + } + } + return subprograms; +} + bool has_axis_word(const std::unordered_map &words) { static constexpr char axes[] = {'X', 'Y', 'Z', 'A', 'B', 'C', 'U', 'V', 'W'}; for (char axis : axes) { @@ -102,6 +1204,28 @@ bool has_axis_word(const std::unordered_map &words) { return false; } +bool has_cycle_position_word(const std::unordered_map &words) { + static constexpr char axes[] = {'X', 'Y', 'A', 'B', 'C', 'U', 'V', 'W'}; + for (char axis : axes) { + if (words.find(axis) != words.end()) { + return true; + } + } + return false; +} + +bool has_rotary_word(const std::unordered_map &words) { + return words.find('A') != words.end() || + words.find('B') != words.end() || + words.find('C') != words.end(); +} + +bool has_uvw_word(const std::unordered_map &words) { + return words.find('U') != words.end() || + words.find('V') != words.end() || + words.find('W') != words.end(); +} + void apply_axis(CncSimPose *pose, const std::unordered_map &words, char axis, double CncSimPose::*member, bool absolute, double scale) { auto it = words.find(axis); if (it == words.end()) { @@ -127,6 +1251,14 @@ void apply_axes(CncSimPose *pose, const std::unordered_map &words, apply_axis(pose, words, 'W', &CncSimPose::w, absolute, scale); } +void apply_cycle_position_axes(CncSimPose *pose, const std::unordered_map &words, bool absolute, double scale) { + apply_axis(pose, words, 'X', &CncSimPose::x, absolute, scale); + apply_axis(pose, words, 'Y', &CncSimPose::y, absolute, scale); + apply_axis(pose, words, 'U', &CncSimPose::u, absolute, scale); + apply_axis(pose, words, 'V', &CncSimPose::v, absolute, scale); + apply_axis(pose, words, 'W', &CncSimPose::w, absolute, scale); +} + void set_arc_center(CncSimEvent *event, const std::unordered_map &words, int plane, double scale) { event->center = event->start; if (words.count('R')) { @@ -204,8 +1336,16 @@ int rounded_word(const std::unordered_map &words, char letter, int return static_cast(std::lround(it->second)); } -int rounded_value(double value) { - return static_cast(std::lround(value)); +int positive_integer_word(const std::unordered_map &words, char letter, int fallback) { + auto it = words.find(letter); + if (it == words.end()) { + return fallback; + } + const double rounded = std::round(it->second); + if (std::fabs(it->second - rounded) > 0.0001 || rounded < 1.0) { + return 0; + } + return static_cast(rounded); } bool g_code_is(double actual, double expected) { @@ -252,15 +1392,36 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string sink_.reset(); sink_.clear_callback_status(); absolute_ = true; + canned_cycle_ = 0; + canned_return_to_initial_ = false; + canned_has_r_ = false; + canned_has_z_ = false; + canned_has_p_ = false; + canned_has_q_ = false; + canned_has_d_ = false; + canned_r_ = 0.0; + canned_z_ = 0.0; + canned_p_ = 0.0; + canned_q_ = 0.0; + canned_d_ = 1.0; - std::string source(program, program + program_len); - std::istringstream input(source); - std::string raw_line; - int line_number = 0; + const std::string source(program, program + program_len); + const std::vector lines = split_source_lines(source); + std::unordered_map subprograms_by_header; + const std::unordered_map subprograms = discover_subprograms(lines, &subprograms_by_header); + std::vector call_stack; + std::unordered_map parameters; + std::unordered_map named_parameters; + std::vector conditional_stack; + std::unordered_map loop_iterations; + size_t pc = 0; int modal_motion = -1; - while (std::getline(input, raw_line)) { - ++line_number; + while (pc < lines.size()) { + const size_t current_pc = pc; + ++pc; + const std::string &raw_line = lines[current_pc].raw; + const int line_number = lines[current_pc].number; std::string comment; std::string line = strip_comments(raw_line, &comment); if (!comment.empty()) { @@ -274,13 +1435,329 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string } } + auto subprogram_header = subprograms_by_header.find(current_pc); + if (subprogram_header != subprograms_by_header.end()) { + pc = subprogram_header->second.end + 1; + continue; + } + + std::string parameter_error; + int assigned_parameter = 0; + std::string assigned_named_parameter; + double assigned_value = 0.0; + if (parse_parameter_assignment(line, + call_stack, + parameters, + named_parameters, + &assigned_parameter, + &assigned_named_parameter, + &assigned_value, + ¶meter_error)) { + if (!parameter_error.empty()) { + if (error) { + *error = parameter_error; + } + return -1; + } + if (!assigned_named_parameter.empty()) { + if (!call_stack.empty() && call_stack.back().oword && assigned_named_parameter[0] != '_') { + call_stack.back().named_params[assigned_named_parameter] = assigned_value; + } else { + named_parameters[assigned_named_parameter] = assigned_value; + } + } else if (assigned_parameter > 0 && assigned_parameter <= 30 && !call_stack.empty() && call_stack.back().oword) { + call_stack.back().params[static_cast(assigned_parameter)] = assigned_value; + } else { + parameters[assigned_parameter] = assigned_value; + } + continue; + } + + const std::string expanded_line = expand_expressions_and_parameters(line, call_stack, parameters, named_parameters, ¶meter_error); + if (!parameter_error.empty()) { + if (error) { + *error = parameter_error; + } + return -1; + } + const std::string keyword_line = line; + line = expanded_line; auto word_list = parse_word_list(line); - if (word_list.empty()) { + const std::string o_identifier = first_o_identifier(keyword_line, word_list); + const bool has_o_word = !o_identifier.empty(); + if (word_list.empty() && !has_o_word) { continue; } auto words = last_words_by_letter(word_list); std::vector m_codes; + const int previous_canned_cycle = canned_cycle_; + const OwordKind current_oword_kind = has_o_word ? oword_kind(keyword_line) : OwordKind::None; + const bool oword_call = current_oword_kind == OwordKind::Call; + const bool oword_return = current_oword_kind == OwordKind::Return; + const bool oword_endsub = current_oword_kind == OwordKind::EndSub; + + if (current_oword_kind == OwordKind::If || current_oword_kind == OwordKind::ElseIf) { + if (current_oword_kind == OwordKind::ElseIf) { + if (conditional_stack.empty()) { + if (error) { + *error = "O-word elseif without active if"; + } + return -1; + } + if (conditional_stack.back().branch_taken) { + const size_t endif = find_matching_endif(lines, pc); + if (endif == lines.size()) { + if (error) { + *error = "O-word elseif without matching endif"; + } + return -1; + } + conditional_stack.pop_back(); + pc = endif + 1; + continue; + } + } + bool condition = false; + if (!evaluate_oword_condition(keyword_line, + call_stack, + parameters, + named_parameters, + &condition, + ¶meter_error)) { + if (error) { + *error = parameter_error; + } + return -1; + } + if (current_oword_kind == OwordKind::If) { + conditional_stack.push_back({condition, current_pc}); + } else if (condition) { + conditional_stack.back().branch_taken = true; + } + if (!condition) { + pc = find_next_conditional_branch(lines, pc); + if (pc == lines.size()) { + if (error) { + *error = "O-word conditional without matching endif"; + } + return -1; + } + } + continue; + } + if (current_oword_kind == OwordKind::Else) { + if (conditional_stack.empty()) { + if (error) { + *error = "O-word else without active if"; + } + return -1; + } + if (conditional_stack.back().branch_taken) { + const size_t endif = find_matching_endif(lines, pc); + if (endif == lines.size()) { + if (error) { + *error = "O-word else without matching endif"; + } + return -1; + } + conditional_stack.pop_back(); + pc = endif + 1; + } else { + conditional_stack.back().branch_taken = true; + } + continue; + } + if (current_oword_kind == OwordKind::EndIf) { + if (conditional_stack.empty()) { + if (error) { + *error = "O-word endif without active if"; + } + return -1; + } + conditional_stack.pop_back(); + continue; + } + if (current_oword_kind == OwordKind::Do) { + const size_t while_index = find_matching_do_while(lines, pc, o_identifier); + if (while_index == lines.size()) { + if (error) { + *error = "O-word do without matching while"; + } + return -1; + } + LoopFrame &loop = loop_iterations[current_pc]; + ++loop.iterations; + if (loop.iterations > kMaxOwordLoopIterations) { + if (error) { + *error = "O-word do loop iteration limit exceeded"; + } + return -1; + } + continue; + } + if (current_oword_kind == OwordKind::While) { + const size_t do_index = find_matching_do_start(lines, current_pc, o_identifier); + if (do_index != lines.size()) { + bool condition = false; + if (!evaluate_oword_condition(keyword_line, + call_stack, + parameters, + named_parameters, + &condition, + ¶meter_error)) { + if (error) { + *error = parameter_error; + } + return -1; + } + if (condition) { + pc = do_index; + } else { + loop_iterations.erase(do_index); + } + continue; + } + const size_t endwhile = find_matching_loop_end(lines, pc, o_identifier, OwordKind::While); + if (endwhile == lines.size()) { + if (error) { + *error = "O-word while without matching endwhile"; + } + return -1; + } + bool condition = false; + if (!evaluate_oword_condition(keyword_line, + call_stack, + parameters, + named_parameters, + &condition, + ¶meter_error)) { + if (error) { + *error = parameter_error; + } + return -1; + } + if (!condition) { + loop_iterations.erase(current_pc); + pc = endwhile + 1; + continue; + } + LoopFrame &loop = loop_iterations[current_pc]; + ++loop.iterations; + if (loop.iterations > kMaxOwordLoopIterations) { + if (error) { + *error = "O-word while loop iteration limit exceeded"; + } + return -1; + } + continue; + } + if (current_oword_kind == OwordKind::Repeat) { + const size_t endrepeat = find_matching_loop_end(lines, pc, o_identifier, OwordKind::Repeat); + if (endrepeat == lines.size()) { + if (error) { + *error = "O-word repeat without matching endrepeat"; + } + return -1; + } + LoopFrame &loop = loop_iterations[current_pc]; + if (!loop.repeat_initialized) { + double repeat_value = 0.0; + if (!evaluate_oword_expression(keyword_line, + call_stack, + parameters, + named_parameters, + &repeat_value, + ¶meter_error)) { + if (error) { + *error = parameter_error; + } + return -1; + } + loop.remaining = static_cast(std::nearbyint(repeat_value)); + loop.repeat_initialized = true; + } + if (loop.remaining <= 0) { + loop_iterations.erase(current_pc); + pc = endrepeat + 1; + continue; + } + --loop.remaining; + ++loop.iterations; + if (loop.iterations > kMaxOwordLoopIterations) { + if (error) { + *error = "O-word repeat loop iteration limit exceeded"; + } + return -1; + } + continue; + } + if (current_oword_kind == OwordKind::EndWhile) { + const size_t while_index = find_matching_loop_start(lines, current_pc, o_identifier, OwordKind::EndWhile); + if (while_index == lines.size()) { + if (error) { + *error = "O-word endwhile without matching while"; + } + return -1; + } + pc = while_index; + continue; + } + if (current_oword_kind == OwordKind::EndRepeat) { + const size_t repeat_index = find_matching_loop_start(lines, current_pc, o_identifier, OwordKind::EndRepeat); + if (repeat_index == lines.size()) { + if (error) { + *error = "O-word endrepeat without matching repeat"; + } + return -1; + } + pc = repeat_index; + continue; + } + if (current_oword_kind == OwordKind::Break || current_oword_kind == OwordKind::Continue) { + size_t loop_end = find_matching_loop_end(lines, pc, o_identifier, OwordKind::While); + OwordKind loop_end_kind = OwordKind::EndWhile; + const size_t endrepeat = find_matching_loop_end(lines, pc, o_identifier, OwordKind::Repeat); + if (endrepeat < loop_end) { + loop_end = endrepeat; + loop_end_kind = OwordKind::EndRepeat; + } + const size_t do_while = find_matching_do_while(lines, pc, o_identifier); + if (do_while < loop_end) { + loop_end = do_while; + loop_end_kind = OwordKind::While; + } + if (loop_end == lines.size()) { + if (error) { + *error = current_oword_kind == OwordKind::Break + ? "O-word break without matching loop" + : "O-word continue without matching loop"; + } + return -1; + } + const size_t loop_start = loop_end_kind == OwordKind::While && + find_matching_do_start(lines, loop_end, o_identifier) != lines.size() + ? find_matching_do_start(lines, loop_end, o_identifier) + : find_matching_loop_start(lines, loop_end, o_identifier, loop_end_kind); + if (loop_start == lines.size()) { + if (error) { + *error = current_oword_kind == OwordKind::Break + ? "O-word break without matching loop" + : "O-word continue without matching loop"; + } + return -1; + } + while (!conditional_stack.empty() && conditional_stack.back().start_pc > loop_start) { + conditional_stack.pop_back(); + } + if (current_oword_kind == OwordKind::Break) { + loop_iterations.erase(loop_start); + pc = loop_end + 1; + } else { + pc = loop_end; + } + continue; + } for (const Word &word : word_list) { if (word.letter == 'G') { @@ -341,11 +1818,22 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string } } else if (g == 0 || g == 1 || g == 2 || g == 3) { modal_motion = g; + canned_cycle_ = 0; } else if (g == 4) { sink_.dwell(words.count('P') ? words['P'] : 0.0, line_number); if (stop_if_callback_aborted(sink_, error)) { return -1; } + } else if (g == 80) { + canned_cycle_ = 0; + modal_motion = -1; + } else if (g == 73 || g == 81 || g == 82 || g == 83 || g == 85 || g == 86 || g == 89) { + canned_cycle_ = g; + modal_motion = -1; + } else if (g == 98) { + canned_return_to_initial_ = true; + } else if (g == 99) { + canned_return_to_initial_ = false; } } else if (word.letter == 'M') { m_codes.push_back(rounded_value(word.value)); @@ -369,7 +1857,60 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string } bool program_end = false; + bool subprogram_control = false; + if (oword_call) { + const auto subprogram = subprograms.find(o_identifier); + if (subprogram == subprograms.end()) { + if (error) { + *error = "O-word call references an unknown subprogram"; + } + return -1; + } + if (!subprogram->second.oword) { + if (error) { + *error = "O-word call cannot invoke an M98/M99 subprogram"; + } + return -1; + } + if (call_stack.size() >= kMaxSubprogramDepth) { + if (error) { + *error = "O-word subprogram nesting limit exceeded"; + } + return -1; + } + CallFrame frame{}; + frame.return_pc = pc; + frame.sub_start = subprogram->second.start; + frame.remaining = 1; + frame.oword = true; + const std::vector args = parse_oword_call_arguments(keyword_line, call_stack, parameters, named_parameters, ¶meter_error); + if (!parameter_error.empty()) { + if (error) { + *error = parameter_error; + } + return -1; + } + for (size_t i = 0; i < args.size() && i < 30; ++i) { + frame.params[i + 1] = args[i]; + } + call_stack.push_back(frame); + pc = subprogram->second.start; + subprogram_control = true; + } else if (oword_return || oword_endsub) { + if (call_stack.empty() || !call_stack.back().oword) { + if (error) { + *error = "O-word return/endsub without active O-word call"; + } + return -1; + } + pc = call_stack.back().return_pc; + call_stack.pop_back(); + subprogram_control = true; + } for (int m : m_codes) { + if (subprogram_control) { + break; + } if (m == 3) { sink_.start_spindle(1, line_number); if (stop_if_callback_aborted(sink_, error)) { @@ -439,6 +1980,54 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string if (stop_if_callback_aborted(sink_, error)) { return -1; } + } else if (m == 98) { + const int sub_number = rounded_word(words, 'P', -1); + const int repeat_count = std::max(1, rounded_word(words, 'L', 1)); + const auto subprogram = subprograms.find(numeric_o_identifier(sub_number)); + if (subprogram == subprograms.end()) { + if (error) { + *error = "M98 references an unknown subprogram"; + } + return -1; + } + if (subprogram->second.oword) { + if (error) { + *error = "M98 cannot invoke an O-word subprogram"; + } + return -1; + } + if (call_stack.size() >= kMaxSubprogramDepth) { + if (error) { + *error = "M98 subprogram nesting limit exceeded"; + } + return -1; + } + CallFrame frame{}; + frame.return_pc = pc; + frame.sub_start = subprogram->second.start; + frame.remaining = repeat_count; + frame.oword = false; + call_stack.push_back(frame); + pc = subprogram->second.start; + subprogram_control = true; + break; + } else if (m == 99) { + if (call_stack.empty() || call_stack.back().oword) { + if (error) { + *error = "M99 without active M98 call"; + } + return -1; + } + CallFrame &frame = call_stack.back(); + --frame.remaining; + if (frame.remaining > 0) { + pc = frame.sub_start; + } else { + pc = frame.return_pc; + call_stack.pop_back(); + } + subprogram_control = true; + break; } else if (m == 2 || m == 30) { sink_.program_end(line_number); if (stop_if_callback_aborted(sink_, error)) { @@ -447,11 +2036,243 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string program_end = true; } } + if (subprogram_control) { + continue; + } if (program_end) { break; } - if (has_axis_word(words) && (modal_motion == 0 || modal_motion == 1 || modal_motion == 2 || modal_motion == 3)) { + if (canned_cycle_ != 0) { + const CncSimPose initial = sink_.position(); + if (words.count('R')) { + canned_r_ = absolute_ ? words.at('R') * sink_.unit_scale() : initial.z + words.at('R') * sink_.unit_scale(); + canned_has_r_ = true; + } + if (words.count('Z')) { + canned_z_ = absolute_ ? words.at('Z') * sink_.unit_scale() : canned_r_ + words.at('Z') * sink_.unit_scale(); + canned_has_z_ = true; + } + if (words.count('P')) { + canned_p_ = words.at('P'); + canned_has_p_ = true; + } + if (words.count('Q')) { + canned_q_ = words.at('Q') * sink_.unit_scale(); + canned_has_q_ = true; + } + if (words.count('D')) { + canned_d_ = words.at('D') * sink_.unit_scale(); + canned_has_d_ = true; + } + + const bool cycle_requested = has_cycle_position_word(words) || words.count('Z') || words.count('R'); + if (cycle_requested && sink_.plane() != 17) { + if (error) { + *error = "smoke parser canned cycles currently require G17 XY plane"; + } + return -1; + } + if (cycle_requested && sink_.feed_rate() == 0.0) { + if (error) { + *error = "Cannot feed with zero feed rate"; + } + return -1; + } + if (cycle_requested && has_rotary_word(words)) { + if (error) { + *error = "rotary axis word is not allowed in canned cycle"; + } + return -1; + } + if (cycle_requested && has_uvw_word(words)) { + if (error) { + *error = "UVW axis word is not allowed in G17 canned cycle"; + } + return -1; + } + const bool same_canned_cycle_mode = previous_canned_cycle == canned_cycle_; + const bool current_line_has_r = words.count('R') != 0; + const bool current_line_has_z = words.count('Z') != 0; + if (cycle_requested && + ((!current_line_has_r && !same_canned_cycle_mode) || + (!current_line_has_z && !same_canned_cycle_mode) || + !canned_has_r_ || + !canned_has_z_)) { + if (error) { + *error = "canned cycle requires R and Z words before motion"; + } + return -1; + } + if (cycle_requested && canned_r_ < canned_z_) { + if (error) { + *error = "canned cycle R plane must not be below Z depth"; + } + return -1; + } + const bool current_line_has_p = words.count('P') != 0; + const bool current_line_has_q = words.count('Q') != 0; + if (cycle_requested && + (canned_cycle_ == 73 || canned_cycle_ == 83) && + ((!current_line_has_q && !same_canned_cycle_mode) || !canned_has_q_ || canned_q_ <= 0.0)) { + if (error) { + *error = canned_cycle_ == 73 ? "G73 canned cycle requires positive Q word" : "G83 canned cycle requires positive Q word"; + } + return -1; + } + if (cycle_requested && + (canned_cycle_ == 82 || canned_cycle_ == 86 || canned_cycle_ == 89) && + ((!current_line_has_p && !same_canned_cycle_mode) || !canned_has_p_)) { + if (error) { + if (canned_cycle_ == 82) { + *error = "G82 canned cycle requires P dwell word"; + } else { + *error = canned_cycle_ == 86 ? "G86 canned cycle requires P dwell word" : "G89 canned cycle requires P dwell word"; + } + } + return -1; + } + if (cycle_requested && canned_cycle_ == 86 && sink_.spindle_direction() == 0) { + if (error) { + *error = "G86 canned cycle requires a running spindle"; + } + return -1; + } + const int repeat_count = positive_integer_word(words, 'L', 1); + if (cycle_requested && repeat_count < 1) { + if (error) { + *error = "canned cycle L word must be a positive integer"; + } + return -1; + } + + if (cycle_requested) { + CncSimPose hole = initial; + apply_cycle_position_axes(&hole, words, absolute_, sink_.unit_scale()); + const double x_increment = (!absolute_ && words.count('X')) ? words.at('X') * sink_.unit_scale() : 0.0; + const double y_increment = (!absolute_ && words.count('Y')) ? words.at('Y') * sink_.unit_scale() : 0.0; + const double a_increment = (!absolute_ && words.count('A')) ? words.at('A') : 0.0; + const double b_increment = (!absolute_ && words.count('B')) ? words.at('B') : 0.0; + const double c_increment = (!absolute_ && words.count('C')) ? words.at('C') : 0.0; + const int cycle_start_spindle_direction = sink_.spindle_direction(); + + auto run_cycle_at_hole = [&](const CncSimPose &target_hole) -> bool { + CncSimPose rapid_hole = target_hole; + rapid_hole.z = sink_.position().z; + sink_.straight_traverse(line_number, rapid_hole); + if (stop_if_callback_aborted(sink_, error)) { + return false; + } + + CncSimPose r_plane = target_hole; + r_plane.z = canned_r_; + if (sink_.position().z != r_plane.z) { + sink_.straight_traverse(line_number, r_plane); + if (stop_if_callback_aborted(sink_, error)) { + return false; + } + } + + if (canned_cycle_ == 73 || canned_cycle_ == 83) { + double next_depth = canned_r_; + while (next_depth > canned_z_) { + next_depth = std::max(canned_z_, next_depth - canned_q_); + CncSimPose peck = r_plane; + peck.z = next_depth; + sink_.straight_feed(line_number, peck); + if (stop_if_callback_aborted(sink_, error)) { + return false; + } + if (next_depth > canned_z_) { + CncSimPose retract = canned_cycle_ == 73 ? peck : r_plane; + const double peck_clearance = canned_has_d_ ? canned_d_ : 1.0 * sink_.unit_scale(); + if (canned_cycle_ == 73) { + retract.z = std::min(canned_r_, next_depth + peck_clearance); + } + sink_.straight_traverse(line_number, retract); + if (stop_if_callback_aborted(sink_, error)) { + return false; + } + if (canned_cycle_ == 83) { + CncSimPose return_depth = r_plane; + return_depth.z = std::min(canned_r_, next_depth + peck_clearance); + sink_.straight_traverse(line_number, return_depth); + if (stop_if_callback_aborted(sink_, error)) { + return false; + } + } + } + } + } else { + CncSimPose bottom = r_plane; + bottom.z = canned_z_; + sink_.straight_feed(line_number, bottom); + if (stop_if_callback_aborted(sink_, error)) { + return false; + } + if (canned_cycle_ == 82 && canned_has_p_) { + sink_.dwell(canned_p_, line_number); + if (stop_if_callback_aborted(sink_, error)) { + return false; + } + } + if (canned_cycle_ == 86 || canned_cycle_ == 89) { + sink_.dwell(canned_p_, line_number); + if (stop_if_callback_aborted(sink_, error)) { + return false; + } + } + if (canned_cycle_ == 86) { + sink_.stop_spindle(line_number); + if (stop_if_callback_aborted(sink_, error)) { + return false; + } + } + if (canned_cycle_ == 85 || canned_cycle_ == 89) { + CncSimPose feed_retract = r_plane; + if (canned_cycle_ == 89 && canned_return_to_initial_ && initial.z > canned_r_) { + feed_retract.z = initial.z; + } + sink_.straight_feed(line_number, feed_retract); + if (stop_if_callback_aborted(sink_, error)) { + return false; + } + } + } + + CncSimPose retract = r_plane; + if (canned_return_to_initial_ && initial.z > canned_r_) { + retract.z = initial.z; + } + if (sink_.position().z != retract.z) { + sink_.straight_traverse(line_number, retract); + if (stop_if_callback_aborted(sink_, error)) { + return false; + } + } + if (canned_cycle_ == 86) { + sink_.start_spindle(cycle_start_spindle_direction == 2 ? -1 : 1, line_number); + if (stop_if_callback_aborted(sink_, error)) { + return false; + } + } + return true; + }; + + for (int repeat = 0; repeat < repeat_count; ++repeat) { + if (repeat > 0 && !absolute_) { + hole.x += x_increment; + hole.y += y_increment; + hole.a += a_increment; + hole.b += b_increment; + hole.c += c_increment; + } + if (!run_cycle_at_hole(hole)) { + return -1; + } + } + } + } else if (has_axis_word(words) && (modal_motion == 0 || modal_motion == 1 || modal_motion == 2 || modal_motion == 3)) { CncSimEvent event{}; event.start = sink_.position(); event.end = sink_.position(); @@ -473,5 +2294,12 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string } } + if (!conditional_stack.empty()) { + if (error) { + *error = "O-word if without matching endif"; + } + return -1; + } + return 0; } diff --git a/core/src/smoke_gcode_parser.h b/core/src/smoke_gcode_parser.h index 3470c06..b155f2f 100644 --- a/core/src/smoke_gcode_parser.h +++ b/core/src/smoke_gcode_parser.h @@ -14,5 +14,16 @@ public: private: CanonEventSink &sink_; bool absolute_ = true; + int canned_cycle_ = 0; + bool canned_return_to_initial_ = false; + bool canned_has_r_ = false; + bool canned_has_z_ = false; + bool canned_has_p_ = false; + bool canned_has_q_ = false; + bool canned_has_d_ = false; + double canned_r_ = 0.0; + double canned_z_ = 0.0; + double canned_p_ = 0.0; + double canned_q_ = 0.0; + double canned_d_ = 1.0; }; - diff --git a/core/tests/cnc_sim_api_linuxcnc_rs274_smoke.cpp b/core/tests/cnc_sim_api_linuxcnc_rs274_smoke.cpp index 5ede830..316dd58 100644 --- a/core/tests/cnc_sim_api_linuxcnc_rs274_smoke.cpp +++ b/core/tests/cnc_sim_api_linuxcnc_rs274_smoke.cpp @@ -94,6 +94,8 @@ int main() { bool saw_g61_mode = false; bool saw_g611_mode = false; bool saw_g64_mode = false; + bool saw_tool_length = false; + bool saw_tool_length_clear = false; for (const auto &event : events) { saw_tool = saw_tool || event.type == CNC_SIM_EVENT_TOOL_CHANGE; saw_rapid = saw_rapid || event.type == CNC_SIM_EVENT_RAPID; @@ -316,6 +318,29 @@ int main() { ok &= expect(saw_g611_mode, "expected LinuxCNC G61.1 exact stop mode"); ok &= expect(saw_g64_mode, "expected LinuxCNC G64 continuous mode"); + const char tool_length_program[] = + "G21 G90 G17\n" + "G43 H1\n" + "G49\n" + "M30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, tool_length_program, sizeof(tool_length_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_tool_length = saw_tool_length || + (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 2 && + event.reserved == 430 && + event.start.z == 0.0); + saw_tool_length_clear = saw_tool_length_clear || + (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 3 && + event.reserved == 430 && + event.start.z == 0.0); + } + ok &= expect(saw_tool_length, "expected LinuxCNC G43 H1 tool length event"); + ok &= expect(saw_tool_length_clear, "expected LinuxCNC G49 tool length clear event"); + const char comment_program[] = "G21 G90 G17\n" "(operator note)\n" diff --git a/core/tests/cnc_sim_api_smoke.cpp b/core/tests/cnc_sim_api_smoke.cpp index 7143d13..640dbd4 100644 --- a/core/tests/cnc_sim_api_smoke.cpp +++ b/core/tests/cnc_sim_api_smoke.cpp @@ -1,5 +1,6 @@ #include "cnc_sim_api.h" +#include #include #include @@ -19,6 +20,10 @@ bool expect(bool value, const char *message) { return true; } +bool near(double lhs, double rhs) { + return std::fabs(lhs - rhs) < 0.000001; +} + } // namespace int main() { @@ -86,6 +91,94 @@ int main() { bool saw_g64_mode = false; bool saw_incremental_move = false; bool saw_r_arc_center = false; + bool saw_g81_feed = false; + bool saw_g81_g98_return = false; + bool saw_g82_feed = false; + bool saw_g82_dwell = false; + bool saw_g83_first_peck = false; + bool saw_g83_second_peck = false; + bool saw_g83_final_retract = false; + bool saw_g83_d_retract_to_r = false; + bool saw_g83_d_return_to_depth = false; + bool saw_g83_d_second_peck = false; + bool saw_g85_feed_bottom = false; + bool saw_g85_feed_retract = false; + bool saw_g85_clearance_retract = false; + bool saw_g89_feed_bottom = false; + bool saw_g89_dwell = false; + bool saw_g89_feed_retract = false; + bool saw_g81_l_first = false; + bool saw_g81_l_second = false; + bool saw_g81_l_third = false; + bool saw_g73_first_peck = false; + bool saw_g73_chip_break = false; + bool saw_g73_second_peck = false; + bool saw_g73_final_retract = false; + bool saw_g86_feed = false; + bool saw_g86_dwell = false; + bool saw_g86_spindle_stop = false; + bool saw_g86_retract = false; + bool saw_g86_spindle_restart = false; + bool saw_g82_sticky_p_first = false; + bool saw_g82_sticky_p_second = false; + bool saw_g81_sticky_zr_second = false; + bool saw_g80_cancel = false; + bool saw_sub_first_call = false; + bool saw_sub_second_call = false; + bool saw_sub_return_motion = false; + bool saw_oword_call = false; + bool saw_oword_return = false; + bool saw_oword_second_arg = false; + bool saw_numbered_parameter = false; + bool saw_parameter_expression = false; + bool saw_oword_local_parameter = false; + bool saw_global_parameter_after_oword = false; + bool saw_global_numbered_parameter_in_oword = false; + bool saw_global_numbered_parameter_after_oword = false; + bool saw_global_low_parameter_after_oword = false; + bool saw_named_oword_call = false; + bool saw_named_oword_return = false; + bool saw_named_parameter_reference = false; + bool saw_named_parameter_call_arg = false; + bool saw_named_parameter_local = false; + bool saw_named_parameter_global_after_call = false; + bool saw_nested_bracket_assignment = false; + bool saw_nested_bracket_motion = false; + bool saw_math_function_assignment = false; + bool saw_trig_function_motion = false; + bool saw_inverse_trig_function_motion = false; + bool saw_atan_function_motion = false; + bool saw_rounding_function_motion = false; + bool saw_exp_ln_function_motion = false; + bool saw_power_expression_motion = false; + bool saw_power_precedence_motion = false; + bool saw_mod_expression_motion = false; + bool saw_negative_mod_motion = false; + bool saw_mod_left_associative_motion = false; + bool saw_comparison_expression_motion = false; + bool saw_comparison_precedence_motion = false; + bool saw_logical_expression_motion = false; + bool saw_logical_precedence_motion = false; + bool saw_equality_tolerance_motion = false; + bool saw_elseif_branch_motion = false; + bool saw_else_branch_motion = false; + bool saw_nested_if_branch_motion = false; + int while_loop_motion_count = 0; + bool saw_while_final_parameter_motion = false; + bool saw_false_while_skip_motion = false; + int while_continue_break_motion_count = 0; + bool saw_continue_break_final_counter_motion = false; + bool saw_continue_break_body_counter_motion = false; + int repeat_loop_motion_count = 0; + bool saw_repeat_expression_latched_motion = false; + bool saw_repeat_counter_motion = false; + int repeat_continue_break_motion_count = 0; + bool saw_repeat_continue_break_final_counter_motion = false; + bool saw_repeat_continue_break_body_counter_motion = false; + int do_while_motion_count = 0; + bool saw_do_while_false_once_motion = false; + bool saw_do_continue_break_final_counter_motion = false; + bool saw_do_continue_break_body_counter_motion = false; for (const auto &event : events) { saw_rapid = saw_rapid || event.type == CNC_SIM_EVENT_RAPID; saw_feed = saw_feed || event.type == CNC_SIM_EVENT_LINEAR_FEED; @@ -291,6 +384,1094 @@ int main() { ok &= expect(saw_g611_mode, "expected G61.1 exact stop mode"); ok &= expect(saw_g64_mode, "expected G64 continuous mode"); + const char canned_cycle_program[] = + "G21 G90 G17\n" + "G0 X0 Y0 Z10\n" + "G98 G81 X1 Y0 Z-2 R3 F100\n" + "G99 G82 X2 Z-1 R2 P0.2\n" + "G83 X3 Z-3 R1 Q1\n" + "G98 G85 X4 Z-4 R0.5\n" + "G89 X5 Z-5 R0.25 P0.15\n" + "G80\n" + "X6\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, canned_cycle_program, sizeof(canned_cycle_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_g81_feed = saw_g81_feed || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 3 && + event.end.x == 1.0 && + event.end.z == -2.0); + saw_g81_g98_return = saw_g81_g98_return || + (event.type == CNC_SIM_EVENT_RAPID && + event.line == 3 && + event.end.x == 1.0 && + event.end.z == 10.0); + saw_g82_feed = saw_g82_feed || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 4 && + event.end.x == 2.0 && + event.end.z == -1.0); + saw_g82_dwell = saw_g82_dwell || + (event.type == CNC_SIM_EVENT_DWELL && + event.line == 4 && + event.dwell_seconds == 0.2); + saw_g83_first_peck = saw_g83_first_peck || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 5 && + event.end.x == 3.0 && + event.end.z == 0.0); + saw_g83_second_peck = saw_g83_second_peck || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 5 && + event.end.x == 3.0 && + event.end.z == -1.0); + saw_g83_final_retract = saw_g83_final_retract || + (event.type == CNC_SIM_EVENT_RAPID && + event.line == 5 && + event.end.x == 3.0 && + event.end.z == 1.0); + saw_g85_feed_bottom = saw_g85_feed_bottom || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 6 && + event.start.x == 4.0 && + event.start.z == 0.5 && + event.end.x == 4.0 && + event.end.z == -4.0); + saw_g85_feed_retract = saw_g85_feed_retract || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 6 && + event.start.x == 4.0 && + event.start.z == -4.0 && + event.end.x == 4.0 && + event.end.z == 0.5); + saw_g85_clearance_retract = saw_g85_clearance_retract || + (event.type == CNC_SIM_EVENT_RAPID && + event.line == 6 && + event.start.x == 4.0 && + event.start.z == 0.5 && + event.end.x == 4.0 && + event.end.z == 1.0); + saw_g89_feed_bottom = saw_g89_feed_bottom || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 7 && + event.start.x == 5.0 && + event.start.z == 0.25 && + event.end.x == 5.0 && + event.end.z == -5.0); + saw_g89_dwell = saw_g89_dwell || + (event.type == CNC_SIM_EVENT_DWELL && + event.line == 7 && + event.dwell_seconds == 0.15); + saw_g89_feed_retract = saw_g89_feed_retract || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 7 && + event.start.x == 5.0 && + event.start.z == -5.0 && + event.end.x == 5.0 && + event.end.z == 1.0); + saw_g80_cancel = saw_g80_cancel || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 9 && + event.end.x == 6.0); + } + ok &= expect(saw_g81_feed, "expected smoke G81 feed to bottom"); + ok &= expect(saw_g81_g98_return, "expected smoke G81 G98 return to initial plane"); + ok &= expect(saw_g82_feed, "expected smoke G82 feed to bottom"); + ok &= expect(saw_g82_dwell, "expected smoke G82 dwell at bottom"); + ok &= expect(saw_g83_first_peck, "expected smoke G83 first peck"); + ok &= expect(saw_g83_second_peck, "expected smoke G83 second peck"); + ok &= expect(saw_g83_final_retract, "expected smoke G83 final retract to R plane"); + ok &= expect(saw_g85_feed_bottom, "expected smoke G85 feed to bottom"); + ok &= expect(saw_g85_feed_retract, "expected smoke G85 feed retract to R plane"); + ok &= expect(saw_g85_clearance_retract, "expected smoke G85 traverse retract to initial plane"); + ok &= expect(saw_g89_feed_bottom, "expected smoke G89 feed to bottom"); + ok &= expect(saw_g89_dwell, "expected smoke G89 dwell at bottom"); + ok &= expect(saw_g89_feed_retract, "expected smoke G89 feed retract to clear plane"); + ok &= expect(!saw_g80_cancel, "expected G80 to cancel canned cycle motion"); + + const char repeated_cycle_program[] = + "G21 G90 G17\n" + "G0 X0 Y0 Z0\n" + "G91 G99 G81 X2 Y1 Z-1 R1 L3 F100\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, repeated_cycle_program, sizeof(repeated_cycle_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_g81_l_first = saw_g81_l_first || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 3 && + event.end.x == 2.0 && + event.end.y == 1.0 && + event.end.z == 0.0); + saw_g81_l_second = saw_g81_l_second || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 3 && + event.end.x == 4.0 && + event.end.y == 2.0 && + event.end.z == 0.0); + saw_g81_l_third = saw_g81_l_third || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 3 && + event.end.x == 6.0 && + event.end.y == 3.0 && + event.end.z == 0.0); + } + ok &= expect(saw_g81_l_first, "expected first incremental G81 L repeat"); + ok &= expect(saw_g81_l_second, "expected second incremental G81 L repeat"); + ok &= expect(saw_g81_l_third, "expected third incremental G81 L repeat"); + + const char g73_program[] = + "G21 G90 G17\n" + "G0 X0 Y0 Z5\n" + "G99 G73 X1 Z-3 R1 Q2 D0.25 F100\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, g73_program, sizeof(g73_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_g73_first_peck = saw_g73_first_peck || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 3 && + event.end.x == 1.0 && + event.end.z == -1.0); + saw_g73_chip_break = saw_g73_chip_break || + (event.type == CNC_SIM_EVENT_RAPID && + event.line == 3 && + event.start.x == 1.0 && + event.start.z == -1.0 && + event.end.x == 1.0 && + event.end.z == -0.75); + saw_g73_second_peck = saw_g73_second_peck || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 3 && + event.start.x == 1.0 && + event.start.z == -0.75 && + event.end.x == 1.0 && + event.end.z == -3.0); + saw_g73_final_retract = saw_g73_final_retract || + (event.type == CNC_SIM_EVENT_RAPID && + event.line == 3 && + event.end.x == 1.0 && + event.end.z == 1.0); + } + ok &= expect(saw_g73_first_peck, "expected smoke G73 first peck"); + ok &= expect(saw_g73_chip_break, "expected smoke G73 chip-break retract by D"); + ok &= expect(saw_g73_second_peck, "expected smoke G73 final peck from chip-break height"); + ok &= expect(saw_g73_final_retract, "expected smoke G73 final retract to R plane"); + + const char g83_d_program[] = + "G21 G90 G17\n" + "G0 X0 Y0 Z5\n" + "G99 G83 X1 Z-3 R1 Q2 D0.25 F100\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, g83_d_program, sizeof(g83_d_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_g83_d_retract_to_r = saw_g83_d_retract_to_r || + (event.type == CNC_SIM_EVENT_RAPID && + event.line == 3 && + event.start.x == 1.0 && + event.start.z == -1.0 && + event.end.x == 1.0 && + event.end.z == 1.0); + saw_g83_d_return_to_depth = saw_g83_d_return_to_depth || + (event.type == CNC_SIM_EVENT_RAPID && + event.line == 3 && + event.start.x == 1.0 && + event.start.z == 1.0 && + event.end.x == 1.0 && + event.end.z == -0.75); + saw_g83_d_second_peck = saw_g83_d_second_peck || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 3 && + event.start.x == 1.0 && + event.start.z == -0.75 && + event.end.x == 1.0 && + event.end.z == -3.0); + } + ok &= expect(saw_g83_d_retract_to_r, "expected smoke G83 intermediate retract to R plane"); + ok &= expect(saw_g83_d_return_to_depth, "expected smoke G83 rapid return to peck clearance depth"); + ok &= expect(saw_g83_d_second_peck, "expected smoke G83 feed from peck clearance depth"); + + const char g86_program[] = + "G21 G90 G17\n" + "S900 M3\n" + "G0 X0 Y0 Z5\n" + "G99 G86 X1 Z-2 R1 P0.1 F100\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, g86_program, sizeof(g86_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_g86_feed = saw_g86_feed || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 4 && + event.end.x == 1.0 && + event.end.z == -2.0); + saw_g86_dwell = saw_g86_dwell || + (event.type == CNC_SIM_EVENT_DWELL && + event.line == 4 && + event.dwell_seconds == 0.1); + saw_g86_spindle_stop = saw_g86_spindle_stop || + (event.type == CNC_SIM_EVENT_SET_SPINDLE && + event.line == 4 && + event.spindle == 0.0 && + event.reserved == 0); + saw_g86_retract = saw_g86_retract || + (event.type == CNC_SIM_EVENT_RAPID && + event.line == 4 && + event.start.x == 1.0 && + event.start.z == -2.0 && + event.end.x == 1.0 && + event.end.z == 1.0); + saw_g86_spindle_restart = saw_g86_spindle_restart || + (event.type == CNC_SIM_EVENT_SET_SPINDLE && + event.line == 4 && + event.spindle == 900.0 && + event.reserved == 1); + } + ok &= expect(saw_g86_feed, "expected smoke G86 feed to bottom"); + ok &= expect(saw_g86_dwell, "expected smoke G86 dwell at bottom"); + ok &= expect(saw_g86_spindle_stop, "expected smoke G86 spindle stop"); + ok &= expect(saw_g86_retract, "expected smoke G86 rapid retract to clear plane"); + ok &= expect(saw_g86_spindle_restart, "expected smoke G86 spindle restart"); + + const char sticky_p_program[] = + "G21 G90 G17\n" + "G0 X0 Y0 Z5\n" + "G82 X1 Z-1 R1 P0.2 F100\n" + "X2\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, sticky_p_program, sizeof(sticky_p_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_g82_sticky_p_first = saw_g82_sticky_p_first || + (event.type == CNC_SIM_EVENT_DWELL && + event.line == 3 && + event.dwell_seconds == 0.2); + saw_g82_sticky_p_second = saw_g82_sticky_p_second || + (event.type == CNC_SIM_EVENT_DWELL && + event.line == 4 && + event.dwell_seconds == 0.2); + } + ok &= expect(saw_g82_sticky_p_first, "expected initial G82 P dwell"); + ok &= expect(saw_g82_sticky_p_second, "expected same-mode G82 to reuse P dwell"); + + const char sticky_zr_program[] = + "G21 G90 G17\n" + "G0 X0 Y0 Z5\n" + "G81 X1 Z-1 R1 F100\n" + "X2\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, sticky_zr_program, sizeof(sticky_zr_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_g81_sticky_zr_second = saw_g81_sticky_zr_second || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 4 && + event.end.x == 2.0 && + event.end.z == -1.0); + } + ok &= expect(saw_g81_sticky_zr_second, "expected same-mode G81 to reuse R/Z"); + + const char invalid_cycle_l_program[] = + "G21 G90 G17\n" + "G81 X1 Z-1 R1 L0\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, invalid_cycle_l_program, sizeof(invalid_cycle_l_program) - 1) != 0, + "expected canned cycle L0 to fail"); + + const char missing_cycle_feed_program[] = + "G21 G90 G17\n" + "G81 X1 Z-1 R1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, missing_cycle_feed_program, sizeof(missing_cycle_feed_program) - 1) != 0, + "expected G81 without feed rate to fail"); + + const char missing_g73_q_program[] = + "G21 G90 G17\n" + "G73 X1 Z-1 R1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, missing_g73_q_program, sizeof(missing_g73_q_program) - 1) != 0, + "expected G73 without Q peck to fail"); + + const char cross_cycle_q_program[] = + "G21 G90 G17\n" + "G73 X1 Z-1 R1 Q0.5\n" + "G83 X2 Z-2 R1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, cross_cycle_q_program, sizeof(cross_cycle_q_program) - 1) != 0, + "expected G83 to require Q after switching from G73"); + + const char stopped_g86_program[] = + "G21 G90 G17\n" + "G86 X1 Z-1 R1 P0.1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, stopped_g86_program, sizeof(stopped_g86_program) - 1) != 0, + "expected G86 with stopped spindle to fail"); + + const char missing_g89_p_program[] = + "G21 G90 G17\n" + "G89 X1 Z-1 R1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, missing_g89_p_program, sizeof(missing_g89_p_program) - 1) != 0, + "expected G89 without P dwell to fail"); + + const char cross_cycle_p_program[] = + "G21 G90 G17\n" + "G82 X1 Z-1 R1 P0.1\n" + "G89 X2 Z-2 R1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, cross_cycle_p_program, sizeof(cross_cycle_p_program) - 1) != 0, + "expected G89 to require P after switching from G82"); + + const char cross_cycle_zr_program[] = + "G21 G90 G17\n" + "G81 X1 Z-1 R1\n" + "G82 X2 P0.1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, cross_cycle_zr_program, sizeof(cross_cycle_zr_program) - 1) != 0, + "expected G82 to require R/Z after switching from G81"); + + const char canceled_cycle_zr_program[] = + "G21 G90 G17\n" + "G81 X1 Z-1 R1\n" + "G80\n" + "G81 X2\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, canceled_cycle_zr_program, sizeof(canceled_cycle_zr_program) - 1) != 0, + "expected G81 to require R/Z after G80"); + + const char rotary_cycle_program[] = + "G21 G90 G17\n" + "G81 X1 A10 Z-1 R1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, rotary_cycle_program, sizeof(rotary_cycle_program) - 1) != 0, + "expected rotary axis word in canned cycle to fail"); + + const char non_xy_cycle_program[] = + "G21 G90 G18\n" + "G81 X1 Z-1 R1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, non_xy_cycle_program, sizeof(non_xy_cycle_program) - 1) != 0, + "expected smoke canned cycle outside G17 to fail"); + + const char uvw_cycle_program[] = + "G21 G90 G17\n" + "G81 X1 U2 Z-1 R1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, uvw_cycle_program, sizeof(uvw_cycle_program) - 1) != 0, + "expected UVW axis word in G17 canned cycle to fail"); + + const char invalid_cycle_r_program[] = + "G21 G90 G17\n" + "G81 X1 Z2 R1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, invalid_cycle_r_program, sizeof(invalid_cycle_r_program) - 1) != 0, + "expected canned cycle R below Z to fail"); + + const char subprogram_program[] = + "G21 G90\n" + "G0 X0 Y0 Z0\n" + "F100\n" + "M98 P100 L2\n" + "G1 X20\n" + "M30\n" + "O100\n" + "G91 G1 X5\n" + "G90\n" + "M99\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, subprogram_program, sizeof(subprogram_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_sub_first_call = saw_sub_first_call || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 8 && + event.start.x == 0.0 && + event.end.x == 5.0); + saw_sub_second_call = saw_sub_second_call || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 8 && + event.start.x == 5.0 && + event.end.x == 10.0); + saw_sub_return_motion = saw_sub_return_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 5 && + event.start.x == 10.0 && + event.end.x == 20.0); + } + ok &= expect(saw_sub_first_call, "expected smoke M98 first subprogram call"); + ok &= expect(saw_sub_second_call, "expected smoke M98 repeated subprogram call"); + ok &= expect(saw_sub_return_motion, "expected smoke M99 return to caller"); + + const char missing_subprogram[] = + "G21\n" + "M98 P999\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, missing_subprogram, sizeof(missing_subprogram) - 1) != 0, + "expected unknown M98 subprogram to fail"); + + const char stray_m99_program[] = + "G21\n" + "M99\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, stray_m99_program, sizeof(stray_m99_program) - 1) != 0, + "expected M99 without M98 to fail"); + + const char recursive_subprogram[] = + "G21\n" + "M98 P100\n" + "M30\n" + "O100\n" + "M98 P100\n" + "M99\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, recursive_subprogram, sizeof(recursive_subprogram) - 1) != 0, + "expected recursive M98 nesting limit to fail"); + + const char oword_subprogram[] = + "G21 G90\n" + "G0 X0 Y0 Z0\n" + "F100\n" + "O100 call [2 + 3] [8 / 4]\n" + "G1 X20\n" + "M30\n" + "O100 sub\n" + "G1 X[#1 + #2]\n" + "G1 Y#2\n" + "O100 return\n" + "G1 X99\n" + "O100 endsub\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, oword_subprogram, sizeof(oword_subprogram) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_oword_call = saw_oword_call || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 8 && + event.start.x == 0.0 && + event.end.x == 7.0); + saw_oword_return = saw_oword_return || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 5 && + event.start.x == 7.0 && + event.end.x == 20.0); + saw_oword_second_arg = saw_oword_second_arg || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 9 && + event.start.y == 0.0 && + event.end.y == 2.0); + ok &= expect(!(event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 11 && + event.end.x == 99.0), + "expected O-word return to skip remaining subprogram body"); + } + ok &= expect(saw_oword_call, "expected LinuxCNC O-word subprogram call"); + ok &= expect(saw_oword_second_arg, "expected LinuxCNC O-word #2 argument"); + ok &= expect(saw_oword_return, "expected LinuxCNC O-word return to caller"); + + const char numbered_parameter_program[] = + "#1 = 4\n" + "#2 = [#1 * 3]\n" + "G21 G90\n" + "F100\n" + "G1 X#2\n" + "#1 = [#2 / 2]\n" + "G1 Y[#1 + 1]\n" + "O200 call [9]\n" + "G1 Z#1\n" + "M30\n" + "O200 sub\n" + "#1 = [#1 + 1]\n" + "G1 X#1\n" + "O200 endsub\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, numbered_parameter_program, sizeof(numbered_parameter_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_numbered_parameter = saw_numbered_parameter || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 5 && + event.end.x == 12.0); + saw_parameter_expression = saw_parameter_expression || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 7 && + event.end.y == 7.0); + saw_oword_local_parameter = saw_oword_local_parameter || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 13 && + event.end.x == 10.0); + saw_global_parameter_after_oword = saw_global_parameter_after_oword || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 9 && + event.end.z == 6.0); + } + ok &= expect(saw_numbered_parameter, "expected smoke numbered parameter reference"); + ok &= expect(saw_parameter_expression, "expected smoke parameter expression reference"); + ok &= expect(saw_oword_local_parameter, "expected O-word local parameter assignment"); + ok &= expect(saw_global_parameter_after_oword, "expected global parameter value after O-word return"); + + const char global_numbered_parameter_program[] = + "#100 = 40\n" + "#1 = 5\n" + "G21 G90\n" + "F100\n" + "O210 call [7]\n" + "G1 X#100\n" + "G1 Y#1\n" + "M30\n" + "O210 sub\n" + "#1 = [#1 + 1]\n" + "#100 = [#100 + #1]\n" + "G1 Z#100\n" + "O210 endsub\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, global_numbered_parameter_program, sizeof(global_numbered_parameter_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_global_numbered_parameter_in_oword = saw_global_numbered_parameter_in_oword || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 12 && + event.end.z == 48.0); + saw_global_numbered_parameter_after_oword = saw_global_numbered_parameter_after_oword || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 6 && + event.end.x == 48.0); + saw_global_low_parameter_after_oword = saw_global_low_parameter_after_oword || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 7 && + event.end.y == 5.0); + } + ok &= expect(saw_global_numbered_parameter_in_oword, "expected #100 to behave as a global O-word parameter"); + ok &= expect(saw_global_numbered_parameter_after_oword, "expected #100 global assignment to survive O-word return"); + ok &= expect(saw_global_low_parameter_after_oword, "expected #1 global assignment to survive O-word local parameter writes"); + + const char out_of_range_numbered_parameter_assignment[] = + "#5602 = 1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + out_of_range_numbered_parameter_assignment, + sizeof(out_of_range_numbered_parameter_assignment) - 1) != 0, + "expected out-of-range numbered parameter assignment to fail"); + + const char out_of_range_numbered_parameter_reference[] = + "G21 G90\n" + "F100\n" + "G1 X#5602\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + out_of_range_numbered_parameter_reference, + sizeof(out_of_range_numbered_parameter_reference) - 1) != 0, + "expected out-of-range numbered parameter reference to fail"); + + const char named_oword_subprogram[] = + "G21 G90\n" + "G0 X0 Y0 Z0\n" + "F100\n" + "O call [4]\n" + "G1 Y2\n" + "M30\n" + "O sub\n" + "G1 X#1\n" + "O return\n" + "G1 X99\n" + "O endsub\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, named_oword_subprogram, sizeof(named_oword_subprogram) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_named_oword_call = saw_named_oword_call || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 8 && + event.end.x == 4.0); + saw_named_oword_return = saw_named_oword_return || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 5 && + event.start.x == 4.0 && + event.end.y == 2.0); + ok &= expect(!(event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 10 && + event.end.x == 99.0), + "expected named O-word return to skip remaining subprogram body"); + } + ok &= expect(saw_named_oword_call, "expected named O-word subprogram call"); + ok &= expect(saw_named_oword_return, "expected named O-word return to caller"); + + const char named_parameter_program[] = + "# = 5\n" + "# = [# + 2]\n" + "G21 G90\n" + "F100\n" + "G1 X#\n" + "O call [#]\n" + "G1 Z#\n" + "M30\n" + "O sub\n" + "# = [#1 + 3]\n" + "G1 Y#\n" + "O endsub\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, named_parameter_program, sizeof(named_parameter_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_named_parameter_reference = saw_named_parameter_reference || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 5 && + event.end.x == 7.0); + saw_named_parameter_call_arg = saw_named_parameter_call_arg || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 11 && + event.end.y == 10.0); + saw_named_parameter_local = saw_named_parameter_local || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 11 && + event.end.y == 10.0); + saw_named_parameter_global_after_call = saw_named_parameter_global_after_call || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 7 && + event.end.z == 5.0); + } + ok &= expect(saw_named_parameter_reference, "expected named parameter reference"); + ok &= expect(saw_named_parameter_call_arg, "expected named parameter as O-word call argument"); + ok &= expect(saw_named_parameter_local, "expected local named parameter assignment in O-word call"); + ok &= expect(saw_named_parameter_global_after_call, "expected global named parameter after O-word return"); + + const char nested_bracket_program[] = + "#1 = 4\n" + "#2 = [[#1 + 2] * [#1 - 1]]\n" + "G21 G90\n" + "F100\n" + "G1 X#2\n" + "G1 Y[[#2 / 3] + [#1 * 2]]\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, nested_bracket_program, sizeof(nested_bracket_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_nested_bracket_assignment = saw_nested_bracket_assignment || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 5 && + event.end.x == 18.0); + saw_nested_bracket_motion = saw_nested_bracket_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 6 && + event.end.y == 14.0); + } + ok &= expect(saw_nested_bracket_assignment, "expected nested bracket assignment expression"); + ok &= expect(saw_nested_bracket_motion, "expected nested bracket motion expression"); + + const char math_function_program[] = + "#1 = [ABS[-3] + SQRT[16]]\n" + "# = [COS[0] + SIN[0] + TAN[0]]\n" + "G21 G90\n" + "F100\n" + "G1 X#1\n" + "G1 Y#\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, math_function_program, sizeof(math_function_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_math_function_assignment = saw_math_function_assignment || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 5 && + event.end.x == 7.0); + saw_trig_function_motion = saw_trig_function_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 6 && + event.end.y == 1.0); + } + ok &= expect(saw_math_function_assignment, "expected ABS/SQRT expression function support"); + ok &= expect(saw_trig_function_motion, "expected SIN/COS/TAN expression function support"); + + const char additional_math_function_program[] = + "#1 = [ASIN[0.5] + ACOS[0.5]]\n" + "#2 = [FIX[1.9] + FUP[1.1] + ROUND[-1.5]]\n" + "#3 = ATAN[1]/[1]\n" + "#4 = [LN[1] + EXP[0]]\n" + "G21 G90\n" + "F100\n" + "G1 X#1\n" + "G1 Y#2\n" + "G1 Z#3\n" + "G1 A#4\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, additional_math_function_program, sizeof(additional_math_function_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_inverse_trig_function_motion = saw_inverse_trig_function_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 7 && + near(event.end.x, 90.0)); + saw_rounding_function_motion = saw_rounding_function_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 8 && + event.end.y == 1.0); + saw_atan_function_motion = saw_atan_function_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 9 && + near(event.end.z, 45.0)); + saw_exp_ln_function_motion = saw_exp_ln_function_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 10 && + near(event.end.a, 1.0)); + } + ok &= expect(saw_inverse_trig_function_motion, "expected ASIN/ACOS expression function support"); + ok &= expect(saw_rounding_function_motion, "expected FIX/FUP/ROUND expression function support"); + ok &= expect(saw_atan_function_motion, "expected ATAN expression function support"); + ok &= expect(saw_exp_ln_function_motion, "expected EXP/LN expression function support"); + + const char expression_operator_program[] = + "#1 = [2 ** 3]\n" + "#2 = [2 + 3 ** 2 * 4]\n" + "#3 = [17 MOD 5]\n" + "#4 = [-3 MOD 5]\n" + "#5 = [20 / 5 MOD 3]\n" + "#6 = [3 LT 5]\n" + "#7 = [2 + 3 GT 4]\n" + "#8 = [1 AND 0 OR 1]\n" + "#9 = [1 OR 0 AND 0]\n" + "#10 = [1.0000001 EQ 1]\n" + "G21 G90\n" + "F100\n" + "G1 X#1\n" + "G1 Y#2\n" + "G1 Z#3\n" + "G1 A#4\n" + "G1 B#5\n" + "G1 C#6\n" + "G1 U#7\n" + "G1 V#8\n" + "G1 W#9\n" + "G1 X#10\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, expression_operator_program, sizeof(expression_operator_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_power_expression_motion = saw_power_expression_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 13 && + event.end.x == 8.0); + saw_power_precedence_motion = saw_power_precedence_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 14 && + event.end.y == 38.0); + saw_mod_expression_motion = saw_mod_expression_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 15 && + event.end.z == 2.0); + saw_negative_mod_motion = saw_negative_mod_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 16 && + event.end.a == 2.0); + saw_mod_left_associative_motion = saw_mod_left_associative_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 17 && + event.end.b == 1.0); + saw_comparison_expression_motion = saw_comparison_expression_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 18 && + event.end.c == 1.0); + saw_comparison_precedence_motion = saw_comparison_precedence_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 19 && + event.end.u == 1.0); + saw_logical_expression_motion = saw_logical_expression_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 20 && + event.end.v == 1.0); + saw_logical_precedence_motion = saw_logical_precedence_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 21 && + event.end.w == 0.0); + saw_equality_tolerance_motion = saw_equality_tolerance_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 22 && + event.end.x == 1.0); + } + ok &= expect(saw_power_expression_motion, "expected ** expression operator support"); + ok &= expect(saw_power_precedence_motion, "expected ** precedence above multiplicative operators"); + ok &= expect(saw_mod_expression_motion, "expected MOD expression operator support"); + ok &= expect(saw_negative_mod_motion, "expected MOD to return positive remainder for negative left operand"); + ok &= expect(saw_mod_left_associative_motion, "expected MOD to share multiplicative left associativity"); + ok &= expect(saw_comparison_expression_motion, "expected LT comparison expression operator support"); + ok &= expect(saw_comparison_precedence_motion, "expected comparison precedence below additive operators"); + ok &= expect(saw_logical_expression_motion, "expected AND/OR logical expression operator support"); + ok &= expect(saw_logical_precedence_motion, "expected logical operators to share LinuxCNC left associativity"); + ok &= expect(saw_equality_tolerance_motion, "expected EQ to use LinuxCNC equality tolerance"); + + const char oword_conditional_program[] = + "G21 G90\n" + "F100\n" + "#1 = 2\n" + "O100 if [#1 EQ 1]\n" + "G1 X99\n" + "O100 elseif [#1 EQ 2]\n" + "G1 X2\n" + "O100 else\n" + "G1 X98\n" + "O100 endif\n" + "O101 if [0]\n" + "G1 Y99\n" + "O101 else\n" + "G1 Y3\n" + "O101 endif\n" + "O102 if [1]\n" + "O103 if [0]\n" + "G1 Z99\n" + "O103 else\n" + "G1 Z4\n" + "O103 endif\n" + "O102 endif\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, oword_conditional_program, sizeof(oword_conditional_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_elseif_branch_motion = saw_elseif_branch_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 7 && + event.end.x == 2.0); + saw_else_branch_motion = saw_else_branch_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 14 && + event.end.y == 3.0); + saw_nested_if_branch_motion = saw_nested_if_branch_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 20 && + event.end.z == 4.0); + ok &= expect(!(event.type == CNC_SIM_EVENT_LINEAR_FEED && + (event.end.x == 99.0 || event.end.x == 98.0 || + event.end.y == 99.0 || event.end.z == 99.0)), + "expected skipped O-word conditional branch to emit no motion"); + } + ok &= expect(saw_elseif_branch_motion, "expected O-word elseif branch execution"); + ok &= expect(saw_else_branch_motion, "expected O-word else branch execution"); + ok &= expect(saw_nested_if_branch_motion, "expected nested O-word conditional branch execution"); + + const char oword_while_program[] = + "G21 G90\n" + "F100\n" + "#1 = 0\n" + "O200 while [#1 LT 3]\n" + "G1 X#1\n" + "#1 = [#1 + 1]\n" + "O200 endwhile\n" + "G1 Y#1\n" + "O201 while [0]\n" + "G1 Z99\n" + "O201 endwhile\n" + "G1 Z5\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, oword_while_program, sizeof(oword_while_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + if (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 5 && + (event.end.x == 0.0 || event.end.x == 1.0 || event.end.x == 2.0)) { + ++while_loop_motion_count; + } + saw_while_final_parameter_motion = saw_while_final_parameter_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 8 && + event.end.y == 3.0); + saw_false_while_skip_motion = saw_false_while_skip_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 12 && + event.end.z == 5.0); + ok &= expect(!(event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 10 && + event.end.z == 99.0), + "expected false O-word while body to emit no motion"); + } + ok &= expect(while_loop_motion_count == 3, "expected O-word while body to execute three times"); + ok &= expect(saw_while_final_parameter_motion, "expected O-word while to update loop parameter"); + ok &= expect(saw_false_while_skip_motion, "expected false O-word while to skip to following motion"); + + const char oword_loop_control_program[] = + "G21 G90\n" + "F100\n" + "#1 = 0\n" + "#2 = 0\n" + "O300 while [#1 LT 5]\n" + "#1 = [#1 + 1]\n" + "O300 if [#1 LT 3]\n" + "O300 continue\n" + "O300 endif\n" + "#2 = [#2 + 1]\n" + "O300 if [#1 GE 4]\n" + "O300 break\n" + "O300 endif\n" + "G1 X#1\n" + "O300 endwhile\n" + "G1 Y#1\n" + "G1 Z#2\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, oword_loop_control_program, sizeof(oword_loop_control_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + if (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 14 && + event.end.x == 3.0) { + ++while_continue_break_motion_count; + } + saw_continue_break_final_counter_motion = saw_continue_break_final_counter_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 16 && + event.end.y == 4.0); + saw_continue_break_body_counter_motion = saw_continue_break_body_counter_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 17 && + event.end.z == 2.0); + ok &= expect(!(event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 14 && + event.end.x == 4.0), + "expected O-word break to skip remaining loop body motion"); + } + ok &= expect(while_continue_break_motion_count == 1, "expected O-word continue/break loop to emit one body motion"); + ok &= expect(saw_continue_break_final_counter_motion, "expected O-word break to preserve loop counter"); + ok &= expect(saw_continue_break_body_counter_motion, "expected O-word continue to skip body counter updates"); + + const char oword_repeat_program[] = + "G21 G90\n" + "F100\n" + "#1 = 3\n" + "#2 = 0\n" + "O400 repeat [#1]\n" + "#1 = [#1 + 1]\n" + "#2 = [#2 + 1]\n" + "G1 X#2\n" + "O400 endrepeat\n" + "G1 Y#1\n" + "G1 Z#2\n" + "O401 repeat [0]\n" + "G1 A99\n" + "O401 endrepeat\n" + "#3 = 0\n" + "#4 = 0\n" + "O402 repeat [5]\n" + "#3 = [#3 + 1]\n" + "O402 if [#3 LT 3]\n" + "O402 continue\n" + "O402 endif\n" + "#4 = [#4 + 1]\n" + "O402 if [#3 GE 4]\n" + "O402 break\n" + "O402 endif\n" + "G1 A#3\n" + "O402 endrepeat\n" + "G1 B#3\n" + "G1 C#4\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, oword_repeat_program, sizeof(oword_repeat_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + if (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 8 && + (event.end.x == 1.0 || event.end.x == 2.0 || event.end.x == 3.0)) { + ++repeat_loop_motion_count; + } + saw_repeat_expression_latched_motion = saw_repeat_expression_latched_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 10 && + event.end.y == 6.0); + saw_repeat_counter_motion = saw_repeat_counter_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 11 && + event.end.z == 3.0); + if (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 26 && + event.end.a == 3.0) { + ++repeat_continue_break_motion_count; + } + saw_repeat_continue_break_final_counter_motion = saw_repeat_continue_break_final_counter_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 28 && + event.end.b == 4.0); + saw_repeat_continue_break_body_counter_motion = saw_repeat_continue_break_body_counter_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 29 && + event.end.c == 2.0); + ok &= expect(!(event.type == CNC_SIM_EVENT_LINEAR_FEED && + ((event.line == 13 && event.end.a == 99.0) || + (event.line == 26 && event.end.a == 4.0))), + "expected skipped O-word repeat branch to emit no motion"); + } + ok &= expect(repeat_loop_motion_count == 3, "expected O-word repeat body to execute three times"); + ok &= expect(saw_repeat_expression_latched_motion, "expected O-word repeat count to be latched on entry"); + ok &= expect(saw_repeat_counter_motion, "expected O-word repeat body counter to update three times"); + ok &= expect(repeat_continue_break_motion_count == 1, "expected O-word repeat continue/break loop to emit one body motion"); + ok &= expect(saw_repeat_continue_break_final_counter_motion, "expected O-word repeat break to preserve loop counter"); + ok &= expect(saw_repeat_continue_break_body_counter_motion, "expected O-word repeat continue to skip body counter updates"); + + const char oword_do_while_program[] = + "G21 G90\n" + "F100\n" + "#1 = 0\n" + "O500 do\n" + "#1 = [#1 + 1]\n" + "G1 X#1\n" + "O500 while [#1 LT 3]\n" + "#2 = 0\n" + "O501 do\n" + "#2 = [#2 + 1]\n" + "G1 Y#2\n" + "O501 while [0]\n" + "#3 = 0\n" + "#4 = 0\n" + "O502 do\n" + "#3 = [#3 + 1]\n" + "O502 if [#3 LT 3]\n" + "O502 continue\n" + "O502 endif\n" + "#4 = [#4 + 1]\n" + "O502 if [#3 GE 4]\n" + "O502 break\n" + "O502 endif\n" + "G1 Z#3\n" + "O502 while [#3 LT 5]\n" + "G1 A#3\n" + "G1 B#4\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, oword_do_while_program, sizeof(oword_do_while_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + if (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 6 && + (event.end.x == 1.0 || event.end.x == 2.0 || event.end.x == 3.0)) { + ++do_while_motion_count; + } + saw_do_while_false_once_motion = saw_do_while_false_once_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 11 && + event.end.y == 1.0); + saw_do_continue_break_final_counter_motion = saw_do_continue_break_final_counter_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 26 && + event.end.a == 4.0); + saw_do_continue_break_body_counter_motion = saw_do_continue_break_body_counter_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 27 && + event.end.b == 2.0); + ok &= expect(!(event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 24 && + event.end.z == 4.0), + "expected O-word do break to skip remaining loop body motion"); + } + ok &= expect(do_while_motion_count == 3, "expected O-word do while body to execute three times"); + ok &= expect(saw_do_while_false_once_motion, "expected O-word do while false condition to execute body once"); + ok &= expect(saw_do_continue_break_final_counter_motion, "expected O-word do break to preserve loop counter"); + ok &= expect(saw_do_continue_break_body_counter_motion, "expected O-word do continue to skip body counter updates"); + + const char mixed_subprogram_styles[] = + "G21\n" + "M98 P100\n" + "M30\n" + "O100 sub\n" + "G1 X1\n" + "O100 endsub\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, mixed_subprogram_styles, sizeof(mixed_subprogram_styles) - 1) != 0, + "expected M98 calling O-word subprogram to fail"); + const char linuxcnc_config[] = "{\"backend\":\"linuxcnc-rs274\"}"; config_rc = cnc_sim_load_config_json(sim, linuxcnc_config, sizeof(linuxcnc_config) - 1); rc = cnc_sim_parse_program(sim, program, sizeof(program) - 1); diff --git a/core/tests/linuxcnc_canon_bridge_smoke.cpp b/core/tests/linuxcnc_canon_bridge_smoke.cpp index 6aef0ee..4023644 100644 --- a/core/tests/linuxcnc_canon_bridge_smoke.cpp +++ b/core/tests/linuxcnc_canon_bridge_smoke.cpp @@ -49,6 +49,10 @@ int main() { SET_G5X_OFFSET(1, 10.0, 20.0, 30.0, 1.0, 2.0, 3.0, 0.0, 0.0, 0.0); SET_G92_OFFSET(1.0, 2.0, 3.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0); SET_XY_ROTATION(15.0); + EmcPose tool_offset{}; + tool_offset.tran.z = 12.5; + cnc_sim_linuxcnc_set_current_line(19); + USE_TOOL_LENGTH_OFFSET(tool_offset); cnc_sim_linuxcnc_set_current_line(20); COMMENT("bridge comment"); cnc_sim_linuxcnc_set_current_line(24); @@ -83,6 +87,7 @@ int main() { bool saw_g5x = false; bool saw_g92 = false; bool saw_rotation = false; + bool saw_tool_length = false; bool saw_comment = false; bool saw_mist_on = false; bool saw_flood_on = false; @@ -131,6 +136,10 @@ int main() { event.start.z == 3.0); saw_rotation = saw_rotation || (event.type == CNC_SIM_EVENT_SET_XY_ROTATION && event.feed == 15.0); + saw_tool_length = saw_tool_length || (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 19 && + event.reserved == 430 && + event.start.z == 12.5); saw_comment = saw_comment || (event.type == CNC_SIM_EVENT_COMMENT && event.line == 20); saw_mist_on = saw_mist_on || (event.type == CNC_SIM_EVENT_COMMENT && @@ -169,6 +178,7 @@ int main() { ok &= expect(saw_g5x, "expected G5X offset event"); ok &= expect(saw_g92, "expected G92 offset event"); ok &= expect(saw_rotation, "expected XY rotation event"); + ok &= expect(saw_tool_length, "expected tool length offset event"); ok &= expect(saw_comment, "expected comment event line"); ok &= expect(saw_mist_on, "expected mist on state"); ok &= expect(saw_flood_on, "expected flood on state"); diff --git a/core/tools/linuxcnc_rs274_dump.cpp b/core/tools/linuxcnc_rs274_dump.cpp index 7fb1ae7..5f8f333 100644 --- a/core/tools/linuxcnc_rs274_dump.cpp +++ b/core/tools/linuxcnc_rs274_dump.cpp @@ -136,14 +136,21 @@ bool execute_line(InterpBase *interp, cnc_sim_linuxcnc_set_current_line(line_number); std::vector control_actions; if (parse_simulator_gcode_control_line(line, &control_actions)) { + bool simulator_only_line = true; for (const auto &action : control_actions) { emit_simulator_gcode_control_action(sink, action, line_number); + if (action.kind == SimulatorGcodeControlKind::RtcpState && + !action.rtcp_enabled) { + simulator_only_line = false; + } if (sink.callback_aborted()) { std::cerr << "event callback aborted parsing\n"; return false; } } - return true; + if (simulator_only_line) { + return true; + } } if (trace_enabled()) { @@ -183,6 +190,54 @@ bool execute_line(InterpBase *interp, return true; } +bool execute_open_file(InterpBase *interp, const char *path) { + if (trace_enabled()) { + std::cerr << "rs274:open " << path << '\n'; + } + int rc = interp->open(path); + if (rc != 0) { + char error[1024]{}; + interp->error_text(rc, error, sizeof(error)); + std::cerr << "open failed: " << error << '\n'; + return false; + } + + bool ok = true; + bool program_done = false; + while (!program_done) { + rc = interp->read(); + if (rc == INTERP_ENDFILE || rc == INTERP_EXIT) { + break; + } + if (rc != 0) { + char error[1024]{}; + interp->error_text(rc, error, sizeof(error)); + std::cerr << "read failed: " << error << '\n'; + ok = false; + break; + } + + cnc_sim_linuxcnc_set_current_line(interp->line()); + if (trace_enabled()) { + std::cerr << "rs274:execute file line " << interp->line() << '\n'; + } + rc = interp->execute(); + if (rc == INTERP_EXIT || rc == INTERP_ENDFILE) { + program_done = true; + } else if (rc == INTERP_EXECUTE_FINISH || rc == INTERP_OK) { + continue; + } else { + char error[1024]{}; + interp->error_text(rc, error, sizeof(error)); + std::cerr << "execute failed: " << error << '\n'; + ok = false; + break; + } + } + interp->close(); + return ok; +} + void init_minimal_tooldata() { tool_mmap_creator(nullptr, 0); tooldata_reset(); @@ -202,7 +257,10 @@ void init_minimal_tooldata() { } // namespace int main(int argc, char **argv) { - const std::string program = read_all(argc > 1 ? argv[1] : "-"); + const bool use_file_mode = std::getenv("CNC_SIM_RS274_FILE_MODE") != nullptr && + argc > 1 && + std::string(argv[1]) != "-"; + const std::string program = use_file_mode ? std::string{} : read_all(argc > 1 ? argv[1] : "-"); if (const char *parameter_file = std::getenv("CNC_SIM_RS274_VAR")) { SET_PARAMETER_FILE_NAME(parameter_file); } @@ -230,16 +288,20 @@ int main(int argc, char **argv) { std::cerr << "rs274:init done\n"; } - std::istringstream input(program); - std::string line; bool ok = true; - bool program_done = false; - int line_number = 0; - while (!program_done && std::getline(input, line)) { - ++line_number; - if (!execute_line(interp, sink, line, line_number, &program_done)) { - ok = false; - break; + if (use_file_mode) { + ok = execute_open_file(interp, argv[1]); + } else { + std::istringstream input(program); + std::string line; + bool program_done = false; + int line_number = 0; + while (!program_done && std::getline(input, line)) { + ++line_number; + if (!execute_line(interp, sink, line, line_number, &program_done)) { + ok = false; + break; + } } } std::cout << "\n]\n"; diff --git a/docs/linuxcnc-porting.md b/docs/linuxcnc-porting.md index 8c7924c..bbe9e05 100644 --- a/docs/linuxcnc-porting.md +++ b/docs/linuxcnc-porting.md @@ -29,6 +29,10 @@ This parser only exists to test the ABI and UI before Emscripten and LinuxCNC ar - `G4 P...` - `F`, `S`, `T`, `M3`, `M4`, `M5`, `M6`, `M2`, `M30` - IJK and R arcs +- numbered and named parameter assignment/reference such as `#1 = ...`, `# = ...`, `X#1`, and nested expressions like `X[[# + 2] * 3]` +- expression functions `ABS[]`, `SQRT[]`, `EXP[]`, `LN[]`, degree-based `SIN[]`/`COS[]`/`TAN[]`/`ASIN[]`/`ACOS[]`, LinuxCNC-style `ATAN[]/[]`, and `FIX[]`/`FUP[]`/`ROUND[]` +- numeric and named O-word subprograms such as `O100 call` and `O call` +- canned cycles `G73`, `G81`, `G82`, `G83`, `G85`, `G86`, `G89` with `G80`, `G98`, `G99`, `L` It is not the production interpreter. @@ -189,6 +193,6 @@ This matrix tracks LinuxCNC feature coverage for the web/WASM simulator. A featu | Canned cycle `G81/G80` | covered for drilling expand-to-canon path | `tests/gcode/linuxcnc_canned_cycle.ngc` | | Coordinate offset Canon events | bridge-level covered | `core/tests/linuxcnc_canon_bridge_smoke.cpp` | | Cutter compensation | pending | add LinuxCNC corpus and tolerance checks | -| O-word subroutines and calls | pending | current line-by-line runner does not execute sub bodies like LinuxCNC task planner | -| Broader canned cycles `G82`-`G89` | pending | add corpus after `G81` baseline | +| O-word subroutines and calls | covered for numeric `O... sub/call/return/endsub` through LinuxCNC file mode; smoke parser also covers named `O` sub/call/return/endsub | `tests/gcode/smoke_oword_subprogram.ngc` | +| Broader canned cycles `G73`, `G82`-`G89` | partially covered: `G73`, `G82`, `G83`, `G85`, `G86`, `G89` | `tests/gcode/linuxcnc_canned_cycles_extended.ngc`, smoke API regression | | Full source-level wasm build | pending | replace Python/HAL/INI/tooldata support dependencies | diff --git a/test-linuxcnc-rs274-native.sh b/test-linuxcnc-rs274-native.sh index 8919ada..91fb0c3 100755 --- a/test-linuxcnc-rs274-native.sh +++ b/test-linuxcnc-rs274-native.sh @@ -43,9 +43,12 @@ CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linux CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_coolant.ngc >/tmp/cnc_sim_linuxcnc_coolant.json CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_feed_modes.ngc >/tmp/cnc_sim_linuxcnc_feed_modes.json CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_motion_modes.ngc >/tmp/cnc_sim_linuxcnc_motion_modes.json +CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_tool_length.ngc >/tmp/cnc_sim_linuxcnc_tool_length.json CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_canned_cycle.ngc >/tmp/cnc_sim_linuxcnc_canned_cycle.json +CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_canned_cycles_extended.ngc >/tmp/cnc_sim_linuxcnc_canned_cycles_extended.json CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_comments.ngc >/tmp/cnc_sim_linuxcnc_comments.json CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_probe_no_error.ngc >/tmp/cnc_sim_linuxcnc_probe_no_error.json +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/smoke_oword_subprogram.ngc >/tmp/cnc_sim_linuxcnc_oword_subprogram.json cp "$base_var_file" "$var_file" CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_coordinate_offsets.ngc >/tmp/cnc_sim_linuxcnc_coordinate_offsets.json cp "$base_var_file" "$var_file" @@ -147,6 +150,24 @@ for expected in [(2, 611, 0), (3, 612, 0), (4, 640, 0.05)]: if expected not in motion_mode_states: raise SystemExit(f"missing motion mode state {expected!r}") +tool_length = json.loads(Path("/tmp/cnc_sim_linuxcnc_tool_length.json").read_text()) +if not any( + event["type"] == "comment" and + event["line"] == 2 and + event["reserved"] == 430 and + event["start"]["z"] == 0 + for event in tool_length +): + raise SystemExit("missing G43 H1 tool length offset event") +if not any( + event["type"] == "comment" and + event["line"] == 3 and + event["reserved"] == 430 and + event["start"]["z"] == 0 + for event in tool_length +): + raise SystemExit("missing G49 tool length clear event") + cycle = json.loads(Path("/tmp/cnc_sim_linuxcnc_canned_cycle.json").read_text()) motions = [event for event in cycle if event["type"] in {"rapid", "linear-feed"}] expected_cycle = [ @@ -160,6 +181,38 @@ actual_cycle = [(event["type"], event["end"]["x"], event["end"]["y"], event["end if actual_cycle != expected_cycle: raise SystemExit(f"unexpected G81/G80 expansion: {actual_cycle!r}") +extended_cycle = json.loads(Path("/tmp/cnc_sim_linuxcnc_canned_cycles_extended.json").read_text()) +if not any( + event["type"] == "linear-feed" and + event["line"] == 3 and + event["end"]["x"] == 10 and + event["end"]["z"] == -2 + for event in extended_cycle +): + raise SystemExit("missing G82 feed to bottom") +if not any( + event["type"] == "dwell" and + event["line"] == 3 and + event["dwellSeconds"] == 0.25 + for event in extended_cycle +): + raise SystemExit("missing G82 dwell at bottom") +g83_feed_depths = [ + event["end"]["z"] + for event in extended_cycle + if event["type"] == "linear-feed" and event["line"] == 4 +] +if g83_feed_depths != [1, 0, -1, -2, -3]: + raise SystemExit(f"unexpected G83 peck depths: {g83_feed_depths!r}") +if not any( + event["type"] == "rapid" and + event["line"] == 4 and + event["end"]["x"] == 20 and + event["end"]["z"] == 2 + for event in extended_cycle +): + raise SystemExit("missing G83 final retract to R plane") + comments = json.loads(Path("/tmp/cnc_sim_linuxcnc_comments.json").read_text()) if not any(event["type"] == "comment" and event["line"] == 2 for event in comments): raise SystemExit("missing LinuxCNC comment line event") @@ -177,6 +230,31 @@ if not any( if any(event["type"] == "linear-feed" and event["line"] == 4 for event in probe): raise SystemExit("G38.3 probe should not be emitted as linear-feed") +oword = json.loads(Path("/tmp/cnc_sim_linuxcnc_oword_subprogram.json").read_text()) +if not any( + event["type"] == "linear-feed" and + event["line"] == 8 and + event["start"]["x"] == 0 and + event["end"]["x"] == 7 + for event in oword +): + raise SystemExit("missing LinuxCNC O-word expression motion") +if not any( + event["type"] == "linear-feed" and + event["line"] == 9 and + event["start"]["y"] == 0 and + event["end"]["y"] == 2 + for event in oword +): + raise SystemExit("missing LinuxCNC O-word #2 argument motion") +if any( + event["type"] == "linear-feed" and + event["line"] == 11 and + event["end"]["x"] == 99 + for event in oword +): + raise SystemExit("LinuxCNC O-word return should skip remaining subprogram body") + coords = json.loads(Path("/tmp/cnc_sim_linuxcnc_coordinate_offsets.json").read_text()) g5x = [event for event in coords if event["type"] == "set-g5x-offset"] g92 = [event for event in coords if event["type"] == "set-g92-offset"] @@ -228,8 +306,11 @@ echo "dumped /tmp/cnc_sim_linuxcnc_program_stops.json" echo "dumped /tmp/cnc_sim_linuxcnc_coolant.json" echo "dumped /tmp/cnc_sim_linuxcnc_feed_modes.json" echo "dumped /tmp/cnc_sim_linuxcnc_motion_modes.json" +echo "dumped /tmp/cnc_sim_linuxcnc_tool_length.json" echo "dumped /tmp/cnc_sim_linuxcnc_canned_cycle.json" +echo "dumped /tmp/cnc_sim_linuxcnc_canned_cycles_extended.json" echo "dumped /tmp/cnc_sim_linuxcnc_comments.json" echo "dumped /tmp/cnc_sim_linuxcnc_probe_no_error.json" +echo "dumped /tmp/cnc_sim_linuxcnc_oword_subprogram.json" echo "dumped /tmp/cnc_sim_linuxcnc_coordinate_offsets.json" echo "dumped /tmp/cnc_sim_linuxcnc_coordinate_l20.json" diff --git a/test-linuxcnc-source-link.sh b/test-linuxcnc-source-link.sh index dc1a033..215f1ff 100755 --- a/test-linuxcnc-source-link.sh +++ b/test-linuxcnc-source-link.sh @@ -103,6 +103,12 @@ CNC_SIM_RS274_VAR="$var_file" \ CNC_SIM_RS274_VAR="$var_file" \ "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_canned_cycle.ngc \ >/tmp/cnc_sim_linuxcnc_source_canned_cycle.json +CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_canned_cycles_extended.ngc \ + >/tmp/cnc_sim_linuxcnc_source_canned_cycles_extended.json +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/smoke_oword_subprogram.ngc \ + >/tmp/cnc_sim_linuxcnc_source_oword_subprogram.json python3 - <<'PY' import json @@ -149,9 +155,68 @@ expected_cycle = [ actual_cycle = [(event["type"], event["end"]["x"], event["end"]["y"], event["end"]["z"]) for event in motions] if actual_cycle != expected_cycle: raise SystemExit(f"unexpected source-linked G81/G80 expansion: {actual_cycle!r}") + +extended_cycle = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_canned_cycles_extended.json").read_text()) +if not any( + event["type"] == "linear-feed" and + event["line"] == 3 and + event["end"]["x"] == 10 and + event["end"]["z"] == -2 + for event in extended_cycle +): + raise SystemExit("missing source-linked G82 feed to bottom") +if not any( + event["type"] == "dwell" and + event["line"] == 3 and + event["dwellSeconds"] == 0.25 + for event in extended_cycle +): + raise SystemExit("missing source-linked G82 dwell at bottom") +g83_feed_depths = [ + event["end"]["z"] + for event in extended_cycle + if event["type"] == "linear-feed" and event["line"] == 4 +] +if g83_feed_depths != [1, 0, -1, -2, -3]: + raise SystemExit(f"unexpected source-linked G83 peck depths: {g83_feed_depths!r}") +if not any( + event["type"] == "rapid" and + event["line"] == 4 and + event["end"]["x"] == 20 and + event["end"]["z"] == 2 + for event in extended_cycle +): + raise SystemExit("missing source-linked G83 final retract to R plane") + +oword = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_oword_subprogram.json").read_text()) +if not any( + event["type"] == "linear-feed" and + event["line"] == 8 and + event["start"]["x"] == 0 and + event["end"]["x"] == 7 + for event in oword +): + raise SystemExit("missing source-linked O-word expression motion") +if not any( + event["type"] == "linear-feed" and + event["line"] == 9 and + event["start"]["y"] == 0 and + event["end"]["y"] == 2 + for event in oword +): + raise SystemExit("missing source-linked O-word #2 argument motion") +if any( + event["type"] == "linear-feed" and + event["line"] == 11 and + event["end"]["x"] == 99 + for event in oword +): + raise SystemExit("source-linked O-word return should skip remaining subprogram body") PY echo "linuxcnc rs274 source link smoke passed (${#objects[@]} local objects)" echo "dumped /tmp/cnc_sim_linuxcnc_source_basic_mill.json" echo "dumped /tmp/cnc_sim_linuxcnc_source_rtcp_controls.json" echo "dumped /tmp/cnc_sim_linuxcnc_source_canned_cycle.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_canned_cycles_extended.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_oword_subprogram.json" diff --git a/test-native.sh b/test-native.sh index 0279b87..817daa6 100755 --- a/test-native.sh +++ b/test-native.sh @@ -51,7 +51,11 @@ mkdir -p "$build_dir" "$build_dir/cnc_sim_api_smoke" "$build_dir/cnc_sim_dump" tests/gcode/basic_mill.ngc >/tmp/cnc_sim_basic_mill.json "$build_dir/cnc_sim_dump" tests/gcode/incremental_and_r_arc.ngc >/tmp/cnc_sim_incremental_and_r_arc.json +"$build_dir/cnc_sim_dump" tests/gcode/smoke_subprogram_m98.ngc >/tmp/cnc_sim_smoke_subprogram_m98.json +"$build_dir/cnc_sim_dump" tests/gcode/smoke_oword_subprogram.ngc >/tmp/cnc_sim_smoke_oword_subprogram.json echo "native tests passed" echo "dumped /tmp/cnc_sim_basic_mill.json" echo "dumped /tmp/cnc_sim_incremental_and_r_arc.json" +echo "dumped /tmp/cnc_sim_smoke_subprogram_m98.json" +echo "dumped /tmp/cnc_sim_smoke_oword_subprogram.json" diff --git a/tests/gcode/linuxcnc_canned_cycles_extended.ngc b/tests/gcode/linuxcnc_canned_cycles_extended.ngc new file mode 100644 index 0000000..fd24e28 --- /dev/null +++ b/tests/gcode/linuxcnc_canned_cycles_extended.ngc @@ -0,0 +1,6 @@ +G21 G90 G17 +G0 X0 Y0 Z10 +G99 G82 X10 Y0 Z-2 R3 P0.25 F100 +G83 X20 Y0 Z-3 R2 Q1 +G80 +M30 diff --git a/tests/gcode/linuxcnc_tool_length.ngc b/tests/gcode/linuxcnc_tool_length.ngc new file mode 100644 index 0000000..1fd055c --- /dev/null +++ b/tests/gcode/linuxcnc_tool_length.ngc @@ -0,0 +1,4 @@ +G21 G90 G17 +G43 H1 +G49 G0 X0 +M30 diff --git a/tests/gcode/smoke_oword_subprogram.ngc b/tests/gcode/smoke_oword_subprogram.ngc new file mode 100644 index 0000000..2631805 --- /dev/null +++ b/tests/gcode/smoke_oword_subprogram.ngc @@ -0,0 +1,12 @@ +G21 G90 +G0 X0 Y0 Z0 +F100 +O100 call [2 + 3] [8 / 4] +G1 X20 +M30 +O100 sub +G1 X[#1 + #2] +G1 Y#2 +O100 return +G1 X99 +O100 endsub diff --git a/tests/gcode/smoke_subprogram_m98.ngc b/tests/gcode/smoke_subprogram_m98.ngc new file mode 100644 index 0000000..82fe234 --- /dev/null +++ b/tests/gcode/smoke_subprogram_m98.ngc @@ -0,0 +1,10 @@ +G21 G90 +G0 X0 Y0 Z0 +F100 +M98 P100 L2 +G1 X20 +M30 +O100 +G91 G1 X5 +G90 +M99