openscad-cpp-evaluator 1.15.0__tar.gz → 1.19.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 (222) hide show
  1. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/CLAUDE.md +88 -0
  2. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/PKG-INFO +1 -1
  3. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/bindings/module.cpp +68 -14
  4. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/include/openscad_cpp_evaluator/bytecode.hpp +6 -0
  5. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/include/openscad_cpp_evaluator/bytecode_compiler.hpp +5 -5
  6. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/include/openscad_cpp_evaluator/colored_body.hpp +43 -0
  7. openscad_cpp_evaluator-1.19.0/include/openscad_cpp_evaluator/coverage.hpp +116 -0
  8. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/include/openscad_cpp_evaluator/eval_use.hpp +12 -0
  9. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/include/openscad_cpp_evaluator/evaluator.hpp +50 -1
  10. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/include/openscad_cpp_evaluator/scope_trail.hpp +40 -4
  11. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/pyproject.toml +1 -1
  12. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/python/openscad_cpp_evaluator/__init__.py +18 -5
  13. openscad_cpp_evaluator-1.19.0/src/.DS_Store +0 -0
  14. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/src/CMakeLists.txt +1 -0
  15. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/src/builtins/booleans.cpp +32 -3
  16. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/src/builtins/color.cpp +2 -2
  17. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/src/builtins/control.cpp +1 -0
  18. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/src/bytecode_compiler.cpp +35 -16
  19. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/src/bytecode_vm.cpp +5 -1
  20. openscad_cpp_evaluator-1.19.0/src/coverage.cpp +397 -0
  21. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/src/csg_generate.cpp +22 -2
  22. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/src/csg_resolve.cpp +13 -0
  23. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/src/eval_use.cpp +6 -0
  24. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/src/expr_eval.cpp +42 -2
  25. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/src/stmt_eval.cpp +9 -0
  26. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/src/user_calls.cpp +13 -7
  27. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/tests/CMakeLists.txt +1 -0
  28. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/tests/test_booleans.cpp +27 -0
  29. openscad_cpp_evaluator-1.19.0/tests/test_coverage.cpp +234 -0
  30. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/tests/test_include_cache.cpp +2 -0
  31. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/tests/test_manifold_cache.cpp +7 -8
  32. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/tests/test_python_bindings.py +33 -0
  33. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/tests/test_scope_trail.cpp +64 -0
  34. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/tests/test_use.cpp +56 -0
  35. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/tools/cli/cli_lib.cpp +1 -0
  36. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/.github/workflows/ci.yml +0 -0
  37. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/.github/workflows/wheels.yml +0 -0
  38. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/.gitignore +0 -0
  39. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/.gitmodules +0 -0
  40. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/CMakeLists.txt +0 -0
  41. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/LICENSE +0 -0
  42. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/README.md +0 -0
  43. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/bindings/CMakeLists.txt +0 -0
  44. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/bindings/ast_to_py.cpp +0 -0
  45. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/cmake/embed_font.cmake +0 -0
  46. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/examples/CMakeLists.txt +0 -0
  47. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/examples/eval_perf_benchmark.cpp +0 -0
  48. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/examples/manifold_cache_reuse.cpp +0 -0
  49. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/examples/minimal_debugger.cpp +0 -0
  50. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/.github/workflows/ci.yml +0 -0
  51. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/.gitignore +0 -0
  52. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/CLAUDE.md +0 -0
  53. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/CMakeLists.txt +0 -0
  54. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/README.md +0 -0
  55. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/api.hpp +0 -0
  56. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/ast_node.hpp +0 -0
  57. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/comments.hpp +0 -0
  58. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/declarations.hpp +0 -0
  59. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/expression.hpp +0 -0
  60. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/module_instantiation.hpp +0 -0
  61. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/scope_builder.hpp +0 -0
  62. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/vector_element.hpp +0 -0
  63. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast.hpp +0 -0
  64. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/comments.hpp +0 -0
  65. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/position.hpp +0 -0
  66. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/pretty_print.hpp +0 -0
  67. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/scope.hpp +0 -0
  68. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/scope_table.hpp +0 -0
  69. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/serialization.hpp +0 -0
  70. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/source_map.hpp +0 -0
  71. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/src/CMakeLists.txt +0 -0
  72. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/src/api.cpp +0 -0
  73. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/src/ast/ast_node.cpp +0 -0
  74. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/src/ast/declarations.cpp +0 -0
  75. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/src/ast/expression.cpp +0 -0
  76. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/src/ast/format_utils.hpp +0 -0
  77. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/src/ast/module_instantiation.cpp +0 -0
  78. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/src/ast/scope_builder.cpp +0 -0
  79. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/src/ast/vector_element.cpp +0 -0
  80. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/src/comment_attach_internal.hpp +0 -0
  81. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/src/comments.cpp +0 -0
  82. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/src/grammar/driver.cpp +0 -0
  83. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/src/grammar/driver.hpp +0 -0
  84. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/src/grammar/lexer.l +0 -0
  85. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/src/grammar/lexer_api.hpp +0 -0
  86. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/src/grammar/oscad_location.hpp +0 -0
  87. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/src/grammar/parser.y +0 -0
  88. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/src/inline_comment_attach.cpp +0 -0
  89. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/src/position.cpp +0 -0
  90. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/src/pretty_print.cpp +0 -0
  91. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/src/scope.cpp +0 -0
  92. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/src/serialization/json_io.cpp +0 -0
  93. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/src/serialization/yaml_io.cpp +0 -0
  94. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/src/source_map.cpp +0 -0
  95. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/tests/CMakeLists.txt +0 -0
  96. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/tests/test_assignments.cpp +0 -0
  97. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/tests/test_ast_generation.cpp +0 -0
  98. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/tests/test_comments_and_files.cpp +0 -0
  99. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/tests/test_const_borrow.cpp +0 -0
  100. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/tests/test_control.cpp +0 -0
  101. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/tests/test_expressions.cpp +0 -0
  102. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/tests/test_functions.cpp +0 -0
  103. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/tests/test_helpers.hpp +0 -0
  104. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/tests/test_inline_comments.cpp +0 -0
  105. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/tests/test_lexical.cpp +0 -0
  106. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/tests/test_modules.cpp +0 -0
  107. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/tests/test_node_str.cpp +0 -0
  108. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/tests/test_pretty_print.cpp +0 -0
  109. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/tests/test_render_expression.cpp +0 -0
  110. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/tests/test_scope.cpp +0 -0
  111. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/tests/test_serialization.cpp +0 -0
  112. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/tests/test_smoke.cpp +0 -0
  113. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/tests/test_source_map.cpp +0 -0
  114. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/tests/test_strict_commas.cpp +0 -0
  115. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/tests/test_use_include.cpp +0 -0
  116. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/tests/test_vectors.cpp +0 -0
  117. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/tools/cli/CMakeLists.txt +0 -0
  118. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/external/openscad_cpp_parser/tools/cli/main.cpp +0 -0
  119. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/include/openscad_cpp_evaluator/bound_args.hpp +0 -0
  120. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/include/openscad_cpp_evaluator/bytecode_vm.hpp +0 -0
  121. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/include/openscad_cpp_evaluator/call_args.hpp +0 -0
  122. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/include/openscad_cpp_evaluator/csg_node.hpp +0 -0
  123. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/include/openscad_cpp_evaluator/css_colors.hpp +0 -0
  124. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/include/openscad_cpp_evaluator/debug_hooks.hpp +0 -0
  125. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/include/openscad_cpp_evaluator/debug_repl.hpp +0 -0
  126. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/include/openscad_cpp_evaluator/dispatch.hpp +0 -0
  127. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/include/openscad_cpp_evaluator/dxf_svg_import.hpp +0 -0
  128. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/include/openscad_cpp_evaluator/eval_context.hpp +0 -0
  129. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/include/openscad_cpp_evaluator/eval_error.hpp +0 -0
  130. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/include/openscad_cpp_evaluator/export.hpp +0 -0
  131. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/include/openscad_cpp_evaluator/font_match.hpp +0 -0
  132. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/include/openscad_cpp_evaluator/font_provider.hpp +0 -0
  133. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/include/openscad_cpp_evaluator/format_closure.hpp +0 -0
  134. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/include/openscad_cpp_evaluator/freetype_font_provider.hpp +0 -0
  135. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/include/openscad_cpp_evaluator/function_builtins.hpp +0 -0
  136. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/include/openscad_cpp_evaluator/import_builtin.hpp +0 -0
  137. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/include/openscad_cpp_evaluator/manifold_cache.hpp +0 -0
  138. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/include/openscad_cpp_evaluator/mesh_check.hpp +0 -0
  139. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/include/openscad_cpp_evaluator/mesh_import.hpp +0 -0
  140. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/include/openscad_cpp_evaluator/osc_range.hpp +0 -0
  141. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/include/openscad_cpp_evaluator/profile.hpp +0 -0
  142. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/include/openscad_cpp_evaluator/segments.hpp +0 -0
  143. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/include/openscad_cpp_evaluator/surface_load.hpp +0 -0
  144. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/include/openscad_cpp_evaluator/text_metrics.hpp +0 -0
  145. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/include/openscad_cpp_evaluator/utf8.hpp +0 -0
  146. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/include/openscad_cpp_evaluator/value.hpp +0 -0
  147. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/include/openscad_cpp_evaluator/zip_stored.hpp +0 -0
  148. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/resources/fonts/LICENSE +0 -0
  149. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/resources/fonts/LiberationSans-Bold.ttf +0 -0
  150. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/resources/fonts/LiberationSans-BoldItalic.ttf +0 -0
  151. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/resources/fonts/LiberationSans-Italic.ttf +0 -0
  152. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/resources/fonts/LiberationSans-Regular.ttf +0 -0
  153. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/src/builtins/builtins.hpp +0 -0
  154. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/src/builtins/call_args.cpp +0 -0
  155. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/src/builtins/dxf_dim.cpp +0 -0
  156. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/src/builtins/extrude.cpp +0 -0
  157. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/src/builtins/function_builtins.cpp +0 -0
  158. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/src/builtins/import.cpp +0 -0
  159. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/src/builtins/modifiers.cpp +0 -0
  160. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/src/builtins/primitives_2d.cpp +0 -0
  161. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/src/builtins/primitives_3d.cpp +0 -0
  162. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/src/builtins/registry.cpp +0 -0
  163. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/src/builtins/roof.cpp +0 -0
  164. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/src/builtins/segments.cpp +0 -0
  165. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/src/builtins/surface.cpp +0 -0
  166. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/src/builtins/text.cpp +0 -0
  167. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/src/builtins/topology.cpp +0 -0
  168. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/src/builtins/transforms.cpp +0 -0
  169. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/src/colored_body.cpp +0 -0
  170. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/src/css_colors.cpp +0 -0
  171. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/src/debug/debug_repl.cpp +0 -0
  172. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/src/debug_profile.cpp +0 -0
  173. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/src/eval_context.cpp +0 -0
  174. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/src/eval_error.cpp +0 -0
  175. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/src/export.cpp +0 -0
  176. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/src/format_closure.cpp +0 -0
  177. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/src/import/dxf_import.cpp +0 -0
  178. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/src/import/mesh_import.cpp +0 -0
  179. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/src/import/surface_load.cpp +0 -0
  180. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/src/import/svg_import.cpp +0 -0
  181. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/src/manifold_cache.cpp +0 -0
  182. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/src/measure_geometry.cpp +0 -0
  183. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/src/mesh_check.cpp +0 -0
  184. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/src/text/font_match.cpp +0 -0
  185. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/src/text/freetype_font_provider.cpp +0 -0
  186. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/src/text/text_metrics.cpp +0 -0
  187. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/src/utf8.cpp +0 -0
  188. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/src/value.cpp +0 -0
  189. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/src/zip_stored.cpp +0 -0
  190. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/tests/test_bytecode_compiler.cpp +0 -0
  191. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/tests/test_children_separate.cpp +0 -0
  192. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/tests/test_cli.cpp +0 -0
  193. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/tests/test_control_flow.cpp +0 -0
  194. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/tests/test_csg_tree.cpp +0 -0
  195. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/tests/test_debug_hooks.cpp +0 -0
  196. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/tests/test_dxf_svg_import.cpp +0 -0
  197. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/tests/test_eval_context.cpp +0 -0
  198. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/tests/test_export.cpp +0 -0
  199. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/tests/test_expr_eval.cpp +0 -0
  200. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/tests/test_extrude_roof.cpp +0 -0
  201. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/tests/test_feature_detection.cpp +0 -0
  202. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/tests/test_function_builtins.cpp +0 -0
  203. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/tests/test_helpers.hpp +0 -0
  204. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/tests/test_import_export.cpp +0 -0
  205. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/tests/test_mesh_check.cpp +0 -0
  206. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/tests/test_multi_color_merge.cpp +0 -0
  207. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/tests/test_primitives.cpp +0 -0
  208. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/tests/test_profiling.cpp +0 -0
  209. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/tests/test_render_expr.cpp +0 -0
  210. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/tests/test_scoping.cpp +0 -0
  211. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/tests/test_surface.cpp +0 -0
  212. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/tests/test_tail_calls.cpp +0 -0
  213. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/tests/test_text.cpp +0 -0
  214. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/tests/test_transforms.cpp +0 -0
  215. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/tests/test_unexpected_args.cpp +0 -0
  216. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/tests/test_value.cpp +0 -0
  217. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/tests/test_viewport_params.cpp +0 -0
  218. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/tests/test_zip_stored.cpp +0 -0
  219. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/tools/cli/CMakeLists.txt +0 -0
  220. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/tools/cli/cli_lib.hpp +0 -0
  221. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/tools/cli/main.cpp +0 -0
  222. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.19.0}/uv.lock +0 -0
