openscad-cpp-evaluator 0.52.0__tar.gz → 0.54.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 (208) hide show
  1. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/CLAUDE.md +21 -0
  2. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/PKG-INFO +1 -1
  3. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/include/openscad_cpp_evaluator/bytecode.hpp +15 -0
  4. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/pyproject.toml +1 -1
  5. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/src/builtins/function_builtins.cpp +142 -28
  6. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/src/builtins/primitives_3d.cpp +8 -0
  7. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/src/bytecode_compiler.cpp +26 -27
  8. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/src/bytecode_vm.cpp +9 -1
  9. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/src/stmt_eval.cpp +13 -13
  10. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/tests/test_bytecode_compiler.cpp +11 -5
  11. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/tests/test_control_flow.cpp +131 -0
  12. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/tests/test_function_builtins.cpp +68 -0
  13. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/tests/test_primitives.cpp +26 -0
  14. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/.github/workflows/ci.yml +0 -0
  15. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/.github/workflows/wheels.yml +0 -0
  16. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/.gitignore +0 -0
  17. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/.gitmodules +0 -0
  18. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/CMakeLists.txt +0 -0
  19. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/LICENSE +0 -0
  20. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/README.md +0 -0
  21. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/bindings/CMakeLists.txt +0 -0
  22. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/bindings/ast_to_py.cpp +0 -0
  23. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/bindings/module.cpp +0 -0
  24. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/cmake/embed_font.cmake +0 -0
  25. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/examples/CMakeLists.txt +0 -0
  26. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/examples/eval_perf_benchmark.cpp +0 -0
  27. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/examples/manifold_cache_reuse.cpp +0 -0
  28. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/examples/minimal_debugger.cpp +0 -0
  29. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/.github/workflows/ci.yml +0 -0
  30. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/.gitignore +0 -0
  31. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/CLAUDE.md +0 -0
  32. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/CMakeLists.txt +0 -0
  33. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/README.md +0 -0
  34. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/api.hpp +0 -0
  35. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/ast_node.hpp +0 -0
  36. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/comments.hpp +0 -0
  37. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/declarations.hpp +0 -0
  38. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/expression.hpp +0 -0
  39. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/module_instantiation.hpp +0 -0
  40. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/scope_builder.hpp +0 -0
  41. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/vector_element.hpp +0 -0
  42. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast.hpp +0 -0
  43. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/comments.hpp +0 -0
  44. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/position.hpp +0 -0
  45. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/pretty_print.hpp +0 -0
  46. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/scope.hpp +0 -0
  47. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/serialization.hpp +0 -0
  48. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/source_map.hpp +0 -0
  49. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/src/CMakeLists.txt +0 -0
  50. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/src/api.cpp +0 -0
  51. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/src/ast/ast_node.cpp +0 -0
  52. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/src/ast/declarations.cpp +0 -0
  53. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/src/ast/expression.cpp +0 -0
  54. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/src/ast/format_utils.hpp +0 -0
  55. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/src/ast/module_instantiation.cpp +0 -0
  56. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/src/ast/scope_builder.cpp +0 -0
  57. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/src/ast/vector_element.cpp +0 -0
  58. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/src/comment_attach_internal.hpp +0 -0
  59. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/src/comments.cpp +0 -0
  60. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/src/grammar/driver.cpp +0 -0
  61. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/src/grammar/driver.hpp +0 -0
  62. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/src/grammar/lexer.l +0 -0
  63. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/src/grammar/lexer_api.hpp +0 -0
  64. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/src/grammar/oscad_location.hpp +0 -0
  65. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/src/grammar/parser.y +0 -0
  66. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/src/inline_comment_attach.cpp +0 -0
  67. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/src/position.cpp +0 -0
  68. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/src/pretty_print.cpp +0 -0
  69. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/src/scope.cpp +0 -0
  70. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/src/serialization/json_io.cpp +0 -0
  71. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/src/serialization/yaml_io.cpp +0 -0
  72. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/src/source_map.cpp +0 -0
  73. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/tests/CMakeLists.txt +0 -0
  74. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/tests/test_assignments.cpp +0 -0
  75. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/tests/test_ast_generation.cpp +0 -0
  76. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/tests/test_comments_and_files.cpp +0 -0
  77. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/tests/test_const_borrow.cpp +0 -0
  78. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/tests/test_control.cpp +0 -0
  79. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/tests/test_expressions.cpp +0 -0
  80. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/tests/test_functions.cpp +0 -0
  81. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/tests/test_helpers.hpp +0 -0
  82. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/tests/test_inline_comments.cpp +0 -0
  83. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/tests/test_lexical.cpp +0 -0
  84. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/tests/test_modules.cpp +0 -0
  85. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/tests/test_node_str.cpp +0 -0
  86. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/tests/test_pretty_print.cpp +0 -0
  87. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/tests/test_render_expression.cpp +0 -0
  88. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/tests/test_scope.cpp +0 -0
  89. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/tests/test_serialization.cpp +0 -0
  90. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/tests/test_smoke.cpp +0 -0
  91. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/tests/test_source_map.cpp +0 -0
  92. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/tests/test_use_include.cpp +0 -0
  93. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/tests/test_vectors.cpp +0 -0
  94. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/tools/cli/CMakeLists.txt +0 -0
  95. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/external/openscad_cpp_parser/tools/cli/main.cpp +0 -0
  96. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/include/openscad_cpp_evaluator/bound_args.hpp +0 -0
  97. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/include/openscad_cpp_evaluator/bytecode_compiler.hpp +0 -0
  98. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/include/openscad_cpp_evaluator/bytecode_vm.hpp +0 -0
  99. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/include/openscad_cpp_evaluator/call_args.hpp +0 -0
  100. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/include/openscad_cpp_evaluator/colored_body.hpp +0 -0
  101. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/include/openscad_cpp_evaluator/csg_node.hpp +0 -0
  102. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/include/openscad_cpp_evaluator/css_colors.hpp +0 -0
  103. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/include/openscad_cpp_evaluator/debug_hooks.hpp +0 -0
  104. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/include/openscad_cpp_evaluator/debug_repl.hpp +0 -0
  105. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/include/openscad_cpp_evaluator/dispatch.hpp +0 -0
  106. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/include/openscad_cpp_evaluator/dxf_svg_import.hpp +0 -0
  107. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/include/openscad_cpp_evaluator/eval_context.hpp +0 -0
  108. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/include/openscad_cpp_evaluator/eval_error.hpp +0 -0
  109. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/include/openscad_cpp_evaluator/eval_use.hpp +0 -0
  110. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/include/openscad_cpp_evaluator/evaluator.hpp +0 -0
  111. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/include/openscad_cpp_evaluator/export.hpp +0 -0
  112. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/include/openscad_cpp_evaluator/font_match.hpp +0 -0
  113. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/include/openscad_cpp_evaluator/font_provider.hpp +0 -0
  114. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/include/openscad_cpp_evaluator/freetype_font_provider.hpp +0 -0
  115. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/include/openscad_cpp_evaluator/function_builtins.hpp +0 -0
  116. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/include/openscad_cpp_evaluator/import_builtin.hpp +0 -0
  117. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/include/openscad_cpp_evaluator/manifold_cache.hpp +0 -0
  118. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/include/openscad_cpp_evaluator/mesh_check.hpp +0 -0
  119. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/include/openscad_cpp_evaluator/mesh_import.hpp +0 -0
  120. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/include/openscad_cpp_evaluator/osc_range.hpp +0 -0
  121. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/include/openscad_cpp_evaluator/profile.hpp +0 -0
  122. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/include/openscad_cpp_evaluator/scope_trail.hpp +0 -0
  123. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/include/openscad_cpp_evaluator/segments.hpp +0 -0
  124. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/include/openscad_cpp_evaluator/surface_load.hpp +0 -0
  125. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/include/openscad_cpp_evaluator/text_metrics.hpp +0 -0
  126. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/include/openscad_cpp_evaluator/value.hpp +0 -0
  127. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/include/openscad_cpp_evaluator/zip_stored.hpp +0 -0
  128. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/python/openscad_cpp_evaluator/__init__.py +0 -0
  129. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/resources/fonts/LICENSE +0 -0
  130. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/resources/fonts/LiberationSans-Regular.ttf +0 -0
  131. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/src/CMakeLists.txt +0 -0
  132. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/src/builtins/booleans.cpp +0 -0
  133. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/src/builtins/builtins.hpp +0 -0
  134. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/src/builtins/call_args.cpp +0 -0
  135. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/src/builtins/color.cpp +0 -0
  136. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/src/builtins/control.cpp +0 -0
  137. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/src/builtins/dxf_dim.cpp +0 -0
  138. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/src/builtins/extrude.cpp +0 -0
  139. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/src/builtins/import.cpp +0 -0
  140. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/src/builtins/modifiers.cpp +0 -0
  141. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/src/builtins/primitives_2d.cpp +0 -0
  142. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/src/builtins/registry.cpp +0 -0
  143. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/src/builtins/roof.cpp +0 -0
  144. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/src/builtins/segments.cpp +0 -0
  145. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/src/builtins/surface.cpp +0 -0
  146. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/src/builtins/text.cpp +0 -0
  147. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/src/builtins/topology.cpp +0 -0
  148. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/src/builtins/transforms.cpp +0 -0
  149. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/src/colored_body.cpp +0 -0
  150. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/src/csg_generate.cpp +0 -0
  151. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/src/csg_resolve.cpp +0 -0
  152. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/src/css_colors.cpp +0 -0
  153. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/src/debug/debug_repl.cpp +0 -0
  154. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/src/debug_profile.cpp +0 -0
  155. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/src/eval_context.cpp +0 -0
  156. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/src/eval_error.cpp +0 -0
  157. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/src/eval_use.cpp +0 -0
  158. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/src/export.cpp +0 -0
  159. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/src/expr_eval.cpp +0 -0
  160. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/src/import/dxf_import.cpp +0 -0
  161. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/src/import/mesh_import.cpp +0 -0
  162. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/src/import/surface_load.cpp +0 -0
  163. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/src/import/svg_import.cpp +0 -0
  164. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/src/manifold_cache.cpp +0 -0
  165. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/src/measure_geometry.cpp +0 -0
  166. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/src/mesh_check.cpp +0 -0
  167. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/src/text/font_match.cpp +0 -0
  168. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/src/text/freetype_font_provider.cpp +0 -0
  169. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/src/text/text_metrics.cpp +0 -0
  170. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/src/user_calls.cpp +0 -0
  171. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/src/value.cpp +0 -0
  172. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/src/zip_stored.cpp +0 -0
  173. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/tests/CMakeLists.txt +0 -0
  174. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/tests/test_booleans.cpp +0 -0
  175. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/tests/test_children_separate.cpp +0 -0
  176. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/tests/test_cli.cpp +0 -0
  177. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/tests/test_csg_tree.cpp +0 -0
  178. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/tests/test_debug_hooks.cpp +0 -0
  179. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/tests/test_dxf_svg_import.cpp +0 -0
  180. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/tests/test_eval_context.cpp +0 -0
  181. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/tests/test_export.cpp +0 -0
  182. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/tests/test_expr_eval.cpp +0 -0
  183. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/tests/test_extrude_roof.cpp +0 -0
  184. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/tests/test_feature_detection.cpp +0 -0
  185. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/tests/test_helpers.hpp +0 -0
  186. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/tests/test_import_export.cpp +0 -0
  187. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/tests/test_manifold_cache.cpp +0 -0
  188. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/tests/test_mesh_check.cpp +0 -0
  189. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/tests/test_multi_color_merge.cpp +0 -0
  190. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/tests/test_profiling.cpp +0 -0
  191. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/tests/test_python_bindings.py +0 -0
  192. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/tests/test_render_expr.cpp +0 -0
  193. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/tests/test_scope_trail.cpp +0 -0
  194. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/tests/test_scoping.cpp +0 -0
  195. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/tests/test_surface.cpp +0 -0
  196. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/tests/test_tail_calls.cpp +0 -0
  197. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/tests/test_text.cpp +0 -0
  198. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/tests/test_transforms.cpp +0 -0
  199. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/tests/test_unexpected_args.cpp +0 -0
  200. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/tests/test_use.cpp +0 -0
  201. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/tests/test_value.cpp +0 -0
  202. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/tests/test_viewport_params.cpp +0 -0
  203. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/tests/test_zip_stored.cpp +0 -0
  204. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/tools/cli/CMakeLists.txt +0 -0
  205. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/tools/cli/cli_lib.cpp +0 -0
  206. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/tools/cli/cli_lib.hpp +0 -0
  207. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/tools/cli/main.cpp +0 -0
  208. {openscad_cpp_evaluator-0.52.0 → openscad_cpp_evaluator-0.54.0}/uv.lock +0 -0
