openscad-cpp-evaluator 1.15.0__tar.gz → 1.18.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 (218) hide show
  1. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/CLAUDE.md +55 -0
  2. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/PKG-INFO +1 -1
  3. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/bindings/module.cpp +12 -10
  4. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/colored_body.hpp +43 -0
  5. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/eval_use.hpp +12 -0
  6. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/evaluator.hpp +30 -0
  7. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/scope_trail.hpp +40 -4
  8. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/pyproject.toml +1 -1
  9. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/python/openscad_cpp_evaluator/__init__.py +5 -2
  10. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/booleans.cpp +32 -3
  11. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/color.cpp +2 -2
  12. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/control.cpp +1 -0
  13. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/src/bytecode_vm.cpp +1 -1
  14. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/src/csg_generate.cpp +22 -2
  15. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/src/csg_resolve.cpp +2 -0
  16. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/src/eval_use.cpp +6 -0
  17. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/src/expr_eval.cpp +35 -0
  18. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/src/user_calls.cpp +6 -2
  19. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/tests/test_booleans.cpp +27 -0
  20. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/tests/test_include_cache.cpp +2 -0
  21. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/tests/test_manifold_cache.cpp +7 -8
  22. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/tests/test_scope_trail.cpp +64 -0
  23. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/tests/test_use.cpp +56 -0
  24. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/tools/cli/cli_lib.cpp +1 -0
  25. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/.github/workflows/ci.yml +0 -0
  26. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/.github/workflows/wheels.yml +0 -0
  27. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/.gitignore +0 -0
  28. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/.gitmodules +0 -0
  29. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/CMakeLists.txt +0 -0
  30. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/LICENSE +0 -0
  31. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/README.md +0 -0
  32. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/bindings/CMakeLists.txt +0 -0
  33. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/bindings/ast_to_py.cpp +0 -0
  34. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/cmake/embed_font.cmake +0 -0
  35. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/examples/CMakeLists.txt +0 -0
  36. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/examples/eval_perf_benchmark.cpp +0 -0
  37. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/examples/manifold_cache_reuse.cpp +0 -0
  38. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/examples/minimal_debugger.cpp +0 -0
  39. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/.github/workflows/ci.yml +0 -0
  40. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/.gitignore +0 -0
  41. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/CLAUDE.md +0 -0
  42. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/CMakeLists.txt +0 -0
  43. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/README.md +0 -0
  44. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/api.hpp +0 -0
  45. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/ast_node.hpp +0 -0
  46. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/comments.hpp +0 -0
  47. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/declarations.hpp +0 -0
  48. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/expression.hpp +0 -0
  49. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/module_instantiation.hpp +0 -0
  50. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/scope_builder.hpp +0 -0
  51. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/vector_element.hpp +0 -0
  52. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast.hpp +0 -0
  53. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/comments.hpp +0 -0
  54. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/position.hpp +0 -0
  55. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/pretty_print.hpp +0 -0
  56. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/scope.hpp +0 -0
  57. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/scope_table.hpp +0 -0
  58. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/serialization.hpp +0 -0
  59. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/source_map.hpp +0 -0
  60. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/CMakeLists.txt +0 -0
  61. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/api.cpp +0 -0
  62. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/ast/ast_node.cpp +0 -0
  63. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/ast/declarations.cpp +0 -0
  64. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/ast/expression.cpp +0 -0
  65. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/ast/format_utils.hpp +0 -0
  66. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/ast/module_instantiation.cpp +0 -0
  67. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/ast/scope_builder.cpp +0 -0
  68. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/ast/vector_element.cpp +0 -0
  69. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/comment_attach_internal.hpp +0 -0
  70. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/comments.cpp +0 -0
  71. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/grammar/driver.cpp +0 -0
  72. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/grammar/driver.hpp +0 -0
  73. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/grammar/lexer.l +0 -0
  74. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/grammar/lexer_api.hpp +0 -0
  75. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/grammar/oscad_location.hpp +0 -0
  76. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/grammar/parser.y +0 -0
  77. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/inline_comment_attach.cpp +0 -0
  78. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/position.cpp +0 -0
  79. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/pretty_print.cpp +0 -0
  80. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/scope.cpp +0 -0
  81. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/serialization/json_io.cpp +0 -0
  82. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/serialization/yaml_io.cpp +0 -0
  83. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/src/source_map.cpp +0 -0
  84. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/CMakeLists.txt +0 -0
  85. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_assignments.cpp +0 -0
  86. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_ast_generation.cpp +0 -0
  87. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_comments_and_files.cpp +0 -0
  88. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_const_borrow.cpp +0 -0
  89. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_control.cpp +0 -0
  90. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_expressions.cpp +0 -0
  91. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_functions.cpp +0 -0
  92. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_helpers.hpp +0 -0
  93. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_inline_comments.cpp +0 -0
  94. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_lexical.cpp +0 -0
  95. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_modules.cpp +0 -0
  96. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_node_str.cpp +0 -0
  97. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_pretty_print.cpp +0 -0
  98. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_render_expression.cpp +0 -0
  99. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_scope.cpp +0 -0
  100. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_serialization.cpp +0 -0
  101. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_smoke.cpp +0 -0
  102. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_source_map.cpp +0 -0
  103. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_strict_commas.cpp +0 -0
  104. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_use_include.cpp +0 -0
  105. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tests/test_vectors.cpp +0 -0
  106. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tools/cli/CMakeLists.txt +0 -0
  107. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/external/openscad_cpp_parser/tools/cli/main.cpp +0 -0
  108. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/bound_args.hpp +0 -0
  109. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/bytecode.hpp +0 -0
  110. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/bytecode_compiler.hpp +0 -0
  111. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/bytecode_vm.hpp +0 -0
  112. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/call_args.hpp +0 -0
  113. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/csg_node.hpp +0 -0
  114. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/css_colors.hpp +0 -0
  115. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/debug_hooks.hpp +0 -0
  116. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/debug_repl.hpp +0 -0
  117. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/dispatch.hpp +0 -0
  118. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/dxf_svg_import.hpp +0 -0
  119. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/eval_context.hpp +0 -0
  120. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/eval_error.hpp +0 -0
  121. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/export.hpp +0 -0
  122. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/font_match.hpp +0 -0
  123. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/font_provider.hpp +0 -0
  124. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/format_closure.hpp +0 -0
  125. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/freetype_font_provider.hpp +0 -0
  126. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/function_builtins.hpp +0 -0
  127. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/import_builtin.hpp +0 -0
  128. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/manifold_cache.hpp +0 -0
  129. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/mesh_check.hpp +0 -0
  130. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/mesh_import.hpp +0 -0
  131. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/osc_range.hpp +0 -0
  132. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/profile.hpp +0 -0
  133. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/segments.hpp +0 -0
  134. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/surface_load.hpp +0 -0
  135. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/text_metrics.hpp +0 -0
  136. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/utf8.hpp +0 -0
  137. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/value.hpp +0 -0
  138. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/include/openscad_cpp_evaluator/zip_stored.hpp +0 -0
  139. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/resources/fonts/LICENSE +0 -0
  140. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/resources/fonts/LiberationSans-Bold.ttf +0 -0
  141. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/resources/fonts/LiberationSans-BoldItalic.ttf +0 -0
  142. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/resources/fonts/LiberationSans-Italic.ttf +0 -0
  143. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/resources/fonts/LiberationSans-Regular.ttf +0 -0
  144. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/src/CMakeLists.txt +0 -0
  145. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/builtins.hpp +0 -0
  146. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/call_args.cpp +0 -0
  147. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/dxf_dim.cpp +0 -0
  148. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/extrude.cpp +0 -0
  149. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/function_builtins.cpp +0 -0
  150. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/import.cpp +0 -0
  151. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/modifiers.cpp +0 -0
  152. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/primitives_2d.cpp +0 -0
  153. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/primitives_3d.cpp +0 -0
  154. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/registry.cpp +0 -0
  155. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/roof.cpp +0 -0
  156. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/segments.cpp +0 -0
  157. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/surface.cpp +0 -0
  158. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/text.cpp +0 -0
  159. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/topology.cpp +0 -0
  160. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/src/builtins/transforms.cpp +0 -0
  161. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/src/bytecode_compiler.cpp +0 -0
  162. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/src/colored_body.cpp +0 -0
  163. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/src/css_colors.cpp +0 -0
  164. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/src/debug/debug_repl.cpp +0 -0
  165. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/src/debug_profile.cpp +0 -0
  166. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/src/eval_context.cpp +0 -0
  167. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/src/eval_error.cpp +0 -0
  168. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/src/export.cpp +0 -0
  169. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/src/format_closure.cpp +0 -0
  170. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/src/import/dxf_import.cpp +0 -0
  171. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/src/import/mesh_import.cpp +0 -0
  172. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/src/import/surface_load.cpp +0 -0
  173. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/src/import/svg_import.cpp +0 -0
  174. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/src/manifold_cache.cpp +0 -0
  175. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/src/measure_geometry.cpp +0 -0
  176. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/src/mesh_check.cpp +0 -0
  177. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/src/stmt_eval.cpp +0 -0
  178. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/src/text/font_match.cpp +0 -0
  179. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/src/text/freetype_font_provider.cpp +0 -0
  180. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/src/text/text_metrics.cpp +0 -0
  181. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/src/utf8.cpp +0 -0
  182. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/src/value.cpp +0 -0
  183. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/src/zip_stored.cpp +0 -0
  184. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/tests/CMakeLists.txt +0 -0
  185. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/tests/test_bytecode_compiler.cpp +0 -0
  186. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/tests/test_children_separate.cpp +0 -0
  187. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/tests/test_cli.cpp +0 -0
  188. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/tests/test_control_flow.cpp +0 -0
  189. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/tests/test_csg_tree.cpp +0 -0
  190. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/tests/test_debug_hooks.cpp +0 -0
  191. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/tests/test_dxf_svg_import.cpp +0 -0
  192. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/tests/test_eval_context.cpp +0 -0
  193. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/tests/test_export.cpp +0 -0
  194. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/tests/test_expr_eval.cpp +0 -0
  195. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/tests/test_extrude_roof.cpp +0 -0
  196. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/tests/test_feature_detection.cpp +0 -0
  197. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/tests/test_function_builtins.cpp +0 -0
  198. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/tests/test_helpers.hpp +0 -0
  199. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/tests/test_import_export.cpp +0 -0
  200. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/tests/test_mesh_check.cpp +0 -0
  201. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/tests/test_multi_color_merge.cpp +0 -0
  202. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/tests/test_primitives.cpp +0 -0
  203. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/tests/test_profiling.cpp +0 -0
  204. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/tests/test_python_bindings.py +0 -0
  205. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/tests/test_render_expr.cpp +0 -0
  206. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/tests/test_scoping.cpp +0 -0
  207. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/tests/test_surface.cpp +0 -0
  208. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/tests/test_tail_calls.cpp +0 -0
  209. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/tests/test_text.cpp +0 -0
  210. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/tests/test_transforms.cpp +0 -0
  211. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/tests/test_unexpected_args.cpp +0 -0
  212. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/tests/test_value.cpp +0 -0
  213. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/tests/test_viewport_params.cpp +0 -0
  214. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/tests/test_zip_stored.cpp +0 -0
  215. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/tools/cli/CMakeLists.txt +0 -0
  216. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/tools/cli/cli_lib.hpp +0 -0
  217. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.0}/tools/cli/main.cpp +0 -0
  218. {openscad_cpp_evaluator-1.15.0 → openscad_cpp_evaluator-1.18.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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openscad_cpp_evaluator
3
- Version: 1.15.0
3
+ Version: 1.18.0
4
4
  Summary: C++ OpenSCAD evaluator with Python bindings
5
5
  Author: NeuSCAD Contributors
6
6
  License-Expression: MIT
@@ -223,8 +223,9 @@ nb::dict idSpansToDict(const std::vector<IdSpan>& idSpans) {
223
223
  return d;
224
224
  }
225
225
 
226
- // CSGNode -> a facade `_CSGNode` (kind/params/bodies/is_builtin/children),
227
- // recursively, eagerly converting every field to plain Python data. No
226
+ // CSGNode -> a facade `_CSGNode` (kind/params/is_builtin/children; `bodies`
227
+ // is always empty, see below), recursively, eagerly converting every field
228
+ // to plain Python data. No
228
229
  // lifetime extension needed -- CSGParams (Value-only, see csg_node.hpp) and
229
230
  // ColoredBody never hold AST pointers, so the result is fully self-contained
230
231
  // once built, unlike CSGNode::node itself (deliberately not exposed: the AST
@@ -234,15 +235,14 @@ nb::dict idSpansToDict(const std::vector<IdSpan>& idSpans) {
234
235
  nb::object csgNodeToPy(const oscadeval::CSGNode& node) {
235
236
  nb::dict params;
236
237
  for (const auto& [k, v] : node.params) params[nb::str(k.c_str())] = valueToPy(v);
238
+ // Deliberately no bodies. This used to GetMeshGL() every body of every
239
+ // node in the tree into numpy on every evaluate() -- and the only
240
+ // consumer, format_csg_tree(), never read them. Since v1.17.0 an inner
241
+ // node's bodies are released once its parent has consumed them (see
242
+ // generateTreeImpl), so there is nothing here to convert anyway; and a
243
+ // body the tree still holds may be a LAZY Manifold whose GetMeshGL()
244
+ // would materialise the very transform that release exists to avoid.
237
245
  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
246
  nb::list children;
247
247
  for (const std::unique_ptr<oscadeval::CSGNode>& child : node.children) children.append(csgNodeToPy(*child));
248
248
  return facadeAttr("_CSGNode")(node.kind, params, bodies, node.isBuiltin, children);
@@ -457,6 +457,7 @@ nb::object evaluate(const std::string& path, nb::dict viewportParams,
457
457
  oscad::ParsedProgram program = oscad::getProgramFromFile(path);
458
458
  oscadeval::ResolvedUseScopes used = oscadeval::resolveUseScopes(program.nodes, path, logFn);
459
459
  oscadeval::Evaluator ev(logFn, nullptr, manifoldCache, oscadeval::DebugHooks{}, profile);
460
+ ev.setUsedFileGlobals(used.usedFileGlobals);
460
461
  oscadeval::EvalContext ctx = oscadeval::EvalContext::makeRoot(used.rootScope.get());
461
462
  bodies = oscadeval::toRenderableBodies(ev.evaluate(used.processedNodes, ctx, vp, generate));
462
463
  collectIdSpans(ev, idSpans);
@@ -764,6 +765,7 @@ nb::object debugEvaluate(const std::string& path, nb::dict viewportParams, nb::c
764
765
  oscadeval::Evaluator ev(echoCpp, nullptr, manifoldCache, hooks, false);
765
766
  evPtr = &ev;
766
767
  if (fastContinueSignal) ev.setFastContinueInterruptFlag(fastContinueSignal->flag());
768
+ ev.setUsedFileGlobals(used.usedFileGlobals);
767
769
  oscadeval::EvalContext ctx = oscadeval::EvalContext::makeRoot(used.rootScope.get());
768
770
  bodies = oscadeval::toRenderableBodies(ev.evaluate(used.processedNodes, ctx, vp));
769
771
  collectIdSpans(ev, idSpans);
@@ -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
@@ -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,7 @@
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"
10
11
  #include "openscad_cpp_evaluator/eval_error.hpp"
11
12
  #include "openscad_cpp_evaluator/font_provider.hpp"
12
13
  #include "openscad_cpp_evaluator/manifold_cache.hpp"
@@ -285,6 +286,11 @@ public:
285
286
  const oscad::ASTNode* producer);
286
287
 
287
288
  std::unordered_map<uint32_t, const oscad::ASTNode*> idToNode;
289
+ // Nodes whose bodies were actually generated this run (not served from
290
+ // the ManifoldCache). Reset per run; a diagnostic, and the only honest
291
+ // way for a test to tell a cache hit from a miss now that an inner
292
+ // node's bodies are released the moment its parent has consumed them.
293
+ std::size_t generatedNodeCount = 0;
288
294
  std::unordered_map<uint32_t, std::optional<std::array<float, 4>>> idToColor;
289
295
 
290
296
  // Called by primitive-construction generate functions (cube, sphere,
@@ -1396,6 +1402,11 @@ public:
1396
1402
  // the same decision evalUserFunction*/evalFunctionLiteral* already
1397
1403
  // make before calling lookupOrCompileChunk/lookupCompiledLiteralChunk
1398
1404
  // themselves.
1405
+ // Every used file's own top-level assignments (ResolvedUseScopes::
1406
+ // usedFileGlobals), so fileGlobal() can evaluate a file's globals once.
1407
+ // A caller that skips this keeps the old per-read fallback for used files.
1408
+ void setUsedFileGlobals(std::vector<UsedFileGlobals> globals) { usedFileGlobals_ = std::move(globals); }
1409
+
1399
1410
  bool useBytecodeVm() const {
1400
1411
  return bytecodeVmEnabled() && (!debugHooks_.debugHook || fastContinueBreakpoints_.has_value());
1401
1412
  }
@@ -1941,6 +1952,25 @@ private:
1941
1952
  // lifetime contract as every other raw AST/EvalContext pointer this
1942
1953
  // class holds).
1943
1954
  const EvalContext* rootCtx_ = nullptr;
1955
+
1956
+ // File-level globals are evaluated ONCE per run. The main file's (and
1957
+ // every include's) already are, by the top-level statement pass, into
1958
+ // rootCtx_'s own let_ level. A used file's are not -- `use` injects only
1959
+ // its declarations -- so the first read of any global from that file
1960
+ // evaluates ALL of its assignments, in source order, into a context of
1961
+ // its own, kept here for the rest of the run. Before this, a function
1962
+ // reading a global re-evaluated the assignment's expression on every
1963
+ // read (evalIdentifier's scope fallback): `r = rands(0,1,1); function
1964
+ // g() = r;` gave `g() != g()`, and a 20K-element global list read 2000
1965
+ // times from a function cost 9.7s. OpenSCAD itself re-evaluates a used
1966
+ // file's globals per CALL, which is a long-standing upstream bug, not
1967
+ // behaviour to match. Returns nullopt for a file this evaluator was
1968
+ // never told about (setUsedFileGlobals), so such a caller keeps the
1969
+ // old fallback; nullptr for a known file whose global is not (yet)
1970
+ // assigned -- a forward read, undef in OpenSCAD too.
1971
+ std::optional<const Value*> fileGlobal(const oscad::Scope& fileRoot, const std::string& name);
1972
+ std::vector<UsedFileGlobals> usedFileGlobals_;
1973
+ std::unordered_map<const oscad::Scope*, EvalContext> usedFileCtx_;
1944
1974
  // The most recent ctx seen by evalStatement() -- error()'s only way to
1945
1975
  // reach a locals snapshot for its own errorBreak hook call, since
1946
1976
  // error() itself (called from dozens of builtin resolve functions)
@@ -186,7 +186,7 @@ public:
186
186
  }
187
187
  dirty_.erase(it);
188
188
  }
189
- parent_[static_cast<size_t>(level)] = 0; // dead: an ancestry walk stops here
189
+ markDead(level);
190
190
  }
191
191
 
192
192
  // nullptr if `name` has no binding on `myLevel`'s own ancestry chain.
@@ -211,7 +211,7 @@ public:
211
211
  const std::vector<Entry>& vec = it->second;
212
212
  auto idx = static_cast<long>(vec.size()) - 1;
213
213
  int ancestor = myLevel;
214
- while (ancestor != 0 && idx >= 0) {
214
+ while (ancestor > 0 && idx >= 0) {
215
215
  const int entryLevel = vec[static_cast<size_t>(idx)].level;
216
216
  if (entryLevel > ancestor) {
217
217
  --idx;
@@ -236,6 +236,11 @@ public:
236
236
  auto it = stacks_.find(name);
237
237
  return it == stacks_.end() ? 0 : it->second.size();
238
238
  }
239
+ // Test-only: how many level slots `parent_` currently holds (index 0
240
+ // included) -- lets ScopeTrailStorageTest assert that popping levels in
241
+ // call/return order really does shrink it back, and that an escaping
242
+ // closure's still-live level stops the shrink at itself.
243
+ size_t debugLevelSlotsForTesting() const { return parent_.size(); }
239
244
 
240
245
  // Ancestry-filtered snapshot -- O(total distinct names ever bound so
241
246
  // far in the whole evaluation), not O(names visible now). Only used
@@ -272,6 +277,27 @@ private:
272
277
  std::vector<int> parent_{0};
273
278
  int nextLevel_ = 0;
274
279
 
280
+ // A popped level is marked -1 (0 is a live level with no parent -- an
281
+ // isolated root -- so the two must stay distinguishable). Then, whenever
282
+ // the level being popped is the newest one, `parent_` shrinks back past
283
+ // every dead level above the newest still-live one, and its number is
284
+ // handed out again by the next openLevel(). This keeps every invariant
285
+ // lookup() relies on: a reused number is greater than every level still
286
+ // holding an entry (all higher levels were dead, and popLevel already
287
+ // removed their entries), so a name's push-stack stays ascending, and a
288
+ // live level's number never changes -- an escaping closure that keeps a
289
+ // level alive simply stops the shrink at it. Without this, `parent_`
290
+ // grew by one int per level for the whole run and never shrank: 3 x
291
+ // 64MB for a 13M-call script, plus the copy of each doubling.
292
+ void markDead(int level) {
293
+ parent_[static_cast<size_t>(level)] = -1;
294
+ if (level == nextLevel_) {
295
+ while (nextLevel_ > 0 && parent_[static_cast<size_t>(nextLevel_)] < 0) {
296
+ parent_.pop_back();
297
+ --nextLevel_;
298
+ }
299
+ }
300
+ }
275
301
  };
276
302
 
277
303
  // Per-EvalContext handle onto a shared ScopeTrailStorage<T>: this view's
@@ -473,7 +499,7 @@ public:
473
499
  }
474
500
  dirty_.erase(it);
475
501
  }
