Skip to content

Option<T> / Result<T,E>: structural named-tuple via [template_tuple]#2601

Merged
borisbat merged 1 commit intomasterfrom
option-template-tuple
May 8, 2026
Merged

Option<T> / Result<T,E>: structural named-tuple via [template_tuple]#2601
borisbat merged 1 commit intomasterfrom
option-template-tuple

Conversation

@borisbat
Copy link
Copy Markdown
Collaborator

@borisbat borisbat commented May 7, 2026

Summary

Fixes #2598. Combining daslib/clargs and modules/dasSQLITE/daslib/sqlite_boost (directly or via require ... public chains) made every Option<string> ambiguous — [template_structure] cloned an instance struct into each consuming module via qmacro_template_class, producing N module-aliased views of the same source decl. Candidate collection treated them as distinct and emitted error[30341]: too many matching functions or generics _::Option<string> with three identical-looking candidates pointing at the same line.

Rather than a typer-side dedupe of candidates by source decl (broader scope, not gated on this PR), Option and Result now resolve to structural named tuples. Tuples have no module home; multiple transitive re-exports collapse to one canonical tTuple TypeDecl regardless of which alias the user lands on.

[template_tuple(T)]
struct template Option {
    _has_value : bool = false
    @safe_when_uninitialized _value : T
}
  • Option<T>tuple<_has_value : bool; _value : T>
  • Result<T,E>tuple<_is_ok : bool; _value : T; _error : E>

Pieces

