Skip to content

distinct types: nominal newtype over workhorse types — typedef distinct Foo = int#3424

Merged
borisbat merged 3 commits into
masterfrom
bbatkin/distinct-types
Jul 10, 2026
Merged

distinct types: nominal newtype over workhorse types — typedef distinct Foo = int#3424
borisbat merged 3 commits into
masterfrom
bbatkin/distinct-types

Conversation

@borisbat

@borisbat borisbat commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

Promotes weak types (int, float, void?, any workhorse type) to unique nominal types with zero runtime/ABI cost:

typedef distinct EntityId = int
typedef distinct Meters = float
typedef distinct FileHandle = void?

From the ABI perspective a distinct type IS its underlying type — same size, alignment, pass-by-value. But it never interconverts: functions taking int won't take EntityId and vice versa, two distincts over the same underlying are mutually incompatible, and overloads on all of them coexist.

Semantics

  • In: Foo(expr) — exactly one argument of exactly the underlying type; a reinterpret relabel, zero runtime cost. var a : Foo = 5 is an error; var a : Foo / default<Foo> zero-init (string/pointer/vector underlyings included).
  • Out: *a — peels exactly one distinct level, yields the underlying as a reference (*a = 5 writes; const flows from the handle per the const model). For distinct-over-pointer, * peels the distinct, not the pointer.
  • Operators: only ==/!= are borrowed (language-level, same-distinct-only, lowered to a compare of the underlyings at infer). Everything else is user-defined: def operator + (a, b : Meters) : Meters => Meters(*a + *b).
  • Not a table key (explicit rejection — the nominal wall is the point). Arrays, struct/tuple/variant fields all work.
  • Module-nominal with typedef private distinct Foo = int visibility, like enums.
  • Underlying must be a workhorse type; distinct-of-distinct rejected.

Implementation

  • distinct is not a keyword at all. The declaration is a distinct_alias production under typedef: typedef [private|public] distinct Foo = int, with distinct a plain NAME validated in the action (Nim's type Foo = distinct int shape). No lexer involvement — comments between tokens lex away normally and every identifier use of distinct (daslib/linq's function etc.) is untouched. Gen2 only; %expect 0 holds. The tree-sitter grammar gets the matching typedef_declaration alternative (parser.c regenerated — one-time large mechanical diff, standing parser.c was tree-sitter-cli 0.22 output, regen is 0.26.9 per the Node-24 requirement; precedent tree-sitter: fix type<T>; parse failure in type position #2877).
  • New Type::tDistinct, AST-only, appended after tFixedArray (append-only keeps serialized ordinals/hashes stable). Entity is DistinctTypeAnnotation : TypeAnnotation riding the existing TypeDecl::annotation slot (already cloned/GC-walked/compared/serialized); firstType holds the underlying. ABI predicates (size/align/pod/copy/move/local/workhorse/gc-flags/string-data) delegate to firstType; semantic predicates stay false so nothing leaks.
  • Runtime erasure at the makeTypeInfo boundary (same as the fixed-array flatten): interpreter, RTTI, and fusion never observe tDistinct. The JIT walks AST types, so it peels in type_to_llvm_type / type_to_llvm_abi_type / vec4f marshaling and treats the distinct deref as a pass-through (LLVM_JIT_CODEGEN_VERSION bumped). AOT emits the underlying C++ type (DAS_COMMENT(distinct Name) annotated), both in describeCppTypeEx and daslib/aot_cpp.das.
  • New mangling char Q<module::name> (emit + parse) keeps f(Foo) / f(int) overloads and AOT hashes distinct.
  • Construction rides the aliasSubstitution-style rail: unresolved ExprCall named like a distinct type → ExprCast with reinterpret. Foo() and Foo(a=1) are rejected. The workhorse make-struct fold's Program::makeConst null-deref (pre-existing for any non-const-able type) is fixed; distinct default<> routes string/pointer underlyings like the non-distinct paths and errors cleanly on anything makeConst can't produce.
  • Serialization: TypeDecl tDistinct case + das-declared entities round-trip with the module, registered before aliasTypes/enumerations deserialize; own-module annotation references stream by name (serializeAnnotationPointer previously asserted macroContext — das-declared distincts are the first annotations owned by a non-macro module). Version 98 → 99. Exercised by CI's ser/deser sweep (full-tree --ser/--deser green locally).
  • C++ side: DistinctTypeAnnotation + MAKE_DISTINCT_TYPE_FACTORY + ModuleLibrary::makeDistinctType let a C++ module register a distinct type and bind externs whose signatures carry it. Worked example: NativeId in dasUnitTest.
  • New errors: invalid_distinct_type = 30297, mismatching_distinct_type = 30413.

Validation

  • tests/language/test_distinct.das (26 subtests: construct/deref/write, equality, Foo-vs-Bar nominal walls, string/pointer/float3 underlyings incl. default<>, user operators, overload coexistence, containers + struct/tuple/variant fields, generics + is_distinct trait + typedecl(*x), cross-module + fixture) — green under interpreter, JIT, AOT, and ser/deser.
  • tests/language/test_distinct_interop.das — C++-registered distinct end-to-end.
  • tests/typer_errors/failed_distinct_*.das — declaration, mismatch (incl. cross-distinct), ctor, table-key, private rejections with exact expect codes.
  • Full sweep: 11315 tests green interpreted and under test_aot; tree-sitter corpus 42/42.
  • Docs: doc/source/reference/language/distinct.rst + rtti Type enum handmade line; das2rst/Sphinx (html + latex -W) clean; preflight full-pass token + sequence smoke green.

🤖 Generated with Claude Code

…int)

