openscad-cpp-evaluator 0.6.0__tar.gz → 0.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/PKG-INFO +1 -1
  2. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/bindings/module.cpp +47 -11
  3. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/include/openscad_cpp_evaluator/evaluator.hpp +65 -5
  4. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/pyproject.toml +1 -1
  5. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/python/openscad_cpp_evaluator/__init__.py +15 -3
  6. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/src/bytecode_vm.cpp +29 -0
  7. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/src/debug_profile.cpp +30 -0
  8. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/src/user_calls.cpp +26 -0
  9. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/tests/test_debug_hooks.cpp +120 -0
  10. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/tests/test_tail_calls.cpp +29 -16
  11. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/.github/workflows/ci.yml +0 -0
  12. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/.github/workflows/wheels.yml +0 -0
  13. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/.gitignore +0 -0
  14. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/.gitmodules +0 -0
  15. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/CLAUDE.md +0 -0
  16. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/CMakeLists.txt +0 -0
  17. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/LICENSE +0 -0
  18. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/README.md +0 -0
  19. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/bindings/CMakeLists.txt +0 -0
  20. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/cmake/embed_font.cmake +0 -0
  21. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/examples/CMakeLists.txt +0 -0
  22. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/examples/eval_perf_benchmark.cpp +0 -0
  23. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/examples/manifold_cache_reuse.cpp +0 -0
  24. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/examples/minimal_debugger.cpp +0 -0
  25. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/.github/workflows/ci.yml +0 -0
  26. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/.gitignore +0 -0
  27. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/CLAUDE.md +0 -0
  28. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/CMakeLists.txt +0 -0
  29. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/README.md +0 -0
  30. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/api.hpp +0 -0
  31. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/ast_node.hpp +0 -0
  32. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/comments.hpp +0 -0
  33. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/declarations.hpp +0 -0
  34. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/expression.hpp +0 -0
  35. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/module_instantiation.hpp +0 -0
  36. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/scope_builder.hpp +0 -0
  37. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/vector_element.hpp +0 -0
  38. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast.hpp +0 -0
  39. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/comments.hpp +0 -0
  40. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/position.hpp +0 -0
  41. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/pretty_print.hpp +0 -0
  42. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/scope.hpp +0 -0
  43. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/serialization.hpp +0 -0
  44. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/source_map.hpp +0 -0
  45. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/src/CMakeLists.txt +0 -0
  46. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/src/api.cpp +0 -0
  47. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/src/ast/ast_node.cpp +0 -0
  48. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/src/ast/declarations.cpp +0 -0
  49. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/src/ast/expression.cpp +0 -0
  50. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/src/ast/format_utils.hpp +0 -0
  51. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/src/ast/module_instantiation.cpp +0 -0
  52. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/src/ast/scope_builder.cpp +0 -0
  53. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/src/ast/vector_element.cpp +0 -0
  54. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/src/comment_attach_internal.hpp +0 -0
  55. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/src/comments.cpp +0 -0
  56. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/src/grammar/driver.cpp +0 -0
  57. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/src/grammar/driver.hpp +0 -0
  58. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/src/grammar/lexer.l +0 -0
  59. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/src/grammar/lexer_api.hpp +0 -0
  60. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/src/grammar/oscad_location.hpp +0 -0
  61. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/src/grammar/parser.y +0 -0
  62. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/src/inline_comment_attach.cpp +0 -0
  63. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/src/position.cpp +0 -0
  64. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/src/pretty_print.cpp +0 -0
  65. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/src/scope.cpp +0 -0
  66. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/src/serialization/json_io.cpp +0 -0
  67. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/src/serialization/yaml_io.cpp +0 -0
  68. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/src/source_map.cpp +0 -0
  69. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/tests/CMakeLists.txt +0 -0
  70. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/tests/test_assignments.cpp +0 -0
  71. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/tests/test_ast_generation.cpp +0 -0
  72. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/tests/test_comments_and_files.cpp +0 -0
  73. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/tests/test_const_borrow.cpp +0 -0
  74. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/tests/test_control.cpp +0 -0
  75. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/tests/test_expressions.cpp +0 -0
  76. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/tests/test_functions.cpp +0 -0
  77. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/tests/test_helpers.hpp +0 -0
  78. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/tests/test_inline_comments.cpp +0 -0
  79. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/tests/test_lexical.cpp +0 -0
  80. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/tests/test_modules.cpp +0 -0
  81. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/tests/test_node_str.cpp +0 -0
  82. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/tests/test_pretty_print.cpp +0 -0
  83. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/tests/test_scope.cpp +0 -0
  84. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/tests/test_serialization.cpp +0 -0
  85. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/tests/test_smoke.cpp +0 -0
  86. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/tests/test_source_map.cpp +0 -0
  87. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/tests/test_use_include.cpp +0 -0
  88. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/tests/test_vectors.cpp +0 -0
  89. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/tools/cli/CMakeLists.txt +0 -0
  90. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/external/openscad_cpp_parser/tools/cli/main.cpp +0 -0
  91. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/include/openscad_cpp_evaluator/bound_args.hpp +0 -0
  92. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/include/openscad_cpp_evaluator/bytecode.hpp +0 -0
  93. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/include/openscad_cpp_evaluator/bytecode_compiler.hpp +0 -0
  94. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/include/openscad_cpp_evaluator/bytecode_vm.hpp +0 -0
  95. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/include/openscad_cpp_evaluator/call_args.hpp +0 -0
  96. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/include/openscad_cpp_evaluator/colored_body.hpp +0 -0
  97. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/include/openscad_cpp_evaluator/csg_node.hpp +0 -0
  98. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/include/openscad_cpp_evaluator/css_colors.hpp +0 -0
  99. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/include/openscad_cpp_evaluator/debug_hooks.hpp +0 -0
  100. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/include/openscad_cpp_evaluator/debug_repl.hpp +0 -0
  101. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/include/openscad_cpp_evaluator/dispatch.hpp +0 -0
  102. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/include/openscad_cpp_evaluator/dxf_svg_import.hpp +0 -0
  103. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/include/openscad_cpp_evaluator/eval_context.hpp +0 -0
  104. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/include/openscad_cpp_evaluator/eval_error.hpp +0 -0
  105. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/include/openscad_cpp_evaluator/eval_use.hpp +0 -0
  106. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/include/openscad_cpp_evaluator/export.hpp +0 -0
  107. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/include/openscad_cpp_evaluator/font_provider.hpp +0 -0
  108. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/include/openscad_cpp_evaluator/function_builtins.hpp +0 -0
  109. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/include/openscad_cpp_evaluator/import_builtin.hpp +0 -0
  110. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/include/openscad_cpp_evaluator/manifold_cache.hpp +0 -0
  111. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/include/openscad_cpp_evaluator/mesh_import.hpp +0 -0
  112. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/include/openscad_cpp_evaluator/osc_range.hpp +0 -0
  113. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/include/openscad_cpp_evaluator/profile.hpp +0 -0
  114. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/include/openscad_cpp_evaluator/scope_trail.hpp +0 -0
  115. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/include/openscad_cpp_evaluator/segments.hpp +0 -0
  116. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/include/openscad_cpp_evaluator/stb_font_provider.hpp +0 -0
  117. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/include/openscad_cpp_evaluator/surface_load.hpp +0 -0
  118. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/include/openscad_cpp_evaluator/text_metrics.hpp +0 -0
  119. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/include/openscad_cpp_evaluator/value.hpp +0 -0
  120. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/include/openscad_cpp_evaluator/zip_stored.hpp +0 -0
  121. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/resources/fonts/LICENSE +0 -0
  122. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/resources/fonts/LiberationSans-Regular.ttf +0 -0
  123. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/src/CMakeLists.txt +0 -0
  124. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/src/builtins/booleans.cpp +0 -0
  125. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/src/builtins/builtins.hpp +0 -0
  126. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/src/builtins/call_args.cpp +0 -0
  127. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/src/builtins/color.cpp +0 -0
  128. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/src/builtins/control.cpp +0 -0
  129. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/src/builtins/extrude.cpp +0 -0
  130. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/src/builtins/function_builtins.cpp +0 -0
  131. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/src/builtins/import.cpp +0 -0
  132. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/src/builtins/modifiers.cpp +0 -0
  133. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/src/builtins/primitives_2d.cpp +0 -0
  134. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/src/builtins/primitives_3d.cpp +0 -0
  135. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/src/builtins/registry.cpp +0 -0
  136. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/src/builtins/roof.cpp +0 -0
  137. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/src/builtins/segments.cpp +0 -0
  138. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/src/builtins/surface.cpp +0 -0
  139. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/src/builtins/text.cpp +0 -0
  140. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/src/builtins/topology.cpp +0 -0
  141. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/src/builtins/transforms.cpp +0 -0
  142. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/src/bytecode_compiler.cpp +0 -0
  143. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/src/colored_body.cpp +0 -0
  144. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/src/csg_generate.cpp +0 -0
  145. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/src/csg_resolve.cpp +0 -0
  146. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/src/css_colors.cpp +0 -0
  147. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/src/debug/debug_repl.cpp +0 -0
  148. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/src/eval_context.cpp +0 -0
  149. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/src/eval_error.cpp +0 -0
  150. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/src/eval_use.cpp +0 -0
  151. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/src/export.cpp +0 -0
  152. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/src/expr_eval.cpp +0 -0
  153. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/src/import/dxf_import.cpp +0 -0
  154. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/src/import/mesh_import.cpp +0 -0
  155. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/src/import/surface_load.cpp +0 -0
  156. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/src/import/svg_import.cpp +0 -0
  157. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/src/manifold_cache.cpp +0 -0
  158. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/src/stmt_eval.cpp +0 -0
  159. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/src/text/stb_font_provider.cpp +0 -0
  160. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/src/text/text_metrics.cpp +0 -0
  161. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/src/value.cpp +0 -0
  162. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/src/zip_stored.cpp +0 -0
  163. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/tests/CMakeLists.txt +0 -0
  164. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/tests/test_booleans.cpp +0 -0
  165. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/tests/test_bytecode_compiler.cpp +0 -0
  166. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/tests/test_cli.cpp +0 -0
  167. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/tests/test_control_flow.cpp +0 -0
  168. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/tests/test_csg_tree.cpp +0 -0
  169. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/tests/test_dxf_svg_import.cpp +0 -0
  170. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/tests/test_eval_context.cpp +0 -0
  171. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/tests/test_export.cpp +0 -0
  172. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/tests/test_expr_eval.cpp +0 -0
  173. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/tests/test_extrude_roof.cpp +0 -0
  174. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/tests/test_function_builtins.cpp +0 -0
  175. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/tests/test_helpers.hpp +0 -0
  176. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/tests/test_import_export.cpp +0 -0
  177. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/tests/test_manifold_cache.cpp +0 -0
  178. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/tests/test_multi_color_merge.cpp +0 -0
  179. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/tests/test_primitives.cpp +0 -0
  180. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/tests/test_profiling.cpp +0 -0
  181. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/tests/test_python_bindings.py +0 -0
  182. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/tests/test_scope_trail.cpp +0 -0
  183. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/tests/test_scoping.cpp +0 -0
  184. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/tests/test_surface.cpp +0 -0
  185. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/tests/test_text.cpp +0 -0
  186. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/tests/test_transforms.cpp +0 -0
  187. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/tests/test_use.cpp +0 -0
  188. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/tests/test_value.cpp +0 -0
  189. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/tests/test_viewport_params.cpp +0 -0
  190. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/tests/test_zip_stored.cpp +0 -0
  191. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/tools/cli/CMakeLists.txt +0 -0
  192. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/tools/cli/cli_lib.cpp +0 -0
  193. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/tools/cli/cli_lib.hpp +0 -0
  194. {openscad_cpp_evaluator-0.6.0 → openscad_cpp_evaluator-0.7.0}/tools/cli/main.cpp +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openscad_cpp_evaluator
