sutra-dev 0.9.3__tar.gz → 0.10.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 (155) hide show
  1. {sutra_dev-0.9.3/sutra_dev.egg-info → sutra_dev-0.10.0}/PKG-INFO +1 -1
  2. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/pyproject.toml +1 -1
  3. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/__init__.py +1 -1
  4. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/__main__.py +13 -2
  5. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/ast_nodes.py +38 -0
  6. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/codegen.py +45 -21
  7. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/codegen_base.py +282 -29
  8. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/codegen_pytorch.py +157 -33
  9. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/inliner.py +38 -0
  10. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/parser.py +134 -0
  11. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/repl.py +49 -1
  12. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/stdlib/logic.su +12 -8
  13. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/symbol_table.py +3 -0
  14. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/validator.py +72 -0
  15. {sutra_dev-0.9.3 → sutra_dev-0.10.0/sutra_dev.egg-info}/PKG-INFO +1 -1
  16. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_dev.egg-info/SOURCES.txt +5 -0
  17. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_cast_codegen.py +21 -8
  18. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_interp_string_codegen.py +20 -7
  19. sutra_dev-0.10.0/tests/test_le_ge_ties.py +59 -0
  20. sutra_dev-0.10.0/tests/test_loop_call_expr.py +643 -0
  21. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_loop_function_decl.py +13 -2
  22. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_no_host_readout.py +3 -1
  23. sutra_dev-0.10.0/tests/test_num_eq_indicator.py +96 -0
  24. sutra_dev-0.10.0/tests/test_num_to_string.py +76 -0
  25. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_repl.py +45 -0
  26. sutra_dev-0.10.0/tests/test_slot_state_diagnostic.py +119 -0
  27. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_string_plus_concat.py +18 -0
  28. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_terminal_string_decode.py +18 -0
  29. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_torch_compile_wrap.py +5 -1
  30. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/LICENSE +0 -0
  31. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/README.md +0 -0
  32. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/setup.cfg +0 -0
  33. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/axon_keys.py +0 -0
  34. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/cached_compile.py +0 -0
  35. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/codegen_thrml.py +0 -0
  36. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/diagnostics.py +0 -0
  37. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/embedding.py +0 -0
  38. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/fv.py +0 -0
  39. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/fv_key_soundness.py +0 -0
  40. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/fv_loop_convergence.py +0 -0
  41. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/fv_obligation_checker.py +0 -0
  42. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/fv_poly_bound.py +0 -0
  43. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/fv_sampler_convergence.py +0 -0
  44. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/lexer.py +0 -0
  45. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/loop_capture.py +0 -0
  46. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/loop_desugar.py +0 -0
  47. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/multi_process.py +0 -0
  48. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/preeval.py +0 -0
  49. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/promise_desugar.py +0 -0
  50. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/review.py +0 -0
  51. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/simplify.py +0 -0
  52. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/simplify_egglog.py +0 -0
  53. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/stdlib/axons.su +0 -0
  54. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/stdlib/bigint.su +0 -0
  55. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/stdlib/bitwise.su +0 -0
  56. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/stdlib/embed.su +0 -0
  57. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/stdlib/javascript_object.su +0 -0
  58. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/stdlib/javascript_primitives.su +0 -0
  59. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/stdlib/math.su +0 -0
  60. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/stdlib/memory.su +0 -0
  61. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/stdlib/modulus.su +0 -0
  62. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/stdlib/numbers.su +0 -0
  63. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/stdlib/promises.su +0 -0
  64. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/stdlib/rotation.su +0 -0
  65. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/stdlib/similarity.su +0 -0
  66. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/stdlib/strings.su +0 -0
  67. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/stdlib/tensor.su +0 -0
  68. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/stdlib/vectors.su +0 -0
  69. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/stdlib_loader.py +0 -0
  70. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/sutradb_embedded.py +0 -0
  71. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/tabulate.py +0 -0
  72. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/trace.py +0 -0
  73. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/workspace.py +0 -0
  74. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_dev.egg-info/dependency_links.txt +0 -0
  75. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_dev.egg-info/entry_points.txt +0 -0
  76. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_dev.egg-info/requires.txt +0 -0
  77. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_dev.egg-info/top_level.txt +0 -0
  78. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_arity_diagnostic.py +0 -0
  79. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_arrow_functions.py +0 -0
  80. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_await_midfunction.py +0 -0
  81. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_await_substrate_pure.py +0 -0
  82. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_axon_build.py +0 -0
  83. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_axon_device_coherence.py +0 -0
  84. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_axon_item_call_result.py +0 -0
  85. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_axon_keys.py +0 -0
  86. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_axon_project.py +0 -0
  87. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_branchless_loop.py +0 -0
  88. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_buffer_transcendentals.py +0 -0
  89. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_cached_compile.py +0 -0
  90. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_class_fields_runtime.py +0 -0
  91. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_codegen.py +0 -0
  92. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_codegen_pytorch.py +0 -0
  93. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_codegen_thrml.py +0 -0
  94. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_corpus.py +0 -0
  95. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_dimension_audit.py +0 -0
  96. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_dimension_audit_warning.py +0 -0
  97. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_embedding_provider.py +0 -0
  98. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_equality_autograd.py +0 -0
  99. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_fused_nn.py +0 -0
  100. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_fv_general_checker.py +0 -0
  101. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_fv_key_soundness.py +0 -0
  102. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_fv_kleene_grid_exactness.py +0 -0
  103. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_fv_loop_convergence.py +0 -0
  104. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_fv_poly_obligation_checker.py +0 -0
  105. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_fv_public_api.py +0 -0
  106. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_fv_sampler_convergence.py +0 -0
  107. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_fv_termination.py +0 -0
  108. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_fv_worked_example.py +0 -0
  109. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_higher_order_functions.py +0 -0
  110. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_implicit_loop_desugar.py +0 -0
  111. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_inliner.py +0 -0
  112. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_int_dict.py +0 -0
  113. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_int_to_string.py +0 -0
  114. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_jvm_core.py +0 -0
  115. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_lexer.py +0 -0
  116. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_list_ops.py +0 -0
  117. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_load_matrix.py +0 -0
  118. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_loop_capture.py +0 -0
  119. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_make_string_idempotent.py +0 -0
  120. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_matrix_literal.py +0 -0
  121. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_measurement_claim_sweep.py +0 -0
  122. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_mini_wasm_machine.py +0 -0
  123. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_multi_process_runtime.py +0 -0
  124. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_native_recursion.py +0 -0
  125. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_ntm_ram.py +0 -0
  126. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_optional_llm_model.py +0 -0
  127. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_parser.py +0 -0
  128. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_preeval.py +0 -0
  129. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_process_pool_runtime.py +0 -0
  130. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_python_builtin_escape.py +0 -0
  131. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_ram_direct_tensor.py +0 -0
  132. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_rotation_prewarm.py +0 -0
  133. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_run_error_diagnostics.py +0 -0
  134. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_simplify.py +0 -0
  135. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_simplify_egglog.py +0 -0
  136. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_simplify_egglog_cse.py +0 -0
  137. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_snap_diagnostic.py +0 -0
  138. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_stdlib_loader.py +0 -0
  139. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_string_equality.py +0 -0
  140. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_substrate_leak_sweep.py +0 -0
  141. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_sutradb_embedded.py +0 -0
  142. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_symbol_table.py +0 -0
  143. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_tabulate.py +0 -0
  144. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_trainable_matrix.py +0 -0
  145. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_transcendentals.py +0 -0
  146. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_type_inference.py +0 -0
  147. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_type_test_gap.py +0 -0
  148. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_unknown_function_diagnostic.py +0 -0
  149. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_unknown_type_diagnostic.py +0 -0
  150. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_unknown_variable_diagnostic.py +0 -0
  151. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_unsafe_override_codegen.py +0 -0
  152. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_vector_literal.py +0 -0
  153. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_wasm_core.py +0 -0
  154. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_workspace.py +0 -0
  155. {sutra_dev-0.9.3 → sutra_dev-0.10.0}/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.10.0
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.10.0"
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.10.0"
32
32
 
