openscad-cpp-evaluator 0.48.0__tar.gz → 0.49.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.48.0 → openscad_cpp_evaluator-0.49.0}/CLAUDE.md +52 -13
  2. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/PKG-INFO +1 -1
  3. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/pyproject.toml +1 -1
  4. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/src/builtins/function_builtins.cpp +160 -26
  5. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/tests/test_feature_detection.cpp +1 -1
  6. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/tests/test_function_builtins.cpp +89 -30
  7. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/.github/workflows/ci.yml +0 -0
  8. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/.github/workflows/wheels.yml +0 -0
  9. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/.gitignore +0 -0
  10. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/.gitmodules +0 -0
  11. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/CMakeLists.txt +0 -0
  12. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/LICENSE +0 -0
  13. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/README.md +0 -0
  14. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/bindings/CMakeLists.txt +0 -0
  15. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/bindings/ast_to_py.cpp +0 -0
  16. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/bindings/module.cpp +0 -0
  17. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/cmake/embed_font.cmake +0 -0
  18. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/examples/CMakeLists.txt +0 -0
  19. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/examples/eval_perf_benchmark.cpp +0 -0
  20. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/examples/manifold_cache_reuse.cpp +0 -0
  21. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/examples/minimal_debugger.cpp +0 -0
  22. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/.github/workflows/ci.yml +0 -0
  23. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/.gitignore +0 -0
  24. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/CLAUDE.md +0 -0
  25. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/CMakeLists.txt +0 -0
  26. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/README.md +0 -0
  27. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/api.hpp +0 -0
  28. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/ast_node.hpp +0 -0
  29. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/comments.hpp +0 -0
  30. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/declarations.hpp +0 -0
  31. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/expression.hpp +0 -0
  32. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/module_instantiation.hpp +0 -0
  33. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/scope_builder.hpp +0 -0
  34. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast/vector_element.hpp +0 -0
  35. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast.hpp +0 -0
  36. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/comments.hpp +0 -0
  37. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/position.hpp +0 -0
  38. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/pretty_print.hpp +0 -0
  39. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/scope.hpp +0 -0
  40. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/serialization.hpp +0 -0
  41. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/source_map.hpp +0 -0
  42. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/src/CMakeLists.txt +0 -0
  43. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/src/api.cpp +0 -0
  44. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/src/ast/ast_node.cpp +0 -0
  45. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/src/ast/declarations.cpp +0 -0
  46. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/src/ast/expression.cpp +0 -0
  47. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/src/ast/format_utils.hpp +0 -0
  48. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/src/ast/module_instantiation.cpp +0 -0
  49. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/src/ast/scope_builder.cpp +0 -0
  50. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/src/ast/vector_element.cpp +0 -0
  51. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/src/comment_attach_internal.hpp +0 -0
  52. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/src/comments.cpp +0 -0
  53. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/src/grammar/driver.cpp +0 -0
  54. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/src/grammar/driver.hpp +0 -0
  55. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/src/grammar/lexer.l +0 -0
  56. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/src/grammar/lexer_api.hpp +0 -0
  57. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/src/grammar/oscad_location.hpp +0 -0
  58. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/src/grammar/parser.y +0 -0
  59. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/src/inline_comment_attach.cpp +0 -0
  60. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/src/position.cpp +0 -0
  61. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/src/pretty_print.cpp +0 -0
  62. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/src/scope.cpp +0 -0
  63. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/src/serialization/json_io.cpp +0 -0
  64. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/src/serialization/yaml_io.cpp +0 -0
  65. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/src/source_map.cpp +0 -0
  66. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/tests/CMakeLists.txt +0 -0
  67. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/tests/test_assignments.cpp +0 -0
  68. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/tests/test_ast_generation.cpp +0 -0
  69. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/tests/test_comments_and_files.cpp +0 -0
  70. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/tests/test_const_borrow.cpp +0 -0
  71. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/tests/test_control.cpp +0 -0
  72. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/tests/test_expressions.cpp +0 -0
  73. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/tests/test_functions.cpp +0 -0
  74. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/tests/test_helpers.hpp +0 -0
  75. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/tests/test_inline_comments.cpp +0 -0
  76. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/tests/test_lexical.cpp +0 -0
  77. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/tests/test_modules.cpp +0 -0
  78. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/tests/test_node_str.cpp +0 -0
  79. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/tests/test_pretty_print.cpp +0 -0
  80. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/tests/test_render_expression.cpp +0 -0
  81. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/tests/test_scope.cpp +0 -0
  82. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/tests/test_serialization.cpp +0 -0
  83. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/tests/test_smoke.cpp +0 -0
  84. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/tests/test_source_map.cpp +0 -0
  85. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/tests/test_use_include.cpp +0 -0
  86. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/tests/test_vectors.cpp +0 -0
  87. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/tools/cli/CMakeLists.txt +0 -0
  88. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/tools/cli/main.cpp +0 -0
  89. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/bound_args.hpp +0 -0
  90. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/bytecode.hpp +0 -0
  91. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/bytecode_compiler.hpp +0 -0
  92. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/bytecode_vm.hpp +0 -0
  93. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/call_args.hpp +0 -0
  94. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/colored_body.hpp +0 -0
  95. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/csg_node.hpp +0 -0
  96. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/css_colors.hpp +0 -0
  97. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/debug_hooks.hpp +0 -0
  98. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/debug_repl.hpp +0 -0
  99. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/dispatch.hpp +0 -0
  100. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/dxf_svg_import.hpp +0 -0
  101. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/eval_context.hpp +0 -0
  102. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/eval_error.hpp +0 -0
  103. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/eval_use.hpp +0 -0
  104. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/evaluator.hpp +0 -0
  105. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/export.hpp +0 -0
  106. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/font_match.hpp +0 -0
  107. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/font_provider.hpp +0 -0
  108. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/freetype_font_provider.hpp +0 -0
  109. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/function_builtins.hpp +0 -0
  110. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/import_builtin.hpp +0 -0
  111. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/manifold_cache.hpp +0 -0
  112. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/mesh_check.hpp +0 -0
  113. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/mesh_import.hpp +0 -0
  114. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/osc_range.hpp +0 -0
  115. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/profile.hpp +0 -0
  116. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/scope_trail.hpp +0 -0
  117. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/segments.hpp +0 -0
  118. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/surface_load.hpp +0 -0
  119. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/text_metrics.hpp +0 -0
  120. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/value.hpp +0 -0
  121. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/zip_stored.hpp +0 -0
  122. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/python/openscad_cpp_evaluator/__init__.py +0 -0
  123. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/resources/fonts/LICENSE +0 -0
  124. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/resources/fonts/LiberationSans-Regular.ttf +0 -0
  125. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/src/CMakeLists.txt +0 -0
  126. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/src/builtins/booleans.cpp +0 -0
  127. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/src/builtins/builtins.hpp +0 -0
  128. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/src/builtins/call_args.cpp +0 -0
  129. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/src/builtins/color.cpp +0 -0
  130. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/src/builtins/control.cpp +0 -0
  131. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/src/builtins/dxf_dim.cpp +0 -0
  132. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/src/builtins/extrude.cpp +0 -0
  133. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/src/builtins/import.cpp +0 -0
  134. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/src/builtins/modifiers.cpp +0 -0
  135. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/src/builtins/primitives_2d.cpp +0 -0
  136. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/src/builtins/primitives_3d.cpp +0 -0
  137. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/src/builtins/registry.cpp +0 -0
  138. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/src/builtins/roof.cpp +0 -0
  139. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/src/builtins/segments.cpp +0 -0
  140. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/src/builtins/surface.cpp +0 -0
  141. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/src/builtins/text.cpp +0 -0
  142. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/src/builtins/topology.cpp +0 -0
  143. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/src/builtins/transforms.cpp +0 -0
  144. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/src/bytecode_compiler.cpp +0 -0
  145. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/src/bytecode_vm.cpp +0 -0
  146. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/src/colored_body.cpp +0 -0
  147. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/src/csg_generate.cpp +0 -0
  148. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/src/csg_resolve.cpp +0 -0
  149. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/src/css_colors.cpp +0 -0
  150. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/src/debug/debug_repl.cpp +0 -0
  151. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/src/debug_profile.cpp +0 -0
  152. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/src/eval_context.cpp +0 -0
  153. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/src/eval_error.cpp +0 -0
  154. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/src/eval_use.cpp +0 -0
  155. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/src/export.cpp +0 -0
  156. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/src/expr_eval.cpp +0 -0
  157. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/src/import/dxf_import.cpp +0 -0
  158. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/src/import/mesh_import.cpp +0 -0
  159. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/src/import/surface_load.cpp +0 -0
  160. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/src/import/svg_import.cpp +0 -0
  161. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/src/manifold_cache.cpp +0 -0
  162. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/src/measure_geometry.cpp +0 -0
  163. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/src/mesh_check.cpp +0 -0
  164. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/src/stmt_eval.cpp +0 -0
  165. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/src/text/font_match.cpp +0 -0
  166. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/src/text/freetype_font_provider.cpp +0 -0
  167. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/src/text/text_metrics.cpp +0 -0
  168. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/src/user_calls.cpp +0 -0
  169. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/src/value.cpp +0 -0
  170. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/src/zip_stored.cpp +0 -0
  171. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/tests/CMakeLists.txt +0 -0
  172. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/tests/test_booleans.cpp +0 -0
  173. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/tests/test_bytecode_compiler.cpp +0 -0
  174. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/tests/test_children_separate.cpp +0 -0
  175. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/tests/test_cli.cpp +0 -0
  176. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/tests/test_control_flow.cpp +0 -0
  177. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/tests/test_csg_tree.cpp +0 -0
  178. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/tests/test_debug_hooks.cpp +0 -0
  179. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/tests/test_dxf_svg_import.cpp +0 -0
  180. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/tests/test_eval_context.cpp +0 -0
  181. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/tests/test_export.cpp +0 -0
  182. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/tests/test_expr_eval.cpp +0 -0
  183. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/tests/test_extrude_roof.cpp +0 -0
  184. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/tests/test_helpers.hpp +0 -0
  185. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/tests/test_import_export.cpp +0 -0
  186. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/tests/test_manifold_cache.cpp +0 -0
  187. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/tests/test_mesh_check.cpp +0 -0
  188. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/tests/test_multi_color_merge.cpp +0 -0
  189. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/tests/test_primitives.cpp +0 -0
  190. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/tests/test_profiling.cpp +0 -0
  191. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/tests/test_python_bindings.py +0 -0
  192. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/tests/test_render_expr.cpp +0 -0
  193. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/tests/test_scope_trail.cpp +0 -0
  194. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/tests/test_scoping.cpp +0 -0
  195. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/tests/test_surface.cpp +0 -0
  196. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/tests/test_tail_calls.cpp +0 -0
  197. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/tests/test_text.cpp +0 -0
  198. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/tests/test_transforms.cpp +0 -0
  199. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/tests/test_unexpected_args.cpp +0 -0
  200. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/tests/test_use.cpp +0 -0
  201. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/tests/test_value.cpp +0 -0
  202. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/tests/test_viewport_params.cpp +0 -0
  203. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/tests/test_zip_stored.cpp +0 -0
  204. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/tools/cli/CMakeLists.txt +0 -0
  205. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/tools/cli/cli_lib.cpp +0 -0
  206. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/tools/cli/cli_lib.hpp +0 -0
  207. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/tools/cli/main.cpp +0 -0
  208. {openscad_cpp_evaluator-0.48.0 → openscad_cpp_evaluator-0.49.0}/uv.lock +0 -0
