sutra-dev 0.9.3__tar.gz → 0.9.4__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 (152) hide show
  1. {sutra_dev-0.9.3/sutra_dev.egg-info → sutra_dev-0.9.4}/PKG-INFO +1 -1
  2. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/pyproject.toml +1 -1
  3. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/__init__.py +1 -1
  4. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/codegen.py +12 -0
  5. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/codegen_base.py +30 -2
  6. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/codegen_pytorch.py +40 -4
  7. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/repl.py +45 -1
  8. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/stdlib/logic.su +12 -8
  9. {sutra_dev-0.9.3 → sutra_dev-0.9.4/sutra_dev.egg-info}/PKG-INFO +1 -1
  10. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_dev.egg-info/SOURCES.txt +2 -0
  11. sutra_dev-0.9.4/tests/test_le_ge_ties.py +59 -0
  12. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_no_host_readout.py +3 -1
  13. sutra_dev-0.9.4/tests/test_num_eq_indicator.py +96 -0
  14. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_repl.py +16 -0
  15. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_string_plus_concat.py +18 -0
  16. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/LICENSE +0 -0
  17. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/README.md +0 -0
  18. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/setup.cfg +0 -0
  19. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/__main__.py +0 -0
  20. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/ast_nodes.py +0 -0
  21. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/axon_keys.py +0 -0
  22. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/cached_compile.py +0 -0
  23. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/codegen_thrml.py +0 -0
  24. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/diagnostics.py +0 -0
  25. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/embedding.py +0 -0
  26. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/fv.py +0 -0
  27. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/fv_key_soundness.py +0 -0
  28. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/fv_loop_convergence.py +0 -0
  29. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/fv_obligation_checker.py +0 -0
  30. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/fv_poly_bound.py +0 -0
  31. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/fv_sampler_convergence.py +0 -0
  32. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/inliner.py +0 -0
  33. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/lexer.py +0 -0
  34. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/loop_capture.py +0 -0
  35. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/loop_desugar.py +0 -0
  36. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/multi_process.py +0 -0
  37. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/parser.py +0 -0
  38. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/preeval.py +0 -0
  39. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/promise_desugar.py +0 -0
  40. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/review.py +0 -0
  41. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/simplify.py +0 -0
  42. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/simplify_egglog.py +0 -0
  43. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/stdlib/axons.su +0 -0
  44. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/stdlib/bigint.su +0 -0
  45. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/stdlib/bitwise.su +0 -0
  46. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/stdlib/embed.su +0 -0
  47. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/stdlib/javascript_object.su +0 -0
  48. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/stdlib/javascript_primitives.su +0 -0
  49. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/stdlib/math.su +0 -0
  50. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/stdlib/memory.su +0 -0
  51. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/stdlib/modulus.su +0 -0
  52. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/stdlib/numbers.su +0 -0
  53. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/stdlib/promises.su +0 -0
  54. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/stdlib/rotation.su +0 -0
  55. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/stdlib/similarity.su +0 -0
  56. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/stdlib/strings.su +0 -0
  57. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/stdlib/tensor.su +0 -0
  58. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/stdlib/vectors.su +0 -0
  59. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/stdlib_loader.py +0 -0
  60. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/sutradb_embedded.py +0 -0
  61. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/symbol_table.py +0 -0
  62. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/tabulate.py +0 -0
  63. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/trace.py +0 -0
  64. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/validator.py +0 -0
  65. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_compiler/workspace.py +0 -0
  66. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_dev.egg-info/dependency_links.txt +0 -0
  67. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_dev.egg-info/entry_points.txt +0 -0
  68. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_dev.egg-info/requires.txt +0 -0
  69. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/sutra_dev.egg-info/top_level.txt +0 -0
  70. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_arity_diagnostic.py +0 -0
  71. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_arrow_functions.py +0 -0
  72. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_await_midfunction.py +0 -0
  73. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_await_substrate_pure.py +0 -0
  74. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_axon_build.py +0 -0
  75. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_axon_device_coherence.py +0 -0
  76. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_axon_item_call_result.py +0 -0
  77. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_axon_keys.py +0 -0
  78. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_axon_project.py +0 -0
  79. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_branchless_loop.py +0 -0
  80. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_buffer_transcendentals.py +0 -0
  81. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_cached_compile.py +0 -0
  82. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_cast_codegen.py +0 -0
  83. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_class_fields_runtime.py +0 -0
  84. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_codegen.py +0 -0
  85. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_codegen_pytorch.py +0 -0
  86. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_codegen_thrml.py +0 -0
  87. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_corpus.py +0 -0
  88. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_dimension_audit.py +0 -0
  89. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_dimension_audit_warning.py +0 -0
  90. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_embedding_provider.py +0 -0
  91. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_equality_autograd.py +0 -0
  92. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_fused_nn.py +0 -0
  93. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_fv_general_checker.py +0 -0
  94. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_fv_key_soundness.py +0 -0
  95. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_fv_kleene_grid_exactness.py +0 -0
  96. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_fv_loop_convergence.py +0 -0
  97. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_fv_poly_obligation_checker.py +0 -0
  98. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_fv_public_api.py +0 -0
  99. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_fv_sampler_convergence.py +0 -0
  100. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_fv_termination.py +0 -0
  101. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_fv_worked_example.py +0 -0
  102. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_higher_order_functions.py +0 -0
  103. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_implicit_loop_desugar.py +0 -0
  104. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_inliner.py +0 -0
  105. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_int_dict.py +0 -0
  106. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_int_to_string.py +0 -0
  107. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_interp_string_codegen.py +0 -0
  108. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_jvm_core.py +0 -0
  109. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_lexer.py +0 -0
  110. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_list_ops.py +0 -0
  111. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_load_matrix.py +0 -0
  112. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_loop_capture.py +0 -0
  113. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_loop_function_decl.py +0 -0
  114. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_make_string_idempotent.py +0 -0
  115. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_matrix_literal.py +0 -0
  116. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_measurement_claim_sweep.py +0 -0
  117. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_mini_wasm_machine.py +0 -0
  118. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_multi_process_runtime.py +0 -0
  119. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_native_recursion.py +0 -0
  120. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_ntm_ram.py +0 -0
  121. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_optional_llm_model.py +0 -0
  122. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_parser.py +0 -0
  123. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_preeval.py +0 -0
  124. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_process_pool_runtime.py +0 -0
  125. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_python_builtin_escape.py +0 -0
  126. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_ram_direct_tensor.py +0 -0
  127. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_rotation_prewarm.py +0 -0
  128. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_run_error_diagnostics.py +0 -0
  129. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_simplify.py +0 -0
  130. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_simplify_egglog.py +0 -0
  131. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_simplify_egglog_cse.py +0 -0
  132. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_snap_diagnostic.py +0 -0
  133. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_stdlib_loader.py +0 -0
  134. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_string_equality.py +0 -0
  135. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_substrate_leak_sweep.py +0 -0
  136. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_sutradb_embedded.py +0 -0
  137. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_symbol_table.py +0 -0
  138. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_tabulate.py +0 -0
  139. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_terminal_string_decode.py +0 -0
  140. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_torch_compile_wrap.py +0 -0
  141. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_trainable_matrix.py +0 -0
  142. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_transcendentals.py +0 -0
  143. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_type_inference.py +0 -0
  144. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_type_test_gap.py +0 -0
  145. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_unknown_function_diagnostic.py +0 -0
  146. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_unknown_type_diagnostic.py +0 -0
  147. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_unknown_variable_diagnostic.py +0 -0
  148. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_unsafe_override_codegen.py +0 -0
  149. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_vector_literal.py +0 -0
  150. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_wasm_core.py +0 -0
  151. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_workspace.py +0 -0
  152. {sutra_dev-0.9.3 → sutra_dev-0.9.4}/tests/test_wrong_arg_type_diagnostic.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sutra-dev
