openscad-cpp-evaluator 0.47.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.47.0 → openscad_cpp_evaluator-0.49.0}/CLAUDE.md +53 -0
  2. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/PKG-INFO +1 -1
  3. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/pyproject.toml +1 -1
  4. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/src/builtins/function_builtins.cpp +308 -1
  5. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/tests/test_feature_detection.cpp +1 -1
  6. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/tests/test_function_builtins.cpp +158 -0
  7. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/.github/workflows/ci.yml +0 -0
  8. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/.github/workflows/wheels.yml +0 -0
  9. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/.gitignore +0 -0
  10. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/.gitmodules +0 -0
  11. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/CMakeLists.txt +0 -0
  12. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/LICENSE +0 -0
  13. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/README.md +0 -0
  14. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/bindings/CMakeLists.txt +0 -0
  15. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/bindings/ast_to_py.cpp +0 -0
  16. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/bindings/module.cpp +0 -0
  17. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/cmake/embed_font.cmake +0 -0
  18. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/examples/CMakeLists.txt +0 -0
  19. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/examples/eval_perf_benchmark.cpp +0 -0
  20. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/examples/manifold_cache_reuse.cpp +0 -0
  21. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/examples/minimal_debugger.cpp +0 -0
  22. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/.github/workflows/ci.yml +0 -0
  23. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/.gitignore +0 -0
  24. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/CLAUDE.md +0 -0
  25. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/CMakeLists.txt +0 -0
  26. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/README.md +0 -0
  27. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/api.hpp +0 -0
  28. {openscad_cpp_evaluator-0.47.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.47.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.47.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.47.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.47.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.47.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.47.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.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/ast.hpp +0 -0
  36. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/comments.hpp +0 -0
  37. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/position.hpp +0 -0
  38. {openscad_cpp_evaluator-0.47.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.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/scope.hpp +0 -0
  40. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/include/openscad_cpp_parser/serialization.hpp +0 -0
  41. {openscad_cpp_evaluator-0.47.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.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/src/CMakeLists.txt +0 -0
  43. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/src/api.cpp +0 -0
  44. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/src/ast/ast_node.cpp +0 -0
  45. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/src/ast/declarations.cpp +0 -0
  46. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/src/ast/expression.cpp +0 -0
  47. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/src/ast/format_utils.hpp +0 -0
  48. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/src/ast/module_instantiation.cpp +0 -0
  49. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/src/ast/scope_builder.cpp +0 -0
  50. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/src/ast/vector_element.cpp +0 -0
  51. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/src/comment_attach_internal.hpp +0 -0
  52. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/src/comments.cpp +0 -0
  53. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/src/grammar/driver.cpp +0 -0
  54. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/src/grammar/driver.hpp +0 -0
  55. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/src/grammar/lexer.l +0 -0
  56. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/src/grammar/lexer_api.hpp +0 -0
  57. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/src/grammar/oscad_location.hpp +0 -0
  58. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/src/grammar/parser.y +0 -0
  59. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/src/inline_comment_attach.cpp +0 -0
  60. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/src/position.cpp +0 -0
  61. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/src/pretty_print.cpp +0 -0
  62. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/src/scope.cpp +0 -0
  63. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/src/serialization/json_io.cpp +0 -0
  64. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/src/serialization/yaml_io.cpp +0 -0
  65. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/src/source_map.cpp +0 -0
  66. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/tests/CMakeLists.txt +0 -0
  67. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/tests/test_assignments.cpp +0 -0
  68. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/tests/test_ast_generation.cpp +0 -0
  69. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/tests/test_comments_and_files.cpp +0 -0
  70. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/tests/test_const_borrow.cpp +0 -0
  71. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/tests/test_control.cpp +0 -0
  72. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/tests/test_expressions.cpp +0 -0
  73. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/tests/test_functions.cpp +0 -0
  74. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/tests/test_helpers.hpp +0 -0
  75. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/tests/test_inline_comments.cpp +0 -0
  76. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/tests/test_lexical.cpp +0 -0
  77. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/tests/test_modules.cpp +0 -0
  78. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/tests/test_node_str.cpp +0 -0
  79. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/tests/test_pretty_print.cpp +0 -0
  80. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/tests/test_render_expression.cpp +0 -0
  81. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/tests/test_scope.cpp +0 -0
  82. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/tests/test_serialization.cpp +0 -0
  83. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/tests/test_smoke.cpp +0 -0
  84. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/tests/test_source_map.cpp +0 -0
  85. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/tests/test_use_include.cpp +0 -0
  86. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/tests/test_vectors.cpp +0 -0
  87. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/tools/cli/CMakeLists.txt +0 -0
  88. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/external/openscad_cpp_parser/tools/cli/main.cpp +0 -0
  89. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/bound_args.hpp +0 -0
  90. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/bytecode.hpp +0 -0
  91. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/bytecode_compiler.hpp +0 -0
  92. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/bytecode_vm.hpp +0 -0
  93. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/call_args.hpp +0 -0
  94. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/colored_body.hpp +0 -0
  95. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/csg_node.hpp +0 -0
  96. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/css_colors.hpp +0 -0
  97. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/debug_hooks.hpp +0 -0
  98. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/debug_repl.hpp +0 -0
  99. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/dispatch.hpp +0 -0
  100. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/dxf_svg_import.hpp +0 -0
  101. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/eval_context.hpp +0 -0
  102. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/eval_error.hpp +0 -0
  103. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/eval_use.hpp +0 -0
  104. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/evaluator.hpp +0 -0
  105. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/export.hpp +0 -0
  106. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/font_match.hpp +0 -0
  107. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/font_provider.hpp +0 -0
  108. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/freetype_font_provider.hpp +0 -0
  109. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/function_builtins.hpp +0 -0
  110. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/import_builtin.hpp +0 -0
  111. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/manifold_cache.hpp +0 -0
  112. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/mesh_check.hpp +0 -0
  113. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/mesh_import.hpp +0 -0
  114. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/osc_range.hpp +0 -0
  115. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/profile.hpp +0 -0
  116. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/scope_trail.hpp +0 -0
  117. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/segments.hpp +0 -0
  118. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/surface_load.hpp +0 -0
  119. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/text_metrics.hpp +0 -0
  120. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/value.hpp +0 -0
  121. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/include/openscad_cpp_evaluator/zip_stored.hpp +0 -0
  122. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/python/openscad_cpp_evaluator/__init__.py +0 -0
  123. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/resources/fonts/LICENSE +0 -0
  124. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/resources/fonts/LiberationSans-Regular.ttf +0 -0
  125. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/src/CMakeLists.txt +0 -0
  126. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/src/builtins/booleans.cpp +0 -0
  127. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/src/builtins/builtins.hpp +0 -0
  128. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/src/builtins/call_args.cpp +0 -0
  129. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/src/builtins/color.cpp +0 -0
  130. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/src/builtins/control.cpp +0 -0
  131. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/src/builtins/dxf_dim.cpp +0 -0
  132. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/src/builtins/extrude.cpp +0 -0
  133. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/src/builtins/import.cpp +0 -0
  134. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/src/builtins/modifiers.cpp +0 -0
  135. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/src/builtins/primitives_2d.cpp +0 -0
  136. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/src/builtins/primitives_3d.cpp +0 -0
  137. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/src/builtins/registry.cpp +0 -0
  138. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/src/builtins/roof.cpp +0 -0
  139. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/src/builtins/segments.cpp +0 -0
  140. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/src/builtins/surface.cpp +0 -0
  141. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/src/builtins/text.cpp +0 -0
  142. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/src/builtins/topology.cpp +0 -0
  143. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/src/builtins/transforms.cpp +0 -0
  144. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/src/bytecode_compiler.cpp +0 -0
  145. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/src/bytecode_vm.cpp +0 -0
  146. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/src/colored_body.cpp +0 -0
  147. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/src/csg_generate.cpp +0 -0
  148. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/src/csg_resolve.cpp +0 -0
  149. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/src/css_colors.cpp +0 -0
  150. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/src/debug/debug_repl.cpp +0 -0
  151. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/src/debug_profile.cpp +0 -0
  152. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/src/eval_context.cpp +0 -0
  153. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/src/eval_error.cpp +0 -0
  154. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/src/eval_use.cpp +0 -0
  155. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/src/export.cpp +0 -0
  156. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/src/expr_eval.cpp +0 -0
  157. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/src/import/dxf_import.cpp +0 -0
  158. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/src/import/mesh_import.cpp +0 -0
  159. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/src/import/surface_load.cpp +0 -0
  160. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/src/import/svg_import.cpp +0 -0
  161. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/src/manifold_cache.cpp +0 -0
  162. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/src/measure_geometry.cpp +0 -0
  163. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/src/mesh_check.cpp +0 -0
  164. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/src/stmt_eval.cpp +0 -0
  165. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/src/text/font_match.cpp +0 -0
  166. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/src/text/freetype_font_provider.cpp +0 -0
  167. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/src/text/text_metrics.cpp +0 -0
  168. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/src/user_calls.cpp +0 -0
  169. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/src/value.cpp +0 -0
  170. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/src/zip_stored.cpp +0 -0
  171. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/tests/CMakeLists.txt +0 -0
  172. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/tests/test_booleans.cpp +0 -0
  173. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/tests/test_bytecode_compiler.cpp +0 -0
  174. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/tests/test_children_separate.cpp +0 -0
  175. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/tests/test_cli.cpp +0 -0
  176. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/tests/test_control_flow.cpp +0 -0
  177. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/tests/test_csg_tree.cpp +0 -0
  178. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/tests/test_debug_hooks.cpp +0 -0
  179. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/tests/test_dxf_svg_import.cpp +0 -0
  180. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/tests/test_eval_context.cpp +0 -0
  181. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/tests/test_export.cpp +0 -0
  182. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/tests/test_expr_eval.cpp +0 -0
  183. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/tests/test_extrude_roof.cpp +0 -0
  184. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/tests/test_helpers.hpp +0 -0
  185. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/tests/test_import_export.cpp +0 -0
  186. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/tests/test_manifold_cache.cpp +0 -0
  187. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/tests/test_mesh_check.cpp +0 -0
  188. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/tests/test_multi_color_merge.cpp +0 -0
  189. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/tests/test_primitives.cpp +0 -0
  190. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/tests/test_profiling.cpp +0 -0
  191. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/tests/test_python_bindings.py +0 -0
  192. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/tests/test_render_expr.cpp +0 -0
  193. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/tests/test_scope_trail.cpp +0 -0
  194. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/tests/test_scoping.cpp +0 -0
  195. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/tests/test_surface.cpp +0 -0
  196. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/tests/test_tail_calls.cpp +0 -0
  197. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/tests/test_text.cpp +0 -0
  198. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/tests/test_transforms.cpp +0 -0
  199. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/tests/test_unexpected_args.cpp +0 -0
  200. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/tests/test_use.cpp +0 -0
  201. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/tests/test_value.cpp +0 -0
  202. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/tests/test_viewport_params.cpp +0 -0
  203. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/tests/test_zip_stored.cpp +0 -0
  204. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/tools/cli/CMakeLists.txt +0 -0
  205. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/tools/cli/cli_lib.cpp +0 -0
  206. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/tools/cli/cli_lib.hpp +0 -0
  207. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/tools/cli/main.cpp +0 -0
  208. {openscad_cpp_evaluator-0.47.0 → openscad_cpp_evaluator-0.49.0}/uv.lock +0 -0
@@ -460,6 +460,59 @@ 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
+ - **`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.
515
+
463
516
  - `include/openscad_cpp_evaluator/value.hpp`, `src/value.cpp` — the `Value` variant (OpenSCAD's
464
517
  dynamic value type) and its free-function arithmetic/comparison helpers. See the plan's §1 for
465
518
  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.47.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.47.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,6 +169,306 @@ Value builtinCross(const Value& aArg, const Value& bArg) {
169
169
  return Value{};
170
170
  }
171
171
 
172
+ // -- linear_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
+ // 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,
292
+ const oscad::ASTNode& node) {
293
+ const auto parsed = numericMatrix(aArg);
294
+ if (!parsed) {
295
+ ev.warn("linear_solve() requires a matrix of numbers", &node.position());
296
+ return Value{};
297
+ }
298
+ std::vector<double> lu = parsed->first;
299
+ const size_t n = parsed->second; // columns == unknowns
300
+ const size_t m = lu.size() / n; // rows == equations
301
+ double maxAbs = 0.0;
302
+ for (double x : lu) {
303
+ if (!std::isfinite(x)) {
304
+ ev.warn("linear_solve() matrix contains a non-finite value", &node.position());
305
+ return Value{};
306
+ }
307
+ maxAbs = std::max(maxAbs, std::abs(x));
308
+ }
309
+
310
+ // Right-hand side: a vector of n is one column; a matrix of n rows is
311
+ // one column per column. Kept row-major alongside the factorisation.
312
+ size_t k = 0;
313
+ bool bWasVector = false;
314
+ std::vector<double> rhs;
315
+ if (haveB) {
316
+ if (const std::optional<std::vector<double>> vec = allNumericList(bArg); vec && vec->size() == m) {
317
+ bWasVector = true;
318
+ k = 1;
319
+ rhs = *vec;
320
+ } else if (const auto bm = numericMatrix(bArg); bm && bm->first.size() / bm->second == m) {
321
+ k = bm->second;
322
+ rhs = bm->first;
323
+ } else {
324
+ ev.warn("linear_solve() right-hand side must be a vector of " + std::to_string(m) +
325
+ " numbers, or a matrix with that many rows",
326
+ &node.position());
327
+ return Value{};
328
+ }
329
+ for (double x : rhs) {
330
+ if (!std::isfinite(x)) {
331
+ ev.warn("linear_solve() right-hand side contains a non-finite value", &node.position());
332
+ return Value{};
333
+ }
334
+ }
335
+ }
336
+
337
+ // Relative singularity threshold. BOSL2 compares R's diagonal against a
338
+ // fixed ABSOLUTE 1e-9 (its _EPSILON) with no scaling by the size of the
339
+ // matrix, so a perfectly well-conditioned system scaled down by 1e-10
340
+ // is declared singular there. Scaling by maxAbs is what makes
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
+ }
407
+
408
+ double det = 1.0;
409
+ bool singular = false;
410
+ for (size_t col = 0; col < n && !singular; ++col) {
411
+ size_t pivot = col;
412
+ for (size_t r = col + 1; r < n; ++r) {
413
+ if (std::abs(lu[r * n + col]) > std::abs(lu[pivot * n + col])) pivot = r;
414
+ }
415
+ if (std::abs(lu[pivot * n + col]) <= tol) {
416
+ singular = true;
417
+ break;
418
+ }
419
+ if (pivot != col) {
420
+ for (size_t c = 0; c < n; ++c) std::swap(lu[col * n + c], lu[pivot * n + c]);
421
+ for (size_t c = 0; c < k; ++c) std::swap(rhs[col * k + c], rhs[pivot * k + c]);
422
+ det = -det;
423
+ }
424
+ const double p = lu[col * n + col];
425
+ det *= p;
426
+ for (size_t r = col + 1; r < n; ++r) {
427
+ const double f = lu[r * n + col] / p;
428
+ if (f == 0.0) continue;
429
+ lu[r * n + col] = 0.0;
430
+ for (size_t c = col + 1; c < n; ++c) lu[r * n + c] -= f * lu[col * n + c];
431
+ for (size_t c = 0; c < k; ++c) rhs[r * k + c] -= f * rhs[col * k + c];
432
+ }
433
+ }
434
+
435
+ std::vector<std::pair<std::string, Value>> out;
436
+ if (singular) {
437
+ // Not a misuse -- "is this matrix singular?" is a legitimate
438
+ // question to ask linear_solve, so it answers rather than warning.
439
+ out.emplace_back("x", Value{});
440
+ out.emplace_back("det", Value{0.0});
441
+ out.emplace_back("singular", Value{true});
442
+ return objectOf(std::move(out));
443
+ }
444
+
445
+ if (haveB) {
446
+ for (size_t col = n; col-- > 0;) {
447
+ for (size_t c = 0; c < k; ++c) {
448
+ double acc = rhs[col * k + c];
449
+ for (size_t j = col + 1; j < n; ++j) acc -= lu[col * n + j] * rhs[j * k + c];
450
+ rhs[col * k + c] = acc / lu[col * n + col];
451
+ }
452
+ }
453
+ if (bWasVector) {
454
+ out.emplace_back("x", numList(rhs));
455
+ } else {
456
+ std::vector<Value> rowsOut;
457
+ rowsOut.reserve(n);
458
+ for (size_t r = 0; r < n; ++r) {
459
+ rowsOut.push_back(numList(std::vector<double>(rhs.begin() + static_cast<long>(r * k),
460
+ rhs.begin() + static_cast<long>((r + 1) * k))));
461
+ }
462
+ out.emplace_back("x", listOf(std::move(rowsOut)));
463
+ }
464
+ } else {
465
+ out.emplace_back("x", Value{});
466
+ }
467
+ out.emplace_back("det", Value{det});
468
+ out.emplace_back("singular", Value{false});
469
+ return objectOf(std::move(out));
470
+ }
471
+
172
472
  Value builtinRands(double minv, double maxv, double nArg, const Value& seedArg) {
173
473
  // ponytail: doesn't reproduce Python's Mersenne-Twister bit-for-bit --
174
474
  // no script should depend on cross-language RNG equality, only on
@@ -450,6 +750,7 @@ bool isBuiltinFunctionName(const std::string& name) {
450
750
  "chr", "ord", "is_undef", "is_num", "is_bool", "is_string", "is_list", "is_function", "is_object",
451
751
  "search", "lookup", "has_key", "version", "version_num", "parent_module",
452
752
  "object", "textmetrics", "fontmetrics", "dxf_dim", "dxf_cross", "supported_feature",
753
+ "linear_solve",
453
754
  };
454
755
  return names.count(name) > 0;
455
756
  }
@@ -591,7 +892,7 @@ enum class BuiltinFnId {
591
892
  TextMetrics, FontMetrics, Abs, Sign, Ceil, Floor, Round, Sqrt, Ln, Log, Exp, Sin, Cos, Tan,
592
893
  Asin, Acos, Atan, Atan2, Max, Min, Pow, Norm, Cross, Rands, Concat, Len, Str, Chr, Ord,
593
894
  IsUndef, IsNum, IsBool, IsString, IsList, IsFunction, IsObject, Search, Lookup, HasKey,
594
- Version, VersionNum, ParentModule, DxfDim, DxfCross, SupportedFeature,
895
+ Version, VersionNum, ParentModule, DxfDim, DxfCross, SupportedFeature, LinearSolve,
595
896
  };
596
897
 
597
898
  // supported_feature("name") -> the level at which this build implements that
@@ -611,6 +912,7 @@ enum class BuiltinFnId {
611
912
  const std::unordered_map<std::string, double>& featureLevels() {
612
913
  static const std::unordered_map<std::string, double> levels = {
613
914
  {"render-expr", 1.0}, // render() in expression position
915
+ {"linear-solve", 1.0}, // linear_solve(A, b) -> {x, det, singular}
614
916
  {"polyhedron-vnf", 1.0}, // polyhedron(vnf) / polyhedron(object)
615
917
  {"separate-children", 1.0}, // children(..., separate=true)
616
918
  {"minkowski-diff", 1.0}, // minkowski_difference()
@@ -645,6 +947,7 @@ const std::unordered_map<std::string, BuiltinFnId>& builtinFnIds() {
645
947
  {"parent_module", BuiltinFnId::ParentModule},
646
948
  {"dxf_dim", BuiltinFnId::DxfDim}, {"dxf_cross", BuiltinFnId::DxfCross},
647
949
  {"supported_feature", BuiltinFnId::SupportedFeature},
950
+ {"linear_solve", BuiltinFnId::LinearSolve},
648
951
  };
649
952
  return ids;
650
953
  }
@@ -726,6 +1029,7 @@ const std::unordered_map<int, BuiltinCheck>& builtinChecks() {
726
1029
  add(BuiltinFnId::ParentModule, {0, 1, "1", {kNum}});
727
1030
  add(BuiltinFnId::HasKey, {-1, -1, nullptr, {{TObj, "object"}, {TStr, "string"}}});
728
1031
  add(BuiltinFnId::SupportedFeature, {1, 1, "1", {}});
1032
+ add(BuiltinFnId::LinearSolve, {1, 2, "1 or 2", {kVec}});
729
1033
  for (BuiltinFnId id : {BuiltinFnId::IsUndef, BuiltinFnId::IsNum, BuiltinFnId::IsBool,
730
1034
  BuiltinFnId::IsString, BuiltinFnId::IsList, BuiltinFnId::IsFunction,
731
1035
  BuiltinFnId::IsObject}) {
@@ -1040,6 +1344,9 @@ Value evalBuiltinFunction(Evaluator& ev, const std::string& name, const CallArgs
1040
1344
  // The OpenSCAD release we track. version_num() is that same
1041
1345
  // year/month/day folded as y * 10000 + m * 100 + d, exactly like the
1042
1346
  // reference's own builtin_version_num (builtin_functions.cc).
1347
+ case BuiltinFnId::LinearSolve:
1348
+ return builtinLinearSolve(ev, getArg(args, 0, "A", Value{}), getArg(args, 1, "b", Value{}),
1349
+ args.findPositional(1) != nullptr || args.findNamed("b") != nullptr, node);
1043
1350
  case BuiltinFnId::SupportedFeature: {
1044
1351
  const Value name = getArg(args, 0, "feature", Value{});
1045
1352
  const std::string* s = std::get_if<std::string>(&name);
@@ -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", "linear-solve"};
39
39
 
40
40
  } // namespace
41
41
 
@@ -725,3 +725,161 @@ TEST(StringListBuiltins, ChrIsVariadicAndRangeChecked) {
725
725
  EXPECT_EQ(asStr(evalSrc("chr(55296)", ev)), ""); // 0xD800, a surrogate
726
726
  EXPECT_EQ(asStr(evalSrc("chr(1114112)", ev)), ""); // 0x110000, one past the top
727
727
  }
728
+
729
+ // -- linear_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(LinearSolve, 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("linear_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(LinearSolve, DeterminantComesFreeFromTheSameFactorisation) {
752
+ Evaluator ev;
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
+ // Sign follows the row swaps, not just the magnitude.
756
+ EXPECT_NEAR(asNum(evalSrc("linear_solve([[0,1],[1,0]]).det", ev)), -1.0, 1e-12);
757
+ }
758
+
759
+ TEST(LinearSolve, WithNoRightHandSideThereIsNoSolution) {
760
+ Evaluator ev;
761
+ EXPECT_TRUE(isUndef(evalSrc("linear_solve([[2,1],[1,3]]).x", ev)));
762
+ }
763
+
764
+ TEST(LinearSolve, 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("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
+ }
772
+
773
+ TEST(LinearSolve, 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("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
+ 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(LinearSolve, 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("linear_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(LinearSolve, RejectsWhatItCannotFactor) {
801
+ Evaluator ev;
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
805
+ }
806
+
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);
833
+ }
834
+
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) {
875
+ for (bool vm : {false, true}) {
876
+ ScopedVm guard(vm);
877
+ const std::vector<std::string> e =
878
+ echoesOf("r = linear_solve([[2,1],[1,3]], [5,10]);\necho(r.x, r.det, r.singular);");
879
+ ASSERT_EQ(e.size(), 1u) << "vm=" << vm;
880
+ EXPECT_NE(e[0].find("[1, 3]"), std::string::npos) << "vm=" << vm << ", got " << e[0];
881
+ EXPECT_NE(e[0].find("5"), std::string::npos) << "vm=" << vm << ", got " << e[0];
882
+ EXPECT_NE(e[0].find("false"), std::string::npos) << "vm=" << vm << ", got " << e[0];
883
+ }
884
+ }
885
+