openscad-cpp-evaluator 0.5.1__tar.gz → 0.6.1__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.5.1 → openscad_cpp_evaluator-0.6.1}/PKG-INFO +1 -1
  2. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/include/openscad_cpp_evaluator/bytecode.hpp +78 -9
  3. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/pyproject.toml +1 -1
  4. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/src/bytecode_compiler.cpp +74 -42
  5. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/src/bytecode_vm.cpp +57 -0
  6. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/src/user_calls.cpp +26 -0
  7. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/tests/test_bytecode_compiler.cpp +131 -21
  8. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/tests/test_tail_calls.cpp +29 -0
  9. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/.github/workflows/ci.yml +0 -0
  10. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/.github/workflows/wheels.yml +0 -0
  11. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/.gitignore +0 -0
  12. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/.gitmodules +0 -0
  13. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/CLAUDE.md +0 -0
  14. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/CMakeLists.txt +0 -0
  15. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/LICENSE +0 -0
  16. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/README.md +0 -0
  17. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/bindings/CMakeLists.txt +0 -0
  18. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/bindings/module.cpp +0 -0
  19. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/cmake/embed_font.cmake +0 -0
  20. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/examples/CMakeLists.txt +0 -0
  21. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/examples/eval_perf_benchmark.cpp +0 -0
  22. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/examples/manifold_cache_reuse.cpp +0 -0
  23. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/examples/minimal_debugger.cpp +0 -0
  24. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/.github/workflows/ci.yml +0 -0
  25. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/.gitignore +0 -0
  26. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/CLAUDE.md +0 -0
  27. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/CMakeLists.txt +0 -0
  28. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/README.md +0 -0
  29. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/api.hpp +0 -0
  30. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/ast_node.hpp +0 -0
  31. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/comments.hpp +0 -0
  32. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/declarations.hpp +0 -0
  33. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/expression.hpp +0 -0
  34. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/module_instantiation.hpp +0 -0
  35. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/scope_builder.hpp +0 -0
  36. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/vector_element.hpp +0 -0
  37. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast.hpp +0 -0
  38. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/comments.hpp +0 -0
  39. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/position.hpp +0 -0
  40. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/pretty_print.hpp +0 -0
  41. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/scope.hpp +0 -0
  42. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/serialization.hpp +0 -0
  43. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/include/openscad_cpp_parser/source_map.hpp +0 -0
  44. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/src/CMakeLists.txt +0 -0
  45. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/src/api.cpp +0 -0
  46. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/src/ast/ast_node.cpp +0 -0
  47. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/src/ast/declarations.cpp +0 -0
  48. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/src/ast/expression.cpp +0 -0
  49. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/src/ast/format_utils.hpp +0 -0
  50. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/src/ast/module_instantiation.cpp +0 -0
  51. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/src/ast/scope_builder.cpp +0 -0
  52. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/src/ast/vector_element.cpp +0 -0
  53. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/src/comment_attach_internal.hpp +0 -0
  54. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/src/comments.cpp +0 -0
  55. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/src/grammar/driver.cpp +0 -0
  56. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/src/grammar/driver.hpp +0 -0
  57. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/src/grammar/lexer.l +0 -0
  58. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/src/grammar/lexer_api.hpp +0 -0
  59. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/src/grammar/oscad_location.hpp +0 -0
  60. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/src/grammar/parser.y +0 -0
  61. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/src/inline_comment_attach.cpp +0 -0
  62. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/src/position.cpp +0 -0
  63. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/src/pretty_print.cpp +0 -0
  64. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/src/scope.cpp +0 -0
  65. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/src/serialization/json_io.cpp +0 -0
  66. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/src/serialization/yaml_io.cpp +0 -0
  67. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/src/source_map.cpp +0 -0
  68. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/tests/CMakeLists.txt +0 -0
  69. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/tests/test_assignments.cpp +0 -0
  70. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/tests/test_ast_generation.cpp +0 -0
  71. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/tests/test_comments_and_files.cpp +0 -0
  72. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/tests/test_const_borrow.cpp +0 -0
  73. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/tests/test_control.cpp +0 -0
  74. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/tests/test_expressions.cpp +0 -0
  75. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/tests/test_functions.cpp +0 -0
  76. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/tests/test_helpers.hpp +0 -0
  77. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/tests/test_inline_comments.cpp +0 -0
  78. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/tests/test_lexical.cpp +0 -0
  79. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/tests/test_modules.cpp +0 -0
  80. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/tests/test_node_str.cpp +0 -0
  81. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/tests/test_pretty_print.cpp +0 -0
  82. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/tests/test_scope.cpp +0 -0
  83. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/tests/test_serialization.cpp +0 -0
  84. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/tests/test_smoke.cpp +0 -0
  85. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/tests/test_source_map.cpp +0 -0
  86. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/tests/test_use_include.cpp +0 -0
  87. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/tests/test_vectors.cpp +0 -0
  88. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/tools/cli/CMakeLists.txt +0 -0
  89. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/external/openscad_cpp_parser/tools/cli/main.cpp +0 -0
  90. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/include/openscad_cpp_evaluator/bound_args.hpp +0 -0
  91. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/include/openscad_cpp_evaluator/bytecode_compiler.hpp +0 -0
  92. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/include/openscad_cpp_evaluator/bytecode_vm.hpp +0 -0
  93. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/include/openscad_cpp_evaluator/call_args.hpp +0 -0
  94. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/include/openscad_cpp_evaluator/colored_body.hpp +0 -0
  95. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/include/openscad_cpp_evaluator/csg_node.hpp +0 -0
  96. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/include/openscad_cpp_evaluator/css_colors.hpp +0 -0
  97. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/include/openscad_cpp_evaluator/debug_hooks.hpp +0 -0
  98. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/include/openscad_cpp_evaluator/debug_repl.hpp +0 -0
  99. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/include/openscad_cpp_evaluator/dispatch.hpp +0 -0
  100. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/include/openscad_cpp_evaluator/dxf_svg_import.hpp +0 -0
  101. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/include/openscad_cpp_evaluator/eval_context.hpp +0 -0
  102. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/include/openscad_cpp_evaluator/eval_error.hpp +0 -0
  103. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/include/openscad_cpp_evaluator/eval_use.hpp +0 -0
  104. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/include/openscad_cpp_evaluator/evaluator.hpp +0 -0
  105. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/include/openscad_cpp_evaluator/export.hpp +0 -0
  106. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/include/openscad_cpp_evaluator/font_provider.hpp +0 -0
  107. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/include/openscad_cpp_evaluator/function_builtins.hpp +0 -0
  108. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/include/openscad_cpp_evaluator/import_builtin.hpp +0 -0
  109. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/include/openscad_cpp_evaluator/manifold_cache.hpp +0 -0
  110. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/include/openscad_cpp_evaluator/mesh_import.hpp +0 -0
  111. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/include/openscad_cpp_evaluator/osc_range.hpp +0 -0
  112. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/include/openscad_cpp_evaluator/profile.hpp +0 -0
  113. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/include/openscad_cpp_evaluator/scope_trail.hpp +0 -0
  114. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/include/openscad_cpp_evaluator/segments.hpp +0 -0
  115. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/include/openscad_cpp_evaluator/stb_font_provider.hpp +0 -0
  116. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/include/openscad_cpp_evaluator/surface_load.hpp +0 -0
  117. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/include/openscad_cpp_evaluator/text_metrics.hpp +0 -0
  118. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/include/openscad_cpp_evaluator/value.hpp +0 -0
  119. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/include/openscad_cpp_evaluator/zip_stored.hpp +0 -0
  120. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/python/openscad_cpp_evaluator/__init__.py +0 -0
  121. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/resources/fonts/LICENSE +0 -0
  122. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/resources/fonts/LiberationSans-Regular.ttf +0 -0
  123. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/src/CMakeLists.txt +0 -0
  124. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/src/builtins/booleans.cpp +0 -0
  125. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/src/builtins/builtins.hpp +0 -0
  126. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/src/builtins/call_args.cpp +0 -0
  127. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/src/builtins/color.cpp +0 -0
  128. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/src/builtins/control.cpp +0 -0
  129. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/src/builtins/extrude.cpp +0 -0
  130. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/src/builtins/function_builtins.cpp +0 -0
  131. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/src/builtins/import.cpp +0 -0
  132. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/src/builtins/modifiers.cpp +0 -0
  133. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/src/builtins/primitives_2d.cpp +0 -0
  134. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/src/builtins/primitives_3d.cpp +0 -0
  135. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/src/builtins/registry.cpp +0 -0
  136. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/src/builtins/roof.cpp +0 -0
  137. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/src/builtins/segments.cpp +0 -0
  138. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/src/builtins/surface.cpp +0 -0
  139. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/src/builtins/text.cpp +0 -0
  140. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/src/builtins/topology.cpp +0 -0
  141. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/src/builtins/transforms.cpp +0 -0
  142. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/src/colored_body.cpp +0 -0
  143. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/src/csg_generate.cpp +0 -0
  144. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/src/csg_resolve.cpp +0 -0
  145. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/src/css_colors.cpp +0 -0
  146. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/src/debug/debug_repl.cpp +0 -0
  147. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/src/debug_profile.cpp +0 -0
  148. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/src/eval_context.cpp +0 -0
  149. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/src/eval_error.cpp +0 -0
  150. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/src/eval_use.cpp +0 -0
  151. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/src/export.cpp +0 -0
  152. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/src/expr_eval.cpp +0 -0
  153. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/src/import/dxf_import.cpp +0 -0
  154. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/src/import/mesh_import.cpp +0 -0
  155. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/src/import/surface_load.cpp +0 -0
  156. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/src/import/svg_import.cpp +0 -0
  157. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/src/manifold_cache.cpp +0 -0
  158. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/src/stmt_eval.cpp +0 -0
  159. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/src/text/stb_font_provider.cpp +0 -0
  160. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/src/text/text_metrics.cpp +0 -0
  161. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/src/value.cpp +0 -0
  162. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/src/zip_stored.cpp +0 -0
  163. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/tests/CMakeLists.txt +0 -0
  164. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/tests/test_booleans.cpp +0 -0
  165. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/tests/test_cli.cpp +0 -0
  166. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/tests/test_control_flow.cpp +0 -0
  167. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/tests/test_csg_tree.cpp +0 -0
  168. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/tests/test_debug_hooks.cpp +0 -0
  169. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/tests/test_dxf_svg_import.cpp +0 -0
  170. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/tests/test_eval_context.cpp +0 -0
  171. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/tests/test_export.cpp +0 -0
  172. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/tests/test_expr_eval.cpp +0 -0
  173. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/tests/test_extrude_roof.cpp +0 -0
  174. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/tests/test_function_builtins.cpp +0 -0
  175. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/tests/test_helpers.hpp +0 -0
  176. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/tests/test_import_export.cpp +0 -0
  177. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/tests/test_manifold_cache.cpp +0 -0
  178. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/tests/test_multi_color_merge.cpp +0 -0
  179. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/tests/test_primitives.cpp +0 -0
  180. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/tests/test_profiling.cpp +0 -0
  181. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/tests/test_python_bindings.py +0 -0
  182. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/tests/test_scope_trail.cpp +0 -0
  183. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/tests/test_scoping.cpp +0 -0
  184. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/tests/test_surface.cpp +0 -0
  185. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/tests/test_text.cpp +0 -0
  186. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/tests/test_transforms.cpp +0 -0
  187. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/tests/test_use.cpp +0 -0
  188. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/tests/test_value.cpp +0 -0
  189. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/tests/test_viewport_params.cpp +0 -0
  190. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/tests/test_zip_stored.cpp +0 -0
  191. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/tools/cli/CMakeLists.txt +0 -0
  192. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/tools/cli/cli_lib.cpp +0 -0
  193. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/tools/cli/cli_lib.hpp +0 -0
  194. {openscad_cpp_evaluator-0.5.1 → openscad_cpp_evaluator-0.6.1}/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.5.1