@@ -449,6 +449,39 @@ closed in one pass after a fresh "anything still unimplemented?" audit. Each bel
449
449
  all (`resolve_use_scopes`/`UseStatement` had zero hits in `openscad_evaluator`'s own `tests/`), so
450
450
  these were verified against real reference *behavior*, not ported from an existing test.
451
451
 
452
+ **File-level globals are evaluated ONCE per run (v1.16.0).** The main file's and every
453
+ `include`'s are, by the top-level statement pass, into the root `let_` level; a `use`d file's
454
+ are not -- `use` injects only declarations -- so `Evaluator::fileGlobal` evaluates ALL of that
455
+ file's top-level assignments, in source order, into a context of its own the first time anything
456
+ in the file reads one, and keeps it for the run. `ResolvedUseScopes::usedFileGlobals` carries
457
+ the per-file assignment lists and every entry point hands them over with
458
+ `Evaluator::setUsedFileGlobals` (CLI, both binding paths, the tests); a caller that skips that
459
+ keeps the old per-read fallback. Before this, `evalIdentifier`'s scope fallback re-evaluated the
460
+ assignment's expression on EVERY read from inside a function -- `callCtx()` isolates `let_` so the
461
+ root level was invisible -- which made `r = rands(0,1,1); function g() = r;` give `g() != g()`
462
+ (OpenSCAD: equal) and a 20K-element global list read 2000 times from a function cost 9.7s
463
+ (now 0.02s). Two deliberate divergences from OpenSCAD 2026.02.01, both verified against it:
464
+ a used file's globals are evaluated once, not once per call into the file (upstream's per-call
465
+ re-evaluation is a long-standing bug there, so its forward-read warning prints once here, not
466
+ per call); and a global read from a function BEFORE its assignment has run is `undef` with an
467
+ "Ignoring unknown variable" warning, exactly as upstream, where the old fallback had quietly
468
+ computed the not-yet-assigned value.
469
+ **Scope-trail level table shrinks (v1.18.0).** `ScopeTrailStorage::parent_` and its indexed twin
470
+ map level -> parent level in a vector indexed by level. Levels came from `++nextLevel_` and were
471
+ never reused, so the vector grew 4 bytes per level for the whole run -- 3 x 64MB on a 13M-call
472
+ script, plus the copy at every doubling -- and was 192MB of fractal_tree.scad's peak. A popped
473
+ level is now marked -1 (0 still means "live, no parent": an isolated root), and when the NEWEST
474
+ level dies `markDead` pops the vector back past every dead level above the newest live one, whose
475
+ numbers `openLevel` then hands out again. Every invariant `lookup()`'s sorted-merge walk relies on
476
+ holds: a reused number is greater than every level still holding an entry (all higher levels were
477
+ dead and `popLevel` had already removed their entries), so a name's push-stack stays ascending,
478
+ and a live level's number never changes -- an escaping closure that keeps a level alive simply
479
+ stops the shrink at it (`tests/test_scope_trail.cpp` covers both). The ancestry walk now stops at
480
+ `ancestor > 0` rather than `!= 0`. Same PR: `callCtxFor`'s per-call scan over active declarations
481
+ does the integer span test before the origin STRING compare (`_platform_memcmp` was 1.4% of
482
+ fractal_tree), and driveVm's completion branch no longer copies the finished frame's name. Anklet
483
+ 74 -> 63MB, a 4M-call synthetic 380 -> 258MB, fractal_tree -3.4% wall.
484
+
452
485
  **Known gap closed in Phase 3, worth remembering for Phase 5+**: every builtin resolve function must