New Type::tDistinct (AST-only, appended after tFixedArray) + DistinctTypeAnnotation
(TypeAnnotation subclass riding the existing TypeDecl annotation slot; firstType =
the underlying type). ABI is exactly the underlying workhorse type: size/align/pod/
copy/workhorse predicates delegate to firstType, and runtime TypeInfo, interpreter,
JIT, and fusion never observe tDistinct -- erased at makeTypeInfo alongside the
tFixedArray flatten; the JIT peels it in type_to_llvm_type/abi/vec4f marshaling
(LLVM_JIT_CODEGEN_VERSION bumped).

Semantics: no interconversion with the underlying type in either direction, so
overloads on Foo vs int coexist (new 'Q<mod::name>' mangling char, emit + parse).
The only way in is Foo(expr) -- exactly one argument of exactly the underlying
type, a reinterpret relabel with zero runtime cost. The only way out is *a, which
peels one distinct level and yields the underlying as a reference (const flows
from the handle; for distinct-over-pointer it peels the distinct, not the pointer).
== and != are borrowed (language-level, same-distinct-only, lowered to a compare
of the underlyings at infer); all other operators are user-defined. Distinct types
are not table keys by design. var a : Foo and default<Foo> zero-init.

Grammar is gen2-only, and 'distinct' is a CONTEXTUAL keyword: a flex trailing
context ('distinct' followed by optional private/public, NAME, '=') keeps every
other use -- notably daslib/linq's distinct() -- lexing as a plain identifier.
%expect 0 holds.

C++ side: DistinctTypeAnnotation + MAKE_DISTINCT_TYPE_FACTORY + makeDistinctType
let a C++ module register a distinct type and bind externs whose signatures carry
it (dasUnitTest NativeId example). das-declared entities round-trip through module
serialization (version 98 -> 99).

Errors: invalid_distinct_type=30297 (bad underlying, distinct-of-distinct, private
access), mismatching_distinct_type=30413 (construction arity/argument type).

Tests: tests/language/test_distinct.das + test_distinct_interop.das +
_distinct_fixture.das (cross-module + private), tests/typer_errors/
failed_distinct_*.das -- green under interpreter, JIT, and AOT; full test sweep
clean. Reference page: doc/source/reference/language/distinct.rst.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 10, 2026 07:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds a new nominal distinct type feature to daScript (Gen2) that allows creating zero-cost newtypes over “workhorse” value types (e.g. distinct Id = int), with explicit in/out via Foo(x) and *foo, nominal overload separation, and runtime erasure to the underlying ABI type across interpreter/JIT/AOT/RTTI.

Changes:

  • Introduces Type::tDistinct + DistinctTypeAnnotation, integrates it into type checking/inference, operator handling (==/!=), mangling, and table-key rejection.
  • Extends the Gen2 lexer/parser to recognize distinct declarations as a contextual keyword and build the corresponding AST annotations/types.
  • Adds cross-tier support (AST serialization, debug info/typeinfo erasure, LLVM JIT + AOT C++ emission) plus tests and documentation.

Reviewed changes

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