3
+ Version: 0.6.1
4
4
  Summary: C++ OpenSCAD evaluator with Python bindings
5
5
  Author: NeuSCAD Contributors
6
6
  License-Expression: MIT
@@ -100,16 +100,47 @@ enum class Op {
100
100
  // a = index into CompiledChunk::upvalues. At runtime, searches the live
101
101
  // call stack (Evaluator::findUpvalue) for a still-active call of the
102
102
  // upvalue's own target declaration and reads that call's own slot --
103
- // undef if that call already returned (this codebase has no escaping
104
- // closures, matching the interpreter's existing childCtx()-based
105
- // semantics exactly; see scope_trail.hpp's own module comment). Never
106
- // needs a STORE counterpart: a captured variable is always a
107
- // parameter/let-binding of some ENCLOSING, already-executing call,
108
- // and nothing this phase compiles ever writes back into an enclosing
109
- // call's own slot (let/ListCompLet assignments always target a
110
- // freshly-opened scope of their OWN, never a parent's).
103
+ // undef if that call already returned. Never needs a STORE counterpart:
104
+ // a captured variable is always a parameter/let-binding of some
105
+ // ENCLOSING, already-executing call, and nothing this phase compiles
106
+ // ever writes back into an enclosing call's own slot (let/ListCompLet
107
+ // assignments always target a freshly-opened scope of their OWN, never
108
+ // a parent's).
109
+ //
110
+ // NOTE: as of Op::MakeClosure (below), no CompiledChunk that's ever
111
+ // actually registered (chunkCache_/literalChunkCache_) has a non-empty
112
+ // `upvalues` list any more -- a FunctionLiteral with any real capture
113
+ // need (direct or transitively bubbled up from a nested literal, see
114
+ // bytecode_compiler.cpp's FunctionLiteral case) is now handled via
115
+ // Op::MakeClosure instead, and always runs interpreted when invoked
116
+ // (Evaluator::lookupCompiledLiteralChunk finds no entry for it). This
117
+ // opcode and Evaluator::findUpvalue/CallStackFrame::upvalueParent are
118
+ // therefore unreachable dead code today, left in place rather than
119
+ // removed in the same change that made them unreachable -- deleting
120
+ // them is a distinct, lower-risk cleanup, not bundled in here.
111
121
  LoadUpvalue,
112
122
 
123
+ // Escaping-closure support. a = index into CompiledChunk::closureSites.
124
+ // Builds a FRESH captured environment every time this instruction
125
+ // actually runs (never a compile-time constant -- a loop creating one
126
+ // closure per iteration must capture THAT iteration's own values, not
127
+ // share one snapshot) by reading each of the site's own captures
128
+ // straight out of the CURRENTLY EXECUTING frame's own `slots` (see
129
+ // ClosureSite's own doc comment for why every capture, however deeply
130
+ // the literal was originally nested, always resolves against the one
131
+ // chunk actually running Op::MakeClosure), and constructs
132
+ // Closure{node, capturedTrail} -- reusing Closure::capturedLet (value.hpp)
133
+ // and callCtxFor's existing capturedLet-rooting (user_calls.cpp)
134
+ // UNCHANGED: invoking this closure later (always via the interpreter,
135
+ // see ClosureSite's own doc comment for why its body is never itself
136
+ // compiled) works exactly like any interpreter-created escaping closure
137
+ // always has, no new invocation-side code needed at all. Replaces
138
+ // Op::PushConst's frozen `Closure{&n, nullptr}` specifically for a
139
+ // literal whose (transitively merged) capture set is non-empty; a
140
+ // literal that closes over nothing at all -- even transitively -- still
141
+ // takes the cheaper PushConst path, unchanged.
142
+ MakeClosure,
143
+
113
144
  // A call whose callee isn't statically resolvable to a builtin or a
114
145
  // named FunctionDeclaration (see CallFn) -- the callee expression is
115
146
  // compiled to push its own Value first, THEN each argument (so at
@@ -225,10 +256,47 @@ struct CompiledChunk {
225
256
  // inside AT COMPILE TIME (matched at runtime via Evaluator::
226
257
  // findUpvalue's exact-identity search over the live call stack, the
227
258
  // same identity CallStackFrame::declNode is stamped with), `slot` is
228
- // that declaration's own slot number for the captured variable.
259
+ // that declaration's own slot number for the captured variable. `name`
260
+ // is that same variable's source name -- unused by Op::LoadUpvalue
261
+ // itself, needed by Op::MakeClosure's ClosureSite (below), which builds
262
+ // a NAME-keyed captured environment (Closure::capturedLet is a
263
+ // TrailView<Value>, looked up by name at invocation time, exactly like
264
+ // every other escaping closure) rather than a slot-indexed one.
229
265
  struct UpvalueRef {
230
266
  const oscad::ASTNode* targetDecl = nullptr;
231
267
  int slot = 0;
268
+ std::string name;
269
+ };
270
+
271
+ // One FunctionLiteral's own capture list for Op::MakeClosure, computed
272
+ // at compile time (bytecode_compiler.cpp's FunctionLiteral case) as the
273
+ // TRANSITIVE union of: every free variable `node` itself references
274
+ // from outside its own body, PLUS -- for each FunctionLiteral nested
275
+ // anywhere within `node` (however deep) -- whatever THAT nested
276
+ // literal's own ClosureSite still needed from beyond `node`'s own scope
277
+ // (i.e. any capture whose resolution target isn't `node` itself). This
278
+ // "bubbling" is what makes a closure nested inside another compiled-
279
+ // creating closure resolve correctly even though the inner one's body
280
+ // is never itself compiled (see this struct's own body-compilation
281
+ // note below): by the time a real, running CompiledChunk executes
282
+ // Op::MakeClosure for `node`, every one of `captures`'s entries is
283
+ // guaranteed to resolve against THAT SAME running chunk's own `slots`
284
+ // array, regardless of how many literal-within-literal levels `node`
285
+ // was originally nested through in the source.
286
+ //
287
+ // `node`'s own BODY is deliberately never itself compiled/registered
288
+ // once it has any capture need (even an empty one bubbled up from
289
+ // something it contains) -- it always runs via the ordinary AST
290
+ // interpreter when invoked (Evaluator::lookupCompiledLiteralChunk finds
291
+ // no cache entry for it), using Closure::capturedLet exactly like any
292
+ // other escaping closure the interpreter creates directly. This keeps
293
+ // the whole feature to "make closure CREATION safe to compile"; a
294
+ // closure's own body additionally getting compiled too (its captures
295
+ // would need to read from this snapshot instead of a live-stack walk)
296
+ // is a natural follow-on, not attempted here.
297
+ struct ClosureSite {
298
+ const oscad::FunctionLiteral* node = nullptr;
299
+ std::vector<UpvalueRef> captures;
232
300
  };
233
301
 
234
302
  // One echo() expression form's own argument names, in source order --
@@ -246,6 +314,7 @@ struct CompiledChunk {
246
314
  std::vector<std::string> names;
247
315
  std::vector<CallSite> callSites;
248
316
  std::vector<UpvalueRef> upvalues;
317
+ std::vector<ClosureSite> closureSites;
249
318
  std::vector<EchoSite> echoSites;
250
319
  int numSlots = 0;
251
320
  // Count of distinct ListCompFor-assignment iterLists allocated across
@@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build"
4
4
 
5
5
  [project]
6
6
  name = "openscad_cpp_evaluator"
7
- version = "0.5.1"
7
+ version = "0.6.1"
8
8
  description = "C++ OpenSCAD evaluator with Python bindings"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -40,6 +40,33 @@ bool isListCompClauseKind(oscad::NodeKind kind) {
40
40
  }
41
41
  }
42
42
 
43
+ // Merges `site`'s own captures into `out` for every entry that escapes
44
+ // `owner` itself (targetDecl != owner) -- see ClosureSite's own doc comment
45
+ // (bytecode.hpp) for why this "bubbling" is necessary: a literal nested
46
+ // inside `owner` may need something from further out than `owner`'s own
47
+ // scope, and since that inner literal's body is never itself compiled
48
+ // (only ever invoked via the interpreter once it has any capture need),
49
+ // `owner`'s OWN Op::MakeClosure must snapshot that name too -- otherwise
50
+ // the inner literal's interpreter-resolved capturedLet ancestry, rooted at
51
+ // whatever `owner` itself captured, would simply never contain it.
52
+ // Dedupes by (targetDecl, slot): the same enclosing binding reached through
53
+ // two different nested literals (or the same literal referencing it twice)
54
+ // is still one capture to make.
55
+ void bubbleEscapingCaptures(const CompiledChunk::ClosureSite& site, const oscad::ASTNode* owner,
56
+ std::vector<CompiledChunk::UpvalueRef>& out) {
57
+ for (const auto& cap : site.captures) {
58
+ if (cap.targetDecl == owner) continue;
59
+ bool already = false;
60
+ for (const auto& existing : out) {
61
+ if (existing.targetDecl == cap.targetDecl && existing.slot == cap.slot) {
62
+ already = true;
63
+ break;
64
+ }
65
+ }
66
+ if (!already) out.push_back(cap);
67
+ }
68
+ }
69
+
43
70
  // Compile-time name -> slot resolution, mirroring the LetOp/nested-scope
44
71
  // shadowing rules callCtx()/letChildCtx() apply at runtime: one frame per
45
72
  // LetOp (pushed/popped around its own assignments+body), innermost frame
@@ -149,7 +176,7 @@ public:
149
176
  // naturally resolve to the innermost active binding too.
150
177
  std::optional<CompiledChunk::UpvalueRef> resolveEnclosing(const std::string& name) const {
151
178
  for (auto it = enclosing_.rbegin(); it != enclosing_.rend(); ++it) {
152
- if (auto slot = it->scope->resolve(name)) return CompiledChunk::UpvalueRef{it->decl, *slot};
179
+ if (auto slot = it->scope->resolve(name)) return CompiledChunk::UpvalueRef{it->decl, *slot, name};
153
180
  }
154
181
  return std::nullopt;
155
182
  }
@@ -253,20 +280,14 @@ public:
253
280
  // other point where its own free-variable references could be
254
281
  // resolved against this enclosing chain (AST nodes have no
255
282
  // parent pointers to discover it from later, see this file's
256
- // own module notes). If the literal itself fails to compile
257
- // (contains a call/echo/assert/unsupported construct, or its
258
- // own free-variable resolution needs an upvalue chain going
259
- // even deeper than what's captured here), the WHOLE containing
260
- // declaration must also bail -- propagated by simply
261
- // rethrowing, since an interpreter-executed literal nested
262
- // inside a compiled container would silently read stale/undef
263
- // values for the container's own (slot-only, never written to
264
- // ctx.let_) locals otherwise. On success, the literal's own
265
- // chunk is stashed in chunk_.nestedLiterals for
266
- // Evaluator::lookupOrCompileChunk to flatten into
267
- // literalChunkCache_; the emitted instruction just pushes the
268
- // AST pointer as a Value, exactly like the interpreter's own
269
- // `Value{&node}` (evalExpr's own FunctionLiteral case).
283
+ // own module notes). If the literal itself fails to compile as
284
+ // bytecode at all (contains an echo/assert/unsupported
285
+ // construct), the WHOLE containing declaration must also bail
286
+ // -- propagated by simply rethrowing -- since there's no lighter
287
+ // way left to find out what it captures (see
288
+ // bubbleEscapingCaptures' own doc comment for why a literal
289
+ // that DOES compile as bytecode no longer needs this bail just
290
+ // for referencing enclosing state).
270
291
  case NodeKind::FunctionLiteral: {
271
292
  auto& n = static_cast<const oscad::FunctionLiteral&>(node);
272
293
  std::vector<EnclosingLevel> childEnclosing = enclosing_;
@@ -276,33 +297,44 @@ public:
276
297
  *n.body)) {
277
298
  throw NotCompilable{};
278
299
  }
279
- // A nested literal that reads ANY enclosing variable
280
- // (literalChunk.upvalues non-empty) is compiled via
281
- // Op::LoadUpvalue -- a live-call-stack walk (findUpvalue)
282
- // that only ever resolves a still-active enclosing call,
283
- // never a captured environment (see LoadUpvalue/
284
- // findUpvalue's own doc comments: "this codebase has no
285
- // escaping closures"). A closure that escapes its creating
286
- // call (returned, stored, passed on -- see Closure's own
287
- // doc comment, value.hpp, for the motivating BOSL2
288
- // example) would silently read undef for every captured
289
- // variable through this path. Bailing the WHOLE containing
290
- // compilation here forces such a function to run through
291
- // the interpreter instead, which DOES support escaping
292
- // closures correctly (evalExpr's FunctionLiteral case
293
- // captures ctx.let_ itself). A literal with no upvalues at
294
- // all (doesn't reference anything from an enclosing scope)
295
- // has nothing that needs escaping-capture support, so it's
296
- // unaffected and keeps compiling normally.
297
- if (!literalChunk.upvalues.empty()) throw NotCompilable{};
298
- chunk_.nestedLiterals.emplace_back(&n, std::move(literalChunk));
299
- // No captured `let_` trail here (nullptr) -- a compile-time
300
- // constant can't carry per-invocation runtime state, but
301
- // (per the upvalues check just above) this literal doesn't
302
- // reference anything outside itself, so there is nothing
303
- // capture. Not a regression; just not (yet) extended here.
304
- out.push_back({Op::PushConst, internConst(Value{std::make_shared<const Closure>(Closure{&n, nullptr})}),
305
- 0, nullptr});
300
+ // Effective capture set: `n`'s own direct free-variable
301
+ // references, plus -- transitively -- whatever any literal
302
+ // nested inside `n` still needs from beyond `n`'s own scope
303
+ // (see bubbleEscapingCaptures' own doc comment, above, and
304
+ // ClosureSite's, bytecode.hpp).
305
+ std::vector<CompiledChunk::UpvalueRef> effectiveCaptures = literalChunk.upvalues;
306
+ for (const auto& nestedSite : literalChunk.closureSites) {
307
+ bubbleEscapingCaptures(nestedSite, &n, effectiveCaptures);
308
+ }
309
+ if (effectiveCaptures.empty()) {
310
+ // Closes over nothing at all, even transitively -- the
311
+ // existing fast path: a single frozen constant works
312
+ // for every invocation, and this literal's own compiled
313
+ // chunk (possibly containing its OWN MakeClosure sites
314
+ // for anything nested inside IT that closes only over
315
+ // ITS OWN scope) is genuinely usable, registered as
316
+ // before.
317
+ chunk_.nestedLiterals.emplace_back(&n, std::move(literalChunk));
318
+ out.push_back(
319
+ {Op::PushConst, internConst(Value{std::make_shared<const Closure>(Closure{&n, nullptr})}), 0,
320
+ nullptr});
321
+ return;
322
+ }
323
+ // Escaping-closure support (Op::MakeClosure, bytecode.hpp).
324
+ // `literalChunk`'s own bytecode is discarded here -- its own
325
+ // upvalue reads would resolve via the live-call-stack walk
326
+ // (Op::LoadUpvalue), which is wrong for a closure that
327
+ // outlives its creator -- so this literal always runs
328
+ // interpreted when invoked instead (Evaluator::
329
+ // lookupCompiledLiteralChunk finds no cache entry for it),
330
+ // using the exact same Closure::capturedLet machinery every
331
+ // other escaping closure the interpreter creates directly
332
+ // already relies on. Only `effectiveCaptures` (the names/
333
+ // slots to snapshot right now) survives from the discarded
334
+ // compile attempt.
335
+ int siteIdx = static_cast<int>(chunk_.closureSites.size());
336
+ chunk_.closureSites.push_back(CompiledChunk::ClosureSite{&n, std::move(effectiveCaptures)});
337
+ out.push_back({Op::MakeClosure, siteIdx, 0, nullptr});
306
338
  return;
307
339
  }