453
486
  call `resolveCallArgs` (call_args.hpp), never bare `resolveArgs`, or a `$`-prefixed named call
454
487
  argument (`circle(r=2, $fn=64)`) silently fails to reach `ctx.dyn`. Caught by a geometric-correctness
@@ -910,6 +943,28 @@ grep for `ponytail:`.
910
943
  operation`. `linear_extrude`/`rotate_extrude`/`offset` are always-2D and `projection` always-3D, so
911
944
  they skip the "mixing" line and emit only the second; `roof()` emits neither, matching the
912
945
  reference. Background (`%`) children are exempt.
946
+ **The emptiness test is cached on the body (`ColoredBody::knownEmpty`, v1.17.0).** It used to
947
+ call `Manifold::NumTri()` on every child at every level, and every Manifold accessor goes through
948
+ `GetImpl()`, which materialises a lazy transform (a full mesh + collider copy) and runs a lazy
949
+ boolean -- so each leaf was copied once per ancestor transform, and because every node kept its
950
+ `bodies` until the tree died, all those copies stayed alive: fractal_tree.scad held 72,929
951
+ transformed copies of 1024 leaves (3.1GB). Now a body is asked once, the flag rides along on the
952
+ struct copies transform/colour/flatten make (emptiness is transform-invariant, and every generate
953
+ function that builds a new body starts from a fresh `ColoredBody`), and `generateTreeImpl` clears
954
+ each child's `bodies` once the parent has consumed them (move-assigning a fresh vector: `clear()`
955
+ and `= {}` both keep the capacity, and 35K nodes x 592-byte slots was 230MB) -- only top-level
956
+ bodies leave, a cache hit re-fills from the cache, and the bindings' `csg_tree` facade no longer
957
+ converts per-node bodies (it used to `GetMeshGL()` every node into numpy on every evaluate(), for
958
+ a dump that never read them). Three more forcing points had to go for the chain to stay lazy: the
959
+ booleans' `Status()` operand filter (now `bodyStatus()`, same cache), the boolean RESULT's own
960
+ emptiness (derived from the operands' flags -- a union is non-empty if any operand is -- rather
961
+ than asked), and `attachTriColors`, which `GetMeshGL()`'d every boolean result and now runs only
962
+ when the operands could actually disagree on colour. `Evaluator::generatedNodeCount` replaced
963
+ "does the child have bodies" as the cache-hit proxy in tests. Transforms compose lazily inside
964
+ Manifold and each leaf is transformed once, at the boolean that needs it. fractal_tree 3.1GB ->
965
+ 0.67GB, Dalek 370 -> 277MB, snappy-reprap full_assembly 1.6GB -> 0.87GB; wall time neutral to
966
+ -3% interleaved. The output is geometrically identical (triangle count, area, volume) but not
967
+ byte-identical: Manifold evaluates one flat batch union instead of our old level-by-level order.
913
968
 
914
969
  Doing this centrally is also what keeps `generateCsg` safe: it builds ONE result and switches on
915
970
  whether it holds a `body` or a `section`, so a group that changed dimension part-way dereferenced
@@ -1425,6 +1480,39 @@ than the current pause point). Ported identically (same qualifier syntax, same e
1425
1480
  - `tools/cli/main.cpp` — a 3-line wrapper: builds `args` from `argv`, calls `runCli(args)` with the
1426
1481
  real `std::cin`/`std::cout`/`std::cerr`, returns its exit code.
1427
1482
 
1483
+ ## Coverage
1484
+
1485
+ `Evaluator(coverage=true)` records which parts of a run's source actually executed and
1486
+ fills `coverageResult`: `spans` (one per coverable node), `files` (one `CoverageFileSummary`
1487
+ per origin, sorted: statements/branches/bodies/spans, each with a hit count and a percent;
1488
+ `branches` counts Branch spans plus arm Statements, `percent` is over every span) and `total`
1489
+ (the same over every file). `summarizeCoverage()` is public so an aggregated span list can be
1490
+ re-summarised. Bindings: `Evaluator(coverage=True)` → `ev.coverage_result`, a dict
1491
+ `{spans, files, total}` of plain dicts. The vocabulary is fixed in one place,
1492
+ `coverage.hpp`/`coverage.cpp`, and used by both the recorder and the universe walk, so
1493
+ "covered" and "uncovered" never disagree about what counts:
1494
+
1495
+ - **statement** — every node in a statement list (top level, module/if/for/let bodies,
1496
+ `render()` children). A modifier (`%`/`#`/`!`/`*`) is the statement; the call it wraps is
1497
+ not one of its own. The first statement of an if/else arm carries `arm=true`.
1498
+ - **branch** — a ternary arm, a comprehension if/else arm, the right operand of `&&`/`||`.
1499
+ - **body** — a function, module or function-literal declaration: was it ever entered?
1500
+
1501
+ Hits are recorded at deliberate points, once per execution: `evalChildren`'s statement loop
1502
+ and `Op::Cover` (emitted per statement by `compileOneStatement`/`tryCompileAssignmentBlock`
1503
+ and at every arm, only when the Evaluator was built with coverage — chunks are per-Evaluator)
1504
+ on the two paths; `enterUserCall` for bodies; the arm sites in `evalExpr`, the tail
1505
+ trampoline (`simplifyTailStep`, which bypasses `evalExpr`'s own TernaryOp case) and the
1506
+ comprehension if/else cases. **Not** from `checkDebug`: the debugger checkpoints an if-arm's
1507
+ first statement twice and the `if` itself never, so piggybacking on it gave counts that
1508
+ differed between the VM and the interpreter. `Coverage.VmAndInterpreterAgree` compares the
1509
+ covered set under both and is the test that keeps them honest. The VM stays on under
1510
+ coverage (unlike a debug hook). The hit table is indexed by the node's `(treeId, slot)`
1511
+ numbering, so a hit is an array increment. Used-file globals are in the universe via
1512
+ `usedFileGlobals_`; everything else comes from the processed node list with each node's own
1513
+ `origin`, so includes and `use`d files report under their own paths. Let-bindings and plain
1514
+ comprehension elements are not in the vocabulary; hits on them are simply not reported.
1515
+
1428
1516
  ## `render()` in expression position
1429
1517
 
1430
1518
  `render()` has two jobs, decided by where it is written:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openscad_cpp_evaluator
3
- Version: 1.15.0
3
+ Version: 1.19.0
4
4
  Summary: C++ OpenSCAD evaluator with Python bindings
5
5
  Author: NeuSCAD Contributors
6
6
  License-Expression: MIT
@@ -26,6 +26,7 @@
26
26
  #include "openscad_cpp_evaluator/freetype_font_provider.hpp"
27
27
  #include "openscad_cpp_evaluator/mesh_check.hpp"
28
28
  #include "openscad_cpp_evaluator/manifold_cache.hpp"
29
+ #include "openscad_cpp_evaluator/coverage.hpp"
29
30
  #include "openscad_cpp_evaluator/profile.hpp"
30
31
  #include "openscad_cpp_evaluator/value.hpp"
31
32
 
@@ -223,8 +224,9 @@ nb::dict idSpansToDict(const std::vector<IdSpan>& idSpans) {
223
224
  return d;
224
225
  }
225
226
 
226
- // CSGNode -> a facade `_CSGNode` (kind/params/bodies/is_builtin/children),
227
- // recursively, eagerly converting every field to plain Python data. No
227
+ // CSGNode -> a facade `_CSGNode` (kind/params/is_builtin/children; `bodies`
228
+ // is always empty, see below), recursively, eagerly converting every field
229
+ // to plain Python data. No
228
230
  // lifetime extension needed -- CSGParams (Value-only, see csg_node.hpp) and
229
231
  // ColoredBody never hold AST pointers, so the result is fully self-contained
230
232
  // once built, unlike CSGNode::node itself (deliberately not exposed: the AST
@@ -234,15 +236,14 @@ nb::dict idSpansToDict(const std::vector<IdSpan>& idSpans) {
234
236
  nb::object csgNodeToPy(const oscadeval::CSGNode& node) {
235
237
  nb::dict params;
236
238
  for (const auto& [k, v] : node.params) params[nb::str(k.c_str())] = valueToPy(v);
239
+ // Deliberately no bodies. This used to GetMeshGL() every body of every
240
+ // node in the tree into numpy on every evaluate() -- and the only
241
+ // consumer, format_csg_tree(), never read them. Since v1.17.0 an inner
242
+ // node's bodies are released once its parent has consumed them (see
243
+ // generateTreeImpl), so there is nothing here to convert anyway; and a
244
+ // body the tree still holds may be a LAZY Manifold whose GetMeshGL()
245
+ // would materialise the very transform that release exists to avoid.
237
246
  nb::list bodies;
238
- for (const oscadeval::ColoredBody& cb : node.bodies) {
239
- if (!cb.isDisplayOnly() && (!cb.body || cb.body->IsEmpty())) continue;
240
- // ColoredBody here is const in this walk (tree ownership stays with
241
- // the caller) -- GetMeshGL() itself is logically read-only, so a
242
- // const_cast here is safe/local, mirroring bodyToDict's mutating
243
- // signature only because manifold3d's own API isn't const-qualified.
244
- bodies.append(bodyToDict(const_cast<oscadeval::ColoredBody&>(cb)));
245
- }
246
247
  nb::list children;
247
248
  for (const std::unique_ptr<oscadeval::CSGNode>& child : node.children) children.append(csgNodeToPy(*child));
248
249
  return facadeAttr("_CSGNode")(node.kind, params, bodies, node.isBuiltin, children);
@@ -432,9 +433,51 @@ nb::list exportModelPy(const std::string& path, const Geometry& geom, const std:
432
433
  return out;
433
434
  }
434
435
 
436
+ nb::dict coverageSummaryToPy(const oscadeval::CoverageFileSummary& f) {
437
+ nb::dict d;
438
+ d["origin"] = f.origin;
439
+ d["statements"] = f.statements;
440
+ d["statements_hit"] = f.statements_hit;
441
+ d["branches"] = f.branches;
442
+ d["branches_hit"] = f.branches_hit;
443
+ d["bodies"] = f.bodies;
444
+ d["bodies_hit"] = f.bodies_hit;
445
+ d["spans"] = f.spans;
446
+ d["spans_hit"] = f.spans_hit;
447
+ d["percent"] = f.percent();
448
+ d["statement_percent"] = f.statement_percent();
449
+ d["branch_percent"] = f.branch_percent();
450
+ d["body_percent"] = f.body_percent();
451
+ return d;
452
+ }
453
+
454
+ nb::object coverageResultToPy(const std::optional<oscadeval::CoverageResult>& cr) {
455
+ if (!cr) return nb::none();
456
+ nb::list files;
457
+ for (const oscadeval::CoverageFileSummary& f : cr->files) files.append(coverageSummaryToPy(f));
458
+ nb::list out;
459
+ for (const oscadeval::CoverageSpan& s : cr->spans) {
460
+ nb::dict d;
461
+ d["origin"] = s.origin;
462
+ d["line"] = s.line;
463
+ d["column"] = s.column;
464
+ d["start"] = s.start_offset;
465
+ d["end"] = s.end_offset;
466
+ d["kind"] = oscadeval::coverageKindName(s.kind);
467
+ d["arm"] = s.arm;
468
+ d["hits"] = s.hits;
469
+ out.append(d);
470
+ }
471
+ nb::dict result;
472
+ result["spans"] = out;
473
+ result["files"] = files;
474
+ result["total"] = coverageSummaryToPy(cr->total);
475
+ return result;
476
+ }
477
+
435
478
  nb::object evaluate(const std::string& path, nb::dict viewportParams,
436
479
  std::shared_ptr<oscadeval::ManifoldCache> manifoldCache, bool profile,
437
- bool generate, bool strictCommas) {
480
+ bool generate, bool strictCommas, bool coverage) {
438
481
  std::unordered_map<std::string, oscadeval::Value> vp = toViewportParams(viewportParams);
439
482
 
440
483
  std::vector<oscadeval::ColoredBody> bodies;
@@ -442,6 +485,7 @@ nb::object evaluate(const std::string& path, nb::dict viewportParams,
442
485
  std::vector<IdSpan> idSpans;
443
486
  std::vector<std::unique_ptr<oscadeval::CSGNode>> csgTree;
444
487
  std::optional<oscadeval::ProfileResult> profileResult;
488
+ std::optional<oscadeval::CoverageResult> coverageResult;
445
489
  nb::dict dyn;
446
490
  nb::object dynExplicit;
447
491
  {
@@ -456,7 +500,8 @@ nb::object evaluate(const std::string& path, nb::dict viewportParams,
456
500
  if (strictCommas) strict.emplace();
457
501
  oscad::ParsedProgram program = oscad::getProgramFromFile(path);
458
502
  oscadeval::ResolvedUseScopes used = oscadeval::resolveUseScopes(program.nodes, path, logFn);
459
- oscadeval::Evaluator ev(logFn, nullptr, manifoldCache, oscadeval::DebugHooks{}, profile);
503
+ oscadeval::Evaluator ev(logFn, nullptr, manifoldCache, oscadeval::DebugHooks{}, profile, coverage);
504
+ ev.setUsedFileGlobals(used.usedFileGlobals);
460
505
  oscadeval::EvalContext ctx = oscadeval::EvalContext::makeRoot(used.rootScope.get());
461
506
  bodies = oscadeval::toRenderableBodies(ev.evaluate(used.processedNodes, ctx, vp, generate));
462
507
  collectIdSpans(ev, idSpans);
@@ -465,6 +510,7 @@ nb::object evaluate(const std::string& path, nb::dict viewportParams,
465
510
  // which a resolve-only caller has no use for.
466
511
  if (generate) csgTree = std::move(ev.csgTree);
467
512
  profileResult = std::move(ev.profileResult);
513
+ coverageResult = std::move(ev.coverageResult);
468
514
  {
469
515
  nb::gil_scoped_acquire g; // building Python objects needs the GIL back
470
516
  std::tie(dyn, dynExplicit) = dynStateToPy(ctx);
@@ -494,7 +540,7 @@ nb::object evaluate(const std::string& path, nb::dict viewportParams,
494
540
  auto geom = std::make_shared<Geometry>();
495
541
  geom->bodies = std::move(bodies);
496
542
  return nb::make_tuple(bodiesToList(geom->bodies), echoList, idSpansToDict(idSpans), csgTreeToPy(csgTree),
497
- profileResultToPy(profileResult), dyn, dynExplicit, geom);
543
+ profileResultToPy(profileResult), dyn, dynExplicit, geom, coverageResultToPy(coverageResult));
498
544
  }
499
545
 
500
546
  // ------------------------------------------------------------------------
@@ -764,6 +810,7 @@ nb::object debugEvaluate(const std::string& path, nb::dict viewportParams, nb::c
764
810
  oscadeval::Evaluator ev(echoCpp, nullptr, manifoldCache, hooks, false);
765
811
  evPtr = &ev;
766
812
  if (fastContinueSignal) ev.setFastContinueInterruptFlag(fastContinueSignal->flag());
813
+ ev.setUsedFileGlobals(used.usedFileGlobals);
767
814
  oscadeval::EvalContext ctx = oscadeval::EvalContext::makeRoot(used.rootScope.get());
768
815
  bodies = oscadeval::toRenderableBodies(ev.evaluate(used.processedNodes, ctx, vp));
769
816
  collectIdSpans(ev, idSpans);
@@ -908,7 +955,14 @@ NB_MODULE(_openscad_cpp_evaluator, m) {
908
955
 
909
956
  m.def("evaluate", &evaluate, nb::arg("path"), nb::arg("viewport_params"), nb::arg("manifold_cache") = nullptr,
910
957
  nb::arg("profile") = false, nb::arg("generate") = true, nb::arg("strict_commas") = false,
911
- "Evaluate a .scad file; return (bodies, echoes, id_to_node, csg_tree, profile_result, dyn, dyn_explicit).\n"
958
+ nb::arg("coverage") = false,
959
+ "Evaluate a .scad file; return (bodies, echoes, id_to_node, csg_tree, profile_result, dyn, dyn_explicit, "
960
+ "geometry, coverage_result).\n"
961
+ "coverage=True records which statements, branch arms and bodies ran: coverage_result is a dict "
962
+ "{spans, files, total}: spans is a list of dicts (origin, line, column, start, end, kind, arm, hits), "
963
+ "one per coverable node of every file the run touched; files is one summary per origin, sorted "
964
+ "(statements/statements_hit, branches/branches_hit, bodies/bodies_hit, spans/spans_hit and the four "
965
+ "percent fields); total is the same over every file. None when off.\n"
912
966
  "generate=False stops after the resolve pass: the script runs and reports everything "
913
967
  "it normally would, but no Manifold geometry is built, and neither bodies nor csg_tree "
914
968
  "are populated.\n\n"
@@ -728,6 +728,12 @@ enum class Op {
728
728
  // on failure, otherwise runs any chained node.children natively (rare;
729
729
  // not worth its own compiled path -- see AssertSite's own doc comment).
730
730
  AssertStatement,
731
+ // a = nativeStatements index of the AST node whose arm this is. Emitted
732
+ // only when the Evaluator was built with coverage on (compile-time
733
+ // flag, and chunks are per-Evaluator): Evaluator::coverHit(node), nothing
734
+ // else. Marks the arms the debugger never checkpoints in compiled code
735
+ // -- ternary arms, comprehension if/else arms, `&&`/`||` right operands.
736
+ Cover,
731
737
  };
732
738
 
733
739
  struct Instruction {
@@ -21,7 +21,7 @@ namespace oscadeval {
21
21
  // whole function, unconditionally correct since nothing about this
22
22
  // function's behavior changes based on whether it happened to compile.
23
23
  std::optional<CompiledChunk> tryCompileFunction(const oscad::FunctionDeclaration& decl,
24
- const oscad::ScopeTable* scopeTable);
24
+ const oscad::ScopeTable* scopeTable, bool coverage = false);
25
25
 
26
26
  // Compiles a bare STATEMENT-context expression -- an assignment's RHS, an
27
27
  // if/for condition, a module-call or echo()/assert() argument -- rather
@@ -50,7 +50,7 @@ std::optional<CompiledChunk> tryCompileFunction(const oscad::FunctionDeclaration
50
50
  // any callee inside it statically, the same way a function body's call
51
51
  // sites are.
52
52
  std::optional<CompiledChunk> tryCompileStatementExpr(const oscad::Expression& expr, const oscad::Scope* scope,
53
- const oscad::ScopeTable* scopeTable);
53
+ const oscad::ScopeTable* scopeTable, bool coverage = false);
54
54
 
55
55
  // Compiles a run of SIBLING assignment statements sharing one scope --
56
56
  // exactly Evaluator::evalChildren's own `assignments` sub-list (stmt_eval.
@@ -85,7 +85,7 @@ std::optional<CompiledChunk> tryCompileStatementExpr(const oscad::Expression& ex
85
85
  // instead of just for that one assignment's own RHS.
86
86
  std::optional<CompiledChunk> tryCompileAssignmentBlock(const std::vector<const oscad::Assignment*>& assigns,
87
87
  const oscad::ScopeTable* scopeTable,
88
- const oscad::Scope* scope);
88
+ const oscad::Scope* scope, bool coverage = false);
89
89
 
90
90
  // Attempts to compile `decl`'s parameter defaults + STATEMENT-list body
91
91
  // (decl.children) to bytecode (Stage 2) -- the module-side analog of
@@ -101,7 +101,7 @@ std::optional<CompiledChunk> tryCompileAssignmentBlock(const std::vector<const o
101
101
  // EXPRESSION (inside an if-condition/for-range/argument) can still bail
102
102
  // (via tryCompileStatementExpr's own NotCompilable, propagated up).
103
103
  std::optional<CompiledChunk> tryCompileModuleBody(const oscad::ModuleDeclaration& decl,
104
- const oscad::ScopeTable* scopeTable);
104
+ const oscad::ScopeTable* scopeTable, bool coverage = false);
105
105
 
106
106
  // Same compilation (assignment/if/for/resolved-module-call get real
107
107
  // bytecode, everything else a native passthrough), but for an ARBITRARY
@@ -126,6 +126,6 @@ std::optional<CompiledChunk> tryCompileModuleBody(const oscad::ModuleDeclaration
126
126
  // scope()`, mirroring tryRunCompiledAssignmentBlock's own convention.
127
127
  std::optional<CompiledChunk> tryCompileChildrenList(const std::vector<const oscad::ASTNode*>& children,
128
128
  const oscad::ScopeTable* scopeTable,
129
- const oscad::Scope* scope);
129
+ const oscad::Scope* scope, bool coverage = false);
130
130
 
131
131
  } // namespace oscadeval
@@ -53,6 +53,22 @@ struct ColoredBody {
53
53
  BodyRole role = BodyRole::Normal;
54
54
  std::optional<std::vector<std::array<float, 4>>> triColors; // per-triangle RGBA, multi-color CSG merges only
55
55
 
56
+ // Whether `body` is empty, once anything has asked (isEmptyBody,
57
+ // csg_generate.cpp). Asking Manifold is not free: every accessor goes
58
+ // through GetImpl(), which MATERIALIZES a lazy transform (a full copy of
59
+ // the mesh and its collider) and, for a boolean, runs it. Emptiness is
60
+ // invariant under transforms and colour, and every generate function
61
+ // that produces a genuinely new body starts from a fresh ColoredBody,
62
+ // so a copy carrying this flag is always still right. Before this, the
63
+ // dimension check at every tree level materialised every child at
64
+ // every level: fractal_tree.scad held 72,929 transformed copies of its
65
+ // 1024 leaf meshes at once (3.1GB).
66
+ std::optional<bool> knownEmpty;
67
+ // Same idea for Manifold::Status(), which the booleans consult to keep an
68
+ // invalid operand from poisoning a union: validity is transform-invariant
69
+ // too, and a boolean of valid operands is valid.
70
+ std::optional<manifold::Manifold::Error> knownStatus;
71
+
56
72
  // Set ONLY when a mesh could not be built into a valid Manifold -- in
57
73
  // practice a polyhedron() whose faces don't close the surface (it has
58
74
  // boundary edges), which Manifold reports by returning an *empty* body
@@ -75,6 +91,33 @@ struct ColoredBody {
75
91
  bool isDisplayOnly() const { return rawMesh.has_value(); }
76
92
  };
77
93
 
94
+ // The cached questions -- see ColoredBody::knownEmpty. Ask Manifold once per
95
+ // body, never once per tree level.
96
+ inline bool bodyIsEmpty(ColoredBody& b) {
97
+ if (!b.knownEmpty) b.knownEmpty = !b.body || b.body->IsEmpty();
98
+ return *b.knownEmpty;
99
+ }
100
+ inline manifold::Manifold::Error bodyStatus(ColoredBody& b) {
101
+ if (!b.knownStatus) b.knownStatus = b.body ? b.body->Status() : manifold::Manifold::Error::NoError;
102
+ return *b.knownStatus;
103
+ }
104
+
105
+ // Emptiness of a union, from its operands' own flags and nothing else:
106
+ // non-empty if any operand is known non-empty, empty if every one is known
107
+ // empty, otherwise unknown (asked of Manifold later, if anyone ever asks).
108
+ // `get` maps a range element to its ColoredBody.
109
+ template <typename Range, typename Get>
110
+ std::optional<bool> unionEmptyOf(const Range& ops, Get get) {
111
+ bool allKnownEmpty = true;
112
+ for (const auto& o : ops) {
113
+ const ColoredBody& b = get(o);
114
+ if (b.knownEmpty && !*b.knownEmpty) return false;
115
+ if (!b.knownEmpty) allKnownEmpty = false;
116
+ }
117
+ if (allKnownEmpty) return true;
118
+ return std::nullopt;
119
+ }
120
+
78
121
  // `EvalContext::color`/CSGParams round-trip: a resolve function reads
79
122
  // ctx.color (double-precision, matching Value's own numeric type) but a
80
123
  // generate function only sees CSGParams (Value-typed, no ctx access) -- so
@@ -0,0 +1,116 @@
1
+ #pragma once
2
+
3
+ // Coverage: which parts of a script (and of the libraries it pulls in) ran.
4
+ //
5
+ // The unit of coverage is the checkpoint set the debugger already uses --
6
+ // Evaluator::checkDebug fires for every statement, every function/module
7
+ // body entered, every if/else arm's first statement, every ternary arm and
8
+ // list-comprehension if/else arm -- plus two arms the debugger never needed
9
+ // to stop at, the right operands of `&&`/`||`. Recording a hit is an array
10
+ // increment indexed by the node's (treeId, slot), the dense per-parse
11
+ // numbering ScopeTable relies on, so the bytecode VM stays on and a hit
12
+ // costs no hashing and no string.
13
+ //
14
+ // What COULD have run -- the universe the report counts against -- comes
15
+ // from a walk over the same trees (collectCoverable) using one predicate
16
+ // for both sides, so "covered" and "uncovered" are one vocabulary. A hit
17
+ // on a node outside that vocabulary (a let-binding, a plain comprehension
18
+ // element) is simply not reported.
19
+
20
+ #include "openscad_cpp_parser/ast/ast_node.hpp"
21
+
22
+ #include <cstdint>
23
+ #include <string>
24
+ #include <vector>
25
+
26
+ namespace oscadeval {
27
+
28
+ enum class CoverageKind {
29
+ Statement, // a statement in a module body, an if/for/let body, or at top level
30
+ Branch, // a ternary arm, a comprehension if/else arm, a `&&`/`||` right operand
31
+ Body, // a function, module or function-literal body: was it ever entered?
32
+ };
33
+
34
+ const char* coverageKindName(CoverageKind kind);
35
+
36
+ struct CoverageSpan {
37
+ std::string origin;
38
+ int line = 0;
39
+ int column = 0;
40
+ int start_offset = 0;
41
+ int end_offset = 0;
42
+ CoverageKind kind = CoverageKind::Statement;
43
+ // A Statement that is the first of an if/else arm. Branch coverage counts
44
+ // these alongside the Branch kind; they stay Statements so a statement
45
+ // count is still a statement count.
46
+ bool arm = false;
47
+ std::uint32_t hits = 0;
48
+ };
49
+
50
+ // Per-file totals. "branches" counts Branch spans plus arm Statements (an
51
+ // if/else arm is a branch too); those arm statements are ALSO in
52
+ // "statements", since a statement count should be a statement count.
53
+ // "percent" is over every span in the file, whatever its kind.
54
+ struct CoverageFileSummary {
55
+ std::string origin;
56
+ std::uint32_t statements = 0, statements_hit = 0;
57
+ std::uint32_t branches = 0, branches_hit = 0;
58
+ std::uint32_t bodies = 0, bodies_hit = 0;
59
+ std::uint32_t spans = 0, spans_hit = 0;
60
+ double percent() const { return spans ? 100.0 * spans_hit / spans : 100.0; }
61
+ double statement_percent() const { return statements ? 100.0 * statements_hit / statements : 100.0; }
62
+ double branch_percent() const { return branches ? 100.0 * branches_hit / branches : 100.0; }
63
+ double body_percent() const { return bodies ? 100.0 * bodies_hit / bodies : 100.0; }
64
+ };
65
+
66
+ struct CoverageResult {
67
+ std::vector<CoverageSpan> spans; // one per coverable node, source order per tree
68
+ std::vector<CoverageFileSummary> files; // one per origin, sorted by origin
69
+ CoverageFileSummary total; // every file together (origin empty)
70
+ };
71
+
72
+ // The per-file and total summaries for `spans`; buildCoverageResult calls
73
+ // this, and a merged/aggregated span list (BelfrySCAD's --test --coverage)
74
+ // can call it again.
75
+ void summarizeCoverage(CoverageResult& result);
76
+
77
+ class CoverageRecorder {
78
+ public:
79
+ void clear() { hits_.clear(); }
80
+ void hit(const oscad::ASTNode& node) {
81
+ const std::uint32_t tree = node.treeId(), slot = node.slot();
82
+ if (tree >= hits_.size()) hits_.resize(tree + 1);
83
+ std::vector<std::uint32_t>& slots = hits_[tree];
84
+ if (slot >= slots.size()) slots.resize(slot + 1, 0);
85
+ ++slots[slot];
86
+ }
87
+ std::uint32_t hitsFor(const oscad::ASTNode& node) const {
88
+ const std::uint32_t tree = node.treeId(), slot = node.slot();
89
+ if (tree >= hits_.size()) return 0;
90
+ const std::vector<std::uint32_t>& slots = hits_[tree];
91
+ return slot < slots.size() ? slots[slot] : 0;
92
+ }
93
+
94
+ private:
95
+ std::vector<std::vector<std::uint32_t>> hits_;
96
+ };
97
+
98
+ struct Coverable {
99
+ const oscad::ASTNode* node = nullptr;
100
+ CoverageKind kind = CoverageKind::Statement;
101
+ bool arm = false;
102
+ };
103
+
104
+ // Every coverable node under `roots` (a top-level statement list: the main
105
+ // file with its includes spliced in and any use-injected declarations) plus
106
+ // `extraStatements` (a used file's own top-level assignments, which `use`
107
+ // never splices anywhere -- see Evaluator::setUsedFileGlobals). Source
108
+ // order within each tree; a node reachable twice is listed once.
109
+ std::vector<Coverable> collectCoverable(const std::vector<const oscad::ASTNode*>& roots,
110
+ const std::vector<const oscad::ASTNode*>& extraStatements);
111
+
112
+ CoverageResult buildCoverageResult(const std::vector<const oscad::ASTNode*>& roots,
113
+ const std::vector<const oscad::ASTNode*>& extraStatements,
114
+ const CoverageRecorder& recorder);
115
+
116
+ } // namespace oscadeval
@@ -15,9 +15,21 @@ namespace oscadeval {
15
15
  // must keep this struct alive for as long as evaluation runs against
16
16
  // .rootScope/.processedNodes, since declarations injected from a used file
17
17
  // are raw (non-owning) pointers into it.
18
+ // One used file's own top-level assignments, in source order, keyed by
19
+ // that file's root Scope (the one its injected declarations were
20
+ // re-anchored onto). Evaluator::setUsedFileGlobals takes the list so a
21
+ // used file's globals can be evaluated ONCE per run, eagerly, the first
22
+ // time anything in that file reads one -- see Evaluator::fileGlobal.
23
+ // Raw pointers into usedFileAsts, same lifetime contract as processedNodes.
24
+ struct UsedFileGlobals {
25
+ const oscad::Scope* root = nullptr;
26
+ std::vector<const oscad::ASTNode*> assignments;
27
+ };
28
+
18
29
  struct ResolvedUseScopes {
19
30
  std::vector<std::vector<std::unique_ptr<oscad::ASTNode>>> usedFileAsts;
20
31
  std::vector<std::unique_ptr<oscad::Scope>> usedFileScopes;
32
+ std::vector<UsedFileGlobals> usedFileGlobals;
21
33
  // What currentFile should actually be evaluated as: declarations
22
34
  // injected via `use` (raw pointers into usedFileAsts, above), followed
23
35
  // by currentFile's own nodes (raw pointers into the caller-owned
@@ -7,6 +7,8 @@
7
7
  #include "openscad_cpp_evaluator/csg_node.hpp"
8
8
  #include "openscad_cpp_evaluator/debug_hooks.hpp"
9
9
  #include "openscad_cpp_evaluator/eval_context.hpp"
10
+ #include "openscad_cpp_evaluator/eval_use.hpp"
11
+ #include "openscad_cpp_evaluator/coverage.hpp"
10
12
  #include "openscad_cpp_evaluator/eval_error.hpp"
11
13
  #include "openscad_cpp_evaluator/font_provider.hpp"
12
14
  #include "openscad_cpp_evaluator/manifold_cache.hpp"
@@ -83,7 +85,7 @@ public:
83
85
  // work at all.
84
86
  explicit Evaluator(EchoFn echoFn = {}, std::shared_ptr<FontProvider> fontProvider = nullptr,
85
87
  std::shared_ptr<ManifoldCache> manifoldCache = nullptr, DebugHooks debugHooks = {},
86
- bool profiling = false);
88
+ bool profiling = false, bool coverage = false);
87
89
 
88
90
  // Text/font builtins' shared access to the resolved FontProvider
89
91
  // (lazily constructing the built-in default on first call if none was
@@ -285,6 +287,11 @@ public:
285
287
  const oscad::ASTNode* producer);
286
288
 
287
289
  std::unordered_map<uint32_t, const oscad::ASTNode*> idToNode;
290
+ // Nodes whose bodies were actually generated this run (not served from
291
+ // the ManifoldCache). Reset per run; a diagnostic, and the only honest
292
+ // way for a test to tell a cache hit from a miss now that an inner
293
+ // node's bodies are released the moment its parent has consumed them.
294
+ std::size_t generatedNodeCount = 0;
288
295
  std::unordered_map<uint32_t, std::optional<std::array<float, 4>>> idToColor;
289
296
 
290
297
  // Called by primitive-construction generate functions (cube, sphere,
@@ -653,6 +660,22 @@ public:
653
660
  // resolve+generate bracket evaluate() itself runs). Mirrors the
654
661
  // reference's Evaluator.profile_result.
655
662
  std::optional<ProfileResult> profileResult;
663
+ // Filled by evaluate() when constructed with coverage=true: one span per
664
+ // coverable node of every tree the run touched, with its hit count.
665
+ // See coverage.hpp for the vocabulary.
666
+ std::optional<CoverageResult> coverageResult;
667
+
668
+ bool coverageEnabled() const { return coverage_; }
669
+ // Called once per statement execution (evalChildren on the interpreter
670
+ // side, Op::Cover on the compiled side), once per body entry
671
+ // (enterUserCall), and once per branch arm taken (ternary, comprehension
672
+ // if/else, `&&`/`||` right operand). Deliberately NOT from checkDebug:
673
+ // the debugger's checkpoints double up on an if-arm's first statement
674
+ // and never land on the `if` itself. Free when coverage is off: one
675
+ // predictable branch.
676
+ void coverHit(const oscad::ASTNode& node) {
677
+ if (coverage_) coverageRecorder_.hit(node);
678
+ }
656
679
 
657
680
  // The resolved (and, after evaluate() runs, generated) CSG tree from the
658
681
  // most recent evaluate() call -- moved here from evaluateImpl's local
@@ -1396,6 +1419,11 @@ public:
1396
1419
  // the same decision evalUserFunction*/evalFunctionLiteral* already
1397
1420
  // make before calling lookupOrCompileChunk/lookupCompiledLiteralChunk
1398
1421
  // themselves.
1422
+ // Every used file's own top-level assignments (ResolvedUseScopes::
1423
+ // usedFileGlobals), so fileGlobal() can evaluate a file's globals once.
1424
+ // A caller that skips this keeps the old per-read fallback for used files.
1425
+ void setUsedFileGlobals(std::vector<UsedFileGlobals> globals) { usedFileGlobals_ = std::move(globals); }
1426
+
1399
1427
  bool useBytecodeVm() const {
1400
1428
  return bytecodeVmEnabled() && (!debugHooks_.debugHook || fastContinueBreakpoints_.has_value());
1401
1429
  }
@@ -1941,6 +1969,25 @@ private:
1941
1969
  // lifetime contract as every other raw AST/EvalContext pointer this
1942
1970
  // class holds).
1943
1971
  const EvalContext* rootCtx_ = nullptr;
1972
+
1973
+ // File-level globals are evaluated ONCE per run. The main file's (and
1974
+ // every include's) already are, by the top-level statement pass, into
1975
+ // rootCtx_'s own let_ level. A used file's are not -- `use` injects only
1976
+ // its declarations -- so the first read of any global from that file
1977
+ // evaluates ALL of its assignments, in source order, into a context of
1978
+ // its own, kept here for the rest of the run. Before this, a function
1979
+ // reading a global re-evaluated the assignment's expression on every
1980
+ // read (evalIdentifier's scope fallback): `r = rands(0,1,1); function
1981
+ // g() = r;` gave `g() != g()`, and a 20K-element global list read 2000
1982
+ // times from a function cost 9.7s. OpenSCAD itself re-evaluates a used
1983
+ // file's globals per CALL, which is a long-standing upstream bug, not
1984
+ // behaviour to match. Returns nullopt for a file this evaluator was
1985
+ // never told about (setUsedFileGlobals), so such a caller keeps the
1986
+ // old fallback; nullptr for a known file whose global is not (yet)
1987
+ // assigned -- a forward read, undef in OpenSCAD too.
1988
+ std::optional<const Value*> fileGlobal(const oscad::Scope& fileRoot, const std::string& name);
1989
+ std::vector<UsedFileGlobals> usedFileGlobals_;
1990
+ std::unordered_map<const oscad::Scope*, EvalContext> usedFileCtx_;
1944
1991
  // The most recent ctx seen by evalStatement() -- error()'s only way to
1945
1992
  // reach a locals snapshot for its own errorBreak hook call, since
1946
1993
  // error() itself (called from dozens of builtin resolve functions)
@@ -1954,6 +2001,8 @@ private:
1954
2001
  // -- Profiling (Phase 9) ----------------------------------------------
1955
2002
 
1956
2003
  bool profiling_ = false;
2004
+ bool coverage_ = false;
2005
+ CoverageRecorder coverageRecorder_;
1957
2006
  std::map<ProfileSiteKey, CallSiteProfile> profileSites_;
1958
2007
  std::set<ProfileSiteKey> profileActive_; // site keys with a call currently on callStack_
1959
2008
  std::vector<double> profileChildTime_; // parallel aux stack to callStack_