From f16f352d1e47bdfb9fa63b134fe7278a9e7be720 Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Tue, 23 Jun 2026 12:29:21 +0200 Subject: [PATCH] distinct type aliasing --- LANGUAGE.md | 1 + TODO.md | 9 +- compiler/checker/checker.odin | 151 +++++++++++++--- compiler/hir/hir.odin | 1 + compiler/ir/ir.odin | 1 + compiler/lexer/lexer.odin | 1 + compiler/llvm/llvm.odin | 70 +++++--- compiler/loader/loader.odin | 5 + compiler/lower/lower.odin | 16 +- compiler/parser/parser.odin | 17 ++ compiler/token/token.odin | 1 + compiler/types/types.odin | 89 +++++++++- compiler_tests.odin | 178 +++++++++++++++++++ examples/programs/distinct_types/ids/ids.bro | 5 + examples/programs/distinct_types/main.bro | 38 ++++ 15 files changed, 525 insertions(+), 58 deletions(-) create mode 100644 examples/programs/distinct_types/ids/ids.bro create mode 100644 examples/programs/distinct_types/main.bro diff --git a/LANGUAGE.md b/LANGUAGE.md index 748a17e..88022fb 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -15,6 +15,7 @@ - exact-width `i8` through `i64`, `u8` through `u64`, `f32`, `f64`, `isize`, `usize`, `bool`, `void`, and inferred integer-constrained `int` - target-dependent atomic C primitives from `c_char` through `c_longdouble` - C primitives remain semantically distinct from exact-width Brolang primitives until target lowering +- nominal distinct types with explicit exact-backing construction: `UserID :: distinct u32` and `UserID(42)` - contextual integer and character literals and constant folding of arithmetic and negation trees - strict numeric conversions, binary `+ - * /` with checked integer overflow and divide-by-zero traps (floats follow IEEE), and unary negation - boolean literals, comparisons, unary `!`, and short-circuiting `and` / `or` diff --git a/TODO.md b/TODO.md index 27311c1..59f0965 100644 --- a/TODO.md +++ b/TODO.md @@ -127,7 +127,12 @@ 7. enums (native and c interop) (see below) -8. distinct types (see below) +8. distinct types (implemented; see below) + - nominal declarations preserve identity across packages and reuse the backing runtime representation + - construction uses `Type(value)` with exactly one value of the exact backing type + - no implicit conversion to or from the backing type + - backing-type operators and reverse explicit conversions remain deferred + - concrete runtime backing types are supported; unresolved, `int`, `void`, function, and opaque backings are rejected ## A word on multi-unwrap @@ -204,7 +209,7 @@ Distinct types are considered distinct from their backing type. They do not impl UserID :: distinct u32 # instantiate distinct type -my_id UserId :: UserID(42) # value must be of to backing type +my_id UserID :: UserID(42) # value must have the exact backing type ``` # A word on enums diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index 745c62b..850b35d 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -870,6 +870,15 @@ validate_declarations :: proc(checker: ^Checker) { validate_type_nodes :: proc(checker: ^Checker) { for item, index in checker.module.types.nodes { id := types.DYNAMIC_START+types.Type(index) + if item.kind == .Distinct && + (!item.declared || !types.is_runtime_value(item.child, &checker.module.types)) { + source.addf( + checker.diagnostics, + source.Span{}, + "distinct type '%s' requires a concrete runtime backing type", + symbol_text(checker, symbol.Id(item.name)), + ) + } if item.has_sentinel { value := i128(item.sentinel) if types.is_signed(item.child, checker.target) { @@ -1311,6 +1320,14 @@ infer_expr :: proc( } _, function_item, function_type, ok := types.function_pointer(callee_type, &checker.module.types) if !ok { + distinct_type := types.find_named(&checker.module.types, u32(target_pkg), u32(expr.name)) + distinct_item, distinct_ok := types.node(&checker.module.types, distinct_type) + if available && distinct_ok && distinct_item.kind == .Distinct && len(expr.args) == 1 { + stack[frame_index].left = distinct_type + stack[frame_index].stage = 7 + append(&stack, Infer_Frame{expr=expr.args[0], template=ast.INVALID_FUNCTION}) + continue + } last = types.INVALID _ = pop(&stack) continue @@ -1426,6 +1443,10 @@ infer_expr :: proc( stack[frame_index].args = nil _ = pop(&stack) } + if frame.stage == 7 { + last = frame.left + _ = pop(&stack) + } } return last } @@ -1951,6 +1972,7 @@ build_float_expr :: proc(checker: ^Checker, expr: ast.Expr, expected: types.Type Build_Expr_Frame :: struct { expr: ast.Expr_Id, expected: types.Type, + target_type: types.Type, stage: u8, left: hir.Expr_Id, arg_index: int, @@ -2701,38 +2723,81 @@ build_expr :: proc( template := find_template(checker, expr.name, target_pkg) if template == ast.INVALID_FUNCTION { callee := hir.INVALID_EXPR - callee_from_global := false + non_callable := false + non_callable_global := false if !symbol.is_valid(expr.qualifier) { if local, ok := find_build_local(locals, expr.name); ok { - callee = add_hir_expr(checker, hir.Expr{ - kind=.Local, span=expr.span, type=local.type, target=hir.local_ref(local.id), - left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, - }) + if _, _, _, callable := types.function_pointer(local.type, &checker.module.types); callable { + callee = add_hir_expr(checker, hir.Expr{ + kind=.Local, span=expr.span, type=local.type, target=hir.local_ref(local.id), + left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, + }) + } else { + non_callable = true + } } } - if callee == hir.INVALID_EXPR { + if callee == hir.INVALID_EXPR && !non_callable { if global := find_global(checker, expr.name, target_pkg); global != ast.INVALID_GLOBAL { - callee = build_global_reference(checker, global, expr.span, global_reads) - callee_from_global = true + if _, _, _, callable := types.function_pointer(checker.global_types[global], &checker.module.types); callable { + callee = build_global_reference(checker, global, expr.span, global_reads) + } else { + non_callable = true + non_callable_global = true + } } } if callee == hir.INVALID_EXPR { - id := add_unsupported_diagnostic(checker, expr.span, target_pkg, expr.name) - if id == source.INVALID_DIAGNOSTIC { - id = add_call_resolution_diagnostic(checker, expr, target_pkg) + distinct_type := types.find_named(&checker.module.types, u32(target_pkg), u32(expr.name)) + distinct_item, distinct_ok := types.node(&checker.module.types, distinct_type) + if distinct_ok && distinct_item.kind == .Distinct { + if !is_runtime_type(checker, distinct_type) { + id := source.addf( + checker.diagnostics, + expr.span, + "distinct type '%s' has no concrete runtime backing type", + symbol_text(checker, expr.name), + ) + last = invalid_hir_expr(checker, expr.span, id) + _ = pop(&stack) + continue + } + if len(expr.args) != 1 { + id := source.addf( + checker.diagnostics, + expr.span, + "distinct type '%s' expects 1 argument, got %d", + symbol_text(checker, expr.name), + len(expr.args), + ) + last = invalid_hir_expr(checker, expr.span, id, distinct_type) + _ = pop(&stack) + continue + } + stack[frame_index].target_type = distinct_type + stack[frame_index].stage = 8 + append(&stack, Build_Expr_Frame{ + expr=expr.args[0], + expected=distinct_item.child, + template=ast.INVALID_FUNCTION, + }) + continue + } + id := source.INVALID_DIAGNOSTIC + if non_callable { + id = add_call_resolution_diagnostic(checker, expr, target_pkg) if non_callable_global else + source.add(checker.diagnostics, expr.span, "call target is not a function pointer") + } else { + id = add_unsupported_diagnostic(checker, expr.span, target_pkg, expr.name) + if id == source.INVALID_DIAGNOSTIC { + id = add_call_resolution_diagnostic(checker, expr, target_pkg) + } } last = invalid_hir_expr(checker, expr.span, id) _ = pop(&stack) continue } - _, function_item, function_type, ok := types.function_pointer(checker.module.exprs[callee].type, &checker.module.types) - if !ok { - id := add_call_resolution_diagnostic(checker, expr, target_pkg) if callee_from_global else - source.add(checker.diagnostics, expr.span, "call target is not a function pointer") - last = invalid_hir_expr(checker, expr.span, id) - _ = pop(&stack) - continue - } + _, function_item, function_type, _ := types.function_pointer(checker.module.exprs[callee].type, &checker.module.types) if !valid_callable_arity(function_item, len(expr.args)) { message := "function pointer expects at least %d arguments, got %d" if function_item.variadic else "function pointer expects %d arguments, got %d" @@ -2999,6 +3064,32 @@ build_expr :: proc( } _ = pop(&stack) } + if frame.stage == 8 { + distinct_item, ok := types.node(&checker.module.types, frame.target_type) + actual := checker.module.exprs[last].type + if !ok || distinct_item.kind != .Distinct || !types.equal(actual, distinct_item.child) { + id := source.addf( + checker.diagnostics, + expr.span, + "distinct type '%s' requires an exact %s value, got %s", + symbol_text(checker, expr.name), + types.name(distinct_item.child), + types.name(actual), + ) + last = invalid_hir_expr(checker, expr.span, id, frame.target_type) + } else { + last = add_hir_expr(checker, hir.Expr{ + kind=.Retype, + span=expr.span, + type=frame.target_type, + left=last, + target=hir.INVALID_REF, + right=hir.INVALID_EXPR, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + } + _ = pop(&stack) + } } return last } @@ -3783,6 +3874,21 @@ expr_problematic :: proc(checker: ^Checker, expr_id: hir.Expr_Id) -> bool { return false } +static_integer_value :: proc(module: ^hir.Module, expr_id: hir.Expr_Id) -> (i64, bool) { + current := expr_id + for current != hir.INVALID_EXPR && int(current) < len(module.exprs) { + expr := module.exprs[current] + if expr.kind == .Integer { + return expr.integer, true + } + if expr.kind != .Retype { + break + } + current = expr.left + } + return 0, false +} + build_globals :: proc(checker: ^Checker) { for global, global_index in checker.ast_module.globals { if global.external { @@ -3864,11 +3970,8 @@ build_globals :: proc(checker: ^Checker) { ) expr = invalid_hir_expr(checker, global.span, diagnostic) } - is_static := checker.module.exprs[expr].kind == .Integer && diagnostic == source.INVALID_DIAGNOSTIC - static_value: i64 - if is_static { - static_value = checker.module.exprs[expr].integer - } + static_value, is_static := static_integer_value(&checker.module, expr) + is_static = is_static && diagnostic == source.INVALID_DIAGNOSTIC _ = hir.global_id(len(checker.module.globals)) append( &checker.module.globals, diff --git a/compiler/hir/hir.odin b/compiler/hir/hir.odin index 53b3429..040bc18 100644 --- a/compiler/hir/hir.odin +++ b/compiler/hir/hir.odin @@ -96,6 +96,7 @@ Expr_Kind :: enum u8 { Orelse, Widen, C_Vararg_Promote, + Retype, Weaken_Pointer, Weaken_Slice, Decay_Array_Pointer, diff --git a/compiler/ir/ir.odin b/compiler/ir/ir.odin index e47affc..0d011a5 100644 --- a/compiler/ir/ir.odin +++ b/compiler/ir/ir.odin @@ -93,6 +93,7 @@ Opcode :: enum u8 { Orelse, Widen, C_Vararg_Promote, + Retype, Weaken_Pointer, Weaken_Slice, Decay_Array_Pointer, diff --git a/compiler/lexer/lexer.odin b/compiler/lexer/lexer.odin index 19a108f..9594482 100644 --- a/compiler/lexer/lexer.odin +++ b/compiler/lexer/lexer.odin @@ -18,6 +18,7 @@ keyword_kind :: proc(text: string) -> token.Kind { case "c_func": return .Keyword_C_Func case "struct": return .Keyword_Struct case "c_struct": return .Keyword_C_Struct + case "distinct": return .Keyword_Distinct case "import": return .Keyword_Import case "return": return .Keyword_Return case "mut": return .Keyword_Mut diff --git a/compiler/llvm/llvm.odin b/compiler/llvm/llvm.odin index f17ac6d..3a7e582 100644 --- a/compiler/llvm/llvm.odin +++ b/compiler/llvm/llvm.odin @@ -135,35 +135,39 @@ c_abi_result_type :: proc(value: types.Type, store: ^types.Store) -> string { } llvm_type :: proc(value: types.Type, store: ^types.Store = nil) -> string { - if types.is_void(value) { + resolved := value + if store != nil { + resolved = types.runtime_representation(value, store) + } + if types.is_void(resolved) { return "void" } - if types.is_bool(value) { + if types.is_bool(resolved) { return "i1" } - #partial switch types.kind(value, store) { + #partial switch types.kind(resolved, store) { case .Pointer: return "ptr" case .Slice: return "{ ptr, i64 }" case .Range: - item, _ := types.node(store, value) + item, _ := types.node(store, resolved) child := llvm_type(item.child, store) return fmt.tprintf("{{ %s, %s, i1 }}", child, child) case .Array: - item, _ := types.node(store, value) - return fmt.tprintf("[%d x %s]", types.physical_count(value, store), llvm_type(item.child, store)) + item, _ := types.node(store, resolved) + return fmt.tprintf("[%d x %s]", types.physical_count(resolved, store), llvm_type(item.child, store)) case .Optional: - item, _ := types.node(store, value) + item, _ := types.node(store, resolved) if types.is_pointer(item.child, store) { return "ptr" } return fmt.tprintf("{{ i1, %s }}", llvm_type(item.child, store)) case .Struct, .Union: - return fmt.tprintf("%%bro.type.%d", value) + return fmt.tprintf("%%bro.type.%d", resolved) } selected := store.selected if store != nil else target.DEFAULT - repr := types.representation(value, selected) + repr := types.representation(resolved, selected) if types.is_float(repr) { return "float" if types.bits(repr) == 32 else "double" } @@ -207,11 +211,15 @@ emit_function_result :: proc(builder: ^strings.Builder, function: ir.Function, s strings.write_string(builder, function_result_type(function, store)) } -sentinel :: proc(value_type: types.Type, selected := target.DEFAULT) -> i64 { - if types.is_bool(value_type) { +sentinel :: proc(value_type: types.Type, store: ^types.Store = nil, selected := target.DEFAULT) -> i64 { + repr := value_type + if store != nil { + repr = types.runtime_representation(value_type, store) + } + if types.is_bool(repr) { return 0 } - switch types.bits(value_type, selected) { + switch types.bits(repr, selected) { case 8: return -86 case 16: return -21846 case 32: return -1431655766 @@ -239,7 +247,7 @@ valid_value :: proc( .Load_Global, .Function_Address, .Address_Of, .Load, .Slice, .Length, .Slice_Ptr, .Extract, .Select, .Unwrap, .Optional_Is_Some, .Optional_Value, .Orelse, - .Widen, .C_Vararg_Promote, .Weaken_Pointer, .Weaken_Slice, .Decay_Array_Pointer, + .Widen, .C_Vararg_Promote, .Retype, .Weaken_Pointer, .Weaken_Slice, .Decay_Array_Pointer, .Neg_Checked, .Add_Checked, .Sub_Checked, .Mul_Checked, .Div_Checked, .Pointer_Add, .Not, .Compare, .Call: return true case .Address_Global, .Alloca, .Index_Address, .Field_Address, .Orelse_Begin, @@ -270,18 +278,22 @@ valid_address :: proc( } write_constant :: proc(builder: ^strings.Builder, value: i64, value_type: types.Type, store: ^types.Store = nil) { - if !types.is_concrete_scalar(value_type) { + resolved := value_type + if store != nil { + resolved = types.runtime_representation(value_type, store) + } + if !types.is_concrete_scalar(resolved) { strings.write_string(builder, "zeroinitializer") return } - if types.is_bool(value_type) { + if types.is_bool(resolved) { strings.write_string(builder, "true" if value != 0 else "false") return } selected := store.selected if store != nil else target.DEFAULT - if types.is_float(value_type, selected) { + if types.is_float(resolved, selected) { text := "" - if types.bits(value_type, selected) == 32 { + if types.bits(resolved, selected) == 32 { bits := u32(value) number := transmute(f32)bits text = fmt.tprintf("%.9g", number) @@ -306,7 +318,7 @@ write_operand :: proc( store: ^types.Store, ) { if !valid_value(instructions, value_id, expected, store) { - write_constant(builder, sentinel(expected, store.selected), expected, store) + write_constant(builder, sentinel(expected, store, store.selected), expected, store) return } value := instructions[value_id] @@ -378,7 +390,7 @@ emit_recovery_value :: proc(emitter: ^Emitter, instruction_id: int, instruction: " %%v%d = add %s 0, %d\n", instruction_id, llvm_type(instruction.type, &emitter.module.types), - sentinel(instruction.type, emitter.module.target), + sentinel(instruction.type, &emitter.module.types, emitter.module.target), ) return } @@ -388,9 +400,9 @@ emit_recovery_value :: proc(emitter: ^Emitter, instruction_id: int, instruction: instruction_id, llvm_type(instruction.type, &emitter.module.types), ) - write_constant(&emitter.builder, sentinel(instruction.type, emitter.module.target), instruction.type, &emitter.module.types) + write_constant(&emitter.builder, sentinel(instruction.type, &emitter.module.types, emitter.module.target), instruction.type, &emitter.module.types) fmt.sbprintf(&emitter.builder, ", %s ", llvm_type(instruction.type, &emitter.module.types)) - write_constant(&emitter.builder, sentinel(instruction.type, emitter.module.target), instruction.type, &emitter.module.types) + write_constant(&emitter.builder, sentinel(instruction.type, &emitter.module.types, emitter.module.target), instruction.type, &emitter.module.types) strings.write_string(&emitter.builder, "\n") } } @@ -1206,6 +1218,22 @@ emit_instruction_stream :: proc( fmt.sbprintf(&emitter.builder, " %%v%d = %s %s ", instruction_index, operation, llvm_type(from_type, &emitter.module.types)) write_operand(&emitter.builder, instructions, instruction.a, from_type, &emitter.module.types) fmt.sbprintf(&emitter.builder, " to %s\n", llvm_type(instruction.type, &emitter.module.types)) + case .Retype: + if !valid_instruction(instructions, instruction.a) || + !types.can_construct_distinct(instructions[instruction.a].type, instruction.type, &emitter.module.types) { + emit_recovery_value(emitter, instruction_index, instruction, "invalid distinct type construction") + continue + } + type_name := llvm_type(instruction.type, &emitter.module.types) + fmt.sbprintf(&emitter.builder, " %%v%d = select i1 true, %s ", instruction_index, type_name) + write_operand( + &emitter.builder, + instructions, + instruction.a, + instructions[instruction.a].type, + &emitter.module.types, + ) + fmt.sbprintf(&emitter.builder, ", %s zeroinitializer\n", type_name) case .Weaken_Pointer: if !valid_instruction(instructions, instruction.a) || !types.can_weaken_pointer(instructions[instruction.a].type, instruction.type, &emitter.module.types) { diff --git a/compiler/loader/loader.odin b/compiler/loader/loader.odin index 7db40f7..e86d942 100644 --- a/compiler/loader/loader.odin +++ b/compiler/loader/loader.odin @@ -1320,6 +1320,11 @@ canonical_type :: proc( mapping[index] = value if !types.is_valid(resolved) else resolved return mapping[index] } + if item.kind == .Distinct { + mapping[index] = value + module.type_store.nodes[index].child = canonical_type(module, item.child, mapping, visiting) + return value + } if item.kind == .Struct || item.kind == .Union { mapping[index] = value fields := types.fields_for(&module.type_store, value) diff --git a/compiler/lower/lower.odin b/compiler/lower/lower.odin index 110c922..9ec63f9 100644 --- a/compiler/lower/lower.odin +++ b/compiler/lower/lower.odin @@ -38,11 +38,12 @@ clone_args :: proc(values: []ir.Instruction_Id, allocator: mem.Allocator) -> []i return result } -sentinel :: proc(value_type: types.Type, selected := target.DEFAULT) -> i64 { - if types.is_float(value_type, selected) { - return i64(0x7fc0_0000) if types.bits(value_type, selected) == 32 else transmute(i64)u64(0x7ff8_0000_0000_0000) +sentinel :: proc(value_type: types.Type, store: ^types.Store, selected := target.DEFAULT) -> i64 { + repr := types.runtime_representation(value_type, store) + if types.is_float(repr, selected) { + return i64(0x7fc0_0000) if types.bits(repr, selected) == 32 else transmute(i64)u64(0x7ff8_0000_0000_0000) } - switch types.bits(value_type, selected) { + switch types.bits(repr, selected) { case 8: return -86 case 16: return -21846 case 32: return -1431655766 @@ -73,7 +74,7 @@ append_recovery_value :: proc( op=.Const, span=span, type=fallback, - integer=sentinel(fallback, state.hir_module.target), + integer=sentinel(fallback, &state.hir_module.types, state.hir_module.target), target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, @@ -466,7 +467,7 @@ lower_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instruction_Id { }) } _ = pop(&stack) - case .Widen, .C_Vararg_Promote, .Weaken_Pointer, .Weaken_Slice, .Decay_Array_Pointer: + case .Widen, .C_Vararg_Promote, .Retype, .Weaken_Pointer, .Weaken_Slice, .Decay_Array_Pointer: stack[frame_index].stage = 1 append(&stack, Lower_Expr_Frame{expr=expr.left}) case .Negate: @@ -524,6 +525,7 @@ lower_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instruction_Id { case .Weaken_Slice: op = .Weaken_Slice case .Decay_Array_Pointer: op = .Decay_Array_Pointer case .C_Vararg_Promote: op = .C_Vararg_Promote + case .Retype: op = .Retype case: op = .Widen } last = append_instruction(state, ir.Instruction{ @@ -1286,7 +1288,7 @@ lower_body :: proc(hir_module: ^hir.Module, function: hir.Function, allocator: m value := append_instruction(&state, ir.Instruction{ op=.Const, type=function.result, - integer=sentinel(function.result, hir_module.target), + integer=sentinel(function.result, &hir_module.types, hir_module.target), target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, diff --git a/compiler/parser/parser.odin b/compiler/parser/parser.odin index 92200c1..492269b 100644 --- a/compiler/parser/parser.odin +++ b/compiler/parser/parser.odin @@ -1477,6 +1477,19 @@ parse_struct :: proc(parser: ^Parser, name: token.Token, c_layout: bool) { _ = finish_statement(parser) } +parse_distinct :: proc(parser: ^Parser, name: token.Token) { + start := advance(parser) + child := parse_type(parser) + id := types.named(&parser.module.type_store, u32(parser.pkg), u32(name.symbol)) + if !types.define_distinct(&parser.module.type_store, id, child) { + source.addf(parser.diagnostics, name.span, "duplicate type declaration '%s'", token_text(parser, name)) + } + if !types.is_valid(child) { + source.add(parser.diagnostics, start.span, "distinct declarations require a backing type") + } + _ = finish_statement(parser) +} + decode_import_path :: proc(parser: ^Parser, tok: token.Token) -> string { text := token_text(parser, tok) if len(text) < 2 { @@ -1588,6 +1601,10 @@ parse_top_level :: proc(parser: ^Parser) { parse_struct(parser, name, current(parser).kind == .Keyword_C_Struct) return } + if operator.kind == .Colon_Colon && current(parser).kind == .Keyword_Distinct { + parse_distinct(parser, name) + return + } expr := parse_expression(parser) _ = ast.global_id(len(parser.module.globals)) diff --git a/compiler/token/token.odin b/compiler/token/token.odin index dce2368..5cb91d6 100644 --- a/compiler/token/token.odin +++ b/compiler/token/token.odin @@ -52,6 +52,7 @@ Kind :: enum u8 { Keyword_C_Func, Keyword_Struct, Keyword_C_Struct, + Keyword_Distinct, Keyword_Import, Keyword_Return, Keyword_Mut, diff --git a/compiler/types/types.odin b/compiler/types/types.odin index 185a68e..38e90fb 100644 --- a/compiler/types/types.odin +++ b/compiler/types/types.odin @@ -64,6 +64,7 @@ Kind :: enum u8 { Function, Named, Alias, + Distinct, Struct, Union, } @@ -128,7 +129,8 @@ clone_store :: proc(source: ^Store, allocator := context.allocator) -> Store { } intern :: proc(store: ^Store, candidate: Node) -> Type { - if candidate.kind != .Struct && candidate.kind != .Union && candidate.kind != .Named { + if candidate.kind != .Struct && candidate.kind != .Union && + candidate.kind != .Named && candidate.kind != .Distinct { for existing, index in store.nodes { if existing == candidate { return DYNAMIC_START+Type(index) @@ -143,7 +145,8 @@ intern :: proc(store: ^Store, candidate: Node) -> Type { named :: proc(store: ^Store, pkg, name: u32, qualifier: u32 = 0, file: u32 = 0xffff_ffff) -> Type { normalized_file := file if qualifier != 0 else u32(0) for existing, index in store.nodes { - if (existing.kind == .Named || existing.kind == .Alias || existing.kind == .Struct || existing.kind == .Union) && + if (existing.kind == .Named || existing.kind == .Alias || existing.kind == .Distinct || + existing.kind == .Struct || existing.kind == .Union) && existing.pkg == pkg && existing.name == name && existing.qualifier == qualifier && existing.file == normalized_file { return DYNAMIC_START+Type(index) @@ -154,7 +157,8 @@ named :: proc(store: ^Store, pkg, name: u32, qualifier: u32 = 0, file: u32 = 0xf find_named :: proc(store: ^Store, pkg, name: u32, qualifier: u32 = 0) -> Type { for existing, index in store.nodes { - if (existing.kind == .Named || existing.kind == .Alias || existing.kind == .Struct || existing.kind == .Union) && + if (existing.kind == .Named || existing.kind == .Alias || existing.kind == .Distinct || + existing.kind == .Struct || existing.kind == .Union) && existing.pkg == pkg && existing.name == name && existing.qualifier == qualifier { return DYNAMIC_START+Type(index) } @@ -174,6 +178,18 @@ define_alias :: proc(store: ^Store, id, child: Type) -> bool { return true } +define_distinct :: proc(store: ^Store, id, child: Type) -> bool { + existing, ok := node(store, id) + if !ok || existing.kind != .Named || existing.declared { + return false + } + index := int(id-DYNAMIC_START) + store.nodes[index].kind = .Distinct + store.nodes[index].child = child + store.nodes[index].declared = true + return true +} + define_record :: proc( store: ^Store, id: Type, @@ -440,6 +456,10 @@ is_union :: proc(value: Type, store: ^Store) -> bool { return kind(value, store) == .Union } +is_distinct :: proc(value: Type, store: ^Store) -> bool { + return kind(value, store) == .Distinct +} + resolve_alias :: proc(value: Type, store: ^Store, depth := 0) -> Type { if depth > 64 { return INVALID @@ -456,6 +476,9 @@ is_c_record_field_type :: proc(value: Type, store: ^Store, depth := 0) -> bool { return false } resolved := resolve_alias(value, store) + if contains_distinct(resolved, store) { + return false + } if is_concrete_scalar(resolved) || is_pointer(resolved, store) || is_optional_pointer(resolved, store) { return true } @@ -486,7 +509,10 @@ is_optional_pointer :: proc(value: Type, store: ^Store) -> bool { return ok && item.kind == .Optional && is_pointer(item.child, store) } -is_runtime_value :: proc(value: Type, store: ^Store) -> bool { +is_runtime_value :: proc(value: Type, store: ^Store, depth := 0) -> bool { + if depth > 256 { + return false + } value_kind := kind(value, store) if value_kind == .Scalar || value_kind == .Pointer { return true @@ -498,9 +524,29 @@ is_runtime_value :: proc(value: Type, store: ^Store) -> bool { item, ok := node(store, value) return ok && item.declared && !item.opaque && (!item.c_layout || item.field_count > 0) } + if value_kind == .Distinct { + item, ok := node(store, value) + return ok && item.declared && is_runtime_value(item.child, store, depth+1) + } return false } +can_construct_distinct :: proc(from, to: Type, store: ^Store) -> bool { + item, ok := node(store, to) + return ok && item.kind == .Distinct && item.declared && equal(from, item.child) +} + +runtime_representation :: proc(value: Type, store: ^Store, depth := 0) -> Type { + if depth > 256 { + return INVALID + } + item, ok := node(store, value) + if !ok || item.kind != .Distinct { + return value + } + return runtime_representation(item.child, store, depth+1) +} + contains_c_struct_by_value :: proc(value: Type, store: ^Store, depth := 0) -> bool { if depth > 256 { return true @@ -536,10 +582,41 @@ is_c_signature_type :: proc(value: Type, store: ^Store, allow_void := false) -> if allow_void && is_void(value) { return true } + if contains_distinct(value, store) { + return false + } return is_concrete_scalar(value) || is_pointer(value, store) || is_optional_pointer(value, store) || (is_c_struct(value, store) && is_runtime_value(value, store)) } +contains_distinct :: proc(value: Type, store: ^Store, depth := 0) -> bool { + if depth > 256 { + return true + } + item, ok := node(store, value) + if !ok { + return false + } + if item.kind == .Distinct { + return true + } + if item.kind == .Struct || item.kind == .Union { + for field in fields_for(store, value) { + if contains_distinct(field.type, store, depth+1) { + return true + } + } + } + if item.kind == .Function { + for param in params_for(store, value) { + if contains_distinct(param.type, store, depth+1) { + return true + } + } + } + return is_valid(item.child) && contains_distinct(item.child, store, depth+1) +} + is_c_integer_promotion_candidate :: proc(value: Type) -> bool { return value >= C_CHAR && value <= C_USHORT } @@ -832,6 +909,8 @@ size :: proc(value: Type, store: ^Store, selected := target.DEFAULT) -> u64 { return ((child_size+1+child_align-1)/child_align)*child_align case .Function: return 0 + case .Distinct: + return size(child_type(value, store), store, selected) case .Struct: item, _ := node(store, value) if item.explicit_size > 0 { @@ -873,6 +952,8 @@ alignment_of :: proc(value: Type, store: ^Store, selected := target.DEFAULT) -> return alignment_of(child_type(value, store), store, selected) case .Function: return 1 + case .Distinct: + return alignment_of(child_type(value, store), store, selected) case .Struct: item, _ := node(store, value) if item.explicit_alignment > 0 { diff --git a/compiler_tests.odin b/compiler_tests.odin index f8b20dd..e7d3c4c 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -5231,3 +5231,181 @@ checked_division_and_subtraction_emit_guarded_llvm :: proc(t: ^testing.T) { testing.expect(t, strings.contains(llvm_text, "divzero_trap")) testing.expect(t, strings.contains(llvm_text, "divovf_trap")) } + +@(test) +distinct_types_preserve_nominal_identity_and_backing_representation :: proc(t: ^testing.T) { + text := `Point :: struct { + x i32 + y i32 +} +UserID :: distinct u32 +OtherID :: distinct u32 +PointID :: distinct Point +Bytes :: distinct [2]u8 +WrappedID :: distinct UserID +static_id UserID :: UserID(42) +take :: func(value UserID) UserID { + return value +} +main :: func() i32 { + id UserID :: UserID(7) + copy UserID = take(id) + maybe ?UserID = copy + pointer @UserID = © + point PointID :: PointID(Point { x = 1, y = 2 }) + bytes Bytes :: Bytes([3, 4]) + wrapped WrappedID :: WrappedID(id) + _ = maybe + _ = pointer + _ = point + _ = bytes + _ = wrapped + return 0 +} +` + source_file := source.Source{path="test.bro", text=text} + diagnostics := source.init_diagnostics(&source_file) + defer source.destroy_diagnostics(&diagnostics) + symbols := symbol.init_table() + defer symbol.destroy_table(&symbols) + stream := lexer.lex(&source_file, &diagnostics, &symbols) + defer delete(stream.items) + ast_module := parser.parse(&stream, &source_file, &diagnostics) + defer ast.destroy_module(&ast_module) + hir_module := checker.check(&ast_module, &diagnostics, &symbols) + defer hir.destroy_module(&hir_module) + ir_module := lower.lower(&hir_module) + defer ir.destroy_module(&ir_module) + llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols) + defer delete(llvm_text) + + user_id := types.find_named(&ast_module.type_store, 0, u32(symbol.intern(&symbols, "UserID"))) + other_id := types.find_named(&ast_module.type_store, 0, u32(symbol.intern(&symbols, "OtherID"))) + point_id := types.find_named(&ast_module.type_store, 0, u32(symbol.intern(&symbols, "PointID"))) + point := types.find_named(&ast_module.type_store, 0, u32(symbol.intern(&symbols, "Point"))) + testing.expect_value(t, len(diagnostics.items), 0) + testing.expect(t, user_id != other_id) + testing.expect(t, user_id != types.U32) + testing.expect(t, types.is_distinct(user_id, &ast_module.type_store)) + testing.expect_value(t, types.runtime_representation(user_id, &ast_module.type_store), types.U32) + testing.expect_value(t, types.runtime_representation(point_id, &ast_module.type_store), point) + testing.expect_value(t, types.size(user_id, &ast_module.type_store), types.size(types.U32, &ast_module.type_store)) + testing.expect(t, hir_module.globals[0].is_static) + testing.expect_value(t, hir_module.globals[0].static_value, i64(42)) + testing.expect(t, strings.contains(llvm_text, "@bro.g.0 = internal constant i32 42")) + testing.expect(t, strings.contains(llvm_text, "select i1 true, i32")) + + retype_count := 0 + for function in ir_module.functions { + for instruction in function.instructions { + retype_count += 1 if instruction.op == .Retype else 0 + } + } + testing.expect_value(t, retype_count, 4) +} + +@(test) +distinct_types_reject_implicit_conversions_operators_and_invalid_backings :: proc(t: ^testing.T) { + text := `Opaque :: c_struct +UserID :: distinct u32 +OtherID :: distinct u32 +BadInt :: distinct int +BadVoid :: distinct void +BadFunction :: distinct c_func() void +BadOpaque :: distinct Opaque +foreign :: c_func(value UserID) void +foreign_pointer :: c_func(value @UserID) void +main :: func() void { + raw u32 = 1 + id UserID = raw + backing u32 = UserID(2) + other OtherID = UserID(3) + narrow u8 = 4 + _ = UserID(narrow) + _ = UserID() + _ = UserID(1, 2) + left UserID :: UserID(5) + right UserID :: UserID(6) + _ = left + right + _ = left == right +} +` + source_file := source.Source{path="test.bro", text=text} + diagnostics := source.init_diagnostics(&source_file) + defer source.destroy_diagnostics(&diagnostics) + symbols := symbol.init_table() + defer symbol.destroy_table(&symbols) + stream := lexer.lex(&source_file, &diagnostics, &symbols) + defer delete(stream.items) + ast_module := parser.parse(&stream, &source_file, &diagnostics) + defer ast.destroy_module(&ast_module) + hir_module := checker.check(&ast_module, &diagnostics, &symbols) + defer hir.destroy_module(&hir_module) + + invalid_backing_count := 0 + implicit_conversion_count := 0 + found_exact := false + found_arity := false + found_arithmetic := false + found_comparison := false + foreign_signature_count := 0 + for diagnostic in diagnostics.items { + invalid_backing_count += 1 if strings.contains(diagnostic.message, "requires a concrete runtime backing type") else 0 + implicit_conversion_count += 1 if strings.contains(diagnostic.message, "cannot implicitly convert") else 0 + found_exact = found_exact || strings.contains(diagnostic.message, "requires an exact u32 value, got u8") + found_arity = found_arity || strings.contains(diagnostic.message, "expects 1 argument") + found_arithmetic = found_arithmetic || strings.contains(diagnostic.message, "arithmetic requires compatible numeric operands") + found_comparison = found_comparison || strings.contains(diagnostic.message, "comparison requires compatible numeric operands") + foreign_signature_count += 1 if strings.contains(diagnostic.message, "requires concrete parameter types") else 0 + } + testing.expect_value(t, invalid_backing_count, 4) + testing.expect(t, implicit_conversion_count >= 3) + testing.expect(t, found_exact) + testing.expect(t, found_arity) + testing.expect(t, found_arithmetic) + testing.expect(t, found_comparison) + testing.expect_value(t, foreign_signature_count, 2) +} + +@(test) +distinct_type_construction_defers_to_callable_names :: proc(t: ^testing.T) { + text := `Value :: distinct u32 +Value :: func(value i32) i32 { + return value +} +main :: func() i32 { + return Value(42) +} +` + source_file := source.Source{path="test.bro", text=text} + diagnostics := source.init_diagnostics(&source_file) + defer source.destroy_diagnostics(&diagnostics) + symbols := symbol.init_table() + defer symbol.destroy_table(&symbols) + stream := lexer.lex(&source_file, &diagnostics, &symbols) + defer delete(stream.items) + ast_module := parser.parse(&stream, &source_file, &diagnostics) + defer ast.destroy_module(&ast_module) + hir_module := checker.check(&ast_module, &diagnostics, &symbols) + defer hir.destroy_module(&hir_module) + + found_call := false + found_retype := false + for expr in hir_module.exprs { + found_call = found_call || expr.kind == .Call + found_retype = found_retype || expr.kind == .Retype + } + testing.expect_value(t, len(diagnostics.items), 0) + testing.expect(t, found_call) + testing.expect(t, !found_retype) +} + +@(test) +distinct_types_compile_and_run_across_packages :: proc(t: ^testing.T) { + output := "/tmp/brolang-test-distinct-types" + defer _ = os.remove(output) + status := compiler_core.compile_package("examples/programs/distinct_types", output) + testing.expect_value(t, status, 0) + state := run_executable(output) + testing.expect_value(t, state.exit_code, 0) +} diff --git a/examples/programs/distinct_types/ids/ids.bro b/examples/programs/distinct_types/ids/ids.bro new file mode 100644 index 0000000..a7cdd58 --- /dev/null +++ b/examples/programs/distinct_types/ids/ids.bro @@ -0,0 +1,5 @@ +UserID :: distinct u32 + +make :: func(value u32) UserID { + return UserID(value) +} diff --git a/examples/programs/distinct_types/main.bro b/examples/programs/distinct_types/main.bro new file mode 100644 index 0000000..370a064 --- /dev/null +++ b/examples/programs/distinct_types/main.bro @@ -0,0 +1,38 @@ +ids :: import "./ids" + +Point :: struct { + x i32 + y i32 +} + +LocalID :: distinct u32 +PointID :: distinct Point +Bytes :: distinct [2]u8 +WrappedID :: distinct LocalID + +static_id LocalID :: LocalID(42) + +take :: func(value LocalID) LocalID { + return value +} + +main :: func() i32 { + id LocalID :: LocalID(7) + copy LocalID = take(id) + maybe ?LocalID = copy + pointer @LocalID = © + point PointID :: PointID(Point { x = 1, y = 2 }) + bytes Bytes :: Bytes([3, 4]) + wrapped WrappedID :: WrappedID(id) + remote ids.UserID :: ids.UserID(8) + remote_copy ids.UserID :: ids.make(9) + _ = static_id + _ = maybe + _ = pointer + _ = point + _ = bytes + _ = wrapped + _ = remote + _ = remote_copy + return 0 +}