33
33
  from .diagnostics import Diagnostic, DiagnosticLevel, DiagnosticBag
34
34
  from .lexer import Lexer, Token, TokenKind
@@ -367,7 +367,18 @@ def _decode_terminal_result(mod, result):
367
367
  and result.ndim == 1 and result.shape[0] == vsa.dim):
368
368
  if vsa.is_string(result):
369
369
  return vsa.string_to_python(result)
370
- return float(result[vsa.semantic_dim + vsa.AXIS_REAL])
370
+ real = float(result[vsa.semantic_dim + vsa.AXIS_REAL])
371
+ truth = float(result[vsa.semantic_dim + vsa.AXIS_TRUTH])
372
+ # Truth-dominant result (a fuzzy/bool return — e.g.
373
+ # `(15 % 3) == 0`): its value lives on AXIS_TRUTH, and
374
+ # printing the real axis showed 0.0 for a TRUE comparison
375
+ # (round-25). Display the truth reading with its polarity
376
+ # named; number-vectors (real-dominant) keep the old face.
377
+ if abs(truth) > abs(real):
378
+ word = ("true" if truth >= 0.5
379
+ else "false" if truth <= -0.5 else "neutral")
380
+ return f"{word} ({truth:+.2f})"
381
+ return real
371
382
  except Exception:
372
383
  pass
373
384
  return result
@@ -526,7 +537,7 @@ def main(argv: List[str] | None = None) -> int:
526
537
 
