Skip to content
← Back

src/syntax/parsers/variables/destructure_left.ghul

1
namespace Syntax.Parsers.Variables is
2
use Source
3
use Logging
4
5
class DESTRUCTURING_VARIABLE_LEFT(
6
identifier_parser: Parser[Trees.Identifiers.Identifier],
7
qualified_identifier_parser: Parser[Trees.Identifiers.Identifier],
8
type_parser: Parser[Trees.TypeExpressions.TypeExpression]
9
): Base[Trees.Variables.DESTRUCTURING_VARIABLE_LEFT] is
10
super()
11
12
// True when the current token starts a literal leaf — an
13
// integer / float / string / char literal, a boolean, or
14
// `null`. A `Color.RED`-shaped enum-member leaf is handled
15
// separately: it begins with an IDENTIFIER and is detected by
16
// the qualified-identifier parser returning a QUALIFIED rather
17
// than a bare Identifier.
18
_is_literal_leaf_start(token: Lexical.TOKEN) -> bool =>
19
token == Lexical.TOKEN.INT_LITERAL \/
20
token == Lexical.TOKEN.FLOAT_LITERAL \/
21
token == Lexical.TOKEN.STRING_LITERAL \/
22
token == Lexical.TOKEN.CHAR_LITERAL \/
23
token == Lexical.TOKEN.TRUE \/
24
token == Lexical.TOKEN.FALSE \/
25
token == Lexical.TOKEN.NULL
26
27
// True when the current token is the `~` match marker. `~` is
28
// scanned as an OPERATOR like any other run of operator
29
// characters, so the spelling has to be checked rather than the
30
// token kind.
31
_is_match_marker(context: CONTEXT) -> bool =>
32
context.current_token == Lexical.TOKEN.OPERATOR /\
33
context.current.value_string =~ "~"
34
35
_parse_literal_expression(context: CONTEXT) -> Trees.Expressions.Expression? is
36
let location = context.location
37
let value_string = context.current.value_string
38
39
case context.current_token
40
when Lexical.TOKEN.INT_LITERAL then
41
context.next_token()
42
return Trees.Expressions.Literals.INTEGER(location, value_string)
43
when Lexical.TOKEN.FLOAT_LITERAL then
44
context.next_token()
45
return Trees.Expressions.Literals.FLOAT(location, value_string)
46
when Lexical.TOKEN.STRING_LITERAL then
47
context.next_token()
48
return Trees.Expressions.Literals.STRING(location, value_string)
49
when Lexical.TOKEN.CHAR_LITERAL then
50
context.next_token()
51
return Trees.Expressions.Literals.CHARACTER(location, value_string)
52
when Lexical.TOKEN.TRUE, Lexical.TOKEN.FALSE then
53
context.next_token()
54
return Trees.Expressions.Literals.BOOLEAN(location, value_string)
55
when Lexical.TOKEN.NULL then
56
context.next_token()
57
return Trees.Expressions.NULL(location)
58
else
59
return null
60
esac
61
si
62
63
parse(context: CONTEXT) -> Trees.Variables.DESTRUCTURING_VARIABLE_LEFT? is
64
let start = context.location
65
let end mut = context.location
66
let elements = Collections.LIST[Trees.Variables.VariableLeft]()
67
68
let should_poison mut = false
69
70
// Tracks whether this group is named (`local = field, …`)
71
// or positional. Set by the first element that carries
72
// an `= field` annotation (or doesn't). Subsequent elements
73
// must match; mixing is rejected with a diagnostic.
74
let group_is_named mut = false
75
let group_named_decided mut = false
76
77
if !context.next_token(Lexical.TOKEN.PAREN_OPEN) then
78
return null
79
fi
80
81
end = context.location
82
83
do
84
let element: Trees.Variables.VariableLeft mut
85
86
// A `~` prefix marks the leaf as a value to match rather
87
// than a name to bind. It is what makes a bare name
88
// matchable at all, and is accepted (as a no-op) on the
89
// leaf forms that already match, so every matching leaf
90
// can be written the same way.
91
let marker_location = context.location
92
let is_marked mut = _is_match_marker(context)
93
94
if is_marked then
95
context.next_token()
96
97
// Outside a refutable pattern a leaf can only bind,
98
// so there is nothing for a marker to mean. Report
99
// it and carry on as the binding it would have
100
// been, to keep one mistake to one diagnostic.
101
if !context.in_refutable_pattern then
102
context.error(
103
marker_location,
104
"a match marker is only allowed in a refutable pattern (if let, while let or case-when arm)"
105
)
106
107
is_marked = false
108
should_poison = true
109
fi
110
fi
111
112
if context.current_token == Lexical.TOKEN.PAREN_OPEN then
113
if is_marked then
114
context.error(
115
marker_location::context.location,
116
"a match marker cannot be applied to a destructure group"
117
)
118
119
should_poison = true
120
fi
121
122
// we've reached the start of a nested list of destructure elements
123
element = parse(context)!
124
elif _is_literal_leaf_start(context.current_token) then
125
// Literal-leaf: the destructure element is a runtime
126
// value-equality test against the parsed expression,
127
// not a binding. Allowed only in refutable contexts
128
// (`if let` / `case`-when patterns); a plain `let`
129
// with literal leaves is rejected at compile time.
130
let leaf_loc = marker_location::context.location
131
let expr = _parse_literal_expression(context)
132
133
if !expr? then
134
should_poison = true
135
end = context.location
136
break
137
fi
138
139
element = Trees.Variables.LITERAL_VARIABLE_LEFT(leaf_loc, expr, is_marked)
140
else
141
let identifier = qualified_identifier_parser.parse(context)
142
143
if !identifier? then
144
// identifier parser bailed (e.g. the current token is
145
// a reserved word or some other non-identifier we ran
146
// into during error recovery). Without this guard
147
// we'd deref identifier.location below and NRE.
148
should_poison = true
149
end = context.location
150
break
151
fi
152
153
if is_marked \/ isa Trees.Identifiers.QUALIFIED(identifier) then
154
// A marked name, or a `Color.RED`-shaped
155
// qualified one, is a value to match: carry the
156
// name as an expression the leaf tests against.
157
let expr = Trees.Expressions.IDENTIFIER(identifier.location, identifier)
158
element = Trees.Variables.LITERAL_VARIABLE_LEFT(marker_location::identifier.location, expr, is_marked)
159
else
160
element = Trees.Variables.SIMPLE_VARIABLE_LEFT(identifier.location, identifier)
161
fi
162
fi
163
164
// Per-element type ascription: `(c: Cat, d: Dog)` or
165
// `((x, y): Point, c: Color)`. In an ordinary `let`
166
// this is a static type assertion on the bound slot;
167
// in an `if let` arm the compile pass promotes it to
168
// a runtime narrowing test.
169
if context.current_token == Lexical.TOKEN.COLON then
170
context.next_token()
171
let element_type = type_parser.parse(context)
172
173
if element_type? then
174
element.set_type_expression(element_type)
175
fi
176
fi
177
178
// By-name annotation: `local = field` binds `local`
179
// (or, in a refutable context, value-matches the
180
// literal) from `source.field`. All elements of the
181
// same `(...)` group must use the same form — mixed
182
// groups are a parse error. The outer `let (…) = expr`
183
// initializer's `=` lives outside this group, so the
184
// `=` we see here is unambiguously the by-name
185
// separator.
186
let element_is_named = context.current_token == Lexical.TOKEN.ASSIGN
187
188
if !group_named_decided then
189
group_is_named = element_is_named
190
group_named_decided = true
191
elif element_is_named != group_is_named then
192
context.error(
193
element.location,
194
"destructure group cannot mix local = field (by name) with bare positional elements"
195
)
196
should_poison = true
197
fi
198
199
if element_is_named then
200
context.next_token()
201
let field_name = identifier_parser.parse(context)
202
203
if field_name? then
204
element.set_source_field_name(field_name)
205
else
206
should_poison = true
207
fi
208
fi
209
210
elements.add(element)
211
212
if context.current_token == Lexical.TOKEN.PAREN_CLOSE then
213
// we've reached the end of a list of destructure elements
214
215
end = element.location
216
context.next_token()
217
218
break
219
elif context.current_token == Lexical.TOKEN.COMMA then
220
// more elements to come
221
222
context.next_token()
223
224
// Trailing comma: stop when the comma is followed
225
// immediately by the closing paren. `let (a, b,) = …`
226
// reads cleanly alongside the list and tuple forms.
227
if context.current_token == Lexical.TOKEN.PAREN_CLOSE then
228
end = context.location
229
context.next_token()
230
break
231
fi
232
elif
233
(context.current.token == Lexical.TOKEN.IDENTIFIER \/ context.current.token == Lexical.TOKEN.PAREN_OPEN) /\
234
context.location.start_line >= element.location.end_line /\
235
context.location.start_column > element.location.end_column
236
then
237
// after parsing an element, we've arrived at something that could be another
238
// element with no intervening comma, and the indentation suggests it's part of
239
// the same list: we'll assume it is, report an error, but continue parsing:
240
241
context.expect_token(Lexical.TOKEN.COMMA)
242
should_poison = true
243
else
244
// something else, so we'll assume we've reached the end of the list but the
245
// user forgot to close the parentheses:
246
247
should_poison = true
248
end = context.location
249
context.expect_token(Lexical.TOKEN.PAREN_CLOSE)
250
251
break
252
fi
253
od
254
255
let result = Trees.Variables.DESTRUCTURING_VARIABLE_LEFT(start::end, elements)
256
257
if should_poison then
258
result.poison()
259
fi
260
261
return result
262
si
263
si
264
si