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.
- {sutra_dev-0.9.3/sutra_dev.egg-info → sutra_dev-0.10.0}/PKG-INFO +1 -1
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/pyproject.toml +1 -1
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/__init__.py +1 -1
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/__main__.py +13 -2
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/ast_nodes.py +38 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/codegen.py +45 -21
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/codegen_base.py +282 -29
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/codegen_pytorch.py +157 -33
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/inliner.py +38 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/parser.py +134 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/repl.py +49 -1
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/stdlib/logic.su +12 -8
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/symbol_table.py +3 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/validator.py +72 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0/sutra_dev.egg-info}/PKG-INFO +1 -1
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_dev.egg-info/SOURCES.txt +5 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_cast_codegen.py +21 -8
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_interp_string_codegen.py +20 -7
- sutra_dev-0.10.0/tests/test_le_ge_ties.py +59 -0
- sutra_dev-0.10.0/tests/test_loop_call_expr.py +643 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_loop_function_decl.py +13 -2
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_no_host_readout.py +3 -1
- sutra_dev-0.10.0/tests/test_num_eq_indicator.py +96 -0
- sutra_dev-0.10.0/tests/test_num_to_string.py +76 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_repl.py +45 -0
- sutra_dev-0.10.0/tests/test_slot_state_diagnostic.py +119 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_string_plus_concat.py +18 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_terminal_string_decode.py +18 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_torch_compile_wrap.py +5 -1
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/LICENSE +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/README.md +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/setup.cfg +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/axon_keys.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/cached_compile.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/codegen_thrml.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/diagnostics.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/embedding.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/fv.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/fv_key_soundness.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/fv_loop_convergence.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/fv_obligation_checker.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/fv_poly_bound.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/fv_sampler_convergence.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/lexer.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/loop_capture.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/loop_desugar.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/multi_process.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/preeval.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/promise_desugar.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/review.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/simplify.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/simplify_egglog.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/stdlib/axons.su +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/stdlib/bigint.su +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/stdlib/bitwise.su +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/stdlib/embed.su +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/stdlib/javascript_object.su +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/stdlib/javascript_primitives.su +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/stdlib/math.su +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/stdlib/memory.su +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/stdlib/modulus.su +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/stdlib/numbers.su +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/stdlib/promises.su +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/stdlib/rotation.su +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/stdlib/similarity.su +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/stdlib/strings.su +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/stdlib/tensor.su +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/stdlib/vectors.su +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/stdlib_loader.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/sutradb_embedded.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/tabulate.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/trace.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_compiler/workspace.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_dev.egg-info/dependency_links.txt +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_dev.egg-info/entry_points.txt +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_dev.egg-info/requires.txt +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/sutra_dev.egg-info/top_level.txt +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_arity_diagnostic.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_arrow_functions.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_await_midfunction.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_await_substrate_pure.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_axon_build.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_axon_device_coherence.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_axon_item_call_result.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_axon_keys.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_axon_project.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_branchless_loop.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_buffer_transcendentals.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_cached_compile.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_class_fields_runtime.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_codegen.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_codegen_pytorch.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_codegen_thrml.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_corpus.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_dimension_audit.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_dimension_audit_warning.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_embedding_provider.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_equality_autograd.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_fused_nn.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_fv_general_checker.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_fv_key_soundness.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_fv_kleene_grid_exactness.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_fv_loop_convergence.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_fv_poly_obligation_checker.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_fv_public_api.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_fv_sampler_convergence.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_fv_termination.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_fv_worked_example.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_higher_order_functions.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_implicit_loop_desugar.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_inliner.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_int_dict.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_int_to_string.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_jvm_core.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_lexer.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_list_ops.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_load_matrix.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_loop_capture.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_make_string_idempotent.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_matrix_literal.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_measurement_claim_sweep.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_mini_wasm_machine.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_multi_process_runtime.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_native_recursion.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_ntm_ram.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_optional_llm_model.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_parser.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_preeval.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_process_pool_runtime.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_python_builtin_escape.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_ram_direct_tensor.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_rotation_prewarm.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_run_error_diagnostics.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_simplify.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_simplify_egglog.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_simplify_egglog_cse.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_snap_diagnostic.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_stdlib_loader.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_string_equality.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_substrate_leak_sweep.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_sutradb_embedded.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_symbol_table.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_tabulate.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_trainable_matrix.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_transcendentals.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_type_inference.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_type_test_gap.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_unknown_function_diagnostic.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_unknown_type_diagnostic.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_unknown_variable_diagnostic.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_unsafe_override_codegen.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_vector_literal.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_wasm_core.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_workspace.py +0 -0
- {sutra_dev-0.9.3 → sutra_dev-0.10.0}/tests/test_wrong_arg_type_diagnostic.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sutra-dev"
|
|
7
|
-
version = "0.
|
|
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.
|
|
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
|
-
|
|
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="
|
|
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
|
|
1142
|
+
self._emit("def slot_state_new(self):")
|
|
1131
1143
|
self._indent += 1
|
|
1132
|
-
self._emit('"""
|
|
1133
|
-
self._emit('
|
|
1134
|
-
self._emit("
|
|
1135
|
-
self._emit("
|
|
1136
|
-
self._emit("
|
|
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("
|
|
1141
|
-
self.
|
|
1142
|
-
self._emit(
|
|
1143
|
-
self._emit("
|
|
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
|
|
1150
|
-
self._emit('')
|
|
1151
|
-
self._emit("
|
|
1152
|
-
self._emit("
|
|
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.
|
|
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.
|
|
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.
|
|
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})
|
|
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
|
-
|
|
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"{
|
|
2443
|
+
self._emit(f"{tmp} = _init_{state_names[idx]}")
|
|
2255
2444
|
else:
|
|
2256
|
-
|
|
2257
|
-
|
|
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
|
|
2640
|
-
"
|
|
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:
|
|
3657
|
-
#
|
|
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
|
-
"
|
|
3669
|
-
"to a
|
|
3670
|
-
"
|
|
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:
|
|
3675
|
-
"
|
|
3676
|
-
"
|
|
3677
|
-
"
|
|
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:
|
|
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}`:
|
|
3789
|
-
"
|
|
3790
|
-
"
|
|
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":
|