527
538
  parser = argparse.ArgumentParser(
528
539
  prog="sutrac",
529
- description="Validate Sutra (.su) source files.",
540
+ description="The Sutra toolchain: validate .su source (default), compile (--emit), execute (--run), or explore interactively (sutrac repl).",
530
541
  epilog=(
531
542
  "interactive REPL:\n"
532
543
  " sutrac repl Launch the interactive evaluator (takes no\n"
@@ -156,6 +156,28 @@ class Call(Expr):
156
156
  args: List[Expr]
157
157
 
158
158
 
159
+ @dataclass
160
+ class LoopCallExpr(Expr):
161
+ """`loop name(cond_arg, state_expr, ...)` in EXPRESSION position —
162
+ invoke a loop function and evaluate to its FINAL state.
163
+
164
+ Distinct from `LoopCallStmt` (the by-reference statement form, where
165
+ state args are slot-variable identifiers mutated in place). Here the
166
+ state args are arbitrary expressions and the whole construct is a
167
+ value — `int x = loop addNumber(x0 < 11, x0);` and
168
+ `return loop f(...);` both work.
169
+
170
+ Stage 1 (2026-07-12) supports the SINGLE-state form only; a call to a
171
+ multi-state loop function raises a codegen diagnostic pointing at the
172
+ by-reference statement form (multi-state tuple-assign is a later
173
+ stage — see todo.md §"Make loops idiomatic"). `state_args` still
174
+ carries the full list so the diagnostic can report the real arity.
175
+ """
176
+ name: str
177
+ condition_arg: Expr
178
+ state_args: List[Expr]
179
+
180
+
159
181
  @dataclass
160
182
  class NewExpr(Expr):
161
183
  """`new ClassName(args)` — auto-constructor for a class with field
@@ -487,6 +509,22 @@ class LoopCallStmt(Stmt):
487
509
  state_arg_names: List[str]
488
510
 
489
511
 
512
+ @dataclass
513
+ class LoopDestructureStmt(Stmt):
514
+ """`(a, b, ...) = loop NAME(cond, s0, s1, ...);` — invoke a MULTI-state
515
+ loop in value position and bind its final state values to newly-declared
516
+ locals `a, b, ...`.
517
+
518
+ This is the multi-state counterpart of the single-state loop expression
519
+ form (`int x = loop f(...)`). Sutra has no general tuples, so a
520
+ parenthesised comma-list on the LHS of `=` is ONLY this loop-destructure
521
+ form — the RHS is always a `LoopCallExpr`. The names are new locals whose
522
+ types are the loop's state-param types.
523
+ """
524
+ names: List[str]
525
+ call: "LoopCallExpr"
526
+
527
+
490
528
  # ============================================================
491
529
  # Declarations
492
530
  # ============================================================
@@ -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})"
@@ -1127,34 +1139,34 @@ class Codegen(BaseCodegen):
1127
1139
  self._emit("return (base, base + 1)")
1128
1140
  self._indent -= 1
1129
1141
  self._emit()
1130
- self._emit("def slot_store(self, state, slot_idx, scalar):")
1142
+ self._emit("def slot_state_new(self):")
1131
1143
  self._indent += 1
1132
- self._emit('"""Write scalar to slot slot_idx. Overwrites the slot\'s plane.')
1133
- self._emit('')
1134
- self._emit("The scalar lives on the real leg of the slot\'s 2D plane; the")
1135
- self._emit("imaginary leg is zeroed. A subsequent slot_load returns the scalar")
1136
- self._emit("exactly. This is the reversible-imperative-state primitive: a")
1137
- self._emit("variable assignment = one slot_store; the inverse is slot_store of")
1138
- self._emit("the previous value. State outside the slot\'s plane is unchanged.")
1144
+ self._emit('"""A fresh slot store: `{slot_idx: value}`. Parity with the pytorch')
1145
+ self._emit("backend's unified d-dim slot representation (rung 3). This backend")
1146
+ self._emit("keeps numbers as host scalars (no num_* substrate ops), so a scalar")
1147
+ self._emit("slot stores a host scalar and a String/vector slot stores its vector;")
1148
+ self._emit("the dict just carries whichever, keyed by compile-time slot index.")
1139
1149
  self._emit('"""')
1140
- self._emit("i, j = self._slot_plane(slot_idx)")
1141
- self._emit("new = state.copy() if hasattr(state, 'copy') else _np.asarray(state).copy()")
1142
- self._emit("new[i] = float(scalar)")
1143
- self._emit("new[j] = 0.0")
1150
+ self._emit("return {}")
1151
+ self._indent -= 1
1152
+ self._emit()
1153
+ self._emit("def slot_store(self, state, slot_idx, scalar):")
1154
+ self._indent += 1
1155
+ self._emit('"""Store the value for slot slot_idx (functional update — returns a')
1156
+ self._emit("new dict). Stores the value as-is: a host scalar for number slots, a")
1157
+ self._emit("full vector for String/vector slots. Reversible-imperative-state")
1158
+ self._emit('primitive: a variable assignment = one slot_store."""')
1159
+ self._emit("new = dict(state)")
1160
+ self._emit("new[int(slot_idx)] = scalar")
1144
1161
  self._emit("return new")
1145
1162
  self._indent -= 1
1146
1163
  self._emit()
1147
1164
  self._emit("def slot_load(self, state, slot_idx):")
1148
1165
  self._indent += 1
1149
- self._emit('"""Read the scalar stored at slot slot_idx (the real leg).')
1150
- self._emit('')
1151
- self._emit("Returns a substrate scalar (numpy 0-dim from state[i]) rather than a")
1152
- self._emit("Python float downstream arithmetic stays in substrate land. Other")
1153
- self._emit("slots and semantic content do not contribute; the disjoint-plane")
1154
- self._emit("allocation makes this a projection, not a noisy readout.")
1155
- self._emit('"""')
1156
- self._emit("i, _j = self._slot_plane(slot_idx)")
1157
- self._emit("return state[i]")
1166
+ self._emit('"""Read the value stored at slot slot_idx (0.0 if never stored — the')
1167
+ self._emit('zero-initialised scalar this backend defaults numbers to)."""')
1168
+ self._emit("v = state.get(int(slot_idx))")
1169
+ self._emit("return v if v is not None else 0.0")
1158
1170
  self._indent -= 1
1159
1171
  self._emit()
1160
1172
  self._emit("def rotate_slot(self, state, slot_idx, angle):")
@@ -1475,6 +1487,18 @@ class Codegen(BaseCodegen):
1475
1487
  self._emit("return v")
1476
1488
  self._indent -= 1
1477
1489
  self._emit()
1490
+ self._emit("def _scalar(self, x):")
1491
+ self._indent += 1
1492
+ self._emit('"""Coerce a number to an int-able scalar: a full number-vector')
1493
+ self._emit("projects to its AXIS_REAL coordinate; a host scalar / 0-d passes")
1494
+ self._emit("through. Mirrors the pytorch backend so the SHARED codegen's")
1495
+ self._emit("`int(_VSA._scalar(count))` (the iterative-loop count read) works on")
1496
+ self._emit("this backend too. Vector-loop-state rung 3 B1a: this backend keeps")
1497
+ self._emit("numbers as host scalars, so today it is a passthrough — the guard")
1498
+ self._emit('is in place for when a loop count arrives as a number-vector."""')
1499
+ self._emit("return self._re(x)")
1500
+ self._indent -= 1
1501
+ self._emit()
1478
1502
  self._emit("def _num_re(self, v):")
1479
1503
  self._indent += 1
1480
1504
  self._emit('"""Real-axis scalar of a number value — mirrors the pytorch backend\'s')
@@ -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
@@ -1243,7 +1256,7 @@ class BaseCodegen:
1243
1256
  self._var_type[p.name] = p.type_ref.name
1244
1257
  self._emit("_program_halt = 1.0")
1245
1258
  if _has_slot_decl(decl.body):
1246
- self._emit("_slot_state = _VSA.zero_vector()")
1259
+ self._emit("_slot_state = _VSA.slot_state_new()")
1247
1260
  if not decl.body.statements:
1248
1261
  self._emit("pass")
1249
1262
  else:
@@ -1373,7 +1386,7 @@ class BaseCodegen:
1373
1386
  self._nonhalting_slot_var = nonhalting_slot_var
1374
1387
  self._emit("_program_halt = 1.0")
1375
1388
  if _has_slot_decl(decl.body):
1376
- self._emit("_slot_state = _VSA.zero_vector()")
1389
+ self._emit("_slot_state = _VSA.slot_state_new()")
1377
1390
  if not decl.body.statements:
1378
1391
  self._emit("pass")
1379
1392
  else:
@@ -1846,9 +1859,16 @@ class BaseCodegen:
1846
1859
  self._emit(f"# iterative_loop: tick = _t+1, halt when tick > count.")
1847
1860
  self._emit(f"_iterator = _t + 1")
1848
1861
  # Heaviside of (count - iterator + 1): positive while iterator
1849
- # <= count; zero or negative once past. Substrate-pure scalar.
1862
+ # <= count; zero or negative once past. The count may be a
1863
+ # slot-threaded state param, which under rung 3's unified d-dim
1864
+ # slot representation arrives as a number-vector — `_scalar`
1865
+ # projects it to its real-axis value before the int() count
1866
+ # read. A no-op on the current 0-d/host-int form (rung 3 B1a:
1867
+ # pre-project the one codegen scalar consumer of a slot value so
1868
+ # the later representation flip does not break it).
1850
1869
  self._emit(
1851
- f"_keep = _VSA.heaviside(int({count_src}) - _iterator + 1)"
1870
+ f"_keep = _VSA.heaviside(int(_VSA._scalar({count_src})) "
1871
+ f"- _iterator + 1)"
1852
1872
  )
1853
1873
  elif decl.kind == "foreach_loop":
1854
1874
  # foreach: array is an explicit step parameter (`arr_param_name`).
@@ -2112,6 +2132,151 @@ class BaseCodegen:
2112
2132
  # this loop's completion gates the function's return value.
2113
2133
  self._emit("_program_halt = _program_halt * _loopret_halt")
2114
2134
 
2135
+ def _translate_loop_call_expr(self, expr: "ast.LoopCallExpr") -> str:
2136
+ """Lower `loop NAME(cond, state_expr)` in EXPRESSION position to the
2137
+ loop's FINAL state value.
2138
+
2139
+ Reuses the exact driver function the by-reference statement form
2140
+ emits (`_loop_NAME`), which returns a `(state..., halted)` tuple;
2141
+ the expression value is the single state slot (`[0]`). No slot
2142
+ writeback and no by-reference identifiers — the state arg is an
2143
+ arbitrary expression passed straight in as the init value.
2144
+
2145
+ Stage 1 (2026-07-12): single-state, non-`this`-threading loops
2146
+ only. Multi-state and non-static class loops raise a diagnostic
2147
+ pointing at the by-reference statement form.
2148
+ """
2149
+ decl = self._loop_decls.get(expr.name)
2150
+ if decl is None:
2151
+ raise CodegenNotSupported(
2152
+ expr,
2153
+ f"loop function `{expr.name}` is not declared. Loop "
2154
+ f"functions must be declared with one of `do_while`, "
2155
+ f"`while_loop`, `iterative_loop`, `foreach_loop` before "
2156
+ f"being invoked with `loop NAME(...)`.",
2157
+ )
2158
+ # Non-static class-bodied loops return `this` first and rebind the
2159
+ # caller instance — that has no meaning as a pure value. Defer.
2160
+ if "." in expr.name and not getattr(decl, "is_static", False):
2161
+ raise CodegenNotSupported(
2162
+ expr,
2163
+ f"loop `{expr.name}` is a non-static class loop; its "
2164
+ f"expression form is not supported yet. Use the "
2165
+ f"by-reference statement form: `loop {expr.name}"
2166
+ f"(instance, ...);`.",
2167
+ )
2168
+ n_state = len(decl.state_params)
2169
+ if n_state != 1:
2170
+ raise CodegenNotSupported(
2171
+ expr,
2172
+ f"the single-value loop expression form supports "
2173
+ f"single-state loops only; `{expr.name}` has {n_state} "
2174
+ f"state parameters. Use the tuple-destructure form for "
2175
+ f"multi-state loops — `(a, b) = loop {expr.name}(cond, "
2176
+ f"...);` — or the by-reference statement form (`slot` vars "
2177
+ f"+ `loop {expr.name}(cond, a, b);`).",
2178
+ )
2179
+ if len(expr.state_args) != 1:
2180
+ raise CodegenNotSupported(
2181
+ expr,
2182
+ f"loop `{expr.name}` takes 1 state argument, got "
2183
+ f"{len(expr.state_args)}.",
2184
+ )
2185
+ state_src = self._translate_expr(expr.state_args[0])
2186
+ py_loop_name = f"_loop_{expr.name.replace('.', '_')}"
2187
+ if decl.kind == "foreach_loop":
2188
+ # The array (condition_arg) is consumed as the first driver
2189
+ # arg — mirror the statement form's array_from_literal path.
2190
+ if isinstance(expr.condition_arg, ast.ArrayLiteral):
2191
+ elem_srcs = [
2192
+ self._translate_expr(e)
2193
+ for e in expr.condition_arg.elements
2194
+ ]
2195
+ arr_src = f"_VSA.array_from_literal({', '.join(elem_srcs)})"
2196
+ else:
2197
+ arr_src = self._translate_expr(expr.condition_arg)
2198
+ return f"{py_loop_name}({arr_src}, {state_src})[0]"
2199
+ # Other kinds: the runtime uses the loop's decl-time condition, so
2200
+ # the cond arg's value is unused. Evaluate it for side-effect
2201
+ # parity with the statement form, then yield the final state.
2202
+ cond_src = self._translate_expr(expr.condition_arg)
2203
+ return f"(({cond_src}), {py_loop_name}({state_src})[0])[-1]"
2204
+
2205
+ def _translate_loop_destructure(self, stmt: "ast.LoopDestructureStmt") -> None:
2206
+ """Lower `(a, b, ...) = loop NAME(cond, s0, s1, ...);` — the
2207
+ multi-state counterpart of the single-state loop expression form.
2208
+
2209
+ Reuses the same driver the statement/expression forms emit
2210
+ (`_loop_NAME`), which returns `(state0, ..., stateK, halted)`, and
2211
+ binds the state slots to the newly-declared locals `a, b, ...`. Like
2212
+ the single-state expression form it drops the halt slot (the driver
2213
+ loops until halt saturates, so it is ~1.0 at return) — the value
2214
+ forms take the state, not the completion flag.
2215
+ """
2216
+ call = stmt.call
2217
+ decl = self._loop_decls.get(call.name)
2218
+ if decl is None:
2219
+ raise CodegenNotSupported(
2220
+ stmt,
2221
+ f"loop function `{call.name}` is not declared. Loop "
2222
+ f"functions must be declared with one of `do_while`, "
2223
+ f"`while_loop`, `iterative_loop`, `foreach_loop` before "
2224
+ f"being invoked with `loop NAME(...)`.",
2225
+ )
2226
+ if "." in call.name and not getattr(decl, "is_static", False):
2227
+ raise CodegenNotSupported(
2228
+ stmt,
2229
+ f"loop `{call.name}` is a non-static class loop; the "
2230
+ f"destructure form is not supported for it. Use the "
2231
+ f"by-reference statement form.",
2232
+ )
2233
+ n_state = len(decl.state_params)
2234
+ if len(stmt.names) != n_state:
2235
+ raise CodegenNotSupported(
2236
+ stmt,
2237
+ f"loop `{call.name}` has {n_state} state parameter(s) but "
2238
+ f"the destructure binds {len(stmt.names)} name(s) — they "
2239
+ f"must match one-to-one.",
2240
+ )
2241
+ if len(call.state_args) != n_state:
2242
+ raise CodegenNotSupported(
2243
+ stmt,
2244
+ f"loop `{call.name}` takes {n_state} state argument(s), got "
2245
+ f"{len(call.state_args)}.",
2246
+ )
2247
+ # Register each bound name's declared type from the loop's state
2248
+ # params so downstream type-dependent codegen (string/complex ops)
2249
+ # routes correctly.
2250
+ for name, sp in zip(stmt.names, decl.state_params):
2251
+ sp_type = getattr(getattr(sp, "type_ref", None), "name", None)
2252
+ if sp_type is not None:
2253
+ self._var_type[name] = sp_type
2254
+ state_srcs = [self._translate_expr(a) for a in call.state_args]
2255
+ py_loop_name = f"_loop_{call.name.replace('.', '_')}"
2256
+ targets = ", ".join(stmt.names)
2257
+ self._emit(f"# loop destructure: ({targets}) = loop {call.name}(...)")
2258
+ if decl.kind == "foreach_loop":
2259
+ if isinstance(call.condition_arg, ast.ArrayLiteral):
2260
+ elem_srcs = [
2261
+ self._translate_expr(e)
2262
+ for e in call.condition_arg.elements
2263
+ ]
2264
+ arr_src = f"_VSA.array_from_literal({', '.join(elem_srcs)})"
2265
+ else:
2266
+ arr_src = self._translate_expr(call.condition_arg)
2267
+ all_args = [arr_src] + state_srcs
2268
+ self._emit(
2269
+ f"({targets}, _) = {py_loop_name}({', '.join(all_args)})"
2270
+ )
2271
+ return
2272
+ # Non-foreach: cond value is unused at runtime (the loop uses its
2273
+ # decl-time condition); evaluate it once for side-effect parity.
2274
+ cond_src = self._translate_expr(call.condition_arg)
2275
+ self._emit(f"_ = {cond_src}")
2276
+ self._emit(
2277
+ f"({targets}, _) = {py_loop_name}({', '.join(state_srcs)})"
2278
+ )
2279
+
2115
2280
  # -- statements -------------------------------------------------------
2116
2281
 
2117
2282
  def _axon_add_parts(self, stmt):
@@ -2248,18 +2413,46 @@ class BaseCodegen:
2248
2413
  f"state parameter `{', '.join(state_names)}`), got "
2249
2414
  f"{len(stmt.values)}",
2250
2415
  )