3
- Version: 0.6.0
3
+ Version: 0.7.0
4
4
  Summary: C++ OpenSCAD evaluator with Python bindings
5
5
  Author: NeuSCAD Contributors
6
6
  License-Expression: MIT
@@ -27,6 +27,7 @@
27
27
 
28
28
  #include "openscad_cpp_parser/api.hpp"
29
29
 
30
+ #include <atomic>
30
31
  #include <cstdint>
31
32
  #include <exception>
32
33
  #include <memory>
@@ -362,17 +363,20 @@ using GetChildrenPositionsFn = std::function<const std::optional<std::vector<std
362
363
  // exactly where it already computes should_pause, without needing to
363
364
  // widen hook()'s existing (cmd, mods) return-tuple contract -- mirrors
364
365
  // generate_partial/get_children_positions's own "extra kwarg the Python
365
- // side may or may not call" shape.
366
- using SetFastContinueFn = std::function<void(std::optional<std::unordered_map<std::string, std::set<int>>>)>;
366
+ // side may or may not call" shape. `hookSkippable` mirrors
367
+ // setFastContinueBreakpoints's own second parameter exactly -- see its
368
+ // doc comment for why this is narrower than "breakpoints is accurate."
369
+ using SetFastContinueFn = std::function<void(std::optional<std::unordered_map<std::string, std::set<int>>>, bool)>;
367
370
 