Show a summary per file
File Description
tests/typer_errors/failed_distinct_table_key.das New negative test: distinct types rejected as table keys.
tests/typer_errors/failed_distinct_private.das New negative test: private distinct visibility across modules.
tests/typer_errors/failed_distinct_mismatch.das New negative test: no implicit interop with underlying / no borrowed ops.
tests/typer_errors/failed_distinct_decl.das New negative test: distinct-of-distinct and non-workhorse underlying rejected.
tests/typer_errors/failed_distinct_ctor.das New negative test: ctor arity/type and “no fields” behavior.
tests/language/test_distinct.das New language-level coverage for construction/deref/equality/containers/generics/modules.
tests/language/test_distinct_interop.das New interop test for C++-registered distinct types.
tests/language/_distinct_fixture.das Shared fixture module defining public/private distinct types for cross-module tests.
tests/aot/CMakeLists.txt Ensures distinct fixture module is included in AOT language test module set.
src/simulate/debug_info.cpp Adds “distinct” label for debug type name mapping.
src/parser/parser_impl.h Declares ast_distinctDeclaration parser helper.
src/parser/parser_impl.cpp Implements distinct declaration validation + registers DistinctTypeAnnotation.
src/parser/lex2.yy.h Regenerated lexer header line mapping update.
src/parser/ds2_parser.ypp Adds distinct token and grammar rule for distinct declarations.
src/parser/ds2_parser.hpp Regenerated parser token enum includes DAS_DISTINCT.
src/parser/ds2_lexer.lpp Adds contextual lexer rule for distinct ... = to return DAS_DISTINCT.
src/parser/ds2_lexer.cpp Regenerated lexer tables/code for the new distinct rule.
src/builtin/module_builtin_rtti.h Exposes Type.tDistinct in RTTI enum binding.
src/builtin/module_builtin_ast_serialize.cpp Serializes tDistinct TypeDecl + round-trips das-declared distinct annotations in modules.
src/ast/ast_typedecl.cpp TypeDecl behavior updates for tDistinct (describe, hashing/mangling, ABI delegation, etc.).
src/ast/ast_simulate.cpp Interpreter simulation: treat *distinct as compile-time relabel (no null-check/deref).
src/ast/ast_program.cpp Type resolution for distinct annotations + makeConst peels distinct to underlying.
src/ast/ast_module.cpp Adds ModuleLibrary::makeDistinctType for C++ type-factory integration.
src/ast/ast_infer_type.cpp Type inference for *distinct, Foo(expr) construction rewrite, and is_distinct trait.
src/ast/ast_infer_type_op.cpp Lowers ==/!= on same distinct type to underlying compare via deref nodes.
src/ast/ast_infer_type_make.cpp Special-cases default<distinct> folding (currently problematic for non-numeric underlyings).
src/ast/ast_infer_type_helper.cpp Verifies distinct validity + rejects distinct table keys with specific error.
src/ast/ast_infer_type_function.cpp Avoids duplicate/incorrect diagnostics for distinct construction path.
src/ast/ast_debug_info_helper.cpp Erases tDistinct at AST→TypeInfo boundary (like fixed-array flatten).
modules/dasUnitTest/unitTest.h Adds C++ NativeId wrapper + cast/WrapType for distinct interop.
modules/dasUnitTest/test_handles.cpp Registers NativeId as a distinct annotation + binds extern native_id_next.
modules/dasLLVM/daslib/llvm_jit.das JIT visitor: treat *distinct as relabel and adjust vec4f marshaling.
modules/dasLLVM/daslib/llvm_jit_run.das Bumps LLVM_JIT_CODEGEN_VERSION for distinct support.
modules/dasLLVM/daslib/llvm_jit_common.das Erases distinct to underlying in LLVM type mapping (ABI/type).
include/daScript/simulate/debug_info.h Adds tDistinct to Type enum docs (AST-only + erasure notes).
include/daScript/ast/compilation_errors.h Adds new error codes for distinct invalidity/mismatch.
include/daScript/ast/ast.h Adds RTTI hook for distinct annotations + makeDistinctType API on ModuleLibrary.
include/daScript/ast/ast_typefactory.h Adds MAKE_DISTINCT_TYPE_FACTORY for C++ signature typing.
include/daScript/ast/ast_typedecl.h Adds TypeDecl::isDistinct() and makeDistinctType declaration.
include/daScript/ast/ast_serializer.h Bumps AST serializer version for distinct support.
include/daScript/ast/ast_handle.h Defines DistinctTypeAnnotation (nominal distinct entity).
doc/source/stdlib/handmade/enumeration-rtti-Type.rst Updates RTTI Type enum docs to include distinct.
doc/source/reference/language/distinct.rst New language reference page for distinct types (semantics + examples).
doc/source/reference/language.rst Adds distinct page to language reference TOC.
daslib/aot_cpp.das AOT: emits underlying C++ type with distinct comment + handles *distinct as relabel.

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