@@ -577,6 +577,17 @@ grep for `ponytail:`.
577
577
  profiled is BOSL2's **argument type-checking**: 25–45%, millions of
578
578
  `is_finite`/`is_num`/`is_nan` calls.
579
579
 
580
+ - **Degree trigonometry is a direct port of OpenSCAD's `src/utils/degree_trig.cc`**
581
+ (`sinDegrees`/`cosDegrees`/`tanDegrees` and the inverses, `builtins/function_builtins.cpp`), not
582
+ `std::sin(x * pi / 180)`. The structure is load-bearing: exact values at 30/45/60, the >45 range
583
+ computed through the COMPLEMENT function, and OpenSCAD's own literal `M_DEG2RAD` constant rather
584
+ than `pi / 180.0` (which rounds differently). That is what makes `sin(45) == cos(45)` and
585
+ `cos(30) == sqrt(3)/2` hold EXACTLY, and the exactness matters to real libraries: BOSL2's
586
+ `rect(rounding=...)` filters corner intersections with an exact `isect != seg[0]`, so one ULP of
587
+ drift made a shared vertex compare unequal, kept two corner points instead of one, and killed
588
+ `rect()` on its own `assert(len(cornerpt)==1, "Cannot find corner point to anchor")`. The
589
+ inverses also snap to a whole number of degrees when the forward function reproduces the input
590
+ exactly, so `asin(sin(30))` is `30`. Pinned by `DegreeTrig.*` in `tests/test_function_builtins.cpp`.
580
591
  - `include/openscad_cpp_evaluator/value.hpp`, `src/value.cpp` — the `Value` variant (OpenSCAD's
581
592
  dynamic value type) and its free-function arithmetic/comparison helpers. See the plan's §1 for
