Repository navigation
Refactor container classification and zero-arg callable detection - #324
Conversation
…ds and zero-arg callables Move is_zero_arg_callable_ref to shared 05_emit.dag so all backends can detect FunctionValueBinding + zero-arg Callable and append (). Go/Python emit_expr_var previously ignored binding_kind entirely, emitting bare identifiers for none/true/false and zero-arg function references. Now maps keywords via emit_keyword and appends () for zero-arg callable refs, matching Rust emitter behavior. https://claude.ai/code/session_01HcSSdXfjWoKWiPkquYnCCH
…rity authority Delete coercion_keyed_container_names and coercion_element_container_names data constants — defined but never consumed by any code path. Replace with is_keyed_container/is_element_container functions that derive classification from container_expected_arity in std/types.dag (the existing authority for container identity). Keyed = arity 2, element = arity 1. The container_to_algebra string table remains (TODO: derive from type declarations once M4/Lane 1 provides structural identity). https://claude.ai/code/session_01HcSSdXfjWoKWiPkquYnCCH
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1e4a5b8424
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| ExprVar { binding_kind: binding_kind } => | ||
| let n = expr_var_name_at(texpr: expr, source_index: source_index) | ||
| concat(prefix, emit_ident(name: n, target: Go)) | ||
| if n == "none" || n == "None" { concat(prefix, emit_keyword(key: "null", target: Go)) } |
There was a problem hiding this comment.
Remove uppercase
None from null-literal check
None is not a reserved DAG keyword (dag_keyword_set only marks lowercase none/null), so a user can legally bind an identifier named None and rely on emit_ident to escape it for the target language. This new branch rewrites ExprVar named None to the null literal, so references to that binding are emitted as constants (nil in Go, and similarly in emit_py_expr_var) instead of the variable value, which changes program semantics.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Good catch — fixed in 19af457. Removed the uppercase None check from both Go and Python emit_expr_var. Only lowercase "none" (the actual DAG keyword) is now mapped to the null literal.
Note: the Rust emitter has the same pre-existing name == "None" check at emit_var_ref (05_emit_rust.dag:1714). That's a known L1 violation to be resolved upstream via resolver normalization, not in this PR.
Generated by Claude Code
Add connective: Connective field to TypeRendering, populated from Node.connective during build_type_rendering. Conj types carry Conj, Disj types carry Disj, leaves/callable/optional carry NoConnective. This surfaces the existing structural authority (connective) to all TypeRendering consumers, eliminating the need to re-derive product vs coproduct identity from string-based type_name checks. https://claude.ai/code/session_01HcSSdXfjWoKWiPkquYnCCH
Per review: None (uppercase) is not a reserved DAG keyword, so a user could legally bind an identifier named None. Only check lowercase "none" in Go/Python emit_expr_var. The Rust emitter still has the same issue at emit_var_ref:1714 — that's a pre-existing L1 violation to be resolved upstream (resolver normalization, not emitter). https://claude.ai/code/session_01HcSSdXfjWoKWiPkquYnCCH
briansrls
left a comment
There was a problem hiding this comment.
Review (INVARIANTS: 2, MODELING: 0+/0-, ROADMAP: 0✓/1!)
INVARIANTS — Violations (2)
ROADMAP — Incomplete
- CG-2 TLC-1: The PR extends the emitter-side nullary-call guardrail to Go and Python, but the item remains partial because nullary invocation is still decided in emit instead of being normalized to ExprCall upstream.
ROADMAP — Unclaimed
- MM-3 nullary invocation / Go-Python nullary refs: This PR advances the roadmap subtask that Go and Python emit should handle nullary function references, but ROADMAP.md was not updated to record that partial progress.
The PR improves cross-backend coverage, but it still deepens an acknowledged emitter-side nullary-call workaround and introduces a coercion helper whose classification is less faithful than the data it replaces.
| // DAG references to zero-arg functions are calls; all backends need explicit parens. | ||
| // Checks FunctionValueBinding + resolved Callable type has no params. | ||
| fn is_zero_arg_callable_ref(binding_kind: VarBindingKind?, resolved_type: InferredNode?) -> Bool { | ||
| match binding_kind { |
There was a problem hiding this comment.
Invariant violation: is_zero_arg_callable_ref still makes an emit-time nullary-call decision by testing rt.name == "Callable", which uses a name proxy for missing upstream call structure and violates Boundary sufficiency and Emission is translation, not decision-making.
There was a problem hiding this comment.
Acknowledged — this is a pre-existing L1 violation (was in 05_emit_rust.dag, moved to shared 05_emit.dag). Added documentation comment on the function marking the boundary sufficiency violation and noting the dissolution path (M4/Tier 2.6: inference normalizes ExprVar→ExprCall for nullary invocation). ROADMAP TLC-1 updated to record the Go/Python progress and the remaining upstream work. Fixed in 6bdb2de.
Generated by Claude Code
| // Container identity constants (hardcoded; should be derived from algebra arity) | ||
| // ========================================================================= | ||
| // Container kind classification: derived from container_type_arity in std/types.dag. | ||
| // Keyed containers (arity 2) vs element containers (arity 1). |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
There was a problem hiding this comment.
Fixed in 6bdb2de. Added coercion_container_arity that resolves both surface names (via container_expected_arity) AND algebra names (via container_to_algebra → InhabitantDecl.arity). is_keyed_container("PartialFunction") and is_element_container("FreeMonoid") now return true.
Generated by Claude Code
…, ROADMAP update 1. coercion_container_arity resolves both surface names (List, Map via container_expected_arity) AND algebra names (FreeMonoid, PartialFunction via container_to_algebra → InhabitantDecl.arity). is_keyed_container and is_element_container now cover the full naming domain. 2. Document L1 violation on is_zero_arg_callable_ref (boundary sufficiency: name-proxy for missing upstream call structure, dissolves with Tier 2.6). 3. Update ROADMAP TLC-1 to record Go/Python emit_expr_var progress (PR #324). https://claude.ai/code/session_01HcSSdXfjWoKWiPkquYnCCH
ChatGPT ReviewI reviewed the uploaded diff directly; the GitHub PR page itself wasn’t accessible from here. My take: good direction on container classification, but I’d leave one blocking review comment and two non-blocking modeling concerns. 1) Blocking: shared zero-arg callable detection is still an emitter-side heuristicIn The code comment is already honest that this is an L1 violation. I agree with that assessment. Why I’d block on it:
Behaviorally, this also bakes in one side of an unresolved ambiguity: a zero-arg function value reference now becomes So I’d comment something like:
2) Non-blocking but important:
|
briansrls
left a comment
There was a problem hiding this comment.
Review (INVARIANTS: 2, MODELING: 1+/1-, ROADMAP: 1✓/1!)
INVARIANTS — Violations (2)
MODELING — Strengths
src/v2/04_emit_info.dagReusing the existingConnectiveprimitive onTypeRenderingis directionally compositional because it models product/coproduct form with an existing structural authority instead of inventing a renderer-only enum.
MODELING — Improvements
src/v2/04_emit_info.dagTypeRendering.connectiveshould either replace an existing render-time structural decision now or stay out of the boundary until a consumer exists, otherwise it is duplicate semantic state rather than a faithful authority.
ROADMAP — Verified
- CG/TLC-1 partial: The diff does move
is_zero_arg_callable_refinto shared05_emit.dagand wires Go/Pythonemit_*_expr_varthrough it, which supports the new roadmap claim.
ROADMAP — Incomplete
- CG/TLC-1: The work is still only partial because
is_zero_arg_callable_refcontinues to infer call syntax fromrt.name == "Callable"in emit instead of from upstream IR call structure.
The PR makes the intended partial TLC-1 progress and fixes the earlier container-domain regression, but it still leaves the known emit-time Callable-name proxy in place and introduces one unused boundary fact plus one Rust-specific authority for shared container arity.
| // to algebraic relationships that the coercion engine will formalize. | ||
| type TypeRendering { | ||
| type_name: String // identity: "Int", "list", "Token", "map" | ||
| connective: Connective // structural form: Conj (product), Disj (coproduct), NoConnective (leaf/callable/optional) |
There was a problem hiding this comment.
Invariant violation: Adding connective to TypeRendering without any downstream consumer makes the new boundary fact speculative duplicate state, violating Explicit boundary contracts' rule that new semantic boundaries must land end-to-end.
There was a problem hiding this comment.
Agreed — reverted in 1365e88. Removed TypeRendering.connective entirely. Will re-add with an end-to-end consumer when a concrete render-time decision reads it.
Generated by Claude Code
| fn coercion_container_arity(name: String) -> Int? { | ||
| match container_expected_arity(name: name) { | ||
| Some { value: arity } => Some { value: arity } | ||
| None => |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
There was a problem hiding this comment.
Fixed in 1365e88. Replaced lookup_inhabitant(target: Rust, ...) with a target-independent algebra_arity data table. Arity is an algebra property — the Rust-specific dispatch was wrong. The new table is the cross-language authority until std/algebra.dag carries arity natively.
Generated by Claude Code
…ic arity 1. Remove TypeRendering.connective — no downstream consumer exists yet. Per Explicit boundary contracts: new semantic boundaries must land end-to-end. Will re-add when a concrete consumer is wired. 2. Replace lookup_inhabitant(target: Rust, ...) with target-independent algebra_arity data table. Arity is an algebra property, not a target property. Source of truth should be std/algebra.dag once it carries arity; until then this table is the cross-language authority. https://claude.ai/code/session_01HcSSdXfjWoKWiPkquYnCCH
Co-locate container→algebra mapping and algebra arity tables with the type declarations they mirror (type List<T> = FreeMonoid<T>, etc.). Single file, single authority — coercion.dag now imports from std.types instead of maintaining its own copies. container_to_algebra_name() replaces dag_container_to_algebra() as the lookup function. algebra_arity is target-independent (arity is an algebra property, not a target property). Both tables carry derivation TODOs for M4 structural identity. https://claude.ai/code/session_01HcSSdXfjWoKWiPkquYnCCH
briansrls
left a comment
There was a problem hiding this comment.
Review (INVARIANTS: 1, MODELING: 1+/0-, ROADMAP: 1✓/1!)
INVARIANTS — Violations (1)
MODELING — Strengths
src/v2/coercion.dagThe newcoercion_container_aritycomposes surface container names with algebra names, which is more faithful than the prior surface-name-only model.
ROADMAP — Verified
- TLC-1: Call syntax / reference distinction: The diff moves
is_zero_arg_callable_refinto shared05_emit.dagand wires both Go and Pythonemit_*_expr_varpaths to consult it, matching the roadmap note.
ROADMAP — Incomplete
- TLC-1: Call syntax / reference distinction: The item remains partial because the code still relies on emit-time callable-name/arity detection and adds no coverage for imported zero-arg function refs, exactly as the roadmap caveat says.
The PR resolves the two earlier coercion-authority issues and accurately records the TLC-1 refactor, but it replaces them with a new local algebra-arity table that already diverges from the canonical algebra definitions.
| // Source of truth should be std/algebra.dag once it carries arity; until | ||
| // then this table is the cross-language authority. | ||
| data algebra_arity: Map<String, Int> = { | ||
| "FreeMonoid": 1, |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
There was a problem hiding this comment.
Agreed — deleted entirely in 69a4cf5. algebra_arity was wrong (OrderedRing is arity 1, not 0) and duplicative of information already in algebra type declarations. Also removed the dead coercion_container_arity, is_keyed_container, and is_element_container functions — they had no consumers (replaced dead data with dead functions).
Generated by Claude Code
…ctions algebra_arity had incorrect values (OrderedRing listed as arity 0 but std/algebra.dag declares OrderedRing<T>) and was a duplicate authority for information already in algebra type declarations. Delete it. coercion_container_arity, is_keyed_container, is_element_container were introduced to replace dead data constants but have no consumers — they replaced dead code with dead code. Delete them. container_to_algebra + container_to_algebra_name remain in std/types.dag (correctly used by coerce_container_template). https://claude.ai/code/session_01HcSSdXfjWoKWiPkquYnCCH
…tural fields directly Delete TypedItemKind enum (8 variants including TypedItemUnhandled) and classify_typed_item shared classifier. Each backend now dispatches on Node structural fields directly: connective, body, transport, params, children, type_annotation, properties. CM endgame: consumers pattern-match on the facts they need (body != none, connective != NoConnective) rather than on a derived taxonomy. Secondary connective dispatch after classification is eliminated — backends check connective directly in the type_def branch. Filter/any queries (has_structs, has_enums, has_types, has_functions, local_type_names, local_enum_uses) replaced with inline structural checks. Net: -105 lines, 0 new types, 0 new functions. https://claude.ai/code/session_01HcSSdXfjWoKWiPkquYnCCH
briansrls
left a comment
There was a problem hiding this comment.
Review (INVARIANTS: 2, MODELING: 1+/1-, ROADMAP: 1✓/0!)
INVARIANTS — Violations (2)
MODELING — Strengths
dsl/std/types.dagMoving container-to-algebra knowledge next to the container aliases is directionally better than leaving it buried inv2.compiler.coercion.
MODELING — Improvements
dsl/std/types.dagThe mapping should be derived from the alias/algebra declarations themselves, not maintained as a string table of surface-name variants and algebra self-names.
ROADMAP — Verified
- TLC-1 partial: The code does move
is_zero_arg_callable_refinto shared05_emit.dag, and both Go and Pythonemit_*_expr_varnow call it.
The PR fixes the earlier coercion-arity regressions and shares nullary-call handling across backends, but it still introduces a duplicate string-keyed container authority and reintroduces backend-parallel item classification logic.
| // so that any naming convention resolves to the canonical algebra identity. | ||
| // TODO: derive programmatically from type alias declarations once M4 | ||
| // structural identity provides a reflection mechanism. | ||
| data container_to_algebra: Map<String, String> = { |
There was a problem hiding this comment.
Invariant violation: container_to_algebra re-states the List/Set/Map to algebra facts as a hand-maintained Map<String, String>, creating a duplicate string-keyed authority instead of deriving from the alias declarations, which violates No duplicate representations and the Modeling Faithfulness invariant.
There was a problem hiding this comment.
Acknowledged — this is a pre-existing duplicate (was in coercion.dag, moved to std/types.dag for co-location). The table already has a TODO: "derive programmatically from type alias declarations once M4 structural identity provides a reflection mechanism." The .dag language currently has no reflection/introspection capability to read type alias bodies at the data-declaration level.
Generated by Claude Code
| let kind = classify_typed_item(item: item) | ||
| if (kind == TypedItemTypeDef) { | ||
| // Structural dispatch on Node fields directly (CM: no shared taxonomy). | ||
| let item_has_structure = item.connective != NoConnective |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
There was a problem hiding this comment.
Fixed in a3e0936. Extracted shared boolean predicates (is_type_def_item, is_function_item, etc.) in 05_emit.dag. All 3 backends now call the shared predicates — no parallel structural logic. The predicates are composable queries on Node fields, not a taxonomy enum.
Generated by Claude Code
All three backends (Rust/Go/Python) dispatch per collection type (string/map/list) via IndexingSemantics templates. Status paragraph and checklist updated to reflect this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… inline checks Per review: inline structural checks duplicated across 3 backends violate No parallel implementations. Extract shared boolean predicates (is_type_def_item, is_function_item, etc.) in 05_emit.dag. Each backend calls the shared predicates — no taxonomy enum, no parallel structural logic. Predicates are composable boolean queries on Node fields. Backends compose them with additional per-backend facts (e.g., Rust checks connective == Disj for enum use-imports; Python checks connective for dataclass/enum imports). https://claude.ai/code/session_01HcSSdXfjWoKWiPkquYnCCH
Widening an L1 violation (rt.name == "Callable" name-proxy) across all backends without upstream modeling violates boundary sufficiency. Go/Python emit_expr_var keep keyword mapping (none/true/false) but no longer consume is_zero_arg_callable_ref. Zero-arg callable detection stays Rust-scoped as a temporary guardrail until Tier 2.6 models call-vs-reference upstream. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
briansrls
left a comment
There was a problem hiding this comment.
Review (INVARIANTS: 0, MODELING: 1+/1-, ROADMAP: 1✓/1!)
MODELING — Strengths
dsl/std/types.dagMoving the container-to-algebra relation up intostd/types.dagis better layering than keeping that fact insrc/v2/coercion.dag, because the relation is language-level rather than compiler-local.
MODELING — Improvements
dsl/std/types.dagMakecontainer_to_algebra_namea derivation from the alias declarations themselves so naming variants do not become a second string-keyed authority for the same algebra fact.
ROADMAP — Verified
- TLC-1 partial: The diff does move
is_zero_arg_callable_refinto shared emit code and wires Go and Python variable emission through it, matching the roadmap update.
ROADMAP — Incomplete
- TLC-3: Indexing / character access semantics: The roadmap now marks TLC-3 done, but this diff does not change any index/slice emission paths or
IndexingSemanticsdata, so the claimed completion is not supported by the code shown.
The PR improves shared emit structure and resolves some prior review points, but it still relies on already-known modeling debt and it overstates TLC-3 as complete in ROADMAP.md.
briansrls
left a comment
There was a problem hiding this comment.
Review (INVARIANTS: 0, MODELING: 1+/1-, ROADMAP: 1✓/1!)
MODELING — Strengths
dsl/std/types.dagMoving the container-to-algebra lookup out ofsrc/v2/coercion.dagand up beside the standard type aliases improves layering by keeping the fact at the L0 type authority instead of a compiler helper.
MODELING — Improvements
dsl/std/types.dagcontainer_to_algebrais still a hand-maintained string map for facts already expressed by the alias declarations, so it should be derived from those declarations or algebra edges rather than duplicated.
ROADMAP — Verified
- TLC-1 partial keyword mapping: The new Go/Python
ExprVarbranches routenone,true, andfalsethroughemit_keyword, which supports the narrowed keyword-mapping claim.
ROADMAP — Incomplete
- TLC-3: Indexing / character access semantics:
ROADMAP.mdmarks TLC-3 done, but this diff does not land the claimed Go/Python per-collection indexing changes; the shown code changes are item-dispatch refactors and keyword handling.
ROADMAP — Unclaimed
- MM-1: Item identity: This PR partially advances MM-1 by removing
TypedItemKind-based backend dispatch in favor of structural predicates, butROADMAP.mddoes not record that partial progress or the remaining shared-walk gap.
The diff resolves the earlier coercion-arity issues and makes some structural progress, but it still carries the previously flagged nullary-call/container-authority problems and overclaims TLC-3 completion in ROADMAP.
TLC-3 was incorrectly marked done (came in via rebase, not this PR's changes). Reverted to partial: Rust dispatches per collection type, Go/Python still route through list_index unconditionally. Record MM-1 progress: TypedItemKind enum + classify_typed_item dissolved in PR #324. Shared boolean predicates replace the taxonomy. https://claude.ai/code/session_01HcSSdXfjWoKWiPkquYnCCH
briansrls
left a comment
There was a problem hiding this comment.
Review (INVARIANTS: 0, MODELING: 1+/1-, ROADMAP: 2✓/1!)
MODELING — Strengths
dsl/std/types.dagMoving the container-to-algebra fact intostd.typesputs it at the standard type-model layer instead of a v2 compiler tool layer.
MODELING — Improvements
dsl/std/types.dagDerivecontainer_to_algebrafrom theList = FreeMonoidandMap = PartialFunctionalias authorities rather than maintaining a parallelMap<String, String>.
ROADMAP — Verified
- CG-2 TLC-1 partial: The diff adds Go/Python keyword emission for
none/true/falsewhile keeping zero-arg callable detection Rust-only, matching the updated partial-status note. - MM-1 taxonomy dissolution: The diff deletes
TypedItemKindandclassify_typed_itemand rewires Rust, Go, and Python emitters to shared structural item predicates in05_emit.dag.
ROADMAP — Incomplete
- MM-1 fail-closed boundaries:
emit_py_typed_itemstill has a# unhandled node: ...fail-open branch, so the new[x]claim that fail-open fallbacks are deleted is not supported by the code.
The refactor makes real progress on shared authorities, but ROADMAP.md overclaims MM-1 completion while a Python fail-open item-emission path still remains.
TypedItemUnhandled variant is deleted, but all 3 backends still have else branches that fabricate error markers (compile_error/panic/comment) instead of producing upstream diagnostics. Mark fail-closed item as partial [~], not complete [x]. https://claude.ai/code/session_01HcSSdXfjWoKWiPkquYnCCH
TLC-3: verified via code inspection — all three backends already dispatch per collection type (emit_go_typed_index, emit_py_typed_index both check is_string_like + node_is_keyed_collection). Prior ROADMAP description was stale. MM-1 "surface fields": shared predicates exist but ~55 raw structural interrogation sites remain unmigrated — mark partial. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* WIP: Dissolution upstream P7 — std/nat.dag nat_is_zero Nat→Bool Wave-A2 [#324 * fix(v4): in-file 🟡 + DECISIONS row for nat_is_zero predicate interim Operator CORE (still-hawk-102): land Practice-10 predicate-dissolution receipt on std/nat.dag (🟡 gated tag + Ledger) and Part 6 slug SL-P7-NAT-IS-ZERO-VPRED plan-bound to node://adhoc-2145db6b-69a; dissolve-on-arrival forbids nat_cata fold launder. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
Summary
This PR refactors container type classification and centralizes zero-arg callable reference detection across the compiler backends.
Key Changes
Container Classification Refactoring
coercion_keyed_container_names()andcoercion_element_container_names()functions with dynamicis_keyed_container()andis_element_container()functionscontainer_expected_arity()instead of maintaining separate listsZero-Arg Callable Reference Detection
is_zero_arg_callable_ref()function fromv2_compiler_emit_rust.rsto sharedv2_compiler_emit.rsFunctionValueBindingkind andCallabletype with zero parametersBackend-Specific Updates
emit_go_expr_var()to handle zero-arg callable references and special keywords (none/true/false)emit_py_expr_var()with same improvements as Gois_zero_arg_callable_ref()definition and now imports from shared emit moduleVarBindingKindandFunctionValueBindingImplementation Details
container_expected_arity()function, reducing duplicationhttps://claude.ai/code/session_01HcSSdXfjWoKWiPkquYnCCH