allow keywords as values in enums (and tagged unions)

This commit is contained in:
2026-07-13 17:51:59 +02:00
parent de56dc7315
commit 5a958d9bfd
5 changed files with 156 additions and 18 deletions
+28
View File
@@ -39,6 +39,34 @@ roadmap and milestone history.
- native sum composition with `A | B` for unbacked enums and tagged unions, using program-global `u16` variant ids - native sum composition with `A | B` for unbacked enums and tagged unions, using program-global `u16` variant ids
- fallible channel types `T ! E`, where `E` is a native enum/tagged union or supported sum composition - fallible channel types `T ! E`, where `E` is a native enum/tagged union or supported sum composition
#### keyword member names
Reserved keywords are valid native enum members and tagged-union variants when used in an
unambiguous member context:
```bro
TokenKind :: enum {
if
else
return
}
Token :: union(TokenKind) {
if i32
else void
return i32
}
conditional func() TokenKind { return TokenKind.if }
fallback func() TokenKind { return .else }
token func() Token { return Token{ if = 1 } }
```
Keyword variants also work with field access and `.variant` match patterns; `.else:` remains
distinct from the `else:` catch-all arm. No escaping syntax is required. Keywords remain reserved
for ordinary declarations, struct fields, untagged-union fields, and anonymous payload-struct
fields. `_` is not a keyword member name.
### expressions and control flow ### expressions and control flow
- checked integer `+ - *`, unary `-`, float-only `/`, IEEE float arithmetic, comparisons, `!`, `and`, and `or` - checked integer `+ - *`, unary `-`, float-only `/`, IEEE float arithmetic, comparisons, `!`, `and`, and `or`
+1
View File
@@ -105,6 +105,7 @@ lex :: proc(
) -> token.Stream { ) -> token.Stream {
stream: token.Stream stream: token.Stream
stream.items.allocator = allocator stream.items.allocator = allocator
stream.symbols = symbols
bytes := transmute([]byte)source_file.text bytes := transmute([]byte)source_file.text
cursor := 0 cursor := 0
+34 -17
View File
@@ -86,6 +86,18 @@ allow :: proc(parser: ^Parser, kind: token.Kind) -> (token.Token, bool) {
return current(parser), false return current(parser), false
} }
parse_member_name :: proc(parser: ^Parser, allow_keyword := true) -> (token.Token, bool) {
name := current(parser)
if name.kind != .Identifier && (!allow_keyword || !token.is_keyword(name.kind)) {
return name, false
}
advance(parser)
if name.kind != .Identifier {
name.symbol = symbol.intern(parser.tokens.symbols, token_text(parser, name))
}
return name, true
}
skip_newlines :: proc(parser: ^Parser) { skip_newlines :: proc(parser: ^Parser) {
for current(parser).kind == .Newline { for current(parser).kind == .Newline {
advance(parser) advance(parser)
@@ -539,12 +551,11 @@ parse_keyed_initializers :: proc(
args.allocator = parser.module.allocator args.allocator = parser.module.allocator
skip_newlines(parser) skip_newlines(parser)
for current(parser).kind != .Right_Brace && current(parser).kind != .Eof { for current(parser).kind != .Right_Brace && current(parser).kind != .Eof {
field := current(parser) field, field_ok := parse_member_name(parser)
if field.kind != .Identifier { if !field_ok {
source.add(parser.diagnostics, field.span, "expected a keyed struct field initializer") source.add(parser.diagnostics, field.span, "expected a keyed struct field initializer")
break break
} }
advance(parser)
// A bare key (`T{ variant }`, no `= value`) constructs a void-payload union // A bare key (`T{ variant }`, no `= value`) constructs a void-payload union
// variant; the checker validates that the field actually has a void type. // variant; the checker validates that the field actually has a void type.
value := ast.INVALID_EXPR value := ast.INVALID_EXPR
@@ -804,11 +815,10 @@ parse_primary :: proc(parser: ^Parser, nesting: int) -> ast.Expr_Id {
return parse_array_literal(parser, nesting) return parse_array_literal(parser, nesting)
case .Dot: case .Dot:
start := advance(parser) start := advance(parser)
member := current(parser) member, member_ok := parse_member_name(parser)
if member.kind != .Identifier { if !member_ok {
return invalid_expr(parser, member.span, "expected an enum member after '.'") return invalid_expr(parser, member.span, "expected an enum member after '.'")
} }
advance(parser)
payload := ast.INVALID_EXPR payload := ast.INVALID_EXPR
end := member.span end := member.span
if left_brace, ok := allow(parser, .Left_Brace); ok { if left_brace, ok := allow(parser, .Left_Brace); ok {
@@ -868,11 +878,12 @@ parse_primary :: proc(parser: ^Parser, nesting: int) -> ast.Expr_Id {
name := first name := first
qualifier := symbol.INVALID qualifier := symbol.INVALID
if _, ok := allow(parser, .Dot); ok { if _, ok := allow(parser, .Dot); ok {
if current(parser).kind != .Identifier { member, member_ok := parse_member_name(parser)
if !member_ok {
return invalid_expr(parser, current(parser).span, "expected a package member after '.'") return invalid_expr(parser, current(parser).span, "expected a package member after '.'")
} }
qualifier = first.symbol qualifier = first.symbol
name = advance(parser) name = member
} }
if current(parser).kind == .Left_Paren { if current(parser).kind == .Left_Paren {
call := parse_call(parser, qualifier, first, name, nesting) call := parse_call(parser, qualifier, first, name, nesting)
@@ -1074,12 +1085,11 @@ parse_expression_bp :: proc(parser: ^Parser, minimum_binding_power, nesting: int
} }
if current(parser).kind == .Dot { if current(parser).kind == .Dot {
advance(parser) advance(parser)
field := current(parser) field, field_ok := parse_member_name(parser)
if field.kind != .Identifier { if !field_ok {
left = invalid_expr(parser, field.span, "expected a field name after '.'") left = invalid_expr(parser, field.span, "expected a field name after '.'")
continue continue
} }
advance(parser)
left_expr := parser.module.exprs[left] left_expr := parser.module.exprs[left]
left = add_expr(parser, ast.Expr{ left = add_expr(parser, ast.Expr{
kind=.Field, kind=.Field,
@@ -2252,6 +2262,7 @@ parse_record_body :: proc(
fields: ^[dynamic]types.Field, fields: ^[dynamic]types.Field,
expected_open: string, expected_open: string,
allow_anonymous_struct_payload := false, allow_anonymous_struct_payload := false,
allow_keyword_names := false,
) -> bool { ) -> bool {
if _, ok := allow(parser, .Left_Brace); !ok { if _, ok := allow(parser, .Left_Brace); !ok {
source.add(parser.diagnostics, current(parser).span, expected_open) source.add(parser.diagnostics, current(parser).span, expected_open)
@@ -2259,7 +2270,8 @@ parse_record_body :: proc(
} }
skip_newlines(parser) skip_newlines(parser)
for current(parser).kind != .Right_Brace && current(parser).kind != .Eof { for current(parser).kind != .Right_Brace && current(parser).kind != .Eof {
if current(parser).kind != .Identifier { field_name, field_ok := parse_member_name(parser, allow_keyword_names)
if !field_ok {
source.add(parser.diagnostics, current(parser).span, "expected a struct field name") source.add(parser.diagnostics, current(parser).span, "expected a struct field name")
for current(parser).kind != .Newline && for current(parser).kind != .Newline &&
current(parser).kind != .Right_Brace && current(parser).kind != .Right_Brace &&
@@ -2269,7 +2281,6 @@ parse_record_body :: proc(
skip_newlines(parser) skip_newlines(parser)
continue continue
} }
field_name := advance(parser)
field_type := parse_record_field_type(parser, allow_anonymous_struct_payload) field_type := parse_record_field_type(parser, allow_anonymous_struct_payload)
append(fields, types.Field{name=u32(field_name.symbol), type=field_type}) append(fields, types.Field{name=u32(field_name.symbol), type=field_type})
if _, ok := allow(parser, .Comma); ok { if _, ok := allow(parser, .Comma); ok {
@@ -2317,7 +2328,7 @@ parse_inline_union_type :: proc(parser: ^Parser) -> types.Type {
fields: [dynamic]types.Field fields: [dynamic]types.Field
fields.allocator = parser.module.allocator fields.allocator = parser.module.allocator
defer delete(fields) defer delete(fields)
if !parse_record_body(parser, &fields, "expected '{' after inline union error type", true) || !valid { if !parse_record_body(parser, &fields, "expected '{' after inline union error type", true, true) || !valid {
return types.INVALID return types.INVALID
} }
tag := synthesize_union_tag(parser, fields[:]) tag := synthesize_union_tag(parser, fields[:])
@@ -2372,7 +2383,13 @@ parse_struct :: proc(parser: ^Parser, name: token.Token, c_layout: bool, is_unio
fields.allocator = parser.module.allocator fields.allocator = parser.module.allocator
defer delete(fields) defer delete(fields)
allow_anonymous_struct_payload := is_union && (inferred_tag || types.is_valid(declared_tag)) allow_anonymous_struct_payload := is_union && (inferred_tag || types.is_valid(declared_tag))
_ = parse_record_body(parser, &fields, "expected '{' after struct fields", allow_anonymous_struct_payload) _ = parse_record_body(
parser,
&fields,
"expected '{' after struct fields",
allow_anonymous_struct_payload,
allow_anonymous_struct_payload,
)
if is_union && (inferred_tag || types.is_valid(declared_tag)) { if is_union && (inferred_tag || types.is_valid(declared_tag)) {
tag = synthesize_union_tag(parser, fields[:]) tag = synthesize_union_tag(parser, fields[:])
} }
@@ -2479,7 +2496,8 @@ parse_enum_body :: proc(
has_previous := false has_previous := false
skip_newlines(parser) skip_newlines(parser)
for current(parser).kind != .Right_Brace && current(parser).kind != .Eof { for current(parser).kind != .Right_Brace && current(parser).kind != .Eof {
if current(parser).kind != .Identifier { member, member_ok := parse_member_name(parser)
if !member_ok {
source.add(parser.diagnostics, current(parser).span, "expected an enum member name") source.add(parser.diagnostics, current(parser).span, "expected an enum member name")
for current(parser).kind != .Newline && for current(parser).kind != .Newline &&
current(parser).kind != .Right_Brace && current(parser).kind != .Right_Brace &&
@@ -2489,7 +2507,6 @@ parse_enum_body :: proc(
skip_newlines(parser) skip_newlines(parser)
continue continue
} }
member := advance(parser)
duplicate := false duplicate := false
for existing in members^ { for existing in members^ {
if existing.name == u32(member.symbol) { if existing.name == u32(member.symbol) {
+6 -1
View File
@@ -114,6 +114,10 @@ Kind :: enum u8 {
Keyword_C_Longdouble, Keyword_C_Longdouble,
} }
is_keyword :: proc(kind: Kind) -> bool {
return kind >= .Keyword_Func && kind <= .Keyword_C_Longdouble
}
Token :: struct { Token :: struct {
span: source.Span, span: source.Span,
symbol: symbol.Id, symbol: symbol.Id,
@@ -122,5 +126,6 @@ Token :: struct {
} }
Stream :: struct { Stream :: struct {
items: [dynamic]Token, items: [dynamic]Token,
symbols: ^symbol.Table,
} }
+87
View File
@@ -10223,6 +10223,93 @@ main func() i32 {
testing.expect(t, found_promotion) testing.expect(t, found_promotion)
} }
@(test)
keywords_are_valid_enum_members_and_tagged_union_variants :: proc(t: ^testing.T) {
testing.expect(t, token.is_keyword(.Keyword_Func))
testing.expect(t, token.is_keyword(.Keyword_C_Longdouble))
testing.expect(t, !token.is_keyword(.Identifier))
testing.expect(t, !token.is_keyword(.Underscore))
text := `TokenKind :: enum {
if
else
return
}
Token :: union(TokenKind) {
if i32
else void
return i32
}
kind func(value bool) TokenKind {
if value {
return .if
}
return TokenKind.else
}
main func() i32 {
first TokenKind = kind(true)
second TokenKind = .return
a Token = Token{ if = 1 }
b Token = Token{ else }
c Token = .return{2}
total i32 = a.if + c.return
match first {
.if: total = total + 1
.else: total = total + 2
.return: total = total + 3
}
match b {
.if |value|: total = total + value
.else: total = total + 4
.return |value|: total = total + value
}
_ = second
return total
}
`
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)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, len(llvm_text) > 0)
}
@(test)
keyword_names_remain_invalid_for_struct_fields :: proc(t: ^testing.T) {
text := `Bad :: struct {
if i32
}
`
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)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(diagnostic.message, "expected a struct field name")
}
testing.expect(t, found)
}
@(test) @(test)
unbacked_enum_uses_global_u16_backing :: proc(t: ^testing.T) { unbacked_enum_uses_global_u16_backing :: proc(t: ^testing.T) {
builder := strings.builder_make() builder := strings.builder_make()