2251
- for state_name, value in zip(state_names, stmt.values):
2416
+ # PARALLEL assignment (2026-07-13 reach-audit fix): every pass
2417
+ # value is evaluated against the CURRENT (pre-assignment)
2418
+ # environment, THEN the state locals update together — the RNN
2419
+ # cell semantics `state ← f(state)`. The old sequential
2420
+ # emission clobbered earlier states before later values read
2421
+ # them: `pass b, a + b` (fibonacci) emitted `a = b` then
2422
+ # `b = num_add(a, b)` with the NEW a, degenerating into
2423
+ # doubling (measured: fib after 8 ticks read 128 = 2^7
2424
+ # instead of 34). Staging through `_passN` temporaries makes
2425
+ # the two-phase update explicit; single-value passes skip the
2426
+ # temporary (nothing to clobber).
2427
+ if len(stmt.values) == 1:
2428
+ value = stmt.values[0]
2429
+ if isinstance(value, ast.ReplaceMarker):
2430
+ self._emit(f"{state_names[0]} = _init_{state_names[0]}")
2431
+ else:
2432
+ self._emit(
2433
+ f"{state_names[0]} = "
2434
+ f"{self._translate_expr(value)}"
2435
+ )
2436
+ return
2437
+ tmp_names: list[str] = []
2438
+ for idx, value in enumerate(stmt.values):
2439
+ tmp = f"_pass{idx}"
2440
+ tmp_names.append(tmp)
2252
2441
  if isinstance(value, ast.ReplaceMarker):