daslib/typemacro_boost.das — new [template_tuple(T...)] [structure_macro]. At apply time it generates a [typemacro_function]; at typemacro-execution time the body has two paths:

  • generic: shape-matches a tTuple by argNames + literal-field baseType, and feeds template-arg fields into infer_template_types via passArgument.argTypes[i].
  • concrete: looks up the source struct via typeinfo ast_typedecl(type<X>) (X = the struct's module-scoped alias) and applies templates_boost's existing TypeDecl visitor (apply_template + replaceTypeWithTypeDecl) to each field. The visitor walks T, array<T>, tuple<int; T; E>, etc. uniformly — no apply-time recursion over field shapes is needed.

Field-level @safe_when_uninitialized propagates: when a source field carries the annotation, the corresponding tuple element's TypeDecl gets TypeDeclFlags.safeWhenUninitialized. The tuple's recursive unsafeInit walks its argTypes, so per-element marking gives the same effect the legacy struct-level [safe_when_uninitialized] used to.

daslib/option.das, daslib/result.das — collapse to near-original struct declarations with annotation swap + move_some / move_ok / move_err rewrites that drop the typedef RT shorthand in favor of anonymous named-tuple construction.

Compiler-side support for safe-when-uninitialized at TypeDecl granularity:

  • TypeDecl::safeWhenUninitialized flag bit + unsafeInit short-circuit. Lets a typemacro mark a tuple component (e.g. Option's _value field) safe to leave uninitialized even when the underlying T isn't.
  • TypeDecl::applyAutoContracts OR-merges the new flag the same way it merges ref / constant / temporary, so generic resolution preserves it across auto.
  • TypeDeclFlags exposes the bit to daslang code.
  • InferTypes::preVisit(ExprAssume*) silently dedupes assume X : T when an outer scope already binds X to the same type. Handles the auto-injected assume placed at the start of a generic instance body when a typemacro-built result type carries an alias the body also references. InferTypes::visit(ExprAssume*) mirrors the same predicate so a deduped re-assume is also kept out of assumeType, leaving findAlias lookups clean.
  • AOT codegen: emit local make-struct destinations without const. Aggregate types holding TTuple<> need to be value-init'd at declaration; the const qualifier was making C++ reject T const x; for those shapes.

dasSQLITE adapter railis_option_field_type / unwrap_option_payload_type detect the typeMacro form (during [sql_table] apply) and the post-instantiation tTuple form. The legacy struct-shape detection drops out — no in-tree code emits a struct-shaped Option after this PR.

Test plan

  • daslang dastest/dastest.das -- --test tests/option/ → 205/205 pass
  • daslang dastest/dastest.das -- --test tests/dasSQLITE/ → 771/771 pass
  • daslang dastest/dastest.das -- --test tests/ → 7961/7961 interpreted
  • test_aot.exe -use-aot dastest/dastest.das -- --use-aot --test tests/ → 7355/7355 AOT
  • Issue Type-macro instantiations from publicly-re-exported templates collide across modules: Option<string> ambiguous when both daslib/clargs and sqlite/sqlite_boost are in scope #2598 repro (require daslib/clargs + require sqlite/sqlite_boost + Option<string> field) compiles cleanly
  • Bare-var regression: removing @safe_when_uninitialized from _value : T and recompiling tests/option/test_option_unsafe_uninitialized.das produces error[31016] on the bare var x : Option<Brittle> line — the per-field annotation is doing real work
  • Composite literal field types (array<T>, tuple<int; T; E>) substitute correctly via the visitor — verified with a hand-rolled [template_tuple] probe carrying array<T> and tuple<int; T; E> field shapes
  • Lint clean on all changed .das files
  • MCP format_file clean on all changed .das files

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings May 7, 2026 18:35
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses daslang generic candidate ambiguity when Option<T>/Result<T,E> templates are publicly re-exported through multiple module chains (issue #2598) by switching them from module-owned [template_structure] instances to structural named tuples produced by a new [template_tuple] structure macro. It also adds compiler/runtime support so tuple elements can carry @safe_when_uninitialized behavior, and updates dependent tooling (dasSQLITE + AOT) plus regression tests.

Changes:

  • Add [template_tuple] in daslib/typemacro_boost.das to synthesize typemacro-based named tuple types (structural identity, no per-module cloning).
  • Convert daslib/option.das and daslib/result.das to structural tuple forms, with per-element @safe_when_uninitialized propagation via a new TypeDecl::safeWhenUninitialized flag.
  • Update compiler/AOT + dasSQLITE Option-shape handling and add regression tests for cross-module Option usage and unsafe-uninitialized payloads.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/option/test_option_user_struct.das New regression test ensuring Option<UserStruct> works across module boundaries and remains unambiguous.
tests/option/test_option_unsafe_uninitialized.das New regression test validating Option<T> payload field safety for “unsafe when uninitialized” payload structs.
tests/option/_test_option_user_struct_mod.das Test helper module that publicly re-exports daslib/option and returns Option<Foo> to exercise cross-module typing.
src/builtin/module_builtin_ast_flags.cpp Exposes new TypeDeclFlags.safeWhenUninitialized bit to daslang code.
src/ast/ast_typedecl.cpp Propagates safeWhenUninitialized through auto contracts; short-circuits unsafeInit when the flag is set.
src/ast/ast_infer_type.cpp Allows silent dedupe of type-only assume X : T when the same alias already resolves to the same type.
modules/dasSQLITE/daslib/sqlite_boost.das Updates Option detection/unwrapping logic to recognize the new structural tuple shape.
include/daScript/ast/ast_typedecl.h Adds TypeDecl::safeWhenUninitialized flag bit.
daslib/typemacro_boost.das Implements the new [template_tuple] structure macro and per-field safe-when-uninitialized propagation.
daslib/result.das Converts Result<T,E> to [template_tuple] + updates constructors/move helpers for tuple construction.
daslib/option.das Converts Option<T> to [template_tuple] + updates move_some to construct tuples directly.
daslib/aot_cpp.das Adjusts AOT local make-struct destination type emission to skip const to satisfy aggregate initialization constraints.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/ast/ast_infer_type.cpp
Comment thread modules/dasSQLITE/daslib/sqlite_boost.das
@borisbat borisbat force-pushed the option-template-tuple branch 2 times, most recently from e33db52 to 4a8332f Compare May 7, 2026 19:14
@borisbat borisbat requested a review from Copilot May 7, 2026 19:16
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 2 comments.

Comment thread daslib/typemacro_boost.das Outdated
Comment thread src/ast/ast_infer_type.cpp Outdated
@borisbat borisbat force-pushed the option-template-tuple branch from 4a8332f to 23f8d59 Compare May 7, 2026 23:22
Fixes #2598. Combining daslib/clargs and modules/dasSQLITE/daslib/sqlite_boost
in one module (directly or via require ... public chains) made every
Option<string> ambiguous: [template_structure] cloned an instance struct into
each consuming module via qmacro_template_class, producing N module-aliased
views of the same source decl. Candidate collection treated them as distinct
and emitted "error[30341]: too many matching functions or generics
_::Option<string>" with three identical-looking candidates pointing at
daslib/option.das.

Rather than a typer-side dedupe of candidates by source decl (broader scope,
not gated on this PR), Option and Result now resolve to STRUCTURAL named
tuples. Tuples have no module home; multiple transitive re-exports collapse
to one canonical tTuple TypeDecl regardless of which alias the user lands on.

  [template_structure(T), safe_when_uninitialized] struct template Option ...
  ->
  [template_tuple(T)] struct template Option {
      _has_value : bool = false
      @safe_when_uninitialized _value : T
  }

  Option<T>   -> tuple<_has_value : bool; _value : T>
  Result<T,E> -> tuple<_is_ok : bool; _value : T; _error : E>

* daslib/typemacro_boost.das: new [template_tuple(T...)] structure_macro.
  At apply time it generates a [typemacro_function]; at typemacro-execution
  time the body has two paths:
   - generic: shape-matches a tTuple by argNames + literal-field baseType,
     and feeds template-arg fields into infer_template_types via
     passArgument.argTypes[i].
   - concrete: looks up the source struct via ``typeinfo ast_typedecl(type<X>)``
     (X = the struct's module-scoped alias) and applies templates_boost's
     existing TypeDecl visitor (apply_template + replaceTypeWithTypeDecl) to
     each field. The visitor walks ``T``, ``array<T>``, ``tuple<int; T; E>``,
     etc. uniformly; no apply-time recursion over field shapes is needed.
  Field-level @safe_when_uninitialized propagates: when a source field
  carries the annotation, the corresponding tuple element's TypeDecl gets
  TypeDeclFlags.safeWhenUninitialized. The tuple's recursive unsafeInit
  walks its argTypes, so per-element marking gives the same effect the
  legacy struct-level [safe_when_uninitialized] used to.

* daslib/option.das, daslib/result.das: collapse to near-original struct
  declarations with annotation swap + move_some / move_ok / move_err
  rewrites that drop the typedef RT shorthand in favor of anonymous
  named-tuple construction (new pattern allowed by gen2 grammar).

Compiler-side support for safe-when-uninitialized at TypeDecl granularity:

* TypeDecl::safeWhenUninitialized flag bit + unsafeInit short-circuit. Lets
  a typemacro mark a tuple component (e.g. Option's _value field) safe to
  leave uninitialized even when the underlying T isn't, restoring the
  property the struct-level [safe_when_uninitialized] previously gave
  Option/Result.
* TypeDecl::applyAutoContracts OR-merges the new flag the same way it
  merges ref/constant/temporary, so generic resolution preserves it
  across auto.
* TypeDeclFlags exposes the bit to daslang code.
* InferTypes::preVisit(ExprAssume*) silently dedupes ``assume X : T`` when
  an outer scope already binds X to the same type. Handles the auto-
  injected assume placed at the start of a generic instance body when a
  typemacro-built result type carries an alias the body also references.
* AOT codegen: emit local make-struct destinations without ``const``.
  Aggregate types holding TTuple<> need to be value-init'd at declaration;
  the const qualifier was making C++ reject ``T const x;`` for those shapes.

dasSQLITE adapter rail: is_option_field_type / unwrap_option_payload_type
detect the typeMacro form (during structure_macro apply) and the
post-instantiation tTuple form. The legacy struct-shaped detection drops
out — no in-tree code emits a struct-shaped Option after this PR.

Tests:
* tests/option/test_option_user_struct.das: cross-module Option<UserStruct>.
* tests/option/test_option_unsafe_uninitialized.das: Option<Brittle> where
  Brittle is unsafe-when-uninitialized; covers default<>, none(), some(),
  unwrap_or_default round-trips, and the bare ``var x : Option<Brittle>``
  case which is the only form that hits ast_infer_type.cpp:5040's
  "Uninitialized variable is unsafe" check (verified by removing the
  field annotation locally and observing error[31016]).

Verified: tests/option 205/205 + tests/dasSQLITE 771/771 (interpreted &
AOT); full tests/ tree 7961/7961 interpreted, 7355/7355 AOT. Issue #2598
repro (clargs + sqlite_boost + Option<string>) compiles cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 2 comments.

Comment thread tests/option/test_option_unsafe_uninitialized.das
Comment thread tests/option/test_option_unsafe_uninitialized.das
@borisbat borisbat merged commit 667e8f7 into master May 8, 2026
35 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants