1704 lines
61 KiB
Plaintext
1704 lines
61 KiB
Plaintext
import "@std/mem"
|
|
import "@std/arraylist"
|
|
|
|
import "@source/diagnostics"
|
|
import "@source/lexer"
|
|
import "@source/ast"
|
|
import "@source/strpool"
|
|
|
|
@hide:file
|
|
SyntaxError :: enum { unexpected_token }
|
|
|
|
@hide:file
|
|
ParseError :: alias SyntaxError | mem.AllocError | diagnostics.CapacityError
|
|
|
|
@hide:file
|
|
BindingPowerScalar :: distinct u32
|
|
|
|
@hide:file
|
|
BindingPower :: struct {
|
|
left BindingPowerScalar
|
|
right BindingPowerScalar
|
|
}
|
|
|
|
@hide:file
|
|
State :: struct {
|
|
builder @mut ast.Builder
|
|
diagnostics @mut diagnostics.Store
|
|
tokens []lexer.Token
|
|
allocator mem.Allocator
|
|
next_token usize
|
|
paren_depth usize = 0
|
|
allow_record_literals bool = true
|
|
match_arm_body bool = false
|
|
}
|
|
|
|
#! borrows `tokens` and `source` while parsing; the returned Tree borrows `source` until ast.deinit.
|
|
#! `allocator` supplies returned node storage and temporary lists released before return.
|
|
#! the caller owns the Tree and must release it with ast.deinit.
|
|
parse proc(
|
|
allocator mem.Allocator,
|
|
tokens []lexer.Token,
|
|
source []u8,
|
|
diagnostic_store @mut diagnostics.Store,
|
|
) ast.Tree ! (mem.AllocError | diagnostics.CapacityError) {
|
|
builder := ast.init_builder(allocator, source)
|
|
defer ast.deinit_builder(&builder)
|
|
root :: try parse_source(&builder, tokens, diagnostic_store)
|
|
return ast.finish(&builder, root)
|
|
}
|
|
|
|
#! appends one source to shared node storage; token spans must identify the builder's current source.
|
|
#! infrastructure failure rolls back all nodes, extra data, spans, and diagnostics added by this parse.
|
|
parse_source proc(
|
|
builder @mut ast.Builder,
|
|
tokens []lexer.Token,
|
|
diagnostic_store @mut diagnostics.Store,
|
|
) ast.NodeId ! (mem.AllocError | diagnostics.CapacityError) {
|
|
node_count :: builder.storage.nodes.items.len
|
|
extra_count :: builder.storage.extra.items.len
|
|
diagnostic_count :: diagnostic_store.list.items.len
|
|
errdefer {
|
|
builder.storage.nodes.items = builder.storage.nodes.items.ptr[..node_count]
|
|
builder.storage.spans.items = builder.storage.spans.items.ptr[..node_count]
|
|
builder.storage.extra.items = builder.storage.extra.items.ptr[..extra_count]
|
|
diagnostic_store.list.items = diagnostic_store.list.items.ptr[..diagnostic_count]
|
|
}
|
|
state := State{
|
|
builder = builder,
|
|
diagnostics = diagnostic_store,
|
|
tokens = tokens,
|
|
allocator = builder.storage.nodes.allocator,
|
|
next_token = 0,
|
|
}
|
|
return try parse_package(&state)
|
|
}
|
|
|
|
@hide:file
|
|
parse_package proc(state @mut State) ast.NodeId ! (mem.AllocError | diagnostics.CapacityError) {
|
|
declarations arraylist.ArrayList(ast.NodeId) := arraylist.init(state.allocator)
|
|
defer arraylist.deinit(&declarations)
|
|
|
|
skip_newlines(state)
|
|
while !next_is(state, .eof) {
|
|
start :: state.next_token
|
|
declaration :: parse_decl(state) catch |err| {
|
|
match err {
|
|
.unexpected_token: {
|
|
recovered :: try recover_decl(state, start)
|
|
try arraylist.append(&declarations, recovered)
|
|
skip_newlines(state)
|
|
continue
|
|
}
|
|
else: return err
|
|
}
|
|
}
|
|
try arraylist.append(&declarations, declaration)
|
|
skip_newlines(state)
|
|
}
|
|
|
|
return try ast.add_package(state.builder, ast.Span{
|
|
start = 0,
|
|
end = u32(state.builder.source.len),
|
|
source = state.builder.source_id,
|
|
}, declarations.items)
|
|
}
|
|
|
|
@hide:file
|
|
parse_decl proc(state @mut State) ast.NodeId ! ParseError {
|
|
if (
|
|
next_is(state, .import) or next_is(state, .test)
|
|
or (next_is(state, .ident) and next_n_is(state, 1, .double_colon) and next_n_is(state, 2, .import))
|
|
) return try parse_import_decl(state)
|
|
if (next_is(state, .ident) and next_n_is(state, 1, .proc)) return try parse_func_decl(state)
|
|
return try parse_statement_decl(state)
|
|
}
|
|
|
|
@hide:file
|
|
parse_import_decl proc(state @mut State) ast.NodeId ! ParseError {
|
|
start :: current(state).span
|
|
test_only :: next_is(state, .test)
|
|
if (test_only) state.next_token += 1
|
|
namespace ?strpool.StringId := null
|
|
if next_is(state, .ident) {
|
|
name :: try consume(state, .ident)
|
|
namespace = name.data.str_id
|
|
_ = try consume(state, .double_colon)
|
|
}
|
|
_ = try consume(state, .import)
|
|
token :: try consume(state, .string)
|
|
path :: ast.Span{ start = token.span.start + 1, end = token.span.end - 1, source = token.span.source }
|
|
# import paths are literal filesystem paths, not escaped string values.
|
|
valid := path.start != path.end
|
|
for (state.builder.source[usize(path.start)..usize(path.end)]) |byte| {
|
|
if (byte < 32 or byte == 127 or byte == '\\') valid = false
|
|
}
|
|
span :: ast.Span{ start = start.start, end = token.span.end, source = start.source }
|
|
if (!valid) {
|
|
diagnostic :: try diagnostics.add(state.diagnostics, .invalid_import_path, path)
|
|
return try ast.add_invalid_decl(state.builder, span, diagnostic)
|
|
}
|
|
return try ast.add_import_decl(state.builder, span, path, namespace, test_only)
|
|
}
|
|
|
|
@hide:file
|
|
parse_func_decl proc(state @mut State) ast.NodeId ! ParseError {
|
|
name :: try consume(state, .ident)
|
|
_ = try consume(state, .proc)
|
|
_ = try consume(state, .open_paren)
|
|
|
|
params arraylist.ArrayList(ast.NodeId) := arraylist.init(state.allocator)
|
|
defer arraylist.deinit(¶ms)
|
|
names arraylist.ArrayList(lexer.Token) := arraylist.init(state.allocator)
|
|
defer arraylist.deinit(&names)
|
|
|
|
while !next_is(state, .close_paren) {
|
|
arraylist.clear(&names)
|
|
while true {
|
|
token :: try consume(state, .ident)
|
|
try arraylist.append(&names, token)
|
|
if (starts_type(current(state).kind)) break
|
|
_ = try consume(state, .comma)
|
|
}
|
|
|
|
type_id :: try parse_type(state)
|
|
type_span :: ast.builder_span(state.builder, type_id)
|
|
for (names.items) |parameter_name| {
|
|
param_id :: try ast.add_param(
|
|
state.builder,
|
|
ast.Span{ start = parameter_name.span.start, end = type_span.end, source = parameter_name.span.source },
|
|
parameter_name.span,
|
|
parameter_name.data.str_id,
|
|
type_id,
|
|
)
|
|
try arraylist.append(¶ms, param_id)
|
|
}
|
|
if next_is(state, .comma) _ = try consume(state, .comma)
|
|
}
|
|
|
|
_ = try consume(state, .close_paren)
|
|
return_type :: try parse_type(state)
|
|
body :: try parse_block(state)
|
|
body_span :: ast.builder_span(state.builder, body)
|
|
return try ast.add_function_decl(
|
|
state.builder,
|
|
ast.Span{
|
|
start = name.span.start,
|
|
end = body_span.end,
|
|
source = name.span.source,
|
|
},
|
|
name.span,
|
|
name.data.str_id,
|
|
return_type,
|
|
params.items,
|
|
body,
|
|
)
|
|
}
|
|
|
|
@hide:file
|
|
parse_block proc(state @mut State) ast.NodeId ! ParseError {
|
|
open :: try consume(state, .open_curly)
|
|
statements arraylist.ArrayList(ast.NodeId) := arraylist.init(state.allocator)
|
|
defer arraylist.deinit(&statements)
|
|
|
|
skip_newlines(state)
|
|
while !next_is(state, .close_curly) and !next_is(state, .eof) {
|
|
start :: state.next_token
|
|
statement :: parse_statement(state, false) catch |err| {
|
|
match err {
|
|
.unexpected_token: {
|
|
recovered :: try recover_statement(state, start)
|
|
try arraylist.append(&statements, recovered)
|
|
skip_newlines(state)
|
|
continue
|
|
}
|
|
else: return err
|
|
}
|
|
}
|
|
try arraylist.append(&statements, statement)
|
|
skip_newlines(state)
|
|
}
|
|
|
|
close :: try consume(state, .close_curly)
|
|
return try ast.add_block(
|
|
state.builder,
|
|
ast.Span{
|
|
start = open.span.start,
|
|
end = close.span.end,
|
|
source = open.span.source,
|
|
},
|
|
statements.items,
|
|
)
|
|
}
|
|
|
|
@hide:file
|
|
parse_statement proc(state @mut State, allow_else bool) ast.NodeId ! ParseError {
|
|
if (
|
|
next_is(state, .ident)
|
|
and (
|
|
next_n_is(state, 1, .ident)
|
|
or next_n_is(state, 1, .struct)
|
|
or next_n_is(state, 1, .enum) or next_n_is(state, 1, .union)
|
|
or next_n_is(state, 1, .distinct)
|
|
or next_n_is(state, 1, .at)
|
|
or (next_n_is(state, 1, .star) and pointer_binding_starts(state))
|
|
or (next_n_is(state, 1, .open_bracket) and array_starts_type(state))
|
|
or next_n_is(state, 1, .double_colon)
|
|
or next_n_is(state, 1, .colon_equal)
|
|
)
|
|
) {
|
|
return try parse_statement_decl(state)
|
|
}
|
|
match current(state).kind {
|
|
.if: return try parse_if(state)
|
|
.while: return try parse_while(state, allow_else)
|
|
.for: return try parse_for(state, allow_else)
|
|
.return: return try parse_return(state, allow_else)
|
|
.break, .continue: return try parse_loop_control(state, allow_else)
|
|
.ident, .int, .float, .string, .true, .false, .minus, .bang, .open_paren, .open_bracket, .open_curly, .struct,
|
|
.enum, .union, .distinct, .alias, .dot, .match, .ampersand, .at, .star: {
|
|
expression :: try parse_expr(state)
|
|
if next_is(state, .equal) return try parse_assignment(state, expression, allow_else)
|
|
return try ast.add_expression_stmt(state.builder, expression)
|
|
}
|
|
else: return .unexpected_token
|
|
}
|
|
}
|
|
|
|
@hide:file
|
|
parse_return proc(state @mut State, allow_else bool) ast.NodeId ! ParseError {
|
|
keyword :: try consume(state, .return)
|
|
if (
|
|
next_is(state, .newline) or next_is(state, .close_curly) or next_is(state, .eof)
|
|
or (state.match_arm_body and next_is(state, .comma))
|
|
or (allow_else and next_is(state, .else))
|
|
) {
|
|
return try ast.add_return_stmt(state.builder, keyword.span, null)
|
|
}
|
|
value :: try parse_expr(state)
|
|
if (
|
|
!next_is(state, .newline) and !next_is(state, .close_curly) and !next_is(state, .eof)
|
|
and !(allow_else and next_is(state, .else))
|
|
and !(state.match_arm_body and next_is(state, .comma))
|
|
) {
|
|
return .unexpected_token
|
|
}
|
|
# include consumed grouping punctuation, which has no separate expression node.
|
|
end :: state.tokens[state.next_token - 1].span.end
|
|
return try ast.add_return_stmt(
|
|
state.builder,
|
|
ast.Span{
|
|
start = keyword.span.start,
|
|
end = end,
|
|
source = keyword.span.source,
|
|
},
|
|
value,
|
|
)
|
|
}
|
|
|
|
@hide:file
|
|
parse_loop_control proc(state @mut State, allow_else bool) ast.NodeId ! ParseError {
|
|
keyword :: current(state)
|
|
state.next_token += 1
|
|
if (
|
|
!next_is(state, .newline)
|
|
and !next_is(state, .close_curly)
|
|
and !next_is(state, .eof)
|
|
and !(allow_else and next_is(state, .else))
|
|
and !(state.match_arm_body and next_is(state, .comma))
|
|
) return .unexpected_token
|
|
|
|
if (keyword.kind == .break)
|
|
return try ast.add_break_stmt(state.builder, keyword.span)
|
|
|
|
return try ast.add_continue_stmt(state.builder, keyword.span)
|
|
}
|
|
|
|
@hide:file
|
|
parse_assignment proc(state @mut State, target ast.NodeId, allow_else bool) ast.NodeId ! ParseError {
|
|
_ = try consume(state, .equal)
|
|
value :: try parse_expr(state)
|
|
if (
|
|
!next_is(state, .newline)
|
|
and !next_is(state, .close_curly)
|
|
and !next_is(state, .eof)
|
|
and !(allow_else and next_is(state, .else))
|
|
and !(state.match_arm_body and next_is(state, .comma))
|
|
) {
|
|
return .unexpected_token
|
|
}
|
|
end :: state.tokens[state.next_token - 1].span.end
|
|
return try ast.add_assignment_stmt(
|
|
state.builder,
|
|
ast.Span{
|
|
start = ast.builder_span(state.builder, target).start,
|
|
end = end,
|
|
source = state.builder.source_id,
|
|
},
|
|
target,
|
|
value,
|
|
)
|
|
}
|
|
|
|
@hide:file
|
|
parse_if proc(state @mut State) ast.NodeId ! ParseError {
|
|
keyword :: try consume(state, .if)
|
|
condition :: try parse_guard(state)
|
|
braced :: next_is(state, .open_curly)
|
|
then_block :: try parse_arm(state, true)
|
|
# only a braced arm permits its else on a later line.
|
|
after_then :: state.next_token
|
|
if (braced) skip_newlines(state)
|
|
alternative ?ast.NodeId := null
|
|
if next_is(state, .else) {
|
|
state.next_token += 1
|
|
alternative = try parse_arm(state, true)
|
|
} else {
|
|
state.next_token = after_then
|
|
}
|
|
end :: if (alternative) |block|
|
|
ast.builder_span(state.builder, block).end
|
|
else
|
|
ast.builder_span(state.builder, then_block).end
|
|
return try ast.add_if_stmt(
|
|
state.builder,
|
|
ast.Span{
|
|
start = keyword.span.start,
|
|
end = end,
|
|
source = keyword.span.source,
|
|
},
|
|
condition,
|
|
then_block,
|
|
alternative,
|
|
)
|
|
}
|
|
|
|
@hide:file
|
|
parse_while proc(state @mut State, allow_else bool) ast.NodeId ! ParseError {
|
|
keyword :: try consume(state, .while)
|
|
condition :: try parse_guard(state)
|
|
body :: try parse_arm(state, allow_else)
|
|
return try ast.add_while_stmt(
|
|
state.builder,
|
|
ast.Span{
|
|
start = keyword.span.start,
|
|
end = ast.builder_span(state.builder, body).end,
|
|
source = keyword.span.source,
|
|
},
|
|
condition,
|
|
body,
|
|
)
|
|
}
|
|
|
|
@hide:file
|
|
CaptureList :: struct {
|
|
item ?ast.NodeId = null
|
|
index ?ast.NodeId = null
|
|
pointer bool = false
|
|
indexed bool = false
|
|
}
|
|
|
|
@hide:file
|
|
parse_capture_name proc(state @mut State) ?ast.NodeId ! ParseError {
|
|
name :: try consume(state, .ident)
|
|
capture ?ast.NodeId := null
|
|
if (
|
|
name.span.end != name.span.start + 1
|
|
or state.builder.source[usize(name.span.start)] != '_'
|
|
) capture = try ast.add_identifier(
|
|
state.builder,
|
|
name.span,
|
|
name.data.str_id,
|
|
)
|
|
return capture
|
|
}
|
|
|
|
@hide:file
|
|
parse_captures proc(state @mut State) CaptureList ! ParseError {
|
|
_ = try consume(state, .pipe)
|
|
captures := CaptureList{}
|
|
if next_is(state, .at) {
|
|
captures.pointer = true
|
|
state.next_token += 1
|
|
}
|
|
captures.item = try parse_capture_name(state)
|
|
if next_is(state, .comma) {
|
|
captures.indexed = true
|
|
state.next_token += 1
|
|
captures.index = try parse_capture_name(state)
|
|
}
|
|
_ = try consume(state, .pipe)
|
|
return captures
|
|
}
|
|
|
|
@hide:file
|
|
parse_for proc(state @mut State, allow_else bool) ast.NodeId ! ParseError {
|
|
keyword :: try consume(state, .for)
|
|
skip_newlines(state)
|
|
iterable_start :: state.next_token
|
|
iterable := try parse_guard_expr(state)
|
|
grouped :: parenthesized_guard(state, iterable_start)
|
|
skip_newlines(state)
|
|
captures :: parse_captures(state) catch |err| {
|
|
match err {
|
|
.unexpected_token: {}
|
|
else: return err
|
|
}
|
|
diagnostic :: try diagnostics.add(
|
|
state.diagnostics,
|
|
.unexpected_token,
|
|
current(state).span,
|
|
)
|
|
iterable = try ast.add_invalid_expr(
|
|
state.builder,
|
|
ast.builder_span(state.builder, iterable),
|
|
diagnostic,
|
|
)
|
|
while (
|
|
!next_is(state, .open_curly) and !next_is(state, .close_curly)
|
|
and !next_is(state, .newline) and !next_is(state, .eof)
|
|
and !next_is(state, .else)
|
|
) state.next_token += 1
|
|
yield CaptureList{}
|
|
}
|
|
after_captures :: state.next_token
|
|
skip_newlines(state)
|
|
if (
|
|
!next_is(state, .open_curly) and !grouped
|
|
and ast.builder_expr_kind(state.builder, iterable) != .call
|
|
and ast.builder_expr_kind(state.builder, iterable) != .invalid
|
|
) {
|
|
diagnostic :: try diagnostics.add(state.diagnostics, .unexpected_token, current(state).span)
|
|
iterable = try ast.add_invalid_expr(
|
|
state.builder, ast.builder_span(state.builder, iterable), diagnostic,
|
|
)
|
|
}
|
|
# a malformed capture list must not swallow the next line's sibling.
|
|
if (
|
|
ast.builder_expr_kind(state.builder, iterable) == .invalid
|
|
and !next_is(state, .open_curly)
|
|
) state.next_token = after_captures
|
|
body :: try parse_arm(state, allow_else)
|
|
return try ast.add_for_stmt(
|
|
state.builder,
|
|
ast.Span{
|
|
start = keyword.span.start,
|
|
end = ast.builder_span(state.builder, body).end,
|
|
source = keyword.span.source,
|
|
},
|
|
iterable, captures.item, captures.index, captures.pointer, captures.indexed, body,
|
|
)
|
|
}
|
|
|
|
@hide:file
|
|
parse_guard_expr proc(state @mut State) ast.NodeId ! ParseError {
|
|
previous :: state.allow_record_literals
|
|
state.allow_record_literals = false
|
|
defer state.allow_record_literals = previous
|
|
return try parse_expr(state)
|
|
}
|
|
|
|
@hide:file
|
|
parse_guard proc(state @mut State) ast.NodeId ! ParseError {
|
|
condition_start :: state.next_token
|
|
condition := parse_guard_expr(state) catch |err| {
|
|
match err {
|
|
.unexpected_token: {}
|
|
else: return err
|
|
}
|
|
diagnostic :: try diagnostics.add(state.diagnostics, .unexpected_token, current(state).span)
|
|
yield try ast.add_invalid_expr(state.builder, current(state).span, diagnostic)
|
|
}
|
|
if (ast.builder_expr_kind(state.builder, condition) == .invalid) {
|
|
# discard a malformed guard's tail, but leave its body and enclosing block intact.
|
|
while (
|
|
!next_is(state, .open_curly) and !next_is(state, .close_curly)
|
|
and !next_is(state, .newline) and !next_is(state, .eof)
|
|
and !next_is(state, .else) and !next_is(state, .return)
|
|
and !next_is(state, .if) and !next_is(state, .while)
|
|
and !next_is(state, .for)
|
|
and !next_is(state, .break) and !next_is(state, .continue)
|
|
) {
|
|
if (next_is(state, .ident) and next_n_is(state, 1, .equal)) break
|
|
state.next_token += 1
|
|
}
|
|
}
|
|
if (
|
|
!next_is(state, .open_curly)
|
|
and !parenthesized_guard(state, condition_start)
|
|
and ast.builder_expr_kind(state.builder, condition) != .call
|
|
and ast.builder_expr_kind(state.builder, condition) != .invalid
|
|
) {
|
|
diagnostic :: try diagnostics.add(state.diagnostics, .unexpected_token, current(state).span)
|
|
condition = try ast.add_invalid_expr(
|
|
state.builder,
|
|
ast.builder_span(state.builder, condition),
|
|
diagnostic,
|
|
)
|
|
}
|
|
return condition
|
|
}
|
|
|
|
@hide:file
|
|
parenthesized_guard proc(state @State, start usize) bool {
|
|
if (state.tokens[start].kind != .open_paren) return false
|
|
depth usize := 0
|
|
for start..state.next_token |index| {
|
|
kind :: state.tokens[index].kind
|
|
if (kind == .open_paren) depth += 1
|
|
if (kind == .close_paren) {
|
|
depth -= 1
|
|
if (depth == 0) return index + 1 == state.next_token
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
@hide:file
|
|
parse_arm proc(state @mut State, allow_else bool) ast.NodeId ! ParseError {
|
|
if next_is(state, .open_curly) return try parse_block(state)
|
|
start :: state.next_token
|
|
statement :: parse_statement(state, allow_else) catch |err| {
|
|
match err {
|
|
.unexpected_token: {}
|
|
else: return err
|
|
}
|
|
diagnostic :: try unexpected_diagnostic(state)
|
|
span :: synchronize(state, start, true, true, false)
|
|
yield try ast.add_invalid_stmt(state.builder, span, diagnostic)
|
|
}
|
|
statements [1]ast.NodeId :: [statement]
|
|
return try ast.add_block(
|
|
state.builder,
|
|
ast.builder_span(state.builder, statement),
|
|
statements[..],
|
|
)
|
|
}
|
|
|
|
|
|
@hide:file
|
|
parse_statement_decl proc(state @mut State) ast.NodeId ! ParseError {
|
|
name :: try consume(state, .ident)
|
|
type_id ?ast.NodeId :: if (starts_type(current(state).kind))
|
|
try parse_type(state)
|
|
else
|
|
null
|
|
|
|
assignment :: try consume_either(state, &[.double_colon, .colon_equal])
|
|
initializer :: try parse_expr(state)
|
|
end :: ast.builder_span(state.builder, initializer).end
|
|
mutability :: if (assignment.kind == .double_colon) .constant else .variable
|
|
return try ast.add_statement_decl(
|
|
state.builder,
|
|
ast.Span{
|
|
start = name.span.start,
|
|
end = end,
|
|
source = name.span.source,
|
|
},
|
|
name.span,
|
|
name.data.str_id,
|
|
type_id,
|
|
mutability,
|
|
initializer,
|
|
)
|
|
}
|
|
|
|
@hide:file
|
|
parse_type proc(state @mut State) ast.NodeId ! ParseError {
|
|
if (next_is(state, .at) or next_is(state, .star)) {
|
|
token :: current(state)
|
|
state.next_token += 1
|
|
mutable :: next_is(state, .mut)
|
|
if (mutable) state.next_token += 1
|
|
element :: try parse_type(state)
|
|
return try ast.add_pointer_type(state.builder, ast.Span{
|
|
start = token.span.start,
|
|
end = ast.builder_span(state.builder, element).end,
|
|
source = token.span.source,
|
|
}, element, mutable, token.kind == .star)
|
|
}
|
|
if next_is(state, .open_bracket) {
|
|
open :: try consume(state, .open_bracket)
|
|
count ?ast.NodeId := null
|
|
if (
|
|
next_is(state, .ident)
|
|
and current(state).span.end == current(state).span.start + 1
|
|
and state.builder.source[usize(current(state).span.start)] == '_'
|
|
) {
|
|
state.next_token += 1
|
|
} else {
|
|
count = try parse_expr(state)
|
|
}
|
|
_ = try consume(state, .close_bracket)
|
|
mutable :: next_is(state, .mut)
|
|
if (mutable) state.next_token += 1
|
|
element :: try parse_type(state)
|
|
return try ast.add_array_type(
|
|
state.builder,
|
|
ast.Span{
|
|
start = open.span.start,
|
|
end = ast.builder_span(state.builder, element).end,
|
|
source = open.span.source,
|
|
},
|
|
count, element, mutable,
|
|
)
|
|
}
|
|
if next_is(state, .struct) return try parse_record_type(state)
|
|
if next_is(state, .enum) return try parse_enum_type(state)
|
|
if next_is(state, .union) return try parse_union_type(state)
|
|
if (next_is(state, .distinct) or next_is(state, .alias)) return try parse_type_wrapper(state)
|
|
token :: try consume(state, .ident)
|
|
type := try ast.add_identifier(state.builder, token.span, token.data.str_id)
|
|
while next_is(state, .dot) {
|
|
state.next_token += 1
|
|
member :: try consume_member(state, false)
|
|
member_id :: try ast.add_identifier(state.builder, member.span, member.data.str_id)
|
|
type = try ast.add_selection(state.builder, ast.Span{
|
|
start = token.span.start,
|
|
end = member.span.end,
|
|
source = token.span.source,
|
|
}, type, member_id)
|
|
}
|
|
return type
|
|
}
|
|
|
|
@hide:file
|
|
starts_type proc(kind lexer.TokenKind) bool {
|
|
return (
|
|
kind == .ident or kind == .open_bracket or kind == .struct
|
|
or kind == .enum or kind == .union or kind == .distinct or kind == .alias
|
|
or kind == .at or kind == .star
|
|
)
|
|
}
|
|
|
|
@hide:file
|
|
member_name proc(kind lexer.TokenKind) bool {
|
|
return match kind {
|
|
.ident, .if, .else, .for, .while, .proc, .return, .break, .continue,
|
|
.true, .false, .and, .or, .mut, .struct, .enum, .union, .distinct,
|
|
.alias, .match, .import, .test: true
|
|
else: false
|
|
}
|
|
}
|
|
|
|
@hide:file
|
|
consume_member proc(state @mut State, numeric bool) lexer.Token ! SyntaxError {
|
|
token :: current(state)
|
|
if (!member_name(token.kind) and !(numeric and token.kind == .int)) return .unexpected_token
|
|
state.next_token += 1
|
|
return token
|
|
}
|
|
|
|
@hide:file
|
|
parse_type_wrapper proc(state @mut State) ast.NodeId ! ParseError {
|
|
keyword :: current(state)
|
|
state.next_token += 1
|
|
operand :: try parse_type(state)
|
|
return try ast.add_type_operand(state.builder, ast.Span{
|
|
start = keyword.span.start,
|
|
end = ast.builder_span(state.builder, operand).end,
|
|
source = keyword.span.source,
|
|
}, operand, keyword.kind == .alias)
|
|
}
|
|
|
|
@hide:file
|
|
parse_enum_type proc(state @mut State) ast.NodeId ! ParseError {
|
|
keyword :: try consume(state, .enum)
|
|
backing ?ast.NodeId := null
|
|
if next_is(state, .open_paren) {
|
|
state.next_token += 1
|
|
backing = try parse_type(state)
|
|
_ = try consume(state, .close_paren)
|
|
}
|
|
skip_newlines(state)
|
|
_ = try consume(state, .open_curly)
|
|
members arraylist.ArrayList(ast.NodeId) := arraylist.init(state.allocator)
|
|
defer arraylist.deinit(&members)
|
|
skip_newlines(state)
|
|
while (!next_is(state, .close_curly) and !next_is(state, .eof)) {
|
|
if (
|
|
next_is(state, .ident)
|
|
and (next_n_is(state, 1, .double_colon) or next_n_is(state, 1, .colon_equal) or next_n_is(state, 1, .proc))
|
|
) return try invalid_record(state, keyword.span.start, false)
|
|
start :: state.next_token
|
|
member :: parse_enum_member(state) catch |err| {
|
|
match err {
|
|
.unexpected_token: {}
|
|
else: return err
|
|
}
|
|
diagnostic :: try unexpected_diagnostic(state)
|
|
span :: recover_record_field(state, start)
|
|
invalid :: try ast.add_invalid_expr(state.builder, span, diagnostic)
|
|
yield try ast.add_field(state.builder, span, null, null, invalid)
|
|
}
|
|
try arraylist.append(&members, member)
|
|
if next_is(state, .close_curly) break
|
|
if next_is(state, .comma) state.next_token += 1
|
|
else if (!next_is(state, .newline)) return try invalid_record(state, keyword.span.start, true)
|
|
skip_newlines(state)
|
|
}
|
|
if (!next_is(state, .close_curly)) return try invalid_record(state, keyword.span.start, false)
|
|
close :: try consume(state, .close_curly)
|
|
return try ast.add_enum_type(state.builder, ast.Span{
|
|
start = keyword.span.start, end = close.span.end, source = keyword.span.source,
|
|
}, backing, members.items)
|
|
}
|
|
|
|
@hide:file
|
|
parse_enum_member proc(state @mut State) ast.NodeId ! ParseError {
|
|
token :: try consume_member(state, false)
|
|
name :: try ast.add_identifier(state.builder, token.span, token.data.str_id)
|
|
value ?ast.NodeId := null
|
|
if next_is(state, .equal) {
|
|
state.next_token += 1
|
|
value = try parse_expr(state)
|
|
}
|
|
end :: if (value) |id| ast.builder_span(state.builder, id).end else token.span.end
|
|
return try ast.add_field(state.builder, ast.Span{
|
|
start = token.span.start,
|
|
end = end,
|
|
source = token.span.source,
|
|
}, name, null, value)
|
|
}
|
|
|
|
@hide:file
|
|
parse_union_type proc(state @mut State) ast.NodeId ! ParseError {
|
|
keyword :: try consume(state, .union)
|
|
tag ?ast.NodeId := null
|
|
auto_tag := false
|
|
if next_is(state, .open_paren) {
|
|
state.next_token += 1
|
|
if next_is(state, .enum) {
|
|
state.next_token += 1
|
|
auto_tag = true
|
|
} else {
|
|
tag = try parse_type(state)
|
|
}
|
|
_ = try consume(state, .close_paren)
|
|
}
|
|
skip_newlines(state)
|
|
_ = try consume(state, .open_curly)
|
|
fields arraylist.ArrayList(ast.NodeId) := arraylist.init(state.allocator)
|
|
defer arraylist.deinit(&fields)
|
|
skip_newlines(state)
|
|
while (!next_is(state, .close_curly) and !next_is(state, .eof)) {
|
|
if (
|
|
next_is(state, .ident)
|
|
and (next_n_is(state, 1, .double_colon) or next_n_is(state, 1, .colon_equal) or next_n_is(state, 1, .proc))
|
|
) return try invalid_record(state, keyword.span.start, false)
|
|
start :: state.next_token
|
|
field :: parse_union_field(state, auto_tag or tag != null) catch |err| {
|
|
match err {
|
|
.unexpected_token: {}
|
|
else: return err
|
|
}
|
|
diagnostic :: try unexpected_diagnostic(state)
|
|
span :: recover_record_field(state, start)
|
|
invalid :: try ast.add_invalid_expr(state.builder, span, diagnostic)
|
|
yield try ast.add_field(state.builder, span, null, invalid, null)
|
|
}
|
|
try arraylist.append(&fields, field)
|
|
if next_is(state, .close_curly) break
|
|
if next_is(state, .comma) state.next_token += 1
|
|
else if (!next_is(state, .newline)) return try invalid_record(state, keyword.span.start, true)
|
|
skip_newlines(state)
|
|
}
|
|
if (!next_is(state, .close_curly)) return try invalid_record(state, keyword.span.start, false)
|
|
close :: try consume(state, .close_curly)
|
|
return try ast.add_union_type(state.builder, ast.Span{
|
|
start = keyword.span.start, end = close.span.end, source = keyword.span.source,
|
|
}, tag, auto_tag, fields.items)
|
|
}
|
|
|
|
@hide:file
|
|
parse_union_field proc(state @mut State, tagged bool) ast.NodeId ! ParseError {
|
|
token :: if (tagged) try consume_member(state, false) else try consume(state, .ident)
|
|
name :: try ast.add_identifier(state.builder, token.span, token.data.str_id)
|
|
type :: try parse_type(state)
|
|
return try ast.add_field(state.builder, ast.Span{
|
|
start = token.span.start, end = ast.builder_span(state.builder, type).end, source = token.span.source,
|
|
}, name, type, null)
|
|
}
|
|
|
|
@hide:file
|
|
parse_match proc(state @mut State) ast.NodeId ! ParseError {
|
|
keyword :: try consume(state, .match)
|
|
skip_newlines(state)
|
|
subject :: try parse_guard_expr(state)
|
|
skip_newlines(state)
|
|
_ = try consume(state, .open_curly)
|
|
previous_depth :: state.paren_depth
|
|
state.paren_depth = 0
|
|
defer state.paren_depth = previous_depth
|
|
arms arraylist.ArrayList(ast.NodeId) := arraylist.init(state.allocator)
|
|
defer arraylist.deinit(&arms)
|
|
skip_newlines(state)
|
|
while (!next_is(state, .close_curly) and !next_is(state, .eof)) {
|
|
if (
|
|
next_is(state, .ident)
|
|
and (next_n_is(state, 1, .double_colon) or next_n_is(state, 1, .colon_equal) or next_n_is(state, 1, .proc))
|
|
) return try invalid_record(state, keyword.span.start, false)
|
|
start :: state.next_token
|
|
arm :: parse_match_arm(state) catch |err| {
|
|
match err {
|
|
.unexpected_token: {}
|
|
else: return err
|
|
}
|
|
diagnostic :: try unexpected_diagnostic(state)
|
|
span :: recover_record_field(state, start)
|
|
invalid :: try ast.add_invalid_expr(state.builder, span, diagnostic)
|
|
yield try ast.add_match_arm(state.builder, span, &[], null, invalid, true, false)
|
|
}
|
|
try arraylist.append(&arms, arm)
|
|
if next_is(state, .close_curly) break
|
|
if next_is(state, .comma) state.next_token += 1
|
|
else if (!next_is(state, .newline)) return try invalid_record(state, keyword.span.start, true)
|
|
skip_newlines(state)
|
|
}
|
|
if (!next_is(state, .close_curly)) return try invalid_record(state, keyword.span.start, false)
|
|
close :: try consume(state, .close_curly)
|
|
return try ast.add_match(state.builder, ast.Span{
|
|
start = keyword.span.start, end = close.span.end, source = keyword.span.source,
|
|
}, subject, arms.items)
|
|
}
|
|
|
|
@hide:file
|
|
parse_match_arm proc(state @mut State) ast.NodeId ! ParseError {
|
|
start :: current(state).span
|
|
patterns arraylist.ArrayList(ast.NodeId) := arraylist.init(state.allocator)
|
|
defer arraylist.deinit(&patterns)
|
|
catch_all :: next_is(state, .else)
|
|
capture ?ast.NodeId := null
|
|
if (catch_all) state.next_token += 1
|
|
else {
|
|
while true {
|
|
try arraylist.append(&patterns, try parse_guard_expr(state))
|
|
if (!next_is(state, .comma)) break
|
|
state.next_token += 1
|
|
skip_newlines(state)
|
|
}
|
|
if next_is(state, .pipe) {
|
|
state.next_token += 1
|
|
# pointer payload captures are outside the current runtime representation.
|
|
if (next_is(state, .at) or next_is(state, .star)) return .unexpected_token
|
|
token :: try consume(state, .ident)
|
|
if (
|
|
token.span.end != token.span.start + 1
|
|
or state.builder.source[usize(token.span.start)] != '_'
|
|
) capture = try ast.add_identifier(state.builder, token.span, token.data.str_id)
|
|
_ = try consume(state, .pipe)
|
|
}
|
|
}
|
|
_ = try consume(state, .colon)
|
|
skip_newlines(state)
|
|
previous :: state.match_arm_body
|
|
state.match_arm_body = true
|
|
defer state.match_arm_body = previous
|
|
statement_body :: match current(state).kind {
|
|
.open_curly, .return, .break, .continue, .if, .while, .for: true
|
|
else: false
|
|
}
|
|
if (statement_body or named_binding_starts(state)) {
|
|
body :: try parse_arm(state, false)
|
|
return try ast.add_match_arm(state.builder, ast.Span{
|
|
start = start.start, end = ast.builder_span(state.builder, body).end, source = start.source,
|
|
}, patterns.items, capture, body, false, catch_all)
|
|
}
|
|
value :: try parse_expr(state)
|
|
if next_is(state, .equal) {
|
|
statement :: try parse_assignment(state, value, false)
|
|
statements [1]ast.NodeId :: [statement]
|
|
body :: try ast.add_block(state.builder, ast.builder_span(state.builder, statement), statements[..])
|
|
return try ast.add_match_arm(state.builder, ast.Span{
|
|
start = start.start, end = ast.builder_span(state.builder, body).end, source = start.source,
|
|
}, patterns.items, capture, body, false, catch_all)
|
|
}
|
|
return try ast.add_match_arm(state.builder, ast.Span{
|
|
start = start.start, end = ast.builder_span(state.builder, value).end, source = start.source,
|
|
}, patterns.items, capture, value, true, catch_all)
|
|
}
|
|
|
|
@hide:file
|
|
parse_record_type proc(state @mut State) ast.NodeId ! ParseError {
|
|
keyword :: try consume(state, .struct)
|
|
return try parse_record(state, null, keyword.span.start, true)
|
|
}
|
|
|
|
@hide:file
|
|
parse_record_literal proc(
|
|
state @mut State, type ?ast.NodeId, start u32,
|
|
) ast.NodeId ! ParseError {
|
|
return try parse_record(state, type, start, false)
|
|
}
|
|
|
|
@hide:file
|
|
parse_record proc(
|
|
state @mut State, type ?ast.NodeId, start u32, declaration bool,
|
|
) ast.NodeId ! ParseError {
|
|
open :: try consume(state, .open_curly)
|
|
previous :: state.allow_record_literals
|
|
state.allow_record_literals = true
|
|
defer state.allow_record_literals = previous
|
|
fields arraylist.ArrayList(ast.NodeId) := arraylist.init(state.allocator)
|
|
defer arraylist.deinit(&fields)
|
|
skip_newlines(state)
|
|
while (!next_is(state, .close_curly) and !next_is(state, .eof)) {
|
|
# leave the next declaration intact when a record's closing brace is missing.
|
|
if (
|
|
next_is(state, .ident)
|
|
and (next_n_is(state, 1, .double_colon) or next_n_is(state, 1, .colon_equal) or next_n_is(state, 1, .proc))
|
|
) return try invalid_record(state, start, false)
|
|
field_start :: state.next_token
|
|
field :: parse_record_field(state, declaration) catch |err| {
|
|
match err {
|
|
.unexpected_token: {}
|
|
else: return err
|
|
}
|
|
diagnostic :: try unexpected_diagnostic(state)
|
|
span :: recover_record_field(state, field_start)
|
|
invalid :: try ast.add_invalid_expr(state.builder, span, diagnostic)
|
|
recovered_type ?ast.NodeId :: if (declaration) invalid else null
|
|
recovered_value ?ast.NodeId :: if (declaration) null else invalid
|
|
yield try ast.add_field(state.builder, span, null, recovered_type, recovered_value)
|
|
}
|
|
try arraylist.append(&fields, field)
|
|
if next_is(state, .close_curly) break
|
|
if next_is(state, .comma) {
|
|
state.next_token += 1
|
|
skip_newlines(state)
|
|
continue
|
|
}
|
|
if (declaration and next_is(state, .newline)) {
|
|
skip_newlines(state)
|
|
continue
|
|
}
|
|
after_field :: state.next_token
|
|
skip_newlines(state)
|
|
if next_is(state, .close_curly) break
|
|
state.next_token = after_field
|
|
return try invalid_record(state, start, true)
|
|
}
|
|
if (!next_is(state, .close_curly)) return try invalid_record(state, start, false)
|
|
close :: try consume(state, .close_curly)
|
|
span :: ast.Span{ start = start, end = close.span.end, source = open.span.source }
|
|
if (declaration) return try ast.add_record_type(state.builder, span, fields.items)
|
|
return try ast.add_record_literal(state.builder, span, type, fields.items)
|
|
}
|
|
|
|
@hide:file
|
|
parse_record_field proc(state @mut State, declaration bool) ast.NodeId ! ParseError {
|
|
start :: current(state).span
|
|
start_token :: state.next_token
|
|
name ?ast.NodeId := null
|
|
type ?ast.NodeId := null
|
|
value ?ast.NodeId := null
|
|
named :: member_name(current(state).kind) and (
|
|
(!declaration and next_n_is(state, 1, .equal))
|
|
or (declaration and starts_type(state.tokens[state.next_token + 1].kind))
|
|
)
|
|
if (named) {
|
|
token :: if (declaration) try consume(state, .ident) else try consume_member(state, false)
|
|
name = try ast.add_identifier(state.builder, token.span, token.data.str_id)
|
|
}
|
|
if (declaration) {
|
|
type = try parse_type(state)
|
|
if next_is(state, .equal) {
|
|
state.next_token += 1
|
|
value = try parse_expr(state)
|
|
}
|
|
} else {
|
|
if (named) _ = try consume(state, .equal)
|
|
value = try parse_expr(state)
|
|
}
|
|
end :: if (state.next_token > start_token) state.tokens[state.next_token - 1].span.end else start.end
|
|
return try ast.add_field(state.builder, ast.Span{
|
|
start = start.start,
|
|
end = end,
|
|
source = start.source,
|
|
}, name, type, value)
|
|
}
|
|
|
|
@hide:file
|
|
recover_record_field proc(state @mut State, start usize) ast.Span {
|
|
depth usize := 0
|
|
for (start..state.next_token) |index| {
|
|
kind :: state.tokens[index].kind
|
|
if (kind == .open_curly or kind == .open_paren or kind == .open_bracket) depth += 1
|
|
if ((kind == .close_curly or kind == .close_paren or kind == .close_bracket) and depth > 0) depth -= 1
|
|
}
|
|
while (!next_is(state, .eof)) {
|
|
kind :: current(state).kind
|
|
if (depth == 0 and (kind == .comma or kind == .newline or kind == .close_curly)) break
|
|
if (kind == .open_curly or kind == .open_paren or kind == .open_bracket) depth += 1
|
|
if (kind == .close_curly or kind == .close_paren or kind == .close_bracket) {
|
|
if (depth == 0) break
|
|
depth -= 1
|
|
}
|
|
state.next_token += 1
|
|
}
|
|
first :: state.tokens[start].span
|
|
end :: if (state.next_token > start) state.tokens[state.next_token - 1].span.end else first.end
|
|
return ast.Span{
|
|
start = first.start,
|
|
end = end,
|
|
source = first.source,
|
|
}
|
|
}
|
|
|
|
@hide:file
|
|
invalid_record proc(state @mut State, start u32, recover_tail bool) ast.NodeId ! ParseError {
|
|
diagnostic :: try unexpected_diagnostic(state)
|
|
if (recover_tail) {
|
|
while (!next_is(state, .newline) and !next_is(state, .close_curly) and !next_is(state, .eof)) {
|
|
before :: state.next_token
|
|
_ = recover_record_field(state, before)
|
|
if (next_is(state, .comma)) state.next_token += 1
|
|
if (state.next_token == before) break
|
|
}
|
|
if (next_is(state, .close_curly)) state.next_token += 1
|
|
}
|
|
return try ast.add_invalid_expr(state.builder, ast.Span{
|
|
start = start,
|
|
end = state.tokens[state.next_token - 1].span.end,
|
|
source = state.builder.source_id,
|
|
}, diagnostic)
|
|
}
|
|
|
|
@hide:file
|
|
parse_expr proc(state @mut State) ast.NodeId ! ParseError {
|
|
return try parse_expr_bp(state, BindingPowerScalar(0))
|
|
}
|
|
|
|
@hide:file
|
|
parse_expr_bp proc(state @mut State, min_bp BindingPowerScalar) ast.NodeId ! ParseError {
|
|
skip_operand_newlines(state)
|
|
token := current(state)
|
|
expression_start :: state.next_token
|
|
left := if (prefix_binding_power(token.kind)) |right_bp| {
|
|
state.next_token += 1
|
|
operand :: try parse_expr_bp(state, right_bp)
|
|
operand_span :: ast.builder_span(state.builder, operand)
|
|
operator ast.UnaryOperator :: match token.kind {
|
|
.bang: .logical_not
|
|
.ampersand: .address_of
|
|
else: .negate
|
|
}
|
|
yield try ast.add_unary(
|
|
state.builder,
|
|
ast.Span{
|
|
start = token.span.start,
|
|
end = operand_span.end,
|
|
source = token.span.source,
|
|
},
|
|
operator,
|
|
operand,
|
|
)
|
|
} else {
|
|
yield try parse_primary(state)
|
|
}
|
|
left_grouped := parenthesized_guard(state, expression_start)
|
|
|
|
while true {
|
|
skip_continuation_newlines(state)
|
|
token = current(state)
|
|
if (token.kind == .caret) {
|
|
state.next_token += 1
|
|
left = try ast.add_unary(state.builder, ast.Span{
|
|
start = state.tokens[expression_start].span.start,
|
|
end = token.span.end,
|
|
source = token.span.source,
|
|
}, .dereference, left)
|
|
left_grouped = false
|
|
continue
|
|
}
|
|
if (token.kind == .open_paren) {
|
|
left = try parse_call(state, left, state.tokens[expression_start].span.start)
|
|
left_grouped = false
|
|
continue
|
|
}
|
|
if (token.kind == .open_bracket) {
|
|
left = try parse_index(state, left, state.tokens[expression_start].span.start)
|
|
left_grouped = false
|
|
continue
|
|
}
|
|
if (token.kind == .open_curly and state.allow_record_literals) {
|
|
left = try parse_record_literal(state, left, state.tokens[expression_start].span.start)
|
|
left_grouped = false
|
|
continue
|
|
}
|
|
if (token.kind == .dot) {
|
|
state.next_token += 1
|
|
member :: try consume_member(state, true)
|
|
member_id :: if (member.kind == .int)
|
|
try ast.add_literal(state.builder, .int, member.span)
|
|
else
|
|
try ast.add_identifier(state.builder, member.span, member.data.str_id)
|
|
left = try ast.add_selection(
|
|
state.builder,
|
|
ast.Span{
|
|
start = state.tokens[expression_start].span.start,
|
|
end = member.span.end,
|
|
source = member.span.source,
|
|
},
|
|
left, member_id,
|
|
)
|
|
left_grouped = false
|
|
continue
|
|
}
|
|
if (token.kind == .range or token.kind == .range_inclusive) {
|
|
if (min_bp > BindingPowerScalar(0)) break
|
|
left = try validate_range_bound(state, left, left_grouped)
|
|
state.next_token += 1
|
|
right_start :: state.next_token
|
|
right := try parse_expr_bp(state, BindingPowerScalar(1))
|
|
right = try validate_range_bound(state, right, parenthesized_guard(state, right_start))
|
|
left = try ast.add_range(
|
|
state.builder,
|
|
ast.Span{
|
|
start = state.tokens[expression_start].span.start,
|
|
end = state.tokens[state.next_token - 1].span.end,
|
|
source = state.builder.source_id,
|
|
},
|
|
left, right, token.kind == .range_inclusive,
|
|
)
|
|
left_grouped = false
|
|
continue
|
|
}
|
|
if (infix_binding_power(token.kind)) |power| {
|
|
if (power.left < min_bp) break
|
|
state.next_token += 1
|
|
right :: try parse_expr_bp(state, power.right)
|
|
left_span :: ast.builder_span(state.builder, left)
|
|
right_span :: ast.builder_span(state.builder, right)
|
|
left = try ast.add_binary(
|
|
state.builder,
|
|
ast.Span{
|
|
start = left_span.start,
|
|
end = right_span.end,
|
|
source = left_span.source,
|
|
},
|
|
binary_operator(token.kind),
|
|
left,
|
|
right,
|
|
)
|
|
left_grouped = false
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
return left
|
|
}
|
|
|
|
@hide:file
|
|
validate_range_bound proc(
|
|
state @mut State, bound ast.NodeId, grouped bool,
|
|
) ast.NodeId ! (mem.AllocError | diagnostics.CapacityError) {
|
|
if (grouped) return bound
|
|
match ast.builder_expr_kind(state.builder, bound) {
|
|
.unary, .binary, .range: {
|
|
span :: ast.builder_span(state.builder, bound)
|
|
diagnostic :: try diagnostics.add(state.diagnostics, .unexpected_token, span)
|
|
return try ast.add_invalid_expr(state.builder, span, diagnostic)
|
|
}
|
|
else: return bound
|
|
}
|
|
}
|
|
|
|
@hide:file
|
|
parse_call proc(state @mut State, callee ast.NodeId, start u32) ast.NodeId ! ParseError {
|
|
_ = try consume(state, .open_paren)
|
|
previous :: state.allow_record_literals
|
|
state.allow_record_literals = true
|
|
defer state.allow_record_literals = previous
|
|
state.paren_depth += 1
|
|
defer state.paren_depth -= 1
|
|
skip_operand_newlines(state)
|
|
arguments arraylist.ArrayList(ast.NodeId) := arraylist.init(state.allocator)
|
|
defer arraylist.deinit(&arguments)
|
|
argument_start :: state.next_token
|
|
node_count :: state.builder.storage.nodes.items.len
|
|
extra_count :: state.builder.storage.extra.items.len
|
|
diagnostic_count :: state.diagnostics.list.items.len
|
|
|
|
while (!is_call_boundary(current(state).kind)) {
|
|
argument :: try parse_expr(state)
|
|
try arraylist.append(&arguments, argument)
|
|
if (!next_is(state, .comma)) break
|
|
state.next_token += 1
|
|
skip_operand_newlines(state)
|
|
}
|
|
if (!previous and !next_is(state, .close_paren)) {
|
|
# a guard's body may look like a record argument until its missing ')' is known.
|
|
state.next_token = argument_start
|
|
state.builder.storage.nodes.items = state.builder.storage.nodes.items.ptr[..node_count]
|
|
state.builder.storage.spans.items = state.builder.storage.spans.items.ptr[..node_count]
|
|
state.builder.storage.extra.items = state.builder.storage.extra.items.ptr[..extra_count]
|
|
state.diagnostics.list.items = state.diagnostics.list.items.ptr[..diagnostic_count]
|
|
return .unexpected_token
|
|
}
|
|
close :: try consume(state, .close_paren)
|
|
return try ast.add_call(
|
|
state.builder,
|
|
ast.Span{
|
|
start = start,
|
|
end = close.span.end,
|
|
source = state.builder.source_id,
|
|
},
|
|
callee,
|
|
arguments.items,
|
|
)
|
|
}
|
|
|
|
@hide:file
|
|
parse_index proc(state @mut State, base ast.NodeId, start u32) ast.NodeId ! ParseError {
|
|
_ = try consume(state, .open_bracket)
|
|
previous :: state.allow_record_literals
|
|
state.allow_record_literals = true
|
|
defer state.allow_record_literals = previous
|
|
index :: try parse_expr(state)
|
|
if !next_is(state, .close_bracket) {
|
|
diagnostic :: try diagnostics.add(state.diagnostics, .unexpected_token, current(state).span)
|
|
return try ast.add_invalid_expr(
|
|
state.builder,
|
|
ast.Span{
|
|
start = start,
|
|
end = ast.builder_span(state.builder, index).end,
|
|
source = state.builder.source_id,
|
|
},
|
|
diagnostic,
|
|
)
|
|
}
|
|
close :: try consume(state, .close_bracket)
|
|
return try ast.add_index(
|
|
state.builder,
|
|
ast.Span{
|
|
start = start,
|
|
end = close.span.end,
|
|
source = state.builder.source_id,
|
|
},
|
|
base, index,
|
|
)
|
|
}
|
|
|
|
@hide:file
|
|
array_starts_type proc(state @State) bool {
|
|
depth usize := 0
|
|
for (state.next_token)..(state.tokens.len) |i| {
|
|
kind :: state.tokens[i].kind
|
|
if (kind == .open_bracket) depth += 1
|
|
if (kind == .close_bracket) {
|
|
depth -= 1
|
|
if (depth == 0) {
|
|
if (i + 1 >= state.tokens.len) return false
|
|
next :: state.tokens[i + 1].kind
|
|
return starts_type(next) or next == .mut
|
|
}
|
|
}
|
|
if (kind == .open_curly or kind == .close_curly or kind == .eof) return false
|
|
}
|
|
return false
|
|
}
|
|
|
|
@hide:file
|
|
parse_array_literal proc(state @mut State) ast.NodeId ! ParseError {
|
|
open :: try consume(state, .open_bracket)
|
|
previous :: state.allow_record_literals
|
|
state.allow_record_literals = true
|
|
defer state.allow_record_literals = previous
|
|
elements arraylist.ArrayList(ast.NodeId) := arraylist.init(state.allocator)
|
|
defer arraylist.deinit(&elements)
|
|
after_open :: state.next_token
|
|
skip_newlines(state)
|
|
if named_binding_starts(state) {
|
|
state.next_token = after_open
|
|
return try invalid_array_literal(state, open.span.start, open.span.end)
|
|
}
|
|
while !next_is(state, .close_bracket) {
|
|
element :: try parse_expr(state)
|
|
try arraylist.append(&elements, element)
|
|
after_element :: state.next_token
|
|
skip_newlines(state)
|
|
if next_is(state, .close_bracket) break
|
|
if !next_is(state, .comma) {
|
|
state.next_token = after_element
|
|
return try invalid_array_literal(state, open.span.start, ast.builder_span(state.builder, element).end)
|
|
}
|
|
state.next_token += 1
|
|
after_comma :: state.next_token
|
|
skip_newlines(state)
|
|
if named_binding_starts(state) {
|
|
state.next_token = after_comma
|
|
return try invalid_array_literal(state, open.span.start, ast.builder_span(state.builder, element).end)
|
|
}
|
|
}
|
|
close :: try consume(state, .close_bracket)
|
|
return try ast.add_array_literal(state.builder, ast.Span{
|
|
start = open.span.start,
|
|
end = close.span.end,
|
|
source = open.span.source,
|
|
}, elements.items)
|
|
}
|
|
|
|
@hide:file
|
|
invalid_array_literal proc(state @mut State, start, end u32) ast.NodeId ! ParseError {
|
|
diagnostic :: try diagnostics.add(state.diagnostics, .unexpected_token, current(state).span)
|
|
return try ast.add_invalid_expr(state.builder, ast.Span{
|
|
start = start,
|
|
end = end,
|
|
source = state.builder.source_id,
|
|
}, diagnostic)
|
|
}
|
|
|
|
@hide:file
|
|
pointer_binding_starts proc(state @State) bool {
|
|
depth := usize(0)
|
|
for (state.next_token + 2)..(state.tokens.len) |i| {
|
|
kind :: state.tokens[i].kind
|
|
if (kind == .open_curly or kind == .open_bracket or kind == .open_paren) depth += 1
|
|
if (kind == .close_curly or kind == .close_bracket or kind == .close_paren) {
|
|
if (depth == 0) return false
|
|
depth -= 1
|
|
}
|
|
if (depth != 0) continue
|
|
if (kind == .double_colon or kind == .colon_equal) return true
|
|
if (kind == .newline or kind == .eof or kind == .equal) return false
|
|
}
|
|
return false
|
|
}
|
|
|
|
@hide:file
|
|
named_binding_starts proc(state @State) bool {
|
|
return (
|
|
next_is(state, .ident)
|
|
and (
|
|
next_n_is(state, 1, .double_colon) or next_n_is(state, 1, .colon_equal)
|
|
or next_n_is(state, 1, .ident) or next_n_is(state, 1, .proc)
|
|
or next_n_is(state, 1, .at)
|
|
or (next_n_is(state, 1, .star) and pointer_binding_starts(state))
|
|
)
|
|
)
|
|
}
|
|
|
|
@hide:file
|
|
parse_primary proc(state @mut State) ast.NodeId ! ParseError {
|
|
token :: current(state)
|
|
match token.kind {
|
|
.at, .star: return try parse_type(state)
|
|
.struct: return try parse_record_type(state)
|
|
.enum: return try parse_enum_type(state)
|
|
.union: return try parse_union_type(state)
|
|
.distinct, .alias: return try parse_type_wrapper(state)
|
|
.match: return try parse_match(state)
|
|
.dot: {
|
|
state.next_token += 1
|
|
member :: try consume_member(state, false)
|
|
member_id :: try ast.add_identifier(state.builder, member.span, member.data.str_id)
|
|
return try ast.add_enum_literal(state.builder, ast.Span{
|
|
start = token.span.start, end = member.span.end, source = token.span.source,
|
|
}, member_id)
|
|
}
|
|
.open_curly: {
|
|
if (state.allow_record_literals) return try parse_record_literal(state, null, token.span.start)
|
|
diagnostic :: try diagnostics.add(state.diagnostics, .unexpected_token, token.span)
|
|
return try ast.add_invalid_expr(state.builder, token.span, diagnostic)
|
|
}
|
|
.open_bracket: {
|
|
if array_starts_type(state) return try parse_type(state)
|
|
return try parse_array_literal(state)
|
|
}
|
|
.open_paren: {
|
|
state.next_token += 1
|
|
previous :: state.allow_record_literals
|
|
state.allow_record_literals = true
|
|
defer state.allow_record_literals = previous
|
|
state.paren_depth += 1
|
|
defer state.paren_depth -= 1
|
|
expression :: try parse_expr(state)
|
|
if next_is(state, .close_paren) {
|
|
state.next_token += 1
|
|
return expression
|
|
}
|
|
diagnostic :: try diagnostics.add(
|
|
state.diagnostics,
|
|
.unexpected_token,
|
|
current(state).span,
|
|
)
|
|
return try ast.add_invalid_expr(state.builder, ast.Span{
|
|
start = token.span.start,
|
|
end = ast.builder_span(state.builder, expression).end,
|
|
source = token.span.source,
|
|
}, diagnostic)
|
|
}
|
|
.int: {
|
|
state.next_token += 1
|
|
return try ast.add_literal(state.builder, .int, token.span)
|
|
}
|
|
.float: {
|
|
state.next_token += 1
|
|
return try ast.add_literal(state.builder, .float, token.span)
|
|
}
|
|
.string: {
|
|
state.next_token += 1
|
|
return try ast.add_literal(state.builder, .string, token.span)
|
|
}
|
|
.true, .false: {
|
|
state.next_token += 1
|
|
return try ast.add_literal(state.builder, .bool, token.span)
|
|
}
|
|
.ident: {
|
|
state.next_token += 1
|
|
return try ast.add_identifier(state.builder, token.span, token.data.str_id)
|
|
}
|
|
else: {
|
|
diagnostic :: try unexpected_diagnostic(state)
|
|
|
|
if !is_expression_recovery_boundary(token.kind) {
|
|
state.next_token += 1
|
|
}
|
|
|
|
return try ast.add_invalid_expr(state.builder, token.span, diagnostic)
|
|
}
|
|
}
|
|
}
|
|
|
|
@hide:file
|
|
is_expression_boundary proc(kind lexer.TokenKind) bool {
|
|
return match kind {
|
|
.close_paren, .newline, .close_curly, .open_curly, .else, .return, .if, .while, .for, .eof: true
|
|
else: false
|
|
}
|
|
}
|
|
|
|
@hide:file
|
|
is_call_boundary proc(kind lexer.TokenKind) bool {
|
|
return (
|
|
kind == .break
|
|
or kind == .continue
|
|
or (kind != .open_curly and is_expression_boundary(kind))
|
|
)
|
|
}
|
|
|
|
@hide:file
|
|
is_expression_recovery_boundary proc(kind lexer.TokenKind) bool {
|
|
return (
|
|
kind == .comma
|
|
or kind == .close_bracket
|
|
or kind == .and
|
|
or kind == .or
|
|
or kind == .pipe
|
|
or is_expression_boundary(kind)
|
|
)
|
|
}
|
|
|
|
@hide:file
|
|
skip_operand_newlines proc(state @mut State) void {
|
|
if (state.paren_depth == 0 or !next_is(state, .newline)) return
|
|
start :: state.next_token
|
|
skip_newlines(state)
|
|
kind :: current(state).kind
|
|
if (kind != .close_paren and is_expression_boundary(kind)) state.next_token = start
|
|
# a missing operand must not consume the declaration or assignment on the next line.
|
|
if (
|
|
named_binding_starts(state)
|
|
or (next_is(state, .ident) and next_n_is(state, 1, .equal))
|
|
) state.next_token = start
|
|
}
|
|
|
|
@hide:file
|
|
skip_continuation_newlines proc(state @mut State) void {
|
|
if (state.paren_depth == 0 or !next_is(state, .newline)) return
|
|
start :: state.next_token
|
|
skip_newlines(state)
|
|
kind :: current(state).kind
|
|
if (
|
|
kind != .close_paren and kind != .comma and kind != .open_paren
|
|
and kind != .close_bracket and kind != .open_bracket
|
|
and kind != .range and kind != .range_inclusive
|
|
and infix_binding_power(kind) == null
|
|
) state.next_token = start
|
|
}
|
|
|
|
@hide:file
|
|
recover_decl proc(
|
|
state @mut State,
|
|
start usize,
|
|
) ast.NodeId ! (mem.AllocError | diagnostics.CapacityError) {
|
|
diagnostic :: try unexpected_diagnostic(state)
|
|
span :: synchronize(state, start, false, false, true)
|
|
return try ast.add_invalid_decl(state.builder, span, diagnostic)
|
|
}
|
|
|
|
@hide:file
|
|
recover_statement proc(
|
|
state @mut State,
|
|
start usize,
|
|
) ast.NodeId ! (mem.AllocError | diagnostics.CapacityError) {
|
|
diagnostic :: try unexpected_diagnostic(state)
|
|
span :: synchronize(state, start, true, false, true)
|
|
return try ast.add_invalid_stmt(state.builder, span, diagnostic)
|
|
}
|
|
|
|
@hide:file
|
|
synchronize proc(
|
|
state @mut State, start usize, stop_at_curly, allow_else, consume_newline bool,
|
|
) ast.Span {
|
|
first :: state.tokens[start].span
|
|
end := first.end
|
|
curly usize := 0
|
|
paren usize := 0
|
|
square usize := 0
|
|
for (start..state.next_token) |i| {
|
|
kind :: state.tokens[i].kind
|
|
if (kind == .open_paren) paren += 1
|
|
if (kind == .close_paren and paren > 0) paren -= 1
|
|
if (kind == .open_bracket) square += 1
|
|
if (kind == .close_bracket and square > 0) square -= 1
|
|
}
|
|
while !next_is(state, .eof) {
|
|
if (curly == 0) {
|
|
if (stop_at_curly and next_is(state, .close_curly)) break
|
|
if (allow_else and next_is(state, .else)) break
|
|
if (
|
|
next_is(state, .newline)
|
|
and !recovery_closes_delimiters(state, paren, square)
|
|
) break
|
|
}
|
|
kind :: current(state).kind
|
|
if (kind == .open_curly) curly += 1
|
|
if (kind == .close_curly and curly > 0) curly -= 1
|
|
if (kind == .open_paren) paren += 1
|
|
if (kind == .close_paren and paren > 0) paren -= 1
|
|
if (kind == .open_bracket) square += 1
|
|
if (kind == .close_bracket and square > 0) square -= 1
|
|
end = current(state).span.end
|
|
state.next_token += 1
|
|
}
|
|
if (consume_newline and next_is(state, .newline)) {
|
|
end = current(state).span.end
|
|
state.next_token += 1
|
|
}
|
|
return ast.Span{
|
|
start = first.start,
|
|
end = end,
|
|
source = first.source,
|
|
}
|
|
}
|
|
|
|
@hide:file
|
|
recovery_closes_delimiters proc(state @State, paren_depth, square_depth usize) bool {
|
|
if (paren_depth == 0 and square_depth == 0) return false
|
|
paren := paren_depth
|
|
square := square_depth
|
|
for (state.next_token)..(state.tokens.len) |i| {
|
|
kind :: state.tokens[i].kind
|
|
match kind {
|
|
.open_curly, .close_curly, .eof, .if, .else, .while, .for,
|
|
.return, .break, .continue: return false
|
|
.ident: {
|
|
if (i + 1 < state.tokens.len) {
|
|
next :: state.tokens[i + 1].kind
|
|
if (next == .double_colon or next == .colon_equal or next == .ident or next == .proc) {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
else: {}
|
|
}
|
|
if (kind == .open_paren) paren += 1
|
|
if (kind == .close_paren and paren > 0) paren -= 1
|
|
if (kind == .open_bracket) square += 1
|
|
if (kind == .close_bracket and square > 0) square -= 1
|
|
if (paren == 0 and square == 0) return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
@hide:file
|
|
unexpected_diagnostic proc(state @mut State) diagnostics.DiagnosticId ! (mem.AllocError | diagnostics.CapacityError) {
|
|
token :: current(state)
|
|
if (token.kind == .invalid and token.data.diagnostic != diagnostics.no_diagnostic) {
|
|
return token.data.diagnostic
|
|
}
|
|
return try diagnostics.add(state.diagnostics, .unexpected_token, token.span)
|
|
}
|
|
|
|
@hide:file
|
|
skip_newlines proc(state @mut State) void {
|
|
while next_is(state, .newline) state.next_token += 1
|
|
}
|
|
|
|
@hide:file
|
|
current proc(state @State) lexer.Token {
|
|
if (state.next_token >= state.tokens.len) return state.tokens[state.tokens.len - 1]
|
|
return state.tokens[state.next_token]
|
|
}
|
|
|
|
@hide:file
|
|
next_is proc(state @State, kind lexer.TokenKind) bool {
|
|
return current(state).kind == kind
|
|
}
|
|
|
|
@hide:file
|
|
next_n_is proc(state @State, count usize, kind lexer.TokenKind) bool {
|
|
index :: state.next_token + count
|
|
return index < state.tokens.len and state.tokens[index].kind == kind
|
|
}
|
|
|
|
@hide:file
|
|
consume proc(state @mut State, kind lexer.TokenKind) lexer.Token ! SyntaxError {
|
|
token :: current(state)
|
|
if (token.kind != kind) return .unexpected_token
|
|
state.next_token += 1
|
|
return token
|
|
}
|
|
|
|
@hide:file
|
|
consume_either proc(state @mut State, kinds []lexer.TokenKind) lexer.Token ! SyntaxError {
|
|
token :: current(state)
|
|
for (kinds) |kind| if token.kind == kind {
|
|
state.next_token += 1
|
|
return token
|
|
}
|
|
return .unexpected_token
|
|
}
|
|
|
|
@hide:file
|
|
prefix_binding_power proc(kind lexer.TokenKind) ?BindingPowerScalar {
|
|
return match kind {
|
|
.minus, .bang, .ampersand: BindingPowerScalar(50)
|
|
else: null
|
|
}
|
|
}
|
|
|
|
@hide:file
|
|
infix_binding_power proc(kind lexer.TokenKind) ?BindingPower {
|
|
return match kind {
|
|
.or: BindingPower{ left = BindingPowerScalar(2), right = BindingPowerScalar(3) }
|
|
.and: BindingPower{ left = BindingPowerScalar(4), right = BindingPowerScalar(5) }
|
|
.equal_equal, .not_equal: BindingPower{ left = BindingPowerScalar(10), right = BindingPowerScalar(11) }
|
|
.less, .less_equal, .greater, .greater_equal: BindingPower{ left = BindingPowerScalar(20), right = BindingPowerScalar(21) }
|
|
.plus, .minus: BindingPower{ left = BindingPowerScalar(30), right = BindingPowerScalar(31) }
|
|
.star, .slash: BindingPower{ left = BindingPowerScalar(40), right = BindingPowerScalar(41) }
|
|
else: null
|
|
}
|
|
}
|
|
|
|
@hide:file
|
|
binary_operator proc(kind lexer.TokenKind) ast.BinaryOperator {
|
|
return match kind {
|
|
.and: .logical_and
|
|
.or: .logical_or
|
|
.plus: .add
|
|
.minus: .subtract
|
|
.star: .multiply
|
|
.slash: .divide
|
|
.equal_equal: .equal
|
|
.not_equal: .not_equal
|
|
.less: .less
|
|
.less_equal: .less_equal
|
|
.greater: .greater
|
|
.greater_equal: .greater_equal
|
|
else: unreachable
|
|
}
|
|
}
|