3
- Version: 0.9.3
3
+ Version: 0.9.4
4
4
  Summary: Compiler for the Sutra programming language — tensor ops on frozen-LLM embedding vectors.
5
5
  Author: Emma Leonhart
6
6
  License-Expression: AGPL-3.0-only
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sutra-dev"
7
- version = "0.9.3"
7
+ version = "0.9.4"
8
8
  description = "Compiler for the Sutra programming language — tensor ops on frozen-LLM embedding vectors."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -28,7 +28,7 @@ The compiler is intentionally liberal where the spec is still open
28
28
  forms and flags the clearly-forbidden ones.
29
29
  """
30
30
 
31
- __version__ = "0.9.3"
31
+ __version__ = "0.9.4"
32
32
 
33
33
  from .diagnostics import Diagnostic, DiagnosticLevel, DiagnosticBag
34
34
  from .lexer import Lexer, Token, TokenKind
@@ -246,6 +246,18 @@ class Codegen(BaseCodegen):
246
246
  meaningful signal there.
247
247
  """
248
248
  assert op in ("eq", "neq")
249
+ # Number-family operands (int/number, incl. arithmetic results
250
+ # like `n % 3`) route through the exact relu indicator (Emma
251
+ # 2026-07-08): cosine is degenerate at the zero vector, so a
252
+ # runtime zero could never == anything — zero-testing was
253
+ # unreachable. Torch-runtime only (the deprecated numpy backend
254
+ # keeps its old routing).
255
+ if (getattr(self, "supports_cast_lowering", False)
256
+ and self._CAST_FAMILY.get(
257
+ self._infer_cast_operand_type(expr.left)) == "number"
258
+ and self._CAST_FAMILY.get(
259
+ self._infer_cast_operand_type(expr.right)) == "number"):
260
+ return f"_VSA.num_{op}({left_src}, {right_src})"
249
261
  if (self._is_synthetic_axis_expr(expr.left)
250
262
  and self._is_synthetic_axis_expr(expr.right)):
