openscad-cpp-evaluator 0.4.6__tar.gz → 0.4.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/PKG-INFO +1 -1
  2. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/include/openscad_cpp_evaluator/eval_context.hpp +17 -0
  3. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/include/openscad_cpp_evaluator/evaluator.hpp +19 -5
  4. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/include/openscad_cpp_evaluator/value.hpp +55 -6
  5. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/pyproject.toml +1 -1
  6. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/src/builtins/function_builtins.cpp +1 -1
  7. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/src/bytecode_compiler.cpp +26 -1
  8. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/src/bytecode_vm.cpp +10 -8
  9. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/src/eval_context.cpp +25 -0
  10. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/src/expr_eval.cpp +28 -6
  11. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/src/manifold_cache.cpp +2 -2
  12. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/src/user_calls.cpp +47 -17
  13. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/src/value.cpp +18 -6
  14. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/tests/test_bytecode_compiler.cpp +34 -20
  15. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/tests/test_expr_eval.cpp +17 -0
  16. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/.github/workflows/ci.yml +0 -0
  17. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/.github/workflows/wheels.yml +0 -0
  18. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/.gitignore +0 -0
  19. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/.gitmodules +0 -0
  20. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/CLAUDE.md +0 -0
  21. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/CMakeLists.txt +0 -0
  22. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/LICENSE +0 -0
  23. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/README.md +0 -0
  24. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/bindings/CMakeLists.txt +0 -0
  25. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/bindings/module.cpp +0 -0
  26. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/cmake/embed_font.cmake +0 -0
  27. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/examples/CMakeLists.txt +0 -0
  28. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/examples/eval_perf_benchmark.cpp +0 -0
  29. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/examples/manifold_cache_reuse.cpp +0 -0
  30. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/examples/minimal_debugger.cpp +0 -0
  31. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/.github/workflows/ci.yml +0 -0
  32. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/.gitignore +0 -0
  33. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/CLAUDE.md +0 -0
  34. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/CMakeLists.txt +0 -0
  35. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/README.md +0 -0
  36. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/include/openscad_cpp_parser/api.hpp +0 -0
  37. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/ast_node.hpp +0 -0
  38. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/comments.hpp +0 -0
  39. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/declarations.hpp +0 -0
  40. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/expression.hpp +0 -0
  41. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/module_instantiation.hpp +0 -0
  42. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/scope_builder.hpp +0 -0
  43. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/vector_element.hpp +0 -0
  44. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast.hpp +0 -0
  45. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/include/openscad_cpp_parser/comments.hpp +0 -0
  46. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/include/openscad_cpp_parser/position.hpp +0 -0
  47. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/include/openscad_cpp_parser/pretty_print.hpp +0 -0
  48. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/include/openscad_cpp_parser/scope.hpp +0 -0
  49. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/include/openscad_cpp_parser/serialization.hpp +0 -0
  50. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/include/openscad_cpp_parser/source_map.hpp +0 -0
  51. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/src/CMakeLists.txt +0 -0
  52. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/src/api.cpp +0 -0
  53. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/src/ast/ast_node.cpp +0 -0
  54. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/src/ast/declarations.cpp +0 -0
  55. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/src/ast/expression.cpp +0 -0
  56. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/src/ast/format_utils.hpp +0 -0
  57. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/src/ast/module_instantiation.cpp +0 -0
  58. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/src/ast/scope_builder.cpp +0 -0
  59. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/src/ast/vector_element.cpp +0 -0
  60. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/src/comment_attach_internal.hpp +0 -0
  61. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/src/comments.cpp +0 -0
  62. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/src/grammar/driver.cpp +0 -0
  63. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/src/grammar/driver.hpp +0 -0
  64. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/src/grammar/lexer.l +0 -0
  65. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/src/grammar/lexer_api.hpp +0 -0
  66. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/src/grammar/oscad_location.hpp +0 -0
  67. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/src/grammar/parser.y +0 -0
  68. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/src/inline_comment_attach.cpp +0 -0
  69. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/src/position.cpp +0 -0
  70. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/src/pretty_print.cpp +0 -0
  71. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/src/scope.cpp +0 -0
  72. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/src/serialization/json_io.cpp +0 -0
  73. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/src/serialization/yaml_io.cpp +0 -0
  74. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/src/source_map.cpp +0 -0
  75. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/tests/CMakeLists.txt +0 -0
  76. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/tests/test_assignments.cpp +0 -0
  77. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/tests/test_ast_generation.cpp +0 -0
  78. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/tests/test_comments_and_files.cpp +0 -0
  79. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/tests/test_const_borrow.cpp +0 -0
  80. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/tests/test_control.cpp +0 -0
  81. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/tests/test_expressions.cpp +0 -0
  82. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/tests/test_functions.cpp +0 -0
  83. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/tests/test_helpers.hpp +0 -0
  84. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/tests/test_inline_comments.cpp +0 -0
  85. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/tests/test_lexical.cpp +0 -0
  86. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/tests/test_modules.cpp +0 -0
  87. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/tests/test_node_str.cpp +0 -0
  88. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/tests/test_pretty_print.cpp +0 -0
  89. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/tests/test_scope.cpp +0 -0
  90. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/tests/test_serialization.cpp +0 -0
  91. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/tests/test_smoke.cpp +0 -0
  92. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/tests/test_source_map.cpp +0 -0
  93. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/tests/test_use_include.cpp +0 -0
  94. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/tests/test_vectors.cpp +0 -0
  95. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/tools/cli/CMakeLists.txt +0 -0
  96. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/external/openscad_cpp_parser/tools/cli/main.cpp +0 -0
  97. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/include/openscad_cpp_evaluator/bound_args.hpp +0 -0
  98. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/include/openscad_cpp_evaluator/bytecode.hpp +0 -0
  99. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/include/openscad_cpp_evaluator/bytecode_compiler.hpp +0 -0
  100. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/include/openscad_cpp_evaluator/bytecode_vm.hpp +0 -0
  101. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/include/openscad_cpp_evaluator/call_args.hpp +0 -0
  102. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/include/openscad_cpp_evaluator/colored_body.hpp +0 -0
  103. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/include/openscad_cpp_evaluator/csg_node.hpp +0 -0
  104. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/include/openscad_cpp_evaluator/css_colors.hpp +0 -0
  105. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/include/openscad_cpp_evaluator/debug_hooks.hpp +0 -0
  106. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/include/openscad_cpp_evaluator/debug_repl.hpp +0 -0
  107. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/include/openscad_cpp_evaluator/dispatch.hpp +0 -0
  108. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/include/openscad_cpp_evaluator/dxf_svg_import.hpp +0 -0
  109. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/include/openscad_cpp_evaluator/eval_error.hpp +0 -0
  110. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/include/openscad_cpp_evaluator/eval_use.hpp +0 -0
  111. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/include/openscad_cpp_evaluator/export.hpp +0 -0
  112. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/include/openscad_cpp_evaluator/font_provider.hpp +0 -0
  113. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/include/openscad_cpp_evaluator/function_builtins.hpp +0 -0
  114. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/include/openscad_cpp_evaluator/import_builtin.hpp +0 -0
  115. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/include/openscad_cpp_evaluator/manifold_cache.hpp +0 -0
  116. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/include/openscad_cpp_evaluator/mesh_import.hpp +0 -0
  117. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/include/openscad_cpp_evaluator/osc_range.hpp +0 -0
  118. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/include/openscad_cpp_evaluator/profile.hpp +0 -0
  119. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/include/openscad_cpp_evaluator/scope_trail.hpp +0 -0
  120. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/include/openscad_cpp_evaluator/segments.hpp +0 -0
  121. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/include/openscad_cpp_evaluator/stb_font_provider.hpp +0 -0
  122. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/include/openscad_cpp_evaluator/surface_load.hpp +0 -0
  123. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/include/openscad_cpp_evaluator/text_metrics.hpp +0 -0
  124. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/include/openscad_cpp_evaluator/zip_stored.hpp +0 -0
  125. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/python/openscad_cpp_evaluator/__init__.py +0 -0
  126. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/resources/fonts/LICENSE +0 -0
  127. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/resources/fonts/LiberationSans-Regular.ttf +0 -0
  128. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/src/CMakeLists.txt +0 -0
  129. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/src/builtins/booleans.cpp +0 -0
  130. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/src/builtins/builtins.hpp +0 -0
  131. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/src/builtins/call_args.cpp +0 -0
  132. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/src/builtins/color.cpp +0 -0
  133. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/src/builtins/control.cpp +0 -0
  134. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/src/builtins/extrude.cpp +0 -0
  135. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/src/builtins/import.cpp +0 -0
  136. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/src/builtins/modifiers.cpp +0 -0
  137. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/src/builtins/primitives_2d.cpp +0 -0
  138. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/src/builtins/primitives_3d.cpp +0 -0
  139. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/src/builtins/registry.cpp +0 -0
  140. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/src/builtins/roof.cpp +0 -0
  141. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/src/builtins/segments.cpp +0 -0
  142. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/src/builtins/surface.cpp +0 -0
  143. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/src/builtins/text.cpp +0 -0
  144. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/src/builtins/topology.cpp +0 -0
  145. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/src/builtins/transforms.cpp +0 -0
  146. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/src/colored_body.cpp +0 -0
  147. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/src/csg_generate.cpp +0 -0
  148. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/src/csg_resolve.cpp +0 -0
  149. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/src/css_colors.cpp +0 -0
  150. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/src/debug/debug_repl.cpp +0 -0
  151. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/src/debug_profile.cpp +0 -0
  152. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/src/eval_error.cpp +0 -0
  153. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/src/eval_use.cpp +0 -0
  154. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/src/export.cpp +0 -0
  155. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/src/import/dxf_import.cpp +0 -0
  156. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/src/import/mesh_import.cpp +0 -0
  157. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/src/import/surface_load.cpp +0 -0
  158. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/src/import/svg_import.cpp +0 -0
  159. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/src/stmt_eval.cpp +0 -0
  160. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/src/text/stb_font_provider.cpp +0 -0
  161. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/src/text/text_metrics.cpp +0 -0
  162. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/src/zip_stored.cpp +0 -0
  163. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/tests/CMakeLists.txt +0 -0
  164. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/tests/test_booleans.cpp +0 -0
  165. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/tests/test_cli.cpp +0 -0
  166. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/tests/test_control_flow.cpp +0 -0
  167. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/tests/test_csg_tree.cpp +0 -0
  168. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/tests/test_debug_hooks.cpp +0 -0
  169. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/tests/test_dxf_svg_import.cpp +0 -0
  170. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/tests/test_eval_context.cpp +0 -0
  171. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/tests/test_export.cpp +0 -0
  172. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/tests/test_extrude_roof.cpp +0 -0
  173. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/tests/test_function_builtins.cpp +0 -0
  174. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/tests/test_helpers.hpp +0 -0
  175. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/tests/test_import_export.cpp +0 -0
  176. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/tests/test_manifold_cache.cpp +0 -0
  177. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/tests/test_multi_color_merge.cpp +0 -0
  178. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/tests/test_primitives.cpp +0 -0
  179. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/tests/test_profiling.cpp +0 -0
  180. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/tests/test_python_bindings.py +0 -0
  181. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/tests/test_scope_trail.cpp +0 -0
  182. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/tests/test_scoping.cpp +0 -0
  183. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/tests/test_surface.cpp +0 -0
  184. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/tests/test_tail_calls.cpp +0 -0
  185. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/tests/test_text.cpp +0 -0
  186. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/tests/test_transforms.cpp +0 -0
  187. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/tests/test_use.cpp +0 -0
  188. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/tests/test_value.cpp +0 -0
  189. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/tests/test_viewport_params.cpp +0 -0
  190. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/tests/test_zip_stored.cpp +0 -0
  191. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/tools/cli/CMakeLists.txt +0 -0
  192. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/tools/cli/cli_lib.cpp +0 -0
  193. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/tools/cli/cli_lib.hpp +0 -0
  194. {openscad_cpp_evaluator-0.4.6 → openscad_cpp_evaluator-0.4.8}/tools/cli/main.cpp +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openscad_cpp_evaluator