2253
2442
  # `replace` keyword: restore the parameter's input value.
2254
- self._emit(f"{state_name} = _init_{state_name}")
2443
+ self._emit(f"{tmp} = _init_{state_names[idx]}")
2255
2444
  else:
2256
- value_src = self._translate_expr(value)
2257
- self._emit(f"{state_name} = {value_src}")
2445
+ self._emit(f"{tmp} = {self._translate_expr(value)}")
2446
+ for state_name, tmp in zip(state_names, tmp_names):
2447
+ self._emit(f"{state_name} = {tmp}")
2258
2448
  return
2259
2449
  # LoopCallStmt: invoke a loop function and write back state.
2260
2450
  if isinstance(stmt, ast.LoopCallStmt):
2261
2451
  self._translate_loop_call(stmt)
2262
2452
  return
2453
+ if isinstance(stmt, ast.LoopDestructureStmt):
2454
+ self._translate_loop_destructure(stmt)
2455
+ return
2263
2456
  if isinstance(stmt, ast.VarDecl):
2264
2457
  self._translate_var_decl(stmt, at_top_level=False)
2265
2458
  return
@@ -2634,10 +2827,21 @@ class BaseCodegen:
2634
2827
  f"unroll by hand.",
2635
2828
  )
2636
2829
  if isinstance(stmt, ast.IfStmt):
