From 2f68fc966a91ca07cb52fd386a841e2d3a921e37 Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Tue, 23 Jun 2026 17:22:41 +0200 Subject: [PATCH] enums --- TODO.md | 25 +- compiler/ast/ast.odin | 1 + compiler/checker/checker.odin | 151 +++++++++++- compiler/cimport/libclang.odin | 70 +++++- compiler/lexer/lexer.odin | 1 + compiler/llvm/llvm.odin | 36 +-- compiler/loader/loader.odin | 5 + compiler/parser/parser.odin | 131 +++++++++++ compiler/token/token.odin | 1 + compiler/types/types.odin | 85 +++++-- compiler_tests.odin | 241 ++++++++++++++++++++ examples/interop/header/app/main.bro | 12 + examples/interop/header/include/native.h | 17 +- examples/interop/header/native.c | 33 +++ examples/programs/enums/animals/animals.bro | 8 + examples/programs/enums/main.bro | 30 +++ 16 files changed, 809 insertions(+), 38 deletions(-) create mode 100644 examples/programs/enums/animals/animals.bro create mode 100644 examples/programs/enums/main.bro diff --git a/TODO.md b/TODO.md index 59f0965..a1a317a 100644 --- a/TODO.md +++ b/TODO.md @@ -125,7 +125,14 @@ floats follow IEEE (`fadd`/`fsub`/`fmul`/`fdiv`, no trap) - constant folding (global initializers) covers `-`, `*`, `/` alongside `+` -7. enums (native and c interop) (see below) +7. enums (native and c interop) (implemented; see below) + - native enums are nominal value types with integer runtime representations + - unbacked enums are non-empty, dense, zero-based, and use the smallest fitting unsigned backing + - explicitly backed enums require an integer type and strictly increasing literal values + - enum members support `Type.member`, `package.Type.member`, and contextual `.member` + - enum values support storage, calls/returns, and same-type equality/inequality + - explicitly backed native enums use their backing ABI in `c_func` signatures and variadic promotion + - imported C enum types alias libclang's target-selected integer backing and enumerators import as package constants 8. distinct types (implemented; see below) - nominal declarations preserve identity across packages and reuse the backing runtime representation @@ -255,3 +262,19 @@ Nat :: enum(u8) { dog_tag1 Animal :: Animal.dog dog_tag2 Animal :: .dog # type inferred ``` + +Unbacked enums cannot assign explicit values. Backed enum values must be decimal integer +literals, fit the backing type, and increase strictly; gaps are allowed. + +Native enum types remain distinct from integers and from other enum types. They support +`==` and `!=`, but not arithmetic, ordering, casts, or backing-value extraction. + +C enums follow C/Zig import semantics rather than native enum semantics: + +``` +native :: import "native.h" +value native.Imported_Enum :: native.IMPORTED_ENUM_VALUE +``` + +The imported enum type is an alias of its target-selected C integer backing, and imported +enumerators are package-level constants. diff --git a/compiler/ast/ast.odin b/compiler/ast/ast.odin index 1835556..22e0ed3 100644 --- a/compiler/ast/ast.odin +++ b/compiler/ast/ast.odin @@ -72,6 +72,7 @@ Expr_Kind :: enum u8 { Array, None, Name, + Enum_Literal, Address, Deref, Index, diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index 850b35d..68646a3 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -636,7 +636,7 @@ mark_expr_imports_used :: proc(checker: ^Checker, expr_id: ast.Expr_Id, file: as append(&stack, expr.left) case .Add, .Sub, .Mul, .Div, .Index, .Orelse, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range: append(&stack, expr.left, expr.right) - case .Invalid, .Integer, .Float, .String, .Bool, .None, .Name: + case .Invalid, .Integer, .Float, .String, .Bool, .None, .Name, .Enum_Literal: } } } @@ -879,6 +879,28 @@ validate_type_nodes :: proc(checker: ^Checker) { symbol_text(checker, symbol.Id(item.name)), ) } + if item.kind == .Enum { + if !item.declared || !types.is_concrete_integer(item.child) { + source.addf( + checker.diagnostics, + source.Span{}, + "enum type '%s' requires a concrete integer backing type", + symbol_text(checker, symbol.Id(item.name)), + ) + } else { + for member in types.enum_members_for(&checker.module.types, id) { + if !fits_integer_type(member.value, item.child, checker.target) { + source.addf( + checker.diagnostics, + source.Span{}, + "enum value %d does not fit in %s", + member.value, + types.name(item.child), + ) + } + } + } + } if item.has_sentinel { value := i128(item.sentinel) if types.is_signed(item.child, checker.target) { @@ -1101,6 +1123,8 @@ infer_compound_expr :: proc( return types.array(store, element, u64(len(expr.args)), false) case .None: return types.INVALID + case .Enum_Literal: + return types.INVALID case .Address: child := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded) return types.pointer(store, child, false, false) @@ -1126,6 +1150,10 @@ infer_compound_expr :: proc( preserve := item.has_sentinel && expr.args[1] == ast.INVALID_EXPR return types.slice(store, item.child, item.mutable, preserve, item.sentinel) case .Field: + if enum_type, enum_ok := enum_type_from_field_expr(checker, expr, pkg, file); enum_ok { + _, member_ok := find_enum_member(checker, enum_type, expr.name) + return enum_type if member_ok else types.INVALID + } value := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded) field_name := symbol_text(checker, expr.name) item, has_item := types.container(value, store) @@ -1220,7 +1248,7 @@ infer_expr :: proc( last = types.F64 _ = pop(&stack) case .String, .Array, .None, .Address, .Deref, .Index, .Slice, - .Field, .Unwrap, .Orelse, .Struct_Literal, .Keyed, + .Field, .Unwrap, .Orelse, .Struct_Literal, .Keyed, .Enum_Literal, .Bool, .Not, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range: last = infer_compound_expr(checker, expr, locals, pkg, file, demanded) _ = pop(&stack) @@ -1250,6 +1278,14 @@ infer_expr :: proc( } } } + if !types.is_valid(last) { + if enum_type, enum_ok := enum_type_from_name_expr(checker, expr, pkg, file); enum_ok { + _, member_ok := find_enum_member(checker, enum_type, expr.name) + if member_ok { + last = enum_type + } + } + } if !types.is_valid(last) { target_pkg, available := expr_package(checker, expr, pkg, file) if available { @@ -1867,7 +1903,7 @@ promote_c_vararg_expr :: proc(checker: ^Checker, expr_id: hir.Expr_Id, span: sou ) return invalid_hir_expr(checker, span, id, types.C_INT) } - promoted := types.c_vararg_promotion(actual, checker.target) + promoted := types.c_vararg_promotion(actual, checker.target, &checker.module.types) if types.equal(actual, promoted) { return expr_id } @@ -2055,6 +2091,75 @@ find_struct_field :: proc(checker: ^Checker, struct_type: types.Type, name: symb return 0, {}, false } +find_enum_member :: proc(checker: ^Checker, enum_type: types.Type, name: symbol.Id) -> (types.Enum_Member, bool) { + for member in types.enum_members_for(&checker.module.types, enum_type) { + if member.name == u32(name) { + return member, true + } + } + return {}, false +} + +enum_type_from_name_expr :: proc( + checker: ^Checker, + expr: ast.Expr, + pkg: ast.Package_Id, + file: ast.File_Id, +) -> (types.Type, bool) { + if !symbol.is_valid(expr.qualifier) || + find_import(checker, file, expr.qualifier) != ast.INVALID_IMPORT { + return types.INVALID, false + } + enum_type := types.find_named(&checker.module.types, u32(pkg), u32(expr.qualifier)) + return enum_type, types.is_enum(enum_type, &checker.module.types) +} + +enum_type_from_field_expr :: proc( + checker: ^Checker, + expr: ast.Expr, + pkg: ast.Package_Id, + file: ast.File_Id, + mark_used := false, +) -> (types.Type, bool) { + if expr.left == ast.INVALID_EXPR || int(expr.left) >= len(checker.ast_module.exprs) { + return types.INVALID, false + } + base := checker.ast_module.exprs[expr.left] + if base.kind != .Name || !symbol.is_valid(base.qualifier) { + return types.INVALID, false + } + target_pkg, available := expr_package(checker, base, pkg, file, mark_used) + if !available { + return types.INVALID, false + } + enum_type := types.find_named(&checker.module.types, u32(target_pkg), u32(base.name)) + return enum_type, types.is_enum(enum_type, &checker.module.types) +} + +enum_member_hir :: proc( + checker: ^Checker, + enum_type: types.Type, + name: symbol.Id, + span: source.Span, +) -> hir.Expr_Id { + member, ok := find_enum_member(checker, enum_type, name) + if !ok { + id := source.addf(checker.diagnostics, span, "unknown enum member '%s'", symbol_text(checker, name)) + return invalid_hir_expr(checker, span, id, enum_type) + } + value := i64(member.value) if member.value < 0 else transmute(i64)u64(member.value) + return add_hir_expr(checker, hir.Expr{ + kind=.Integer, + span=span, + type=enum_type, + integer=value, + target=hir.INVALID_REF, + left=hir.INVALID_EXPR, + right=hir.INVALID_EXPR, + diagnostic=source.INVALID_DIAGNOSTIC, + }) +} + build_function_value :: proc( checker: ^Checker, template: ast.Function_Id, @@ -2213,6 +2318,17 @@ build_compound_expr :: proc( kind=.None, span=expr.span, type=expected, target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) + case .Enum_Literal: + if !types.is_enum(expected, store) { + id := source.addf( + checker.diagnostics, + expr.span, + "'.%s' requires an enum context", + symbol_text(checker, expr.name), + ) + return invalid_hir_expr(checker, expr.span, id, expected) + } + return enum_member_hir(checker, expected, expr.name, expr.span) case .Address: value := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file) if !hir_is_location(checker, value) { @@ -2278,6 +2394,9 @@ build_compound_expr :: proc( target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) case .Field: + if enum_type, enum_ok := enum_type_from_field_expr(checker, expr, pkg, file, true); enum_ok { + return enum_member_hir(checker, enum_type, expr.name, expr.span) + } base := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file) base_type := checker.module.exprs[base].type field_name := symbol_text(checker, expr.name) @@ -2417,7 +2536,15 @@ build_compound_expr :: proc( left_const := eval_constant(checker, expr.left) right_const := eval_constant(checker, expr.right) left, right: hir.Expr_Id - if right_const.kind == .Value && left_const.kind != .Value { + left_expr := checker.ast_module.exprs[expr.left] + right_expr := checker.ast_module.exprs[expr.right] + if right_expr.kind == .Enum_Literal && left_expr.kind != .Enum_Literal { + left = build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file) + right = build_nested_expr(checker, expr.right, locals, global_reads, calls, checker.module.exprs[left].type, pkg, file) + } else if left_expr.kind == .Enum_Literal && right_expr.kind != .Enum_Literal { + right = build_nested_expr(checker, expr.right, locals, global_reads, calls, types.INVALID, pkg, file) + left = build_nested_expr(checker, expr.left, locals, global_reads, calls, checker.module.exprs[right].type, pkg, file) + } else if right_const.kind == .Value && left_const.kind != .Value { left = build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file) hint := checker.module.exprs[left].type right = build_nested_expr(checker, expr.right, locals, global_reads, calls, hint, pkg, file) @@ -2435,7 +2562,13 @@ build_compound_expr :: proc( return invalid_hir_expr(checker, expr.span, expr.diagnostic, types.BOOL) } operand_type := types.INVALID - if types.is_bool(left_type) && types.is_bool(right_type) { + if types.is_enum(left_type, store) || types.is_enum(right_type, store) { + if !types.equal(left_type, right_type) || (expr.kind != .Eq && expr.kind != .Ne) { + id := source.add(checker.diagnostics, expr.span, "enum values only support '==' and '!=' with the same enum type") + return invalid_hir_expr(checker, expr.span, id, types.BOOL) + } + operand_type = left_type + } else if types.is_bool(left_type) && types.is_bool(right_type) { if expr.kind != .Eq && expr.kind != .Ne { id := source.add(checker.diagnostics, expr.span, "bool values only support '==' and '!='") return invalid_hir_expr(checker, expr.span, id, types.BOOL) @@ -2617,7 +2750,8 @@ build_expr :: proc( switch expr.kind { case .String, .Array, .None, .Address, .Deref, .Index, .Slice, .Field, .Unwrap, .Orelse, .Struct_Literal, .Keyed, - .Bool, .Not, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range: + .Bool, .Not, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range, + .Enum_Literal: last = build_compound_expr( checker, expr, locals, global_reads, calls, frame.expected, pkg, file, ) @@ -2674,6 +2808,11 @@ build_expr :: proc( }) } } + if last == hir.INVALID_EXPR { + if enum_type, enum_ok := enum_type_from_name_expr(checker, expr, pkg, file); enum_ok { + last = enum_member_hir(checker, enum_type, expr.name, expr.span) + } + } if last == hir.INVALID_EXPR { target_pkg, available := expr_package(checker, expr, pkg, file, true) if !available { diff --git a/compiler/cimport/libclang.odin b/compiler/cimport/libclang.odin index 1b79ab7..f669561 100644 --- a/compiler/cimport/libclang.odin +++ b/compiler/cimport/libclang.odin @@ -81,6 +81,9 @@ Api :: struct { get_type_spelling: proc "c"(CXType) -> CXString, get_typedef_underlying_type: proc "c"(CXCursor) -> CXType, get_type_declaration: proc "c"(CXType) -> CXCursor, + get_enum_decl_integer_type: proc "c"(CXCursor) -> CXType, + get_enum_constant_value: proc "c"(CXCursor) -> i64, + get_enum_constant_unsigned: proc "c"(CXCursor) -> u64, get_canonical_type: proc "c"(CXType) -> CXType, get_pointee_type: proc "c"(CXType) -> CXType, get_array_element_type: proc "c"(CXType) -> CXType, @@ -119,6 +122,7 @@ CXCursor_VarDecl :: i32(9) CXCursor_TypedefDecl :: i32(20) CXCursor_MacroDefinition :: i32(501) CXCursor_FieldDecl :: i32(6) +CXCursor_EnumConstantDecl :: i32(7) CXLinkage_External :: i32(4) CXTLS_None :: i32(0) @@ -201,6 +205,9 @@ load_api_from :: proc(path: string) -> (Api, bool) { load_proc(&api, "clang_getTypeSpelling", &api.get_type_spelling) && load_proc(&api, "clang_getTypedefDeclUnderlyingType", &api.get_typedef_underlying_type) && load_proc(&api, "clang_getTypeDeclaration", &api.get_type_declaration) && + load_proc(&api, "clang_getEnumDeclIntegerType", &api.get_enum_decl_integer_type) && + load_proc(&api, "clang_getEnumConstantDeclValue", &api.get_enum_constant_value) && + load_proc(&api, "clang_getEnumConstantDeclUnsignedValue", &api.get_enum_constant_unsigned) && load_proc(&api, "clang_getCanonicalType", &api.get_canonical_type) && load_proc(&api, "clang_getPointeeType", &api.get_pointee_type) && load_proc(&api, "clang_getArrayElementType", &api.get_array_element_type) && @@ -468,6 +475,10 @@ translate_type :: proc(ctx: ^Context, value: CXType, preferred_record_name := "" record := add_record(ctx, declaration, preferred_record_name) populate_record(ctx, record, declaration) return add_type(ctx, Type{kind=.Record, child=INVALID_TYPE, record=record}) + case CXType_Enum: + declaration := ctx.api.get_type_declaration(value) + backing := ctx.api.get_enum_decl_integer_type(declaration) + return translate_type(ctx, backing, preferred_record_name, depth+1) case CXType_FunctionProto: result := translate_type(ctx, ctx.api.get_result_type(value), "", depth+1) if result == INVALID_TYPE { @@ -508,7 +519,7 @@ translate_type :: proc(ctx: ^Context, value: CXType, preferred_record_name := "" return INVALID_TYPE } return translate_type(ctx, canonical, preferred_record_name, depth+1) - case CXType_Enum, CXType_FunctionNoProto, + case CXType_FunctionNoProto, CXType_IncompleteArray, CXType_VariableArray, CXType_DependentSizedArray: return INVALID_TYPE } @@ -551,6 +562,49 @@ add_alias :: proc(ctx: ^Context, name: string, value: Type_Id, reason := "") { }) } +enum_backing_unsigned :: proc(value: CXType) -> bool { + switch value.kind { + case CXType_Char_U, CXType_UChar, CXType_UShort, CXType_UInt, CXType_ULong, CXType_ULongLong: + return true + } + return false +} + +Enum_Constant_Context :: struct { + ctx: ^Context, + backing: Type_Id, + unsigned: bool, +} + +visit_enum_constant :: proc "c"(cursor, parent: CXCursor, client_data: rawptr) -> i32 { + context = runtime.default_context() + enum_ctx := (^Enum_Constant_Context)(client_data) + ctx := enum_ctx.ctx + if ctx.api.get_cursor_kind(cursor) != CXCursor_EnumConstantDecl { + return CXChildVisit_Continue + } + name := clone_cx_string(ctx.api, ctx.api.get_cursor_spelling(cursor), ctx.allocator) + defer delete(name, ctx.allocator) + if len(name) == 0 { + return CXChildVisit_Continue + } + value := Macro_Value{kind=.Integer, type=enum_ctx.backing} + if enum_ctx.unsigned { + value.integer = ctx.api.get_enum_constant_unsigned(cursor) + } else { + signed := ctx.api.get_enum_constant_value(cursor) + value.negative = signed < 0 + value.integer = u64(-i128(signed)) if signed < 0 else u64(signed) + } + append(&ctx.result.macros, Macro_Constant{ + name=fmt.aprintf("%s", name, allocator=ctx.allocator), + type=enum_ctx.backing, + value=value, + reason=fmt.aprintf("", allocator=ctx.allocator), + }) + return CXChildVisit_Continue +} + is_macro_identifier :: proc(value: string) -> bool { if len(value) == 0 { return false @@ -1367,7 +1421,19 @@ visit_cursor :: proc "c"(cursor, parent: CXCursor, client_data: rawptr) -> i32 { add_alias(ctx, name, value) } case CXCursor_EnumDecl: - add_unsupported(ctx, name, "C enums are not supported") + backing_c := ctx.api.get_enum_decl_integer_type(cursor) + backing := translate_type(ctx, backing_c) + if backing == INVALID_TYPE { + add_unsupported(ctx, name, "C enum backing type is not supported") + break + } + add_alias(ctx, name, backing) + enum_ctx := Enum_Constant_Context{ + ctx=ctx, + backing=backing, + unsigned=enum_backing_unsigned(backing_c), + } + _ = ctx.api.visit_children(cursor, visit_enum_constant, &enum_ctx) case CXCursor_VarDecl: if len(name) == 0 { return CXChildVisit_Continue diff --git a/compiler/lexer/lexer.odin b/compiler/lexer/lexer.odin index 9594482..27c700a 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 "enum": return .Keyword_Enum case "distinct": return .Keyword_Distinct case "import": return .Keyword_Import case "return": return .Keyword_Return diff --git a/compiler/llvm/llvm.odin b/compiler/llvm/llvm.odin index 3a7e582..3b6aadb 100644 --- a/compiler/llvm/llvm.odin +++ b/compiler/llvm/llvm.odin @@ -189,11 +189,16 @@ function_result_type :: proc(function: ir.Function, store: ^types.Store) -> stri return llvm_type(function.result, store) } -c_abi_extension :: proc(value: types.Type, selected: target.Target) -> string { - if !types.is_concrete_integer(value) { +c_abi_extension :: proc(value: types.Type, store: ^types.Store) -> string { + resolved := types.runtime_representation(value, store) + if !types.is_concrete_integer(resolved) { return "" } - switch target.c_integer_extension(selected, types.bits(value, selected), types.is_signed(value, selected)) { + switch target.c_integer_extension( + store.selected, + types.bits(resolved, store.selected), + types.is_signed(resolved, store.selected), + ) { case .Sign: return "signext" case .Zero: return "zeroext" case .None: return "" @@ -203,7 +208,7 @@ c_abi_extension :: proc(value: types.Type, selected: target.Target) -> string { emit_function_result :: proc(builder: ^strings.Builder, function: ir.Function, store: ^types.Store) { if function.calling_convention == .C { - extension := c_abi_extension(function.result, store.selected) + extension := c_abi_extension(function.result, store) if len(extension) > 0 { fmt.sbprintf(builder, "%s ", extension) } @@ -423,7 +428,7 @@ emit_call_args :: proc( (instructions[arg].type if valid_instruction(instructions, arg) else types.INVALID) fmt.sbprintf(builder, "%s ", llvm_type(arg_type, store)) if c_abi && index < len(param_types) { - extension := c_abi_extension(arg_type, store.selected) + extension := c_abi_extension(arg_type, store) if len(extension) > 0 { fmt.sbprintf(builder, "%s ", extension) } @@ -1200,11 +1205,12 @@ emit_instruction_stream :: proc( } from_type := instructions[instruction.a].type if types.equal(from_type, instruction.type) || - !types.equal(types.c_vararg_promotion(from_type, emitter.module.target), instruction.type) { + !types.equal(types.c_vararg_promotion(from_type, emitter.module.target, &emitter.module.types), instruction.type) { emit_recovery_value(emitter, instruction_index, instruction, "invalid C variadic promotion operand") continue } - if types.bits(from_type, emitter.module.target) == types.bits(instruction.type, emitter.module.target) { + from_repr := types.runtime_representation(from_type, &emitter.module.types) + if types.bits(from_repr, emitter.module.target) == types.bits(instruction.type, emitter.module.target) { 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, from_type, &emitter.module.types) @@ -1213,8 +1219,8 @@ emit_instruction_stream :: proc( strings.write_string(&emitter.builder, "\n") continue } - operation := "fpext" if types.is_float(from_type, emitter.module.target) else - ("sext" if types.is_signed(from_type, emitter.module.target) else "zext") + operation := "fpext" if types.is_float(from_repr, emitter.module.target) else + ("sext" if types.is_signed(from_repr, emitter.module.target) else "zext") 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)) @@ -1367,7 +1373,7 @@ emit_instruction_stream :: proc( (instructions[arg].type if valid_instruction(instructions, arg) else types.INVALID) if index >= len(param_fields) && (!types.is_c_vararg_type(expected, &emitter.module.types) || - !types.equal(types.c_vararg_promotion(expected, emitter.module.target), expected)) { + !types.equal(types.c_vararg_promotion(expected, emitter.module.target, &emitter.module.types), expected)) { valid_args = false break } @@ -1409,7 +1415,7 @@ emit_instruction_stream :: proc( strings.write_string(&emitter.builder, "fastcc ") } if function_item.c_abi { - extension := c_abi_extension(function_item.child, emitter.module.target) + extension := c_abi_extension(function_item.child, &emitter.module.types) if len(extension) > 0 { fmt.sbprintf(&emitter.builder, "%s ", extension) } @@ -1455,7 +1461,7 @@ emit_instruction_stream :: proc( } else { fmt.sbprintf(&emitter.builder, "%s ", llvm_type(arg_type, &emitter.module.types)) if function_item.c_abi && fixed { - extension := c_abi_extension(arg_type, emitter.module.target) + extension := c_abi_extension(arg_type, &emitter.module.types) if len(extension) > 0 { fmt.sbprintf(&emitter.builder, "%s ", extension) } @@ -1490,7 +1496,7 @@ emit_instruction_stream :: proc( (instructions[arg].type if valid_instruction(instructions, arg) else types.INVALID) if index >= len(target.param_types) && (!types.is_c_vararg_type(expected, &emitter.module.types) || - !types.equal(types.c_vararg_promotion(expected, emitter.module.target), expected)) { + !types.equal(types.c_vararg_promotion(expected, emitter.module.target, &emitter.module.types), expected)) { valid_args = false break } @@ -1572,7 +1578,7 @@ emit_instruction_stream :: proc( } else { fmt.sbprintf(&emitter.builder, "%s ", llvm_type(arg_type, &emitter.module.types)) if target.calling_convention == .C && fixed { - extension := c_abi_extension(arg_type, emitter.module.target) + extension := c_abi_extension(arg_type, &emitter.module.types) if len(extension) > 0 { fmt.sbprintf(&emitter.builder, "%s ", extension) } @@ -1898,7 +1904,7 @@ emit_functions :: proc(emitter: ^Emitter) { type_name := c_abi_param_type(param_type, &emitter.module.types) if function.calling_convention == .C else llvm_type(param_type, &emitter.module.types) fmt.sbprintf(&emitter.builder, "%s", type_name) if function.calling_convention == .C && !types.is_record(param_type, &emitter.module.types) { - extension := c_abi_extension(param_type, emitter.module.target) + extension := c_abi_extension(param_type, &emitter.module.types) if len(extension) > 0 { fmt.sbprintf(&emitter.builder, " %s", extension) } diff --git a/compiler/loader/loader.odin b/compiler/loader/loader.odin index e86d942..cce81c5 100644 --- a/compiler/loader/loader.odin +++ b/compiler/loader/loader.odin @@ -1325,6 +1325,11 @@ canonical_type :: proc( module.type_store.nodes[index].child = canonical_type(module, item.child, mapping, visiting) return value } + if item.kind == .Enum { + 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/parser/parser.odin b/compiler/parser/parser.odin index 492269b..919c192 100644 --- a/compiler/parser/parser.odin +++ b/compiler/parser/parser.odin @@ -588,6 +588,21 @@ parse_primary :: proc(parser: ^Parser, nesting: int) -> ast.Expr_Id { }) case .Left_Bracket: return parse_array_literal(parser, nesting) + case .Dot: + start := advance(parser) + member := current(parser) + if member.kind != .Identifier { + return invalid_expr(parser, member.span, "expected an enum member after '.'") + } + advance(parser) + return add_expr(parser, ast.Expr{ + kind=.Enum_Literal, + span=span_from(start.span, member.span), + name=member.symbol, + left=ast.INVALID_EXPR, + right=ast.INVALID_EXPR, + diagnostic=source.INVALID_DIAGNOSTIC, + }) case .Identifier: first := advance(parser) name := first @@ -1490,6 +1505,118 @@ parse_distinct :: proc(parser: ^Parser, name: token.Token) { _ = finish_statement(parser) } +parse_enum :: proc(parser: ^Parser, name: token.Token) { + start := advance(parser) + explicit_backing := false + backing := types.INVALID + if _, ok := allow(parser, .Left_Paren); ok { + explicit_backing = true + backing = parse_type(parser) + if _, close_ok := allow(parser, .Right_Paren); !close_ok { + source.add(parser.diagnostics, current(parser).span, "expected ')' after enum backing type") + } + } + skip_newlines(parser) + if _, ok := allow(parser, .Left_Brace); !ok { + source.add(parser.diagnostics, current(parser).span, "expected '{' after enum declaration") + _ = finish_statement(parser) + return + } + members: [dynamic]types.Enum_Member + members.allocator = parser.module.allocator + defer delete(members) + next_value: i128 + previous_value: i128 + has_previous := false + skip_newlines(parser) + for current(parser).kind != .Right_Brace && current(parser).kind != .Eof { + if current(parser).kind != .Identifier { + source.add(parser.diagnostics, current(parser).span, "expected an enum member name") + for current(parser).kind != .Newline && + current(parser).kind != .Right_Brace && + current(parser).kind != .Eof { + advance(parser) + } + skip_newlines(parser) + continue + } + member := advance(parser) + duplicate := false + for existing in members { + if existing.name == u32(member.symbol) { + duplicate = true + break + } + } + if duplicate { + source.addf(parser.diagnostics, member.span, "duplicate enum member '%s'", token_text(parser, member)) + } + value := next_value + if _, ok := allow(parser, .Equal); ok { + if !explicit_backing { + source.add(parser.diagnostics, member.span, "explicit enum values require a backing type") + } + negative := false + if _, minus_ok := allow(parser, .Minus); minus_ok { + negative = true + } + literal := current(parser) + if literal.kind != .Integer { + source.add(parser.diagnostics, literal.span, "expected a decimal integer literal for enum value") + } else { + advance(parser) + magnitude, magnitude_ok := parse_integer_magnitude(token_text(parser, literal)) + if !magnitude_ok { + source.add(parser.diagnostics, literal.span, "enum value magnitude does not fit in u64") + } else { + value = i128(magnitude) + if negative { + value = -value + } + } + } + } + if has_previous && value <= previous_value { + source.add(parser.diagnostics, member.span, "enum values must be strictly increasing") + } + if !duplicate { + append(&members, types.Enum_Member{name=u32(member.symbol), value=value}) + } + previous_value = value + has_previous = true + next_value = value+1 + if _, ok := allow(parser, .Comma); ok { + skip_newlines(parser) + } else { + _ = finish_statement(parser, true) + } + } + if _, ok := allow(parser, .Right_Brace); !ok { + source.add(parser.diagnostics, current(parser).span, "expected '}' after enum members") + } + if len(members) == 0 { + source.add(parser.diagnostics, start.span, "enum declarations require at least one member") + } + if !explicit_backing { + max_value := u64(max(len(members)-1, 0)) + backing = types.U8 + if max_value > 0xff { + backing = types.U16 + } + if max_value > 0xffff { + backing = types.U32 + } + if max_value > 0xffff_ffff { + backing = types.U64 + } + } + id := types.named(&parser.module.type_store, u32(parser.pkg), u32(name.symbol)) + if !types.define_enum(&parser.module.type_store, id, backing, members[:], explicit_backing) { + source.addf(parser.diagnostics, name.span, "duplicate type declaration '%s'", token_text(parser, name)) + } + _ = finish_statement(parser) +} + decode_import_path :: proc(parser: ^Parser, tok: token.Token) -> string { text := token_text(parser, tok) if len(text) < 2 { @@ -1601,6 +1728,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_Enum { + parse_enum(parser, name) + return + } if operator.kind == .Colon_Colon && current(parser).kind == .Keyword_Distinct { parse_distinct(parser, name) return diff --git a/compiler/token/token.odin b/compiler/token/token.odin index 5cb91d6..e0231e0 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_Enum, Keyword_Distinct, Keyword_Import, Keyword_Return, diff --git a/compiler/types/types.odin b/compiler/types/types.odin index 38e90fb..db4bf11 100644 --- a/compiler/types/types.odin +++ b/compiler/types/types.odin @@ -65,6 +65,7 @@ Kind :: enum u8 { Named, Alias, Distinct, + Enum, Struct, Union, } @@ -91,6 +92,7 @@ Node :: struct { c_layout: bool, opaque: bool, declared: bool, + explicit_backing: bool, } Field :: struct { @@ -99,17 +101,24 @@ Field :: struct { offset: u64, } +Enum_Member :: struct { + name: u32, + value: i128, +} + Store :: struct { - nodes: [dynamic]Node, - fields: [dynamic]Field, - selected: target.Target, - allocator: mem.Allocator, + nodes: [dynamic]Node, + fields: [dynamic]Field, + enum_members: [dynamic]Enum_Member, + selected: target.Target, + allocator: mem.Allocator, } init_store :: proc(allocator := context.allocator) -> Store { store: Store store.nodes.allocator = allocator store.fields.allocator = allocator + store.enum_members.allocator = allocator store.selected = target.DEFAULT store.allocator = allocator return store @@ -118,19 +127,21 @@ init_store :: proc(allocator := context.allocator) -> Store { destroy_store :: proc(store: ^Store) { delete(store.nodes) delete(store.fields) + delete(store.enum_members) } clone_store :: proc(source: ^Store, allocator := context.allocator) -> Store { store := init_store(allocator) append(&store.nodes, ..source.nodes[:]) append(&store.fields, ..source.fields[:]) + append(&store.enum_members, ..source.enum_members[:]) store.selected = source.selected return store } intern :: proc(store: ^Store, candidate: Node) -> Type { if candidate.kind != .Struct && candidate.kind != .Union && - candidate.kind != .Named && candidate.kind != .Distinct { + candidate.kind != .Named && candidate.kind != .Distinct && candidate.kind != .Enum { for existing, index in store.nodes { if existing == candidate { return DYNAMIC_START+Type(index) @@ -146,7 +157,7 @@ named :: proc(store: ^Store, pkg, name: u32, qualifier: u32 = 0, file: u32 = 0xf normalized_file := file if qualifier != 0 else u32(0) for existing, index in store.nodes { if (existing.kind == .Named || existing.kind == .Alias || existing.kind == .Distinct || - existing.kind == .Struct || existing.kind == .Union) && + existing.kind == .Enum || existing.kind == .Struct || existing.kind == .Union) && existing.pkg == pkg && existing.name == name && existing.qualifier == qualifier && existing.file == normalized_file { return DYNAMIC_START+Type(index) @@ -158,7 +169,7 @@ 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 == .Distinct || - existing.kind == .Struct || existing.kind == .Union) && + existing.kind == .Enum || existing.kind == .Struct || existing.kind == .Union) && existing.pkg == pkg && existing.name == name && existing.qualifier == qualifier { return DYNAMIC_START+Type(index) } @@ -190,6 +201,22 @@ define_distinct :: proc(store: ^Store, id, child: Type) -> bool { return true } +define_enum :: proc(store: ^Store, id, backing: Type, members: []Enum_Member, explicit_backing: bool) -> bool { + existing, ok := node(store, id) + if !ok || existing.kind != .Named || existing.declared { + return false + } + index := int(id-DYNAMIC_START) + store.nodes[index].kind = .Enum + store.nodes[index].child = backing + store.nodes[index].field_start = u32(len(store.enum_members)) + store.nodes[index].field_count = u32(len(members)) + store.nodes[index].explicit_backing = explicit_backing + store.nodes[index].declared = true + append(&store.enum_members, ..members) + return true +} + define_record :: proc( store: ^Store, id: Type, @@ -247,6 +274,19 @@ params_for :: proc(store: ^Store, value: Type) -> []Field { return store.fields[start:end] } +enum_members_for :: proc(store: ^Store, value: Type) -> []Enum_Member { + item, ok := node(store, value) + if !ok || item.kind != .Enum { + return nil + } + start := int(item.field_start) + end := start+int(item.field_count) + if start < 0 || end > len(store.enum_members) { + return nil + } + return store.enum_members[start:end] +} + kind :: proc(value: Type, store: ^Store = nil) -> Kind { switch value { case INVALID: @@ -409,7 +449,8 @@ is_concrete_scalar :: proc(value: Type) -> bool { is_concrete :: proc(value: Type, store: ^Store = nil) -> bool { value_kind := kind(value, store) if value_kind == .Scalar || value_kind == .Array || value_kind == .Pointer || - value_kind == .Slice || value_kind == .Range || value_kind == .Optional { + value_kind == .Slice || value_kind == .Range || value_kind == .Optional || + value_kind == .Enum { return true } if value_kind == .Struct || value_kind == .Union { @@ -460,6 +501,10 @@ is_distinct :: proc(value: Type, store: ^Store) -> bool { return kind(value, store) == .Distinct } +is_enum :: proc(value: Type, store: ^Store) -> bool { + return kind(value, store) == .Enum +} + resolve_alias :: proc(value: Type, store: ^Store, depth := 0) -> Type { if depth > 64 { return INVALID @@ -486,6 +531,9 @@ is_c_record_field_type :: proc(value: Type, store: ^Store, depth := 0) -> bool { if !ok { return false } + if item.kind == .Enum { + return item.explicit_backing && is_concrete_integer(item.child) + } if item.kind == .Array { return item.count > 0 && !item.has_sentinel && !item.inferred_count && is_c_record_field_type(item.child, store, depth+1) @@ -524,7 +572,7 @@ is_runtime_value :: proc(value: Type, store: ^Store, depth := 0) -> bool { item, ok := node(store, value) return ok && item.declared && !item.opaque && (!item.c_layout || item.field_count > 0) } - if value_kind == .Distinct { + if value_kind == .Distinct || value_kind == .Enum { item, ok := node(store, value) return ok && item.declared && is_runtime_value(item.child, store, depth+1) } @@ -541,7 +589,7 @@ runtime_representation :: proc(value: Type, store: ^Store, depth := 0) -> Type { return INVALID } item, ok := node(store, value) - if !ok || item.kind != .Distinct { + if !ok || (item.kind != .Distinct && item.kind != .Enum) { return value } return runtime_representation(item.child, store, depth+1) @@ -585,6 +633,9 @@ is_c_signature_type :: proc(value: Type, store: ^Store, allow_void := false) -> if contains_distinct(value, store) { return false } + if item, ok := node(store, value); ok && item.kind == .Enum { + return item.explicit_backing && is_concrete_integer(item.child) + } return is_concrete_scalar(value) || is_pointer(value, store) || is_optional_pointer(value, store) || (is_c_struct(value, store) && is_runtime_value(value, store)) } @@ -621,7 +672,12 @@ is_c_integer_promotion_candidate :: proc(value: Type) -> bool { return value >= C_CHAR && value <= C_USHORT } -c_vararg_promotion :: proc(value: Type, selected := target.DEFAULT) -> Type { +c_vararg_promotion :: proc(value: Type, selected := target.DEFAULT, store: ^Store = nil) -> Type { + if store != nil { + if item, ok := node(store, value); ok && item.kind == .Enum { + return c_vararg_promotion(item.child, selected, store) + } + } if !is_concrete_scalar(value) { return value } @@ -642,6 +698,9 @@ c_vararg_promotion :: proc(value: Type, selected := target.DEFAULT) -> Type { } is_c_vararg_type :: proc(value: Type, store: ^Store) -> bool { + if item, ok := node(store, value); ok && item.kind == .Enum { + return item.explicit_backing && is_concrete_integer(item.child) + } return is_concrete_scalar(value) || is_pointer(value, store) || is_optional_pointer(value, store) } @@ -909,7 +968,7 @@ 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: + case .Distinct, .Enum: return size(child_type(value, store), store, selected) case .Struct: item, _ := node(store, value) @@ -952,7 +1011,7 @@ 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: + case .Distinct, .Enum: return alignment_of(child_type(value, store), store, selected) case .Struct: item, _ := node(store, value) diff --git a/compiler_tests.odin b/compiler_tests.odin index e7d3c4c..944fa9c 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -2379,6 +2379,42 @@ libclang_import_preserves_external_object_and_final_macro_semantics :: proc(t: ^ testing.expect(t, result.available) testing.expect_value(t, result.error_message, "") + enum_alias_type := cimport.INVALID_TYPE + for alias in result.aliases { + if alias.name == "Imported_Enum" { + enum_alias_type = alias.type + break + } + } + testing.expect(t, enum_alias_type != cimport.INVALID_TYPE) + if enum_alias_type != cimport.INVALID_TYPE { + testing.expect_value(t, result.types[enum_alias_type].kind, cimport.Type_Kind.C_Int) + } + enum_negative, found_enum_negative := find_cimport_macro(&result, "IMPORTED_ENUM_NEGATIVE") + enum_same, found_enum_same := find_cimport_macro(&result, "IMPORTED_ENUM_SAME") + enum_value, found_enum_value := find_cimport_macro(&result, "IMPORTED_ENUM_VALUE") + enum_back, found_enum_back := find_cimport_macro(&result, "IMPORTED_ENUM_BACK") + enum_anon, found_enum_anon := find_cimport_macro(&result, "IMPORTED_ANON_ENUM") + testing.expect(t, found_enum_negative && found_enum_same && found_enum_value && found_enum_back && found_enum_anon) + if found_enum_negative { + testing.expect(t, enum_negative.value.negative) + testing.expect_value(t, enum_negative.value.integer, u64(2)) + } + if found_enum_same { + testing.expect(t, enum_same.value.negative) + testing.expect_value(t, enum_same.value.integer, u64(2)) + } + if found_enum_value { + testing.expect(t, !enum_value.value.negative) + testing.expect_value(t, enum_value.value.integer, u64(7)) + } + if found_enum_back { + testing.expect_value(t, enum_back.value.integer, u64(3)) + } + if found_enum_anon { + testing.expect_value(t, enum_anon.value.integer, u64(9)) + } + tls, found_tls := find_cimport_variable(&result, "imported_tls_global") testing.expect(t, found_tls) if found_tls { @@ -5409,3 +5445,208 @@ distinct_types_compile_and_run_across_packages :: proc(t: ^testing.T) { state := run_executable(output) testing.expect_value(t, state.exit_code, 0) } + +@(test) +native_enums_preserve_identity_members_and_integer_representation :: proc(t: ^testing.T) { + text := `Animal :: enum { + dog + cat + bird +} +Nat :: enum(u16) { + one = 1 + two + five = 5 +} +global Animal :: Animal.dog +take :: func(value Animal) Animal { + return value +} +identity :: c_func(value Nat) Nat { + return value +} +variadic :: c_func(marker c_int, ...) c_int +main :: func() i32 { + value Animal = .cat + values [2]Animal :: [.dog, Animal.bird] + number Nat = identity(.two) + _ = variadic(0, number) + if take(value) == Animal.cat and values[0] != values[1] and number == Nat.two { + return 0 + } + return 1 +} +` + 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) + + animal := types.find_named(&ast_module.type_store, 0, u32(symbol.intern(&symbols, "Animal"))) + nat := types.find_named(&ast_module.type_store, 0, u32(symbol.intern(&symbols, "Nat"))) + animal_node, animal_ok := types.node(&ast_module.type_store, animal) + nat_node, nat_ok := types.node(&ast_module.type_store, nat) + animal_members := types.enum_members_for(&ast_module.type_store, animal) + nat_members := types.enum_members_for(&ast_module.type_store, nat) + + testing.expect_value(t, len(diagnostics.items), 0) + testing.expect(t, animal_ok && nat_ok) + testing.expect(t, types.is_enum(animal, &ast_module.type_store)) + testing.expect_value(t, animal_node.child, types.U8) + testing.expect(t, !animal_node.explicit_backing) + testing.expect_value(t, nat_node.child, types.U16) + testing.expect(t, nat_node.explicit_backing) + testing.expect_value(t, len(animal_members), 3) + testing.expect_value(t, animal_members[0].value, i128(0)) + testing.expect_value(t, animal_members[2].value, i128(2)) + testing.expect_value(t, nat_members[0].value, i128(1)) + testing.expect_value(t, nat_members[1].value, i128(2)) + testing.expect_value(t, nat_members[2].value, i128(5)) + testing.expect_value(t, types.runtime_representation(animal, &ast_module.type_store), types.U8) + testing.expect_value(t, types.size(animal, &ast_module.type_store), u64(1)) + testing.expect(t, hir_module.globals[0].is_static) + testing.expect_value(t, hir_module.globals[0].static_value, i64(0)) + testing.expect(t, strings.contains(llvm_text, "@bro.g.0 = internal constant i8 0")) + found_promotion := false + for function in ir_module.functions { + for instruction in function.instructions { + found_promotion = found_promotion || instruction.op == .C_Vararg_Promote + } + } + testing.expect(t, found_promotion) +} + +@(test) +unbacked_enum_selects_the_smallest_fitting_unsigned_backing :: proc(t: ^testing.T) { + builder := strings.builder_make() + defer strings.builder_destroy(&builder) + strings.write_string(&builder, "Large :: enum {\n") + for index in 0..<257 { + fmt.sbprintf(&builder, "value_%d\n", index) + } + strings.write_string(&builder, "}\nmain :: func() void {}\n") + source_file := source.Source{path="test.bro", text=strings.to_string(builder)} + 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) + module := parser.parse(&stream, &source_file, &diagnostics) + defer ast.destroy_module(&module) + + large := types.find_named(&module.type_store, 0, u32(symbol.intern(&symbols, "Large"))) + item, ok := types.node(&module.type_store, large) + testing.expect_value(t, len(diagnostics.items), 0) + testing.expect(t, ok) + testing.expect_value(t, item.child, types.U16) + testing.expect_value(t, len(types.enum_members_for(&module.type_store, large)), 257) +} + +@(test) +native_enum_invalid_declarations_and_operations_are_diagnosed :: proc(t: ^testing.T) { + text := `Empty :: enum {} +Dense :: enum { + zero = 0 + one +} +BadBacking :: enum(f32) { + value +} +Duplicate :: enum(u8) { + value + value +} +Jumbled :: enum(i8) { + second = 2 + first = 1 +} +Overflow :: enum(u8) { + value = 256 +} +Other :: enum { + value +} +foreign :: c_func(value Dense) void +allowed :: c_func(value Overflow) Overflow +main :: func() void { + dense Dense = Other.value + _ = Dense.zero + Dense.one + _ = Dense.zero < Dense.one + _ = Dense.missing + _ = .zero + _ = dense +} +` + 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_empty := false + found_unbacked_value := false + found_backing := false + found_duplicate := false + found_order := false + found_overflow := false + found_foreign := false + found_conversion := false + found_arithmetic := false + found_comparison := false + found_member := false + found_context := false + for diagnostic in diagnostics.items { + found_empty = found_empty || strings.contains(diagnostic.message, "require at least one member") + found_unbacked_value = found_unbacked_value || strings.contains(diagnostic.message, "explicit enum values require a backing type") + found_backing = found_backing || strings.contains(diagnostic.message, "requires a concrete integer backing type") + found_duplicate = found_duplicate || strings.contains(diagnostic.message, "duplicate enum member") + found_order = found_order || strings.contains(diagnostic.message, "strictly increasing") + found_overflow = found_overflow || strings.contains(diagnostic.message, "does not fit in u8") + found_foreign = found_foreign || strings.contains(diagnostic.message, "requires concrete parameter types") + found_conversion = found_conversion || strings.contains(diagnostic.message, "cannot implicitly convert") + found_arithmetic = found_arithmetic || strings.contains(diagnostic.message, "arithmetic requires compatible numeric operands") + found_comparison = found_comparison || strings.contains(diagnostic.message, "enum values only support") + found_member = found_member || strings.contains(diagnostic.message, "unknown enum member") + found_context = found_context || strings.contains(diagnostic.message, "requires an enum context") + } + testing.expect(t, found_empty) + testing.expect(t, found_unbacked_value) + testing.expect(t, found_backing) + testing.expect(t, found_duplicate) + testing.expect(t, found_order) + testing.expect(t, found_overflow) + testing.expect(t, found_foreign) + testing.expect(t, found_conversion) + testing.expect(t, found_arithmetic) + testing.expect(t, found_comparison) + testing.expect(t, found_member) + testing.expect(t, found_context) +} + +@(test) +native_enums_compile_and_run_across_packages :: proc(t: ^testing.T) { + output := "/tmp/brolang-test-enums" + defer _ = os.remove(output) + status := compiler_core.compile_package("examples/programs/enums", output) + testing.expect_value(t, status, 0) + state := run_executable(output) + testing.expect_value(t, state.exit_code, 0) +} diff --git a/examples/interop/header/app/main.bro b/examples/interop/header/app/main.bro index edd1df6..dc24144 100644 --- a/examples/interop/header/app/main.bro +++ b/examples/interop/header/app/main.bro @@ -12,6 +12,10 @@ call_mapper :: func(mapper native.Imported_Mapper) c_int { return mapper?(21) } +enum_identity :: c_func(value native.Imported_Enum) native.Imported_Enum { + return value +} + main :: func() void { _ = native.imported_add(pass_alias(20), 22) _ = native.imported_scalar(3, 4) @@ -19,6 +23,14 @@ main :: func() void { _ = native.imported_read(native.imported_handle()?) _ = native.imported_string("hello") _ = native.imported_apply(double_value, 21) + enum_value native.Imported_Enum :: native.IMPORTED_ENUM_NEGATIVE + enum_same native.Imported_Enum :: native.IMPORTED_ENUM_SAME + _ = native.imported_enum_check(enum_value, native.IMPORTED_ENUM_VALUE) + _ = native.imported_enum_variadic(-2, enum_same) + enum_record native.Imported_Enum_Record :: native.Imported_Enum_Record { value = native.IMPORTED_ENUM_BACK } + _ = native.imported_enum_record(enum_record) + _ = native.imported_enum_apply(enum_identity, native.imported_enum_global) + _ = native.IMPORTED_ANON_ENUM _ = call_mapper(double_value) _ = native.configured_value(9) _ = native.IMPORTED_SHADOW_OBJECT diff --git a/examples/interop/header/include/native.h b/examples/interop/header/include/native.h index 5b80b93..f505ec0 100644 --- a/examples/interop/header/include/native.h +++ b/examples/interop/header/include/native.h @@ -80,8 +80,18 @@ typedef struct Imported_Anonymous { }; } Imported_Anonymous; typedef enum Imported_Enum { - IMPORTED_ENUM_VALUE, + IMPORTED_ENUM_NEGATIVE = -2, + IMPORTED_ENUM_SAME = -2, + IMPORTED_ENUM_VALUE = 7, + IMPORTED_ENUM_BACK = 3, } Imported_Enum; +enum { + IMPORTED_ANON_ENUM = 9, +}; +typedef struct Imported_Enum_Record { + Imported_Enum value; +} Imported_Enum_Record; +typedef Imported_Enum (*Imported_Enum_Callback)(Imported_Enum value); int imported_add(imported_int_alias left, int right); unsigned long imported_scalar(unsigned char value, unsigned long extra); @@ -89,6 +99,10 @@ Imported_Handle *imported_handle(void); int imported_read(const Imported_Handle *handle); int imported_string(const char *value); int imported_apply(Imported_Mapper mapper, int value); +int imported_enum_check(Imported_Enum first, Imported_Enum second); +int imported_enum_variadic(int expected, ...); +Imported_Enum_Record imported_enum_record(Imported_Enum_Record value); +Imported_Enum imported_enum_apply(Imported_Enum_Callback callback, Imported_Enum value); Imported_Value imported_by_value(Imported_Value value); int imported_check_state( Imported_Color color, @@ -126,6 +140,7 @@ extern int imported_redeclared_array[4]; extern Imported_Value imported_record_global; extern const Imported_Array_Record imported_const_array_record; extern Imported_Array_Record imported_mutable_array_record; +extern Imported_Enum imported_enum_global; extern _Thread_local int imported_tls_global; extern int IMPORTED_SHADOW_OBJECT; int IMPORTED_SHADOW_FUNCTION(void); diff --git a/examples/interop/header/native.c b/examples/interop/header/native.c index 39169a7..ea4ce0d 100644 --- a/examples/interop/header/native.c +++ b/examples/interop/header/native.c @@ -16,6 +16,7 @@ int imported_redeclared_array[4] = {13, 14, 15, 16}; Imported_Value imported_record_global = {40}; const Imported_Array_Record imported_const_array_record = {{20, 21}}; Imported_Array_Record imported_mutable_array_record = {{0, 1}}; +Imported_Enum imported_enum_global = IMPORTED_ENUM_VALUE; int child_shared_global = 0; int child_value(int value) { @@ -56,6 +57,38 @@ int imported_apply(Imported_Mapper mapper, int value) { return result; } +int imported_enum_check(Imported_Enum first, Imported_Enum second) { + if (!(first == IMPORTED_ENUM_NEGATIVE && second == IMPORTED_ENUM_VALUE)) { + abort(); + } + return 0; +} + +int imported_enum_variadic(int expected, ...) { + va_list args; + va_start(args, expected); + int value = va_arg(args, int); + va_end(args); + if (value != expected) { + abort(); + } + return 0; +} + +Imported_Enum_Record imported_enum_record(Imported_Enum_Record value) { + if (value.value != IMPORTED_ENUM_BACK) { + abort(); + } + return value; +} + +Imported_Enum imported_enum_apply(Imported_Enum_Callback callback, Imported_Enum value) { + if (callback == NULL || callback(value) != value) { + abort(); + } + return value; +} + int imported_check_state( Imported_Color color, int macro_value, diff --git a/examples/programs/enums/animals/animals.bro b/examples/programs/enums/animals/animals.bro new file mode 100644 index 0000000..3354b6d --- /dev/null +++ b/examples/programs/enums/animals/animals.bro @@ -0,0 +1,8 @@ +Animal :: enum { + dog + cat +} + +favorite :: func() Animal { + return .cat +} diff --git a/examples/programs/enums/main.bro b/examples/programs/enums/main.bro new file mode 100644 index 0000000..32b647d --- /dev/null +++ b/examples/programs/enums/main.bro @@ -0,0 +1,30 @@ +animals :: import "./animals" + +State :: enum(u16) { + started = 10 + running + stopped = 20 +} + +initial State :: State.started + +same :: func(left State, right State) bool { + return left == right +} + +identity :: c_func(value State) State { + return value +} + +main :: func() i32 { + state State = identity(.running) + values [2]State :: [.started, State.stopped] + animal animals.Animal :: animals.Animal.dog + if same(state, .running) and + values[0] != values[1] and + animal != animals.favorite() and + initial == State.started { + return 0 + } + return 1 +}