3
- Version: 0.4.6
3
+ Version: 0.4.8
4
4
  Summary: C++ OpenSCAD evaluator with Python bindings
5
5
  Author: NeuSCAD Contributors
6
6
  License-Expression: MIT
@@ -105,6 +105,23 @@ struct EvalContext {
105
105
  std::shared_ptr<const ChildrenNodeList> newChildrenNodes = nullptr,
106
106
  const EvalContext* newChildrenCallerCtx = nullptr) const;
107
107
 
108
+ // Identical to callCtx() except `let_`'s new isolated level is opened
109
+ // from `capturedLet` (a closure's own captured defining-scope trail,
110
+ // see Closure's doc comment in value.hpp) instead of `this->let_`.
111
+ // Needed specifically for an ESCAPED closure call -- one where
112
+ // callCtxFor's own closure-nesting check (still-live enclosing call on
113
+ // callStack_) comes back false, meaning the ordinary ancestry walk from
114
+ // `this` (the CALL SITE's context) has no path back to wherever the
115
+ // closure's own captured variables live. `dyn`/`dynPositions` still
116
+ // derive from `this` exactly like callCtx() -- $-vars are dynamically
117
+ // scoped (the caller's own $-vars, not the closure's definer's), so
118
+ // they're unaffected by where the closure came from.
119
+ EvalContext callCtxFromCapturedLet(const std::shared_ptr<TrailView<Value>>& capturedLet,
120
+ const oscad::Scope* newScope = nullptr,
121
+ std::optional<std::array<double, 4>> newColor = std::nullopt,
122
+ std::shared_ptr<const ChildrenNodeList> newChildrenNodes = nullptr,
123
+ const EvalContext* newChildrenCallerCtx = nullptr) const;
124
+
108
125
  // let(...)/list-comprehension let() scoping: opens a new level for all
