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.
- {sutra_dev-0.9.4/sutra_dev.egg-info → sutra_dev-0.10.0}/PKG-INFO +1 -1
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/pyproject.toml +1 -1
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/__init__.py +1 -1
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/__main__.py +13 -2
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/ast_nodes.py +38 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/codegen.py +33 -21
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/codegen_base.py +252 -27
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/codegen_pytorch.py +117 -29
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/inliner.py +38 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/parser.py +134 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/repl.py +4 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/symbol_table.py +3 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/validator.py +72 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0/sutra_dev.egg-info}/PKG-INFO +1 -1
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_dev.egg-info/SOURCES.txt +3 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_cast_codegen.py +21 -8
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_interp_string_codegen.py +20 -7
- sutra_dev-0.10.0/tests/test_loop_call_expr.py +643 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_loop_function_decl.py +13 -2
- sutra_dev-0.10.0/tests/test_num_to_string.py +76 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_repl.py +29 -0
- sutra_dev-0.10.0/tests/test_slot_state_diagnostic.py +119 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_terminal_string_decode.py +18 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_torch_compile_wrap.py +5 -1
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/LICENSE +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/README.md +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/setup.cfg +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/axon_keys.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/cached_compile.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/codegen_thrml.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/diagnostics.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/embedding.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/fv.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/fv_key_soundness.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/fv_loop_convergence.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/fv_obligation_checker.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/fv_poly_bound.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/fv_sampler_convergence.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/lexer.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/loop_capture.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/loop_desugar.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/multi_process.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/preeval.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/promise_desugar.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/review.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/simplify.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/simplify_egglog.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/stdlib/axons.su +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/stdlib/bigint.su +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/stdlib/bitwise.su +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/stdlib/embed.su +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/stdlib/javascript_object.su +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/stdlib/javascript_primitives.su +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/stdlib/logic.su +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/stdlib/math.su +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/stdlib/memory.su +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/stdlib/modulus.su +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/stdlib/numbers.su +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/stdlib/promises.su +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/stdlib/rotation.su +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/stdlib/similarity.su +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/stdlib/strings.su +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/stdlib/tensor.su +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/stdlib/vectors.su +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/stdlib_loader.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/sutradb_embedded.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/tabulate.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/trace.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_compiler/workspace.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_dev.egg-info/dependency_links.txt +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_dev.egg-info/entry_points.txt +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_dev.egg-info/requires.txt +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/sutra_dev.egg-info/top_level.txt +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_arity_diagnostic.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_arrow_functions.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_await_midfunction.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_await_substrate_pure.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_axon_build.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_axon_device_coherence.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_axon_item_call_result.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_axon_keys.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_axon_project.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_branchless_loop.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_buffer_transcendentals.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_cached_compile.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_class_fields_runtime.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_codegen.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_codegen_pytorch.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_codegen_thrml.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_corpus.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_dimension_audit.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_dimension_audit_warning.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_embedding_provider.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_equality_autograd.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_fused_nn.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_fv_general_checker.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_fv_key_soundness.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_fv_kleene_grid_exactness.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_fv_loop_convergence.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_fv_poly_obligation_checker.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_fv_public_api.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_fv_sampler_convergence.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_fv_termination.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_fv_worked_example.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_higher_order_functions.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_implicit_loop_desugar.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_inliner.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_int_dict.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_int_to_string.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_jvm_core.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_le_ge_ties.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_lexer.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_list_ops.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_load_matrix.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_loop_capture.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_make_string_idempotent.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_matrix_literal.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_measurement_claim_sweep.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_mini_wasm_machine.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_multi_process_runtime.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_native_recursion.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_no_host_readout.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_ntm_ram.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_num_eq_indicator.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_optional_llm_model.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_parser.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_preeval.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_process_pool_runtime.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_python_builtin_escape.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_ram_direct_tensor.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_rotation_prewarm.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_run_error_diagnostics.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_simplify.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_simplify_egglog.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_simplify_egglog_cse.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_snap_diagnostic.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_stdlib_loader.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_string_equality.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_string_plus_concat.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_substrate_leak_sweep.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_sutradb_embedded.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_symbol_table.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_tabulate.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_trainable_matrix.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_transcendentals.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_type_inference.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_type_test_gap.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_unknown_function_diagnostic.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_unknown_type_diagnostic.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_unknown_variable_diagnostic.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_unsafe_override_codegen.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_vector_literal.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_wasm_core.py +0 -0
- {sutra_dev-0.9.4 → sutra_dev-0.10.0}/tests/test_workspace.py +0 -0
- {sutra_dev-0.9.4 → 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
|
# ============================================================
|
|
@@ -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
|
|
1142
|
+
self._emit("def slot_state_new(self):")
|
|
1143
1143
|
self._indent += 1
|
|
1144
|
-
self._emit('"""
|
|
1145
|
-
self._emit('
|
|
1146
|
-
self._emit("
|
|
1147
|
-
self._emit("
|
|
1148
|
-
self._emit("
|
|
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("
|
|
1153
|
-
self.
|
|
1154
|
-
self._emit(
|
|
1155
|
-
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")
|
|
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
|
|
1162
|
-
self._emit('')
|
|
1163
|
-
self._emit("
|
|
1164
|
-
self._emit("
|
|
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.
|
|
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.
|
|
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.
|
|
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})
|
|
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
|
-
|
|
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"{
|
|
2443
|
+
self._emit(f"{tmp} = _init_{state_names[idx]}")
|
|
2268
2444
|
else:
|
|
2269
|
-
|
|
2270
|
-
|
|
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:
|
|
3685
|
-
#
|
|
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
|
-
"
|
|
3697
|
-
"to a
|
|
3698
|
-
"
|
|
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:
|
|
3703
|
-
"
|
|
3704
|
-
"
|
|
3705
|
-
"
|
|
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:
|
|
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}`:
|
|
3817
|
-
"
|
|
3818
|
-
"
|
|
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":
|