sutra-dev 0.9.2__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 (154) hide show
  1. {sutra_dev-0.9.2/sutra_dev.egg-info → sutra_dev-0.9.4}/PKG-INFO +1 -1
  2. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/pyproject.toml +1 -1
  3. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_compiler/__init__.py +1 -1
  4. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_compiler/__main__.py +35 -9
  5. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_compiler/codegen.py +13 -1
  6. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_compiler/codegen_base.py +273 -2
  7. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_compiler/codegen_pytorch.py +147 -13
  8. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_compiler/embedding.py +10 -2
  9. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_compiler/fv.py +24 -0
  10. sutra_dev-0.9.4/sutra_compiler/fv_loop_convergence.py +177 -0
  11. sutra_dev-0.9.4/sutra_compiler/fv_sampler_convergence.py +199 -0
  12. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_compiler/parser.py +31 -1
  13. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_compiler/repl.py +132 -6
  14. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_compiler/stdlib/logic.su +12 -8
  15. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_compiler/stdlib/strings.su +6 -0
  16. sutra_dev-0.9.4/sutra_compiler/symbol_table.py +550 -0
  17. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_compiler/validator.py +250 -1
  18. {sutra_dev-0.9.2 → sutra_dev-0.9.4/sutra_dev.egg-info}/PKG-INFO +1 -1
  19. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_dev.egg-info/SOURCES.txt +23 -1
  20. sutra_dev-0.9.4/tests/test_arity_diagnostic.py +82 -0
  21. sutra_dev-0.9.4/tests/test_cast_codegen.py +183 -0
  22. sutra_dev-0.9.4/tests/test_fv_loop_convergence.py +164 -0
  23. sutra_dev-0.9.4/tests/test_fv_sampler_convergence.py +121 -0
  24. sutra_dev-0.9.4/tests/test_int_to_string.py +84 -0
  25. sutra_dev-0.9.4/tests/test_interp_string_codegen.py +86 -0
  26. sutra_dev-0.9.4/tests/test_le_ge_ties.py +59 -0
  27. sutra_dev-0.9.4/tests/test_make_string_idempotent.py +62 -0
  28. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_no_host_readout.py +3 -1
  29. sutra_dev-0.9.4/tests/test_ntm_ram.py +492 -0
  30. sutra_dev-0.9.4/tests/test_num_eq_indicator.py +96 -0
  31. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_parser.py +25 -0
  32. sutra_dev-0.9.4/tests/test_python_builtin_escape.py +66 -0
  33. sutra_dev-0.9.4/tests/test_repl.py +132 -0
  34. sutra_dev-0.9.4/tests/test_run_error_diagnostics.py +53 -0
  35. sutra_dev-0.9.4/tests/test_string_plus_concat.py +102 -0
  36. sutra_dev-0.9.4/tests/test_symbol_table.py +250 -0
  37. sutra_dev-0.9.4/tests/test_type_inference.py +105 -0
  38. sutra_dev-0.9.4/tests/test_unknown_function_diagnostic.py +100 -0
  39. sutra_dev-0.9.4/tests/test_unknown_type_diagnostic.py +89 -0
  40. sutra_dev-0.9.4/tests/test_unknown_variable_diagnostic.py +114 -0
  41. sutra_dev-0.9.4/tests/test_unsafe_override_codegen.py +42 -0
  42. sutra_dev-0.9.4/tests/test_wrong_arg_type_diagnostic.py +81 -0
  43. sutra_dev-0.9.2/tests/test_ntm_ram.py +0 -263
  44. sutra_dev-0.9.2/tests/test_repl.py +0 -73
  45. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/LICENSE +0 -0
  46. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/README.md +0 -0
  47. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/setup.cfg +0 -0
  48. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_compiler/ast_nodes.py +0 -0
  49. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_compiler/axon_keys.py +0 -0
  50. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_compiler/cached_compile.py +0 -0
  51. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_compiler/codegen_thrml.py +0 -0
  52. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_compiler/diagnostics.py +0 -0
  53. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_compiler/fv_key_soundness.py +0 -0
  54. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_compiler/fv_obligation_checker.py +0 -0
  55. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_compiler/fv_poly_bound.py +0 -0
  56. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_compiler/inliner.py +0 -0
  57. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_compiler/lexer.py +0 -0
  58. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_compiler/loop_capture.py +0 -0
  59. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_compiler/loop_desugar.py +0 -0
  60. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_compiler/multi_process.py +0 -0
  61. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_compiler/preeval.py +0 -0
  62. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_compiler/promise_desugar.py +0 -0
  63. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_compiler/review.py +0 -0
  64. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_compiler/simplify.py +0 -0
  65. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_compiler/simplify_egglog.py +0 -0
  66. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_compiler/stdlib/axons.su +0 -0
  67. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_compiler/stdlib/bigint.su +0 -0
  68. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_compiler/stdlib/bitwise.su +0 -0
  69. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_compiler/stdlib/embed.su +0 -0
  70. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_compiler/stdlib/javascript_object.su +0 -0
  71. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_compiler/stdlib/javascript_primitives.su +0 -0
  72. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_compiler/stdlib/math.su +0 -0
  73. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_compiler/stdlib/memory.su +0 -0
  74. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_compiler/stdlib/modulus.su +0 -0
  75. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_compiler/stdlib/numbers.su +0 -0
  76. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_compiler/stdlib/promises.su +0 -0
  77. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_compiler/stdlib/rotation.su +0 -0
  78. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_compiler/stdlib/similarity.su +0 -0
  79. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_compiler/stdlib/tensor.su +0 -0
  80. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_compiler/stdlib/vectors.su +0 -0
  81. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_compiler/stdlib_loader.py +0 -0
  82. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_compiler/sutradb_embedded.py +0 -0
  83. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_compiler/tabulate.py +0 -0
  84. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_compiler/trace.py +0 -0
  85. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_compiler/workspace.py +0 -0
  86. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_dev.egg-info/dependency_links.txt +0 -0
  87. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_dev.egg-info/entry_points.txt +0 -0
  88. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_dev.egg-info/requires.txt +0 -0
  89. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/sutra_dev.egg-info/top_level.txt +0 -0
  90. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_arrow_functions.py +0 -0
  91. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_await_midfunction.py +0 -0
  92. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_await_substrate_pure.py +0 -0
  93. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_axon_build.py +0 -0
  94. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_axon_device_coherence.py +0 -0
  95. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_axon_item_call_result.py +0 -0
  96. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_axon_keys.py +0 -0
  97. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_axon_project.py +0 -0
  98. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_branchless_loop.py +0 -0
  99. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_buffer_transcendentals.py +0 -0
  100. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_cached_compile.py +0 -0
  101. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_class_fields_runtime.py +0 -0
  102. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_codegen.py +0 -0
  103. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_codegen_pytorch.py +0 -0
  104. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_codegen_thrml.py +0 -0
  105. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_corpus.py +0 -0
  106. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_dimension_audit.py +0 -0
  107. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_dimension_audit_warning.py +0 -0
  108. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_embedding_provider.py +0 -0
  109. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_equality_autograd.py +0 -0
  110. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_fused_nn.py +0 -0
  111. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_fv_general_checker.py +0 -0
  112. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_fv_key_soundness.py +0 -0
  113. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_fv_kleene_grid_exactness.py +0 -0
  114. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_fv_poly_obligation_checker.py +0 -0
  115. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_fv_public_api.py +0 -0
  116. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_fv_termination.py +0 -0
  117. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_fv_worked_example.py +0 -0
  118. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_higher_order_functions.py +0 -0
  119. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_implicit_loop_desugar.py +0 -0
  120. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_inliner.py +0 -0
  121. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_int_dict.py +0 -0
  122. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_jvm_core.py +0 -0
  123. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_lexer.py +0 -0
  124. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_list_ops.py +0 -0
  125. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_load_matrix.py +0 -0
  126. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_loop_capture.py +0 -0
  127. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_loop_function_decl.py +0 -0
  128. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_matrix_literal.py +0 -0
  129. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_measurement_claim_sweep.py +0 -0
  130. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_mini_wasm_machine.py +0 -0
  131. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_multi_process_runtime.py +0 -0
  132. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_native_recursion.py +0 -0
  133. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_optional_llm_model.py +0 -0
  134. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_preeval.py +0 -0
  135. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_process_pool_runtime.py +0 -0
  136. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_ram_direct_tensor.py +0 -0
  137. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_rotation_prewarm.py +0 -0
  138. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_simplify.py +0 -0
  139. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_simplify_egglog.py +0 -0
  140. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_simplify_egglog_cse.py +0 -0
  141. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_snap_diagnostic.py +0 -0
  142. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_stdlib_loader.py +0 -0
  143. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_string_equality.py +0 -0
  144. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_substrate_leak_sweep.py +0 -0
  145. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_sutradb_embedded.py +0 -0
  146. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_tabulate.py +0 -0
  147. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_terminal_string_decode.py +0 -0
  148. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_torch_compile_wrap.py +0 -0
  149. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_trainable_matrix.py +0 -0
  150. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_transcendentals.py +0 -0
  151. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_type_test_gap.py +0 -0
  152. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_vector_literal.py +0 -0
  153. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_wasm_core.py +0 -0
  154. {sutra_dev-0.9.2 → sutra_dev-0.9.4}/tests/test_workspace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sutra-dev
