openscad-cpp-evaluator 0.46.0__tar.gz → 0.48.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.46.0 → openscad_cpp_evaluator-0.48.0}/CLAUDE.md +21 -3
  2. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/PKG-INFO +1 -1
  3. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/pyproject.toml +1 -1
  4. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/src/builtins/function_builtins.cpp +174 -1
  5. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/src/bytecode_compiler.cpp +27 -7
  6. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/src/user_calls.cpp +26 -16
  7. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/tests/test_feature_detection.cpp +1 -1
  8. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/tests/test_function_builtins.cpp +191 -11
  9. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/.github/workflows/ci.yml +0 -0
  10. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/.github/workflows/wheels.yml +0 -0
  11. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/.gitignore +0 -0
  12. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/.gitmodules +0 -0
  13. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/CMakeLists.txt +0 -0
  14. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/LICENSE +0 -0
  15. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/README.md +0 -0
  16. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/bindings/CMakeLists.txt +0 -0
  17. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/bindings/ast_to_py.cpp +0 -0
  18. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/bindings/module.cpp +0 -0
  19. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/cmake/embed_font.cmake +0 -0
  20. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/examples/CMakeLists.txt +0 -0
  21. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/examples/eval_perf_benchmark.cpp +0 -0
  22. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/examples/manifold_cache_reuse.cpp +0 -0
  23. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/examples/minimal_debugger.cpp +0 -0
  24. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/.github/workflows/ci.yml +0 -0
  25. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/.gitignore +0 -0
  26. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/CLAUDE.md +0 -0
  27. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/CMakeLists.txt +0 -0
  28. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/README.md +0 -0
  29. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/api.hpp +0 -0
  30. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/ast_node.hpp +0 -0
  31. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/comments.hpp +0 -0
  32. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/declarations.hpp +0 -0
  33. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/expression.hpp +0 -0
  34. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/module_instantiation.hpp +0 -0
  35. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/scope_builder.hpp +0 -0
  36. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/vector_element.hpp +0 -0
  37. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast.hpp +0 -0
  38. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/comments.hpp +0 -0
  39. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/position.hpp +0 -0
  40. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/pretty_print.hpp +0 -0
  41. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/scope.hpp +0 -0
  42. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/serialization.hpp +0 -0
  43. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/source_map.hpp +0 -0
  44. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/src/CMakeLists.txt +0 -0
  45. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/src/api.cpp +0 -0
  46. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/src/ast/ast_node.cpp +0 -0
  47. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/src/ast/declarations.cpp +0 -0
  48. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/src/ast/expression.cpp +0 -0
  49. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/src/ast/format_utils.hpp +0 -0
  50. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/src/ast/module_instantiation.cpp +0 -0
  51. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/src/ast/scope_builder.cpp +0 -0
  52. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/src/ast/vector_element.cpp +0 -0
  53. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/src/comment_attach_internal.hpp +0 -0
  54. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/src/comments.cpp +0 -0
  55. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/src/grammar/driver.cpp +0 -0
  56. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/src/grammar/driver.hpp +0 -0
  57. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/src/grammar/lexer.l +0 -0
  58. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/src/grammar/lexer_api.hpp +0 -0
  59. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/src/grammar/oscad_location.hpp +0 -0
  60. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/src/grammar/parser.y +0 -0
  61. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/src/inline_comment_attach.cpp +0 -0
  62. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/src/position.cpp +0 -0
  63. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/src/pretty_print.cpp +0 -0
  64. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/src/scope.cpp +0 -0
  65. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/src/serialization/json_io.cpp +0 -0
  66. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/src/serialization/yaml_io.cpp +0 -0
  67. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/src/source_map.cpp +0 -0
  68. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/tests/CMakeLists.txt +0 -0
  69. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/tests/test_assignments.cpp +0 -0
  70. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/tests/test_ast_generation.cpp +0 -0
  71. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/tests/test_comments_and_files.cpp +0 -0
  72. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/tests/test_const_borrow.cpp +0 -0
  73. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/tests/test_control.cpp +0 -0
  74. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/tests/test_expressions.cpp +0 -0
  75. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/tests/test_functions.cpp +0 -0
  76. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/tests/test_helpers.hpp +0 -0
  77. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/tests/test_inline_comments.cpp +0 -0
  78. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/tests/test_lexical.cpp +0 -0
  79. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/tests/test_modules.cpp +0 -0
  80. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/tests/test_node_str.cpp +0 -0
  81. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/tests/test_pretty_print.cpp +0 -0
  82. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/tests/test_render_expression.cpp +0 -0
  83. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/tests/test_scope.cpp +0 -0
  84. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/tests/test_serialization.cpp +0 -0
  85. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/tests/test_smoke.cpp +0 -0
  86. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/tests/test_source_map.cpp +0 -0
  87. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/tests/test_use_include.cpp +0 -0
  88. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/tests/test_vectors.cpp +0 -0
  89. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/tools/cli/CMakeLists.txt +0 -0
  90. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/external/openscad_cpp_parser/tools/cli/main.cpp +0 -0
  91. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/include/openscad_cpp_evaluator/bound_args.hpp +0 -0
  92. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/include/openscad_cpp_evaluator/bytecode.hpp +0 -0
  93. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/include/openscad_cpp_evaluator/bytecode_compiler.hpp +0 -0
  94. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/include/openscad_cpp_evaluator/bytecode_vm.hpp +0 -0
  95. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/include/openscad_cpp_evaluator/call_args.hpp +0 -0
  96. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/include/openscad_cpp_evaluator/colored_body.hpp +0 -0
  97. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/include/openscad_cpp_evaluator/csg_node.hpp +0 -0
  98. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/include/openscad_cpp_evaluator/css_colors.hpp +0 -0
  99. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/include/openscad_cpp_evaluator/debug_hooks.hpp +0 -0
  100. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/include/openscad_cpp_evaluator/debug_repl.hpp +0 -0
  101. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/include/openscad_cpp_evaluator/dispatch.hpp +0 -0
  102. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/include/openscad_cpp_evaluator/dxf_svg_import.hpp +0 -0
  103. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/include/openscad_cpp_evaluator/eval_context.hpp +0 -0
  104. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/include/openscad_cpp_evaluator/eval_error.hpp +0 -0
  105. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/include/openscad_cpp_evaluator/eval_use.hpp +0 -0
  106. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/include/openscad_cpp_evaluator/evaluator.hpp +0 -0
  107. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/include/openscad_cpp_evaluator/export.hpp +0 -0
  108. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/include/openscad_cpp_evaluator/font_match.hpp +0 -0
  109. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/include/openscad_cpp_evaluator/font_provider.hpp +0 -0
  110. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/include/openscad_cpp_evaluator/freetype_font_provider.hpp +0 -0
  111. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/include/openscad_cpp_evaluator/function_builtins.hpp +0 -0
  112. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/include/openscad_cpp_evaluator/import_builtin.hpp +0 -0
  113. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/include/openscad_cpp_evaluator/manifold_cache.hpp +0 -0
  114. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/include/openscad_cpp_evaluator/mesh_check.hpp +0 -0
  115. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/include/openscad_cpp_evaluator/mesh_import.hpp +0 -0
  116. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/include/openscad_cpp_evaluator/osc_range.hpp +0 -0
  117. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/include/openscad_cpp_evaluator/profile.hpp +0 -0
  118. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/include/openscad_cpp_evaluator/scope_trail.hpp +0 -0
  119. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/include/openscad_cpp_evaluator/segments.hpp +0 -0
  120. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/include/openscad_cpp_evaluator/surface_load.hpp +0 -0
  121. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/include/openscad_cpp_evaluator/text_metrics.hpp +0 -0
  122. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/include/openscad_cpp_evaluator/value.hpp +0 -0
  123. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/include/openscad_cpp_evaluator/zip_stored.hpp +0 -0
  124. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/python/openscad_cpp_evaluator/__init__.py +0 -0
  125. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/resources/fonts/LICENSE +0 -0
  126. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/resources/fonts/LiberationSans-Regular.ttf +0 -0
  127. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/src/CMakeLists.txt +0 -0
  128. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/src/builtins/booleans.cpp +0 -0
  129. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/src/builtins/builtins.hpp +0 -0
  130. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/src/builtins/call_args.cpp +0 -0
  131. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/src/builtins/color.cpp +0 -0
  132. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/src/builtins/control.cpp +0 -0
  133. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/src/builtins/dxf_dim.cpp +0 -0
  134. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/src/builtins/extrude.cpp +0 -0
  135. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/src/builtins/import.cpp +0 -0
  136. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/src/builtins/modifiers.cpp +0 -0
  137. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/src/builtins/primitives_2d.cpp +0 -0
  138. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/src/builtins/primitives_3d.cpp +0 -0
  139. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/src/builtins/registry.cpp +0 -0
  140. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/src/builtins/roof.cpp +0 -0
  141. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/src/builtins/segments.cpp +0 -0
  142. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/src/builtins/surface.cpp +0 -0
  143. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/src/builtins/text.cpp +0 -0
  144. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/src/builtins/topology.cpp +0 -0
  145. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/src/builtins/transforms.cpp +0 -0
  146. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/src/bytecode_vm.cpp +0 -0
  147. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/src/colored_body.cpp +0 -0
  148. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/src/csg_generate.cpp +0 -0
  149. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/src/csg_resolve.cpp +0 -0
  150. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/src/css_colors.cpp +0 -0
  151. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/src/debug/debug_repl.cpp +0 -0
  152. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/src/debug_profile.cpp +0 -0
  153. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/src/eval_context.cpp +0 -0
  154. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/src/eval_error.cpp +0 -0
  155. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/src/eval_use.cpp +0 -0
  156. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/src/export.cpp +0 -0
  157. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/src/expr_eval.cpp +0 -0
  158. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/src/import/dxf_import.cpp +0 -0
  159. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/src/import/mesh_import.cpp +0 -0
  160. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/src/import/surface_load.cpp +0 -0
  161. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/src/import/svg_import.cpp +0 -0
  162. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/src/manifold_cache.cpp +0 -0
  163. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/src/measure_geometry.cpp +0 -0
  164. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/src/mesh_check.cpp +0 -0
  165. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/src/stmt_eval.cpp +0 -0
  166. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/src/text/font_match.cpp +0 -0
  167. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/src/text/freetype_font_provider.cpp +0 -0
  168. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/src/text/text_metrics.cpp +0 -0
  169. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/src/value.cpp +0 -0
  170. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/src/zip_stored.cpp +0 -0
  171. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/tests/CMakeLists.txt +0 -0
  172. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/tests/test_booleans.cpp +0 -0
  173. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/tests/test_bytecode_compiler.cpp +0 -0
  174. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/tests/test_children_separate.cpp +0 -0
  175. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/tests/test_cli.cpp +0 -0
  176. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/tests/test_control_flow.cpp +0 -0
  177. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/tests/test_csg_tree.cpp +0 -0
  178. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/tests/test_debug_hooks.cpp +0 -0
  179. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/tests/test_dxf_svg_import.cpp +0 -0
  180. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/tests/test_eval_context.cpp +0 -0
  181. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/tests/test_export.cpp +0 -0
  182. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/tests/test_expr_eval.cpp +0 -0
  183. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/tests/test_extrude_roof.cpp +0 -0
  184. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/tests/test_helpers.hpp +0 -0
  185. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/tests/test_import_export.cpp +0 -0
  186. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/tests/test_manifold_cache.cpp +0 -0
  187. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/tests/test_mesh_check.cpp +0 -0
  188. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/tests/test_multi_color_merge.cpp +0 -0
  189. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/tests/test_primitives.cpp +0 -0
  190. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/tests/test_profiling.cpp +0 -0
  191. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/tests/test_python_bindings.py +0 -0
  192. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/tests/test_render_expr.cpp +0 -0
  193. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/tests/test_scope_trail.cpp +0 -0
  194. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/tests/test_scoping.cpp +0 -0
  195. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/tests/test_surface.cpp +0 -0
  196. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/tests/test_tail_calls.cpp +0 -0
  197. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/tests/test_text.cpp +0 -0
  198. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/tests/test_transforms.cpp +0 -0
  199. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/tests/test_unexpected_args.cpp +0 -0
  200. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/tests/test_use.cpp +0 -0
  201. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/tests/test_value.cpp +0 -0
  202. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/tests/test_viewport_params.cpp +0 -0
  203. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/tests/test_zip_stored.cpp +0 -0
  204. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/tools/cli/CMakeLists.txt +0 -0
  205. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/tools/cli/cli_lib.cpp +0 -0
  206. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/tools/cli/cli_lib.hpp +0 -0
  207. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/tools/cli/main.cpp +0 -0
  208. {openscad_cpp_evaluator-0.46.0 → openscad_cpp_evaluator-0.48.0}/uv.lock +0 -0
@@ -45,9 +45,13 @@ comprehension clause kinds, user function calls (recursion, defaults, `FunctionL
45
45
  user module calls (`children()`/`children(N)`, closure detection via `callCtxFor`), list
46
46
  indexing/swizzle member access; the full math/string/list/type-check builtin *function* dispatch
47
47
  (`sin`/`cos`/`pow`/`search`/`lookup`/`chr`/`ord`/`is_*`/`object`/`has_key`/etc, ~43 names — a
48
- completely separate namespace/precedence rule from the builtin *modules* in `registry.cpp`: a
49
- builtin function name always wins over a same-named user function, checked *before* the
50
- user-function lookup), STL/OBJ/OFF/3MF `import()` in both module (geometry) and expression
48
+ same precedence rule as the builtin *modules* in `registry.cpp`: a user-defined function
49
+ **shadows** a same-named builtin, looked up *before* the builtin table. This was backwards until
50
+ 0.47.0 — the port copied the old Python evaluator's `_eval_function_call` order, which had it
51
+ wrong; OpenSCAD 2026.02.01 echoes 999 for `function abs(x)=999; abs(-3)` and we echoed 3. Fixing
52
+ it needs **both** resolution sites: `evalFunctionCall` (`user_calls.cpp`) and the compiler's
53
+ call-site resolution (`bytecode_compiler.cpp`), which bakes builtin-vs-user in at compile time,
54
+ plus gating the silent `is_undef` probe on the call actually resolving to the builtin), STL/OBJ/OFF/3MF `import()` in both module (geometry) and expression
51
55
  (VNF/JSON) contexts, and 3MF read/write; (Phase 6) `hull()`/`minkowski()` (transparent-splice
52
56
  CSG-tree shape like union/difference/intersection, role-split via the shared `splitByRole`),
53
57
  `linear_extrude()`/`rotate_extrude()`/`projection(cut=)`, and `roof()`; and (Phase 7) `offset()`,
@@ -456,6 +460,20 @@ grep for `ponytail:`.
456
460
  unknown arguments -- `children(separate=true)` renders the wrong shape there with no warning --
457
461
  and both read as `undef` in OpenSCAD, so a guard written against them works in both.
458
462
 
463
+ - **`matrix_solve(A, b)`** (`src/builtins/function_builtins.cpp`, feature name `matrix-solve`) —
464
+ one pivoted LU answering three questions from the same factorisation: `.x` (solution; a vector
465
+ for a vector `b`, a matrix for a matrix `b`, `undef` when `b` is omitted), `.det`, `.singular`.
466
+ Square systems only — BOSL2's `linear_solve` also does least-squares/minimum-norm via QR, which
467
+ LU cannot, and those paths are already fast in script. A non-square or non-numeric matrix warns
468
+ and returns `undef`; a **singular** one is reported (`singular=true`, `det=0`, `x=undef`), not
469
+ warned about, because asking whether a matrix is singular is a fair question.
470
+ Why it exists: BOSL2's `determinant()` is a cofactor expansion, O(n!) in time *and* in
471
+ intermediate list allocation. Measured through the CLI on identical matrices, with identical
472
+ results: **n=10 3.17s → 0.33s, n=11 31.49s → 0.33s**. Its `linear_solve` is not the problem
473
+ (128×128 in ~0.18s since its Apr 2026 Householder rewrite) — but its singularity test is a fixed
474
+ **absolute** `1e-9` on R's diagonal with no scaling by ‖A‖, so `2*I` scaled by 1e-10 is declared
475
+ singular there and solves correctly here. This one uses a relative threshold.
476
+
459
477
  - `include/openscad_cpp_evaluator/value.hpp`, `src/value.cpp` — the `Value` variant (OpenSCAD's
460
478
  dynamic value type) and its free-function arithmetic/comparison helpers. See the plan's §1 for
461
479
  the type-mapping table and why there's no numpy-equivalent library.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openscad_cpp_evaluator
3
- Version: 0.46.0
3
+ Version: 0.48.0
4
4
  Summary: C++ OpenSCAD evaluator with Python bindings
5
5
  Author: NeuSCAD Contributors
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build"
4
4
 
5
5
  [project]
6
6
  name = "openscad_cpp_evaluator"
7
- version = "0.46.0"
7
+ version = "0.48.0"
8
8
  description = "C++ OpenSCAD evaluator with Python bindings"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -169,6 +169,172 @@ Value builtinCross(const Value& aArg, const Value& bArg) {
169
169
  return Value{};
170
170
  }
171
171
 
172
+ // -- matrix_solve(A, b) --------------------------------------------------
173
+ //
174
+ // One pivoted LU answers three questions at once -- the solution, the
175
+ // determinant, and whether A is singular -- because all three fall out of
176
+ // the same factorisation.
177
+ //
178
+ // The motivation is BOSL2's determinant() (linalg.scad), a cofactor
179
+ // expansion: O(n!) time AND O(n!) intermediate list allocation. Measured
180
+ // through this evaluator: 0.04s at n=8, 0.38s at n=9, 2.7s at n=10, and
181
+ // projecting to minutes at n=12. This is O(n^3). BOSL2's linear_solve, by
182
+ // contrast, was already fast (128x128 in ~0.18s after its Apr 2026
183
+ // Householder rewrite), so speed is not the argument for the solve half --
184
+ // a correct singularity test is. See below.
185
+ //
186
+ // Square systems only. BOSL2's linear_solve also handles overdetermined
187
+ // (least-squares) and underdetermined (minimum-norm) systems via QR; LU
188
+ // cannot, and those paths are quick enough in script. A non-square matrix
189
+ // warns rather than silently doing something else.
190
+
191
+ // Defined further down, next to the other object() helpers.
192
+ Value objectOf(std::vector<std::pair<std::string, Value>> items);
193
+
194
+ // A rectangular numeric matrix as (row-major values, column count).
195
+ // nullopt for anything that is not a non-empty list of equal-length,
196
+ // non-empty numeric lists.
197
+ std::optional<std::pair<std::vector<double>, size_t>> numericMatrix(const Value& v) {
198
+ const ListPtr* rows = std::get_if<ListPtr>(&v);
199
+ if (!rows || !*rows || (*rows)->items.empty()) return std::nullopt;
200
+ std::vector<double> flat;
201
+ size_t cols = 0;
202
+ for (size_t r = 0; r < (*rows)->items.size(); ++r) {
203
+ const std::optional<std::vector<double>> row = allNumericList((*rows)->items[r]);
204
+ if (!row || row->empty()) return std::nullopt;
205
+ if (r == 0) {
206
+ cols = row->size();
207
+ } else if (row->size() != cols) {
208
+ return std::nullopt;
209
+ }
210
+ flat.insert(flat.end(), row->begin(), row->end());
211
+ }
212
+ return std::make_pair(std::move(flat), cols);
213
+ }
214
+
215
+ Value builtinMatrixSolve(Evaluator& ev, const Value& aArg, const Value& bArg, bool haveB,
216
+ const oscad::ASTNode& node) {
217
+ const auto parsed = numericMatrix(aArg);
218
+ if (!parsed) {
219
+ ev.warn("matrix_solve() requires a matrix of numbers", &node.position());
220
+ return Value{};
221
+ }
222
+ std::vector<double> lu = parsed->first;
223
+ const size_t cols = parsed->second;
224
+ const size_t n = lu.size() / cols;
225
+ if (n != cols) {
226
+ ev.warn("matrix_solve() requires a square matrix, found " + std::to_string(n) + " x " +
227
+ std::to_string(cols),
228
+ &node.position());
229
+ return Value{};
230
+ }
231
+ double maxAbs = 0.0;
232
+ for (double x : lu) {
233
+ if (!std::isfinite(x)) {
234
+ ev.warn("matrix_solve() matrix contains a non-finite value", &node.position());
235
+ return Value{};
236
+ }
237
+ maxAbs = std::max(maxAbs, std::abs(x));
238
+ }
239
+
240
+ // Right-hand side: a vector of n is one column; a matrix of n rows is
241
+ // one column per column. Kept row-major alongside the factorisation.
242
+ size_t k = 0;
243
+ bool bWasVector = false;
244
+ std::vector<double> rhs;
245
+ if (haveB) {
246
+ if (const std::optional<std::vector<double>> vec = allNumericList(bArg); vec && vec->size() == n) {
247
+ bWasVector = true;
248
+ k = 1;
249
+ rhs = *vec;
250
+ } else if (const auto bm = numericMatrix(bArg); bm && bm->first.size() / bm->second == n) {
251
+ k = bm->second;
252
+ rhs = bm->first;
253
+ } else {
254
+ ev.warn("matrix_solve() right-hand side must be a vector of " + std::to_string(n) +
255
+ " numbers, or a matrix with that many rows",
256
+ &node.position());
257
+ return Value{};
258
+ }
259
+ for (double x : rhs) {
260
+ if (!std::isfinite(x)) {
261
+ ev.warn("matrix_solve() right-hand side contains a non-finite value", &node.position());
262
+ return Value{};
263
+ }
264
+ }
265
+ }
266
+
267
+ // Relative singularity threshold. BOSL2 compares R's diagonal against a
268
+ // fixed ABSOLUTE 1e-9 (its _EPSILON) with no scaling by the size of the
269
+ // matrix, so a perfectly well-conditioned system scaled down by 1e-10
270
+ // is declared singular there. Scaling by maxAbs is what makes
271
+ // matrix_solve(A*1e-10) still solvable.
272
+ const double tol = std::numeric_limits<double>::epsilon() * static_cast<double>(n) * std::max(maxAbs, 1.0);
273
+
274
+ double det = 1.0;
275
+ bool singular = false;
276
+ for (size_t col = 0; col < n && !singular; ++col) {
277
+ size_t pivot = col;
278
+ for (size_t r = col + 1; r < n; ++r) {
279
+ if (std::abs(lu[r * n + col]) > std::abs(lu[pivot * n + col])) pivot = r;
280
+ }
281
+ if (std::abs(lu[pivot * n + col]) <= tol) {
282
+ singular = true;
283
+ break;
284
+ }
285
+ if (pivot != col) {
286
+ for (size_t c = 0; c < n; ++c) std::swap(lu[col * n + c], lu[pivot * n + c]);
287
+ for (size_t c = 0; c < k; ++c) std::swap(rhs[col * k + c], rhs[pivot * k + c]);
288
+ det = -det;
289
+ }
290
+ const double p = lu[col * n + col];
291
+ det *= p;
292
+ for (size_t r = col + 1; r < n; ++r) {
293
+ const double f = lu[r * n + col] / p;
294
+ if (f == 0.0) continue;
295
+ lu[r * n + col] = 0.0;
296
+ for (size_t c = col + 1; c < n; ++c) lu[r * n + c] -= f * lu[col * n + c];
297
+ for (size_t c = 0; c < k; ++c) rhs[r * k + c] -= f * rhs[col * k + c];
298
+ }
299
+ }
300
+
301
+ std::vector<std::pair<std::string, Value>> out;
302
+ if (singular) {
303
+ // Not a misuse -- "is this matrix singular?" is a legitimate
304
+ // question to ask matrix_solve, so it answers rather than warning.
305
+ out.emplace_back("x", Value{});
306
+ out.emplace_back("det", Value{0.0});
307
+ out.emplace_back("singular", Value{true});
308
+ return objectOf(std::move(out));
309
+ }
310
+
311
+ if (haveB) {
312
+ for (size_t col = n; col-- > 0;) {
313
+ for (size_t c = 0; c < k; ++c) {
314
+ double acc = rhs[col * k + c];
315
+ for (size_t j = col + 1; j < n; ++j) acc -= lu[col * n + j] * rhs[j * k + c];
316
+ rhs[col * k + c] = acc / lu[col * n + col];
317
+ }
318
+ }
319
+ if (bWasVector) {
320
+ out.emplace_back("x", numList(rhs));
321
+ } else {
322
+ std::vector<Value> rowsOut;
323
+ rowsOut.reserve(n);
324
+ for (size_t r = 0; r < n; ++r) {
325
+ rowsOut.push_back(numList(std::vector<double>(rhs.begin() + static_cast<long>(r * k),
326
+ rhs.begin() + static_cast<long>((r + 1) * k))));
327
+ }
328
+ out.emplace_back("x", listOf(std::move(rowsOut)));
329
+ }
330
+ } else {
331
+ out.emplace_back("x", Value{});
332
+ }
333
+ out.emplace_back("det", Value{det});
334
+ out.emplace_back("singular", Value{false});
335
+ return objectOf(std::move(out));
336
+ }
337
+
172
338
  Value builtinRands(double minv, double maxv, double nArg, const Value& seedArg) {
173
339
  // ponytail: doesn't reproduce Python's Mersenne-Twister bit-for-bit --
174
340
  // no script should depend on cross-language RNG equality, only on
@@ -450,6 +616,7 @@ bool isBuiltinFunctionName(const std::string& name) {
450
616
  "chr", "ord", "is_undef", "is_num", "is_bool", "is_string", "is_list", "is_function", "is_object",
451
617
  "search", "lookup", "has_key", "version", "version_num", "parent_module",
452
618
  "object", "textmetrics", "fontmetrics", "dxf_dim", "dxf_cross", "supported_feature",
619
+ "matrix_solve",
453
620
  };
454
621
  return names.count(name) > 0;
455
622
  }
@@ -591,7 +758,7 @@ enum class BuiltinFnId {
591
758
  TextMetrics, FontMetrics, Abs, Sign, Ceil, Floor, Round, Sqrt, Ln, Log, Exp, Sin, Cos, Tan,
592
759
  Asin, Acos, Atan, Atan2, Max, Min, Pow, Norm, Cross, Rands, Concat, Len, Str, Chr, Ord,
593
760
  IsUndef, IsNum, IsBool, IsString, IsList, IsFunction, IsObject, Search, Lookup, HasKey,
594
- Version, VersionNum, ParentModule, DxfDim, DxfCross, SupportedFeature,
761
+ Version, VersionNum, ParentModule, DxfDim, DxfCross, SupportedFeature, MatrixSolve,
595
762
  };
596
763
 
597
764
  // supported_feature("name") -> the level at which this build implements that
@@ -611,6 +778,7 @@ enum class BuiltinFnId {
611
778
  const std::unordered_map<std::string, double>& featureLevels() {
612
779
  static const std::unordered_map<std::string, double> levels = {
613
780
  {"render-expr", 1.0}, // render() in expression position
781
+ {"matrix-solve", 1.0}, // matrix_solve(A, b) -> {x, det, singular}
614
782
  {"polyhedron-vnf", 1.0}, // polyhedron(vnf) / polyhedron(object)
615
783
  {"separate-children", 1.0}, // children(..., separate=true)
616
784
  {"minkowski-diff", 1.0}, // minkowski_difference()
@@ -645,6 +813,7 @@ const std::unordered_map<std::string, BuiltinFnId>& builtinFnIds() {
645
813
  {"parent_module", BuiltinFnId::ParentModule},
646
814
  {"dxf_dim", BuiltinFnId::DxfDim}, {"dxf_cross", BuiltinFnId::DxfCross},
647
815
  {"supported_feature", BuiltinFnId::SupportedFeature},
816
+ {"matrix_solve", BuiltinFnId::MatrixSolve},
648
817
  };
649
818
  return ids;
650
819
  }
@@ -726,6 +895,7 @@ const std::unordered_map<int, BuiltinCheck>& builtinChecks() {
726
895
  add(BuiltinFnId::ParentModule, {0, 1, "1", {kNum}});
727
896
  add(BuiltinFnId::HasKey, {-1, -1, nullptr, {{TObj, "object"}, {TStr, "string"}}});
728
897
  add(BuiltinFnId::SupportedFeature, {1, 1, "1", {}});
898
+ add(BuiltinFnId::MatrixSolve, {1, 2, "1 or 2", {kVec}});
729
899
  for (BuiltinFnId id : {BuiltinFnId::IsUndef, BuiltinFnId::IsNum, BuiltinFnId::IsBool,
730
900
  BuiltinFnId::IsString, BuiltinFnId::IsList, BuiltinFnId::IsFunction,
731
901
  BuiltinFnId::IsObject}) {
@@ -1040,6 +1210,9 @@ Value evalBuiltinFunction(Evaluator& ev, const std::string& name, const CallArgs
1040
1210
  // The OpenSCAD release we track. version_num() is that same
1041
1211
  // year/month/day folded as y * 10000 + m * 100 + d, exactly like the
1042
1212
  // reference's own builtin_version_num (builtin_functions.cc).
1213
+ case BuiltinFnId::MatrixSolve:
1214
+ return builtinMatrixSolve(ev, getArg(args, 0, "A", Value{}), getArg(args, 1, "b", Value{}),
1215
+ args.findPositional(1) != nullptr || args.findNamed("b") != nullptr, node);
1043
1216
  case BuiltinFnId::SupportedFeature: {
1044
1217
  const Value name = getArg(args, 0, "feature", Value{});
1045
1218
  const std::string* s = std::get_if<std::string>(&name);
@@ -567,6 +567,19 @@ public:
567
567
  // evalBuiltinFunction when isImport is set.
568
568
  site.isImport = true;
569
569
  isStatic = true;
570
+ } else if (const oscad::ASTNode* userFn =
571
+ scope_ ? scope_->lookupFunction(calleeName) : nullptr;
572
+ userFn && userFn->kind() == NodeKind::FunctionDeclaration) {
573
+ // A user-defined function SHADOWS a same-named
574
+ // builtin, so this lookup runs BEFORE the builtin
575
+ // table -- the same reordering as
576
+ // evalFunctionCall's (user_calls.cpp), and it has
577
+ // to happen here too: this decision is baked in at
578
+ // COMPILE time, so leaving the builtin test first
579
+ // would keep the bug on the default engine no
580
+ // matter what the interpreter does.
581
+ site.decl = static_cast<const oscad::FunctionDeclaration*>(userFn);
582
+ isStatic = true;
570
583
  } else if (isBuiltinFunctionName(calleeName)) {
571
584
  // object() is already in this table -- it gets
572
585
  // isBuiltin=true here like any other builtin, and
@@ -578,13 +591,10 @@ public:
578
591
  // split positional/named CallArgs shape).
579
592
  site.isBuiltin = true;
580
593
  isStatic = true;
581
- } else {
582
- const oscad::ASTNode* found = scope_ ? scope_->lookupFunction(calleeName) : nullptr;
583
- if (found && found->kind() == NodeKind::FunctionDeclaration) {
584
- site.decl = static_cast<const oscad::FunctionDeclaration*>(found);
585
- isStatic = true;
586
- }
587
594
  }
595
+ // Anything else stays dynamic: a function-literal value,
596
+ // or a name that is simply unknown. Op::CallDynamic
597
+ // resolves it (and warns) at runtime.
588
598
  }
589
599
 
590
600
  if (!isStatic) {
@@ -626,7 +636,17 @@ public:
626
636
  // is_undef(name) probes rather than reads -- the same quiet
627
637
  // load the interpreter uses (Evaluator::undefProbeTarget),
628
638
  // so the two engines agree on which reads warn.
629
- if (const oscad::Identifier* undefProbe = Evaluator::undefProbeTarget(n)) {
639
+ //
640
+ // Only when the call resolves to the BUILTIN, though. A
641
+ // user function named is_undef shadows it (site resolution
642
+ // above) and evaluates its argument the ordinary way,
643
+ // warnings included -- which is what OpenSCAD does. The
644
+ // interpreter gets this for free by testing the probe
645
+ // inside its builtin branch; here the two are separate, so
646
+ // the condition has to be spelled out.
647
+ const oscad::Identifier* undefProbe =
648
+ site.isBuiltin ? Evaluator::undefProbeTarget(n) : nullptr;
649
+ if (undefProbe) {
630
650
  compileIdentifierLoad(undefProbe->name, undefProbe->position(), out, scope,
631
651
  /*warnIfUndef=*/false);
632
652
  site.argNames.push_back(std::nullopt);
@@ -876,11 +876,32 @@ Value Evaluator::evalFunctionCall(const oscad::PrimaryCall& node, EvalContext& c
876
876
  CallArgs args = resolveArgs(*this, node.arguments, ctx);
877
877
  return importAsValue(*this, args, node);
878
878
  }
879
- // A builtin function name always wins over a same-named user
880
- // function -- checked *before* the user-function lookup, not
881
- // after, matching the reference's _eval_function_call precedence
882
- // exactly (name in _BUILTIN_FN_NAMES gates which branch runs at
883
- // all).
879
+ // A user-defined function SHADOWS a same-named builtin, so the
880
+ // user lookup runs FIRST. Verified against OpenSCAD 2026.02.01:
881
+ // `function norm(x) = 999; echo(norm([3,4]));` echoes 999 there,
882
+ // and 5 here until this was fixed.
883
+ //
884
+ // The comment this replaces asserted the opposite order matched
885
+ // "the reference's _eval_function_call precedence exactly" -- but
886
+ // _eval_function_call / _BUILTIN_FN_NAMES are the old PYTHON
887
+ // evaluator, not OpenSCAD, and that prototype had it backwards.
888
+ // The C++ port inherited the mistake. Modules were always right
889
+ // (a user `module sphere()` does override the builtin); only
890
+ // functions resolved the wrong way round.
891
+ //
892
+ // bytecode_compiler.cpp's Op::CallFn site resolution has the same
893
+ // ordering and is fixed with it -- the VM is the default engine,
894
+ // so fixing only this one would leave the bug in place.
895
+ const oscad::ASTNode* decl = ctx.scope->lookupFunction(leftId->name);
896
+ if (decl && decl->kind() == oscad::NodeKind::FunctionDeclaration) {
897
+ // Call-site stop, in the CALLER's context, before descending
898
+ // into the callee (whose own body-entry stop comes later, from
899
+ // evalUserFunctionCore). Builtins deliberately get none --
900
+ // only the user-function and function-literal branches stop.
901
+ checkDebug(node, ctx, /*forced=*/false, /*exprLevel=*/false, /*callSite=*/true);
902
+ return evalUserFunction(leftId->name, static_cast<const oscad::FunctionDeclaration&>(*decl), node.arguments,
903
+ ctx, &node);
904
+ }
884
905
  if (isBuiltinFunctionName(leftId->name)) {
885
906
  // is_undef(name) is a probe: asking whether a name is defined
886
907
  // must not itself warn that it isn't. The $-branch of
@@ -901,17 +922,6 @@ Value Evaluator::evalFunctionCall(const oscad::PrimaryCall& node, EvalContext& c
901
922
  CallArgs args = resolveArgs(*this, node.arguments, ctx);
902
923
  return evalBuiltinFunction(*this, leftId->name, args, node);
903
924
  }
904
- const oscad::ASTNode* decl = ctx.scope->lookupFunction(leftId->name);
905
- if (decl && decl->kind() == oscad::NodeKind::FunctionDeclaration) {
906
- // Call-site stop, in the CALLER's context, before descending
907
- // into the callee (whose own body-entry stop comes later, from
908
- // evalUserFunctionCore). Builtins deliberately get none --
909
- // mirrors _eval_function_call, where only the user-function and
910
- // function-literal branches call _check_debug.
911
- checkDebug(node, ctx, /*forced=*/false, /*exprLevel=*/false, /*callSite=*/true);
912
- return evalUserFunction(leftId->name, static_cast<const oscad::FunctionDeclaration&>(*decl), node.arguments,
913
- ctx, &node);
914
- }
915
925
  }
916
926
 
917
927
  // Not a builtin or user function -- check whether the callee expression
@@ -35,7 +35,7 @@ std::vector<std::string> echoesFrom(const std::string& src) {
35
35
  // in featureLevels() -- shows up as a 0.
36
36
  const char* kFeatures[] = {"separate-children", "minkowski-diff", "sphere-styles", "export-name",
37
37
  "simplify-op", "expr-import", "object-function", "roof-op",
38
- "render-expr", "polyhedron-vnf"};
38
+ "render-expr", "polyhedron-vnf", "matrix-solve"};
39
39
 
40
40
  } // namespace
41
41
 
@@ -508,18 +508,99 @@ TEST(ObjectBuiltin, PositionalNonObjectNonListArgumentIsUndef) {
508
508
  EXPECT_TRUE(isUndef(evalSrc("object(5)", ev)));
509
509
  }
510
510
 
511
- // -- Precedence: a builtin function name always wins over a same-named --
512
- // user function, unlike modules (where a user module CAN shadow a builtin).
511
+ // -- Precedence: a user function SHADOWS a same-named builtin ------------
512
+ //
513
+ // Same rule as modules, which always got this right. It used to be
514
+ // backwards for functions: the builtin won, because the port copied the old
515
+ // Python evaluator's _eval_function_call order rather than OpenSCAD's.
516
+ //
517
+ // Read off OpenSCAD 2026.02.01:
518
+ // function abs(x) = 999; echo(abs(-3)); -> 999
519
+ // function is_undef(x) = "mine"; echo(is_undef(q)); -> "mine", and it
520
+ // WARNS about the unknown variable q, because a user function
521
+ // evaluates its arguments normally -- only the builtin probe is
522
+ // silent.
523
+ //
524
+ // Every case runs under both engines: the VM decides builtin-vs-user at
525
+ // COMPILE time (bytecode_compiler.cpp), so it needs its own fix and its
526
+ // own coverage.
513
527
 
514
- TEST(FunctionBuiltinPrecedence, BuiltinWinsOverSameNamedUserFunction) {
515
- Evaluator ev;
516
- auto ast = parseSrc("function abs(x) = 999;\nresult = abs(-3);");
517
- auto scope = oscad::buildScopes(ast);
518
- auto* assign = dynamic_cast<oscad::Assignment*>(ast[1].get());
519
- ASSERT_NE(assign, nullptr);
520
- EvalContext ctx = EvalContext::makeRoot(scope.get());
521
- Value v = ev.evalExpr(*assign->expr, ctx);
522
- EXPECT_DOUBLE_EQ(asNum(v), 3.0); // builtin abs(), not the user's abs()
528
+ namespace {
529
+
530
+ class ScopedVm {
531
+ public:
532
+ explicit ScopedVm(bool enabled) { Evaluator::setBytecodeVmEnabledForTesting(enabled); }
533
+ ~ScopedVm() { Evaluator::setBytecodeVmEnabledForTesting(std::nullopt); }
534
+ };
535
+
536
+ std::vector<std::string> echoesOf(const std::string& src) {
537
+ std::vector<std::string> out;
538
+ oscadeval::test::evalSrc(src, [&](const std::string& m) { out.push_back(m); });
539
+ return out;
540
+ }
541
+
542
+ } // namespace
543
+
544
+ TEST(FunctionBuiltinPrecedence, UserFunctionShadowsSameNamedBuiltin) {
545
+ for (bool vm : {false, true}) {
546
+ ScopedVm guard(vm);
547
+ const std::vector<std::string> e = echoesOf("function abs(x) = 999;\necho(abs(-3));");
548
+ ASSERT_EQ(e.size(), 1u) << "vm=" << vm;
549
+ EXPECT_NE(e[0].find("999"), std::string::npos) << "vm=" << vm << ", got " << e[0];
550
+ }
551
+ }
552
+
553
+ TEST(FunctionBuiltinPrecedence, TheBuiltinStillWinsWhenNobodyRedefinesIt) {
554
+ for (bool vm : {false, true}) {
555
+ ScopedVm guard(vm);
556
+ const std::vector<std::string> e = echoesOf("echo(abs(-3));");
557
+ ASSERT_EQ(e.size(), 1u) << "vm=" << vm;
558
+ EXPECT_NE(e[0].find("3"), std::string::npos) << "vm=" << vm << ", got " << e[0];
559
+ EXPECT_EQ(e[0].find("999"), std::string::npos) << "vm=" << vm;
560
+ }
561
+ }
562
+
563
+ TEST(FunctionBuiltinPrecedence, ShadowingHoldsInsideAUserFunctionBody) {
564
+ // The call is inside another function, so on the VM path it is reached
565
+ // through a COMPILED chunk rather than the interpreter -- this is the
566
+ // case a compiler-side fix is needed for.
567
+ for (bool vm : {false, true}) {
568
+ ScopedVm guard(vm);
569
+ const std::vector<std::string> e =
570
+ echoesOf("function abs(x) = 999;\nfunction outer() = abs(-3);\necho(outer());");
571
+ ASSERT_EQ(e.size(), 1u) << "vm=" << vm;
572
+ EXPECT_NE(e[0].find("999"), std::string::npos) << "vm=" << vm << ", got " << e[0];
573
+ }
574
+ }
575
+
576
+ TEST(FunctionBuiltinPrecedence, AUserDefinedIsUndefShadowsTheProbe) {
577
+ // is_undef() is the one builtin with argument handling of its own (it
578
+ // must not warn about the name it is probing). A user's own is_undef
579
+ // takes over completely, arguments evaluated the ordinary way -- so the
580
+ // unknown-variable warning the probe suppresses DOES appear, exactly as
581
+ // upstream.
582
+ for (bool vm : {false, true}) {
583
+ ScopedVm guard(vm);
584
+ std::vector<std::string> msgs;
585
+ oscadeval::test::evalSrc("function is_undef(x) = \"mine\";\necho(is_undef(nothing));",
586
+ [&](const std::string& m) { msgs.push_back(m); });
587
+ bool sawMine = false, sawWarning = false;
588
+ for (const std::string& m : msgs) {
589
+ if (m.find("mine") != std::string::npos) sawMine = true;
590
+ if (m.find("nothing") != std::string::npos) sawWarning = true;
591
+ }
592
+ EXPECT_TRUE(sawMine) << "vm=" << vm;
593
+ EXPECT_TRUE(sawWarning) << "vm=" << vm << ": a user function evaluates its argument normally";
594
+ }
595
+ }
596
+
597
+ TEST(FunctionBuiltinPrecedence, TheIsUndefProbeIsStillSilentWhenNotRedefined) {
598
+ for (bool vm : {false, true}) {
599
+ ScopedVm guard(vm);
600
+ const std::vector<std::string> e = echoesOf("echo(is_undef(nothing));");
601
+ ASSERT_EQ(e.size(), 1u) << "vm=" << vm << ": probing must not warn";
602
+ EXPECT_NE(e[0].find("true"), std::string::npos) << "vm=" << vm << ", got " << e[0];
603
+ }
523
604
  }
524
605
 
525
606
  // -- Builtin argument diagnostics (reference parity) ---------------------
@@ -644,3 +725,102 @@ TEST(StringListBuiltins, ChrIsVariadicAndRangeChecked) {
644
725
  EXPECT_EQ(asStr(evalSrc("chr(55296)", ev)), ""); // 0xD800, a surrogate
645
726
  EXPECT_EQ(asStr(evalSrc("chr(1114112)", ev)), ""); // 0x110000, one past the top
646
727
  }
728
+
729
+ // -- matrix_solve(A, b) --------------------------------------------------
730
+ //
731
+ // One pivoted LU, three answers: x, det, singular. Oracle is BOSL2 plus
732
+ // arithmetic -- OpenSCAD has no equivalent, so there is no reference
733
+ // binary to compare against.
734
+ //
735
+ // Measured against BOSL2 through the CLI, same matrices, identical
736
+ // determinants: n=10 3.17s -> 0.33s, n=11 31.49s -> 0.33s. BOSL2's
737
+ // determinant() is a cofactor expansion, growing x10 per step; this is
738
+ // O(n^3).
739
+
740
+ TEST(MatrixSolve, SolvesASquareSystem) {
741
+ Evaluator ev;
742
+ // asList() returns a reference INTO v, so v has to outlive it -- taking
743
+ // asList(evalSrc(...)) directly dangles the moment the temporary dies.
744
+ const Value v = evalSrc("matrix_solve([[2,1],[1,3]], [5,10]).x", ev);
745
+ const std::vector<Value>& x = asList(v);
746
+ ASSERT_EQ(x.size(), 2u);
747
+ EXPECT_NEAR(asNum(x[0]), 1.0, 1e-12);
748
+ EXPECT_NEAR(asNum(x[1]), 3.0, 1e-12);
749
+ }
750
+
751
+ TEST(MatrixSolve, DeterminantComesFreeFromTheSameFactorisation) {
752
+ Evaluator ev;
753
+ EXPECT_NEAR(asNum(evalSrc("matrix_solve([[2,1],[1,3]]).det", ev)), 5.0, 1e-12);
754
+ EXPECT_NEAR(asNum(evalSrc("matrix_solve([[1,0,0],[0,1,0],[0,0,1]]).det", ev)), 1.0, 1e-12);
755
+ // Sign follows the row swaps, not just the magnitude.
756
+ EXPECT_NEAR(asNum(evalSrc("matrix_solve([[0,1],[1,0]]).det", ev)), -1.0, 1e-12);
757
+ }
758
+
759
+ TEST(MatrixSolve, WithNoRightHandSideThereIsNoSolution) {
760
+ Evaluator ev;
761
+ EXPECT_TRUE(isUndef(evalSrc("matrix_solve([[2,1],[1,3]]).x", ev)));
762
+ }
763
+
764
+ TEST(MatrixSolve, ASingularMatrixAnswersRatherThanWarning) {
765
+ // "Is this matrix singular?" is a fair question to ask, so it is
766
+ // reported, not warned about: det 0, singular true, no solution.
767
+ Evaluator ev;
768
+ EXPECT_TRUE(asBool(evalSrc("matrix_solve([[1,2],[2,4]], [1,2]).singular", ev)));
769
+ EXPECT_NEAR(asNum(evalSrc("matrix_solve([[1,2],[2,4]], [1,2]).det", ev)), 0.0, 1e-12);
770
+ EXPECT_TRUE(isUndef(evalSrc("matrix_solve([[1,2],[2,4]], [1,2]).x", ev)));
771
+ }
772
+
773
+ TEST(MatrixSolve, SingularityIsRelativeToTheMatrixNotAbsolute) {
774
+ // 2*I scaled by 1e-10 is perfectly conditioned, and BOSL2's
775
+ // linear_solve calls it singular -- its test is a fixed absolute 1e-9
776
+ // on R's diagonal with no scaling by the size of the matrix. Verified
777
+ // through the CLI: BOSL2 says SINGULAR at 1e-10 and 1e-12 where this
778
+ // solves correctly.
779
+ Evaluator ev;
780
+ EXPECT_FALSE(asBool(evalSrc("matrix_solve([[2e-10,0],[0,2e-10]], [1e-10,1e-10]).singular", ev)));
781
+ const Value v = evalSrc("matrix_solve([[2e-10,0],[0,2e-10]], [1e-10,1e-10]).x", ev);
782
+ const std::vector<Value>& x = asList(v);
783
+ ASSERT_EQ(x.size(), 2u);
784
+ EXPECT_NEAR(asNum(x[0]), 0.5, 1e-9);
785
+ }
786
+
787
+ TEST(MatrixSolve, AMatrixRightHandSideSolvesEveryColumn) {
788
+ Evaluator ev;
789
+ // Solving against the identity is the inverse; [[2,1],[1,3]] has
790
+ // det 5, so the inverse is [[0.6,-0.2],[-0.2,0.4]].
791
+ const Value v = evalSrc("matrix_solve([[2,1],[1,3]], [[1,0],[0,1]]).x", ev);
792
+ const std::vector<Value>& rows = asList(v);
793
+ ASSERT_EQ(rows.size(), 2u);
794
+ const std::vector<Value>& r0 = asList(rows[0]);
795
+ ASSERT_EQ(r0.size(), 2u);
796
+ EXPECT_NEAR(asNum(r0[0]), 0.6, 1e-12);
797
+ EXPECT_NEAR(asNum(r0[1]), -0.2, 1e-12);
798
+ }
799
+
800
+ TEST(MatrixSolve, RejectsWhatItCannotFactor) {
801
+ Evaluator ev;
802
+ EXPECT_TRUE(isUndef(evalSrc("matrix_solve([[1,2,3],[4,5,6]], [1,2])", ev))); // not square
803
+ EXPECT_TRUE(isUndef(evalSrc("matrix_solve([[1,2],[3,\"x\"]], [1,2])", ev))); // not numeric
804
+ EXPECT_TRUE(isUndef(evalSrc("matrix_solve([[1,2],[3,4]], [1,2,3])", ev))); // wrong rhs length
805
+ EXPECT_TRUE(isUndef(evalSrc("matrix_solve([[1,2],[3,4,5]], [1,2])", ev))); // ragged
806
+ }
807
+
808
+ TEST(MatrixSolve, NonSquareWarnsWithItsShape) {
809
+ const std::vector<std::string> w = builtinWarnings("matrix_solve([[1,2,3],[4,5,6]], [1,2])");
810
+ ASSERT_EQ(w.size(), 1u);
811
+ EXPECT_NE(w[0].find("matrix_solve() requires a square matrix, found 2 x 3"), std::string::npos)
812
+ << "got " << w[0];
813
+ }
814
+
815
+ TEST(MatrixSolve, WorksUnderBothEngines) {
816
+ for (bool vm : {false, true}) {
817
+ ScopedVm guard(vm);
818
+ const std::vector<std::string> e =
819
+ echoesOf("r = matrix_solve([[2,1],[1,3]], [5,10]);\necho(r.x, r.det, r.singular);");
820
+ ASSERT_EQ(e.size(), 1u) << "vm=" << vm;
821
+ EXPECT_NE(e[0].find("[1, 3]"), std::string::npos) << "vm=" << vm << ", got " << e[0];
822
+ EXPECT_NE(e[0].find("5"), std::string::npos) << "vm=" << vm << ", got " << e[0];
823
+ EXPECT_NE(e[0].find("false"), std::string::npos) << "vm=" << vm << ", got " << e[0];
824
+ }
825
+ }
826
+