2830
+ # Design commitment, not a gap (control-flow.md § "`if` /
2831
+ # `else` is parsed but rejected at codegen"): Sutra
2832
+ # conditionals are weighted superpositions, not discrete
2833
+ # branches. The message must hand the newcomer the actual
2834
+ # rewrite, not just the philosophy (round-23 audit).
2637
2835
  raise CodegenNotSupported(
2638
2836
  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",
2837
+ "`if`/`else` never compiles by design, Sutra "
2838
+ "conditionals are weighted superpositions, not discrete "
2839
+ "branches. Write `select([score_a, score_b, ...], "
2840
+ "[result_a, result_b, ...])`: the highest score's result "
2841
+ "dominates the output. A boolean two-way branch is "
2842
+ "`select([is_true(cond), is_true(!cond)], [then_value, "
2843
+ "else_value])`. See the control-flow docs and "
2844
+ "examples/fuzzy_dispatch.su for the idiom.",
2641
2845
  )
2642
2846
  if isinstance(stmt, ast.TryStmt):
2643
2847
  self._translate_try_catch(stmt)
@@ -3042,6 +3246,10 @@ class BaseCodegen:
3042
3246
  return (self._is_text_expr(expr.left)
3043
3247
  and self._is_text_expr(expr.right))
3044
3248
  if isinstance(expr, ast.Call):