308
340
  case NodeKind::RangeLiteral: {
@@ -4,6 +4,7 @@
4
4
  #include "openscad_cpp_evaluator/evaluator.hpp"
5
5
  #include "openscad_cpp_evaluator/function_builtins.hpp"
6
6
  #include "openscad_cpp_evaluator/import_builtin.hpp"
7
+ #include "openscad_cpp_evaluator/scope_trail.hpp"
7
8
 
8
9
  #include "openscad_cpp_parser/ast/declarations.hpp"
9
10
 
@@ -132,6 +133,33 @@ Value runChunk(Evaluator& ev, const CompiledChunk& chunk, const std::vector<Inst
132
133
  ++pc;
133
134
  break;
134
135
  }
136
+ case Op::MakeClosure: {
137
+ // Builds a FRESH captured environment every time this
138
+ // instruction actually runs (never a compile-time constant
139
+ // -- a loop creating one closure per iteration must capture
140
+ // THAT iteration's own values, not share one snapshot) by
141
+ // reading each of the site's own captures straight out of
142
+ // THIS frame's own `slots` -- see ClosureSite's own doc
143
+ // comment (bytecode.hpp) for why every capture, however
144
+ // deeply the literal was originally nested, is guaranteed
145
+ // to resolve against this SAME currently-running chunk.
146
+ // The resulting Closure::capturedLet is a real, standalone
147
+ // TrailView (isolated root -- nothing else shares it, and
148
+ // it owes nothing to `ctx.let_`, which a compiled call
149
+ // never populates -- see bindCompiledArgs' own doc
150
+ // comment), so invoking this closure later works through
151
+ // the EXACT same callCtxFor/capturedLetTrail machinery
152
+ // every interpreter-created escaping closure already uses,
153
+ // unchanged.
154
+ const CompiledChunk::ClosureSite& site = chunk.closureSites[static_cast<size_t>(ins.a)];
155
+ auto capturedTrail = TrailView<Value>::makeRoot();
156
+ for (const auto& cap : site.captures) {
157
+ capturedTrail->set(cap.name, slots[static_cast<size_t>(cap.slot)]);
158
+ }
159
+ stack.push_back(Value{std::make_shared<const Closure>(Closure{site.node, std::move(capturedTrail)})});
160
+ ++pc;
161
+ break;
162
+ }
135
163
  case Op::Range: {
136
164
  Value step = std::move(stack.back());
137
165
  stack.pop_back();
@@ -636,10 +664,38 @@ Value runCompiledFunctionFromBound(Evaluator& ev, const CompiledChunk& chunk, co
636
664
  // *stack* growth (what this trampoline exists to eliminate) for heap
637
665
  // growth instead -- O(iteration count) EvalContext objects, bounded by
638
666
  // available RAM rather than a ~few-MB thread stack.
667
+ namespace {
668
+ // Tears a trampoline's own `chain` down back-to-front on EVERY exit path --
669
+ // normal return OR exception unwind (recordTailCallHop's own recursion-guard
670
+ // error is the exact case that matters: an infinite tail-recursive function
671
+ // with no base case throws out of the loop entirely, bypassing any teardown
672
+ // code placed after it). Relying on `chain` simply falling out of scope
673
+ // instead is NOT equivalent: std::vector<T>'s own element-destruction order
674
+ // is unspecified by the standard -- libc++ destroys back-to-front (matching
675
+ // ScopeTrailStorage::popLevel's own "scan from the back" doc comment, which
676
+ // assumes the level being popped is usually the most recently pushed one --
677
+ // O(1) per pop that way), but libstdc++ destroys front-to-back, the
678
+ // adversarial order for that same scan -- each pop then walks almost the
679
+ // entire remaining vector, turning an intended O(N) teardown into a real,
680
+ // measured O(N^2) (see issue #50: an N-scaling experiment plus a minimal
681
+ // repro proving libc++/libstdc++ disagree on std::vector<T>::clear()'s
682
+ // destruction order for identical source). Popping back-to-front explicitly,
683
+ // via a destructor that always runs before `chain`'s own, is correct AND
684
+ // O(N) on every standard library and every exit path, not just the ones
685
+ // that happen to agree with libc++ and return normally.
686
+ struct ChainTeardown {
687
+ std::vector<EvalContext>& chain;
688
+ ~ChainTeardown() {
689
+ while (!chain.empty()) chain.pop_back();
690
+ }
691
+ };
692
+ } // namespace
693
+
639
694
  Value runCompiledFunctionTrampoline(Evaluator& ev, const CompiledChunk& chunk,
640
695
  const std::vector<std::unique_ptr<oscad::Argument>>& arguments,
641
696
  EvalContext& callerCtx, EvalContext& childCtx) {
642
697
  std::vector<EvalContext> chain{childCtx};
698
+ ChainTeardown teardown{chain};
643
699
  TailCallRequest req;
644
700
  Value result = runCompiledFunction(ev, chunk, arguments, callerCtx, chain.back(), &req);
645
701
  unsigned recursionGuard = 0;
@@ -661,6 +717,7 @@ Value runCompiledFunctionTrampoline(Evaluator& ev, const CompiledChunk& chunk,
661
717
  Value runCompiledFunctionFromBoundTrampoline(Evaluator& ev, const CompiledChunk& chunk, const BoundArgs& bound,
662
718
  EvalContext& childCtx) {
663
719
  std::vector<EvalContext> chain{childCtx};
720
+ ChainTeardown teardown{chain};
664
721
  TailCallRequest req;
665
722
  Value result = runCompiledFunctionFromBound(ev, chunk, bound, chain.back(), &req);
666
723
  unsigned recursionGuard = 0;
@@ -443,6 +443,32 @@ Value Evaluator::evalFunctionBodyTrampoline(const oscad::Expression& bodyExpr, E
443
443
  // dropped there) if memory ever measurably matters more than this
444
444
  // simpler all-or-nothing approach.
445
445
  std::vector<EvalContext> chain{ctx};
446
+ // Tears `chain` down back-to-front on EVERY exit path -- normal return
447
+ // OR exception unwind (recordTailCallHop's own recursion-guard error,
448
+ // below, is the exact case that matters: an infinite tail-recursive
449
+ // function with no base case throws out of this loop entirely,
450
+ // bypassing any teardown code placed after it). Relying on `chain`
451
+ // simply falling out of scope instead is NOT equivalent: std::vector<T>'s
452
+ // own element-destruction order is unspecified by the standard --
453
+ // libc++ destroys back-to-front (matching ScopeTrailStorage::popLevel's
454
+ // own "scan from the back" doc comment, which assumes the level being
455
+ // popped is usually the most recently pushed one -- O(1) per pop that
456
+ // way), but libstdc++ destroys front-to-back, the adversarial order for
457
+ // that same scan -- each pop then walks almost the entire remaining
458
+ // vector, turning an intended O(N) teardown into a real, measured
459
+ // O(N^2) (see issue #50: an N-scaling experiment plus a minimal repro
460
+ // proving libc++/libstdc++ disagree on std::vector<T>::clear()'s
461
+ // destruction order for identical source). Popping back-to-front
462
+ // explicitly, via a destructor that always runs before `chain`'s own,
463
+ // is correct AND O(N) on every standard library and every exit path,
464
+ // not just the ones that happen to agree with libc++ and return
465
+ // normally.
466
+ struct ChainTeardown {
467
+ std::vector<EvalContext>& chain;
468
+ ~ChainTeardown() {
469
+ while (!chain.empty()) chain.pop_back();
470
+ }
471
+ } teardown{chain};
446
472
  unsigned recursionGuard = 0;
447
473
  while (true) {
448
474
  auto result = simplifyTailStep(*expr, ctx);
@@ -380,14 +380,27 @@ TEST(BytecodeCompiler, VmOffAndVmOnAgreeOnListComprehensionCases) {
380
380
  }
381
381
 
382
382
  // -- Phase 2: closures/upvalues --------------------------------------------
383
- // A closure has no practical payoff unless something can call it BACK
384
- // while its capturing frame is still active (this codebase has no
385
- // escaping closures) -- the natural, most common shape is calling a
386
- // locally-held function value directly (`g(5)` where `g` is a let/param
387
- // bound to a FunctionLiteral), which needed its own dynamic-dispatch
388
- // opcode (CALL_DYNAMIC) alongside LOAD_UPVALUE to be reachable at all; see
389
- // bytecode_compiler.cpp's PrimaryCall case and bytecode_vm.cpp's
390
- // CallDynamic handler.
383
+ // The natural, most common shape is calling a locally-held function value
384
+ // directly (`g(5)` where `g` is a let/param bound to a FunctionLiteral),
385
+ // which needed its own dynamic-dispatch opcode (CALL_DYNAMIC) alongside
386
+ // LOAD_UPVALUE to be reachable at all; see bytecode_compiler.cpp's
387
+ // PrimaryCall case and bytecode_vm.cpp's CallDynamic handler.
388
+ //
389
+ // Phase 2b (Op::MakeClosure) then made the CONTAINING function compilable
390
+ // even when it creates a closure that escapes (returned, stored, passed on)
391
+ // -- previously ANY FunctionLiteral referencing enclosing state bailed the
392
+ // whole container, unconditionally, since Op::LOAD_UPVALUE's live-call-
393
+ // stack walk (findUpvalue) can only resolve a still-active call frame,
394
+ // never survive past it. Op::MakeClosure instead snapshots the exact
395
+ // values a literal needs into a real TrailView<Value> at the moment of
396
+ // creation (see bytecode.hpp's own doc comment), reusing Closure::
397
+ // capturedLet/callCtxFor's existing capture-rooting unchanged -- so the
398
+ // closure's own BODY still always runs interpreted when invoked (never
399
+ // itself compiled), but the function that CREATES it no longer has to.
400
+ // Several tests below predate Phase 2b and were written to describe the
401
+ // OLD bail-and-fall-back behavior; their own comments now note what
402
+ // actually happens post-Phase-2b instead of describing stale behavior as
403
+ // if it were still current.
391
404
 
392
405
  TEST(BytecodeCompiler, ClosureCapturesParameterAndIsCalledDirectly) {
393
406
  ScopedVm vm(true);
@@ -457,13 +470,12 @@ TEST(BytecodeCompiler, ClosureOverNonActiveEnclosingCallResolvesCorrectly) {
457
470
  // (no escaping-closure support) as if it were the intended contract.
458
471
  // The fix: Value's FunctionLiteral alternative became a real Closure
459
472
  // (node + capturedLet, a shared_ptr<TrailView<Value>> snapshot of
460
- // ctx.let_ at creation time, see value.hpp), and every FunctionLiteral
461
- // whose body reads any enclosing-scope variable (non-empty upvalues)
462
- // is now deliberately kept off the VM's compiled path (thrown as
463
- // NotCompilable in bytecode_compiler.cpp) since Op::LoadUpvalue can
464
- // only resolve a still-live call frame, never a captured environment --
465
- // such a closure always runs through the interpreter, which reads its
466
- // capturedLet directly.
473
+ // ctx.let_ at creation time, see value.hpp). Post-Phase-2b, `outer`
474
+ // itself now compiles too (Op::MakeClosure builds an equivalent
475
+ // snapshot from compiled code, see bytecode.hpp) -- only the returned
476
+ // closure's own body (`function(y) y + n`) still always runs
477
+ // interpreted when invoked, reading its capturedLet directly via the
478
+ // same callCtxFor path an interpreter-created closure always has.
467
479
  EXPECT_EQ(runCapturingEcho("function outer(n) = function(y) y + n;\n"
468
480
  "stored = outer(5);\n"
469
481
  "echo(stored(3));"),
@@ -490,6 +502,100 @@ TEST(BytecodeCompiler, VmOffAndVmOnAgreeOnClosureCases) {
490
502
  EXPECT_EQ(offResult, onResult);
491
503
  }
492
504
 
505
+ TEST(BytecodeCompiler, MakeClosureLetsContainerCompileDespiteNonEscapingClosure) {
506
+ ScopedVm vm(true);
507
+ // `make` creates AND immediately calls `g` -- before Op::MakeClosure
508
+ // this bailed `make` entirely (any non-empty upvalues did, escaping or
509
+ // not). A debug hook alone forces the interpreter regardless of
510
+ // compilability (see DebugAttachedWithoutFastContinueAlwaysInterprets
511
+ // above), so fast-continue must be enabled (an empty breakpoint map --
512
+ // fast-continue "on", nothing set) to actually observe whether `make`
513
+ // runs compiled here.
514
+ const int stops = countDebugHookStops("function make(x) = let(g = function(y) y + x) g(5);\n"
515
+ "echo(make(10));",
516
+ std::unordered_map<std::string, std::set<int>>{});
517
+ EXPECT_EQ(stops, 4);
518
+ }
519
+
520
+ TEST(BytecodeCompiler, MakeClosureLetsContainerCompileDespiteEscapingClosure) {
521
+ ScopedVm vm(true);
522
+ // `outer` returns a closure that escapes past its own call entirely
523
+ // (called later, from a separate top-level statement, see
524
+ // ClosureOverNonActiveEnclosingCallResolvesCorrectly above for the
525
+ // correctness side of this same shape) -- `outer` itself now compiles
526
+ // too (fast-continue enabled for the same reason as the non-escaping
527
+ // case above). The ternary is needed here purely to make compiled-vs-
528
+ // interpreted OBSERVABLE via stop count: a body that's nothing but
529
+ // "return a closure" has no sub-expression checkpoints in the
530
+ // interpreter either way (a FunctionLiteral's own creation isn't itself
531
+ // a checkpoint), so it produces the SAME stop count whether `outer`
532
+ // compiled or not -- confirmed empirically, not assumed, by diffing
533
+ // this test's own result against the pre-Op::MakeClosure compiler.
534
+ const int stops = countDebugHookStops(
535
+ "function outer(n) = n > 0 ? function(y) y + n : function(y) y - n;\n"
536
+ "stored = outer(5);\n"
537
+ "echo(stored(3));",
538
+ std::unordered_map<std::string, std::set<int>>{});
539
+ EXPECT_EQ(stops, 6);
540
+ }
541
+
542
+ TEST(BytecodeCompiler, MakeClosureCapturesFreshValuePerInvocation) {
543
+ ScopedVm vm(true);
544
+ // Op::MakeClosure runs fresh every time the instruction is actually
545
+ // reached (never a compile-time constant) -- a closure built once per
546
+ // list-comprehension iteration must capture THAT iteration's own `i`,
547
+ // not all share whatever `i` ended at. `make_adders(3)` returns
548
+ // [function(y) y+0, function(y) y+1, function(y) y+2]; calling each
549
+ // with 10 and summing proves each closed over its OWN value.
550
+ EXPECT_EQ(runCapturingEcho("function make_adders(n) = [for (i = [0:1:n-1]) function(y) y + i];\n"
551
+ "fns = make_adders(3);\n"
552
+ "echo(fns[0](10) + fns[1](10) + fns[2](10));"),
553
+ "ECHO: 33");
554
+ }
555
+
556
+ TEST(BytecodeCompiler, MakeClosureBubblesCaptureThroughAnIntermediateClosureLevel) {
557
+ ScopedVm vm(true);
558
+ // `inner` (nested inside `mid`, nested inside `outer`) references
559
+ // `outer`'s own `x` DIRECTLY, skipping `mid` entirely -- exercising
560
+ // bubbleEscapingCaptures' own transitive merge (bytecode_compiler.cpp):
561
+ // `mid`'s own ClosureSite must bubble this capture up so `outer`'s own
562
+ // Op::MakeClosure (for `mid`) snapshots `x` too, even though `mid`
563
+ // itself never reads `x`. Same values/shape as
564
+ // NestedClosureCapturesBothEnclosingLevels above, but this time
565
+ // `mid` escapes `outer` (returned, called from a separate statement)
566
+ // rather than being called back immediately -- so `outer` itself must
567
+ // now compile, and the correctness depends on the bubbled capture
568
+ // actually reaching `mid`'s own snapshot correctly.
569
+ const std::string script = "function outer(x) = let(mid = function(y) let(inner = function(z) x + y + z) "
570
+ "inner(1)) mid;\n"
571
+ "stored = outer(100);\n"
572
+ "echo(stored(10));";
573
+ EXPECT_EQ(runCapturingEcho(script), "ECHO: 111");
574
+ }
575
+
576
+ TEST(BytecodeCompiler, ClosureWithDollarParameterStillBailsContainer) {
577
+ ScopedVm vm(true);
578
+ // Residual, deliberate limitation: a FunctionLiteral with its OWN
579
+ // dollar-prefixed parameter still fails compileFunctionLike outright
580
+ // (same $-prefixed-parameter rule as a plain named function, see
581
+ // DollarPrefixedParameterBailsCompilationAndStillWorks above) -- there's
582
+ // no lighter way left to discover what it captures in that case, so the
583
+ // WHOLE containing declaration still bails (throw NotCompilable), same
584
+ // as before Op::MakeClosure. Value must still be correct via the
585
+ // interpreter fallback.
586
+ const int stops = countDebugHookStops(
587
+ "function outer(x) = let(g = function(y, $fn) y + x + $fn) g(5, $fn=2);\n"
588
+ "echo(outer(10));",
589
+ std::nullopt);
590
+ // `outer` never compiles at all here (more than 1 stop for its own
591
+ // call), unlike MakeClosureLetsContainerCompileDespiteNonEscapingClosure
592
+ // above where the analogous ($-free) container compiles to 1.
593
+ EXPECT_GT(stops, 2);
594
+ EXPECT_EQ(runCapturingEcho("function outer(x) = let(g = function(y, $fn) y + x + $fn) g(5, $fn=2);\n"
595
+ "echo(outer(10));"),
596
+ "ECHO: 17");
597
+ }
598
+
493
599
  // -- Tail-call optimization, VM path (Phase B) -----------------------------
494
600
  //
495
601
  // Mirrors tests/test_tail_calls.cpp's own Phase A (interpreter) suite, but
@@ -537,13 +643,17 @@ TEST(BytecodeCompiler, MixedCompiledInterpretedTailChainStaysCorrect) {
537
643
 
538
644
  TEST(BytecodeCompiler, ClosureNestedTailCallUnderVmFallsBackAndStillResolves) {
539
645
  ScopedVm vm(true);
540
- // Same closure-nesting hazard as the interpreter path's own
646
+ // `g` (a closure over `make`'s own `x`) is called in tail position from
647
+ // `make`'s own compiled body -- but `g`'s own body is never itself
648
+ // compiled post-Phase-2b (Op::MakeClosure's whole point is to let
649
+ // `make` compile despite creating `g`; `g` still always runs
650
+ // interpreted when invoked, see Op::MakeClosure's own doc comment,
651
+ // bytecode.hpp). CallDynamicTail's handler therefore finds no compiled
652
+ // chunk for `g` (Evaluator::lookupCompiledLiteralChunk returns nullptr)
653
+ // and falls back to a real evalFunctionLiteralFromBound call instead of
654
+ // trampolining, exactly like the interpreter path's own
541
655
  // ClosureNestedTailCallFallsBackToRealRecursionAndStillResolves
542
- // (test_tail_calls.cpp) -- g's own call is lexically nested inside
543
- // make's body (a closure over `x`, read via Op::LoadUpvalue), so
544
- // isolatedCallCtxFor must return nullopt for it, and CallDynamicTail's
545
- // handler must fall back to a real evalFunctionLiteralFromBound call
546
- // instead of trampolining.
656
+ // (test_tail_calls.cpp).
547
657
  const std::string script = "function make(x) = let(g = function(y) y + x) g(5);\necho(make(10));";
548
658
  EXPECT_EQ(runCapturingEcho(script), "ECHO: 15");
549
659
  }
@@ -19,6 +19,7 @@
19
19
 
20
20
  #include "test_helpers.hpp"
21
21
 
22
+ #include <chrono>
22
23
  #include <gtest/gtest.h>
23
24
 
24
25
  using namespace oscadeval;
@@ -153,6 +154,34 @@ TEST(TailCalls, InfiniteTailRecursionErrorMentionsTheFunctionName) {
153
154
  }
154
155
  }
155
156
 
157
+ TEST(TailCalls, InfiniteTailRecursionHitsTheCapInBoundedWallTime) {
158
+ // Regression tripwire for issue #50: evalFunctionBodyTrampoline's own
159
+ // `chain` (every hop's EvalContext, kept alive for $-var ancestry --
160
+ // see its own doc comment) used to rely on std::vector<EvalContext>'s
161
+ // OWN destructor to tear itself down, whose element-destruction order
162
+ // is unspecified by the standard. libstdc++ (GCC) and, per CI evidence,
163
+ // MSVC destroy front-to-back -- the adversarial order for
164
+ // ScopeTrailStorage::popLevel's "scan from the back" optimization
165
+ // (its own doc comment), turning an intended O(N) teardown into a
166
+ // real, measured O(N^2): the exact same 1,000,000-iteration script
167
+ // below took 25-50+ minutes on GCC/MSVC CI runners before the fix
168
+ // (an explicit back-to-front teardown, immune to the underlying
169
+ // vector's own unspecified order), vs ~1s on Clang/libc++ throughout.
170
+ // ponytail: generous fixed ceiling, not a strict perf target -- trips
171
+ // only on a real regression (this exact O(N^2) reintroduced), not
172
+ // machine noise; raise if a slower CI runner ever needs it.
173
+ ScopedVm vm(false);
174
+ Evaluator ev;
175
+ auto ast = parseSrc("function loop(n) = loop(n + 1);\nresult = loop(0);");
176
+ auto scope = oscad::buildScopes(ast);
177
+ EvalContext ctx = EvalContext::makeRoot(scope.get());
178
+ const auto start = std::chrono::steady_clock::now();
179
+ EXPECT_THROW(ev.resolveTree(ast, ctx), EvalError);
180
+ const double ms = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count();
181
+ constexpr double kCeilingMs = 30000.0;
182
+ EXPECT_LT(ms, kCeilingMs) << "took " << ms << "ms -- possible O(N^2) regression, see issue #50";
183
+ }
184
+
156
185
  TEST(TailCalls, ErrorThrownDeepInATailChainProducesABoundedTrace) {
157
186
  ScopedVm vm(false);
158
187
  Evaluator ev;