368
371
  // Wraps a SetFastContinueFn as a Python callable taking either a
369
372
  // dict[str, set[int]]/dict[str, list[int]] (origin -> breakpoint lines) or
370
- // None. Mirrors generatePartialTrampoline's own shape.
373
+ // None, plus a hook_skippable bool (default False). Mirrors
374
+ // generatePartialTrampoline's own shape.
371
375
  nb::object setFastContinueTrampoline(const SetFastContinueFn& setFastContinue) {
372
376
  return nb::cpp_function(
373
- [&setFastContinue](nb::object breakpoints) {
377
+ [&setFastContinue](nb::object breakpoints, bool hookSkippable) {
374
378
  if (breakpoints.is_none()) {
375
- setFastContinue(std::nullopt);
379
+ setFastContinue(std::nullopt, hookSkippable);
376
380
  return;
377
381
  }
378
382
  std::unordered_map<std::string, std::set<int>> bp;
@@ -381,7 +385,7 @@ nb::object setFastContinueTrampoline(const SetFastContinueFn& setFastContinue) {
381
385
  for (nb::handle line : v) lines.insert(nb::cast<int>(line));
382
386
  bp.emplace(nb::cast<std::string>(k), std::move(lines));
383
387
  }
384
- setFastContinue(std::move(bp));
388
+ setFastContinue(std::move(bp), hookSkippable);
385
389
  },
386
390
  // Without .none(), nanobind rejects a Python `None` argument for an
387
391
  // `nb::object`-typed parameter registered this ad-hoc way (unlike a
@@ -391,9 +395,33 @@ nb::object setFastContinueTrampoline(const SetFastContinueFn& setFastContinue) {
391
395
  // was called with breakpoints=None (DebugSession's own
392
396
  // _apply_fast_continue calls it with None whenever fast-continue
393
397
  // mode isn't safe right now -- see debugger.py).
394
- nb::arg("breakpoints").none());
398
+ nb::arg("breakpoints").none(), nb::arg("hook_skippable") = false);
395
399
  }
396
400
 
401
+ // Python-visible handle onto a lock-free, GIL-free interrupt flag -- see
402
+ // Evaluator::setFastContinueInterruptFlag's own doc comment (evaluator.hpp)
403
+ // for why this exists at all: debug_evaluate() runs as one single blocking
404
+ // call with the GIL released for its whole duration, so there is no live,
405
+ // Python-callable Evaluator handle DebugSession.pause()/set_breakpoints()
406
+ // (running on the MAIN/GUI thread) could otherwise invoke directly to
407
+ // interrupt hook-skippable mode. A caller creates ONE of these before
408
+ // calling debug_evaluate() (passing it as fast_continue_signal), keeps it
409
+ // around for the whole debug session, and calls .request() from the main
410
+ // thread any time a hook-skippable checkDebug() call needs to stop
411
+ // skipping and actually consult Python again -- Pause, or a breakpoint
412
+ // being toggled in an editor tab while a render is mid-flight. Nothing
413
+ // else needs to read it back on the Python side; C++ test-and-clears it
414
+ // (see checkDebug's own doc comment, debug_profile.cpp) the moment it acts
415
+ // on it, so there's no separate "acknowledge" step.
416
+ class FastContinueSignal {
417
+ public:
418
+ void request() { flag_->store(true, std::memory_order_release); }
419
+ const std::shared_ptr<std::atomic<bool>>& flag() const { return flag_; }
420
+
421
+ private:
422
+ std::shared_ptr<std::atomic<bool>> flag_ = std::make_shared<std::atomic<bool>>(false);
423
+ };
424
+
397
425
  // GIL held (reacquired by the caller). `getFrame`/`callStack`/`generatePartial`/
398
426
  // `getChildrenPositions` are valid only for this synchronous call, so their
399
427
  // closures are only ever invoked from within the Python hook, before it returns.
@@ -470,7 +498,8 @@ void callPyReturnHook(nb::handle returnHook, const std::string& name, const osca
470
498
  // blocks on the Python side (threading.Event) so the GUI thread keeps running.
471
499
  nb::object debugEvaluate(const std::string& path, nb::dict viewportParams, nb::callable debugHook,
472
500
  nb::callable errorBreak, nb::callable echoFn,
473
- std::shared_ptr<oscadeval::ManifoldCache> manifoldCache, nb::object returnHook) {
501
+ std::shared_ptr<oscadeval::ManifoldCache> manifoldCache, nb::object returnHook,
502
+ FastContinueSignal* fastContinueSignal) {
474
503
  std::unordered_map<std::string, oscadeval::Value> vp = toViewportParams(viewportParams);
475
504
 
476
505
  std::vector<oscadeval::ColoredBody> bodies;
@@ -501,8 +530,8 @@ nb::object debugEvaluate(const std::string& path, nb::dict viewportParams, nb::c
501
530
  return evPtr->lastChildrenPositions();
502
531
  };
503
532
  SetFastContinueFn setFastContinue =
504
- [&evPtr](std::optional<std::unordered_map<std::string, std::set<int>>> breakpoints) {
505
- evPtr->setFastContinueBreakpoints(std::move(breakpoints));
533
+ [&evPtr](std::optional<std::unordered_map<std::string, std::set<int>>> breakpoints, bool hookSkippable) {
534
+ evPtr->setFastContinueBreakpoints(std::move(breakpoints), hookSkippable);
506
535
  };
507
536
  oscadeval::DebugHooks hooks;
508
537
  hooks.debugHook = [&](int line, int depth, bool forced, bool exprLevel, const std::string& origin,
@@ -528,6 +557,7 @@ nb::object debugEvaluate(const std::string& path, nb::dict viewportParams, nb::c
528
557
  oscadeval::ResolvedUseScopes used = oscadeval::resolveUseScopes(ast, path, echoCpp);
529
558
  oscadeval::Evaluator ev(echoCpp, nullptr, manifoldCache, hooks, false);
530
559
  evPtr = &ev;
560
+ if (fastContinueSignal) ev.setFastContinueInterruptFlag(fastContinueSignal->flag());
531
561
  oscadeval::EvalContext ctx = oscadeval::EvalContext::makeRoot(used.rootScope.get());
532
562
  bodies = oscadeval::toRenderableBodies(ev.evaluate(used.processedNodes, ctx, vp));
533
563
  collectIdSpans(ev, idSpans);
@@ -602,6 +632,12 @@ NB_MODULE(_openscad_cpp_evaluator, m) {
602
632
  .def(nb::init<>())
603
633
  .def("clear", &oscadeval::ManifoldCache::clear);
604
634
 
635
+ // See FastContinueSignal's own doc comment, above -- only a
636
+ // constructor and request() are ever called from Python.
637
+ nb::class_<FastContinueSignal>(m, "FastContinueSignal")
638
+ .def(nb::init<>())
639
+ .def("request", &FastContinueSignal::request);
640
+
605
641
  m.def("evaluate", &evaluate, nb::arg("path"), nb::arg("viewport_params"), nb::arg("manifold_cache") = nullptr,
606
642
  nb::arg("profile") = false,
607
643
  "Evaluate a .scad file; return (bodies, echoes, id_to_node, csg_tree, profile_result, dyn, dyn_explicit).");
@@ -609,6 +645,6 @@ NB_MODULE(_openscad_cpp_evaluator, m) {
609
645
  "Parse a .scad file; return top-level declaration (namespace, name, start, end, line, column, origin) tuples.");
610
646
  m.def("debug_evaluate", &debugEvaluate, nb::arg("path"), nb::arg("viewport_params"), nb::arg("debug_hook"),
611
647
  nb::arg("error_break"), nb::arg("echo_fn"), nb::arg("manifold_cache") = nullptr,
612
- nb::arg("return_hook") = nb::none(),
648
+ nb::arg("return_hook") = nb::none(), nb::arg("fast_continue_signal") = nullptr,
613
649
  "Evaluate with the debugger wired in; returns (bodies, [], id_to_node, dyn, dyn_explicit). Callbacks fire under the GIL.");
614
650
  }
@@ -15,6 +15,7 @@
15
15
 
16
16
  #include "openscad_cpp_parser/ast.hpp"
17
17
 
18
+ #include <atomic>
18
19
  #include <chrono>
19
20
  #include <cstdint>
20
21
  #include <functional>
@@ -329,12 +330,61 @@ public:
329
330
  // exception at all" (today's original, always-safe behavior) --
330
331
  // e.g. the instant a step command starts or a pause is requested.
331
332
  // Cheap to call as often as the debugger's own state changes: this
332
- // just updates one member, no cache invalidation needed (chunkEligibleNow
333
- // is re-checked on every lookupOrCompileChunk/lookupCompiledLiteralChunk
334
- // call, not cached itself -- only the compiled bytecode ITSELF, which
335
- // never depends on debugger state, is cached forever).
336
- void setFastContinueBreakpoints(std::optional<std::unordered_map<std::string, std::set<int>>> breakpoints) {
333
+ // just updates two members, no cache invalidation needed (chunkEligibleNow/
334
+ // checkDebug are re-checked on every call, not cached themselves -- only
335
+ // the compiled bytecode ITSELF, which never depends on debugger state,
336
+ // is cached forever).
337
+ //
338
+ // `hookSkippable`: a STRICTLY narrower claim than "breakpoints is
339
+ // accurate" above -- true only when NOTHING needs to inspect a
340
+ // statement-level checkpoint's own line/depth at all, not just "nothing
341
+ // needs to inspect it unless it's a compiled function's entry." This is
342
+ // false for step_over/step_out even though THEY also pass a real
343
+ // breakpoints set (chunkEligibleNow/useBytecodeVm still apply for them):
344
+ // both need checkDebug() to keep calling into the debug hook on every
345
+ // statement so the caller's own step_hit logic (line/depth comparison
346
+ // against the step's own starting point) can run -- there is no way to
347
+ // decide that in advance the way a breakpoint LOCATION can be. Only a
348
+ // plain "Continue" with no step pending can safely skip the call
349
+ // entirely for a line with no breakpoint -- see checkDebug's own doc
350
+ // comment (debug_profile.cpp) for where this is actually consulted.
351
+ void setFastContinueBreakpoints(std::optional<std::unordered_map<std::string, std::set<int>>> breakpoints,
352
+ bool hookSkippable = false) {
337
353
  fastContinueBreakpoints_ = std::move(breakpoints);
354
+ fastContinueHookSkippable_ = hookSkippable;
355
+ }
356
+
357
+ // The other half of hook-skippable mode's safety net. checkDebug()'s
358
+ // whole premise (see its own doc comment, debug_profile.cpp) is that it
359
+ // can skip calling into Python for a line with no breakpoint -- but the
360
+ // caller that decided that (DebugSession, on the MAIN/GUI thread) needs
361
+ // a way to say "actually, don't skip the very next one" from OUTSIDE any
362
+ // hook call, since debug_evaluate() runs as one single blocking call
363
+ // with the GIL released for its whole duration: there is no live,
364
+ // Python-callable Evaluator handle to invoke setFastContinueBreakpoints
365
+ // on directly the way a synchronous API would allow. A user clicking
366
+ // Pause, or toggling a breakpoint in an editor tab, while a hook-
367
+ // skippable render is mid-flight needs to take effect on the very next
368
+ // checkpoint, not "whenever a breakpoint happens to be hit next" (which,
369
+ // in hook-skippable mode, could be never, for a script with none set).
370
+ //
371
+ // `flag` is a plain shared_ptr<atomic<bool>> -- lock-free and GIL-free
372
+ // by construction, so the caller (bindings/module.cpp wraps it in a
373
+ // small Python-visible class) can set it from the main thread at any
374
+ // moment with no synchronization needed beyond the atomic itself.
375
+ // checkDebug() atomically test-and-clears it (exchange) on every call
376
+ // that would otherwise skip: if it was set, this call falls through and
377
+ // actually invokes the Python hook instead, which re-derives and pushes
378
+ // fresh breakpoints/hookSkippable state via its own existing logic --
379
+ // clearing it here (not from Python) means there's no separate
380
+ // "acknowledge" round-trip needed. Never itself compared against
381
+ // anything else; nullptr (the default, and what a plain debugger
382
+ // attach that never wires this up leaves it at) simply means hook-
383
+ // skippable mode -- if ever engaged at all -- can't be interrupted this
384
+ // way, which is only actually reachable if a caller opts into
385
+ // hookSkippable=true above without also providing this.
386
+ void setFastContinueInterruptFlag(std::shared_ptr<std::atomic<bool>> flag) {
387
+ fastContinueInterrupt_ = std::move(flag);
338
388
  }
339
389
 
340
390
  // Checks whether a debug pause should happen at `node` (via the
@@ -952,6 +1002,16 @@ private:
952
1002
  // setter leaves it at) means "no fast-continue exception" -- exactly
953
1003
  // today's original, always-safe behavior.
954
1004
  std::optional<std::unordered_map<std::string, std::set<int>>> fastContinueBreakpoints_;
1005
+ // See setFastContinueBreakpoints's own doc comment for why this is a
1006
+ // separate, narrower flag from fastContinueBreakpoints_ itself -- false
1007
+ // (the default) whenever a debugger is attached without explicitly
1008
+ // opting in, matching today's always-safe "checkDebug always calls the
1009
+ // hook" behavior.
1010
+ bool fastContinueHookSkippable_ = false;
1011
+ // See setFastContinueInterruptFlag's own doc comment. nullptr (the
1012
+ // default) means hook-skippable mode, if ever engaged, can't be
1013
+ // interrupted from outside a hook call.
1014
+ std::shared_ptr<std::atomic<bool>> fastContinueInterrupt_;
955
1015
  // The root EvalContext resolveTree() was called with -- lets
956
1016
  // checkDebug()'s locals snapshot fall back to top-level script
957
1017
  // variables when paused inside a nested user call, the same way the
@@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build"
4
4
 
5
5
  [project]
6
6
  name = "openscad_cpp_evaluator"
7
- version = "0.6.0"
7
+ version = "0.7.0"
8
8
  description = "C++ OpenSCAD evaluator with Python bindings"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -13,11 +13,12 @@ from dataclasses import dataclass
13
13
  from typing import Optional
14
14
 
15
15
  from . import _openscad_cpp_evaluator as _ext
16
- from ._openscad_cpp_evaluator import ManifoldCache
16
+ from ._openscad_cpp_evaluator import FastContinueSignal, ManifoldCache
17
17
 
18
18
  __all__ = [
19
19
  "Evaluator", "ColoredBody", "EvalError", "ParseError", "OscObject", "parse", "to_renderable_bodies",
20
20
  "ManifoldCache", "CallSiteProfile", "ProfileResult", "format_csg_tree", "bodies_from_dicts",
21
+ "FastContinueSignal",
21
22
  ]
22
23
 
23
24
 
@@ -317,6 +318,16 @@ class Evaluator:
317
318
  `return_hook`: fires after a user function/function-literal call
318
319
  computes its result, before returning -- (name, value, depth). Only
319
320
  meaningful with debug_hook set (the plain evaluate() path never calls it).
321
+ `fast_continue_signal`: an optional FastContinueSignal the caller keeps
322
+ across the whole debug session and calls `.request()` on (e.g. from
323
+ DebugSession.pause()/set_breakpoints(), BelfrySCAD's own debugger.py)
324
+ any time a checkDebug() checkpoint skipped via hook-skippable fast-
325
+ continue mode (see debug_hook's own `set_fast_continue(breakpoints,
326
+ hook_skippable)` kwarg) needs to stop skipping and consult Python
327
+ again -- there is no other way to reach a running debug_evaluate() call
328
+ from outside a hook invocation, since it runs as one single blocking
329
+ call with the GIL released for its duration. Only meaningful with
330
+ debug_hook set; ignored otherwise.
320
331
 
321
332
  After evaluate() returns, `self.csg_tree` (list of _CSGNode, see
322
333
  format_csg_tree), `self.dyn` (dict of every currently-visible
@@ -328,13 +339,14 @@ class Evaluator:
328
339
  """
329
340
 
330
341
  def __init__(self, echo_fn=None, debug_hook=None, error_break_fn=None, return_hook=None,
331
- manifold_cache=None, profile=False):
342
+ manifold_cache=None, profile=False, fast_continue_signal=None):
332
343
  self._echo_fn = echo_fn
333
344
  self._debug_hook = debug_hook
334
345
  self._error_break_fn = error_break_fn
335
346
  self._return_hook = return_hook
336
347
  self._manifold_cache = manifold_cache
337
348
  self._profile = profile
349
+ self._fast_continue_signal = fast_continue_signal
338
350
  self.csg_tree = []
339
351
  self.profile_result = None
340
352
  self.dyn = {}
@@ -350,7 +362,7 @@ class Evaluator:
350
362
  source_path, vp, self._debug_hook,
351
363
  self._error_break_fn or (lambda *a, **k: None),
352
364
  self._echo_fn or (lambda _m: None),
353
- self._manifold_cache, self._return_hook)
365
+ self._manifold_cache, self._return_hook, self._fast_continue_signal)
354
366
  self.csg_tree = []
355
367
  self.profile_result = None
356
368
  else:
@@ -664,10 +664,38 @@ Value runCompiledFunctionFromBound(Evaluator& ev, const CompiledChunk& chunk, co
664
664
  // *stack* growth (what this trampoline exists to eliminate) for heap
665
665
  // growth instead -- O(iteration count) EvalContext objects, bounded by
666
666
  // available RAM rather than a ~few-MB thread stack.
667
+ namespace {
668
+ // Tears a trampoline's own `chain` down back-to-front on EVERY exit path --
669
+ // normal return OR exception unwind (recordTailCallHop's own recursion-guard
670
+ // error is the exact case that matters: an infinite tail-recursive function
671
+ // with no base case throws out of the loop entirely, bypassing any teardown
672
+ // code placed after it). Relying on `chain` simply falling out of scope
673
+ // instead is NOT equivalent: std::vector<T>'s own element-destruction order
674
+ // is unspecified by the standard -- libc++ destroys back-to-front (matching
675
+ // ScopeTrailStorage::popLevel's own "scan from the back" doc comment, which
676
+ // assumes the level being popped is usually the most recently pushed one --
677
+ // O(1) per pop that way), but libstdc++ destroys front-to-back, the
678
+ // adversarial order for that same scan -- each pop then walks almost the
679
+ // entire remaining vector, turning an intended O(N) teardown into a real,
680
+ // measured O(N^2) (see issue #50: an N-scaling experiment plus a minimal
681
+ // repro proving libc++/libstdc++ disagree on std::vector<T>::clear()'s
682
+ // destruction order for identical source). Popping back-to-front explicitly,
683
+ // via a destructor that always runs before `chain`'s own, is correct AND
684
+ // O(N) on every standard library and every exit path, not just the ones
685
+ // that happen to agree with libc++ and return normally.
686
+ struct ChainTeardown {
687
+ std::vector<EvalContext>& chain;
688
+ ~ChainTeardown() {
689
+ while (!chain.empty()) chain.pop_back();
690
+ }
691
+ };
692
+ } // namespace
693
+
667
694
  Value runCompiledFunctionTrampoline(Evaluator& ev, const CompiledChunk& chunk,
668
695
  const std::vector<std::unique_ptr<oscad::Argument>>& arguments,
669
696
  EvalContext& callerCtx, EvalContext& childCtx) {
670
697
  std::vector<EvalContext> chain{childCtx};
698
+ ChainTeardown teardown{chain};
671
699
  TailCallRequest req;
672
700
  Value result = runCompiledFunction(ev, chunk, arguments, callerCtx, chain.back(), &req);
673
701
  unsigned recursionGuard = 0;
@@ -689,6 +717,7 @@ Value runCompiledFunctionTrampoline(Evaluator& ev, const CompiledChunk& chunk,
689
717
  Value runCompiledFunctionFromBoundTrampoline(Evaluator& ev, const CompiledChunk& chunk, const BoundArgs& bound,
690
718
  EvalContext& childCtx) {
691
719
  std::vector<EvalContext> chain{childCtx};
720
+ ChainTeardown teardown{chain};
692
721
  TailCallRequest req;
693
722
  Value result = runCompiledFunctionFromBound(ev, chunk, bound, chain.back(), &req);
694
723
  unsigned recursionGuard = 0;
@@ -77,6 +77,36 @@ std::vector<DebugFrame> Evaluator::buildDebugFrames(const EvalContext* ctx) cons
77
77
  void Evaluator::checkDebug(const oscad::ASTNode& node, EvalContext& ctx, bool forced, bool exprLevel) {
78
78
  if (!debugHooks_.debugHook) return;
79
79
  const oscad::Position& pos = node.position();
80
+ // Fast-continue's hook-skippable mode (setFastContinueBreakpoints' own
81
+ // doc comment): a plain "Continue" with no step pending needs the debug
82
+ // hook called ONLY for a line that actually has a breakpoint -- every
83
+ // other checkpoint is guaranteed to do nothing (no forced/breakpoint/
84
+ // step_hit condition on the Python side can possibly fire), so skip the
85
+ // call (and the childStatementPositions/getFrame setup below, all
86
+ // wasted work otherwise) entirely rather than crossing into Python just
87
+ // to be told "continue". `forced` (the explicit breakpoint() builtin)
88
+ // always bypasses this, matching its own "bypasses nothing" contract.
89
+ // Never applies to step_over/step_out (hookSkippable is false for those
90
+ // even though they also set a real breakpoints set, see the setter's
91
+ // own doc comment) or step_into/step_to_child (fastContinueBreakpoints_
92
+ // itself is nullopt then) -- both need every statement inspected.
93
+ if (!forced && fastContinueHookSkippable_ && fastContinueBreakpoints_) {
94
+ // Test-and-clear: if the main thread requested an interrupt (Pause,
95
+ // or a breakpoint edit -- see setFastContinueInterruptFlag's own doc
96
+ // comment) since the last checkDebug() call, this call falls
97
+ // through and actually invokes the hook below instead of skipping,
98
+ // regardless of whether THIS specific line has a breakpoint --
99
+ // that's what lets the hook's own logic (pause_now, or a freshly
100
+ // updated breakpoints dict) run at all in hook-skippable mode.
101
+ const bool interrupted =
102
+ fastContinueInterrupt_ && fastContinueInterrupt_->exchange(false, std::memory_order_acq_rel);
103
+ if (!interrupted) {
104
+ auto originIt = fastContinueBreakpoints_->find(pos.origin);
105
+ if (originIt == fastContinueBreakpoints_->end() || !originIt->second.count(pos.line)) {
106
+ return;
107
+ }
108
+ }
109
+ }
80
110
  const int depth = static_cast<int>(callStack_.size());
81
111
  const DebugFramesFn getFrame = [this, &ctx]() { return buildDebugFrames(&ctx); };
82
112
  lastChildrenPositions_ = childStatementPositions(node);
@@ -443,6 +443,32 @@ Value Evaluator::evalFunctionBodyTrampoline(const oscad::Expression& bodyExpr, E
443
443
  // dropped there) if memory ever measurably matters more than this
444
444
  // simpler all-or-nothing approach.
445
445
  std::vector<EvalContext> chain{ctx};
446
+ // Tears `chain` down back-to-front on EVERY exit path -- normal return
447
+ // OR exception unwind (recordTailCallHop's own recursion-guard error,
448
+ // below, is the exact case that matters: an infinite tail-recursive
449
+ // function with no base case throws out of this loop entirely,
450
+ // bypassing any teardown code placed after it). Relying on `chain`
451
+ // simply falling out of scope instead is NOT equivalent: std::vector<T>'s
452
+ // own element-destruction order is unspecified by the standard --
453
+ // libc++ destroys back-to-front (matching ScopeTrailStorage::popLevel's
454
+ // own "scan from the back" doc comment, which assumes the level being
455
+ // popped is usually the most recently pushed one -- O(1) per pop that
456
+ // way), but libstdc++ destroys front-to-back, the adversarial order for
457
+ // that same scan -- each pop then walks almost the entire remaining
458
+ // vector, turning an intended O(N) teardown into a real, measured
459
+ // O(N^2) (see issue #50: an N-scaling experiment plus a minimal repro
460
+ // proving libc++/libstdc++ disagree on std::vector<T>::clear()'s
461
+ // destruction order for identical source). Popping back-to-front
462
+ // explicitly, via a destructor that always runs before `chain`'s own,
463
+ // is correct AND O(N) on every standard library and every exit path,
464
+ // not just the ones that happen to agree with libc++ and return
465
+ // normally.
466
+ struct ChainTeardown {
467
+ std::vector<EvalContext>& chain;
468
+ ~ChainTeardown() {
469
+ while (!chain.empty()) chain.pop_back();
470
+ }
471
+ } teardown{chain};
446
472
  unsigned recursionGuard = 0;
447
473
  while (true) {
448
474
  auto result = simplifyTailStep(*expr, ctx);
@@ -6,8 +6,12 @@
6
6
  #include <gtest/gtest.h>
7
7
 
8
8
  #include <algorithm>
9
+ #include <atomic>
10
+ #include <memory>
9
11
  #include <ostream>
12
+ #include <set>
10
13
  #include <string>
14
+ #include <unordered_map>
11
15
  #include <vector>
12
16
 
13
17
  using namespace oscadeval;
@@ -230,6 +234,122 @@ TEST(DebugHooks, NoHooksInstalledMeansZeroOverheadCodePathStillWorks) {
230
234
  EXPECT_EQ(e.bodies.size(), 1u);
231
235
  }
232
236
 
237
+ // ---------------------------------------------------------------------------
238
+ // Fast-continue's hook-skippable mode (issue found via BelfrySCAD's own
239
+ // nurbs.scad-heavy debug session: 462k checkDebug() calls survived even
240
+ // with per-function VM fast-continue fully engaged, since module/geometry
241
+ // evaluation never compiles and every one of those still crossed into
242
+ // Python just to be told "continue"). See setFastContinueBreakpoints's own
243
+ // doc comment (evaluator.hpp) for the full contract.
244
+ // ---------------------------------------------------------------------------
245
+
246
+ TEST(DebugHooks, FastContinueHookSkippableSkipsCheckpointsWithNoMatchingBreakpoint) {
247
+ int calls = 0;
248
+ DebugHooks hooks;
249
+ hooks.debugHook = [&](int, int, bool, bool, const std::string&, const std::vector<CallStackFrame>&, const DebugFramesFn&) {
250
+ ++calls;
251
+ return DebugAction{};
252
+ };
253
+ Evaluator ev(EchoFn{}, nullptr, nullptr, hooks);
254
+ ev.setFastContinueBreakpoints(std::unordered_map<std::string, std::set<int>>{}, /*hookSkippable=*/true);
255
+ auto ast = parseSrc("cube(1);\ntranslate([1,0,0]) sphere(r=1,$fn=8);\necho(\"x\");");
256
+ auto scope = oscad::buildScopes(ast);
257
+ EvalContext ctx = EvalContext::makeRoot(scope.get());
258
+ ev.evaluate(ast, ctx);
259
+ // Same script as HookFiresOnceForEachStatementAtEveryNestingLevel (7
260
+ // checkpoints without fast-continue) -- with hook-skippable mode and no
261
+ // breakpoints anywhere, every single one is a plain C++ skip.
262
+ EXPECT_EQ(calls, 0);
263
+ }
264
+
265
+ TEST(DebugHooks, FastContinueHookSkippableStillFiresAtMatchingBreakpointLine) {
266
+ std::vector<int> firedLines;
267
+ DebugHooks hooks;
268
+ hooks.debugHook = [&](int line, int, bool, bool, const std::string&, const std::vector<CallStackFrame>&, const DebugFramesFn&) {
269
+ firedLines.push_back(line);
270
+ return DebugAction{};
271
+ };
272
+ Evaluator ev(EchoFn{}, nullptr, nullptr, hooks);
273
+ ev.setFastContinueBreakpoints(std::unordered_map<std::string, std::set<int>>{{"<string>", {2}}}, /*hookSkippable=*/true);
274
+ auto ast = parseSrc("cube(1);\ntranslate([1,0,0]) sphere(r=1,$fn=8);\necho(\"x\");");
275
+ auto scope = oscad::buildScopes(ast);
276
+ EvalContext ctx = EvalContext::makeRoot(scope.get());
277
+ ev.evaluate(ast, ctx);
278
+ // Every line-2 checkpoint (translate itself, its 3 expr-level [1,0,0]
279
+ // list elements, and the nested sphere child -- 5 total, matching
280
+ // HookFiresOnceForEachStatementAtEveryNestingLevel's own sequence)
281
+ // survives the skip; line 1 (cube) and line 3 (echo) have no
282
+ // breakpoint and are skipped entirely.
283
+ EXPECT_EQ(firedLines, (std::vector<int>{2, 2, 2, 2, 2}));
284
+ }
285
+
286
+ TEST(DebugHooks, FastContinueNotHookSkippableStillFiresEveryCheckpoint) {
287
+ int calls = 0;
288
+ DebugHooks hooks;
289
+ hooks.debugHook = [&](int, int, bool, bool, const std::string&, const std::vector<CallStackFrame>&, const DebugFramesFn&) {
290
+ ++calls;
291
+ return DebugAction{};
292
+ };
293
+ Evaluator ev(EchoFn{}, nullptr, nullptr, hooks);
294
+ // Mirrors step_over/step_out: a real (here, empty) breakpoints set is
295
+ // provided -- so chunkEligibleNow/useBytecodeVm still apply -- but
296
+ // hookSkippable stays false, since both need checkDebug() to keep
297
+ // calling into the hook on every statement so their own step_hit logic
298
+ // (line/depth comparison against the step's own starting point) can
299
+ // run; there is no way to decide that in advance.
300
+ ev.setFastContinueBreakpoints(std::unordered_map<std::string, std::set<int>>{}, /*hookSkippable=*/false);
301
+ auto ast = parseSrc("cube(1);\ntranslate([1,0,0]) sphere(r=1,$fn=8);\necho(\"x\");");
302
+ auto scope = oscad::buildScopes(ast);
303
+ EvalContext ctx = EvalContext::makeRoot(scope.get());
304
+ ev.evaluate(ast, ctx);
305
+ EXPECT_EQ(calls, 7); // unaffected -- same total as HookFiresOnceForEachStatementAtEveryNestingLevel
306
+ }
307
+
308
+ TEST(DebugHooks, FastContinueInterruptFlagForcesTheNextCheckpointThrough) {
309
+ std::vector<int> firedLines;
310
+ DebugHooks hooks;
311
+ hooks.debugHook = [&](int line, int, bool, bool, const std::string&, const std::vector<CallStackFrame>&, const DebugFramesFn&) {
312
+ firedLines.push_back(line);
313
+ return DebugAction{};
314
+ };
315
+ Evaluator ev(EchoFn{}, nullptr, nullptr, hooks);
316
+ ev.setFastContinueBreakpoints(std::unordered_map<std::string, std::set<int>>{}, /*hookSkippable=*/true);
317
+ // Pre-armed, as if Pause (or a breakpoint edit) was requested from the
318
+ // main thread before this run even started -- see
319
+ // setFastContinueInterruptFlag's own doc comment (evaluator.hpp) for
320
+ // why this exists: there is no other way to reach a running
321
+ // debug_evaluate() call from outside a hook invocation.
322
+ auto flag = std::make_shared<std::atomic<bool>>(true);
323
+ ev.setFastContinueInterruptFlag(flag);
324
+ auto ast = parseSrc("cube(1);\ntranslate([1,0,0]) sphere(r=1,$fn=8);\necho(\"x\");");
325
+ auto scope = oscad::buildScopes(ast);
326
+ EvalContext ctx = EvalContext::makeRoot(scope.get());
327
+ ev.evaluate(ast, ctx);
328
+ // Only the FIRST checkpoint (line 1, cube) fires -- checkDebug's own
329
+ // test-and-clear means the flag is consumed there, so every subsequent
330
+ // checkpoint goes back to being skipped normally (no breakpoints
331
+ // anywhere).
332
+ EXPECT_EQ(firedLines, (std::vector<int>{1}));
333
+ EXPECT_FALSE(flag->load());
334
+ }
335
+
336
+ TEST(DebugHooks, ForcedBreakpointBuiltinAlwaysFiresEvenInHookSkippableMode) {
337
+ int forcedCalls = 0, normalCalls = 0;
338
+ DebugHooks hooks;
339
+ hooks.debugHook = [&](int, int, bool forced, bool, const std::string&, const std::vector<CallStackFrame>&, const DebugFramesFn&) {
340
+ (forced ? forcedCalls : normalCalls)++;
341
+ return DebugAction{};
342
+ };
343
+ Evaluator ev(EchoFn{}, nullptr, nullptr, hooks);
344
+ ev.setFastContinueBreakpoints(std::unordered_map<std::string, std::set<int>>{}, /*hookSkippable=*/true);
345
+ auto ast = parseSrc("cube(1);\nbreakpoint();\nsphere(r=1,$fn=8);");
346
+ auto scope = oscad::buildScopes(ast);
347
+ EvalContext ctx = EvalContext::makeRoot(scope.get());
348
+ ev.evaluate(ast, ctx);
349
+ EXPECT_EQ(forcedCalls, 1); // breakpoint() always bypasses hook-skippable mode
350
+ EXPECT_EQ(normalCalls, 0); // cube/breakpoint()-as-statement/sphere: no breakpoint line matches, all skipped
351
+ }
352
+
233
353
  // ---------------------------------------------------------------------------
234
354
  // Full parity with the Python reference's _check_debug call sites.
235
355
  //
@@ -19,6 +19,7 @@
19
19
 
20
20
  #include "test_helpers.hpp"
21
21
 
22
+ #include <chrono>
22
23
  #include <gtest/gtest.h>
23
24
 
24
25
  using namespace oscadeval;
@@ -126,17 +127,6 @@ TEST(TailCalls, NonTailRecursionIsUnaffectedByTheTrampoline) {
126
127
  }
127
128
 
128
129
  TEST(TailCalls, InfiniteTailRecursionHitsTheIterationCapInsteadOfHanging) {
129
- #ifndef __APPLE__
130
- // Skipped on non-Apple platforms: this drives the interpreter trampoline
131
- // to the full 1,000,000-iteration recursion-guard cap, which is O(N)
132
- // under Clang/libc++ (~1s) but a confirmed O(N^2) under GCC/libstdc++
133
- // (and, empirically, MSVC's STL too) -- 25-50+ minutes instead of
134
- // seconds. See https://github.com/BelfrySCAD/openscad_cpp_evaluator/issues/50
135
- // for the full investigation (peak RSS/CPU/instruction-count evidence
136
- // and an N-scaling experiment proving the asymptotic-complexity gap).
137
- // Re-enable once that's root-caused and fixed.
138
- GTEST_SKIP() << "O(N^2) under this toolchain -- see issue #50";
139
- #endif
140
130
  ScopedVm vm(false);
141
131
  Evaluator ev;
142
132
  auto ast = parseSrc("function loop(n) = loop(n + 1);\nresult = loop(0);");
@@ -150,11 +140,6 @@ TEST(TailCalls, InfiniteTailRecursionHitsTheIterationCapInsteadOfHanging) {
150
140
  }
151
141
 
152
142
  TEST(TailCalls, InfiniteTailRecursionErrorMentionsTheFunctionName) {
153
- #ifndef __APPLE__
154
- // See InfiniteTailRecursionHitsTheIterationCapInsteadOfHanging's own
155
- // comment just above -- same O(N^2)-under-this-toolchain issue (#50).
156
- GTEST_SKIP() << "O(N^2) under this toolchain -- see issue #50";
157
- #endif
158
143
  ScopedVm vm(false);
159
144
  Evaluator ev;
160
145
  auto ast = parseSrc("function loop(n) = loop(n + 1);\nresult = loop(0);");
@@ -169,6 +154,34 @@ TEST(TailCalls, InfiniteTailRecursionErrorMentionsTheFunctionName) {
169
154
  }
170
155
  }
171
156
 
157
+ TEST(TailCalls, InfiniteTailRecursionHitsTheCapInBoundedWallTime) {
158
+ // Regression tripwire for issue #50: evalFunctionBodyTrampoline's own
159
+ // `chain` (every hop's EvalContext, kept alive for $-var ancestry --
160
+ // see its own doc comment) used to rely on std::vector<EvalContext>'s
161
+ // OWN destructor to tear itself down, whose element-destruction order
162
+ // is unspecified by the standard. libstdc++ (GCC) and, per CI evidence,
163
+ // MSVC destroy front-to-back -- the adversarial order for
164
+ // ScopeTrailStorage::popLevel's "scan from the back" optimization
165
+ // (its own doc comment), turning an intended O(N) teardown into a
166
+ // real, measured O(N^2): the exact same 1,000,000-iteration script
167
+ // below took 25-50+ minutes on GCC/MSVC CI runners before the fix
168
+ // (an explicit back-to-front teardown, immune to the underlying
169
+ // vector's own unspecified order), vs ~1s on Clang/libc++ throughout.
170
+ // ponytail: generous fixed ceiling, not a strict perf target -- trips
171
+ // only on a real regression (this exact O(N^2) reintroduced), not
172
+ // machine noise; raise if a slower CI runner ever needs it.
173
+ ScopedVm vm(false);
174
+ Evaluator ev;
175
+ auto ast = parseSrc("function loop(n) = loop(n + 1);\nresult = loop(0);");
176
+ auto scope = oscad::buildScopes(ast);
177
+ EvalContext ctx = EvalContext::makeRoot(scope.get());
178
+ const auto start = std::chrono::steady_clock::now();
179
+ EXPECT_THROW(ev.resolveTree(ast, ctx), EvalError);
180
+ const double ms = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count();
181
+ constexpr double kCeilingMs = 30000.0;
182
+ EXPECT_LT(ms, kCeilingMs) << "took " << ms << "ms -- possible O(N^2) regression, see issue #50";
183
+ }
184
+
172
185
  TEST(TailCalls, ErrorThrownDeepInATailChainProducesABoundedTrace) {
173
186
  ScopedVm vm(false);
174
187
  Evaluator ev;