sutra-dev 0.9.4__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.4/sutra_dev.egg-info → sutra_dev-0.10.0}/PKG-INFO +1 -1
  2. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/pyproject.toml +1 -1
  3. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/__init__.py +1 -1
  4. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/__main__.py +13 -2
  5. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/ast_nodes.py +38 -0
  6. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/codegen.py +33 -21
  7. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/codegen_base.py +252 -27
  8. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/codegen_pytorch.py +117 -29
  9. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/inliner.py +38 -0
  10. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/parser.py +134 -0
  11. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/repl.py +4 -0
  12. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/symbol_table.py +3 -0
  13. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/validator.py +72 -0
  14. {sutra_dev-0.9.4 → sutra_dev-0.10.0/sutra_dev.egg-info}/PKG-INFO +1 -1
  15. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_dev.egg-info/SOURCES.txt +3 -0
  16. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_cast_codegen.py +21 -8
  17. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_interp_string_codegen.py +20 -7
  18. sutra_dev-0.10.0/tests/test_loop_call_expr.py +643 -0
  19. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_loop_function_decl.py +13 -2
  20. sutra_dev-0.10.0/tests/test_num_to_string.py +76 -0
  21. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_repl.py +29 -0
  22. sutra_dev-0.10.0/tests/test_slot_state_diagnostic.py +119 -0
  23. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_terminal_string_decode.py +18 -0
  24. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_torch_compile_wrap.py +5 -1
  25. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/LICENSE +0 -0
  26. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/README.md +0 -0
  27. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/setup.cfg +0 -0
  28. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/axon_keys.py +0 -0
  29. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/cached_compile.py +0 -0
  30. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/codegen_thrml.py +0 -0
  31. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/diagnostics.py +0 -0
  32. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/embedding.py +0 -0
  33. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/fv.py +0 -0
  34. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/fv_key_soundness.py +0 -0
  35. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/fv_loop_convergence.py +0 -0
  36. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/fv_obligation_checker.py +0 -0
  37. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/fv_poly_bound.py +0 -0
  38. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/fv_sampler_convergence.py +0 -0
  39. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/lexer.py +0 -0
  40. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/loop_capture.py +0 -0
  41. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/loop_desugar.py +0 -0
  42. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/multi_process.py +0 -0
  43. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/preeval.py +0 -0
  44. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/promise_desugar.py +0 -0
  45. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/review.py +0 -0
  46. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/simplify.py +0 -0
  47. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/simplify_egglog.py +0 -0
  48. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/stdlib/axons.su +0 -0
  49. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/stdlib/bigint.su +0 -0
  50. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/stdlib/bitwise.su +0 -0
  51. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/stdlib/embed.su +0 -0
  52. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/stdlib/javascript_object.su +0 -0
  53. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/stdlib/javascript_primitives.su +0 -0
  54. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/stdlib/logic.su +0 -0
  55. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/stdlib/math.su +0 -0
  56. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/stdlib/memory.su +0 -0
  57. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/stdlib/modulus.su +0 -0
  58. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/stdlib/numbers.su +0 -0
  59. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/stdlib/promises.su +0 -0
  60. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/stdlib/rotation.su +0 -0
  61. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/stdlib/similarity.su +0 -0
  62. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/stdlib/strings.su +0 -0
  63. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/stdlib/tensor.su +0 -0
  64. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/stdlib/vectors.su +0 -0
  65. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/stdlib_loader.py +0 -0
  66. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/sutradb_embedded.py +0 -0
  67. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/tabulate.py +0 -0
  68. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/trace.py +0 -0
  69. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/workspace.py +0 -0
  70. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_dev.egg-info/dependency_links.txt +0 -0
  71. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_dev.egg-info/entry_points.txt +0 -0
  72. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_dev.egg-info/requires.txt +0 -0
  73. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_dev.egg-info/top_level.txt +0 -0
  74. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_arity_diagnostic.py +0 -0
  75. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_arrow_functions.py +0 -0
  76. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_await_midfunction.py +0 -0
  77. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_await_substrate_pure.py +0 -0
  78. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_axon_build.py +0 -0
  79. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_axon_device_coherence.py +0 -0
  80. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_axon_item_call_result.py +0 -0
  81. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_axon_keys.py +0 -0
  82. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_axon_project.py +0 -0
  83. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_branchless_loop.py +0 -0
  84. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_buffer_transcendentals.py +0 -0
  85. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_cached_compile.py +0 -0
  86. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_class_fields_runtime.py +0 -0
  87. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_codegen.py +0 -0
  88. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_codegen_pytorch.py +0 -0
  89. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_codegen_thrml.py +0 -0
  90. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_corpus.py +0 -0
  91. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_dimension_audit.py +0 -0
  92. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_dimension_audit_warning.py +0 -0
  93. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_embedding_provider.py +0 -0
  94. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_equality_autograd.py +0 -0
  95. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_fused_nn.py +0 -0
  96. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_fv_general_checker.py +0 -0
  97. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_fv_key_soundness.py +0 -0
  98. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_fv_kleene_grid_exactness.py +0 -0
  99. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_fv_loop_convergence.py +0 -0
  100. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_fv_poly_obligation_checker.py +0 -0
  101. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_fv_public_api.py +0 -0
  102. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_fv_sampler_convergence.py +0 -0
  103. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_fv_termination.py +0 -0
  104. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_fv_worked_example.py +0 -0
  105. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_higher_order_functions.py +0 -0
  106. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_implicit_loop_desugar.py +0 -0
  107. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_inliner.py +0 -0
  108. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_int_dict.py +0 -0
  109. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_int_to_string.py +0 -0
  110. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_jvm_core.py +0 -0
  111. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_le_ge_ties.py +0 -0
  112. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_lexer.py +0 -0
  113. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_list_ops.py +0 -0
  114. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_load_matrix.py +0 -0
  115. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_loop_capture.py +0 -0
  116. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_make_string_idempotent.py +0 -0
  117. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_matrix_literal.py +0 -0
  118. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_measurement_claim_sweep.py +0 -0
  119. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_mini_wasm_machine.py +0 -0
  120. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_multi_process_runtime.py +0 -0
  121. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_native_recursion.py +0 -0
  122. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_no_host_readout.py +0 -0
  123. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_ntm_ram.py +0 -0
  124. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_num_eq_indicator.py +0 -0
  125. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_optional_llm_model.py +0 -0
  126. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_parser.py +0 -0
  127. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_preeval.py +0 -0
  128. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_process_pool_runtime.py +0 -0
  129. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_python_builtin_escape.py +0 -0
  130. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_ram_direct_tensor.py +0 -0
  131. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_rotation_prewarm.py +0 -0
  132. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_run_error_diagnostics.py +0 -0
  133. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_simplify.py +0 -0
  134. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_simplify_egglog.py +0 -0
  135. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_simplify_egglog_cse.py +0 -0
  136. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_snap_diagnostic.py +0 -0
  137. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_stdlib_loader.py +0 -0
  138. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_string_equality.py +0 -0
  139. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_string_plus_concat.py +0 -0
  140. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_substrate_leak_sweep.py +0 -0
  141. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_sutradb_embedded.py +0 -0
  142. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_symbol_table.py +0 -0
  143. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_tabulate.py +0 -0
  144. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_trainable_matrix.py +0 -0
  145. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_transcendentals.py +0 -0
  146. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_type_inference.py +0 -0
  147. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_type_test_gap.py +0 -0
  148. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_unknown_function_diagnostic.py +0 -0
  149. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_unknown_type_diagnostic.py +0 -0
  150. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_unknown_variable_diagnostic.py +0 -0
  151. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_unsafe_override_codegen.py +0 -0
  152. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_vector_literal.py +0 -0
  153. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_wasm_core.py +0 -0
  154. {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_workspace.py +0 -0
  155. {sutra_dev-0.9.4 → 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.4
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.4"
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.4"
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
  # ============================================================
@@ -1139,34 +1139,34 @@ class Codegen(BaseCodegen):
1139
1139
  self._emit("return (base, base + 1)")
1140
1140
  self._indent -= 1
1141
1141
  self._emit()
1142
- self._emit("def slot_store(self, state, slot_idx, scalar):")
1142
+ self._emit("def slot_state_new(self):")
1143
1143
  self._indent += 1
1144
- self._emit('"""Write scalar to slot slot_idx. Overwrites the slot\'s plane.')
1145
- self._emit('')
1146
- self._emit("The scalar lives on the real leg of the slot\'s 2D plane; the")
1147
- self._emit("imaginary leg is zeroed. A subsequent slot_load returns the scalar")
1148
- self._emit("exactly. This is the reversible-imperative-state primitive: a")
1149
- self._emit("variable assignment = one slot_store; the inverse is slot_store of")
1150
- 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.")
1151
1149
  self._emit('"""')
1152
- self._emit("i, j = self._slot_plane(slot_idx)")
1153
- self._emit("new = state.copy() if hasattr(state, 'copy') else _np.asarray(state).copy()")
1154
- self._emit("new[i] = float(scalar)")
1155
- 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")
1156
1161
  self._emit("return new")
1157
1162
  self._indent -= 1
1158
1163
  self._emit()
1159
1164
  self._emit("def slot_load(self, state, slot_idx):")
1160
1165
  self._indent += 1
1161
- self._emit('"""Read the scalar stored at slot slot_idx (the real leg).')
1162
- self._emit('')
1163
- self._emit("Returns a substrate scalar (numpy 0-dim from state[i]) rather than a")
1164
- self._emit("Python float downstream arithmetic stays in substrate land. Other")
1165
- self._emit("slots and semantic content do not contribute; the disjoint-plane")
1166
- self._emit("allocation makes this a projection, not a noisy readout.")
1167
- self._emit('"""')
1168
- self._emit("i, _j = self._slot_plane(slot_idx)")
1169
- 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")
1170
1170
  self._indent -= 1
1171
1171
  self._emit()
1172
1172
  self._emit("def rotate_slot(self, state, slot_idx, angle):")
@@ -1487,6 +1487,18 @@ class Codegen(BaseCodegen):
1487
1487
  self._emit("return v")
1488
1488
  self._indent -= 1
1489
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()
1490
1502
  self._emit("def _num_re(self, v):")
1491
1503
  self._indent += 1
1492
1504
  self._emit('"""Real-axis scalar of a number value — mirrors the pytorch backend\'s')
@@ -1256,7 +1256,7 @@ class BaseCodegen:
1256
1256
  self._var_type[p.name] = p.type_ref.name
1257
1257
  self._emit("_program_halt = 1.0")
1258
1258
  if _has_slot_decl(decl.body):
1259
- self._emit("_slot_state = _VSA.zero_vector()")
1259
+ self._emit("_slot_state = _VSA.slot_state_new()")
1260
1260
  if not decl.body.statements:
1261
1261
  self._emit("pass")
1262
1262
  else:
@@ -1386,7 +1386,7 @@ class BaseCodegen:
1386
1386
  self._nonhalting_slot_var = nonhalting_slot_var
1387
1387
  self._emit("_program_halt = 1.0")
1388
1388
  if _has_slot_decl(decl.body):
1389
- self._emit("_slot_state = _VSA.zero_vector()")
1389
+ self._emit("_slot_state = _VSA.slot_state_new()")
1390
1390
  if not decl.body.statements:
1391
1391
  self._emit("pass")
1392
1392
  else:
@@ -1859,9 +1859,16 @@ class BaseCodegen:
1859
1859
  self._emit(f"# iterative_loop: tick = _t+1, halt when tick > count.")
1860
1860
  self._emit(f"_iterator = _t + 1")
1861
1861
  # Heaviside of (count - iterator + 1): positive while iterator
1862
- # <= 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).
1863
1869
  self._emit(
1864
- f"_keep = _VSA.heaviside(int({count_src}) - _iterator + 1)"
1870
+ f"_keep = _VSA.heaviside(int(_VSA._scalar({count_src})) "
1871
+ f"- _iterator + 1)"
1865
1872
  )
1866
1873
  elif decl.kind == "foreach_loop":
1867
1874
  # foreach: array is an explicit step parameter (`arr_param_name`).
@@ -2125,6 +2132,151 @@ class BaseCodegen:
2125
2132
  # this loop's completion gates the function's return value.
2126
2133
  self._emit("_program_halt = _program_halt * _loopret_halt")
2127
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
+
2128
2280
  # -- statements -------------------------------------------------------
2129
2281
 
2130
2282
  def _axon_add_parts(self, stmt):
@@ -2261,18 +2413,46 @@ class BaseCodegen:
2261
2413
  f"state parameter `{', '.join(state_names)}`), got "
2262
2414
  f"{len(stmt.values)}",
2263
2415
  )
2264
- 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)
2265
2441
  if isinstance(value, ast.ReplaceMarker):
2266
2442
  # `replace` keyword: restore the parameter's input value.
2267
- self._emit(f"{state_name} = _init_{state_name}")
2443
+ self._emit(f"{tmp} = _init_{state_names[idx]}")
2268
2444
  else:
2269
- value_src = self._translate_expr(value)
2270
- 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}")
2271
2448
  return
2272
2449
  # LoopCallStmt: invoke a loop function and write back state.
2273
2450
  if isinstance(stmt, ast.LoopCallStmt):
2274
2451
  self._translate_loop_call(stmt)
2275
2452
  return
2453
+ if isinstance(stmt, ast.LoopDestructureStmt):
2454
+ self._translate_loop_destructure(stmt)
2455
+ return
2276
2456
  if isinstance(stmt, ast.VarDecl):
2277
2457
  self._translate_var_decl(stmt, at_top_level=False)
2278
2458
  return
@@ -3086,6 +3266,15 @@ class BaseCodegen:
3086
3266
  return self._var_type.get(expr.name) in self._SYNTHETIC_AXIS_TYPES
3087
3267
  if isinstance(expr, ast.Parenthesized):
3088
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)
3089
3278
  return False
3090
3279
 
3091
3280
  def _is_number_expr(self, expr: ast.Expr) -> bool:
@@ -3418,6 +3607,8 @@ class BaseCodegen:
3418
3607
  return f"{target_src}[{index_src}]"
3419
3608
  if isinstance(expr, ast.Call):
3420
3609
  return self._translate_call(expr)
3610
+ if isinstance(expr, ast.LoopCallExpr):
3611
+ return self._translate_loop_call_expr(expr)
3421
3612
  if isinstance(expr, ast.BinaryOp):
3422
3613
  left = self._translate_expr(expr.left)
3423
3614
  right = self._translate_expr(expr.right)
@@ -3681,29 +3872,34 @@ class BaseCodegen:
3681
3872
  self._translate_expr(part, dest_type="string"))
3682
3873
  continue
3683
3874
  if part_t == "int":
3684
- # strings.md § Integer formatting: the substrate
3685
- # int→string formatter. int ONLY — a fractional
3686
- # `number` rendering as its rounded integer would
3687
- # silently lie, so it keeps rejecting below.
3875
+ # strings.md § Integer formatting: exact integer
3876
+ # rendering.
3688
3877
  pieces.append(
3689
3878
  f"_VSA.int_to_string("
3690
3879
  f"{self._translate_expr(part)})")
3691
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
3692
3889
  if part_t is None:
3693
3890
  raise CodegenNotSupported(
3694
3891
  part,
3695
3892
  "interpolated expression must be string- or "
3696
- "int-typed and statically inferable — assign it "
3697
- "to a `string` (or `int`) variable first, or "
3698
- "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."
3699
3896
  )
3700
3897
  raise CodegenNotSupported(
3701
3898
  part,
3702
- f"cannot interpolate a `{part_t}` value: only string- "
3703
- "and int-typed interpolants lower (int_to_string is "
3704
- "integer-shaped; fractional rendering is not built). "
3705
- "For an integer value, declare it `int`; otherwise "
3706
- "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."
3707
3903
  )
3708
3904
  if not pieces:
3709
3905
  return "_VSA.make_string('')"
@@ -3771,6 +3967,20 @@ class BaseCodegen:
3771
3967
  exprs. None when unknown — the caller decides whether the
3772
3968
  target makes the relabel/axis-move distinction load-bearing."""
3773
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"
3774
3984
  kind = type(expr).__name__
3775
3985
  if kind in _LITERAL_TYPES:
3776
3986
  return _LITERAL_TYPES[kind]
@@ -3789,6 +3999,19 @@ class BaseCodegen:
3789
3999
  if (self._CAST_FAMILY.get(lt) == "number"
3790
4000
  and self._CAST_FAMILY.get(rt) == "number"):
3791
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)
3792
4015
  return None
3793
4016
 
3794
4017
  def _translate_cast(self, expr: ast.CastExpr) -> str:
@@ -3806,17 +4029,19 @@ class BaseCodegen:
3806
4029
  # --- text walls -------------------------------------------------
3807
4030
  if dst_family == "text" and src_family != "text":
3808
4031
  if src == "int":
3809
- # strings.md § Integer formatting: `(string) n` on an
3810
- # int is the integer formatter — the one number→text
3811
- # cast with a real substrate implementation.
4032
+ # strings.md § Integer formatting: exact integer render.
3812
4033
  return (f"_VSA.int_to_string("
3813
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)})")
3814
4040
  raise CodegenNotSupported(
3815
4041
  expr,
3816
- f"cannot cast to `{dst}`: only `int` values format "
3817
- "(int_to_string integer-shaped; fractional rendering is "
3818
- "not built). For a literal, use `make_string(\"...\")`; "
3819
- "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`."
3820
4045
  )
3821
4046
  if src_family == "text" and dst_family != "text":
3822
4047
  if dst_family == "geometric":