3249
+ if (isinstance(expr.callee, ast.Identifier)
3250
+ and getattr(self, "_user_fn_return_types", {}).get(
3251
+ expr.callee.name) in self._TEXT_TYPES):
3252
+ return True
3045
3253
  return self._resolved_return_type(expr.callee) in self._TEXT_TYPES
3046
3254
  return False
3047
3255
 
@@ -3058,6 +3266,15 @@ class BaseCodegen:
3058
3266
  return self._var_type.get(expr.name) in self._SYNTHETIC_AXIS_TYPES
3059
3267
  if isinstance(expr, ast.Parenthesized):
3060
3268
  return self._is_synthetic_axis_expr(expr.inner)
3269
+ # A call whose declared return type is synthetic-axis-encoded
3270
+ # (2026-07-13 reach-audit defect #6): `make_string("cat") ==
3271
+ # make_string("dog")` fell through to the general cosine `eq`,
3272
+ # and two codepoint vectors are nearly parallel — ALL Strings
3273
+ # read ~equal (measured +0.994). Same conservative
3274
+ # Call-return-type pattern `_is_number_expr` already uses.
3275
+ if isinstance(expr, ast.Call):
3276
+ return (self._resolved_return_type(expr.callee)
3277
+ in self._SYNTHETIC_AXIS_TYPES)
3061
3278
  return False
3062
3279
 
3063
3280
  def _is_number_expr(self, expr: ast.Expr) -> bool:
@@ -3390,6 +3607,8 @@ class BaseCodegen:
3390
3607
  return f"{target_src}[{index_src}]"
3391
3608
  if isinstance(expr, ast.Call):
3392
3609
  return self._translate_call(expr)
3610
+ if isinstance(expr, ast.LoopCallExpr):
3611
+ return self._translate_loop_call_expr(expr)
3393
3612
  if isinstance(expr, ast.BinaryOp):
3394
3613
  left = self._translate_expr(expr.left)
3395
3614
  right = self._translate_expr(expr.right)
@@ -3653,29 +3872,34 @@ class BaseCodegen:
3653
3872
  self._translate_expr(part, dest_type="string"))
3654
3873
  continue
3655
3874
  if part_t == "int":
3656
- # strings.md § Integer formatting: the substrate
3657
- # int→string formatter. int ONLY — a fractional
3658
- # `number` rendering as its rounded integer would
3659
- # silently lie, so it keeps rejecting below.
3875
+ # strings.md § Integer formatting: exact integer
3876
+ # rendering.
3660
3877
  pieces.append(
3661
3878
  f"_VSA.int_to_string("
3662
3879
  f"{self._translate_expr(part)})")
3663
3880
  continue
3881
+ if self._CAST_FAMILY.get(part_t) == "number":
3882
+ # strings.md § Decimal formatting: fractional
3883
+ # number interpolants render via num_to_string
3884
+ # (shortest decimal, <=6 places).
3885
+ pieces.append(
3886
+ f"_VSA.num_to_string("
3887
+ f"{self._translate_expr(part)})")
3888
+ continue
3664
3889
  if part_t is None:
3665
3890
  raise CodegenNotSupported(
3666
3891
  part,
3667
3892
  "interpolated expression must be string- or "
3668
- "int-typed and statically inferable — assign it "
3669
- "to a `string` (or `int`) variable first, or "
3670
- "build the text with make_string / string_concat."
3893
+ "number-typed and statically inferable — assign "
3894
+ "it to a typed variable first, or build the text "
3895
+ "with make_string / string_concat."
3671
3896
  )