Comment thread src/ast/ast_infer_type_make.cpp Outdated
Comment thread src/parser/ds2_lexer.lpp Outdated
CI's ser/deser sweep hit the serializeAnnotationPointer own-module verify
('expected to see macro module') -- a das-declared DistinctTypeAnnotation is
the first annotation owned by a NON-macro module. Own-module distinct
references now stream as a name and resolve on read against the module's own
registry (the entities round-trip in Module::serialize before anything that
references them). Verified: full-tree --ser/--deser sweep green locally, and
the deserialized distinct tests run 25/25.

Also: explicit tests that two distincts over the same underlying (Id vs
OtherId) are mutually incompatible -- calls and == both reject (30341).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 10, 2026 08:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 45 out of 46 changed files in this pull request and generated 1 comment.

Comment thread include/daScript/ast/ast_handle.h
…lot round-1 fixes

Grammar rework per review discussion: the declaration is now
'typedef [private|public] distinct Foo = int' - a distinct_alias production
under DAS_TYPEDEF where 'distinct' is a plain NAME validated in the action.
No lexer involvement at all: the trailing-context contextual-keyword rule is
gone, comments between tokens lex away normally, and every identifier use of
'distinct' (daslib/linq etc.) is untouched at any position. Privacy sits in
the typedef family's slot (typedef private distinct ...) - the kw-first order
needs two-token lookahead and would break %expect 0. Nim precedent
(type Foo = distinct int).

Copilot round 1:
- default<Foo> over string/pointer underlyings crashed: the distinct fold
  called Program::makeConst which covers neither. String/pointer now route
  like the non-distinct default<> paths (ExprConstString / ExprConstPtr);
  any other makeConst-null underlying errors 30413 instead of crashing.
  Probe-confirmed before/after; tests added.
- DistinctTypeAnnotation::getOwnSemanticHash null-guards underlyingType,
  consistent with the class's other methods.

tree-sitter grammar: typedef_declaration grows the distinct alternative;
corpus case added (42/42); parser.c regenerated (one-time large mechanical
diff - standing parser.c was tree-sitter-cli 0.22 output, regen is 0.26.9
per the Node-24 requirement; precedent PR #2877).

Validation: full sweep 11315 green interp + AOT, distinct tests green under
JIT and ser/deser, Sphinx latex+html clean, lint/format clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 10, 2026 08:58
@borisbat borisbat changed the title distinct types: nominal newtype over workhorse types — distinct Foo = int distinct types: nominal newtype over workhorse types — typedef distinct Foo = int Jul 10, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 45 out of 47 changed files in this pull request and generated 1 comment.

Comment on lines +1153 to +1156
auto underT = expr->type->firstType;
ExpressionPtr ews;
if (underT->isString()) {
ews = new ExprConstString(expr->at);
@borisbat borisbat merged commit 52c8bcc into master Jul 10, 2026
38 checks passed
pull Bot pushed a commit to forksnd/daScript that referenced this pull request Jul 10, 2026
…e zero enumerant

The zero-value constant folds in InferTypes::visit(ExprMakeStruct*) (string/
enum/pointer/workhorse) ran even when the make-struct carried field
initializers; the reportAstChanged() rerun then discarded preVisit's
invalid_structure_type error, so E(a=1) — and V(x=1) via an alias of any
workhorse/pointer type — compiled silently to the zero value.

Guard the fold chain: with non-empty structs on a non-composite make-type,
skip the fold and return, so the preVisit error survives to the final pass.
This subsumes the distinct-type guard from GaijinEntertainment#3424 (removed). The zero-value
folds stay for the legitimate empty forms (default<T>, T()).

Rides along: CLAUDE.md distinct-types entry + .gitignore build32/ leftovers
from the GaijinEntertainment#3424 session.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants