openscad-cpp-evaluator 0.43.0__tar.gz → 0.45.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/CLAUDE.md +15 -0
  2. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/CMakeLists.txt +13 -1
  3. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/PKG-INFO +1 -1
  4. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/include/openscad_cpp_evaluator/evaluator.hpp +7 -0
  5. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/pyproject.toml +1 -1
  6. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/src/CMakeLists.txt +8 -0
  7. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/src/builtins/function_builtins.cpp +45 -2
  8. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/src/bytecode_compiler.cpp +22 -10
  9. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/src/bytecode_vm.cpp +2 -0
  10. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/src/eval_context.cpp +28 -0
  11. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/src/expr_eval.cpp +23 -1
  12. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/src/user_calls.cpp +10 -0
  13. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/tests/CMakeLists.txt +1 -0
  14. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/tests/test_bytecode_compiler.cpp +7 -7
  15. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/tests/test_expr_eval.cpp +24 -11
  16. openscad_cpp_evaluator-0.45.0/tests/test_feature_detection.cpp +162 -0
  17. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/tests/test_use.cpp +6 -3
  18. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/.github/workflows/ci.yml +0 -0
  19. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/.github/workflows/wheels.yml +0 -0
  20. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/.gitignore +0 -0
  21. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/.gitmodules +0 -0
  22. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/LICENSE +0 -0
  23. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/README.md +0 -0
  24. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/bindings/CMakeLists.txt +0 -0
  25. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/bindings/ast_to_py.cpp +0 -0
  26. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/bindings/module.cpp +0 -0
  27. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/cmake/embed_font.cmake +0 -0
  28. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/examples/CMakeLists.txt +0 -0
  29. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/examples/eval_perf_benchmark.cpp +0 -0
  30. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/examples/manifold_cache_reuse.cpp +0 -0
  31. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/examples/minimal_debugger.cpp +0 -0
  32. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/.github/workflows/ci.yml +0 -0
  33. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/.gitignore +0 -0
  34. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/CLAUDE.md +0 -0
  35. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/CMakeLists.txt +0 -0
  36. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/README.md +0 -0
  37. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/api.hpp +0 -0
  38. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/ast_node.hpp +0 -0
  39. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/comments.hpp +0 -0
  40. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/declarations.hpp +0 -0
  41. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/expression.hpp +0 -0
  42. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/module_instantiation.hpp +0 -0
  43. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/scope_builder.hpp +0 -0
  44. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/vector_element.hpp +0 -0
  45. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast.hpp +0 -0
  46. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/comments.hpp +0 -0
  47. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/position.hpp +0 -0
  48. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/pretty_print.hpp +0 -0
  49. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/scope.hpp +0 -0
  50. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/serialization.hpp +0 -0
  51. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/source_map.hpp +0 -0
  52. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/src/CMakeLists.txt +0 -0
  53. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/src/api.cpp +0 -0
  54. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/src/ast/ast_node.cpp +0 -0
  55. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/src/ast/declarations.cpp +0 -0
  56. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/src/ast/expression.cpp +0 -0
  57. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/src/ast/format_utils.hpp +0 -0
  58. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/src/ast/module_instantiation.cpp +0 -0
  59. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/src/ast/scope_builder.cpp +0 -0
  60. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/src/ast/vector_element.cpp +0 -0
  61. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/src/comment_attach_internal.hpp +0 -0
  62. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/src/comments.cpp +0 -0
  63. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/src/grammar/driver.cpp +0 -0
  64. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/src/grammar/driver.hpp +0 -0
  65. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/src/grammar/lexer.l +0 -0
  66. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/src/grammar/lexer_api.hpp +0 -0
  67. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/src/grammar/oscad_location.hpp +0 -0
  68. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/src/grammar/parser.y +0 -0
  69. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/src/inline_comment_attach.cpp +0 -0
  70. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/src/position.cpp +0 -0
  71. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/src/pretty_print.cpp +0 -0
  72. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/src/scope.cpp +0 -0
  73. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/src/serialization/json_io.cpp +0 -0
  74. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/src/serialization/yaml_io.cpp +0 -0
  75. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/src/source_map.cpp +0 -0
  76. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/tests/CMakeLists.txt +0 -0
  77. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/tests/test_assignments.cpp +0 -0
  78. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/tests/test_ast_generation.cpp +0 -0
  79. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/tests/test_comments_and_files.cpp +0 -0
  80. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/tests/test_const_borrow.cpp +0 -0
  81. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/tests/test_control.cpp +0 -0
  82. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/tests/test_expressions.cpp +0 -0
  83. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/tests/test_functions.cpp +0 -0
  84. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/tests/test_helpers.hpp +0 -0
  85. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/tests/test_inline_comments.cpp +0 -0
  86. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/tests/test_lexical.cpp +0 -0
  87. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/tests/test_modules.cpp +0 -0
  88. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/tests/test_node_str.cpp +0 -0
  89. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/tests/test_pretty_print.cpp +0 -0
  90. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/tests/test_render_expression.cpp +0 -0
  91. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/tests/test_scope.cpp +0 -0
  92. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/tests/test_serialization.cpp +0 -0
  93. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/tests/test_smoke.cpp +0 -0
  94. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/tests/test_source_map.cpp +0 -0
  95. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/tests/test_use_include.cpp +0 -0
  96. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/tests/test_vectors.cpp +0 -0
  97. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/tools/cli/CMakeLists.txt +0 -0
  98. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/external/openscad_cpp_parser/tools/cli/main.cpp +0 -0
  99. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/include/openscad_cpp_evaluator/bound_args.hpp +0 -0
  100. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/include/openscad_cpp_evaluator/bytecode.hpp +0 -0
  101. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/include/openscad_cpp_evaluator/bytecode_compiler.hpp +0 -0
  102. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/include/openscad_cpp_evaluator/bytecode_vm.hpp +0 -0
  103. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/include/openscad_cpp_evaluator/call_args.hpp +0 -0
  104. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/include/openscad_cpp_evaluator/colored_body.hpp +0 -0
  105. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/include/openscad_cpp_evaluator/csg_node.hpp +0 -0
  106. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/include/openscad_cpp_evaluator/css_colors.hpp +0 -0
  107. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/include/openscad_cpp_evaluator/debug_hooks.hpp +0 -0
  108. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/include/openscad_cpp_evaluator/debug_repl.hpp +0 -0
  109. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/include/openscad_cpp_evaluator/dispatch.hpp +0 -0
  110. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/include/openscad_cpp_evaluator/dxf_svg_import.hpp +0 -0
  111. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/include/openscad_cpp_evaluator/eval_context.hpp +0 -0
  112. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/include/openscad_cpp_evaluator/eval_error.hpp +0 -0
  113. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/include/openscad_cpp_evaluator/eval_use.hpp +0 -0
  114. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/include/openscad_cpp_evaluator/export.hpp +0 -0
  115. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/include/openscad_cpp_evaluator/font_match.hpp +0 -0
  116. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/include/openscad_cpp_evaluator/font_provider.hpp +0 -0
  117. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/include/openscad_cpp_evaluator/freetype_font_provider.hpp +0 -0
  118. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/include/openscad_cpp_evaluator/function_builtins.hpp +0 -0
  119. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/include/openscad_cpp_evaluator/import_builtin.hpp +0 -0
  120. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/include/openscad_cpp_evaluator/manifold_cache.hpp +0 -0
  121. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/include/openscad_cpp_evaluator/mesh_check.hpp +0 -0
  122. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/include/openscad_cpp_evaluator/mesh_import.hpp +0 -0
  123. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/include/openscad_cpp_evaluator/osc_range.hpp +0 -0
  124. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/include/openscad_cpp_evaluator/profile.hpp +0 -0
  125. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/include/openscad_cpp_evaluator/scope_trail.hpp +0 -0
  126. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/include/openscad_cpp_evaluator/segments.hpp +0 -0
  127. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/include/openscad_cpp_evaluator/surface_load.hpp +0 -0
  128. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/include/openscad_cpp_evaluator/text_metrics.hpp +0 -0
  129. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/include/openscad_cpp_evaluator/value.hpp +0 -0
  130. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/include/openscad_cpp_evaluator/zip_stored.hpp +0 -0
  131. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/python/openscad_cpp_evaluator/__init__.py +0 -0
  132. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/resources/fonts/LICENSE +0 -0
  133. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/resources/fonts/LiberationSans-Regular.ttf +0 -0
  134. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/src/builtins/booleans.cpp +0 -0
  135. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/src/builtins/builtins.hpp +0 -0
  136. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/src/builtins/call_args.cpp +0 -0
  137. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/src/builtins/color.cpp +0 -0
  138. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/src/builtins/control.cpp +0 -0
  139. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/src/builtins/dxf_dim.cpp +0 -0
  140. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/src/builtins/extrude.cpp +0 -0
  141. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/src/builtins/import.cpp +0 -0
  142. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/src/builtins/modifiers.cpp +0 -0
  143. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/src/builtins/primitives_2d.cpp +0 -0
  144. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/src/builtins/primitives_3d.cpp +0 -0
  145. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/src/builtins/registry.cpp +0 -0
  146. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/src/builtins/roof.cpp +0 -0
  147. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/src/builtins/segments.cpp +0 -0
  148. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/src/builtins/surface.cpp +0 -0
  149. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/src/builtins/text.cpp +0 -0
  150. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/src/builtins/topology.cpp +0 -0
  151. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/src/builtins/transforms.cpp +0 -0
  152. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/src/colored_body.cpp +0 -0
  153. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/src/csg_generate.cpp +0 -0
  154. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/src/csg_resolve.cpp +0 -0
  155. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/src/css_colors.cpp +0 -0
  156. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/src/debug/debug_repl.cpp +0 -0
  157. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/src/debug_profile.cpp +0 -0
  158. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/src/eval_error.cpp +0 -0
  159. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/src/eval_use.cpp +0 -0
  160. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/src/export.cpp +0 -0
  161. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/src/import/dxf_import.cpp +0 -0
  162. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/src/import/mesh_import.cpp +0 -0
  163. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/src/import/surface_load.cpp +0 -0
  164. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/src/import/svg_import.cpp +0 -0
  165. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/src/manifold_cache.cpp +0 -0
  166. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/src/measure_geometry.cpp +0 -0
  167. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/src/mesh_check.cpp +0 -0
  168. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/src/stmt_eval.cpp +0 -0
  169. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/src/text/font_match.cpp +0 -0
  170. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/src/text/freetype_font_provider.cpp +0 -0
  171. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/src/text/text_metrics.cpp +0 -0
  172. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/src/value.cpp +0 -0
  173. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/src/zip_stored.cpp +0 -0
  174. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/tests/test_booleans.cpp +0 -0
  175. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/tests/test_children_separate.cpp +0 -0
  176. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/tests/test_cli.cpp +0 -0
  177. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/tests/test_control_flow.cpp +0 -0
  178. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/tests/test_csg_tree.cpp +0 -0
  179. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/tests/test_debug_hooks.cpp +0 -0
  180. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/tests/test_dxf_svg_import.cpp +0 -0
  181. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/tests/test_eval_context.cpp +0 -0
  182. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/tests/test_export.cpp +0 -0
  183. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/tests/test_extrude_roof.cpp +0 -0
  184. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/tests/test_function_builtins.cpp +0 -0
  185. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/tests/test_helpers.hpp +0 -0
  186. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/tests/test_import_export.cpp +0 -0
  187. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/tests/test_manifold_cache.cpp +0 -0
  188. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/tests/test_mesh_check.cpp +0 -0
  189. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/tests/test_multi_color_merge.cpp +0 -0
  190. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/tests/test_primitives.cpp +0 -0
  191. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/tests/test_profiling.cpp +0 -0
  192. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/tests/test_python_bindings.py +0 -0
  193. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/tests/test_render_expr.cpp +0 -0
  194. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/tests/test_scope_trail.cpp +0 -0
  195. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/tests/test_scoping.cpp +0 -0
  196. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/tests/test_surface.cpp +0 -0
  197. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/tests/test_tail_calls.cpp +0 -0
  198. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/tests/test_text.cpp +0 -0
  199. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/tests/test_transforms.cpp +0 -0
  200. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/tests/test_unexpected_args.cpp +0 -0
  201. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/tests/test_value.cpp +0 -0
  202. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/tests/test_viewport_params.cpp +0 -0
  203. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/tests/test_zip_stored.cpp +0 -0
  204. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/tools/cli/CMakeLists.txt +0 -0
  205. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/tools/cli/cli_lib.cpp +0 -0
  206. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/tools/cli/cli_lib.hpp +0 -0
  207. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/tools/cli/main.cpp +0 -0
  208. {openscad_cpp_evaluator-0.43.0 → openscad_cpp_evaluator-0.45.0}/uv.lock +0 -0