251
263
  return f"_VSA.{op}_synthetic({left_src}, {right_src})"
@@ -702,6 +702,19 @@ class BaseCodegen:
702
702
  def translate(self, module: ast.Module) -> str:
703
703
  self._emit_prelude()
704
704
  self._emit()
705
+ # Pre-pass 0: record every top-level user function's declared
706
+ # return type, so expression-type checks (e.g. the text-concat
707
+ # dispatch asking "does f(x) return a string?") can resolve
708
+ # FORWARD references — a call site is often translated before
709
+ # its callee. _resolved_return_type only knows class methods and
710
+ # the stdlib; without this map, `f(1) + " "` with a user
711
+ # `function string f` fell to the numeric path and crashed at
712
+ # runtime (round-23 audit).
713
+ self._user_fn_return_types: dict[str, str] = {}
714
+ for item in module.items:
715
+ if (isinstance(item, ast.FunctionDecl)
716
+ and item.return_type is not None):
717
+ self._user_fn_return_types[item.name] = item.return_type.name
705
718
  # Pre-pass A: pull in stdlib class intrinsics (e.g.
706
719
  # `Tensor.MatrixMul`, `Tensor.matmul`, etc.) so namespaced
707
720
  # stdlib calls dispatch to `_VSA.<name>` even though the
@@ -2634,10 +2647,21 @@ class BaseCodegen:
2634
2647
  f"unroll by hand.",
2635
2648
  )
2636
2649
  if isinstance(stmt, ast.IfStmt):
2650
+ # Design commitment, not a gap (control-flow.md § "`if` /
2651
+ # `else` is parsed but rejected at codegen"): Sutra
2652
+ # conditionals are weighted superpositions, not discrete
2653
+ # branches. The message must hand the newcomer the actual
2654
+ # rewrite, not just the philosophy (round-23 audit).
2637
2655
  raise CodegenNotSupported(
2638
2656
  stmt,
2639
- "if/else is not supported by the V1 codegen — the whole "
2640
- "point is to compile it away into a prototype-table lookup",
2657
+ "`if`/`else` never compiles by design, Sutra "
2658
+ "conditionals are weighted superpositions, not discrete "
2659
+ "branches. Write `select([score_a, score_b, ...], "
2660
+ "[result_a, result_b, ...])`: the highest score's result "
2661
+ "dominates the output. A boolean two-way branch is "
2662
+ "`select([is_true(cond), is_true(!cond)], [then_value, "
2663
+ "else_value])`. See the control-flow docs and "
2664
+ "examples/fuzzy_dispatch.su for the idiom.",
2641
2665
  )
2642
2666
  if isinstance(stmt, ast.TryStmt):
2643
2667
  self._translate_try_catch(stmt)
@@ -3042,6 +3066,10 @@ class BaseCodegen:
3042
3066
  return (self._is_text_expr(expr.left)
3043
3067
  and self._is_text_expr(expr.right))
3044
3068
  if isinstance(expr, ast.Call):
3069
+ if (isinstance(expr.callee, ast.Identifier)
3070
+ and getattr(self, "_user_fn_return_types", {}).get(
3071
+ expr.callee.name) in self._TEXT_TYPES):
3072
+ return True
3045
3073
  return self._resolved_return_type(expr.callee) in self._TEXT_TYPES