3672
3897
  raise CodegenNotSupported(
3673
3898
  part,
3674
- f"cannot interpolate a `{part_t}` value: only string- "
3675
- "and int-typed interpolants lower (int_to_string is "
3676
- "integer-shaped; fractional rendering is not built). "
3677
- "For an integer value, declare it `int`; otherwise "
3678
- "build the text with make_string / string_concat."
3899
+ f"cannot interpolate a `{part_t}` value: string- and "
3900
+ "number-family interpolants lower (int_to_string / "
3901
+ "num_to_string); build other text with make_string / "
3902
+ "string_concat."
3679
3903
  )
3680
3904
  if not pieces:
3681
3905
  return "_VSA.make_string('')"
@@ -3743,6 +3967,20 @@ class BaseCodegen:
3743
3967
  exprs. None when unknown — the caller decides whether the
3744
3968
  target makes the relabel/axis-move distinction load-bearing."""
3745
3969
  from .symbol_table import _LITERAL_TYPES
3970
+ # Inliner typing provenance (2026-07-13): a comparison that the
3971
+ # operator-lowering inlined (`best >= e` → ge's body polynomial)
3972
+ # carries `_truth_typed` on its TOP node — the expression's VALUE
3973
+ # is a truth-axis fuzzy, so report "bool" for the cast decision.
3974
+ #
3975
+ # Do NOT read `_logical_truth` here: `_mark_logical_truth` sprays
3976
+ # that flag over the ENTIRE inlined subtree including substituted
3977
+ # ARGUMENT subtrees (it drives arithmetic routing, not typing).
3978
+ # Reading it as type evidence made the `==` operands inside ge's
3979
+ # body infer "bool", miss num_eq, and soften through eq_synthetic
3980
+ # — the CI-caught le/ge regression (`2 >= 3` → −0.52 instead of
3981
+ # the crisp −1.0 the 2026-07-08 tie fix guarantees).
3982
+ if getattr(expr, "_truth_typed", False):
3983
+ return "bool"
3746
3984
  kind = type(expr).__name__
3747
3985
  if kind in _LITERAL_TYPES:
3748
3986
  return _LITERAL_TYPES[kind]
@@ -3761,6 +3999,19 @@ class BaseCodegen:
3761
3999
  if (self._CAST_FAMILY.get(lt) == "number"
3762
4000
  and self._CAST_FAMILY.get(rt) == "number"):
3763
4001
  return "number"
4002
+ # Call whose declared return type is known (2026-07-13 reach-audit
4003
+ # fix). Two silent failures traced here: (a) the inliner rewrites
4004
+ # `<`/`<=`/`>=`/`!=` into stdlib Calls (`lt`/`le`/`ge`/`neq`,
4005
+ # declared `fuzzy`), so `(number)(best >= e)` arrived as a Call,
4006
+ # inferred None, and the cast DIED "static type can't be inferred";
4007
+ # (b) `string_char_at(s, i) == 97` — char_at declares `int`, but
4008
+ # with no Call inference the == routed to general vector `eq`,
4009
+ # whose cosine reads ANY two number-vectors as equal (98 == 97 →
4010
+ # true) — silently wrong counts. `_resolved_return_type` consults
4011
+ # user-class methods + the stdlib symbol table; conservative (None
4012
+ # when undeclared), same source `_is_complex_expr` already trusts.
4013
+ if isinstance(expr, ast.Call):
4014
+ return self._resolved_return_type(expr.callee)
3764
4015
  return None
3765
4016
 
3766
4017
  def _translate_cast(self, expr: ast.CastExpr) -> str:
@@ -3778,17 +4029,19 @@ class BaseCodegen:
3778
4029
  # --- text walls -------------------------------------------------
3779
4030
  if dst_family == "text" and src_family != "text":
3780
4031
  if src == "int":
3781
- # strings.md § Integer formatting: `(string) n` on an
3782
- # int is the integer formatter — the one number→text
3783
- # cast with a real substrate implementation.
4032
+ # strings.md § Integer formatting: exact integer render.
3784
4033
  return (f"_VSA.int_to_string("
3785
4034
  f"{self._translate_expr(expr.expr)})")
4035
+ if src_family == "number":
4036
+ # strings.md § Decimal formatting: fractional numbers
4037
+ # render via num_to_string (shortest decimal, <=6 places).
4038
+ return (f"_VSA.num_to_string("
4039
+ f"{self._translate_expr(expr.expr)})")
3786
4040
  raise CodegenNotSupported(
3787
4041
  expr,
3788
- f"cannot cast to `{dst}`: only `int` values format "
3789
- "(int_to_string integer-shaped; fractional rendering is "
3790
- "not built). For a literal, use `make_string(\"...\")`; "
3791
- "join strings with `string_concat`."
4042
+ f"cannot cast to `{dst}`: number-family values format via "
4043
+ "int_to_string / num_to_string; for a literal use "
4044
+ "`make_string(\"...\")`, join strings with `string_concat`."
3792
4045
  )
3793
4046
  if src_family == "text" and dst_family != "text":
3794
4047
  if dst_family == "geometric":