Appearance
| 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 |