582
593
  the type-mapping table and why there's no numpy-equivalent library.
@@ -644,6 +655,16 @@ grep for `ponytail:`.
644
655
  accordingly; read its doc comment before touching it, it's the second-trickiest mechanism in this
645
656
  codebase after the CSG tree stack), `evalUserModule`/`evalUserFunction`/`evalFunctionLiteral`,
646
657
  and `builtinChildren` (children()/children(N), deferred evaluation).
658
+ **A function literal declared in a MODULE body captures that body's bindings**, its own
659
+ parameters included, and keeps them when it escapes (passed to another module and called there).
660
+ The interpreter gets this for free — it captures `ctx.let_` wholesale. The VM snapshots only the
661
+ names it can resolve, and inside a module body it can resolve NONE: module parameters are bound
662
+ natively into `ctx.let_` rather than slot-addressed, and a module chunk compiles with no
663
+ enclosing level. So such a literal used to be frozen as a captureless constant whose free names
664
+ resolved against whatever ctx CALLED it — silently `undef`, which is how BOSL2's
665
+ `attachable(override=function(anchor) ...)` broke. `ClosureSite::captureDefiningLet` (set
666
+ whenever the literal's body still contains `Op::LoadFree`) makes `Op::MakeClosure` root the
667
+ captured trail at the DEFINING `ctx.let_`. Pinned by `EscapingClosure.*` under both engines.
647
668
  **`children(..., separate=true)`** is a BelfrySCAD extension handled *before* any of that, by
648
669
  `expandChildStatements`: a block is read with each separating forward replaced by one synthetic
649
670
  `children(k)` statement per child it selects (`makeChildrenIndexCall`, owned by
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openscad_cpp_evaluator
3
- Version: 0.52.0
3
+ Version: 0.54.0
4
4
  Summary: C++ OpenSCAD evaluator with Python bindings
5
5
  Author: NeuSCAD Contributors
6
6
  License-Expression: MIT
@@ -845,6 +845,21 @@ struct CompiledChunk {
845
845
  struct ClosureSite {
846
846
  const oscad::FunctionLiteral* node = nullptr;
847
847
  std::vector<UpvalueRef> captures;
848
+ // True when the literal's body still contains Op::LoadFree -- a name
849
+ // the compiler could not resolve to a slot or an upvalue. Such a
850
+ // closure is NOT self-contained: LoadFree resolves against whatever
851
+ // ctx is current when the closure is CALLED, so if it escapes the
852
+ // scope that defined it, those names silently resolve to the
853
+ // caller's bindings (or to undef).
854
+ //
855
+ // That is exactly what happens to a function literal declared in a
856
+ // MODULE body: module parameters are bound natively into ctx.let_
857
+ // rather than slot-addressed, and a module chunk is compiled with no
858
+ // enclosing level, so the compiler can resolve none of them. With
859
+ // this set, Op::MakeClosure roots the captured trail at the DEFINING
860
+ // ctx.let_ -- which is what the interpreter has always done
861
+ // unconditionally (see the FunctionLiteral case in expr_eval.cpp).
862
+ bool captureDefiningLet = false;
848
863
  };
849
864
 
850
865
  // One echo() expression form's own argument names, in source order --
@@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build"
4
4
 
5
5
  [project]
6
6
  name = "openscad_cpp_evaluator"
7
- version = "0.52.0"
7
+ version = "0.54.0"
8
8
  description = "C++ OpenSCAD evaluator with Python bindings"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -55,28 +55,141 @@ Value numList(const std::vector<double>& xs) {
55
55
  return listOf(std::move(items));
56
56
  }
57
57
 
58
- double degrees(double rad) { return rad * 180.0 / std::numbers::pi; }
59
- double radians(double deg) { return deg * std::numbers::pi / 180.0; }
60
-
61
- // At exact multiples of 90 degrees, sin/cos/tan use exact table values
62
- // instead of sin/cos/tan(radians(x)), which accumulate floating-point
63
- // noise (cos(90) -> 6.12e-17 etc) -- matches real OpenSCAD's degree-based
64
- // trig. Mirrors _deg_trig.
65
- constexpr std::array<double, 4> kSin90 = {0.0, 1.0, 0.0, -1.0};
66
- constexpr std::array<double, 4> kCos90 = {1.0, 0.0, -1.0, 0.0};
67
- const std::array<double, 4> kTan90 = {0.0, std::numeric_limits<double>::infinity(), 0.0,
68
- -std::numeric_limits<double>::infinity()};
69
-
70
- double degTrig(double x, const std::array<double, 4>& table, double (*fallback)(double)) {
71
- if (std::isnan(x) || std::isinf(x)) return std::numeric_limits<double>::quiet_NaN();
72
- const double n = x / 90.0;
73
- const double rn = std::round(n);
74
- if (rn == n) {
75
- int idx = static_cast<int>(rn) % 4;
76
- if (idx < 0) idx += 4;
77
- return table[static_cast<size_t>(idx)];
58
+ // Degree trigonometry, a direct port of real OpenSCAD's
59
+ // src/utils/degree_trig.cc. Not `std::sin(x * pi / 180)`: that is off by an
60
+ // ULP from OpenSCAD for most angles, and OpenSCAD's own extra structure --
61
+ // exact values at 30/45/60, and computing the >45 range through the
62
+ // COMPLEMENT function -- is what makes identities like sin(45) == cos(45)
63
+ // and cos(30) == sqrt(3)/2 hold EXACTLY there.
64
+ //
65
+ // That exactness is load-bearing for real libraries, not cosmetic. BOSL2's
66
+ // rect(rounding=...) builds a corner arc and then filters intersections
67
+ // with `isect != seg[0]`, an exact float comparison. One ULP of drift makes
68
+ // the shared vertex of two adjacent segments compare unequal, the filter
69
+ // keeps two corner points instead of one, and rect() dies on its own
70
+ // `assert(len(cornerpt)==1, "Cannot find corner point to anchor")`.
71
+ //
72
+ // The constants are OpenSCAD's own literals, deliberately -- M_DEG2RAD as a
73
+ // single pre-rounded double is not bitwise identical to `pi / 180.0`.
74
+ constexpr double kSqrt3 = 1.73205080756887719318; // sqrt(3)
75
+ constexpr double kSqrt3_4 = 0.86602540378443859659; // sqrt(3/4) == sqrt(3)/2
76
+ constexpr double kSqrt1_3 = 0.57735026918962573106; // sqrt(1/3) == sqrt(3)/3
77
+ constexpr double kSqrt1_2 = 0.70710678118654752440; // sqrt(1/2)
78
+ constexpr double kRad2Deg = 57.2957795130823208767; // 180/PI
79
+ constexpr double kDeg2Rad = 0.017453292519943295769; // PI/180
80
+
81
+ // Assumes a 26+26=52-bit mantissa; beyond it, reduction loses all accuracy
82
+ // and OpenSCAD returns NaN rather than a meaningless answer.
83
+ constexpr double kTrigHugeVal = (1L << 26) * 360.0 * (1L << 26);
84
+
85
+ double degrees(double rad) { return rad * kRad2Deg; }
86
+ double radians(double deg) { return deg * kDeg2Rad; }
87
+
88
+ double sinDegrees(double x) {
89
+ // Positive tests, so Inf/NaN fall through to the domain check.
90
+ if (x < 360.0 && x >= 0.0) {
91
+ // already reduced
92
+ } else if (x < kTrigHugeVal && x > -kTrigHugeVal) {
93
+ x -= 360.0 * std::floor(x / 360.0);
94
+ } else {
95
+ return std::numeric_limits<double>::quiet_NaN();
96
+ }
97
+ bool oppose = x >= 180.0;
98
+ if (oppose) x -= 180.0;
99
+ if (x > 90.0) x = 180.0 - x;
100
+ if (x < 45.0) {
101
+ x = (x == 30.0) ? 0.5 : std::sin(radians(x));
102
+ } else if (x == 45.0) {
103
+ x = kSqrt1_2;
104
+ } else if (x == 60.0) {
105
+ x = kSqrt3_4;
106
+ } else { // Inf/NaN would fall here
107
+ x = std::cos(radians(90.0 - x));
108
+ }
109
+ return oppose ? -x : x;
110
+ }
111
+
112
+ double cosDegrees(double x) {
113
+ if (x < 360.0 && x >= 0.0) {
114
+ // already reduced
115
+ } else if (x < kTrigHugeVal && x > -kTrigHugeVal) {
116
+ x -= 360.0 * std::floor(x / 360.0);
117
+ } else {
118
+ return std::numeric_limits<double>::quiet_NaN();
119
+ }
120
+ bool oppose = x >= 180.0;
121
+ if (oppose) x -= 180.0;
122
+ if (x > 90.0) {
123
+ x = 180.0 - x;
124
+ oppose = !oppose;
125
+ }
126
+ if (x > 45.0) {
127
+ x = (x == 60.0) ? 0.5 : std::sin(radians(90.0 - x));
128
+ } else if (x == 45.0) {
129
+ x = kSqrt1_2;
130
+ } else if (x == 30.0) {
131
+ x = kSqrt3_4;
132
+ } else { // Inf/NaN would fall here
133
+ x = std::cos(radians(x));
134
+ }
135
+ return oppose ? -x : x;
136
+ }
137
+
138
+ double tanDegrees(double x) {
139
+ const double cycles = std::floor(x / 180.0);
140
+ if (x < 180.0 && x >= 0.0) {
141
+ // already reduced
142
+ } else if (x < kTrigHugeVal && x > -kTrigHugeVal) {
143
+ x -= 180.0 * cycles;
144
+ } else {
145
+ return std::numeric_limits<double>::quiet_NaN();
146
+ }
147
+ const bool evenCycle = std::fmod(cycles, 2.0) == 0.0;
148
+ bool oppose = x > 90.0;
149
+ if (oppose) x = 180.0 - x;
150
+ if (x == 0.0) {
151
+ x = evenCycle ? 0.0 : -0.0;
152
+ } else if (x == 30.0) {
153
+ x = kSqrt1_3;
154
+ } else if (x == 45.0) {
155
+ x = 1.0;
156
+ } else if (x == 60.0) {
157
+ x = kSqrt3;
158
+ } else if (x == 90.0) {
159
+ x = evenCycle ? std::numeric_limits<double>::infinity()
160
+ : -std::numeric_limits<double>::infinity();
161
+ } else {
162
+ x = std::tan(radians(x));
78
163
  }
79
- return fallback(radians(x));
164
+ return oppose ? -x : x;
165
+ }
166
+
167
+ // The inverses snap to a whole number of degrees whenever the forward
168
+ // function reproduces the input exactly, so asin(sin(30)) is 30 and not
169
+ // 29.999999999999996.
170
+ double asinDegrees(double x) {
171
+ const double degs = degrees(std::asin(x));
172
+ const double whole = std::round(degs);
173
+ return sinDegrees(whole) == x ? whole : degs;
174
+ }
175
+
176
+ double acosDegrees(double x) {
177
+ const double degs = degrees(std::acos(x));
178
+ const double whole = std::round(degs);
179
+ return cosDegrees(whole) == x ? whole : degs;
180
+ }
181
+
182
+ double atanDegrees(double x) {
183
+ const double degs = degrees(std::atan(x));
184
+ const double whole = std::round(degs);
185
+ return tanDegrees(whole) == x ? whole : degs;
186
+ }
187
+
188
+ // atan2 snaps on a tolerance rather than a round-trip: OpenSCAD's own rule.
189
+ double atan2Degrees(double y, double x) {
190
+ const double degs = degrees(std::atan2(y, x));
191
+ const double whole = std::round(degs);
192
+ return std::fabs(degs - whole) < 3.0E-14 ? whole : degs;
80
193
  }
81
194
 
82
195
  // UTF-8 encode/decode for chr()/ord() -- OpenSCAD strings are unicode text,
@@ -1183,20 +1296,21 @@ Value evalBuiltinFunction(Evaluator& ev, const std::string& name, const CallArgs
1183
1296
  return Value{x < 0 ? std::numeric_limits<double>::quiet_NaN() : std::log10(x)};
1184
1297
  }
1185
1298
  case BuiltinFnId::Exp: return Value{std::exp(toDoubleLenient(getArg(args, 0, "x", Value{})))};
1186
- case BuiltinFnId::Sin: return Value{degTrig(toDoubleLenient(getArg(args, 0, "x", Value{})), kSin90, &std::sin)};
1187
- case BuiltinFnId::Cos: return Value{degTrig(toDoubleLenient(getArg(args, 0, "x", Value{})), kCos90, &std::cos)};
1188
- case BuiltinFnId::Tan: return Value{degTrig(toDoubleLenient(getArg(args, 0, "x", Value{})), kTan90, &std::tan)};
1299
+ case BuiltinFnId::Sin: return Value{sinDegrees(toDoubleLenient(getArg(args, 0, "x", Value{})))};
1300
+ case BuiltinFnId::Cos: return Value{cosDegrees(toDoubleLenient(getArg(args, 0, "x", Value{})))};
1301
+ case BuiltinFnId::Tan: return Value{tanDegrees(toDoubleLenient(getArg(args, 0, "x", Value{})))};
1189
1302
  case BuiltinFnId::Asin: {
1190
1303
  const double x = toDoubleLenient(getArg(args, 0, "x", Value{}));
1191
- return Value{std::fabs(x) > 1 ? std::numeric_limits<double>::quiet_NaN() : degrees(std::asin(x))};
1304
+ return Value{std::fabs(x) > 1 ? std::numeric_limits<double>::quiet_NaN() : asinDegrees(x)};
1192
1305
  }
1193
1306
  case BuiltinFnId::Acos: {
1194
1307
  const double x = toDoubleLenient(getArg(args, 0, "x", Value{}));
1195
- return Value{std::fabs(x) > 1 ? std::numeric_limits<double>::quiet_NaN() : degrees(std::acos(x))};
1308
+ return Value{std::fabs(x) > 1 ? std::numeric_limits<double>::quiet_NaN() : acosDegrees(x)};
1196
1309
  }
1197
- case BuiltinFnId::Atan: return Value{degrees(std::atan(toDoubleLenient(getArg(args, 0, "x", Value{}))))};
1310
+ case BuiltinFnId::Atan: return Value{atanDegrees(toDoubleLenient(getArg(args, 0, "x", Value{})))};
1198
1311
  case BuiltinFnId::Atan2:
1199
- return Value{degrees(std::atan2(toDoubleLenient(getArg(args, 0, "y", Value{})), toDoubleLenient(getArg(args, 1, "x", Value{}))))};
1312
+ return Value{atan2Degrees(toDoubleLenient(getArg(args, 0, "y", Value{})),
1313
+ toDoubleLenient(getArg(args, 1, "x", Value{})))};
1200
1314
  case BuiltinFnId::Max: return builtinMinMax(args, true);
1201
1315
  case BuiltinFnId::Min: return builtinMinMax(args, false);
1202
1316
  case BuiltinFnId::Pow: return builtinPow(toDoubleLenient(getArg(args, 0, "x", Value{})), toDoubleLenient(getArg(args, 1, "y", Value{})));
@@ -781,6 +781,14 @@ std::vector<ColoredBody> generatePolyhedron(Evaluator& ev, const CSGParams& para
781
781
  for (const Value& t : std::get<ListPtr>(params.at("tris"))->items) {
782
782
  mesh.triVerts.push_back(static_cast<uint64_t>(std::get<double>(t)));
783
783
  }
784
+ // No faces at all is empty geometry, not a failure. BOSL2's debug_vnf()
785
+ // passes [verts, []] to draw vertex labels with no surface, and real
786
+ // OpenSCAD accepts `polyhedron(points=..., faces=[])` silently. Checked
787
+ // before the status branches below, which would otherwise report the
788
+ // empty mesh as NotManifold and discard it with a warning.
789
+ if (mesh.triVerts.empty()) {
790
+ return {ev.tagGenerated(manifold::Manifold(), node, params.at("color"))};
791
+ }
784
792
  manifold::Manifold body(mesh);
785
793
  if (isDrawableFailure(body, mesh)) {
786
794
  // An open surface -- faces that don't close the solid. Manifold
@@ -447,10 +447,12 @@ public:
447
447
  // `ctx.let_`, which callCtxFor roots at this exact closure's
448
448
  // own capturedLet snapshot whenever it's invoked -- the same
449
449
  // snapshot `effectiveCaptures` describes below.
450
- if (effectiveCaptures.empty() || !containsLoadFree(literalChunk)) {
450
+ // Must be read BEFORE literalChunk is moved from below.
451
+ const bool needsDefiningLet = containsLoadFree(literalChunk);
452
+ if (effectiveCaptures.empty() || !needsDefiningLet) {
451
453
  chunk_.nestedLiterals.emplace_back(&n, std::move(literalChunk));
452
454
  }
453
- if (effectiveCaptures.empty()) {
455
+ if (effectiveCaptures.empty() && !needsDefiningLet) {
454
456
  // Closes over nothing at all, even transitively -- a
455
457
  // single frozen constant works for every invocation, no
456
458
  // per-call snapshot needed.
@@ -461,7 +463,8 @@ public:
461
463
  }
462
464
  // Escaping-closure support (Op::MakeClosure, bytecode.hpp).
463
465
  int siteIdx = static_cast<int>(chunk_.closureSites.size());
464
- chunk_.closureSites.push_back(CompiledChunk::ClosureSite{&n, std::move(effectiveCaptures)});
466
+ chunk_.closureSites.push_back(
467
+ CompiledChunk::ClosureSite{&n, std::move(effectiveCaptures), needsDefiningLet});
465
468
  out.push_back({Op::MakeClosure, siteIdx, 0, nullptr});
466
469
  return;
467
470
  }
@@ -1561,35 +1564,31 @@ public:
1561
1564
  return;
1562
1565
  }
1563
1566
  case NodeKind::ModularLet: {
1564
- // Mirrors Evaluator::evalLetBlock exactly (stmt_eval.cpp):
1565
- // every assignment's RHS is evaluated against the
1566
- // ORIGINAL (parent) ctx, never an earlier sibling's own
1567
- // write in THIS SAME let-block (a documented, deliberate
1568
- // divergence from the let-EXPRESSION form's sequential
1569
- // visibility) -- so every RHS is compiled+evaluated FIRST,
1570
- // all left on f.stack, and only written into the freshly-
1571
- // opened child scope (Op::OpenLetScope) AFTER every one of
1572
- // them has already run against the still-current PARENT
1573
- // ctx. No outer statement-level check for the let-block
1574
- // node itself (matches evalChildren's own ModularLet
1575
- // exclusion) -- each assignment gets its own instead,
1576
- // exactly like evalLetBlock's own per-assignment
1577
- // checkDebug. The body (n.children) compiles inline,
1578
- // recursively, against the now-current child ctx -- same
1579
- // Compiler instance, so nested statements get every bit of
1580
- // this same real-bytecode treatment too.
1567
+ // Mirrors Evaluator::evalLetBlock (stmt_eval.cpp): the
1568
+ // child scope opens FIRST, then each assignment's RHS is
1569
+ // compiled and stored in source order, so a later binding
1570
+ // sees an earlier one -- `let(a=1, b=a+1) ...` binds b to
1571
+ // 2, exactly as the let-EXPRESSION form does.
1572
+ //
1573
+ // This used to compile every RHS before Op::OpenLetScope
1574
+ // and then store them back-to-front, deliberately denying
1575
+ // that visibility. Real OpenSCAD grants it, and BOSL2
1576
+ // depends on it (isosurface.scad chains five bindings where
1577
+ // each uses the previous).
1578
+ //
1579
+ // No outer statement-level check for the let-block node
1580
+ // itself (matches evalChildren's own ModularLet exclusion)
1581
+ // -- each assignment gets its own, exactly like
1582
+ // evalLetBlock's own per-assignment checkDebug. The body
1583
+ // (n.children) compiles inline, recursively, against the
1584
+ // now-current child ctx.
1581
1585
  auto& n = static_cast<const oscad::ModularLet&>(stmt);
1582
1586
  CompileScope exprScope;
1587
+ out.push_back({Op::OpenLetScope, 0, 0, nullptr});
1583
1588
  for (const auto& assign : n.assignments) {
1584
1589
  out.push_back({Op::CheckDebugStatement, internNativeStatement(assign.get()), 0, nullptr});
1585
1590
  compileIsolatedExpr(*assign->expr, out, exprScope);
1586
- }
1587
- out.push_back({Op::OpenLetScope, 0, 0, nullptr});
1588
- // Values pop in REVERSE push order (LIFO) -- iterate the
1589
- // assignment list backwards so each Op::StoreLetVar is
1590
- // paired with the value that was actually pushed for IT.
1591
- for (auto it = n.assignments.rbegin(); it != n.assignments.rend(); ++it) {
1592
- out.push_back({Op::StoreLetVar, internName((*it)->name->name), 0, &(*it)->position()});
1591
+ out.push_back({Op::StoreLetVar, internName(assign->name->name), 0, &assign->position()});
1593
1592
  }
1594
1593
  compileStatementList(n.children, out);
1595
1594
  out.push_back({Op::CloseExprScope, 0, 0, nullptr});
@@ -599,7 +599,15 @@ Value driveVm(Evaluator& ev, size_t floor) {
599
599
  }
600
600
  case Op::MakeClosure: {
601
601
  const CompiledChunk::ClosureSite& site = f.chunk->closureSites[static_cast<size_t>(ins.a)];
602
- auto capturedTrail = TrailView<Value>::makeRoot();
602
+ // A child of the DEFINING ctx.let_ when the body still
603
+ // has unresolved names (see ClosureSite::
604
+ // captureDefiningLet), so they resolve where the closure
605
+ // was written rather than wherever it is later called.
606
+ // openChild, not ctx.let_ itself: the explicit captures
607
+ // below are layered on top and must not leak back into
608
+ // the defining scope.
609
+ auto capturedTrail = site.captureDefiningLet ? ctx.let_->openChild(/*isolate=*/false)
610
+ : TrailView<Value>::makeRoot();
603
611
  std::vector<const std::string*> selfNames;
604
612
  for (const auto& cap : site.captures) {
605
613
  if (cap.isSelfReference) {
@@ -155,20 +155,20 @@ void Evaluator::evalFor(const oscad::ModularFor& node, EvalContext& ctx) {
155
155
 
156
156
  void Evaluator::evalLetBlock(const oscad::ModularLet& node, EvalContext& ctx) {
157
157
  EvalContext childCtx = ctx.childCtx(nullptr, std::nullopt, ctx.childrenNodes, ctx.childrenCallerCtx);
158
- // ponytail/port-fidelity note: each assignment's RHS is evaluated
159
- // against the ORIGINAL `ctx`, not the growing `childCtx` -- unlike the
160
- // let() *expression* form's documented sequential visibility
161
- // (`let(a=1, b=a+1) expr` sees `a` while evaluating `b`), the
162
- // *statement* form `let(a=1, b=a+1) { ... }` does NOT: `b`'s `a+1`
163
- // looks `a` up in `ctx`, which doesn't have this let's own `a` binding
164
- // yet. This is a direct, deliberate port of the reference's
165
- // _eval_let_block, not an oversight -- it evaluates every RHS against
166
- // `ctx` in the loop, only ever writing into `child_ctx`.
158
+ // Each RHS is evaluated against the GROWING childCtx, so a later
159
+ // binding sees an earlier one -- `let(a=1, b=a+1) ...` binds b to 2.
160
+ // The statement form behaves exactly like the expression form here.
161
+ //
162
+ // This used to evaluate every RHS against the original `ctx` instead,
163
+ // described in a comment as a deliberate port of the reference's
164
+ // _eval_let_block. That reference was this project's own earlier
165
+ // implementation, which had the same bug; real OpenSCAD 2026.02.01
166
+ // echoes 2, and BOSL2 depends on it heavily -- isosurface.scad chains
167
+ // five bindings where each uses the previous, and rounding.scad's
168
+ // join_prism examples do the same.
167
169
  for (const auto& assign : node.assignments) {
168
- // Checked against the ORIGINAL ctx (like the RHS eval below), not
169
- // childCtx -- mirrors _eval_let_block's `_check_debug(assign, ctx)`.
170
- checkDebug(*assign, ctx);
171
- Value v = evalExprMaybeCompiled(*assign->expr, ctx);
170
+ checkDebug(*assign, childCtx);
171
+ Value v = evalExprMaybeCompiled(*assign->expr, childCtx);
172
172
  const std::string& name = assign->name->name;
173
173
  if (!name.empty() && name[0] == '$') {
174
174
  childCtx.dyn->set(name, v);
@@ -1889,15 +1889,21 @@ TEST(ModuleBodyCompiles, AssertStatementCompiledFormFailsWithNamedMessage) {
1889
1889
  }
1890
1890
  }
1891
1891
 
1892
- TEST(ModuleBodyCompiles, LetBlockStatementDoesNotSeeItsOwnEarlierSiblingAssignment) {
1892
+ TEST(ModuleBodyCompiles, LetBlockStatementSeesItsOwnEarlierSiblingAssignment) {
1893
1893
  ScopedVm vm(true);
1894
- // The statement form's own documented divergence from let-EXPRESSION
1895
- // sequential visibility: b's RHS must see the OUTER a (1), not this
1896
- // same let-block's own a=2 -- b should be 2, not 3.
1894
+ // b's RHS sees THIS let-block's own a=2, not the outer a=1, so b is 3.
1895
+ // The statement form has the same sequential visibility the expression
1896
+ // form does, and the binding still does not escape the block.
1897
+ //
1898
+ // This test previously asserted "ECHO: 2, 2" -- b seeing the outer a --
1899
+ // and called that a documented divergence. It was not: real OpenSCAD
1900
+ // 2026.02.01 echoes "2, 3" then "1", which is what both engines now
1901
+ // produce. BOSL2 depends on the sequential form (isosurface.scad chains
1902
+ // five bindings where each uses the previous).
1897
1903
  EXPECT_EQ(runCapturingEcho("a = 1;\n"
1898
1904
  "let (a = 2, b = a + 1) { echo(a, b); }\n"
1899
1905
  "echo(a);\n"),
1900
- "ECHO: 2, 2\nECHO: 1");
1906
+ "ECHO: 2, 3\nECHO: 1");
1901
1907
  }
1902
1908
 
1903
1909
  TEST(ModuleBodyCompiles, LetBlockStatementIsolatesDollarVarOverrideFromLaterStatements) {
@@ -348,8 +348,139 @@ TEST(OperatorBlockScope, BlockAssignmentCannotFeedBackIntoArgumentsInterpreted)
348
348
  TEST(OperatorBlockScope, UserModuleChildBlockIsItsOwnScopeCompiled) { checkUserModuleChildBlockIsItsOwnScope(true); }
349
349
  TEST(OperatorBlockScope, UserModuleChildBlockIsItsOwnScopeInterpreted) { checkUserModuleChildBlockIsItsOwnScope(false); }
350
350
 
351
+ // A function literal declared in a MODULE body captures that body's
352
+ // bindings, including the module's own parameters, and keeps them when it
353
+ // escapes -- passed to another module and called there.
354
+ //
355
+ // The interpreter always did this (it captures ctx.let_ wholesale). The VM
356
+ // tries to snapshot only the names it can resolve, and inside a module body
357
+ // it can resolve none: module parameters are bound natively into ctx.let_
358
+ // rather than slot-addressed, and a module chunk compiles with no enclosing
359
+ // level. So the literal was frozen as a captureless constant and its free
360
+ // names resolved against whatever ctx CALLED it -- silently undef.
361
+ //
362
+ // Verified against real OpenSCAD 2026.02.01: every case below echoes 20.
363
+
364
+ namespace {
365
+
366
+ void checkEscapingClosureKeepsItsDefiningScope(bool useVm, const std::string& body) {
367
+ ScopedVm vm(useVm);
368
+ std::vector<std::string> echoed;
369
+ runScript(body, [&](const std::string& msg) { echoed.push_back(msg); });
370
+ EXPECT_EQ(echoed, (std::vector<std::string>{"ECHO: r = 20"})) << "in: " << body;
371
+ }
372
+
373
+ } // namespace
374
+
375
+ TEST(EscapingClosure, CapturesModuleParameterCompiled) {
376
+ checkEscapingClosureKeepsItsDefiningScope(true,
377
+ "module callee(fn) { echo(r = fn(2)); }\n"
378
+ "module caller(s=100) { f = function (x) s / 8 * x; callee(f); }\n"
379
+ "caller(80);\n");
380
+ }
381
+ TEST(EscapingClosure, CapturesModuleParameterInterpreted) {
382
+ checkEscapingClosureKeepsItsDefiningScope(false,
383
+ "module callee(fn) { echo(r = fn(2)); }\n"
384
+ "module caller(s=100) { f = function (x) s / 8 * x; callee(f); }\n"
385
+ "caller(80);\n");
386
+ }
387
+
388
+ // BOSL2's attachable(override=...) shape: the closure travels inside a list.
389
+ TEST(EscapingClosure, SurvivesInsideAListCompiled) {
390
+ checkEscapingClosureKeepsItsDefiningScope(true,
391
+ "module callee(spec) { echo(r = spec[0](2)); }\n"
392
+ "module caller(s=100) { f = function (x) s / 8 * x; callee([f]); }\n"
393
+ "caller(80);\n");
394
+ }
395
+ TEST(EscapingClosure, SurvivesInsideAListInterpreted) {
396
+ checkEscapingClosureKeepsItsDefiningScope(false,
397
+ "module callee(spec) { echo(r = spec[0](2)); }\n"
398
+ "module caller(s=100) { f = function (x) s / 8 * x; callee([f]); }\n"
399
+ "caller(80);\n");
400
+ }
401
+
402
+ // The callee has its OWN `s`. The closure must still see the one it was
403
+ // written next to -- this is what proves it captures rather than merely
404
+ // resolving late.
405
+ TEST(EscapingClosure, IsNotCapturedByTheCallersOwnBindingCompiled) {
406
+ checkEscapingClosureKeepsItsDefiningScope(true,
407
+ "module callee(fn, s=999) { echo(r = fn(2)); }\n"
408
+ "module caller(s=100) { f = function (x) s / 8 * x; callee(f); }\n"
409
+ "caller(80);\n");
410
+ }
411
+ TEST(EscapingClosure, IsNotCapturedByTheCallersOwnBindingInterpreted) {
412
+ checkEscapingClosureKeepsItsDefiningScope(false,
413
+ "module callee(fn, s=999) { echo(r = fn(2)); }\n"
414
+ "module caller(s=100) { f = function (x) s / 8 * x; callee(f); }\n"
415
+ "caller(80);\n");
416
+ }
417
+
418
+ // A literal closing over nothing keeps the cheap frozen-constant path.
419
+ TEST(EscapingClosure, CapturelessLiteralStillWorksCompiled) {
420
+ ScopedVm vm(true);
421
+ std::vector<std::string> echoed;
422
+ runScript("module callee(fn) { echo(r = fn(4)); }\n"
423
+ "module caller() { callee(function (x) x * 5); }\n"
424
+ "caller();\n",
425
+ [&](const std::string& msg) { echoed.push_back(msg); });
426
+ EXPECT_EQ(echoed, (std::vector<std::string>{"ECHO: r = 20"}));
427
+ }
428
+
351
429
  // -- let (statement + expression forms) --------------------------------
352
430
 
431
+ // The let STATEMENT form has the same sequential visibility as the
432
+ // expression form: a later binding sees an earlier one. Verified against
433
+ // real OpenSCAD 2026.02.01, which echoes 2 for both forms below.
434
+ //
435
+ // This was wrong for a long time, described in-source as a deliberate port
436
+ // of "the reference's _eval_let_block" -- that reference being this
437
+ // project's own earlier implementation, which had the same bug. BOSL2 leans
438
+ // on it: isosurface.scad chains five bindings where each uses the previous,
439
+ // and rounding.scad's join_prism examples do too.
440
+
441
+ namespace {
442
+
443
+ void checkLetStatementIsSequential(bool useVm) {
444
+ ScopedVm vm(useVm);
445
+ std::vector<std::string> echoed;
446
+ runScript("let (a = 1, b = a + 1) echo(b = b);\n",
447
+ [&](const std::string& msg) { echoed.push_back(msg); });
448
+ EXPECT_EQ(echoed, (std::vector<std::string>{"ECHO: b = 2"}));
449
+ }
450
+
451
+ void checkLetStatementShadowsWithoutEscaping(bool useVm) {
452
+ ScopedVm vm(useVm);
453
+ std::vector<std::string> echoed;
454
+ // The inner a=2 is what b sees, and it does not survive the block.
455
+ runScript("a = 1;\n"
456
+ "let (a = 2, b = a + 1) echo(inside_a = a, inside_b = b);\n"
457
+ "echo(after_a = a);\n",
458
+ [&](const std::string& msg) { echoed.push_back(msg); });
459
+ EXPECT_EQ(echoed, (std::vector<std::string>{"ECHO: inside_a = 2, inside_b = 3",
460
+ "ECHO: after_a = 1"}));
461
+ }
462
+
463
+ void checkLetStatementChainsSeveralBindings(bool useVm) {
464
+ ScopedVm vm(useVm);
465
+ std::vector<std::string> echoed;
466
+ // The isosurface.scad shape: each binding built from the previous.
467
+ runScript("let (a = 2, b = a * 3, c = b + a, d = c * b) echo(d = d);\n",
468
+ [&](const std::string& msg) { echoed.push_back(msg); });
469
+ EXPECT_EQ(echoed, (std::vector<std::string>{"ECHO: d = 48"}));
470
+ }
471
+
472
+ } // namespace
473
+
474
+ TEST(LetStatementScope, IsSequentialCompiled) { checkLetStatementIsSequential(true); }
475
+ TEST(LetStatementScope, IsSequentialInterpreted) { checkLetStatementIsSequential(false); }
476
+
477
+ TEST(LetStatementScope, ShadowsWithoutEscapingCompiled) { checkLetStatementShadowsWithoutEscaping(true); }
478
+ TEST(LetStatementScope, ShadowsWithoutEscapingInterpreted) { checkLetStatementShadowsWithoutEscaping(false); }
479
+
480
+ TEST(LetStatementScope, ChainsSeveralBindingsCompiled) { checkLetStatementChainsSeveralBindings(true); }
481
+ TEST(LetStatementScope, ChainsSeveralBindingsInterpreted) { checkLetStatementChainsSeveralBindings(false); }
482
+
483
+
353
484
  TEST(LetStatement, BindsVisibleInsideBlockOnly) {
354
485
  Evaluated e = evalSrc("let(s=3) cube(s);");
355
486
  manifold::Box bbox = e.bodies[0].body->BoundingBox();