476
- parent_[static_cast<size_t>(level)] = 0; // dead: an ancestry walk stops here
502
+ markDead(level);
477
503
  }
478
504
 
479
505
  // Same sorted-merge ancestry walk as ScopeTrailStorage::lookup -- see
@@ -485,7 +511,7 @@ public:
485
511
  const std::vector<Entry>& vec = stacks_[static_cast<size_t>(id)];
486
512
  auto idx = static_cast<long>(vec.size()) - 1;
487
513
  int ancestor = myLevel;
488
- while (ancestor != 0 && idx >= 0) {
514
+ while (ancestor > 0 && idx >= 0) {
489
515
  const int entryLevel = vec[static_cast<size_t>(idx)].level;
490
516
  if (entryLevel > ancestor) {
491
517
  --idx;
@@ -505,6 +531,7 @@ public:
505
531
  int id = intern_->idFor(name);
506
532
  return id >= 0 && static_cast<size_t>(id) < stacks_.size() ? stacks_[static_cast<size_t>(id)].size() : 0;
507
533
  }
534
+ size_t debugLevelSlotsForTesting() const { return parent_.size(); } // see ScopeTrailStorage's
508
535
 
509
536
  std::vector<std::pair<std::string, T>> items(int myLevel) const {
510
537
  std::vector<std::pair<std::string, T>> result;
@@ -530,6 +557,15 @@ private:
530
557
  std::vector<int> parent_{0}; // see ScopeTrailStorage::parent_
531
558
  int nextLevel_ = 0;
532
559
 
560
+ void markDead(int level) { // see ScopeTrailStorage::markDead
561
+ parent_[static_cast<size_t>(level)] = -1;
562
+ if (level == nextLevel_) {
563
+ while (nextLevel_ > 0 && parent_[static_cast<size_t>(nextLevel_)] < 0) {
564
+ parent_.pop_back();
565
+ --nextLevel_;
566
+ }
567
+ }
568
+ }
533
569
  };
534
570
 
535
571
  // Per-EvalContext handle onto a shared IndexedScopeTrailStorage<T> -- the
@@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build"
4
4
 
5
5
  [project]
6
6
  name = "openscad_cpp_evaluator"
7
- version = "1.15.0"
7
+ version = "1.18.0"
8
8
  description = "C++ OpenSCAD evaluator with Python bindings"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -123,8 +123,11 @@ class RootScope:
123
123
 
124
124
  class _CSGNode:
125
125
  """One resolved (and, after evaluate(), generated) node in the CSG tree --
126
- mirrors the reference's CSGNode dataclass. `bodies` is a list of already-
127
- converted ColoredBody dicts (see bodyToDict on the C++ side); `params` is
126
+ mirrors the reference's CSGNode dataclass. `bodies` is always empty since
127
+ v1.17.0: an inner node's bodies are released as soon as its parent has
128
+ consumed them, and converting every node's mesh to numpy on every
129
+ evaluate() was work nothing read (format_csg_tree() prints structure,
130
+ not geometry; the rendered bodies come back separately). `params` is
128
131
  plain Python data (numbers/strings/bools/lists/dicts/OscObject), never an
129
132
  AST pointer -- eagerly converted, no lifetime tie to the Evaluator that
130
133
  produced it. Used by format_csg_tree(); not otherwise part of the public
@@ -286,6 +286,14 @@ std::vector<ColoredBody> generateCsg(Evaluator& ev, const CSGParams& params, con
286
286
  std::vector<Part2d> res2d;
287
287
  bool have2d = false;
288
288
  size_t idx = 0;
289
+ // Whether the 3D operands could possibly disagree on colour. Only then
290
+ // is attachTriColors worth its GetMeshGL(), which evaluates the boolean
291
+ // and copies the whole mesh out -- at EVERY level of a nested union
292
+ // chain, when nothing but the top ever needed it. Operands that all
293
+ // carry the same colour and no per-triangle colours of their own can
294
+ // only produce a uniformly coloured result.
295
+ std::optional<std::optional<std::array<float, 4>>> firstColor;
296
+ bool mixedColors = false;
289
297
 
290
298
  size_t stmtIndex = 0;
291
299
  for (const Value& sizeVal : groupSizes) {
@@ -314,7 +322,7 @@ std::vector<ColoredBody> generateCsg(Evaluator& ev, const CSGParams& params, con
314
322
  allDo.insert(allDo.end(), split.displayOnly.begin(), split.displayOnly.end());
315
323
 
316
324
  std::vector<ColoredBody> bodies3d, sections2d;
317
- for (const ColoredBody& c : split.foreground) {
325
+ for (ColoredBody& c : split.foreground) {
318
326
  // A body whose own Manifold::Status() isn't NoError (e.g.
319
327
  // NonFiniteVertex, from a degenerate accumulated transform deep
320
328
  // in an unrelated ancestor's positioning math -- found via a
@@ -331,7 +339,12 @@ std::vector<ColoredBody> generateCsg(Evaluator& ev, const CSGParams& params, con
331
339
  // operand and unioning everything else is the closest match to
332
340
  // its own behavior available without new numerical-robustness
333
341
  // work on Manifold's own boolean ops.
334
- if (c.body && c.body->Status() == manifold::Manifold::Error::NoError) bodies3d.push_back(c);
342
+ if (c.body && bodyStatus(c) == manifold::Manifold::Error::NoError) bodies3d.push_back(c);
343
+ }
344
+ for (const ColoredBody& c : bodies3d) {
345
+ if (c.triColors) mixedColors = true;
346
+ if (!firstColor) firstColor = c.color;
347
+ else if (*firstColor != c.color) mixedColors = true;
335
348
  }
336
349
  for (const ColoredBody& c : split.foreground) {
337
350
  if (c.section) sections2d.push_back(c);
@@ -358,17 +371,33 @@ std::vector<ColoredBody> generateCsg(Evaluator& ev, const CSGParams& params, con
358
371
  if (!bodies3d.empty()) {
359
372
  manifold::Manifold grp = *bodies3d.front().body;
360
373
  for (size_t i = 1; i < bodies3d.size(); ++i) grp = grp + *bodies3d[i].body;
374
+ // The result stays LAZY inside Manifold: its emptiness is derived
375
+ // from the operands' flags here rather than asked, so a chain of
376
+ // unions under transforms is evaluated once, at the top, instead
377
+ // of once per level (see ColoredBody::knownEmpty).
378
+ const std::optional<bool> grpEmpty =
379
+ unionEmptyOf(bodies3d, [](const ColoredBody& b) -> const ColoredBody& { return b; });
380
+ const auto known = [](const std::optional<bool>& e, bool v) { return e && *e == v; };
361
381
  if (!res3d) {
362
382
  ColoredBody cb;
363
383
  cb.body = std::move(grp);
364
384
  cb.color = bodies3d.front().color;
385
+ cb.knownStatus = manifold::Manifold::Error::NoError; // every operand was
386
+ cb.knownEmpty = grpEmpty;
365
387
  res3d = std::move(cb);
366
388
  } else if (op == "union") {
367
389
  res3d->body = *res3d->body + grp;
390
+ if (known(res3d->knownEmpty, false) || known(grpEmpty, false)) res3d->knownEmpty = false;
391
+ else if (known(res3d->knownEmpty, true) && known(grpEmpty, true)) res3d->knownEmpty = true;
392
+ else res3d->knownEmpty.reset();
368
393
  } else if (op == "difference") {
369
394
  res3d->body = *res3d->body - grp;
395
+ if (known(res3d->knownEmpty, true)) res3d->knownEmpty = true;
396
+ else if (!known(grpEmpty, true)) res3d->knownEmpty.reset(); // subtracting nothing changes nothing
370
397
  } else if (op == "intersection") {
371
398
  res3d->body = *res3d->body ^ grp;
399
+ if (known(res3d->knownEmpty, true) || known(grpEmpty, true)) res3d->knownEmpty = true;
400
+ else res3d->knownEmpty.reset();
372
401
  }
373
402
  }
374
403
  if (!sections2d.empty()) {
@@ -394,7 +423,7 @@ std::vector<ColoredBody> generateCsg(Evaluator& ev, const CSGParams& params, con
394
423
  }
395
424
  }
396
425
 
397
- if (res3d && res3d->body) attachTriColors(ev, *res3d);
426
+ if (res3d && res3d->body && mixedColors) attachTriColors(ev, *res3d);
398
427
 
399
428
  std::vector<ColoredBody> result;
400
429
  if (res3d) result.push_back(std::move(*res3d));
@@ -65,8 +65,8 @@ namespace {
65
65
  // `union() { color("red") stroke(...); text(); }` had no red recorded for
66
66
  // the stroke's runs at all, so every run looked identical, the merge kept
67
67
  // the first child's colour, and the whole thing came out red.
68
- void recordRunColors(Evaluator& ev, const ColoredBody& b, const std::optional<std::array<float, 4>>& rgba) {
69
- if (!b.body || b.body->IsEmpty()) return;
68
+ void recordRunColors(Evaluator& ev, ColoredBody& b, const std::optional<std::array<float, 4>>& rgba) {
69
+ if (!b.body || bodyIsEmpty(b)) return;
70
70
  // A body that is still one original knows its own ID without building
71
71
  // a mesh -- the common case, and the cheap one.
72
72
  const int original = b.body->OriginalID();
@@ -68,6 +68,7 @@ ColoredBody combineBodies(const std::vector<ColoredBody>& bodies) {
68
68
  ColoredBody cb;
69
69
  cb.body = std::move(merged);
70
70
  cb.color = bodies3d.front()->color;
71
+ cb.knownEmpty = unionEmptyOf(bodies3d, [](const ColoredBody* b) -> const ColoredBody& { return *b; });
71
72
  return cb;
72
73
  }
73
74
  std::vector<const ColoredBody*> sections;
@@ -585,7 +585,7 @@ Value driveVm(Evaluator& ev, size_t floor) {
585
585
  ev.vmCallStack_.pop_back();
586
586
  std::optional<Evaluator::UserCallHandle> bracket = std::move(ev.vmCallBrackets_.back());
587
587
  ev.vmCallBrackets_.pop_back();
588
- const std::string finishedName = finished->logicalName;
588
+ const std::string& finishedName = finished->logicalName; // `finished` outlives every use below
589
589
  const bool ownsModuleSplice = finished->ownsModuleSplice;
590
590
  const std::uint64_t moduleRandsBefore = finished->moduleRandsBefore;
591
591
  const oscad::ASTNode* moduleSpliceCallNode = finished->moduleSpliceCallNode;
@@ -81,6 +81,7 @@ std::vector<ColoredBody> Evaluator::generateTreeImpl(const std::vector<CSGNode*>
81
81
  insideHull = savedInsideHull;
82
82
 
83
83
  applyDimensionRules(node);
84
+ ++generatedNodeCount;
84
85
 
85
86
  auto it = dispatch.find(node.kind);
86
87
  if (node.isBuiltin && it != dispatch.end()) {
@@ -109,6 +110,25 @@ std::vector<ColoredBody> Evaluator::generateTreeImpl(const std::vector<CSGNode*>
109
110
  manifoldCache_->put(*key, node.bodies);
110
111
  if (!measuring_) cacheProducer_[*key] = node.node;
111
112
  }
113
+ // The children's bodies have been consumed (by the dispatch above,
114
+ // or flattened into this node's own) and nothing reads them again:
115
+ // only a TOP-LEVEL node's bodies leave this function, a cache hit
116
+ // re-fills a node from the cache, and the CSG-tree facade in the
117
+ // bindings never used them. Keeping them held every intermediate
118
+ // result of the whole tree alive until the tree died -- one
119
+ // transformed copy per level per leaf. A cache, when there is one,
120
+ // already holds its own reference to whatever it wants to keep.
121
+ // The children's bodies have been consumed (by the dispatch above,
122
+ // or flattened into this node's own) and nothing reads them again:
123
+ // only a TOP-LEVEL node's bodies leave this function, a cache hit
124
+ // re-fills a node from the cache, and the CSG-tree facade in the
125
+ // bindings never used them. Keeping them held every intermediate
126
+ // result of the whole tree alive until the tree died -- one
127
+ // transformed copy per level per leaf. A cache, when there is one,
128
+ // already holds its own reference to whatever it wants to keep.
129
+ // Move-assign a fresh vector, not clear() or `= {}`: both keep the
130
+ // capacity, and 35K nodes' worth of 592-byte slots was 230MB.
131
+ for (const std::unique_ptr<CSGNode>& c : node.children) c->bodies = std::vector<ColoredBody>();
112
132
  }
113
133
  for (const ColoredBody& b : node.bodies) topLevelBodies.push_back(b);
114
134
  }
@@ -151,8 +171,8 @@ DimRule dimRuleFor(const std::string& kind) {
151
171
  return DimRule::Group;
152
172
  }
153
173
 
154
- bool isEmptyBody(const ColoredBody& b) {
155
- if (b.body) return b.body->NumTri() == 0;
174
+ bool isEmptyBody(ColoredBody& b) {
175
+ if (b.body) return bodyIsEmpty(b); // see ColoredBody::knownEmpty
156
176
  if (b.section) return b.section->IsEmpty();
157
177
  return true;
158
178
  }
@@ -216,6 +216,8 @@ std::vector<std::unique_ptr<CSGNode>> Evaluator::resolveTreeImpl(const NodeList&
216
216
  treeStack_.emplace_back();
217
217
  rootCtx_ = &ctx;
218
218
  lastCtx_ = &ctx;
219
+ usedFileCtx_.clear();
220
+ generatedNodeCount = 0;
219
221
  // Cleared here, not just guarded by inResolvePass_ below, so a host
220
222
  // that reuses one Evaluator across separate top-level resolveTree()/
221
223
  // evaluate() calls (each against a freshly re-parsed AST -- the normal
@@ -87,6 +87,12 @@ ResolvedUseScopes resolveUseScopesInto(const std::vector<const oscad::ASTNode*>&
87
87
  for (auto& sc : nested.usedFileScopes) result.usedFileScopes.push_back(std::move(sc));
88
88
  result.usedFileScopes.push_back(std::move(nested.rootScope));
89
89
  if (!libInjected.empty()) reanchor.emplace_back(std::move(libInjected), result.usedFileScopes.back().get());
90
+ for (auto& g : nested.usedFileGlobals) result.usedFileGlobals.push_back(std::move(g));
91
+ UsedFileGlobals globals{result.usedFileScopes.back().get(), {}};
92
+ for (const oscad::ASTNode* n : nested.ownNodesFiltered) {
93
+ if (n->kind() == oscad::NodeKind::Assignment) globals.assignments.push_back(n);
94
+ }
95
+ result.usedFileGlobals.push_back(std::move(globals));
90
96
  }
91
97
 
92
98
  for (const oscad::ASTNode* nodePtr : ownNodes) {
@@ -162,9 +162,44 @@ Value Evaluator::evalIdentifier(const std::string& name, const oscad::Position*
162
162
  return Value{};
163
163
  }
164
164
  if (decl->kind() == oscad::NodeKind::ParameterDeclaration) return Value{};
165
+ // A file-level global (its declaring scope has no parent) is read back
166
+ // from its file's once-evaluated values, never re-evaluated -- see
167
+ // fileGlobal. Anything else (a module body's own variable reached from
168
+ // a function declared inside it) keeps the on-demand evaluation.
169
+ const oscad::Scope* declScope = ctx.scopeOf(*decl);
170
+ if (declScope && declScope->parent() == nullptr && rootCtx_) {
171
+ if (std::optional<const Value*> v = fileGlobal(*declScope, name)) {
172
+ if (*v) return **v;
173
+ if (warnIfUndef) warn("Ignoring unknown variable '" + name + "'", position);
174
+ return Value{};
175
+ }
176
+ }
165
177
  return evalExpr(*static_cast<const oscad::Assignment*>(decl)->expr, ctx);
166
178
  }
167
179
 
180
+ std::optional<const Value*> Evaluator::fileGlobal(const oscad::Scope& fileRoot, const std::string& name) {
181
+ if (&fileRoot == rootCtx_->scope) return rootCtx_->let_->find(name);
182
+ auto it = usedFileCtx_.find(&fileRoot);
183
+ if (it == usedFileCtx_.end()) {
184
+ const UsedFileGlobals* globals = nullptr;
185
+ for (const UsedFileGlobals& g : usedFileGlobals_) {
186
+ if (g.root == &fileRoot) { globals = &g; break; }
187
+ }
188
+ if (!globals) return std::nullopt;
189
+ // Reference, not iterator: a global's initializer can call a function
190
+ // from ANOTHER used file, whose first read inserts here too, and a
191
+ // rehash invalidates iterators but never element references.
192
+ EvalContext& fctx = usedFileCtx_.emplace(&fileRoot, rootCtx_->callCtx(&fileRoot)).first->second;
193
+ // Source order, once; a read of a later global from an earlier one's
194
+ // initializer finds nothing yet and is undef, as in OpenSCAD.
195
+ for (const oscad::ASTNode* a : globals->assignments) {
196
+ evalAssignment(static_cast<const oscad::Assignment&>(*a), fctx);
197
+ }
198
+ return fctx.let_->find(name);
199
+ }
200
+ return it->second.let_->find(name);
201
+ }
202
+
168
203
  // -- List comprehensions ----------------------------------------------
169
204
 
170
205
  // Debug checkpoints below mirror the reference's _eval_list_comp AND its
@@ -251,9 +251,13 @@ EvalContext Evaluator::callCtxFor(const oscad::ASTNode& decl, EvalContext& ctx,
251
251
  const oscad::Position& declPos = decl.position();
252
252
  for (const auto& [activeDecl, count] : activeDeclRefcount_) {
253
253
  const oscad::Position* outer = &activeDecl->position();
254
- if (outer->origin != declPos.origin) continue;
254
+ // Integer span test first: it rejects almost every candidate, and the
255
+ // origin compare it used to follow is a string compare per active
256
+ // declaration per call.
255
257
  const bool sameSpan = (outer->start_offset == declPos.start_offset && outer->end_offset == declPos.end_offset);
256
- if (!sameSpan && outer->start_offset <= declPos.start_offset && declPos.end_offset <= outer->end_offset) {
258
+ if (sameSpan || outer->start_offset > declPos.start_offset || declPos.end_offset > outer->end_offset) continue;
259
+ if (outer->origin != declPos.origin) continue;
260
+ {
257
261
  if (usedChildCtx) *usedChildCtx = true;
258
262
  return ctx.childCtx(scope, std::nullopt, std::move(childrenNodes), childrenCallerCtx);
259
263
  }