3
- Version: 0.9.2
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.2"
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.2"
31
+ __version__ = "0.9.4"
32
32
 
33
33
  from .diagnostics import Diagnostic, DiagnosticLevel, DiagnosticBag
34
34
  from .lexer import Lexer, Token, TokenKind
@@ -325,11 +325,24 @@ def _run_execute(path: str, *, runtime_dim: int, runtime_seed: int,
325
325
  return 1
326
326
  mod = types.ModuleType("_sutra_run")
327
327
  mod.__file__ = f"<generated from {path}>"
328
- exec(compile(py_src, mod.__file__, "exec"), mod.__dict__)
329
- if hasattr(mod, "main") and callable(mod.main):
330
- result = mod.main()
331
- if result is not None:
332
- print(_decode_terminal_result(mod, result))
328
+ try:
329
+ exec(compile(py_src, mod.__file__, "exec"), mod.__dict__)
330
+ if hasattr(mod, "main") and callable(mod.main):
331
+ result = mod.main()
332
+ if result is not None:
333
+ print(_decode_terminal_result(mod, result))
334
+ else:
335
+ # The substrate program is the body of main(); a file without one
336
+ # has nothing to run. Say so rather than exiting silently with no
337
+ # output (a newcomer otherwise can't tell why nothing happened).
338
+ print(f"{path}: no main() found - nothing to run", file=sys.stderr)
339
+ except Exception as exc:
340
+ # A runtime error in the generated module — e.g. a type mismatch the v0.1
341
+ # validator can't catch yet — should read like a Sutra diagnostic, not an
342
+ # uncaught Python traceback. KeyboardInterrupt / SystemExit are not caught
343
+ # (they subclass BaseException, not Exception), so Ctrl-C still works.
344
+ print(f"{path}: runtime error: {type(exc).__name__}: {exc}", file=sys.stderr)
345
+ return 1
333
346
  return 0
334
347
 
335
348
 
@@ -387,7 +400,7 @@ def _run_viz(path: str, *, runtime_dim: int, runtime_seed: int,
387
400
  # Inject tracing shim: after the _VSA = _TorchVSA(...) line,
388
401
  # wrap every method with a tracing version.
389
402
  shim = '''
390
- # ── Tracing shim (injected by --run-viz) ──
403
+ # -- Tracing shim (injected by --run-viz) --
391
404
  _orig_embed = _VSA.embed
392
405
  _orig_bind = _VSA.bind
393
406
  _orig_unbind = _VSA.unbind
@@ -417,7 +430,7 @@ _VSA.embed = _traced_embed
417
430
  _VSA.bind = _traced_bind
418
431
  _VSA.unbind = _traced_unbind
419
432
  _VSA.bundle = _traced_bundle
420
- # ── End tracing shim ──
433
+ # -- End tracing shim --
421
434
  '''
422
435
  # Find the _VSA = _TorchVSA(...) line and inject after it
423
436
  lines = py_src.split('\n')
@@ -514,6 +527,19 @@ def main(argv: List[str] | None = None) -> int:
514
527
  parser = argparse.ArgumentParser(
515
528
  prog="sutrac",
516
529
  description="Validate Sutra (.su) source files.",
530
+ epilog=(
531
+ "interactive REPL:\n"
532
+ " sutrac repl Launch the interactive evaluator (takes no\n"
533
+ " file). Type an expression to see its value;\n"
534
+ " declarations accumulate as session state.\n"
535
+ "\n"
536
+ "examples:\n"
537
+ " sutrac file.su Validate a source file.\n"
538
+ " sutrac --run file.su Compile and execute (needs torch).\n"
539
+ " sutrac --emit file.su Print the generated PyTorch module.\n"
540
+ " sutrac repl Explore interactively."
541
+ ),
542
+ formatter_class=argparse.RawDescriptionHelpFormatter,
517
543
  )
518
544
  parser.add_argument(
519
545
  "paths",
@@ -541,7 +567,7 @@ def main(argv: List[str] | None = None) -> int:
541
567
  help=(
542
568
  "Compile the first input file to self-contained torch Python and "
543
569
  "print it to stdout. Picks CUDA at module init if available; "
544
- "falls back to CPU otherwise. This is the one main codegen target "
570
+ "falls back to CPU otherwise. This is the one main codegen target - "
545
571
  "PyTorch is the runtime and the tensor-op library Sutra compiles "
546
572
  "against."
547
573
  ),
@@ -564,7 +590,7 @@ def main(argv: List[str] | None = None) -> int:
564
590
  "stdout. The default PyTorch path (--emit/--run) is untouched; this is "
565
591
  "the Extropic thermodynamic-sampling compile target (queue.md approach "
566
592
  "G). Requires jax + the thrml submodule. Lowering coverage is the "
567
- "validated subset only see codegen_thrml.py."
593
+ "validated subset only - see codegen_thrml.py."
568
594
  ),
569
595
  )
570
596
  parser.add_argument(
@@ -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})"
@@ -1700,7 +1712,7 @@ class Codegen(BaseCodegen):
1700
1712
  self._emit("distinguish `'a'` (97 with flag) from `97` (97 without).")
1701
1713
  self._emit("Arithmetic on chars works the same as on ints — both")
1702
1714
  self._emit("live on the number axis. Downstream code that cares")
1703
- self._emit("about the distinction can read the flag via `is_char`.")
1715
+ self._emit("about the distinction can read the flag via `is_string`.")
1704
1716
  self._emit('"""')
1705
1717
  self._emit("v = _np.zeros(self.dim, dtype=_np.float64)")
1706
1718
  self._emit("v[self.semantic_dim + self.AXIS_REAL] = float(codepoint)")
@@ -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)
@@ -3021,6 +3045,34 @@ class BaseCodegen:
3021
3045
  "char", "Character", "string", "String"}
3022
3046
  )
3023
3047
 
3048
+ #: Types whose values are substrate Strings (text family).
3049
+ _TEXT_TYPES = frozenset({"string", "String", "char", "Character"})
3050
+
3051
+ def _is_text_expr(self, expr: ast.Expr) -> bool:
3052
+ """True iff expr is provably a text (String) value at compile
3053
+ time. Conservative — unknown falls through to the numeric /
3054
+ element-wise `+` paths, same posture as _is_number_expr."""
3055
+ if isinstance(expr, (ast.StringLiteral, ast.CharLiteral,
3056
+ ast.InterpolatedString)):
3057
+ return True
3058
+ if isinstance(expr, ast.Identifier):
3059
+ return self._var_type.get(expr.name) in self._TEXT_TYPES
3060
+ if isinstance(expr, ast.Parenthesized):
3061
+ return self._is_text_expr(expr.inner)
3062
+ if isinstance(expr, (ast.CastExpr, ast.UnsafeCastExpr)):
3063
+ return (expr.target_type is not None
3064
+ and expr.target_type.name in self._TEXT_TYPES)
3065
+ if isinstance(expr, ast.BinaryOp) and expr.op == "+":
3066
+ return (self._is_text_expr(expr.left)
3067
+ and self._is_text_expr(expr.right))
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
3073
+ return self._resolved_return_type(expr.callee) in self._TEXT_TYPES
3074
+ return False
3075
+
3024
3076
  def _is_synthetic_axis_expr(self, expr: ast.Expr) -> bool:
3025
3077
  """True iff expr is provably a synthetic-axis-encoded value at
3026
3078
  compile time — int, float, complex, scalar, char, or string.
@@ -3380,6 +3432,21 @@ class BaseCodegen:
3380
3432
  )
3381
3433
  if user_op_name is not None:
3382
3434
  return f"{user_op_name}({left}, {right})"
3435
+ # Text concatenation: `+` with BOTH operands provably text
3436
+ # dispatches to the substrate string_concat (round-17 audit
3437
+ # fix). Without this, lowercase-`string`-typed operands fell
3438
+ # through to ELEMENT-WISE VECTOR ADD — 'ab' + 'cd' silently
3439
+ # summed codepoint axes into garbage — and two string
3440
+ # literals crashed at runtime (host strs reached
3441
+ # string_concat). The String CLASS `operator +` still wins
3442
+ # above for class-typed operands. Literals re-translate with
3443
+ # dest_type="string" so they cross as substrate Strings.
3444
+ if (expr.op == "+" and self.supports_string_runtime
3445
+ and self._is_text_expr(expr.left)
3446
+ and self._is_text_expr(expr.right)):
3447
+ lt = self._translate_expr(expr.left, dest_type="string")
3448
+ rt = self._translate_expr(expr.right, dest_type="string")
3449
+ return f"_VSA.string_concat({lt}, {rt})"
3383
3450
  # Logical operators dispatch through the substrate so they
3384
3451
  # work uniformly on bool / fuzzy / trit / truth-axis-vector
3385
3452
  # inputs. Zadeh fuzzy logic — min for AND, max for OR — on
@@ -3589,10 +3656,214 @@ class BaseCodegen:
3589
3656
  return self._embed_expr_src(expr)
3590
3657
  if isinstance(expr, ast.DefuzzyExpr):
3591
3658
  return self._defuzzy_expr_src(expr)
3659
+ if isinstance(expr, ast.InterpolatedString):
3660
+ # `$"hello {s}!"` desugars to a make_string / string_concat
3661
+ # chain — every piece a substrate String, every join a
3662
+ # substrate op (strings.md). Only STRING-TYPED interpolants
3663
+ # are lowerable today: a number/fuzzy interpolant needs the
3664
+ # substrate number→string formatter, which is not built, so
3665
+ # those reject with a steer (same wall as the text casts).
3666
+ if not self.supports_string_runtime:
3667
+ raise CodegenNotSupported(
3668
+ expr,
3669
+ "interpolated strings need the substrate String "
3670
+ "runtime; use the PyTorch backend (codegen_pytorch)."
3671
+ )
3672
+ pieces: list[str] = []
3673
+ for part in expr.parts:
3674
+ if isinstance(part, str):
3675
+ if part:
3676
+ pieces.append(f"_VSA.make_string({part!r})")
3677
+ continue
3678
+ part_t = self._infer_cast_operand_type(part)
3679
+ if self._CAST_FAMILY.get(part_t) == "text":
3680
+ pieces.append(
3681
+ self._translate_expr(part, dest_type="string"))
3682
+ continue
3683
+ 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.
3688
+ pieces.append(
3689
+ f"_VSA.int_to_string("
3690
+ f"{self._translate_expr(part)})")
3691
+ continue
3692
+ if part_t is None:
3693
+ raise CodegenNotSupported(
3694
+ part,
3695
+ "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."
3699
+ )
3700
+ raise CodegenNotSupported(
3701
+ 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."
3707
+ )
3708
+ if not pieces:
3709
+ return "_VSA.make_string('')"
3710
+ src = pieces[0]
3711
+ for nxt in pieces[1:]:
3712
+ src = f"_VSA.string_concat({src}, {nxt})"
3713
+ return src
3714
+ if isinstance(expr, ast.UnsafeCastExpr):
3715
+ # types.md § "Casting — relabeling, not transformation":
3716
+ # unsafeCast<T>(x) is ALWAYS the pure relabel — the value
3717
+ # crosses unchanged and only the static type changes. It
3718
+ # never axis-moves; `(fuzzy) n` converts, `unsafeCast<fuzzy>(n)`
3719
+ # reinterprets (truth reading of a real-axis value is 0).
3720
+ return self._translate_expr(expr.expr)
3721
+ if isinstance(expr, ast.UnsafeOverrideExpr):
3722
+ # `unsafeOverride(v)` suppresses a type-check at the call
3723
+ # site and does not change the value — the runtime lowering
3724
+ # is the pure passthrough (same class as unsafeCast, minus
3725
+ # the type argument). Round-19 audit: was the last special
3726
+ # call form with no codegen.
3727
+ return self._translate_expr(expr.expr)
3728
+ if isinstance(expr, ast.CastExpr):
3729
+ return self._translate_cast(expr)
3592
3730
  raise CodegenNotSupported(
3593
3731
  expr, f"unsupported expression: {type(expr).__name__}"
3594
3732
  )
3595
3733
 
3734
+ # ---- cast lowering (types.md § "Casting — relabeling, not
3735
+ # transformation") -------------------------------------------------
3736
+ #
3737
+ # The default cast is a NO-OP relabel: every Sutra value is already a
3738
+ # d-dim substrate vector (or a 0-d number at the entry boundary), and
3739
+ # downstream ops project the axes they need, so relabeling is free.
3740
+ # The ONE genuine axis-move pair is numeric↔truth: a number carries
3741
+ # its value on AXIS_REAL, a fuzzy/bool/trit on AXIS_TRUTH — a pure
3742
+ # relabel would strand the value on the wrong axis and every read
3743
+ # would see 0 (neutral). Text is walled off: number→string needs the
3744
+ # unbuilt substrate formatter; string→vector is the spec's "embedding
3745
+ # cast" whose real implementation is `embed()` at the entry boundary.
3746
+
3747
+ #: Sutra type name → cast family. Types not in the map (user classes,
3748
+ #: generics) relabel as "geometric" — the spec's free no-op default.
3749
+ _CAST_FAMILY = {
3750
+ "int": "number", "number": "number",
3751
+ "fuzzy": "truth", "bool": "truth", "trit": "truth",
3752
+ "complex": "geometric", "vector": "geometric", "role": "geometric",
3753
+ "matrix": "geometric",
3754
+ "string": "text", "String": "text",
3755
+ "char": "text", "Character": "text",
3756
+ }
3757
+
3758
+ #: Whether this backend's runtime carries the numeric↔truth cast
3759
+ #: helpers (cast_number_to_truth / cast_truth_to_number / _cnum).
3760
+ #: The deprecated numpy backend does not; it keeps rejecting casts.
3761
+ supports_cast_lowering = False
3762
+
3763
+ #: Whether this backend's runtime carries the substrate String ops
3764
+ #: (make_string / string_concat). Gates InterpolatedString lowering;
3765
+ #: the deprecated numpy backend has no String runtime.
3766
+ supports_string_runtime = False
3767
+
3768
+ def _infer_cast_operand_type(self, expr: ast.Expr) -> str | None:
3769
+ """Conservative static type of a cast operand: literals, typed
3770
+ locals/params (via _var_type), nested casts, parenthesized
3771
+ exprs. None when unknown — the caller decides whether the
3772
+ target makes the relabel/axis-move distinction load-bearing."""
3773
+ from .symbol_table import _LITERAL_TYPES
3774
+ kind = type(expr).__name__
3775
+ if kind in _LITERAL_TYPES:
3776
+ return _LITERAL_TYPES[kind]
3777
+ if isinstance(expr, ast.Identifier):
3778
+ return self._var_type.get(expr.name)
3779
+ if isinstance(expr, (ast.CastExpr, ast.UnsafeCastExpr)):
3780
+ return expr.target_type.name if expr.target_type else None
3781
+ if type(expr).__name__ == "Parenthesized" and hasattr(expr, "inner"):
3782
+ return self._infer_cast_operand_type(expr.inner)
3783
+ if isinstance(expr, ast.BinaryOp):
3784
+ if expr.op in ("==", "!=", "<", ">", "<=", ">=", "&&", "||"):
3785
+ return "bool"
3786
+ if expr.op in ("+", "-", "*", "/", "%"):
3787
+ lt = self._infer_cast_operand_type(expr.left)
3788
+ rt = self._infer_cast_operand_type(expr.right)
3789
+ if (self._CAST_FAMILY.get(lt) == "number"
3790
+ and self._CAST_FAMILY.get(rt) == "number"):
3791
+ return "number"
3792
+ return None
3793
+
3794
+ def _translate_cast(self, expr: ast.CastExpr) -> str:
3795
+ if not self.supports_cast_lowering:
3796
+ raise CodegenNotSupported(
3797
+ expr,
3798
+ "`(Type) expr` casts are not supported by this backend; "
3799
+ "use the PyTorch backend (codegen_pytorch)."
3800
+ )
3801
+ dst = expr.target_type.name if expr.target_type else None
3802
+ dst_family = self._CAST_FAMILY.get(dst, "geometric")
3803
+ src = self._infer_cast_operand_type(expr.expr)
3804
+ src_family = self._CAST_FAMILY.get(src, "geometric") if src else None
3805
+
3806
+ # --- text walls -------------------------------------------------
3807
+ if dst_family == "text" and src_family != "text":
3808
+ 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.
3812
+ return (f"_VSA.int_to_string("
3813
+ f"{self._translate_expr(expr.expr)})")
3814
+ raise CodegenNotSupported(
3815
+ 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`."
3820
+ )
3821
+ if src_family == "text" and dst_family != "text":
3822
+ if dst_family == "geometric":
3823
+ raise CodegenNotSupported(
3824
+ expr,
3825
+ f"cannot cast a string to `{dst}`: that is the embedding "
3826
+ "cast, and its implementation is `embed(...)` at the "
3827
+ "program boundary — a String vector encodes codepoints, "
3828
+ "not semantics."
3829
+ )
3830
+ raise CodegenNotSupported(
3831
+ expr,
3832
+ f"cannot cast a string to `{dst}`: a String vector encodes "
3833
+ "packed codepoints, so its numeric axes are not a number. "
3834
+ "Use the string operations (string_length, char_at, ...) "
3835
+ "instead."
3836
+ )
3837
+
3838
+ inner = self._translate_expr(expr.expr)
3839
+
3840
+ # --- same family / text→text: the free relabel --------------------
3841
+ if src_family == dst_family:
3842
+ return inner
3843
+
3844
+ # --- the one genuine axis-move pair -------------------------------
3845
+ if src_family == "number" and dst_family == "truth":
3846
+ return f"_VSA.cast_number_to_truth({inner})"
3847
+ if src_family == "truth" and dst_family == "number":
3848
+ return f"_VSA.cast_truth_to_number({inner})"
3849
+
3850
+ # --- unknown source: only reject when the move/relabel choice
3851
+ # --- is load-bearing (truth/number targets read a single axis) ----
3852
+ if src_family is None and dst_family in ("truth", "number"):
3853
+ raise CodegenNotSupported(
3854
+ expr,
3855
+ f"cannot cast to `{dst}` here: the operand's static type "
3856
+ "can't be inferred, and truth/number casts need it to pick "
3857
+ "between relabel and axis-move. Assign the operand to a "
3858
+ "typed variable first."
3859
+ )
3860
+
3861
+ # --- every remaining pair relabels; _cnum canonicalizes a 0-d
3862
+ # --- number at the entry boundary and passes vectors through ------
3863
+ if src_family == "number" or src is None:
3864
+ return f"_VSA._cnum({inner})"
3865
+ return inner
3866
+
3596
3867
  def _embed_expr_src(self, expr: ast.EmbedExpr) -> str:
3597
3868
  """Override point for per-backend `embed(<expr>)` lowering.
3598
3869