3046
3074
  return False
3047
3075
 
@@ -2910,12 +2910,17 @@ class PyTorchCodegen(Codegen):
2910
2910
  self._emit('was a host `for k in range`, `.item()`, host `if`, host')
2911
2911
  self._emit('`return k+1`). Gather the codepoint block, mark non-zero')
2912
2912
  self._emit('positions, take the highest 1-based position that is')
2913
- self._emit('non-zero: length = max((k+1) where cps[k] != 0). All tensor')
2913
+ self._emit('non-zero: length = max((k+1) where cps[k] >= 0.5). All tensor')
2914
2914
  self._emit('ops; 0-d tensor out. Trailing-zero-as-sentinel preserved')
2915
- self._emit('(a 0 codepoint in the tail reads shorter, same as before)."""')
2915
+ self._emit('(a 0 codepoint in the tail reads shorter, same as before).')
2916
+ self._emit('The 0.5 threshold (was `!= 0`) makes decode robust to')
2917
+ self._emit('superposition residue: a String coming out of select() carries')
2918
+ self._emit('~1e-9 leakage from losing branches on higher codepoint axes,')
2919
+ self._emit('which the exact-nonzero walk counted as characters (phantom')
2920
+ self._emit('NULs). Real codepoints are >= 32; residue is << 1."""')
2916
2921
  self._emit("ax = self._str_axes()")
2917
2922
  self._emit("cps = v.index_select(0, ax)")
2918
- self._emit("nz = (cps != 0).to(self.dtype)")
2923
+ self._emit("nz = (cps.abs() >= 0.5).to(self.dtype)")
2919
2924
  self._emit("pos = _torch.arange(1, ax.shape[0] + 1, dtype=self.dtype, device=self.device)")
2920
2925
  self._emit("return (pos * nz).max()")
2921
2926
  self._indent -= 1
@@ -3334,7 +3339,10 @@ class PyTorchCodegen(Codegen):
3334
3339
  self._emit("for i in range(n):")
3335
3340
  self._indent += 1
3336
3341
  self._emit("axis = self._string_axis(i)")
3337
- self._emit("chars.append(chr(int(v[self.semantic_dim + axis].item())))")
3342
+ self._emit("# round(), not int(): a select() superposition leaves the")
3343
+ self._emit("# winner's codepoints at ~(1-eps)*cp — int() truncated 101.9997")
3344
+ self._emit("# to 101 and garbled every decoded character by one.")
3345
+ self._emit("chars.append(chr(round(v[self.semantic_dim + axis].item())))")
3338
3346
  self._indent -= 1
3339
3347
  self._emit('return "".join(chars)')
3340
3348
  self._indent -= 1
@@ -3594,6 +3602,34 @@ class PyTorchCodegen(Codegen):
3594
3602
  self._emit("return out")
3595
3603
  self._indent -= 1
3596
3604
  self._emit()
3605
+ # Number-family equality — the exact relu indicator (Emma
3606
+ # 2026-07-08). Cosine eq is DEGENERATE at the zero vector
3607
+ # (cos(0,v) is 0/0 → a runtime zero can never equal anything,
3608
+ # including zero), which made zero-testing unreachable — see
3609
+ # planning/findings/2026-07-08-zero-equality-reads-neutral-
3610
+ # cosine-degenerate.md.
3611
+ self._emit("def num_eq(self, a, b):")
3612
+ self._indent += 1
3613
+ self._emit('"""Number == number via the exact indicator:')
3614
+ self._emit('truth = 2*relu(1 - |x - y|) - 1 — +1 at equal, -1 at')
3615
+ self._emit('|diff| >= 1 (exact gap 2.0 at integer spacing, no residual;')
3616
+ self._emit('the neural-Unix keystone on the truth convention). Fractional')
3617
+ self._emit('nearness interpolates linearly (|d|=0.5 -> 0, neutral). The')
3618
+ self._emit('unit width is a trainable-surface candidate (a gain k on |d|),')
3619
+ self._emit('fixed at 1 today. All tensor ops; differentiable a.e."""')
3620
+ self._emit("d = _torch.abs(self._scalar(a) - self._scalar(b))")
3621
+ self._emit("truth = 2.0 * _torch.clamp(1.0 - d, min=0.0) - 1.0")
3622
+ self._emit("out = _torch.zeros(self.dim, dtype=self.dtype, device=self.device)")
3623
+ self._emit("out[self.semantic_dim + self.AXIS_TRUTH] = truth")
3624
+ self._emit("return out")
3625
+ self._indent -= 1
3626
+ self._emit()
3627
+ self._emit("def num_neq(self, a, b):")
3628
+ self._indent += 1
3629
+ self._emit('"""!= for number-family values — truth-axis negation of num_eq."""')
3630
+ self._emit("return -self.num_eq(a, b)")
3631
+ self._indent -= 1
3632
+ self._emit()
3597
3633
  # Synthetic-axis equality — Euclidean distance + tanh
3598
3634
  # (2026-05-08 directive). For int / float / complex / char /
3599
3635
  # string operands; cosine doesn't distinguish well between
@@ -45,7 +45,7 @@ _EVAL_WRAP_TYPES = {"string"}
45
45
  _BANNER = (
46
46
  "Sutra REPL - type an expression to evaluate it; end a line with ';' or '}' "
47
47
  "to add a declaration.\n"
48
- "Commands: :help :decls :reset :quit\n"
48
+ "Commands: :help :ops :decls :reset :quit\n"
49
49
  )
50
50
  _HELP = (
51
51
  "Sutra REPL help\n"
@@ -53,12 +53,54 @@ _HELP = (
53
53
  " (number -> real value; concept -> nearest known string)\n"
54
54
  " <stmt>; add a declaration/statement to the session (e.g. vector k = embed(\"king\");)\n"
55
55
  " function ... { } add a function to the session\n"
56
+ " :ops list the callable operations (builtins, stdlib, special forms)\n"
56
57
  " :decls print the accumulated session declarations\n"
57
58
  " :reset clear all session declarations\n"
58
59
  " :help this help\n"
59
60
  " :quit / :q leave the REPL (Ctrl-D also works)\n"
60
61
  )
61
62
 
63
+
64
+ def _ops_listing() -> str:
65
+ """The `:ops` discovery surface: every operation callable from REPL
66
+ source, grouped. Built from the live dispatch tables (BUILTINS + the
67
+ stdlib intrinsic registry) so it cannot drift from what actually
68
+ resolves — a hand-written list here would rot."""
69
+ from .codegen_base import BUILTINS
70
+ try:
71
+ from .stdlib_loader import stdlib_class_intrinsic_methods
72
+ stdlib = stdlib_class_intrinsic_methods()
73
+ except Exception:
74
+ stdlib = {}
75
+
76
+ def _columns(names, indent=" ", width=76):
77
+ lines, cur = [], indent
78
+ for n in names:
79
+ if len(cur) + len(n) + 2 > width and cur.strip():
80
+ lines.append(cur.rstrip())
81
+ cur = indent
82
+ cur += n + " "
83
+ if cur.strip():
84
+ lines.append(cur.rstrip())
85
+ return "\n".join(lines)
86
+
87
+ parts = ["Operations callable from Sutra source in this REPL\n"]
88
+ parts.append(" builtins:\n" + _columns(sorted(BUILTINS.keys())) + "\n")
89
+ for cls in sorted(stdlib):
90
+ methods = sorted(stdlib[cls])
91
+ if not methods:
92
+ continue
93
+ parts.append(f" {cls} (call bare or as {cls}.<name>):\n"
94
+ + _columns(methods) + "\n")
95
+ parts.append(
96
+ " special forms:\n"
97
+ " embed(\"...\") defuzzy(v) unsafeCast<T>(v) unsafeOverride(v)\n"
98
+ " (Type) expr casts $\"text {interpolant}\" interpolation\n"
99
+ )
100
+ parts.append(
101
+ " full reference: https://sutra.topazcomputing.com/capabilities/\n")
102
+ return "".join(parts)
103
+
62
104
  _EVAL_FN = "__repl_eval__"
63
105
 
64
106
 
@@ -238,6 +280,8 @@ def run_repl(
238
280
  break
239
281
  if stripped == ":help":
240
282
  out.write(_HELP); out.flush(); continue
283
+ if stripped == ":ops":
284
+ out.write(_ops_listing()); out.flush(); continue
241
285
  if stripped == ":reset":
242
286
  decls.clear(); out.write("(session cleared)\n"); out.flush(); continue
243
287
  if stripped == ":decls":
@@ -129,16 +129,20 @@ function fuzzy lt(complex a, complex b) {
129
129
  }
130
130
 
131
131
 
132
- // ge / le — on the differentiable-tanh scheme, `>=` and `<=` collapse
133
- // to `>` and `<`: both give tanh(0) = 0 on exact ties. Programs that
134
- // need to distinguish strict from tie compose with `==` (cosine
135
- // similarity, crisp on identical operands).
136
- function fuzzy ge(complex a, complex b) {
137
- return a > b;
132
+ // ge / le — or(strict, ==) per Emma 2026-07-08 (the previous
133
+ // collapse-to-strict gave tanh(0) = 0 on exact ties, so the standard
134
+ // loop guard `i <= n` read NEUTRAL at the boundary — finding
135
+ // 2026-07-06-le-ge-tie-returns-neutral). The params are number-typed
136
+ // so the `==` component dispatches to the exact num_eq indicator:
137
+ // ties read +1, and the false directions stay exactly -1
138
+ // (max(-1, -1)) instead of softening through cosine. `||` is the
139
+ // truth-axis max.
140
+ function fuzzy ge(number a, number b) {
141
+ return (a > b) || (a == b);
138
142
  }
139
143
 
140
- function fuzzy le(complex a, complex b) {
141
- return a < b;
144
+ function fuzzy le(number a, number b) {
145
+ return (a < b) || (a == b);
142
146
  }
143
147
 
144
148
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sutra-dev
3
- Version: 0.9.3
3
+ Version: 0.9.4
4
4
  Summary: Compiler for the Sutra programming language — tensor ops on frozen-LLM embedding vectors.
5
5
  Author: Emma Leonhart
6
6
  License-Expression: AGPL-3.0-only
@@ -99,6 +99,7 @@ tests/test_int_dict.py
99
99
  tests/test_int_to_string.py
100
100
  tests/test_interp_string_codegen.py
101
101
  tests/test_jvm_core.py
102
+ tests/test_le_ge_ties.py
102
103
  tests/test_lexer.py
103
104
  tests/test_list_ops.py
104
105
  tests/test_load_matrix.py
@@ -112,6 +113,7 @@ tests/test_multi_process_runtime.py
112
113
  tests/test_native_recursion.py
113
114
  tests/test_no_host_readout.py
114
115
  tests/test_ntm_ram.py
116
+ tests/test_num_eq_indicator.py
115
117
  tests/test_optional_llm_model.py
116
118
  tests/test_parser.py
117
119
  tests/test_preeval.py
@@ -0,0 +1,59 @@
1
+ """`<=` / `>=` at exact ties (Emma decided 2026-07-08: adopt or(<, ==)).
2
+
3
+ Before: stdlib le/ge collapsed to the strict ops — `2 <= 2` read
4
+ tanh(0) = 0 (NEUTRAL), so the standard loop guard `i <= n` misbehaved
5
+ at the boundary. Now `le = or(<, ==)` / `ge = or(>, ==)` with the ==
6
+ component on the exact num_eq indicator: ties read +1, and both
7
+ false directions stay exactly -1 (max(-1, -1)).
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import unittest
12
+
13
+ from sutra_compiler.lexer import Lexer
14
+ from sutra_compiler.parser import Parser
15
+ from sutra_compiler.codegen_pytorch import translate_module
16
+
17
+
18
+ def _truth(expr: str, dim: int = 16) -> float:
19
+ src = f"function fuzzy main(){{ int x = 2; int y = 2; return {expr}; }}"
20
+ lx = Lexer(src, file="<t>")
21
+ ast = Parser(lx.tokenize(), file="<t>", diagnostics=lx.diagnostics).parse_module()
22
+ assert not lx.diagnostics.has_errors(), list(lx.diagnostics)
23
+ ns: dict = {}
24
+ exec(translate_module(ast, llm_model="none", runtime_dim=dim), ns)
25
+ r = ns["main"]()
26
+ v = ns["_VSA"]
27
+ if not hasattr(r, "ndim") or r.ndim == 0:
28
+ return float(r)
29
+ return float(r[v.semantic_dim + v.AXIS_TRUTH])
30
+
31
+
32
+ class TestTies(unittest.TestCase):
33
+ def test_le_tie_true(self):
34
+ self.assertAlmostEqual(_truth("2 <= 2"), 1.0, places=4)
35
+
36
+ def test_ge_tie_true(self):
37
+ self.assertAlmostEqual(_truth("2 >= 2"), 1.0, places=4)
38
+
39
+ def test_var_tie(self):
40
+ self.assertAlmostEqual(_truth("x <= y"), 1.0, places=4)
41
+
42
+
43
+ class TestStrictUnchanged(unittest.TestCase):
44
+ def test_le_true_direction(self):
45
+ self.assertAlmostEqual(_truth("2 <= 3"), 1.0, places=4)
46
+
47
+ def test_le_false_direction_stays_crisp(self):
48
+ self.assertAlmostEqual(_truth("3 <= 2"), -1.0, places=4)
49
+
50
+ def test_ge_false_direction_stays_crisp(self):
51
+ self.assertAlmostEqual(_truth("2 >= 3"), -1.0, places=4)
52
+
53
+ def test_strict_lt_tie_still_neutral(self):
54
+ # strict < at a tie is tanh(0) = 0 — deliberately unchanged.
55
+ self.assertAlmostEqual(_truth("2 < 2"), 0.0, places=4)
56
+
57
+
58
+ if __name__ == "__main__":
59
+ unittest.main()
@@ -32,7 +32,9 @@ import pytest
32
32
  # - _js_str_cmp — JS-interop carve-out (host string comparison).
33
33
  # GOAL: 0 for genuine introspection; the above are sanctioned I/O/JS/control
34
34
  # boundaries. Only ever lower this.
35
- BASELINE_ITEM_READOUTS = 18
35
+ # 18 -> 17 on 2026-07-08: retiring the dead is_char alias removed its
36
+ # .item() flag-read (round-19). Progress toward 0.
37
+ BASELINE_ITEM_READOUTS = 17
36
38
 
37
39
 
38
40
  def _generated_runtime_source() -> str:
@@ -0,0 +1,96 @@
1
+ """Number-family `==` via the exact relu indicator (Emma 2026-07-08).
2
+
3
+ Cosine `==` is degenerate at the zero vector — `(15 % 3) == 0` read
4
+ NEUTRAL, making zero-testing (divisibility, emptiness, termination)
5
+ unreachable (finding 2026-07-08-zero-equality-reads-neutral...).
6
+ Emma's pick: number-family operands route through
7
+ `truth = 2·relu(1 − |x−y|) − 1` — +1 at equal, −1 at |diff| ≥ 1,
8
+ exact gap 2.0 at integer spacing, all tensor ops, differentiable a.e.
9
+ Cosine `==` stays for vector/semantic operands; the Euclidean
10
+ `eq_synthetic` stays for string/char/complex.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import unittest
15
+
16
+ from sutra_compiler.lexer import Lexer
17
+ from sutra_compiler.parser import Parser
18
+ from sutra_compiler.codegen_pytorch import translate_module
19
+
20
+
21
+ def _truth(src: str, dim: int = 16) -> float:
22
+ lx = Lexer(src, file="<t>")
23
+ ast = Parser(lx.tokenize(), file="<t>", diagnostics=lx.diagnostics).parse_module()
24
+ assert not lx.diagnostics.has_errors(), list(lx.diagnostics)
25
+ ns: dict = {}
26
+ exec(translate_module(ast, llm_model="none", runtime_dim=dim), ns)
27
+ r = ns["main"]()
28
+ v = ns["_VSA"]
29
+ if not hasattr(r, "ndim") or r.ndim == 0:
30
+ return float(r)
31
+ return float(r[v.semantic_dim + v.AXIS_TRUTH])
32
+
33
+
34
+ class TestZeroEquality(unittest.TestCase):
35
+ def test_mod_zero_is_true(self):
36
+ # THE finding case: was 0.0 (neutral), must be +1.
37
+ self.assertAlmostEqual(_truth(
38
+ "function fuzzy main(){ return (15 % 3) == 0; }"), 1.0, places=4)
39
+
40
+ def test_mod_nonzero_is_false(self):
41
+ self.assertAlmostEqual(_truth(
42
+ "function fuzzy main(){ return (16 % 3) == 0; }"), -1.0, places=4)
43
+
44
+ def test_runtime_zero_vars(self):
45
+ self.assertAlmostEqual(_truth(
46
+ "function fuzzy main(){ number x = make_real(0.0); "
47
+ "return x == 0; }"), 1.0, places=4)
48
+
49
+
50
+ class TestIntegerEquality(unittest.TestCase):
51
+ def test_equal_ints(self):
52
+ self.assertAlmostEqual(_truth(
53
+ "function fuzzy main(){ int a = 2; int b = 2; return a == b; }"),
54
+ 1.0, places=4)
55
+
56
+ def test_unequal_ints(self):
57
+ self.assertAlmostEqual(_truth(
58
+ "function fuzzy main(){ int a = 2; int b = 3; return a == b; }"),
59
+ -1.0, places=4)
60
+
61
+ def test_neq(self):
62
+ self.assertAlmostEqual(_truth(
63
+ "function fuzzy main(){ int a = 2; int b = 3; return a != b; }"),
64
+ 1.0, places=4)
65
+ self.assertAlmostEqual(_truth(
66
+ "function fuzzy main(){ int a = 2; int b = 2; return a != b; }"),
67
+ -1.0, places=4)
68
+
69
+
70
+ class TestFizzBuzzEndToEnd(unittest.TestCase):
71
+ def test_fizzbuzz_via_select(self):
72
+ src = '''
73
+ function string fizzbuzz(int n) {
74
+ number s3 = (number) ((n % 3) == 0);
75
+ number s5 = (number) ((n % 5) == 0);
76
+ return select(
77
+ [10 * (s3 + s5), 10 * (s3 - s5), 10 * (s5 - s3), 10 * (0 - s3 - s5)],
78
+ [make_string("fizzbuzz"), make_string("fizz"),
79
+ make_string("buzz"), int_to_string(n)]);
80
+ }
81
+ function string main() {
82
+ return fizzbuzz(15) + " " + fizzbuzz(3) + " " + fizzbuzz(5) + " " + fizzbuzz(7);
83
+ }
84
+ '''
85
+ lx = Lexer(src, file="<t>")
86
+ ast = Parser(lx.tokenize(), file="<t>",
87
+ diagnostics=lx.diagnostics).parse_module()
88
+ assert not lx.diagnostics.has_errors()
89
+ ns: dict = {}
90
+ exec(translate_module(ast, llm_model="none", runtime_dim=64), ns)
91
+ got = ns["_VSA"].string_to_python(ns["main"]())
92
+ self.assertEqual(got, "fizzbuzz fizz buzz 7")
93
+
94
+
95
+ if __name__ == "__main__":
96
+ unittest.main()
@@ -114,3 +114,19 @@ def test_scalar_tensor_result_shows_clean_number_not_repr():
114
114
  out = _decode_result(_Mod(), torch.tensor(0.6812))
115
115
  assert "tensor(" not in out and "device=" not in out
116
116
  assert out == "= 0.6812"
117
+
118
+
119
+ def test_ops_lists_builtins_stdlib_and_special_forms():
120
+ # :ops is built from the LIVE dispatch tables, so it must show a
121
+ # builtin, a String intrinsic (incl. the new int_to_string), and
122
+ # the special forms — the REPL's stdlib-discovery surface
123
+ # (usability round 22).
124
+ out = _drive([":ops", ":quit"])
125
+ for needle in ("bundle", "argmax_cosine", "string_concat",
126
+ "int_to_string", "embed(", "unsafeCast", "capabilities"):
127
+ assert needle in out, f"missing {needle!r} in :ops output"
128
+
129
+
130
+ def test_help_mentions_ops():
131
+ out = _drive([":help", ":quit"])
132
+ assert ":ops" in out
@@ -80,5 +80,23 @@ class TestNumericPlusUntouched(unittest.TestCase):
80
80
  3.5, places=4)
81
81
 
82
82
 
83
+ class TestUserFunctionReturnConcat(unittest.TestCase):
84
+ def test_user_string_function_plus_literal(self):
85
+ # Round-23 regression: `f(x) + " "` with a user-declared
86
+ # `function string f` crashed at runtime (Tensor + str) — the
87
+ # text dispatch only knew stdlib/class return types, so the
88
+ # literal stayed a host str on the numeric path.
89
+ self.assertEqual(_run_text(
90
+ 'function string shout(string s){ return s + "!"; }\n'
91
+ 'function string main(){ return shout("hi") + " there"; }'),
92
+ "hi! there")
93
+
94
+ def test_chained_user_calls(self):
95
+ self.assertEqual(_run_text(
96
+ 'function string tag(int n){ return int_to_string(n); }\n'
97
+ 'function string main(){ return tag(1) + "-" + tag(2); }'),
98
+ "1-2")
99
+
100
+
83
101
  if __name__ == "__main__":
84
102
  unittest.main()
File without changes
File without changes
File without changes
File without changes