@@ -441,6 +441,21 @@ handling in arithmetic, the closure-detection rule for `call_ctx` vs `child_ctx`
441
441
  verbatim port, not an independent design. Deliberate divergences are called out in code comments —
442
442
  grep for `ponytail:`.
443
443
 
444
+ - **Feature detection** — `$_SUPPORTED_FEATURE` (just `true`; names the CAPABILITY rather than the
445
+ vendor, so any evaluator adding `supported_feature()` is meant to set it, and checking it first
446
+ solves the bootstrapping problem of calling a function you don't know exists. Written
447
+ `!is_undef($_SUPPORTED_FEATURE) && supported_feature(...)` the guard is **silent** in OpenSCAD --
448
+ `is_undef()` is the one way to read an unknown variable there without a warning, and `&&`
449
+ short-circuits past the unknown call; verified against the reference), `$_BELFRYSCAD` (seeded in `EvalContext::makeRoot`, `src/eval_context.cpp`, as
450
+ `[major, minor, patch]` from `OSCAD_EVAL_VERSION_*` compile definitions that the top-level
451
+ `CMakeLists.txt` parses out of `pyproject.toml`, so wheel and local builds cannot disagree and a
452
+ bump needs one edit) and `supported_feature("name")` (`featureLevels()` in
453
+ `src/builtins/function_builtins.cpp`, returning a LEVEL so a feature whose semantics change can
454
+ be told from its earlier self; 0 for unknown names and non-string arguments, deliberately
455
+ silent so probing for a future feature is safe). Both exist because OpenSCAD **silently ignores**
456
+ unknown arguments -- `children(separate=true)` renders the wrong shape there with no warning --
457
+ and both read as `undef` in OpenSCAD, so a guard written against them works in both.
458
+
444
459
  - `include/openscad_cpp_evaluator/value.hpp`, `src/value.cpp` — the `Value` variant (OpenSCAD's
445
460
  dynamic value type) and its free-function arithmetic/comparison helpers. See the plan's §1 for