109
126
  // four trails, floor unchanged on every one (reads see through to the
110
127
  // parent; this let-body's own writes -- including to dyn/dynPositions/
@@ -303,7 +303,7 @@ public:
303
303
  // for) -- `bound` is matched against `funcNode`'s OWN parameters
304
304
  // (discovered now, not at compile time, since the callee wasn't
305
305
  // statically known). Mirrors evalFunctionLiteral exactly otherwise.
306
- Value evalFunctionLiteralFromBound(const oscad::FunctionLiteral& funcNode, BoundArgs bound, EvalContext& ctx,
306
+ Value evalFunctionLiteralFromBound(const Closure& closure, BoundArgs bound, EvalContext& ctx,
307
307
  const oscad::Position* callPos);
308
308
 
309
309
  // Testing-only override, checked before the (once-cached) env var --
@@ -466,9 +466,18 @@ private:
466
466
  // (see CallStackFrame::upvalueParent's own doc comment for the real
467
467
  // scenario this was caught on -- a closure passed through an
468
468
  // unrelated intermediary function).
469
+ // `capturedLet`: non-null only for a FunctionLiteral *value* callee
470
+ // (see Closure's doc comment, value.hpp) -- when the closure-nesting
471
+ // check below comes back false (the defining call has already
472
+ // returned -- an ESCAPED closure), the isolated call context is rooted
473
+ // at `capturedLet` instead of `ctx`'s own trail, so the closure's
474
+ // captured variables stay resolvable. Ignored (as if null) when the
475
+ // check comes back true: an actually-still-live enclosing call is
476
+ // already reachable via `ctx`'s own ancestry, the ordinary path.
469
477
  EvalContext callCtxFor(const oscad::ASTNode& decl, EvalContext& ctx, const oscad::Scope* scope,
470
478
  std::shared_ptr<const ChildrenNodeList> childrenNodes = nullptr,
471
- const EvalContext* childrenCallerCtx = nullptr, bool* usedChildCtx = nullptr);
479
+ const EvalContext* childrenCallerCtx = nullptr, bool* usedChildCtx = nullptr,
480
+ const std::shared_ptr<TrailView<Value>>& capturedLet = nullptr);
472
481
 
473
482
  // Fills in any parameter not already present in `bound` (bindArgs'
474
483
  // own return value -- the authoritative "did the caller actually
@@ -629,7 +638,8 @@ private:
629
638
  const std::vector<std::unique_ptr<oscad::ParameterDeclaration>>& params,
630
639
  const oscad::Expression& body, bool hasCompiledChunk,
631
640
  const std::vector<std::unique_ptr<oscad::Argument>>& arguments,
632
- EvalContext& ctx, const oscad::Position& callPos);
641
+ EvalContext& ctx, const oscad::Position& callPos,
642
+ const std::shared_ptr<TrailView<Value>>& capturedLet = nullptr);
633
643
 
634
644
  // Marker for "not one of the 5 trampolinable node kinds" -- the
635
645
  // trampoline falls back to a genuine evalExpr(node, ctx) call for
@@ -655,7 +665,7 @@ private:
655
665
  Value evalUserFunction(const std::string& name, const oscad::FunctionDeclaration& decl,
656
666
  const std::vector<std::unique_ptr<oscad::Argument>>& arguments, EvalContext& ctx,
657
667
  const oscad::ASTNode* callNode);
