allow keywords as values in enums (and tagged unions)
This commit is contained in:
+28
@@ -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
|
||||
- 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
|
||||
|
||||
- checked integer `+ - *`, unary `-`, float-only `/`, IEEE float arithmetic, comparisons, `!`, `and`, and `or`
|
||||
|
||||
@@ -105,6 +105,7 @@ lex :: proc(
|
||||
) -> token.Stream {
|
||||
stream: token.Stream
|
||||
stream.items.allocator = allocator
|
||||
stream.symbols = symbols
|
||||
bytes := transmute([]byte)source_file.text
|
||||
cursor := 0
|
||||
|
||||
|
||||
+34
-17
@@ -86,6 +86,18 @@ allow :: proc(parser: ^Parser, kind: token.Kind) -> (token.Token, bool) {
|
||||
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) {
|
||||
for current(parser).kind == .Newline {
|
||||
advance(parser)
|
||||
@@ -539,12 +551,11 @@ parse_keyed_initializers :: proc(
|
||||
args.allocator = parser.module.allocator
|
||||
skip_newlines(parser)
|
||||
for current(parser).kind != .Right_Brace && current(parser).kind != .Eof {
|
||||
field := current(parser)
|
||||
if field.kind != .Identifier {
|
||||
field, field_ok := parse_member_name(parser)
|
||||
if !field_ok {
|
||||
source.add(parser.diagnostics, field.span, "expected a keyed struct field initializer")
|
||||
break
|
||||
}
|
||||
advance(parser)
|
||||
// A bare key (`T{ variant }`, no `= value`) constructs a void-payload union
|
||||
// variant; the checker validates that the field actually has a void type.
|
||||
value := ast.INVALID_EXPR
|
||||
@@ -804,11 +815,10 @@ parse_primary :: proc(parser: ^Parser, nesting: int) -> ast.Expr_Id {
|
||||
return parse_array_literal(parser, nesting)
|
||||
case .Dot:
|
||||
start := advance(parser)
|
||||
member := current(parser)
|
||||
if member.kind != .Identifier {
|
||||
member, member_ok := parse_member_name(parser)
|
||||
if !member_ok {
|
||||
return invalid_expr(parser, member.span, "expected an enum member after '.'")
|
||||
}
|
||||
advance(parser)
|
||||
payload := ast.INVALID_EXPR
|
||||
end := member.span
|
||||
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
|
||||
qualifier := symbol.INVALID
|
||||
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 '.'")
|
||||
}
|
||||
qualifier = first.symbol
|
||||
name = advance(parser)
|
||||
name = member
|
||||
}
|
||||
if current(parser).kind == .Left_Paren {
|
||||
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 {
|
||||
advance(parser)
|
||||
field := current(parser)
|
||||
if field.kind != .Identifier {
|
||||
field, field_ok := parse_member_name(parser)
|
||||
if !field_ok {
|
||||
left = invalid_expr(parser, field.span, "expected a field name after '.'")
|
||||
continue
|
||||
}
|
||||
advance(parser)
|
||||
left_expr := parser.module.exprs[left]
|
||||
left = add_expr(parser, ast.Expr{
|
||||
kind=.Field,
|
||||
@@ -2252,6 +2262,7 @@ parse_record_body :: proc(
|
||||
fields: ^[dynamic]types.Field,
|
||||
expected_open: string,
|
||||
allow_anonymous_struct_payload := false,
|
||||
allow_keyword_names := false,
|
||||
) -> bool {
|
||||
if _, ok := allow(parser, .Left_Brace); !ok {
|
||||
source.add(parser.diagnostics, current(parser).span, expected_open)
|
||||
@@ -2259,7 +2270,8 @@ parse_record_body :: proc(
|
||||
}
|
||||
skip_newlines(parser)
|
||||
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")
|
||||
for current(parser).kind != .Newline &&
|
||||
current(parser).kind != .Right_Brace &&
|
||||
@@ -2269,7 +2281,6 @@ parse_record_body :: proc(
|
||||
skip_newlines(parser)
|
||||
continue
|
||||
}
|
||||
field_name := advance(parser)
|
||||
field_type := parse_record_field_type(parser, allow_anonymous_struct_payload)
|
||||
append(fields, types.Field{name=u32(field_name.symbol), type=field_type})
|
||||
if _, ok := allow(parser, .Comma); ok {
|
||||
@@ -2317,7 +2328,7 @@ parse_inline_union_type :: proc(parser: ^Parser) -> types.Type {
|
||||
fields: [dynamic]types.Field
|
||||
fields.allocator = parser.module.allocator
|
||||
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
|
||||
}
|
||||
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
|
||||
defer delete(fields)
|
||||
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)) {
|
||||
tag = synthesize_union_tag(parser, fields[:])
|
||||
}
|
||||
@@ -2479,7 +2496,8 @@ parse_enum_body :: proc(
|
||||
has_previous := false
|
||||
skip_newlines(parser)
|
||||
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")
|
||||
for current(parser).kind != .Newline &&
|
||||
current(parser).kind != .Right_Brace &&
|
||||
@@ -2489,7 +2507,6 @@ parse_enum_body :: proc(
|
||||
skip_newlines(parser)
|
||||
continue
|
||||
}
|
||||
member := advance(parser)
|
||||
duplicate := false
|
||||
for existing in members^ {
|
||||
if existing.name == u32(member.symbol) {
|
||||
|
||||
@@ -114,6 +114,10 @@ Kind :: enum u8 {
|
||||
Keyword_C_Longdouble,
|
||||
}
|
||||
|
||||
is_keyword :: proc(kind: Kind) -> bool {
|
||||
return kind >= .Keyword_Func && kind <= .Keyword_C_Longdouble
|
||||
}
|
||||
|
||||
Token :: struct {
|
||||
span: source.Span,
|
||||
symbol: symbol.Id,
|
||||
@@ -122,5 +126,6 @@ Token :: struct {
|
||||
}
|
||||
|
||||
Stream :: struct {
|
||||
items: [dynamic]Token,
|
||||
items: [dynamic]Token,
|
||||
symbols: ^symbol.Table,
|
||||
}
|
||||
|
||||
@@ -10223,6 +10223,93 @@ main func() i32 {
|
||||
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)
|
||||
unbacked_enum_uses_global_u16_backing :: proc(t: ^testing.T) {
|
||||
builder := strings.builder_make()
|
||||
|
||||
Reference in New Issue
Block a user