446
461
  the type-mapping table and why there's no numpy-equivalent library.
@@ -1,5 +1,17 @@
1
1
  cmake_minimum_required(VERSION 3.24)
2
- project(openscad_cpp_evaluator CXX)
2
+
3
+ # Read the version straight out of pyproject.toml rather than repeating it
4
+ # here. It reaches C++ as OSCAD_EVAL_VERSION_{MAJOR,MINOR,PATCH} (see
5
+ # src/CMakeLists.txt), which is what seeds $_BELFRYSCAD -- so a released
6
+ # wheel and a local `cmake --build` always report the same numbers, and a
7
+ # version bump needs one edit, not three.
8
+ file(READ "${CMAKE_CURRENT_SOURCE_DIR}/pyproject.toml" _oscad_pyproject)
9
+ if(NOT _oscad_pyproject MATCHES "\nversion = \"([0-9]+)\.([0-9]+)\.([0-9]+)\"")
10
+ message(FATAL_ERROR "could not read version = \"X.Y.Z\" from pyproject.toml")
11
+ endif()
12
+ set(OSCAD_EVAL_VERSION "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}")
13
+
14
+ project(openscad_cpp_evaluator VERSION ${OSCAD_EVAL_VERSION} LANGUAGES CXX)
3
15
 
4
16
  # On Apple Silicon, warn (don't fail -- a universal or intentionally-x86_64
5
17
  # build is legitimate) if the effective target architecture is x86_64 while
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openscad_cpp_evaluator
3
- Version: 0.43.0
3
+ Version: 0.45.0
4
4
  Summary: C++ OpenSCAD evaluator with Python bindings
5
5
  Author: NeuSCAD Contributors
6
6
  License-Expression: MIT
@@ -372,6 +372,13 @@ public:
372
372
  // One synthetic `children(index)` statement, owned by syntheticNodes_.
373
373
  const oscad::ASTNode* makeChildrenIndexCall(const oscad::ModularCall& origin, size_t index);
374
374
 
375
+ // The sole argument of `is_undef(x)` when x is a bare identifier, else
376
+ // nullptr. That call is a PROBE -- asking whether a name is defined must
377
+ // not itself complain that it isn't -- so the name is read with
378
+ // warnIfUndef=false. Static and syntactic so the bytecode compiler can
379
+ // ask at compile time and emit LoadFreeNoWarn.
380
+ static const oscad::Identifier* undefProbeTarget(const oscad::PrimaryCall& call);
381
+
375
382
  // Syntactic gate: is this statement a `children(..., separate=true)`?
376
383
  // Cheap and side-effect-free, so the bytecode compiler can ask it at
377
384
  // COMPILE time to decide which opcode to emit, long before the flag's
@@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build"
4
4
 
5
5
  [project]
6
6
  name = "openscad_cpp_evaluator"
7
- version = "0.43.0"
7
+ version = "0.45.0"
8
8
  description = "C++ OpenSCAD evaluator with Python bindings"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -46,6 +46,14 @@ add_library(openscad_cpp_evaluator STATIC
46
46
  builtins/surface.cpp
47
47
  )
48
48
 
49
+ # Seeds $_BELFRYSCAD (eval_context.cpp). Parsed from pyproject.toml by the
50
+ # top-level CMakeLists so there is one source of truth for the version.
51
+ target_compile_definitions(openscad_cpp_evaluator PRIVATE
52
+ OSCAD_EVAL_VERSION_MAJOR=${PROJECT_VERSION_MAJOR}
53
+ OSCAD_EVAL_VERSION_MINOR=${PROJECT_VERSION_MINOR}
54
+ OSCAD_EVAL_VERSION_PATCH=${PROJECT_VERSION_PATCH}
55
+ )
56
+
49
57
  target_include_directories(openscad_cpp_evaluator PUBLIC
50
58
  $<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/include>
51
59
  )
@@ -449,7 +449,7 @@ bool isBuiltinFunctionName(const std::string& name) {
449
449
  "acos", "atan", "atan2", "max", "min", "pow", "norm", "cross", "rands", "concat", "len", "str",
450
450
  "chr", "ord", "is_undef", "is_num", "is_bool", "is_string", "is_list", "is_function", "is_object",
451
451
  "search", "lookup", "has_key", "version", "version_num", "parent_module",
452
- "object", "textmetrics", "fontmetrics", "dxf_dim", "dxf_cross",
452
+ "object", "textmetrics", "fontmetrics", "dxf_dim", "dxf_cross", "supported_feature",
453
453
  };
454
454
  return names.count(name) > 0;
455
455
  }
@@ -591,9 +591,39 @@ enum class BuiltinFnId {
591
591
  TextMetrics, FontMetrics, Abs, Sign, Ceil, Floor, Round, Sqrt, Ln, Log, Exp, Sin, Cos, Tan,
592
592
  Asin, Acos, Atan, Atan2, Max, Min, Pow, Norm, Cross, Rands, Concat, Len, Str, Chr, Ord,
593
593
  IsUndef, IsNum, IsBool, IsString, IsList, IsFunction, IsObject, Search, Lookup, HasKey,
594
- Version, VersionNum, ParentModule, DxfDim, DxfCross,
594
+ Version, VersionNum, ParentModule, DxfDim, DxfCross, SupportedFeature,
595
595
  };
596
596
 
597
+ // supported_feature("name") -> the level at which this build implements that
598
+ // feature, or 0 for one it does not implement (and for a name it has never
599
+ // heard of, which is deliberate: probing for a feature from a future release
600
+ // is supposed to be safe).
601
+ //
602
+ // A LEVEL rather than a boolean so a feature whose semantics change later
603
+ // can be told apart from its earlier self. Everything here is 1 today;
604
+ // nothing has changed since this function existed, and a script cannot
605
+ // observe what a build without supported_feature() did anyway.
606
+ //
607
+ // This is the answer to a real hazard: OpenSCAD does not reject arguments or
608
+ // names it doesn't know -- children(separate=true) is silently ignored there
609
+ // -- so a script using an extension runs and quietly renders something else.
610
+ // Guarding on supported_feature() is how a script says so out loud.
611
+ const std::unordered_map<std::string, double>& featureLevels() {
612
+ static const std::unordered_map<std::string, double> levels = {
613
+ {"render-expr", 1.0}, // render() in expression position
614
+ {"polyhedron-vnf", 1.0}, // polyhedron(vnf) / polyhedron(object)
615
+ {"separate-children", 1.0}, // children(..., separate=true)
616
+ {"minkowski-diff", 1.0}, // minkowski_difference()
617
+ {"sphere-styles", 1.0}, // sphere(style=)
618
+ {"export-name", 1.0}, // $export_name
619
+ {"simplify-op", 1.0}, // simplify()
620
+ {"expr-import", 1.0}, // import() in expression position
621
+ {"object-function", 1.0}, // object(), unconditional here
622
+ {"roof-op", 1.0}, // roof(), method="voronoi" only
623
+ };
624
+ return levels;
625
+ }
626
+
597
627
  const std::unordered_map<std::string, BuiltinFnId>& builtinFnIds() {
598
628
  static const std::unordered_map<std::string, BuiltinFnId> ids = {
599
629
  {"textmetrics", BuiltinFnId::TextMetrics}, {"fontmetrics", BuiltinFnId::FontMetrics},
@@ -614,6 +644,7 @@ const std::unordered_map<std::string, BuiltinFnId>& builtinFnIds() {
614
644
  {"version", BuiltinFnId::Version}, {"version_num", BuiltinFnId::VersionNum},
615
645
  {"parent_module", BuiltinFnId::ParentModule},
616
646
  {"dxf_dim", BuiltinFnId::DxfDim}, {"dxf_cross", BuiltinFnId::DxfCross},
647
+ {"supported_feature", BuiltinFnId::SupportedFeature},
617
648
  };
618
649
  return ids;
619
650
  }
@@ -694,6 +725,7 @@ const std::unordered_map<int, BuiltinCheck>& builtinChecks() {
694
725
  // warning, so only the too-many case is an arity error.
695
726
  add(BuiltinFnId::ParentModule, {0, 1, "1", {kNum}});
696
727
  add(BuiltinFnId::HasKey, {-1, -1, nullptr, {{TObj, "object"}, {TStr, "string"}}});
728
+ add(BuiltinFnId::SupportedFeature, {1, 1, "1", {}});
697
729
  for (BuiltinFnId id : {BuiltinFnId::IsUndef, BuiltinFnId::IsNum, BuiltinFnId::IsBool,
698
730
  BuiltinFnId::IsString, BuiltinFnId::IsList, BuiltinFnId::IsFunction,
699
731
  BuiltinFnId::IsObject}) {
@@ -1008,6 +1040,17 @@ Value evalBuiltinFunction(Evaluator& ev, const std::string& name, const CallArgs
1008
1040
  // The OpenSCAD release we track. version_num() is that same
1009
1041
  // year/month/day folded as y * 10000 + m * 100 + d, exactly like the
1010
1042
  // reference's own builtin_version_num (builtin_functions.cc).
1043
+ case BuiltinFnId::SupportedFeature: {
1044
+ const Value name = getArg(args, 0, "feature", Value{});
1045
+ const std::string* s = std::get_if<std::string>(&name);
1046
+ // A non-string is 0 rather than an error, same as an unknown
1047
+ // name: the whole point of this function is that asking is
1048
+ // always safe.
1049
+ if (!s) return Value{0.0};
1050
+ const auto& levels = featureLevels();
1051
+ const auto it = levels.find(*s);
1052
+ return Value{it == levels.end() ? 0.0 : it->second};
1053
+ }
1011
1054
  case BuiltinFnId::Version: return numList({2026.0, 1.0, 1.0});
1012
1055
  case BuiltinFnId::VersionNum: {
1013
1056
  // The optional vector argument the reference also accepts: with
@@ -262,7 +262,10 @@ public:
262
262
  void compileIdentifierLoad(const std::string& name, const oscad::Position& pos, std::vector<Instruction>& out,
263
263
  CompileScope& scope, bool warnIfUndef) {
264
264
  if (!name.empty() && name[0] == '$') {
265
- out.push_back({Op::LoadDyn, internName(name), 0, &pos});
265
+ // `b` is the warn flag -- a $-name reached through the
266
+ // is_undef() probe must stay quiet, same as LoadFreeNoWarn does
267
+ // for a plain one.
268
+ out.push_back({Op::LoadDyn, internName(name), warnIfUndef ? 1 : 0, &pos});
266
269
  } else if (auto slot = scope.resolve(name)) {
267
270
  out.push_back({Op::LoadLocal, *slot, 0, &pos});
268
271
  } else if (auto upvalSlot = resolveEnclosing(name)) {
@@ -620,15 +623,24 @@ public:
620
623
  }
621
624
  }
622
625
 
623
- for (const auto& argPtr : n.arguments) {
624
- if (argPtr->kind() == NodeKind::NamedArgument) {
625
- auto& a = static_cast<const oscad::NamedArgument&>(*argPtr);
626
- compileExpr(*a.expr, out, scope); // an argument is never tail
627
- site.argNames.push_back(a.name->name);
628
- } else {
629
- auto& a = static_cast<const oscad::PositionalArgument&>(*argPtr);
630
- compileExpr(*a.expr, out, scope);
631
- site.argNames.push_back(std::nullopt);
626
+ // is_undef(name) probes rather than reads -- the same quiet
627
+ // load the interpreter uses (Evaluator::undefProbeTarget),
628
+ // so the two engines agree on which reads warn.
629
+ if (const oscad::Identifier* undefProbe = Evaluator::undefProbeTarget(n)) {
630
+ compileIdentifierLoad(undefProbe->name, undefProbe->position(), out, scope,
631
+ /*warnIfUndef=*/false);
632
+ site.argNames.push_back(std::nullopt);
633
+ } else {
634
+ for (const auto& argPtr : n.arguments) {
635
+ if (argPtr->kind() == NodeKind::NamedArgument) {
636
+ auto& a = static_cast<const oscad::NamedArgument&>(*argPtr);
637
+ compileExpr(*a.expr, out, scope); // an argument is never tail
638
+ site.argNames.push_back(a.name->name);
639
+ } else {
640
+ auto& a = static_cast<const oscad::PositionalArgument&>(*argPtr);
641
+ compileExpr(*a.expr, out, scope);
642
+ site.argNames.push_back(std::nullopt);
643
+ }
632
644
  }
633
645
  }
634
646
  int siteIdx = static_cast<int>(chunk_.callSites.size());
@@ -562,6 +562,8 @@ Value driveVm(Evaluator& ev, size_t floor) {
562
562
  } else if (const Value* v = ctx.dyn->find(name)) {
563
563
  f.stack.push_back(*v);
564
564
  } else {
565
+ // ins.b = warn flag; see compileIdentifierLoad.
566
+ if (ins.b) ev.warn("Ignoring unknown variable '" + name + "'", ins.pos);
565
567
  f.stack.push_back(Value{});
566
568
  }
567
569
  ++f.pc;
@@ -21,6 +21,34 @@ EvalContext EvalContext::makeRoot(const oscad::Scope* rootScope) {
21
21
  // see undef, take the falsy branch by accident rather than by rule, and
22
22
  // warn about an unknown variable while doing it.
23
23
  ctx.dyn->set("$preview", Value{false});
24
+ // $_BELFRYSCAD -- present only here, so a script can tell which
25
+ // evaluator it is running under. OpenSCAD leaves it undef, which is the
26
+ // whole point: `if ($_BELFRYSCAD == undef)` is the portable test, and it
27
+ // works because an unknown $-variable is undef rather than an error.
28
+ //
29
+ // Shaped like OpenSCAD's own version() -- a [major, minor, patch] list
30
+ // rather than a string -- so comparisons need no string splitting.
31
+ // Which FEATURES exist is a separate question with a better answer:
32
+ // supported_feature() (function_builtins.cpp).
33
+ // $_SUPPORTED_FEATURE -- "supported_feature() is callable here". Set by
34
+ // any implementation that provides the function, not just this one, so a
35
+ // script can check for the FUNCTION rather than for a vendor:
36
+ //
37
+ // if (!is_undef($_SUPPORTED_FEATURE) && supported_feature("sep..."))
38
+ //
39
+ // Solves the bootstrapping problem -- you cannot safely call
40
+ // supported_feature() without first knowing it exists -- and in that
41
+ // exact form it is SILENT in OpenSCAD: is_undef() is the one way to
42
+ // read an unknown variable there without a warning, and && short-
43
+ // circuits so the unknown function is never reached. Verified against
44
+ // the reference binary: no warning, correct branch.
45
+ // If OpenSCAD ever adds supported_feature(), it sets this too and the
46
+ // same script starts working there with no edit.
47
+ ctx.dyn->set("$_SUPPORTED_FEATURE", Value{true});
48
+ ctx.dyn->set("$_BELFRYSCAD",
49
+ Value{std::make_shared<const ValueList>(ValueList{{Value{double{OSCAD_EVAL_VERSION_MAJOR}},
50
+ Value{double{OSCAD_EVAL_VERSION_MINOR}},
51
+ Value{double{OSCAD_EVAL_VERSION_PATCH}}}})});
24
52
  ctx.let_ = TrailView<Value>::makeRoot();
25
53
  ctx.dynPositions = TrailView<const oscad::Position*>::makeRoot();
26
54
  ctx.childrenNodes = std::make_shared<const ChildrenNodeList>();
@@ -107,6 +107,16 @@ void Evaluator::warn(const std::string& message, const oscad::Position* position
107
107
  echoFn_(formatWarning(message, position, callStack_));
108
108
  }
109
109
 
110
+ const oscad::Identifier* Evaluator::undefProbeTarget(const oscad::PrimaryCall& call) {
111
+ if (call.left->kind() != oscad::NodeKind::Identifier) return nullptr;
112
+ if (static_cast<const oscad::Identifier&>(*call.left).name != "is_undef") return nullptr;
113
+ if (call.arguments.size() != 1) return nullptr;
114
+ if (call.arguments[0]->kind() != oscad::NodeKind::PositionalArgument) return nullptr;
115
+ const auto& arg = static_cast<const oscad::PositionalArgument&>(*call.arguments[0]);
116
+ if (arg.expr->kind() != oscad::NodeKind::Identifier) return nullptr;
117
+ return static_cast<const oscad::Identifier*>(arg.expr.get());
118
+ }
119
+
110
120
  Value Evaluator::evalIdentifier(const std::string& name, const oscad::Position* position, EvalContext& ctx,
111
121
  bool warnIfUndef) {
112
122
  if (const Value* v = ctx.let_->find(name)) return *v;
@@ -128,7 +138,19 @@ Value Evaluator::evalIdentifier(const std::string& name, const oscad::Position*
128
138
  // $-name anyway (dynamic variables aren't scope-declarable) and
129
139
  // exists only for plain identifiers.
130
140
  const Value* v = ctx.dyn->find(name);
131
- return v ? *v : Value{};
141
+ if (v) return *v;
142
+ // Warns like any other unknown name. This used to return silently
143
+ // for EVERY $-read, on the strength of BOSL2's get_slop() --
144
+ // `is_undef($slop) ? 0 : $slop` with $slop never assigned -- being
145
+ // silent in the reference. It is, but for two narrower reasons: the
146
+ // probe does not warn about its own argument (undefProbeTarget), and
147
+ // the else branch never runs when is_undef() is true. A plain
148
+ // `echo($nope)` DOES warn there, in every context checked --
149
+ // assignment, function body, module body, comparison. Real BOSL2
150
+ // (gears, threading, rounded_prism, diff/attach, path_sweep) emits
151
+ // none of these, because it guards its own $-reads.
152
+ if (warnIfUndef) warn("Ignoring unknown variable '" + name + "'", position);
153
+ return Value{};
132
154
  }
133
155
 
134
156
  if (name == "PI") return Value{std::numbers::pi};
@@ -882,6 +882,16 @@ Value Evaluator::evalFunctionCall(const oscad::PrimaryCall& node, EvalContext& c
882
882
  // exactly (name in _BUILTIN_FN_NAMES gates which branch runs at
883
883
  // all).
884
884
  if (isBuiltinFunctionName(leftId->name)) {
885
+ // is_undef(name) is a probe: asking whether a name is defined
886
+ // must not itself warn that it isn't. The $-branch of
887
+ // evalIdentifier is already silent for any name, but a PLAIN
888
+ // identifier warned -- so `is_undef(some_flag)` was noisy here
889
+ // and silent in the reference. Verified against OpenSCAD:
890
+ // no warning for either spelling inside is_undef().
891
+ if (const oscad::Identifier* probe = undefProbeTarget(node)) {
892
+ return Value{std::holds_alternative<std::monostate>(
893
+ evalIdentifier(probe->name, &probe->position(), ctx, /*warnIfUndef=*/false))};
894
+ }
885
895
  // object() needs the raw argument list, not a pre-resolved
886
896
  // CallArgs, to merge positional/named arguments in their exact
887
897
  // call-site interleaved order (see builtinObject's own
@@ -21,6 +21,7 @@ add_executable(oscad_eval_tests
21
21
  test_render_expr.cpp
22
22
  test_booleans.cpp
23
23
  test_children_separate.cpp
24
+ test_feature_detection.cpp
24
25
  test_control_flow.cpp
25
26
  test_tail_calls.cpp
26
27
  test_function_builtins.cpp
@@ -162,13 +162,13 @@ TEST(BytecodeCompiler, SelfReferentialParameterDefaultResolvesToUndef) {
162
162
  ScopedVm vm(true);
163
163
  // Referencing "x" inside its own default falls all the way through to
164
164
  // Scope::lookupVariable finding nothing usable there (a
165
- // ParameterDeclaration), producing the same "Ignoring unknown
166
- // variable" WARNING the plain interpreter does today (verified against
167
- // the unmodified CLI directly, not assumed) -- this test's own point is
168
- // that the compiled path reproduces that exactly, warning included, not
169
- // that the warning is otherwise desirable.
170
- EXPECT_EQ(runCapturingEcho("function foo(x = is_undef(x) ? 5 : x) = x;\necho(foo());"),
171
- "WARNING: Ignoring unknown variable 'x' in file <string>, line 1\nECHO: 5");
165
+ // ParameterDeclaration) -- undef, so the ternary takes the 5. Silent:
166
+ // is_undef() is a probe and does not warn about the name it asks about,
167
+ // matching the reference, which is silent for this exact script. This
168
+ // test's own point is that the compiled path reproduces the interpreter
169
+ // exactly; it used to assert a warning both engines emitted and the
170
+ // reference did not.
171
+ EXPECT_EQ(runCapturingEcho("function foo(x = is_undef(x) ? 5 : x) = x;\necho(foo());"), "ECHO: 5");
172
172
  // Caller-supplied value: default expression never runs at all, so no
173
173
  // warning either.
174
174
  EXPECT_EQ(runCapturingEcho("function foo(x = is_undef(x) ? 5 : x) = x;\necho(foo(10));"), "ECHO: 10");
@@ -52,21 +52,34 @@ TEST(ExprEvalIdentifiers, UnknownVariableWarnsAndIsUndef) {
52
52
  EXPECT_NE(lastWarning.find("Ignoring unknown variable 'nope'"), std::string::npos);
53
53
  }
54
54
 
55
- TEST(ExprEvalIdentifiers, UndefinedDollarVariableIsUndefWithNoWarning) {
56
- // Found via BOSL2's constants.scad: `function get_slop() = is_undef($slop)
57
- // ? 0 : $slop;` with $slop never assigned anywhere in the script. Real
58
- // OpenSCAD.app produces no warning at all -- $-prefixed names are a
59
- // separate, always-implicitly-available namespace, never requiring a
60
- // scope declaration the way a bare identifier does. This port's
61
- // bytecode VM (Op::LoadDyn, bytecode_vm.cpp) already got this right by
62
- // construction; the plain interpreter incorrectly fell through to the
63
- // same "Ignoring unknown variable" path a genuinely undeclared bare
64
- // identifier hits.
55
+ TEST(ExprEvalIdentifiers, UndefinedDollarVariableIsUndefAndWarns) {
56
+ // A never-assigned $-name reads as undef and warns, exactly like a
57
+ // never-declared bare identifier.
58
+ //
59
+ // This test used to assert the opposite, on the strength of BOSL2's
60
+ // constants.scad -- `function get_slop() = is_undef($slop) ? 0 : $slop;`
61
+ // with $slop never assigned -- being silent in real OpenSCAD. It is,
62
+ // but not because $-reads are exempt: is_undef() does not warn about the
63
+ // name it probes, and the else branch never runs while is_undef() is
64
+ // true. Measured against the reference, a PLAIN read warns in every
65
+ // context tried -- echo, assignment, function body, module body,
66
+ // comparison -- so the old blanket exemption was over-generalised from
67
+ // one guarded expression.
65
68
  std::string lastWarning;
66
69
  Evaluator ev([&](const std::string& msg) { lastWarning = msg; });
67
70
  Value v = evalSrc("$slop", ev);
68
71
  EXPECT_TRUE(std::holds_alternative<std::monostate>(v));
69
- EXPECT_TRUE(lastWarning.empty());
72
+ EXPECT_NE(lastWarning.find("Ignoring unknown variable '$slop'"), std::string::npos) << lastWarning;
73
+ }
74
+
75
+ TEST(ExprEvalIdentifiers, TheBoslGetSlopIdiomStaysSilent) {
76
+ // The case the old exemption existed for, kept as a test in its own
77
+ // right: it must remain silent through the is_undef() probe alone.
78
+ std::string lastWarning;
79
+ Evaluator ev([&](const std::string& msg) { lastWarning = msg; });
80
+ Value v = evalSrc("is_undef($slop) ? 0 : $slop", ev);
81
+ EXPECT_TRUE(lastWarning.empty()) << lastWarning;
82
+ EXPECT_EQ(std::get<double>(v), 0.0);
70
83
  }
71
84
 
72
85
  TEST(ExprEvalIdentifiers, ResolvesViaScopeFallbackWhenNotEagerlyBound) {
@@ -0,0 +1,162 @@
1
+ // $_BELFRYSCAD and supported_feature() -- how a script asks which evaluator
2
+ // it is running under, and what that evaluator can actually do.
3
+ //
4
+ // The hazard they exist for: OpenSCAD does not reject arguments or names it
5
+ // has never heard of. `children(separate=true)` is silently ignored there,
6
+ // so a script using an extension runs and quietly renders something else.
7
+ // Both of these are readable from OpenSCAD too -- an unknown $-variable and
8
+ // an unknown function are undef (with a warning), not errors -- so a guard
9
+ // written against them works in both.
10
+
11
+ #include "openscad_cpp_evaluator/evaluator.hpp"
12
+
13
+ #include "test_helpers.hpp"
14
+
15
+ #include <gtest/gtest.h>
16
+
17
+ using namespace oscadeval;
18
+ using namespace oscadeval::test;
19
+
20
+ namespace {
21
+
22
+ class ScopedVm {
23
+ public:
24
+ explicit ScopedVm(bool enabled) { Evaluator::setBytecodeVmEnabledForTesting(enabled); }
25
+ ~ScopedVm() { Evaluator::setBytecodeVmEnabledForTesting(std::nullopt); }
26
+ };
27
+
28
+ std::vector<std::string> echoesFrom(const std::string& src) {
29
+ std::vector<std::string> out;
30
+ evalSrc(src, [&](const std::string& m) { out.push_back(m); });
31
+ return out;
32
+ }
33
+
34
+ // Every feature name the docs promise. A typo in either direction -- here or
35
+ // in featureLevels() -- shows up as a 0.
36
+ const char* kFeatures[] = {"separate-children", "minkowski-diff", "sphere-styles", "export-name",
37
+ "simplify-op", "expr-import", "object-function", "roof-op",
38
+ "render-expr", "polyhedron-vnf"};
39
+
40
+ } // namespace
41
+
42
+ TEST(FeatureDetection, BelfryscadVariableIsTheEvaluatorVersion) {
43
+ for (bool vm : {false, true}) {
44
+ ScopedVm guard(vm);
45
+ // Shaped like OpenSCAD's own version(): [major, minor, patch], so a
46
+ // comparison needs no string splitting. Read from pyproject.toml at
47
+ // configure time, so this cannot drift from the released version.
48
+ const std::vector<std::string> e = echoesFrom("echo(is_list($_BELFRYSCAD), len($_BELFRYSCAD));");
49
+ ASSERT_EQ(e.size(), 1u) << "vm=" << vm;
50
+ EXPECT_NE(e[0].find("true"), std::string::npos) << "vm=" << vm << ", got " << e[0];
51
+ EXPECT_NE(e[0].find("3"), std::string::npos) << "vm=" << vm << ", got " << e[0];
52
+ }
53
+ }
54
+
55
+ TEST(FeatureDetection, BelfryscadVariableHoldsThreeNumbers) {
56
+ for (bool vm : {false, true}) {
57
+ ScopedVm guard(vm);
58
+ const std::vector<std::string> e = echoesFrom(
59
+ "echo(is_num($_BELFRYSCAD[0]), is_num($_BELFRYSCAD[1]), is_num($_BELFRYSCAD[2]));");
60
+ ASSERT_EQ(e.size(), 1u) << "vm=" << vm;
61
+ EXPECT_EQ(e[0].find("false"), std::string::npos) << "vm=" << vm << ", got " << e[0];
62
+ }
63
+ }
64
+
65
+ TEST(FeatureDetection, EveryDocumentedFeatureReportsALevel) {
66
+ for (bool vm : {false, true}) {
67
+ ScopedVm guard(vm);
68
+ for (const char* f : kFeatures) {
69
+ const std::vector<std::string> e =
70
+ echoesFrom(std::string("echo(supported_feature(\"") + f + "\"));");
71
+ ASSERT_EQ(e.size(), 1u) << f << ", vm=" << vm;
72
+ EXPECT_NE(e[0].find("1"), std::string::npos) << f << ", vm=" << vm << ", got " << e[0];
73
+ EXPECT_EQ(e[0].find("0"), std::string::npos) << f << " reported unsupported, vm=" << vm;
74
+ }
75
+ }
76
+ }
77
+
78
+ TEST(FeatureDetection, AnUnknownFeatureIsZeroAndSilent) {
79
+ // Probing for a feature from a future release has to be safe -- that is
80
+ // the whole point of asking. No warning, no error, just 0.
81
+ for (bool vm : {false, true}) {
82
+ ScopedVm guard(vm);
83
+ const std::vector<std::string> e = echoesFrom("echo(supported_feature(\"time-travel\"));");
84
+ ASSERT_EQ(e.size(), 1u) << "vm=" << vm << " (unexpected warning?)";
85
+ EXPECT_NE(e[0].find("0"), std::string::npos) << "vm=" << vm << ", got " << e[0];
86
+ }
87
+ }
88
+
89
+ TEST(FeatureDetection, ANonStringArgumentIsZeroRatherThanAnError) {
90
+ for (bool vm : {false, true}) {
91
+ ScopedVm guard(vm);
92
+ for (const char* arg : {"42", "undef", "[1,2]"}) {
93
+ const std::vector<std::string> e =
94
+ echoesFrom(std::string("echo(supported_feature(") + arg + "));");
95
+ ASSERT_EQ(e.size(), 1u) << arg << ", vm=" << vm;
96
+ EXPECT_NE(e[0].find("0"), std::string::npos) << arg << ", vm=" << vm << ", got " << e[0];
97
+ }
98
+ }
99
+ }
100
+
101
+ TEST(FeatureDetection, SupportedFeatureMarkerIsSet) {
102
+ // Vendor-neutral: it says "supported_feature() is callable", not "this is
103
+ // BelfrySCAD". Any implementation adding the function is meant to set it,
104
+ // so a script guards on the capability rather than on who is running it.
105
+ for (bool vm : {false, true}) {
106
+ ScopedVm guard(vm);
107
+ const std::vector<std::string> e = echoesFrom("echo($_SUPPORTED_FEATURE);");
108
+ ASSERT_EQ(e.size(), 1u) << "vm=" << vm;
109
+ EXPECT_NE(e[0].find("true"), std::string::npos) << "vm=" << vm << ", got " << e[0];
110
+ }
111
+ }
112
+
113
+ TEST(FeatureDetection, TheMarkerGuardsTheCallItself) {
114
+ // The bootstrapping shape: you cannot safely CALL supported_feature()
115
+ // without first knowing it exists. Checking the marker first solves that,
116
+ // and because && short-circuits, an evaluator lacking both warns once
117
+ // rather than twice -- verified against the reference binary, which
118
+ // reports exactly one warning for this source and takes the else branch.
119
+ for (bool vm : {false, true}) {
120
+ ScopedVm guard(vm);
121
+ const std::vector<std::string> e = echoesFrom(
122
+ "if (!is_undef($_SUPPORTED_FEATURE) && supported_feature(\"separate-children\"))\n"
123
+ " echo(\"extended\");\n"
124
+ "else echo(\"portable\");\n");
125
+ ASSERT_EQ(e.size(), 1u) << "vm=" << vm;
126
+ EXPECT_NE(e[0].find("extended"), std::string::npos) << "vm=" << vm << ", got " << e[0];
127
+ }
128
+ }
129
+
130
+ TEST(FeatureDetection, IsUndefDoesNotWarnAboutTheNameItProbes) {
131
+ // Asking whether a name is defined must not itself complain that it
132
+ // isn't -- otherwise the portable guard is noisy here while being
133
+ // silent in OpenSCAD. Both spellings, since only the $-prefixed one
134
+ // used to be quiet.
135
+ for (bool vm : {false, true}) {
136
+ ScopedVm guard(vm);
137
+ for (const char* name : {"$_NO_SUCH_DOLLAR", "no_such_plain"}) {
138
+ const std::vector<std::string> e = echoesFrom(std::string("echo(is_undef(") + name + "));");
139
+ ASSERT_EQ(e.size(), 1u) << name << ", vm=" << vm << " (warned?)";
140
+ EXPECT_NE(e[0].find("true"), std::string::npos) << name << ", vm=" << vm << ", got " << e[0];
141
+ }
142
+ // A plain read of an unknown name still warns -- only the probe is
143
+ // exempt, and only for its own argument.
144
+ const std::vector<std::string> e = echoesFrom("echo(no_such_plain);");
145
+ ASSERT_EQ(e.size(), 2u) << "vm=" << vm;
146
+ EXPECT_NE(e[0].find("Ignoring unknown variable"), std::string::npos) << "vm=" << vm;
147
+ }
148
+ }
149
+
150
+ TEST(FeatureDetection, TheGuardIdiomReadsTrueHere) {
151
+ // What a script actually writes. In OpenSCAD $_BELFRYSCAD is undef and
152
+ // supported_feature() is an unknown function returning undef -- both
153
+ // falsy -- so the same source takes the other branch there.
154
+ for (bool vm : {false, true}) {
155
+ ScopedVm guard(vm);
156
+ const std::vector<std::string> e = echoesFrom(
157
+ "if ($_BELFRYSCAD == undef) echo(\"portable\");\n"
158
+ "else if (supported_feature(\"separate-children\")) echo(\"extended\");\n");
159
+ ASSERT_EQ(e.size(), 1u) << "vm=" << vm;
160
+ EXPECT_NE(e[0].find("extended"), std::string::npos) << "vm=" << vm << ", got " << e[0];
161
+ }
162
+ }
@@ -52,11 +52,14 @@ TEST(UseStatement, InjectsFunctionAndModuleButNotVariables) {
52
52
  std::vector<std::string> echoed;
53
53
  UseEvaluated e = evalFile(main, [&](const std::string& m) { echoed.push_back(m); });
54
54
 
55
- ASSERT_EQ(echoed.size(), 3u);
55
+ // Two, not three: is_undef() is a probe and does not warn about the name
56
+ // it is asking about -- matching the reference, which is silent for this
57
+ // exact script. `ECHO: true` is what proves lib_var wasn't injected; the
58
+ // warning this used to also assert was our own divergence, not the point.
59
+ ASSERT_EQ(echoed.size(), 2u);
56
60
  EXPECT_EQ(echoed[0], "ECHO: 103"); // 1 + 2 + lib_var(100) -- proves lib_add's body resolves
57
61
  // lib_var from *its own* file's scope (reanchoring)
58
- EXPECT_NE(echoed[1].find("WARNING: Ignoring unknown variable 'lib_var'"), std::string::npos); // NOT injected
59
- EXPECT_EQ(echoed[2], "ECHO: true"); // is_undef(lib_var) in main's own scope
62
+ EXPECT_EQ(echoed[1], "ECHO: true"); // is_undef(lib_var) in main's own scope -- NOT injected
60
63
  EXPECT_EQ(e.bodies.size(), 1u); // lib_cube(5) still produced geometry
61
64
  std::filesystem::remove(lib);
62
65
  std::filesystem::remove(main);