658
- Value evalFunctionLiteral(const oscad::FunctionLiteral& funcNode,
668
+ Value evalFunctionLiteral(const Closure& closure,
659
669
  const std::vector<std::unique_ptr<oscad::Argument>>& arguments, EvalContext& ctx,
660
670
  const oscad::ASTNode* callNode);
661
671
 
@@ -741,7 +751,11 @@ public:
741
751
  // nullopt if `declNode` is closure-nested inside the currently-active
742
752
  // call, in which case the caller must fall back to a real recursive
743
753
  // call (evalUserFunctionFromBound/evalFunctionLiteralFromBound).
744
- std::optional<EvalContext> isolatedCallCtxFor(const oscad::ASTNode& declNode, EvalContext& ctx);
754
+ // `capturedLet`: see callCtxFor's own doc comment -- pass the callee
755
+ // closure's captured trail for a FunctionLiteral-value callee (nullptr
756
+ // for a named FunctionDeclaration callee, which has none).
757
+ std::optional<EvalContext> isolatedCallCtxFor(const oscad::ASTNode& declNode, EvalContext& ctx,
758
+ const std::shared_ptr<TrailView<Value>>& capturedLet = nullptr);
745
759
 
746
760
  // Shared per-hop bookkeeping for BOTH trampolines (evalFunctionBodyTrampoline,
747
761
  // user_calls.cpp, AND runCompiledFunctionTrampoline/
@@ -18,19 +18,23 @@ namespace oscadeval {
18
18
 
19
19
  struct ValueList;
20
20
  struct ValueObject;
21
+ struct Closure;
21
22
 
22
23
  using ListPtr = std::shared_ptr<const ValueList>;
23
24
  using ObjectPtr = std::shared_ptr<const ValueObject>;
25
+ using ClosurePtr = std::shared_ptr<const Closure>;
24
26
 
25
27
  // The OpenSCAD dynamic value type. `bool` is a distinct alternative from
26
28
  // `double` throughout the evaluator -- never implicitly coerced, unlike
27
29
  // Python's own bool-is-int -- see every free function below.
28
30
  //
29
- // A function-literal *value* (`g = function(x) x*2;`) is the AST node
30
- // pointer itself, mirroring the Python reference's _expr_function_literal
31
- // (`return node`) -- no separate closure-capture wrapper. Non-owning; valid
32
- // as long as the parsed AST it points into is alive (see evaluator.hpp's
33
- // ownership contract, added in a later phase).
31
+ // A function-literal *value* (`g = function(x) x*2;`) is a Closure: the AST
32
+ // node pointer plus a captured snapshot of its defining scope's own `let_`
33
+ // trail (see Closure's own doc comment below) -- unlike the Python
34
+ // reference's _expr_function_literal (a bare `return node`, no capture at
35
+ // all), needed for a closure that outlives the call that created it (see
36
+ // Closure's doc comment for the motivating BOSL2 example and why the
37
+ // simpler bare-pointer approach silently breaks that pattern).
34
38
  using Value = std::variant<std::monostate, // undef
35
39
  bool,
36
40
  double, // OpenSCAD has one numeric type
@@ -38,7 +42,52 @@ using Value = std::variant<std::monostate, // undef
38
42
  ListPtr,
39
43
  OscRange,
40
44
  ObjectPtr,
41
- const oscad::FunctionLiteral*>;
45
+ ClosurePtr>;
46
+
47
+ // A function-literal value: the AST node (params/body, non-owning -- valid
48
+ // as long as the parsed AST it points into is alive, same ownership
49
+ // contract as every other raw AST pointer this evaluator holds) plus
50
+ // `capturedLet`, a type-erased `shared_ptr<TrailView<Value>>` (see
51
+ // scope_trail.hpp) snapshotting the `let_` trail live at the moment this
52
+ // literal was evaluated into a value. Type-erased here (not
53
+ // `shared_ptr<TrailView<Value>>` directly) purely to avoid a value.hpp <->
54
+ // scope_trail.hpp circular include (scope_trail.hpp already includes
55
+ // value.hpp for Value itself); callers that need the typed pointer back
56
+ // (eval_context.cpp, user_calls.cpp, bytecode_vm.cpp -- anywhere
57
+ // scope_trail.hpp is already visible) use capturedLetTrail() below.
58
+ //
59
+ // WHY this exists: a plain `TrailView` level is popped (and its bindings
60
+ // destroyed) the instant the EvalContext holding it goes out of scope --
61
+ // fine for ordinary nested evaluation, wrong for a closure that ESCAPES
62
+ // its creating call, e.g. BOSL2's `hashmap()`:
63
+ // function make_closure(captured) = function(k) captured + k;
64
+ // c = make_closure(10);
65
+ // echo(c(5)); // must be 15 -- `captured` has to survive make_closure()
66
+ // // having already returned by the time c(5) runs.
67
+ // Holding this shared_ptr copy keeps that trail level's refcount above
68
+ // zero for as long as ANY closure value referencing it is still reachable,
69
+ // so `captured` stays resolvable via the ordinary ancestry-walk lookup
70
+ // long after the call that bound it has returned -- no snapshot/copy of
71
+ // the bindings themselves needed, just extending what already exists.
72
+ // `Closure::operator==` compares `node` only (matching the old bare-
73
+ // pointer equality exactly) -- two closures over the same AST node are
74
+ // still "the same function", regardless of what each captured.
75
+ struct Closure {
76
+ const oscad::FunctionLiteral* node = nullptr;
77
+ std::shared_ptr<void> capturedLet;
78
+
79
+ friend bool operator==(const Closure& a, const Closure& b) { return a.node == b.node; }
80
+ };
81
+
82
+ template <typename T>
83
+ class TrailView;
84
+
85
+ // Typed access to Closure::capturedLet -- see its own doc comment for why
86
+ // the field itself is type-erased. Only ever called where scope_trail.hpp
87
+ // is already included.
88
+ inline std::shared_ptr<TrailView<Value>> capturedLetTrail(const Closure& c) {
89
+ return std::static_pointer_cast<TrailView<Value>>(c.capturedLet);
90
+ }
42
91
 
43
92
  // Defined after Value so both can hold Value by value -- the standard
44
93
  // recursive-variant pattern (indirection through a forward-declared,
@@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build"
4
4
 
5
5
  [project]
6
6
  name = "openscad_cpp_evaluator"
7
- version = "0.4.6"
7
+ version = "0.4.8"
8
8
  description = "C++ OpenSCAD evaluator with Python bindings"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -615,7 +615,7 @@ Value evalBuiltinFunction(Evaluator& ev, const std::string& name, const CallArgs
615
615
  }
616
616
  case BuiltinFnId::IsFunction: {
617
617
  const Value x = getArg(args, 0, "x", Value{});
618
- return Value{std::holds_alternative<const oscad::FunctionLiteral*>(x) && std::get<const oscad::FunctionLiteral*>(x) != nullptr};
618
+ return Value{std::holds_alternative<ClosurePtr>(x) && std::get<ClosurePtr>(x) != nullptr};
619
619
  }
620
620
  case BuiltinFnId::IsObject: {
621
621
  const Value x = getArg(args, 0, "x", Value{});
@@ -261,8 +261,33 @@ public:
261
261
  *n.body)) {
262
262
  throw NotCompilable{};
263
263
  }
264
+ // A nested literal that reads ANY enclosing variable
265
+ // (literalChunk.upvalues non-empty) is compiled via
266
+ // Op::LoadUpvalue -- a live-call-stack walk (findUpvalue)
267
+ // that only ever resolves a still-active enclosing call,
268
+ // never a captured environment (see LoadUpvalue/
269
+ // findUpvalue's own doc comments: "this codebase has no
270
+ // escaping closures"). A closure that escapes its creating
271
+ // call (returned, stored, passed on -- see Closure's own
272
+ // doc comment, value.hpp, for the motivating BOSL2
273
+ // example) would silently read undef for every captured
274
+ // variable through this path. Bailing the WHOLE containing
275
+ // compilation here forces such a function to run through
276
+ // the interpreter instead, which DOES support escaping
277
+ // closures correctly (evalExpr's FunctionLiteral case
278
+ // captures ctx.let_ itself). A literal with no upvalues at
279
+ // all (doesn't reference anything from an enclosing scope)
280
+ // has nothing that needs escaping-capture support, so it's
281
+ // unaffected and keeps compiling normally.
282
+ if (!literalChunk.upvalues.empty()) throw NotCompilable{};
264
283
  chunk_.nestedLiterals.emplace_back(&n, std::move(literalChunk));
265
- out.push_back({Op::PushConst, internConst(Value{&n}), 0, nullptr});
284
+ // No captured `let_` trail here (nullptr) -- a compile-time
285
+ // constant can't carry per-invocation runtime state, but
286
+ // (per the upvalues check just above) this literal doesn't
287
+ // reference anything outside itself, so there is nothing
288
+ // capture. Not a regression; just not (yet) extended here.
289
+ out.push_back({Op::PushConst, internConst(Value{std::make_shared<const Closure>(Closure{&n, nullptr})}),
290
+ 0, nullptr});
266
291
  return;
267
292
  }
268
293
  case NodeKind::RangeLiteral: {
@@ -342,22 +342,23 @@ Value runChunk(Evaluator& ev, const CompiledChunk& chunk, const std::vector<Inst
342
342
  Value callee = std::move(stack.back());
343
343
  stack.pop_back();
344
344
  Value result;
345
- if (const auto* flPtr = std::get_if<const oscad::FunctionLiteral*>(&callee); flPtr && *flPtr) {
345
+ if (const auto* closurePtr = std::get_if<ClosurePtr>(&callee); closurePtr && *closurePtr) {
346
+ const Closure& closure = **closurePtr;
346
347
  BoundArgs bound;
347
348
  bound.reserve(argCount);
348
349
  size_t positionalIdx = 0;
349
- const size_t nparams = (*flPtr)->parameters.size();
350
+ const size_t nparams = closure.node->parameters.size();
350
351
  for (size_t i = 0; i < argCount; ++i) {
351
352
  if (site.argNames[i]) {
352
353
  bound.set(*site.argNames[i], std::move(args[i]));
353
354
  } else {
354
355
  if (positionalIdx < nparams) {
355
- bound.set((*flPtr)->parameters[positionalIdx]->name->name, std::move(args[i]));
356
+ bound.set(closure.node->parameters[positionalIdx]->name->name, std::move(args[i]));
356
357
  }
357
358
  ++positionalIdx;
358
359
  }
359
360
  }
360
- result = ev.evalFunctionLiteralFromBound(**flPtr, std::move(bound), ctx, ins.pos);
361
+ result = ev.evalFunctionLiteralFromBound(closure, std::move(bound), ctx, ins.pos);
361
362
  } else if (!site.calleeName.empty()) {
362
363
  ev.warn("Ignoring unknown function '" + site.calleeName + "'", ins.pos);
363
364
  }
@@ -424,8 +425,9 @@ Value runChunk(Evaluator& ev, const CompiledChunk& chunk, const std::vector<Inst
424
425
  Value callee = std::move(stack.back());
425
426
  stack.pop_back();
426
427
  Value result;
427
- if (const auto* flPtr = std::get_if<const oscad::FunctionLiteral*>(&callee); flPtr && *flPtr) {
428
- const oscad::FunctionLiteral& funcNode = **flPtr;
428
+ if (const auto* closurePtr = std::get_if<ClosurePtr>(&callee); closurePtr && *closurePtr) {
429
+ const Closure& closure = **closurePtr;
430
+ const oscad::FunctionLiteral& funcNode = *closure.node;
429
431
  BoundArgs bound;
430
432
  bound.reserve(argCount);
431
433
  size_t positionalIdx = 0;
@@ -441,7 +443,7 @@ Value runChunk(Evaluator& ev, const CompiledChunk& chunk, const std::vector<Inst
441
443
  }
442
444
  }
443
445
  if (tailOut != nullptr) {
444
- if (auto hopCtx = ev.isolatedCallCtxFor(funcNode, ctx)) {
446
+ if (auto hopCtx = ev.isolatedCallCtxFor(funcNode, ctx, capturedLetTrail(closure))) {
445
447
  if (ev.lookupCompiledLiteralChunk(funcNode) != nullptr) {
446
448
  tailOut->decl = nullptr;
447
449
  tailOut->literal = &funcNode;
@@ -453,7 +455,7 @@ Value runChunk(Evaluator& ev, const CompiledChunk& chunk, const std::vector<Inst
453
455
  }
454
456
  }
455
457
  }
456
- result = ev.evalFunctionLiteralFromBound(funcNode, std::move(bound), ctx, ins.pos);
458
+ result = ev.evalFunctionLiteralFromBound(closure, std::move(bound), ctx, ins.pos);
457
459
  } else if (!site.calleeName.empty()) {
458
460
  ev.warn("Ignoring unknown function '" + site.calleeName + "'", ins.pos);
459
461
  }
@@ -60,6 +60,31 @@ EvalContext EvalContext::callCtx(const oscad::Scope* newScope, std::optional<std
60
60
  return result;
61
61
  }
62
62
 
63
+ EvalContext EvalContext::callCtxFromCapturedLet(const std::shared_ptr<TrailView<Value>>& capturedLet,
64
+ const oscad::Scope* newScope,
65
+ std::optional<std::array<double, 4>> newColor,
66
+ std::shared_ptr<const ChildrenNodeList> newChildrenNodes,
67
+ const EvalContext* newChildrenCallerCtx) const {
68
+ EvalContext result;
69
+ result.scope = newScope ? newScope : scope;
70
+ auto newDynTrail = dyn.trail()->openChild(/*isolate=*/false); // stays dynamically scoped through the CALL SITE
71
+ result.dyn = DynValueView(newDynTrail);
72
+ result.dynExplicit = DynExplicitView(newDynTrail);
73
+ // isolate=false: continue the ancestry chain THROUGH capturedLet, not
74
+ // terminate at it -- the entire point is inheriting the closure's own
75
+ // captured bindings (isolate=true would sever that immediately,
76
+ // making the capture pointless; the isolation this needs -- NOT
77
+ // seeing the call SITE's own locals -- already comes for free from
78
+ // rooting at capturedLet instead of at `this->let_` in the first
79
+ // place).
80
+ result.let_ = capturedLet->openChild(/*isolate=*/false);
81
+ result.dynPositions = dynPositions->openChild(true);
82
+ result.color = newColor.has_value() ? newColor : color;
83
+ result.childrenNodes = newChildrenNodes ? newChildrenNodes : std::make_shared<const ChildrenNodeList>();
84
+ result.childrenCallerCtx = newChildrenCallerCtx;
85
+ return result;
86
+ }
87
+
63
88
  EvalContext EvalContext::letChildCtx() const {
64
89
  EvalContext result = *this;
65
90
  auto newDynTrail = dyn.trail()->openChild(/*isolate=*/false);
@@ -103,7 +103,23 @@ Value Evaluator::evalIdentifier(const std::string& name, const oscad::Position*
103
103
  if (const Value* v = ctx.let_->find(name)) return *v;
104
104
 
105
105
  if (!name.empty() && name[0] == '$') {
106
- if (const Value* v = ctx.dyn->find(name)) return *v;
106
+ // $-prefixed names are dynamic variables: a completely separate
107
+ // namespace from lexically-scoped identifiers, never requiring a
108
+ // scope declaration (unlike a bare identifier). Found via
109
+ // BelfrySCAD/BOSL2's constants.scad `get_slop()`
110
+ // (`is_undef($slop) ? 0 : $slop` with $slop never assigned
111
+ // anywhere) producing a spurious "Ignoring unknown variable
112
+ // '$slop'" warning here but not through the bytecode VM's
113
+ // Op::LoadDyn (bytecode_vm.cpp), which already does exactly
114
+ // this -- find in ctx.dyn, undef if absent, no scope-lookup
115
+ // fallback, no warning, ever. Verified against real OpenSCAD.app:
116
+ // no warning for a never-set $-variable. Must return here
117
+ // unconditionally rather than falling through to the
118
+ // scope->lookupVariable() check below, which can never resolve a
119
+ // $-name anyway (dynamic variables aren't scope-declarable) and
120
+ // exists only for plain identifiers.
121
+ const Value* v = ctx.dyn->find(name);
122
+ return v ? *v : Value{};
107
123
  }
108
124
 
109
125
  if (name == "PI") return Value{std::numbers::pi};
@@ -375,11 +391,17 @@ Value Evaluator::evalExpr(const oscad::Expression& node, EvalContext& ctx) {
375
391
  case NodeKind::ListComprehension:
376
392
  return evalListLiteral(static_cast<const oscad::ListComprehension&>(node), ctx);
377
393
  case NodeKind::FunctionLiteral:
378
- // A function-literal *value* is the AST node pointer itself --
379
- // no closure-capture wrapper (evalFunctionLiteral re-resolves
380
- // the node's own lexical scope at call time). Mirrors
381
- // _expr_function_literal's `return node` exactly.
382
- return Value{static_cast<const oscad::FunctionLiteral*>(&node)};
394
+ // A function-literal *value* captures ctx.let_ (extending that
395
+ // trail level's lifetime for as long as this Closure is
396
+ // reachable -- see Closure's own doc comment, value.hpp) so a
397
+ // closure that escapes this call (returned, stored, passed on)
398
+ // can still resolve variables from its defining scope after
399
+ // this call has returned. `dyn`/scope are NOT captured: $-vars
400
+ // are dynamically scoped (see the caller's own ctx.dyn at call
401
+ // time, matching real OpenSCAD), and the lexical scope is
402
+ // static AST data already reachable via the node itself.
403
+ return Value{std::make_shared<const Closure>(
404
+ Closure{static_cast<const oscad::FunctionLiteral*>(&node), ctx.let_})};
383
405
  case NodeKind::PrimaryCall:
384
406
  return evalFunctionCall(static_cast<const oscad::PrimaryCall&>(node), ctx);
385
407
  case NodeKind::LetOp:
@@ -84,9 +84,9 @@ std::string canonValue(const Value& v) {
84
84
  }
85
85
  out += ")";
86
86
  return out;
87
- } else { // const oscad::FunctionLiteral*
87
+ } else { // ClosurePtr
88
88
  char buf[32];
89
- std::snprintf(buf, sizeof(buf), "F%p", static_cast<const void*>(val));
89
+ std::snprintf(buf, sizeof(buf), "F%p", val ? static_cast<const void*>(val->node) : nullptr);
90
90
  return buf;
91
91
  }
92
92
  },
@@ -104,7 +104,30 @@ BoundArgs Evaluator::bindArgs(const std::vector<std::unique_ptr<oscad::Parameter
104
104
 
105
105
  EvalContext Evaluator::callCtxFor(const oscad::ASTNode& decl, EvalContext& ctx, const oscad::Scope* scope,
106
106
  std::shared_ptr<const ChildrenNodeList> childrenNodes,
107
- const EvalContext* childrenCallerCtx, bool* usedChildCtx) {
107
+ const EvalContext* childrenCallerCtx, bool* usedChildCtx,
108
+ const std::shared_ptr<TrailView<Value>>& capturedLet) {
109
+ // A closure's own captured environment (capturedLet, non-null for
110
+ // every interpreter-evaluated FunctionLiteral -- see expr_eval.cpp's
111
+ // own FunctionLiteral case) is always the correct root for this call,
112
+ // full stop -- checked BEFORE the live-call-stack walk below.
113
+ // callCtxFromCapturedLet roots the new scope at `capturedLet` itself,
114
+ // never at `ctx.let_`, so it's already correct regardless of whether
115
+ // `ctx` (the CALLER's own context) happens to be ancestor-linked back
116
+ // to the closure's declaring call or not. That distinction matters in
117
+ // exactly the case the live-frame walk below gets wrong: a closure
118
+ // declared inside function A, passed as a Value into a plain function
119
+ // B, and invoked from inside B's body. B's own ctx is isolated from
120
+ // A's (an ordinary call boundary, see EvalContext::callCtx's own
121
+ // isolate=true), so even though A's own frame may still be technically
122
+ // live on callStack_ (matching the containment check below) and this
123
+ // WOULD take the childCtx() branch, `ctx.childCtx()` continues B's own
124
+ // (isolated) ancestry, not A's -- silently losing every variable the
125
+ // closure captured from A. Skipping straight to capturedLet avoids
126
+ // this regardless of which frames happen to still be live.
127
+ if (capturedLet) {
128
+ if (usedChildCtx) *usedChildCtx = false;
129
+ return ctx.callCtxFromCapturedLet(capturedLet, scope, std::nullopt, std::move(childrenNodes), childrenCallerCtx);
130
+ }
108
131
  const oscad::Position& declPos = decl.position();
109
132
  for (const CallStackFrame& frame : callStack_) {
110
133
  const oscad::Position* outer = frame.declPosition;
@@ -119,9 +142,10 @@ EvalContext Evaluator::callCtxFor(const oscad::ASTNode& decl, EvalContext& ctx,
119
142
  return ctx.callCtx(scope, std::nullopt, std::move(childrenNodes), childrenCallerCtx);
120
143
  }
121
144
 
122
- std::optional<EvalContext> Evaluator::isolatedCallCtxFor(const oscad::ASTNode& declNode, EvalContext& ctx) {
145
+ std::optional<EvalContext> Evaluator::isolatedCallCtxFor(const oscad::ASTNode& declNode, EvalContext& ctx,
146
+ const std::shared_ptr<TrailView<Value>>& capturedLet) {
123
147
  bool usedChildCtx = false;
124
- EvalContext result = callCtxFor(declNode, ctx, ctx.scope, nullptr, nullptr, &usedChildCtx);
148
+ EvalContext result = callCtxFor(declNode, ctx, ctx.scope, nullptr, nullptr, &usedChildCtx, capturedLet);
125
149
  if (usedChildCtx) return std::nullopt;
126
150
  return result;
127
151
  }
@@ -242,11 +266,11 @@ std::optional<Evaluator::TailStep> Evaluator::tryTailStepFor(
242
266
  const std::string& calleeName, const oscad::ASTNode& declNode,
243
267
  const std::vector<std::unique_ptr<oscad::ParameterDeclaration>>& params, const oscad::Expression& body,
244
268
  bool hasCompiledChunk, const std::vector<std::unique_ptr<oscad::Argument>>& arguments, EvalContext& ctx,
245
- const oscad::Position& callPos) {
269
+ const oscad::Position& callPos, const std::shared_ptr<TrailView<Value>>& capturedLet) {
246
270
  if (hasCompiledChunk) return std::nullopt;
247
271
  const oscad::Scope* fnScope = declNode.scope() ? declNode.scope() : ctx.scope;
248
272
  bool usedChildCtx = false;
249
- EvalContext childCtx = callCtxFor(declNode, ctx, fnScope, nullptr, nullptr, &usedChildCtx);
273
+ EvalContext childCtx = callCtxFor(declNode, ctx, fnScope, nullptr, nullptr, &usedChildCtx, capturedLet);
250
274
  if (usedChildCtx) return std::nullopt;
251
275
  bindCallArgsInto(params, bindArgs(params, arguments, ctx), childCtx);
252
276
  TailStep step;
@@ -360,14 +384,16 @@ std::variant<Value, Evaluator::TailStep, Evaluator::NotTailStep> Evaluator::simp
360
384
  // directly. Never re-evaluate n.left below this point -- it
361
385
  // may itself be an arbitrary (side-effecting) expression.
362
386
  Value funcVal = leftId ? evalIdentifier(leftId->name, &leftId->position(), ctx, false) : evalExpr(*n.left, ctx);
363
- if (const auto* flPtr = std::get_if<const oscad::FunctionLiteral*>(&funcVal); flPtr && *flPtr) {
364
- const oscad::FunctionLiteral& funcNode = **flPtr;
387
+ if (const auto* closurePtr = std::get_if<ClosurePtr>(&funcVal); closurePtr && *closurePtr) {
388
+ const Closure& closure = **closurePtr;
389
+ const oscad::FunctionLiteral& funcNode = *closure.node;
365
390
  checkDebug(n, ctx); // call-site stop, function-literal callee
366
391
  const bool hasChunk = useBytecodeVm() && lookupCompiledLiteralChunk(funcNode) != nullptr;
367
- std::optional<TailStep> step = tryTailStepFor("<function literal>", funcNode, funcNode.parameters,
368
- *funcNode.body, hasChunk, n.arguments, ctx, n.position());
392
+ std::optional<TailStep> step =
393
+ tryTailStepFor("<function literal>", funcNode, funcNode.parameters, *funcNode.body, hasChunk,
394
+ n.arguments, ctx, n.position(), capturedLetTrail(closure));
369
395
  if (step) return std::move(*step);
370
- return Value{evalFunctionLiteral(funcNode, n.arguments, ctx, &n)};
396
+ return Value{evalFunctionLiteral(closure, n.arguments, ctx, &n)};
371
397
  }
372
398
 
373
399
  if (leftId) warn("Ignoring unknown function '" + leftId->name + "'", &n.position());
@@ -484,13 +510,15 @@ Value Evaluator::evalUserFunctionFromBound(const std::string& name, const oscad:
484
510
  });
485
511
  }
486
512
 
487
- Value Evaluator::evalFunctionLiteral(const oscad::FunctionLiteral& funcNode,
513
+ Value Evaluator::evalFunctionLiteral(const Closure& closure,
488
514
  const std::vector<std::unique_ptr<oscad::Argument>>& arguments, EvalContext& ctx,
489
515
  const oscad::ASTNode* callNode) {
516
+ const oscad::FunctionLiteral& funcNode = *closure.node;
490
517
  const oscad::Scope* fnScope = funcNode.scope() ? funcNode.scope() : ctx.scope;
491
518
  const int callerFrameIdx = callStack_.empty() ? -1 : static_cast<int>(callStack_.size()) - 1;
492
519
  bool usedChildCtx = false;
493
- EvalContext childCtx = callCtxFor(funcNode, ctx, fnScope, nullptr, nullptr, &usedChildCtx);
520
+ EvalContext childCtx =
521
+ callCtxFor(funcNode, ctx, fnScope, nullptr, nullptr, &usedChildCtx, capturedLetTrail(closure));
494
522
  const int upvalueParent = usedChildCtx ? callerFrameIdx : -1;
495
523
 
496
524
  // A literal only ever has a compiled chunk if some OTHER declaration's
@@ -519,12 +547,14 @@ Value Evaluator::evalFunctionLiteral(const oscad::FunctionLiteral& funcNode,
519
547
  });
520
548
  }
521
549
 
522
- Value Evaluator::evalFunctionLiteralFromBound(const oscad::FunctionLiteral& funcNode, BoundArgs bound,
550
+ Value Evaluator::evalFunctionLiteralFromBound(const Closure& closure, BoundArgs bound,
523
551
  EvalContext& ctx, const oscad::Position* callPos) {
552
+ const oscad::FunctionLiteral& funcNode = *closure.node;
524
553
  const oscad::Scope* fnScope = funcNode.scope() ? funcNode.scope() : ctx.scope;
525
554
  const int callerFrameIdx = callStack_.empty() ? -1 : static_cast<int>(callStack_.size()) - 1;
526
555
  bool usedChildCtx = false;
527
- EvalContext childCtx = callCtxFor(funcNode, ctx, fnScope, nullptr, nullptr, &usedChildCtx);
556
+ EvalContext childCtx =
557
+ callCtxFor(funcNode, ctx, fnScope, nullptr, nullptr, &usedChildCtx, capturedLetTrail(closure));
528
558
  const int upvalueParent = usedChildCtx ? callerFrameIdx : -1;
529
559
  const CompiledChunk* chunk = useBytecodeVm() ? lookupCompiledLiteralChunk(funcNode) : nullptr;
530
560
  if (!chunk) {
@@ -579,10 +609,10 @@ Value Evaluator::evalFunctionCall(const oscad::PrimaryCall& node, EvalContext& c
579
609
  // g(3)`). warnIfUndef=false: probing here shouldn't itself warn
580
610
  // "unknown variable" if `left` isn't bound to anything -- a genuinely
581
611
  // unknown callee gets exactly one warning below, not two.
582
- Value funcNode = leftId ? evalIdentifier(leftId->name, &leftId->position(), ctx, false) : evalExpr(*node.left, ctx);
583
- if (const auto* flPtr = std::get_if<const oscad::FunctionLiteral*>(&funcNode); flPtr && *flPtr) {
612
+ Value funcVal = leftId ? evalIdentifier(leftId->name, &leftId->position(), ctx, false) : evalExpr(*node.left, ctx);
613
+ if (const auto* closurePtr = std::get_if<ClosurePtr>(&funcVal); closurePtr && *closurePtr) {
584
614
  checkDebug(node, ctx); // same call-site stop, function-literal callee
585
- return evalFunctionLiteral(**flPtr, node.arguments, ctx, &node);
615
+ return evalFunctionLiteral(**closurePtr, node.arguments, ctx, &node);
586
616
  }
587
617
 
588
618
  if (leftId) {
@@ -53,7 +53,7 @@ std::string oscTypeName(const Value& v) {
53
53
  if (std::holds_alternative<std::string>(v)) return "string";
54
54
  if (std::holds_alternative<ListPtr>(v)) return "vector";
55
55
  if (std::holds_alternative<ObjectPtr>(v)) return "object";
56
- return "undefined"; // OscRange, FunctionLiteral*
56
+ return "undefined"; // OscRange, ClosurePtr
57
57
  }
58
58
 
59
59
  bool oscEqual(const Value& a, const Value& b) {
@@ -88,9 +88,21 @@ bool oscEqual(const Value& a, const Value& b) {
88
88
  return true;
89
89
  }
90
90
 
91
- // Neither operand is a list or object here, so this only ever compares
92
- // monostate/bool/double/string/OscRange/FunctionLiteral* against its own
93
- // kind (variant::operator== checks the active index first).
91
+ // ClosurePtr is a shared_ptr -- its own operator== compares the pointee
92
+ // ADDRESS, not Closure::operator==(), so two independently-created
93
+ // closures over the identical AST node (the semantics this mirrors --
94
+ // see Closure's own doc comment) would wrongly compare unequal via the
95
+ // variant fallthrough below. Dereference and compare explicitly.
96
+ const ClosurePtr* ca = std::get_if<ClosurePtr>(&a);
97
+ const ClosurePtr* cb = std::get_if<ClosurePtr>(&b);
98
+ if (ca || cb) {
99
+ if (!ca || !cb || !*ca || !*cb) return false;
100
+ return **ca == **cb;
101
+ }
102
+
103
+ // Neither operand is a list, object, or closure here, so this only ever
104
+ // compares monostate/bool/double/string/OscRange against its own kind
105
+ // (variant::operator== checks the active index first).
94
106
  return a == b;
95
107
  }
96
108
 
@@ -107,7 +119,7 @@ bool truthy(const Value& v) {
107
119
  if (const ListPtr* l = std::get_if<ListPtr>(&v)) return *l && !(*l)->items.empty();
108
120
  if (const ObjectPtr* o = std::get_if<ObjectPtr>(&v)) return *o && !(*o)->items.empty();
109
121
  if (std::holds_alternative<std::monostate>(v)) return false;
110
- return true; // OscRange, FunctionLiteral*
122
+ return true; // OscRange, ClosurePtr
111
123
  }
112
124
 
113
125
  bool oscComparable(const Value& a, const Value& b) {
@@ -325,7 +337,7 @@ std::string fmtValue(const Value& v) {
325
337
  return s + ")";
326
338
  }
327
339
  if (const std::string* s = std::get_if<std::string>(&v)) return "\"" + *s + "\"";
328
- return "<function-literal>"; // const FunctionLiteral* -- no meaningful textual form in the reference either
340
+ return "<function-literal>"; // OscRange handled above; ClosurePtr has no meaningful textual form either
329
341
  }
330
342
 
331
343
  // (Range::numValues()'s own count, closed-form) -- 1 + floor((end-start)/step),