@@ -460,19 +460,58 @@ grep for `ponytail:`.
460
460
  unknown arguments -- `children(separate=true)` renders the wrong shape there with no warning --
461
461
  and both read as `undef` in OpenSCAD, so a guard written against them works in both.
462
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.
463
+ - **`linear_solve(A, b)`** (`src/builtins/function_builtins.cpp`, feature name `linear-solve`) —
464
+ returns `.x` (solution), `.det`, `.singular`. Dispatches on shape:
465
+
466
+ | shape | method | `.x` | `.det` |
467
+ |---|---|---|---|
468
+ | m = n | pivoted **LU** | the solution | product of U's diagonal, signed by row swaps |
469
+ | m > n | **Householder QR** | least squares | `undef` |
470
+ | m < n | **QR of Aᵀ** | minimum norm | `undef` |
471
+
472
+ Q is never formed. The reflectors are stored below the diagonal (v[0]
473
+ normalised to 1, scale in `taus`) and applied in forward order for `Qᵀb`,
474
+ reverse order for `Qy` — which is why this is far smaller and quicker than
475
+ BOSL2, whose QR materialises an m×m Q *and* an n×n permutation matrix and
476
+ then multiplies by it. **Minimum norm** comes from padding `y` with zeros to
477
+ length n before applying Q; a residual check alone will NOT catch getting
478
+ that wrong, since any solution passes it — assert the norm.
479
+ Rank deficiency without column pivoting cannot distinguish rank-deficient
480
+ from very ill-conditioned. It is a relative-tolerance heuristic on the
481
+ diagonal; say so rather than implying it is exact.
482
+
483
+ **Named to collide, deliberately.** BOSL2 defines `linear_solve()`, and since
484
+ 0.47.0 a user function *shadows* a builtin — so this is unreachable from any
485
+ script that includes BOSL2 until BOSL2 drops its own. That is the intended
486
+ path (BelfrySCAD's author maintains BOSL2); callers then gain a `.x`, since
487
+ BOSL2's returns the bare solution and `[]` for singular. **Do not "fix" the
488
+ shadowing to make this reachable** — the shadowing is the correct, reference-
489
+ matching behaviour, and was itself a bug fix.
490
+
491
+ Measured per call, BOSL2 load subtracted, enough repetitions to clear noise
492
+ (harness checked: 200/2000/20000 reps at n=64 give 43/45/43 µs):
493
+
494
+ | n | BOSL2 `linear_solve` | ours | | n | BOSL2 `determinant` | ours |
495
+ |---:|---:|---:|---:|---:|---:|---:|
496
+ | 4 | 280 µs | 1.5 µs | 187× | 10 | 3.17 s | 0.33 s |
497
+ | 16 | 2.58 ms | 4.0 µs | 644× | 11 | 31.49 s | 0.33 s |
498
+ | 64 | 42.5 ms | 43 µs | 988× | | *cofactor expansion, O(n!)* | |
499
+
500
+ **Do not repeat the claim this replaces**: that BOSL2's solve "is not the
501
+ problem (128×128 in ~0.18s)". That was one-shot, with the solve lost inside
502
+ BOSL2's own load time. Keep the comparison fair though — BOSL2 does QR where
503
+ the square path here does LU, and it validates arguments every call.
504
+ Also correctness, not only speed: BOSL2's singularity test is a fixed
505
+ **absolute** `1e-9` on R's diagonal, unscaled by ‖A‖, so `2*I` scaled by
506
+ 1e-10 is called singular there and solves correctly here.
507
+
508
+ **Profiling that shaped the scope** (don't redo it): NURBS *evaluation* is
509
+ 0.6% linear algebra — basis eval 44%, type checks 25%. NURBS *interpolation*
510
+ is 35% at n=400, and every hot solve is **square**, so the QR paths are a
511
+ capability, not a measured win. `plane_intersection` (2×3, underdetermined)
512
+ is 0.1% of a `rounded_prism` model. The recurring cost across every workload
513
+ profiled is BOSL2's **argument type-checking**: 25–45%, millions of
514
+ `is_finite`/`is_num`/`is_nan` calls.
476
515
 
477
516
  - `include/openscad_cpp_evaluator/value.hpp`, `src/value.cpp` — the `Value` variant (OpenSCAD's
478
517
  dynamic value type) and its free-function arithmetic/comparison helpers. See the plan's §1 for
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openscad_cpp_evaluator
3
- Version: 0.48.0
3
+ Version: 0.49.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.48.0"
7
+ version = "0.49.0"
8
8
  description = "C++ OpenSCAD evaluator with Python bindings"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -169,7 +169,7 @@ Value builtinCross(const Value& aArg, const Value& bArg) {
169
169
  return Value{};
170
170
  }
171
171
 
172
- // -- matrix_solve(A, b) --------------------------------------------------
172
+ // -- linear_solve(A, b) --------------------------------------------------
173
173
  //
174
174
  // One pivoted LU answers three questions at once -- the solution, the
175
175
  // determinant, and whether A is singular -- because all three fall out of
@@ -212,26 +212,96 @@ std::optional<std::pair<std::vector<double>, size_t>> numericMatrix(const Value&
212
212
  return std::make_pair(std::move(flat), cols);
213
213
  }
214
214
 
215
- Value builtinMatrixSolve(Evaluator& ev, const Value& aArg, const Value& bArg, bool haveB,
215
+ // Householder QR of a tall matrix `a` (m rows, n cols, m >= n), in place.
216
+ //
217
+ // On return the upper triangle of `a` is R. Below the diagonal, column j
218
+ // holds the reflector vector v_j from element 1 onwards -- v_j[0] is
219
+ // normalised to 1 and therefore not stored -- and `taus[j]` is its scale,
220
+ // so H_j = I - tau_j v_j v_j^T. Q is never formed: everything that needs
221
+ // it applies the reflectors instead, which is the whole reason this is
222
+ // smaller and quicker than materialising an m x m matrix.
223
+ //
224
+ // The sign of alpha is chosen away from x[0] so that v[0] can never cancel
225
+ // to zero -- the textbook guard against catastrophic cancellation here.
226
+ //
227
+ // Returns false if a diagonal falls at or below `tol`, i.e. rank-deficient
228
+ // to the tolerance given. Without column pivoting this cannot tell
229
+ // rank-deficient from merely very ill-conditioned; see the caller's note.
230
+ bool householderQr(std::vector<double>& a, size_t m, size_t n, std::vector<double>& taus, double tol) {
231
+ taus.assign(n, 0.0);
232
+ for (size_t j = 0; j < n; ++j) {
233
+ double normx = 0.0;
234
+ for (size_t r = j; r < m; ++r) normx += a[r * n + j] * a[r * n + j];
235
+ normx = std::sqrt(normx);
236
+ if (normx <= tol) return false;
237
+
238
+ const double x0 = a[j * n + j];
239
+ const double alpha = (x0 >= 0.0) ? -normx : normx;
240
+ const double v0 = x0 - alpha;
241
+ // Store v scaled so v[0] == 1; the below-diagonal entries are the tail.
242
+ double vv = 1.0;
243
+ for (size_t r = j + 1; r < m; ++r) {
244
+ a[r * n + j] /= v0;
245
+ vv += a[r * n + j] * a[r * n + j];
246
+ }
247
+ taus[j] = 2.0 / vv;
248
+ a[j * n + j] = alpha;
249
+
250
+ // Apply H_j to the trailing columns.
251
+ for (size_t c = j + 1; c < n; ++c) {
252
+ double dot = a[j * n + c];
253
+ for (size_t r = j + 1; r < m; ++r) dot += a[r * n + j] * a[r * n + c];
254
+ const double f = taus[j] * dot;
255
+ a[j * n + c] -= f;
256
+ for (size_t r = j + 1; r < m; ++r) a[r * n + c] -= f * a[r * n + j];
257
+ }
258
+ }
259
+ return true;
260
+ }
261
+
262
+ // b <- Q^T b, for b with `k` columns. Reflectors applied in forward order.
263
+ void applyQtranspose(const std::vector<double>& a, size_t m, size_t n, const std::vector<double>& taus,
264
+ std::vector<double>& b, size_t k) {
265
+ for (size_t j = 0; j < n; ++j) {
266
+ for (size_t c = 0; c < k; ++c) {
267
+ double dot = b[j * k + c];
268
+ for (size_t r = j + 1; r < m; ++r) dot += a[r * n + j] * b[r * k + c];
269
+ const double f = taus[j] * dot;
270
+ b[j * k + c] -= f;
271
+ for (size_t r = j + 1; r < m; ++r) b[r * k + c] -= f * a[r * n + j];
272
+ }
273
+ }
274
+ }
275
+
276
+ // z <- Q z. Same reflectors, reverse order -- that is the only difference
277
+ // between applying Q and applying Q^T.
278
+ void applyQ(const std::vector<double>& a, size_t m, size_t n, const std::vector<double>& taus,
279
+ std::vector<double>& z, size_t k) {
280
+ for (size_t jj = n; jj-- > 0;) {
281
+ for (size_t c = 0; c < k; ++c) {
282
+ double dot = z[jj * k + c];
283
+ for (size_t r = jj + 1; r < m; ++r) dot += a[r * n + jj] * z[r * k + c];
284
+ const double f = taus[jj] * dot;
285
+ z[jj * k + c] -= f;
286
+ for (size_t r = jj + 1; r < m; ++r) z[r * k + c] -= f * a[r * n + jj];
287
+ }
288
+ }
289
+ }
290
+
291
+ Value builtinLinearSolve(Evaluator& ev, const Value& aArg, const Value& bArg, bool haveB,
216
292
  const oscad::ASTNode& node) {
217
293
  const auto parsed = numericMatrix(aArg);
218
294
  if (!parsed) {
219
- ev.warn("matrix_solve() requires a matrix of numbers", &node.position());
295
+ ev.warn("linear_solve() requires a matrix of numbers", &node.position());
220
296
  return Value{};
221
297
  }
222
298
  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
- }
299
+ const size_t n = parsed->second; // columns == unknowns
300
+ const size_t m = lu.size() / n; // rows == equations
231
301
  double maxAbs = 0.0;
232
302
  for (double x : lu) {
233
303
  if (!std::isfinite(x)) {
234
- ev.warn("matrix_solve() matrix contains a non-finite value", &node.position());
304
+ ev.warn("linear_solve() matrix contains a non-finite value", &node.position());
235
305
  return Value{};
236
306
  }
237
307
  maxAbs = std::max(maxAbs, std::abs(x));
@@ -243,22 +313,22 @@ Value builtinMatrixSolve(Evaluator& ev, const Value& aArg, const Value& bArg, bo
243
313
  bool bWasVector = false;
244
314
  std::vector<double> rhs;
245
315
  if (haveB) {
246
- if (const std::optional<std::vector<double>> vec = allNumericList(bArg); vec && vec->size() == n) {
316
+ if (const std::optional<std::vector<double>> vec = allNumericList(bArg); vec && vec->size() == m) {
247
317
  bWasVector = true;
248
318
  k = 1;
249
319
  rhs = *vec;
250
- } else if (const auto bm = numericMatrix(bArg); bm && bm->first.size() / bm->second == n) {
320
+ } else if (const auto bm = numericMatrix(bArg); bm && bm->first.size() / bm->second == m) {
251
321
  k = bm->second;
252
322
  rhs = bm->first;
253
323
  } else {
254
- ev.warn("matrix_solve() right-hand side must be a vector of " + std::to_string(n) +
324
+ ev.warn("linear_solve() right-hand side must be a vector of " + std::to_string(m) +
255
325
  " numbers, or a matrix with that many rows",
256
326
  &node.position());
257
327
  return Value{};
258
328
  }
259
329
  for (double x : rhs) {
260
330
  if (!std::isfinite(x)) {
261
- ev.warn("matrix_solve() right-hand side contains a non-finite value", &node.position());
331
+ ev.warn("linear_solve() right-hand side contains a non-finite value", &node.position());
262
332
  return Value{};
263
333
  }
264
334
  }
@@ -268,8 +338,72 @@ Value builtinMatrixSolve(Evaluator& ev, const Value& aArg, const Value& bArg, bo
268
338
  // fixed ABSOLUTE 1e-9 (its _EPSILON) with no scaling by the size of the
269
339
  // matrix, so a perfectly well-conditioned system scaled down by 1e-10
270
340
  // 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);
341
+ // linear_solve(A*1e-10) still solvable.
342
+ const double tol =
343
+ std::numeric_limits<double>::epsilon() * static_cast<double>(std::max(m, n)) * std::max(maxAbs, 1.0);
344
+
345
+ // Non-square: QR, and no determinant to report.
346
+ //
347
+ // m > n overdetermined -> least squares. Factor A, apply Q^T to b, then
348
+ // back-substitute the leading n x n block of R.
349
+ // m < n underdetermined -> minimum norm. Factor A^T instead, forward-solve
350
+ // R^T y = b, and return Q [y; 0]. Padding with zeros is what makes
351
+ // it the SMALLEST solution rather than just any solution.
352
+ if (m != n) {
353
+ std::vector<std::pair<std::string, Value>> outNs;
354
+ std::vector<double> taus;
355
+ const auto answer = [&](Value x, bool sing) {
356
+ outNs.emplace_back("x", std::move(x));
357
+ outNs.emplace_back("det", Value{}); // undefined for a non-square matrix
358
+ outNs.emplace_back("singular", Value{sing});
359
+ return objectOf(std::move(outNs));
360
+ };
361
+ const auto shape = [&](const std::vector<double>& v, size_t rows, size_t cols) {
362
+ if (bWasVector) return numList(v);
363
+ std::vector<Value> rowsOut;
364
+ rowsOut.reserve(rows);
365
+ for (size_t r = 0; r < rows; ++r) {
366
+ rowsOut.push_back(numList(std::vector<double>(v.begin() + static_cast<long>(r * cols),
367
+ v.begin() + static_cast<long>((r + 1) * cols))));
368
+ }
369
+ return listOf(std::move(rowsOut));
370
+ };
371
+
372
+ if (m > n) {
373
+ if (!householderQr(lu, m, n, taus, tol)) return answer(Value{}, true);
374
+ if (!haveB) return answer(Value{}, false);
375
+ applyQtranspose(lu, m, n, taus, rhs, k);
376
+ for (size_t row = n; row-- > 0;) {
377
+ for (size_t c = 0; c < k; ++c) {
378
+ double acc = rhs[row * k + c];
379
+ for (size_t j = row + 1; j < n; ++j) acc -= lu[row * n + j] * rhs[j * k + c];
380
+ rhs[row * k + c] = acc / lu[row * n + row];
381
+ }
382
+ }
383
+ rhs.resize(n * k);
384
+ return answer(shape(rhs, n, k), false);
385
+ }
386
+
387
+ // m < n: factor the transpose, which is the tall one.
388
+ std::vector<double> at(n * m);
389
+ for (size_t r = 0; r < m; ++r)
390
+ for (size_t c = 0; c < n; ++c) at[c * m + r] = lu[r * n + c];
391
+ if (!householderQr(at, n, m, taus, tol)) return answer(Value{}, true);
392
+ if (!haveB) return answer(Value{}, false);
393
+ // Forward-solve R^T y = b (R is m x m upper, so R^T is lower).
394
+ for (size_t row = 0; row < m; ++row) {
395
+ for (size_t c = 0; c < k; ++c) {
396
+ double acc = rhs[row * k + c];
397
+ for (size_t j = 0; j < row; ++j) acc -= at[j * m + row] * rhs[j * k + c];
398
+ rhs[row * k + c] = acc / at[row * m + row];
399
+ }
400
+ }
401
+ std::vector<double> z(n * k, 0.0);
402
+ for (size_t r = 0; r < m; ++r)
403
+ for (size_t c = 0; c < k; ++c) z[r * k + c] = rhs[r * k + c];
404
+ applyQ(at, n, m, taus, z, k);
405
+ return answer(shape(z, n, k), false);
406
+ }
273
407
 
274
408
  double det = 1.0;
275
409
  bool singular = false;
@@ -301,7 +435,7 @@ Value builtinMatrixSolve(Evaluator& ev, const Value& aArg, const Value& bArg, bo
301
435
  std::vector<std::pair<std::string, Value>> out;
302
436
  if (singular) {
303
437
  // Not a misuse -- "is this matrix singular?" is a legitimate
304
- // question to ask matrix_solve, so it answers rather than warning.
438
+ // question to ask linear_solve, so it answers rather than warning.
305
439
  out.emplace_back("x", Value{});
306
440
  out.emplace_back("det", Value{0.0});
307
441
  out.emplace_back("singular", Value{true});
@@ -616,7 +750,7 @@ bool isBuiltinFunctionName(const std::string& name) {
616
750
  "chr", "ord", "is_undef", "is_num", "is_bool", "is_string", "is_list", "is_function", "is_object",
617
751
  "search", "lookup", "has_key", "version", "version_num", "parent_module",
618
752
  "object", "textmetrics", "fontmetrics", "dxf_dim", "dxf_cross", "supported_feature",
619
- "matrix_solve",
753
+ "linear_solve",
620
754
  };
621
755
  return names.count(name) > 0;
622
756
  }
@@ -758,7 +892,7 @@ enum class BuiltinFnId {
758
892
  TextMetrics, FontMetrics, Abs, Sign, Ceil, Floor, Round, Sqrt, Ln, Log, Exp, Sin, Cos, Tan,
759
893
  Asin, Acos, Atan, Atan2, Max, Min, Pow, Norm, Cross, Rands, Concat, Len, Str, Chr, Ord,
760
894
  IsUndef, IsNum, IsBool, IsString, IsList, IsFunction, IsObject, Search, Lookup, HasKey,
761
- Version, VersionNum, ParentModule, DxfDim, DxfCross, SupportedFeature, MatrixSolve,
895
+ Version, VersionNum, ParentModule, DxfDim, DxfCross, SupportedFeature, LinearSolve,
762
896
  };
763
897
 
764
898
  // supported_feature("name") -> the level at which this build implements that
@@ -778,7 +912,7 @@ enum class BuiltinFnId {
778
912
  const std::unordered_map<std::string, double>& featureLevels() {
779
913
  static const std::unordered_map<std::string, double> levels = {
780
914
  {"render-expr", 1.0}, // render() in expression position
781
- {"matrix-solve", 1.0}, // matrix_solve(A, b) -> {x, det, singular}
915
+ {"linear-solve", 1.0}, // linear_solve(A, b) -> {x, det, singular}
782
916
  {"polyhedron-vnf", 1.0}, // polyhedron(vnf) / polyhedron(object)
783
917
  {"separate-children", 1.0}, // children(..., separate=true)
784
918
  {"minkowski-diff", 1.0}, // minkowski_difference()
@@ -813,7 +947,7 @@ const std::unordered_map<std::string, BuiltinFnId>& builtinFnIds() {
813
947
  {"parent_module", BuiltinFnId::ParentModule},
814
948
  {"dxf_dim", BuiltinFnId::DxfDim}, {"dxf_cross", BuiltinFnId::DxfCross},
815
949
  {"supported_feature", BuiltinFnId::SupportedFeature},
816
- {"matrix_solve", BuiltinFnId::MatrixSolve},
950
+ {"linear_solve", BuiltinFnId::LinearSolve},
817
951
  };
818
952
  return ids;
819
953
  }
@@ -895,7 +1029,7 @@ const std::unordered_map<int, BuiltinCheck>& builtinChecks() {
895
1029
  add(BuiltinFnId::ParentModule, {0, 1, "1", {kNum}});
896
1030
  add(BuiltinFnId::HasKey, {-1, -1, nullptr, {{TObj, "object"}, {TStr, "string"}}});
897
1031
  add(BuiltinFnId::SupportedFeature, {1, 1, "1", {}});
898
- add(BuiltinFnId::MatrixSolve, {1, 2, "1 or 2", {kVec}});
1032
+ add(BuiltinFnId::LinearSolve, {1, 2, "1 or 2", {kVec}});
899
1033
  for (BuiltinFnId id : {BuiltinFnId::IsUndef, BuiltinFnId::IsNum, BuiltinFnId::IsBool,
900
1034
  BuiltinFnId::IsString, BuiltinFnId::IsList, BuiltinFnId::IsFunction,
901
1035
  BuiltinFnId::IsObject}) {
@@ -1210,8 +1344,8 @@ Value evalBuiltinFunction(Evaluator& ev, const std::string& name, const CallArgs
1210
1344
  // The OpenSCAD release we track. version_num() is that same
1211
1345
  // year/month/day folded as y * 10000 + m * 100 + d, exactly like the
1212
1346
  // 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{}),
1347
+ case BuiltinFnId::LinearSolve:
1348
+ return builtinLinearSolve(ev, getArg(args, 0, "A", Value{}), getArg(args, 1, "b", Value{}),
1215
1349
  args.findPositional(1) != nullptr || args.findNamed("b") != nullptr, node);
1216
1350
  case BuiltinFnId::SupportedFeature: {
1217
1351
  const Value name = getArg(args, 0, "feature", Value{});
@@ -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", "matrix-solve"};
38
+ "render-expr", "polyhedron-vnf", "linear-solve"};
39
39
 
40
40
  } // namespace
41
41
 
@@ -726,7 +726,7 @@ TEST(StringListBuiltins, ChrIsVariadicAndRangeChecked) {
726
726
  EXPECT_EQ(asStr(evalSrc("chr(1114112)", ev)), ""); // 0x110000, one past the top
727
727
  }
728
728
 
729
- // -- matrix_solve(A, b) --------------------------------------------------
729
+ // -- linear_solve(A, b) --------------------------------------------------
730
730
  //
731
731
  // One pivoted LU, three answers: x, det, singular. Oracle is BOSL2 plus
732
732
  // arithmetic -- OpenSCAD has no equivalent, so there is no reference
@@ -737,58 +737,58 @@ TEST(StringListBuiltins, ChrIsVariadicAndRangeChecked) {
737
737
  // determinant() is a cofactor expansion, growing x10 per step; this is
738
738
  // O(n^3).
739
739
 
740
- TEST(MatrixSolve, SolvesASquareSystem) {
740
+ TEST(LinearSolve, SolvesASquareSystem) {
741
741
  Evaluator ev;
742
742
  // asList() returns a reference INTO v, so v has to outlive it -- taking
743
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);
744
+ const Value v = evalSrc("linear_solve([[2,1],[1,3]], [5,10]).x", ev);
745
745
  const std::vector<Value>& x = asList(v);
746
746
  ASSERT_EQ(x.size(), 2u);
747
747
  EXPECT_NEAR(asNum(x[0]), 1.0, 1e-12);
748
748
  EXPECT_NEAR(asNum(x[1]), 3.0, 1e-12);
749
749
  }
750
750
 
751
- TEST(MatrixSolve, DeterminantComesFreeFromTheSameFactorisation) {
751
+ TEST(LinearSolve, DeterminantComesFreeFromTheSameFactorisation) {
752
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);
753
+ EXPECT_NEAR(asNum(evalSrc("linear_solve([[2,1],[1,3]]).det", ev)), 5.0, 1e-12);
754
+ EXPECT_NEAR(asNum(evalSrc("linear_solve([[1,0,0],[0,1,0],[0,0,1]]).det", ev)), 1.0, 1e-12);
755
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);
756
+ EXPECT_NEAR(asNum(evalSrc("linear_solve([[0,1],[1,0]]).det", ev)), -1.0, 1e-12);
757
757
  }
758
758
 
759
- TEST(MatrixSolve, WithNoRightHandSideThereIsNoSolution) {
759
+ TEST(LinearSolve, WithNoRightHandSideThereIsNoSolution) {
760
760
  Evaluator ev;
761
- EXPECT_TRUE(isUndef(evalSrc("matrix_solve([[2,1],[1,3]]).x", ev)));
761
+ EXPECT_TRUE(isUndef(evalSrc("linear_solve([[2,1],[1,3]]).x", ev)));
762
762
  }
763
763
 
764
- TEST(MatrixSolve, ASingularMatrixAnswersRatherThanWarning) {
764
+ TEST(LinearSolve, ASingularMatrixAnswersRatherThanWarning) {
765
765
  // "Is this matrix singular?" is a fair question to ask, so it is
766
766
  // reported, not warned about: det 0, singular true, no solution.
767
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)));
768
+ EXPECT_TRUE(asBool(evalSrc("linear_solve([[1,2],[2,4]], [1,2]).singular", ev)));
769
+ EXPECT_NEAR(asNum(evalSrc("linear_solve([[1,2],[2,4]], [1,2]).det", ev)), 0.0, 1e-12);
770
+ EXPECT_TRUE(isUndef(evalSrc("linear_solve([[1,2],[2,4]], [1,2]).x", ev)));
771
771
  }
772
772
 
773
- TEST(MatrixSolve, SingularityIsRelativeToTheMatrixNotAbsolute) {
773
+ TEST(LinearSolve, SingularityIsRelativeToTheMatrixNotAbsolute) {
774
774
  // 2*I scaled by 1e-10 is perfectly conditioned, and BOSL2's
775
775
  // linear_solve calls it singular -- its test is a fixed absolute 1e-9
776
776
  // on R's diagonal with no scaling by the size of the matrix. Verified
777
777
  // through the CLI: BOSL2 says SINGULAR at 1e-10 and 1e-12 where this
778
778
  // solves correctly.
779
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);
780
+ EXPECT_FALSE(asBool(evalSrc("linear_solve([[2e-10,0],[0,2e-10]], [1e-10,1e-10]).singular", ev)));
781
+ const Value v = evalSrc("linear_solve([[2e-10,0],[0,2e-10]], [1e-10,1e-10]).x", ev);
782
782
  const std::vector<Value>& x = asList(v);
783
783
  ASSERT_EQ(x.size(), 2u);
784
784
  EXPECT_NEAR(asNum(x[0]), 0.5, 1e-9);
785
785
  }
786
786
 
787
- TEST(MatrixSolve, AMatrixRightHandSideSolvesEveryColumn) {
787
+ TEST(LinearSolve, AMatrixRightHandSideSolvesEveryColumn) {
788
788
  Evaluator ev;
789
789
  // Solving against the identity is the inverse; [[2,1],[1,3]] has
790
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);
791
+ const Value v = evalSrc("linear_solve([[2,1],[1,3]], [[1,0],[0,1]]).x", ev);
792
792
  const std::vector<Value>& rows = asList(v);
793
793
  ASSERT_EQ(rows.size(), 2u);
794
794
  const std::vector<Value>& r0 = asList(rows[0]);
@@ -797,26 +797,85 @@ TEST(MatrixSolve, AMatrixRightHandSideSolvesEveryColumn) {
797
797
  EXPECT_NEAR(asNum(r0[1]), -0.2, 1e-12);
798
798
  }
799
799
 
800
- TEST(MatrixSolve, RejectsWhatItCannotFactor) {
800
+ TEST(LinearSolve, RejectsWhatItCannotFactor) {
801
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
802
+ EXPECT_TRUE(isUndef(evalSrc("linear_solve([[1,2],[3,\"x\"]], [1,2])", ev))); // not numeric
803
+ EXPECT_TRUE(isUndef(evalSrc("linear_solve([[1,2],[3,4]], [1,2,3])", ev))); // wrong rhs length
804
+ EXPECT_TRUE(isUndef(evalSrc("linear_solve([[1,2],[3,4,5]], [1,2])", ev))); // ragged
806
805
  }
807
806
 
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];
807
+ // -- non-square: QR ------------------------------------------------------
808
+ //
809
+ // m > n is least squares, m < n is minimum norm -- the same two answers
810
+ // BOSL2's linear_solve gives, and cross-checked against it.
811
+
812
+ TEST(LinearSolve, OverdeterminedGivesTheLeastSquaresFit) {
813
+ // Fit y = a + b*x to (0,1) (1,3) (2,5) (3,7): exactly on the line
814
+ // y = 1 + 2x, so the residual is zero and a=1, b=2.
815
+ Evaluator ev;
816
+ const Value v = evalSrc("linear_solve([[1,0],[1,1],[1,2],[1,3]], [1,3,5,7]).x", ev);
817
+ const std::vector<Value>& x = asList(v);
818
+ ASSERT_EQ(x.size(), 2u);
819
+ EXPECT_NEAR(asNum(x[0]), 1.0, 1e-10);
820
+ EXPECT_NEAR(asNum(x[1]), 2.0, 1e-10);
821
+ }
822
+
823
+ TEST(LinearSolve, OverdeterminedWithNoExactSolutionMinimisesTheResidual) {
824
+ // (0,0) (1,1) (2,2) (3,10): no line fits. The normal equations put the
825
+ // least-squares line at y = -1.4 + 3.1x -- computed independently, not
826
+ // read off this implementation.
827
+ Evaluator ev;
828
+ const Value v = evalSrc("linear_solve([[1,0],[1,1],[1,2],[1,3]], [0,1,2,10]).x", ev);
829
+ const std::vector<Value>& x = asList(v);
830
+ ASSERT_EQ(x.size(), 2u);
831
+ EXPECT_NEAR(asNum(x[0]), -1.4, 1e-9);
832
+ EXPECT_NEAR(asNum(x[1]), 3.1, 1e-9);
813
833
  }
814
834
 
815
- TEST(MatrixSolve, WorksUnderBothEngines) {
835
+ TEST(LinearSolve, UnderdeterminedGivesTheMinimUmNormSolution) {
836
+ // x + y = 2 has infinitely many solutions; the smallest is [1,1].
837
+ // Anything satisfying the equation passes a residual check, so this
838
+ // asserts the NORM -- that is the whole claim of a minimum-norm solve.
839
+ Evaluator ev;
840
+ const Value v = evalSrc("linear_solve([[1,1]], [2]).x", ev);
841
+ const std::vector<Value>& x = asList(v);
842
+ ASSERT_EQ(x.size(), 2u);
843
+ EXPECT_NEAR(asNum(x[0]), 1.0, 1e-12);
844
+ EXPECT_NEAR(asNum(x[1]), 1.0, 1e-12);
845
+ }
846
+
847
+ TEST(LinearSolve, UnderdeterminedSatisfiesEveryEquation) {
848
+ // 2 x 3: the plane_intersection shape BOSL2 relies on.
849
+ Evaluator ev;
850
+ const Value v = evalSrc("linear_solve([[1,0,1],[0,1,1]], [3,5]).x", ev);
851
+ const std::vector<Value>& x = asList(v);
852
+ ASSERT_EQ(x.size(), 3u);
853
+ const double a = asNum(x[0]), b = asNum(x[1]), c = asNum(x[2]);
854
+ EXPECT_NEAR(a + c, 3.0, 1e-12);
855
+ EXPECT_NEAR(b + c, 5.0, 1e-12);
856
+ // minimum norm: the component along the null direction [1,1,-1] is zero
857
+ EXPECT_NEAR(a + b - c, 0.0, 1e-9);
858
+ }
859
+
860
+ TEST(LinearSolve, NonSquareHasNoDeterminant) {
861
+ Evaluator ev;
862
+ EXPECT_TRUE(isUndef(evalSrc("linear_solve([[1,0],[1,1],[1,2]], [1,2,3]).det", ev)));
863
+ EXPECT_FALSE(asBool(evalSrc("linear_solve([[1,0],[1,1],[1,2]], [1,2,3]).singular", ev)));
864
+ }
865
+
866
+ TEST(LinearSolve, RankDeficientNonSquareIsReportedNotWrong) {
867
+ // Two identical columns: rank 1, not 2.
868
+ Evaluator ev;
869
+ EXPECT_TRUE(asBool(evalSrc("linear_solve([[1,1],[2,2],[3,3]], [1,2,3]).singular", ev)));
870
+ EXPECT_TRUE(isUndef(evalSrc("linear_solve([[1,1],[2,2],[3,3]], [1,2,3]).x", ev)));
871
+ }
872
+
873
+
874
+ TEST(LinearSolve, WorksUnderBothEngines) {
816
875
  for (bool vm : {false, true}) {
817
876
  ScopedVm guard(vm);
818
877
  const std::vector<std::string> e =
819
- echoesOf("r = matrix_solve([[2,1],[1,3]], [5,10]);\necho(r.x, r.det, r.singular);");
878
+ echoesOf("r = linear_solve([[2,1],[1,3]], [5,10]);\necho(r.x, r.det, r.singular);");
820
879
  ASSERT_EQ(e.size(), 1u) << "vm=" << vm;
821
880
  EXPECT_NE(e[0].find("[1, 3]"), std::string::npos) << "vm=" << vm << ", got " << e[0];
822
881
  EXPECT_NE(e[0].find("5"), std::string::npos) << "vm=" << vm << ", got " << e[0];