Coverage for trlc/parser.py: 96%
1079 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-01-15 09:56 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-01-15 09:56 +0000
1#!/usr/bin/env python3
2#
3# TRLC - Treat Requirements Like Code
4# Copyright (C) 2022-2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
5# Copyright (C) 2025 Florian Schanda
6#
7# This file is part of the TRLC Python Reference Implementation.
8#
9# TRLC is free software: you can redistribute it and/or modify it
10# under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# TRLC is distributed in the hope that it will be useful, but WITHOUT
15# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
16# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
17# License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with TRLC. If not, see <https://www.gnu.org/licenses/>.
22import re
24from trlc.nested import Nested_Lexer
25from trlc.lexer import Token_Base, Token, Lexer_Base, TRLC_Lexer
26from trlc.errors import Message_Handler, TRLC_Error
27from trlc import ast
30class Markup_Token(Token_Base):
31 # lobster-trace: LRM.Markup_String_Format
33 KIND = {
34 "CHARACTER" : "character",
35 "REFLIST_BEGIN" : "[[",
36 "REFLIST_END" : "]]",
37 "REFLIST_COMMA" : ",",
38 "REFLIST_DOT" : ".",
39 "REFLIST_IDENTIFIER" : "identifier",
40 }
42 def __init__(self, location, kind, value):
43 super().__init__(location, kind, value)
44 assert isinstance(value, str)
47class Markup_Lexer(Nested_Lexer):
48 def __init__(self, mh, literal):
49 super().__init__(mh, literal)
51 self.in_reflist = False
53 def file_location(self):
54 return self.origin_location
56 def token(self):
57 # lobster-trace: LRM.Markup_String_Errors
59 if self.in_reflist:
60 self.skip_whitespace()
61 else:
62 self.advance()
63 if self.cc is None:
64 return None
66 start_pos = self.lexpos
67 start_line = self.line_no
68 start_col = self.col_no
70 if self.cc == "[" and self.nc == "[":
71 kind = "REFLIST_BEGIN"
72 self.advance()
73 if self.in_reflist:
74 self.mh.lex_error(self.source_location(start_line,
75 start_col,
76 start_pos,
77 start_pos + 1),
78 "cannot nest reference lists")
79 else:
80 self.in_reflist = True
82 elif self.cc == "]" and self.nc == "]":
83 kind = "REFLIST_END"
84 self.advance()
85 if self.in_reflist:
86 self.in_reflist = False
87 else:
88 self.mh.lex_error(self.source_location(start_line,
89 start_col,
90 start_pos,
91 start_pos + 1),
92 "opening [[ for this ]] found")
94 elif not self.in_reflist:
95 kind = "CHARACTER"
97 elif self.cc == ",":
98 kind = "REFLIST_COMMA"
100 elif self.cc == ".":
101 kind = "REFLIST_DOT"
103 elif self.is_alpha(self.cc): 103 ↛ 110line 103 didn't jump to line 110 because the condition on line 103 was always true
104 kind = "REFLIST_IDENTIFIER"
105 while self.nc and (self.is_alnum(self.nc) or
106 self.nc == "_"):
107 self.advance()
109 else:
110 self.mh.lex_error(self.source_location(start_line,
111 start_col,
112 start_pos,
113 start_pos),
114 "unexpected character '%s'" % self.cc)
116 loc = self.source_location(start_line,
117 start_col,
118 start_pos,
119 self.lexpos)
121 # pylint: disable=possibly-used-before-assignment
122 return Markup_Token(loc,
123 kind,
124 self.content[start_pos:self.lexpos + 1])
127class Parser_Base:
128 def __init__(self, mh, lexer, eoc_name, token_map, keywords):
129 assert isinstance(mh, Message_Handler)
130 assert isinstance(lexer, Lexer_Base)
131 assert isinstance(eoc_name, str)
132 assert isinstance(token_map, dict)
133 assert isinstance(keywords, frozenset)
134 self.mh = mh
135 self.lexer = lexer
137 self.eoc_name = eoc_name
138 self.language_tokens = token_map
139 self.language_keywords = keywords
141 self.ct = None
142 self.nt = None
143 self.advance()
145 def advance(self):
146 # lobster-trace: LRM.Comments
147 self.ct = self.nt
148 while True:
149 self.nt = self.lexer.token()
150 if self.nt is None or self.nt.kind != "COMMENT":
151 break
153 def skip_until_newline(self):
154 if self.ct is None: 154 ↛ 155line 154 didn't jump to line 155 because the condition on line 154 was never true
155 return
156 current_line = self.ct.location.line_no
157 while self.nt and self.nt.location.line_no == current_line:
158 self.advance()
160 def peek(self, kind):
161 assert kind in self.language_tokens, \
162 "%s is not a valid token" % kind
163 return self.nt is not None and self.nt.kind == kind
165 def peek_eof(self):
166 return self.nt is None
168 def peek_kw(self, value):
169 assert value in self.language_keywords, \
170 "%s is not a valid keyword" % value
171 return self.peek("KEYWORD") and self.nt.value == value
173 def match(self, kind):
174 # lobster-trace: LRM.Matching_Value_Types
176 assert kind in self.language_tokens, \
177 "%s is not a valid token" % kind
178 if self.nt is None:
179 if self.ct is None: 179 ↛ 180line 179 didn't jump to line 180 because the condition on line 179 was never true
180 self.mh.error(self.lexer.file_location(),
181 "expected %s, encountered %s instead" %
182 (self.language_tokens[kind], self.eoc_name))
183 else:
184 self.mh.error(self.ct.location,
185 "expected %s, encountered %s instead" %
186 (self.language_tokens[kind], self.eoc_name))
187 elif self.nt.kind != kind:
188 self.mh.error(self.nt.location,
189 "expected %s, encountered %s instead" %
190 (self.language_tokens[kind],
191 self.language_tokens[self.nt.kind]))
192 self.advance()
194 def match_eof(self):
195 if self.nt is not None: 195 ↛ 196line 195 didn't jump to line 196 because the condition on line 195 was never true
196 self.mh.error(self.nt.location,
197 "expected %s, encountered %s instead" %
198 (self.eoc_name,
199 self.language_tokens[self.nt.kind]))
201 def match_kw(self, value):
202 assert value in self.language_keywords, \
203 "%s is not a valid keyword" % value
204 if self.nt is None:
205 if self.ct is None: 205 ↛ 210line 205 didn't jump to line 210 because the condition on line 205 was always true
206 self.mh.error(self.lexer.file_location(),
207 "expected keyword %s, encountered %s instead" %
208 (value, self.eoc_name))
209 else:
210 self.mh.error(self.ct.location,
211 "expected keyword %s, encountered %s instead" %
212 (value, self.eoc_name))
213 elif self.nt.kind != "KEYWORD":
214 self.mh.error(self.nt.location,
215 "expected keyword %s, encountered %s instead" %
216 (value,
217 self.language_tokens[self.nt.kind]))
218 elif self.nt.value != value:
219 self.mh.error(self.nt.location,
220 "expected keyword %s,"
221 " encountered keyword %s instead" %
222 (value, self.nt.value))
223 self.advance()
226class Markup_Parser(Parser_Base):
227 def __init__(self, parent, literal):
228 assert isinstance(parent, Parser)
229 super().__init__(parent.mh, Markup_Lexer(parent.mh, literal),
230 eoc_name = "end-of-string",
231 token_map = Markup_Token.KIND,
232 keywords = frozenset())
233 self.parent = parent
234 self.references = literal.references
236 def parse_all_references(self):
237 while self.nt:
238 if self.peek("CHARACTER"):
239 self.advance()
240 else:
241 self.parse_ref_list()
242 self.match_eof()
243 return self.references
245 def parse_ref_list(self):
246 self.match("REFLIST_BEGIN")
247 self.parse_qualified_name()
248 while self.peek("REFLIST_COMMA"):
249 self.match("REFLIST_COMMA")
250 self.parse_qualified_name()
251 self.match("REFLIST_END")
253 def parse_qualified_name(self):
254 # lobster-trace: LRM.Qualified_Name
255 # lobster-trace: LRM.Valid_Qualifier
256 # lobster-trace: LRM.Valid_Name
257 # lobster-trace: LRM.Markup_String_Resolution
258 # lobster-trace: LRM.Markup_String_Types
260 self.match("REFLIST_IDENTIFIER")
261 if self.peek("REFLIST_DOT"):
262 package = self.parent.stab.lookup_direct(
263 mh = self.mh,
264 name = self.ct.value,
265 error_location = self.ct.location,
266 required_subclass = ast.Package)
267 if not self.parent.cu.is_visible(package):
268 self.mh.error(self.ct.location,
269 "package must be imported before use")
271 self.match("REFLIST_DOT")
272 self.match("REFLIST_IDENTIFIER")
273 else:
274 package = self.parent.cu.package
276 ref = ast.Record_Reference(location = self.ct.location,
277 name = self.ct.value,
278 typ = None,
279 package = package)
280 self.references.append(ref)
283class Parser(Parser_Base):
284 COMPARISON_OPERATOR = ("==", "!=", "<", "<=", ">", ">=")
285 ADDING_OPERATOR = ("+", "-")
286 MULTIPLYING_OPERATOR = ("*", "/", "%")
288 def __init__(self,
289 mh,
290 stab,
291 file_name,
292 lint_mode,
293 error_recovery,
294 primary_file=True,
295 lexer=None):
296 assert isinstance(mh, Message_Handler)
297 assert isinstance(stab, ast.Symbol_Table)
298 assert isinstance(file_name, str)
299 assert isinstance(lint_mode, bool)
300 assert isinstance(error_recovery, bool)
301 assert isinstance(primary_file, bool)
302 assert isinstance(lexer, TRLC_Lexer) or lexer is None
303 if lexer: 303 ↛ 309line 303 didn't jump to line 309 because the condition on line 303 was always true
304 super().__init__(mh, lexer,
305 eoc_name = "end-of-file",
306 token_map = Token.KIND,
307 keywords = TRLC_Lexer.KEYWORDS)
308 else:
309 super().__init__(mh, TRLC_Lexer(mh, file_name),
310 eoc_name = "end-of-file",
311 token_map = Token.KIND,
312 keywords = TRLC_Lexer.KEYWORDS)
314 self.lint_mode = lint_mode
315 self.error_recovery = error_recovery
317 self.stab = stab
318 self.cu = ast.Compilation_Unit(file_name)
320 self.primary = primary_file
321 self.secondary = False
322 # Controls if the file is actually fully parsed: primary means
323 # it was selected on the command-line and secondary means it
324 # was selected by dependency analysis.
326 self.builtin_bool = stab.lookup_assuming(self.mh, "Boolean")
327 self.builtin_int = stab.lookup_assuming(self.mh, "Integer")
328 self.builtin_decimal = stab.lookup_assuming(self.mh, "Decimal")
329 self.builtin_str = stab.lookup_assuming(self.mh, "String")
330 self.builtin_mstr = stab.lookup_assuming(self.mh, "Markup_String")
332 self.section = []
333 self.default_scope = ast.Scope()
334 self.default_scope.push(self.stab)
336 def parse_described_name(self):
337 # lobster-trace: LRM.Described_Names
338 # lobster-trace: LRM.Described_Name_Description
339 self.match("IDENTIFIER")
340 name = self.ct
342 if self.peek("STRING"):
343 self.match("STRING")
344 t_descr = self.ct
345 return name, t_descr.value, t_descr
346 else:
347 return name, None, None
349 def parse_qualified_name(self,
350 scope,
351 required_subclass=None,
352 match_ident=True):
353 # lobster-trace: LRM.Qualified_Name
354 # lobster-trace: LRM.Valid_Qualifier
355 # lobster-trace: LRM.Valid_Name
356 assert isinstance(scope, ast.Scope)
357 assert required_subclass is None or isinstance(required_subclass, type)
358 assert isinstance(match_ident, bool)
360 if match_ident:
361 self.match("IDENTIFIER")
362 sym = scope.lookup(self.mh, self.ct)
363 sym.set_ast_link(self.ct)
365 if isinstance(sym, ast.Package):
366 if not self.cu.is_visible(sym):
367 self.mh.error(self.ct.location,
368 "package must be imported before use")
369 self.match("DOT")
370 sym.set_ast_link(self.ct)
371 self.match("IDENTIFIER")
372 return sym.symbols.lookup(self.mh, self.ct, required_subclass)
373 else:
374 # Easiest way to generate the correct error message
375 return scope.lookup(self.mh, self.ct, required_subclass)
377 def parse_type_declaration(self):
378 # lobster-trace: LRM.Type_Declarations
379 if self.peek_kw("enum"):
380 n_item = self.parse_enum_declaration()
381 elif self.peek_kw("tuple"):
382 n_item = self.parse_tuple_declaration()
383 else:
384 n_item = self.parse_record_declaration()
385 assert isinstance(n_item, ast.Concrete_Type)
386 return n_item
388 def parse_enum_declaration(self):
389 # lobster-trace: LRM.Enumeration_Declaration
390 self.match_kw("enum")
391 t_enum = self.ct
392 name, description, t_description = self.parse_described_name()
394 enum = ast.Enumeration_Type(name = name.value,
395 description = description,
396 location = name.location,
397 package = self.cu.package)
398 self.cu.package.symbols.register(self.mh, enum)
399 enum.set_ast_link(t_enum)
400 enum.set_ast_link(name)
401 if t_description:
402 enum.set_ast_link(t_description)
404 self.match("C_BRA")
405 enum.set_ast_link(self.ct)
406 empty = True
407 while not self.peek("C_KET"):
408 name, description, t_description = self.parse_described_name()
409 lit = ast.Enumeration_Literal_Spec(name = name.value,
410 description = description,
411 location = name.location,
412 enum = enum)
413 lit.set_ast_link(name)
414 if t_description:
415 lit.set_ast_link(self.ct)
416 empty = False
417 enum.literals.register(self.mh, lit)
418 self.match("C_KET")
419 enum.set_ast_link(self.ct)
421 if empty:
422 # lobster-trace: LRM.No_Empty_Enumerations
423 self.mh.error(enum.location,
424 "empty enumerations are not permitted")
426 return enum
428 def parse_tuple_field(self,
429 n_tuple,
430 optional_allowed,
431 optional_reason,
432 optional_required):
433 assert isinstance(n_tuple, ast.Tuple_Type)
434 assert isinstance(optional_allowed, bool)
435 assert isinstance(optional_reason, str)
436 assert isinstance(optional_required, bool)
437 assert optional_allowed or not optional_required
439 field_name, field_description, t_descr = self.parse_described_name()
441 if optional_required or self.peek_kw("optional"):
442 self.match_kw("optional")
443 t_optional = self.ct
444 field_is_optional = True
445 if not optional_allowed:
446 self.mh.error(self.ct.location, optional_reason)
447 else:
448 field_is_optional = False
449 t_optional = None
451 # lobster-trace: LRM.Tuple_Field_Types
452 field_type = self.parse_qualified_name(self.default_scope,
453 ast.Type)
454 comp = ast.Composite_Component(name = field_name.value,
455 description = field_description,
456 location = field_name.location,
457 member_of = n_tuple,
458 n_typ = field_type,
459 optional = field_is_optional)
460 comp.set_ast_link(field_name)
461 if t_descr:
462 comp.set_ast_link(t_descr)
463 if field_is_optional:
464 comp.set_ast_link(t_optional)
466 return comp
468 def parse_tuple_declaration(self):
469 # lobster-trace: LRM.Tuple_Declaration
470 self.match_kw("tuple")
471 t_tuple = self.ct
472 name, description, t_descr = self.parse_described_name()
474 n_tuple = ast.Tuple_Type(name = name.value,
475 description = description,
476 location = name.location,
477 package = self.cu.package)
479 n_tuple.set_ast_link(t_tuple)
480 n_tuple.set_ast_link(name)
481 if t_descr:
482 n_tuple.set_ast_link(t_descr)
483 self.match("C_BRA")
484 n_tuple.set_ast_link(self.ct)
486 n_field = self.parse_tuple_field(
487 n_tuple,
488 optional_allowed = False,
489 optional_reason = "first field may not be optional",
490 optional_required = False)
491 n_tuple.components.register(self.mh, n_field)
493 has_separators = False
494 optional_required = False
495 separator_allowed = True
497 while self.peek_kw("separator") or self.peek("IDENTIFIER"):
498 if has_separators or self.peek_kw("separator"):
499 has_separators = True
500 self.match_kw("separator")
501 t_sep = self.ct
502 if not separator_allowed:
503 # lobster-trace: LRM.Tuple_Separators_All_Or_None
504 self.mh.error(self.ct.location,
505 "either all fields must be separated,"
506 " or none")
507 if self.peek("IDENTIFIER") or \ 507 ↛ 519line 507 didn't jump to line 519 because the condition on line 507 was always true
508 self.peek("AT") or \
509 self.peek("COLON") or \
510 self.peek("SEMICOLON"):
511 self.advance()
512 sep = ast.Separator(self.ct)
513 sep.set_ast_link(t_sep)
514 sep.set_ast_link(self.ct)
515 n_tuple.add_separator(sep)
516 else:
517 separator_allowed = False
518 # lobster-trace: LRM.Tuple_Optional_Requires_Separators
519 n_field = self.parse_tuple_field(
520 n_tuple,
521 optional_allowed = has_separators,
522 optional_reason = ("optional only permitted in tuples"
523 " with separators"),
524 optional_required = optional_required)
525 n_tuple.components.register(self.mh, n_field)
526 # lobster-trace: LRM.Tuple_Optional_Fields
527 optional_required |= n_field.optional
529 self.match("C_KET")
530 n_tuple.set_ast_link(self.ct)
532 # Final check to ban tuples with separators containing other
533 # tuples.
534 if has_separators:
535 # lobster-trace: LRM.Restricted_Tuple_Nesting
536 for n_field in n_tuple.components.values():
537 if isinstance(n_field.n_typ, ast.Tuple_Type) and \
538 n_field.n_typ.has_separators():
539 self.mh.error(
540 n_field.location,
541 "tuple type %s, which contains separators,"
542 " may not contain another tuple with separators"
543 % n_tuple.name)
545 # Late registration to avoid recursion in tuples
546 # lobster-trace: LRM.Tuple_Field_Types
547 self.cu.package.symbols.register(self.mh, n_tuple)
549 return n_tuple
551 def parse_record_component(self, n_record):
552 assert isinstance(n_record, ast.Record_Type)
554 c_name, c_descr, t_descr = self.parse_described_name()
555 t_optional = None
556 c_optional = False
557 if self.peek_kw("optional"):
558 self.match_kw("optional")
559 t_optional = self.ct
560 c_optional = True
561 c_typ = self.parse_qualified_name(self.default_scope,
562 ast.Type)
563 c_typ.set_ast_link(self.ct)
565 if self.peek("S_BRA"):
566 self.match("S_BRA")
567 t_s_bra = self.ct
568 self.match("INTEGER")
569 t_lo = self.ct
570 a_lo = self.ct.value
571 loc_lo = self.ct.location
572 self.match("RANGE")
573 t_range = self.ct
574 a_loc = self.ct.location
575 a_hi = None
576 if self.peek("INTEGER"):
577 self.match("INTEGER")
578 a_hi = self.ct.value
579 elif self.peek("OPERATOR") and self.nt.value == "*": 579 ↛ 582line 579 didn't jump to line 582 because the condition on line 579 was always true
580 self.match("OPERATOR")
581 else:
582 self.mh.error(self.nt.location,
583 "expected INTEGER or * for upper bound")
584 t_hi = self.ct
585 loc_hi = self.ct.location
586 self.match("S_KET")
587 t_s_ket = self.ct
588 c_typ = ast.Array_Type(location = a_loc,
589 element_type = c_typ,
590 lower_bound = a_lo,
591 upper_bound = a_hi,
592 loc_lower = loc_lo,
593 loc_upper = loc_hi)
594 c_typ.set_ast_link(t_s_bra)
595 c_typ.set_ast_link(t_lo)
596 c_typ.set_ast_link(t_range)
597 c_typ.set_ast_link(t_hi)
598 c_typ.set_ast_link(t_s_ket)
600 c_comp = ast.Composite_Component(name = c_name.value,
601 description = c_descr,
602 location = c_name.location,
603 member_of = n_record,
604 n_typ = c_typ,
605 optional = c_optional)
606 c_comp.set_ast_link(c_name)
607 if t_descr:
608 c_comp.set_ast_link(t_descr)
609 if c_optional:
610 c_comp.set_ast_link(t_optional)
612 return c_comp
614 def parse_record_declaration(self):
615 t_abstract = None
616 t_final = None
617 is_abstract = False
618 is_final = False
619 if self.peek_kw("abstract"):
620 self.match_kw("abstract")
621 t_abstract = self.ct
622 is_abstract = True
623 elif self.peek_kw("final"):
624 self.match_kw("final")
625 t_final = self.ct
626 is_final = True
628 self.match_kw("type")
629 t_type = self.ct
630 name, description, t_description = self.parse_described_name()
632 if self.peek_kw("extends"):
633 self.match_kw("extends")
634 t_extends = self.ct
635 root_record = self.parse_qualified_name(self.default_scope,
636 ast.Record_Type)
637 root_record.set_ast_link(t_extends)
638 root_record.set_ast_link(self.ct)
639 else:
640 root_record = None
642 if self.lint_mode and \
643 root_record and root_record.is_final and \
644 not is_final:
645 self.mh.check(name.location,
646 "consider clarifying that this record is final",
647 "clarify_final",
648 ("Parent record %s is final, making this record\n"
649 "also final. Marking it explicitly as final\n"
650 "clarifies this to casual readers." %
651 root_record.fully_qualified_name()))
653 record = ast.Record_Type(name = name.value,
654 description = description,
655 location = name.location,
656 package = self.cu.package,
657 n_parent = root_record,
658 is_abstract = is_abstract)
659 self.cu.package.symbols.register(self.mh, record)
660 if is_abstract:
661 record.set_ast_link(t_abstract)
662 if is_final:
663 record.set_ast_link(t_final)
664 record.set_ast_link(t_type)
665 record.set_ast_link(name)
666 if t_description:
667 record.set_ast_link(t_description)
669 self.match("C_BRA")
670 record.set_ast_link(self.ct)
671 while not self.peek("C_KET"):
672 if self.peek_kw("freeze"):
673 self.match_kw("freeze")
674 t_freeze = self.ct
675 self.match("IDENTIFIER")
676 n_comp = record.components.lookup(self.mh,
677 self.ct,
678 ast.Composite_Component)
679 if record.is_frozen(n_comp):
680 n_value = record.get_freezing_expression(n_comp)
681 self.mh.error(
682 self.ct.location,
683 "duplicate freezing of %s, previously frozen at %s" %
684 (n_comp.name,
685 self.mh.cross_file_reference(n_value.location)))
686 n_comp.set_ast_link(t_freeze)
687 n_comp.set_ast_link(self.ct)
688 self.match("ASSIGN")
689 n_comp.set_ast_link(self.ct)
690 n_value = self.parse_value(n_comp.n_typ)
691 n_value.set_ast_link(self.ct)
693 record.frozen[n_comp.name] = n_value
695 else:
696 n_comp = self.parse_record_component(record)
697 if record.is_final:
698 self.mh.error(n_comp.location,
699 "cannot declare new components in"
700 " final record type")
701 else:
702 record.components.register(self.mh, n_comp)
704 self.match("C_KET")
705 record.set_ast_link(self.ct)
707 # Finally mark record final if applicable
708 if is_final:
709 record.is_final = True
711 return record
713 def parse_expression(self, scope):
714 # lobster-trace: LRM.Expression
715 assert isinstance(scope, ast.Scope)
717 n_lhs = self.parse_relation(scope)
719 if self.peek_kw("and"):
720 while self.peek_kw("and"):
721 self.match_kw("and")
722 t_op = self.ct
723 a_op = ast.Binary_Operator.LOGICAL_AND
724 t_op.ast_link = a_op
725 n_rhs = self.parse_relation(scope)
726 n_lhs = ast.Binary_Expression(
727 mh = self.mh,
728 location = t_op.location,
729 typ = self.builtin_bool,
730 operator = a_op,
731 n_lhs = n_lhs,
732 n_rhs = n_rhs)
734 elif self.peek_kw("or"):
735 while self.peek_kw("or"):
736 self.match_kw("or")
737 t_op = self.ct
738 a_op = ast.Binary_Operator.LOGICAL_OR
739 t_op.ast_link = a_op
740 n_rhs = self.parse_relation(scope)
741 n_lhs = ast.Binary_Expression(
742 mh = self.mh,
743 location = t_op.location,
744 typ = self.builtin_bool,
745 operator = a_op,
746 n_lhs = n_lhs,
747 n_rhs = n_rhs)
749 elif self.peek_kw("xor"):
750 self.match_kw("xor")
751 t_op = self.ct
752 a_op = ast.Binary_Operator.LOGICAL_XOR
753 t_op.ast_link = a_op
754 n_rhs = self.parse_relation(scope)
755 n_lhs = ast.Binary_Expression(
756 mh = self.mh,
757 location = t_op.location,
758 typ = self.builtin_bool,
759 operator = a_op,
760 n_lhs = n_lhs,
761 n_rhs = n_rhs)
763 elif self.peek_kw("implies"):
764 self.match_kw("implies")
765 t_op = self.ct
766 a_op = ast.Binary_Operator.LOGICAL_IMPLIES
767 t_op.ast_link = a_op
768 n_rhs = self.parse_relation(scope)
769 n_lhs = ast.Binary_Expression(
770 mh = self.mh,
771 location = t_op.location,
772 typ = self.builtin_bool,
773 operator = a_op,
774 n_lhs = n_lhs,
775 n_rhs = n_rhs)
777 return n_lhs
779 def parse_relation(self, scope):
780 # lobster-trace: LRM.Relation
781 # lobster-trace: LRM.Operators
782 assert isinstance(scope, ast.Scope)
783 relop_mapping = {"==" : ast.Binary_Operator.COMP_EQ,
784 "!=" : ast.Binary_Operator.COMP_NEQ,
785 "<" : ast.Binary_Operator.COMP_LT,
786 "<=" : ast.Binary_Operator.COMP_LEQ,
787 ">" : ast.Binary_Operator.COMP_GT,
788 ">=" : ast.Binary_Operator.COMP_GEQ}
789 assert set(relop_mapping) == set(Parser.COMPARISON_OPERATOR)
791 n_lhs = self.parse_simple_expression(scope)
793 if self.peek("OPERATOR") and \
794 self.nt.value in Parser.COMPARISON_OPERATOR:
795 self.match("OPERATOR")
796 t_op = self.ct
797 a_op = relop_mapping[t_op.value]
798 t_op.ast_link = a_op
799 n_rhs = self.parse_simple_expression(scope)
800 return ast.Binary_Expression(
801 mh = self.mh,
802 location = t_op.location,
803 typ = self.builtin_bool,
804 operator = a_op,
805 n_lhs = n_lhs,
806 n_rhs = n_rhs)
808 elif self.peek_kw("not") or self.peek_kw("in"):
809 if self.peek_kw("not"):
810 self.match_kw("not")
811 t_not = self.ct
812 else:
813 t_not = None
815 self.match_kw("in")
816 t_in = self.ct
818 n_a = self.parse_simple_expression(scope)
819 t_n_a = self.ct
820 if self.peek("RANGE"):
821 self.match("RANGE")
822 t_range = self.ct
823 n_b = self.parse_simple_expression(scope)
824 n_b.set_ast_link(self.ct)
825 n_a.set_ast_link(t_n_a)
826 rv = ast.Range_Test(
827 mh = self.mh,
828 location = t_in.location,
829 typ = self.builtin_bool,
830 n_lhs = n_lhs,
831 n_lower = n_a,
832 n_upper = n_b)
833 rv.set_ast_link(t_range)
834 rv.set_ast_link(t_in)
836 elif isinstance(n_a.typ, ast.Builtin_String):
837 rv = ast.Binary_Expression(
838 mh = self.mh,
839 location = t_in.location,
840 typ = self.builtin_bool,
841 operator = ast.Binary_Operator.STRING_CONTAINS,
842 n_lhs = n_lhs,
843 n_rhs = n_a)
844 rv.set_ast_link(t_in)
846 elif isinstance(n_a.typ, ast.Array_Type): 846 ↛ 858line 846 didn't jump to line 858 because the condition on line 846 was always true
847 a_op = ast.Binary_Operator.ARRAY_CONTAINS
848 t_in.ast_link = a_op
849 rv = ast.Binary_Expression(
850 mh = self.mh,
851 location = t_in.location,
852 typ = self.builtin_bool,
853 operator = a_op,
854 n_lhs = n_lhs,
855 n_rhs = n_a)
857 else:
858 self.mh.error(
859 n_a.location,
860 "membership test only defined for Strings and Arrays,"
861 " not for %s" % n_a.typ.name)
863 if t_not is not None:
864 a_unary_op = ast.Unary_Operator.LOGICAL_NOT
865 t_not.ast_link = a_unary_op
866 rv = ast.Unary_Expression(
867 mh = self.mh,
868 location = t_not.location,
869 typ = self.builtin_bool,
870 operator = a_unary_op,
871 n_operand = rv)
873 return rv
875 else:
876 return n_lhs
878 def parse_simple_expression(self, scope):
879 # lobster-trace: LRM.Simple_Expression
880 # lobster-trace: LRM.Operators
881 # lobster-trace: LRM.Unary_Minus_Parsing
882 assert isinstance(scope, ast.Scope)
883 un_add_map = {"+" : ast.Unary_Operator.PLUS,
884 "-" : ast.Unary_Operator.MINUS}
885 bin_add_map = {"+" : ast.Binary_Operator.PLUS,
886 "-" : ast.Binary_Operator.MINUS}
887 assert set(un_add_map) == set(Parser.ADDING_OPERATOR)
888 assert set(bin_add_map) == set(Parser.ADDING_OPERATOR)
890 if self.peek("OPERATOR") and \
891 self.nt.value in Parser.ADDING_OPERATOR:
892 self.match("OPERATOR")
893 t_unary = self.ct
894 a_unary = un_add_map[t_unary.value]
895 t_unary.ast_link = a_unary
896 has_explicit_brackets = self.peek("BRA")
897 else:
898 t_unary = None
900 n_lhs = self.parse_term(scope)
901 if t_unary:
902 # pylint: disable=possibly-used-before-assignment
903 if self.lint_mode and \
904 isinstance(n_lhs, ast.Binary_Expression) and \
905 not has_explicit_brackets:
906 self.mh.check(t_unary.location,
907 "expression means -(%s), place explicit "
908 "brackets to clarify intent" %
909 n_lhs.to_string(),
910 "unary_minus_precedence")
912 n_lhs = ast.Unary_Expression(
913 mh = self.mh,
914 location = t_unary.location,
915 typ = n_lhs.typ,
916 operator = a_unary,
917 n_operand = n_lhs)
919 if isinstance(n_lhs.typ, ast.Builtin_String):
920 rtyp = self.builtin_str
921 else:
922 rtyp = n_lhs.typ
924 while self.peek("OPERATOR") and \
925 self.nt.value in Parser.ADDING_OPERATOR:
926 self.match("OPERATOR")
927 t_op = self.ct
928 a_op = bin_add_map[t_op.value]
929 t_op.ast_link = a_op
930 n_rhs = self.parse_term(scope)
931 n_lhs = ast.Binary_Expression(
932 mh = self.mh,
933 location = t_op.location,
934 typ = rtyp,
935 operator = a_op,
936 n_lhs = n_lhs,
937 n_rhs = n_rhs)
939 return n_lhs
941 def parse_term(self, scope):
942 # lobster-trace: LRM.Term
943 # lobster-trace: LRM.Operators
944 assert isinstance(scope, ast.Scope)
945 mul_map = {"*" : ast.Binary_Operator.TIMES,
946 "/" : ast.Binary_Operator.DIVIDE,
947 "%" : ast.Binary_Operator.REMAINDER}
948 assert set(mul_map) == set(Parser.MULTIPLYING_OPERATOR)
950 n_lhs = self.parse_factor(scope)
951 while self.peek("OPERATOR") and \
952 self.nt.value in Parser.MULTIPLYING_OPERATOR:
953 self.match("OPERATOR")
954 t_op = self.ct
955 a_op = mul_map[t_op.value]
956 t_op.ast_link = a_op
957 n_rhs = self.parse_factor(scope)
958 n_lhs = ast.Binary_Expression(
959 mh = self.mh,
960 location = t_op.location,
961 typ = n_lhs.typ,
962 operator = a_op,
963 n_lhs = n_lhs,
964 n_rhs = n_rhs)
966 return n_lhs
968 def parse_factor(self, scope):
969 # lobster-trace: LRM.Factor
970 assert isinstance(scope, ast.Scope)
972 if self.peek_kw("not"):
973 self.match_kw("not")
974 t_op = self.ct
975 n_operand = self.parse_primary(scope)
976 a_not = ast.Unary_Operator.LOGICAL_NOT
977 t_op.ast_link = a_not
978 return ast.Unary_Expression(
979 mh = self.mh,
980 location = t_op.location,
981 typ = self.builtin_bool,
982 operator = a_not,
983 n_operand = n_operand)
985 elif self.peek_kw("abs"):
986 self.match_kw("abs")
987 t_op = self.ct
988 n_operand = self.parse_primary(scope)
989 a_abs = ast.Unary_Operator.ABSOLUTE_VALUE
990 t_op.ast_link = a_abs
991 return ast.Unary_Expression(
992 mh = self.mh,
993 location = t_op.location,
994 typ = n_operand.typ,
995 operator = a_abs,
996 n_operand = n_operand)
998 else:
999 n_lhs = self.parse_primary(scope)
1000 if self.peek("OPERATOR") and self.nt.value == "**":
1001 self.match("OPERATOR")
1002 t_op = self.ct
1003 n_rhs = self.parse_primary(scope)
1004 rhs_value = n_rhs.evaluate(self.mh, None, None)
1005 a_binary = ast.Binary_Operator.POWER
1006 t_op.ast_link = a_binary
1007 n_lhs = ast.Binary_Expression(
1008 mh = self.mh,
1009 location = t_op.location,
1010 typ = n_lhs.typ,
1011 operator = a_binary,
1012 n_lhs = n_lhs,
1013 n_rhs = n_rhs)
1014 if rhs_value.value < 0: 1014 ↛ 1015line 1014 didn't jump to line 1015 because the condition on line 1014 was never true
1015 self.mh.error(n_rhs.location,
1016 "exponent must not be negative")
1017 return n_lhs
1019 def parse_primary(self, scope):
1020 # lobster-trace: LRM.Primary
1021 assert isinstance(scope, ast.Scope)
1023 if self.peek("INTEGER"):
1024 # lobster-trace: LRM.Integer_Values
1025 self.match("INTEGER")
1026 int_lit = ast.Integer_Literal(self.ct, self.builtin_int)
1027 int_lit.set_ast_link(self.ct)
1028 return int_lit
1030 elif self.peek("DECIMAL"):
1031 # lobster-trace: LRM.Decimal_Values
1032 self.match("DECIMAL")
1033 dec_lit = ast.Decimal_Literal(self.ct, self.builtin_decimal)
1034 dec_lit.set_ast_link(self.ct)
1035 return dec_lit
1037 elif self.peek("STRING"):
1038 # lobster-trace: LRM.String_Values
1039 self.match("STRING")
1040 string_lit = ast.String_Literal(self.ct, self.builtin_str)
1041 string_lit.set_ast_link(self.ct)
1042 return string_lit
1044 elif self.peek_kw("true") or self.peek_kw("false"):
1045 # lobster-trace: LRM.Boolean_Values
1046 self.match("KEYWORD")
1047 bool_lit = ast.Boolean_Literal(self.ct, self.builtin_bool)
1048 bool_lit.set_ast_link(self.ct)
1049 return bool_lit
1051 elif self.peek_kw("null"):
1052 self.match_kw("null")
1053 null_lit = ast.Null_Literal(self.ct)
1054 null_lit.set_ast_link(self.ct)
1055 return null_lit
1057 elif self.peek("BRA"):
1058 self.match("BRA")
1059 t_bra = self.ct
1060 if self.peek_kw("forall") or self.peek_kw("exists"):
1061 rv = self.parse_quantified_expression(scope)
1062 elif self.peek_kw("if"):
1063 rv = self.parse_conditional_expression(scope)
1064 else:
1065 rv = self.parse_expression(scope)
1066 rv.set_ast_link(t_bra)
1067 self.match("KET")
1068 rv.set_ast_link(self.ct)
1069 return rv
1071 else:
1072 return self.parse_name(scope)
1074 def parse_quantified_expression(self, scope):
1075 # lobster-trace: LRM.Quantified_Expression
1076 assert isinstance(scope, ast.Scope)
1078 if self.peek_kw("forall"):
1079 self.match_kw("forall")
1080 t_quantified = self.ct
1081 universal = True
1082 else:
1083 self.match_kw("exists")
1084 t_quantified = self.ct
1085 universal = False
1086 loc = self.ct.location
1087 self.match("IDENTIFIER")
1088 t_qv = self.ct
1089 if scope.contains(t_qv.value):
1090 # lobster-trace: LRM.Quantification_Naming_Scope
1091 pdef = scope.lookup(self.mh, t_qv)
1092 self.mh.error(t_qv.location,
1093 "shadows %s %s from %s" %
1094 (pdef.__class__.__name__,
1095 pdef.name,
1096 self.mh.cross_file_reference(pdef.location)))
1097 self.match_kw("in")
1098 t_in = self.ct
1099 self.match("IDENTIFIER")
1100 field = scope.lookup(self.mh, self.ct, ast.Composite_Component)
1101 n_source = ast.Name_Reference(self.ct.location,
1102 field)
1103 n_source.set_ast_link(self.ct)
1104 if not isinstance(field.n_typ, ast.Array_Type):
1105 # lobster-trace: LRM.Quantification_Object
1106 self.mh.error(self.ct.location,
1107 "you can only quantify over arrays")
1108 n_var = ast.Quantified_Variable(t_qv.value,
1109 t_qv.location,
1110 field.n_typ.element_type)
1111 n_var.set_ast_link(t_qv)
1112 self.match("ARROW")
1113 t_arrow = self.ct
1115 new_table = ast.Symbol_Table()
1116 new_table.register(self.mh, n_var)
1117 scope.push(new_table)
1118 n_expr = self.parse_expression(scope)
1119 scope.pop()
1121 quantified_expression = ast.Quantified_Expression(
1122 mh = self.mh,
1123 location = loc,
1124 typ = self.builtin_bool,
1125 universal = universal,
1126 n_variable = n_var,
1127 n_source = n_source,
1128 n_expr = n_expr)
1130 quantified_expression.set_ast_link(t_quantified)
1131 quantified_expression.set_ast_link(t_in)
1132 quantified_expression.set_ast_link(t_arrow)
1134 return quantified_expression
1136 def parse_conditional_expression(self, scope):
1137 # lobster-trace: LRM.Conditional_Expression
1138 # lobster-trace: LRM.Restricted_Null
1139 assert isinstance(scope, ast.Scope)
1141 self.match_kw("if")
1142 t_if = self.ct
1143 if_cond = self.parse_expression(scope)
1144 self.match_kw("then")
1145 t_then = self.ct
1146 if_expr = self.parse_expression(scope)
1147 if if_expr.typ is None:
1148 self.mh.error(if_expr.location,
1149 "null is not permitted here")
1150 if_action = ast.Action(self.mh, t_if, if_cond, if_expr)
1152 rv = ast.Conditional_Expression(location = t_if.location,
1153 if_action = if_action)
1154 if_action.set_ast_link(t_if)
1155 if_action.set_ast_link(t_then)
1157 while self.peek_kw("elsif"):
1158 self.match_kw("elsif")
1159 t_elsif = self.ct
1160 elsif_cond = self.parse_expression(scope)
1161 self.match_kw("then")
1162 t_then = self.ct
1163 elsif_expr = self.parse_expression(scope)
1164 elsif_action = ast.Action(self.mh, t_elsif, elsif_cond, elsif_expr)
1165 elsif_action.set_ast_link(t_elsif)
1166 elsif_action.set_ast_link(t_then)
1167 rv.add_elsif(self.mh, elsif_action)
1169 self.match_kw("else")
1170 rv.set_ast_link(self.ct)
1171 else_expr = self.parse_expression(scope)
1172 rv.set_else_part(self.mh, else_expr)
1174 return rv
1176 def parse_builtin(self, scope, n_name, t_name):
1177 # lobster-trace: LRM.Builtin_Functions
1178 # lobster-trace: LRM.Builtin_Type_Conversion_Functions
1179 assert isinstance(scope, ast.Scope)
1180 assert isinstance(n_name, (ast.Builtin_Function,
1181 ast.Builtin_Numeric_Type))
1182 assert isinstance(t_name, Token)
1184 # Parse the arguments.
1185 parameters = []
1186 n_name.set_ast_link(self.ct)
1187 self.match("BRA")
1188 n_name.set_ast_link(self.ct)
1189 while not self.peek("KET"): 1189 ↛ 1200line 1189 didn't jump to line 1200 because the condition on line 1189 was always true
1190 exp = self.parse_expression(scope)
1191 if not self.ct.ast_link: 1191 ↛ 1192line 1191 didn't jump to line 1192 because the condition on line 1191 was never true
1192 exp.set_ast_link(self.ct)
1193 parameters.append(exp)
1195 if self.peek("COMMA"): 1195 ↛ 1199line 1195 didn't jump to line 1199 because the condition on line 1195 was always true
1196 self.match("COMMA")
1197 n_name.set_ast_link(self.ct)
1198 else:
1199 break
1200 self.match("KET")
1201 n_name.set_ast_link(self.ct)
1203 # Enforce arity
1204 if isinstance(n_name, ast.Builtin_Function):
1205 required_arity = n_name.arity
1206 precise = not n_name.arity_at_least
1207 else:
1208 required_arity = 1
1209 precise = True
1211 if precise:
1212 if required_arity != len(parameters):
1213 self.mh.error(t_name.location,
1214 "function requires %u parameters" %
1215 n_name.arity)
1216 else:
1217 if required_arity > len(parameters): 1217 ↛ 1218line 1217 didn't jump to line 1218 because the condition on line 1217 was never true
1218 self.mh.error(t_name.location,
1219 "function requires at least %u parameters" %
1220 n_name.arity)
1222 # Enforce types
1223 if n_name.name == "len":
1224 if isinstance(parameters[0].typ, ast.Builtin_String):
1225 return ast.Unary_Expression(
1226 mh = self.mh,
1227 location = t_name.location,
1228 typ = self.builtin_int,
1229 operator = ast.Unary_Operator.STRING_LENGTH,
1230 n_operand = parameters[0])
1231 else:
1232 return ast.Unary_Expression(
1233 mh = self.mh,
1234 location = t_name.location,
1235 typ = self.builtin_int,
1236 operator = ast.Unary_Operator.ARRAY_LENGTH,
1237 n_operand = parameters[0])
1239 elif n_name.name in ("startswith",
1240 "endswith"):
1241 return ast.Binary_Expression(
1242 mh = self.mh,
1243 location = t_name.location,
1244 typ = self.builtin_bool,
1245 operator = (ast.Binary_Operator.STRING_STARTSWITH
1246 if "startswith" in n_name.name
1247 else ast.Binary_Operator.STRING_ENDSWITH),
1248 n_lhs = parameters[0],
1249 n_rhs = parameters[1])
1251 elif n_name.name == "matches":
1252 parameters[1].ensure_type(self.mh, ast.Builtin_String)
1253 try:
1254 # lobster-trace: LRM.Static_Regular_Expression
1255 # scope is None on purpose to enforce static context
1256 value = parameters[1].evaluate(self.mh, None, None)
1257 assert isinstance(value.typ, ast.Builtin_String)
1258 re.compile(value.value)
1259 except re.error as err:
1260 self.mh.error(value.location,
1261 str(err))
1262 return ast.Binary_Expression(
1263 mh = self.mh,
1264 location = t_name.location,
1265 typ = self.builtin_bool,
1266 operator = ast.Binary_Operator.STRING_REGEX,
1267 n_lhs = parameters[0],
1268 n_rhs = parameters[1])
1270 elif n_name.name == "oneof":
1271 return ast.OneOf_Expression(
1272 mh = self.mh,
1273 location = t_name.location,
1274 typ = self.builtin_bool,
1275 choices = parameters)
1277 elif isinstance(n_name, ast.Builtin_Numeric_Type):
1278 parameters[0].ensure_type(self.mh, ast.Builtin_Numeric_Type)
1279 if isinstance(n_name, ast.Builtin_Integer):
1280 return ast.Unary_Expression(
1281 mh = self.mh,
1282 location = t_name.location,
1283 typ = self.builtin_int,
1284 operator = ast.Unary_Operator.CONVERSION_TO_INT,
1285 n_operand = parameters[0])
1286 elif isinstance(n_name, ast.Builtin_Decimal):
1287 return ast.Unary_Expression(
1288 mh = self.mh,
1289 location = t_name.location,
1290 typ = self.builtin_decimal,
1291 operator = ast.Unary_Operator.CONVERSION_TO_DECIMAL,
1292 n_operand = parameters[0])
1293 else:
1294 self.mh.ice_loc(t_name.location,
1295 "unexpected type conversion")
1297 else:
1298 self.mh.ice_loc(t_name.location,
1299 "unexpected builtin")
1301 def parse_name(self, scope):
1302 # lobster-trace: LRM.Names
1304 # This is a bit more complex. The grammar is:
1305 #
1306 # qualified_name ::= [ IDENTIFIER_package_name '.' ] IDENTIFIER_name
1307 #
1308 # name ::= qualified_name
1309 # | name '.' IDENTIFIER
1310 # | name '[' expression ']'
1311 # | name '(' parameter_list ')'
1312 #
1313 # parameter_list ::= expression { ',' expression }
1315 assert isinstance(scope, ast.Scope)
1317 # All names start with a (qualified) identifier. We parse that
1318 # first. There is a special complication for functions, as
1319 # builtin functions (e.g. len) can shadow record
1320 # components. However as functions cannot be stored in
1321 # components the true grammar for function calls is always
1322 # IDENTIFIER '('; so we can slightly special case this.
1324 # lobster-trace: LRM.Builtin_Functions
1325 # lobster-trace: LRM.Builtin_Type_Conversion_Functions
1326 self.match("IDENTIFIER")
1327 if self.peek("BRA"):
1328 # If we follow our name with brackets
1329 # immediately, we have a builtin function call.
1330 n_name = self.stab.lookup(self.mh,
1331 self.ct)
1332 if not isinstance(n_name, (ast.Builtin_Function, 1332 ↛ 1334line 1332 didn't jump to line 1334 because the condition on line 1332 was never true
1333 ast.Builtin_Numeric_Type)):
1334 self.mh.error(self.ct.location,
1335 "not a valid builtin function "
1336 "or numeric type")
1337 else:
1338 n_name = self.parse_qualified_name(scope, match_ident=False)
1340 # Enum literals are a bit different, so we deal with them
1341 # first.
1342 if isinstance(n_name, ast.Enumeration_Type):
1343 n_name.set_ast_link(self.ct)
1344 self.match("DOT")
1345 n_name.set_ast_link(self.ct)
1346 self.match("IDENTIFIER")
1347 lit = n_name.literals.lookup(self.mh,
1348 self.ct,
1349 ast.Enumeration_Literal_Spec)
1350 enum_lit = ast.Enumeration_Literal(location = self.ct.location,
1351 literal = lit)
1352 enum_lit.set_ast_link(self.ct)
1353 return enum_lit
1355 # Anything that remains is either a function call or an actual
1356 # name. Let's just enforce this for sanity.
1357 if not isinstance(n_name, (ast.Builtin_Function, 1357 ↛ 1362line 1357 didn't jump to line 1362 because the condition on line 1357 was never true
1358 ast.Builtin_Numeric_Type,
1359 ast.Composite_Component,
1360 ast.Quantified_Variable,
1361 )):
1362 self.mh.error(self.ct.location,
1363 "%s %s is not a valid name" %
1364 (n_name.__class__.__name__,
1365 n_name.name))
1367 # Right now function calls and type conversions must be
1368 # top-level, so let's get these out of the way as well.
1369 if isinstance(n_name, (ast.Builtin_Function,
1370 ast.Builtin_Numeric_Type)):
1371 # lobster-trace: LRM.Builtin_Functions
1372 # lobster-trace: LRM.Builtin_Type_Conversion_Functions
1373 return self.parse_builtin(scope, n_name, self.ct)
1375 assert isinstance(n_name, (ast.Composite_Component,
1376 ast.Quantified_Variable))
1378 # We now process the potentially recursive part:
1379 # | name '.' IDENTIFIER
1380 # | name '[' expression ']'
1381 n_name = ast.Name_Reference(location = self.ct.location,
1382 entity = n_name)
1383 n_name.set_ast_link(self.ct)
1384 while self.peek("DOT") or self.peek("S_BRA"):
1385 if self.peek("DOT"):
1386 if not isinstance(n_name.typ, (ast.Tuple_Type, 1386 ↛ 1389line 1386 didn't jump to line 1389 because the condition on line 1386 was never true
1387 ast.Record_Type)):
1388 # lobster-trace: LRM.Valid_Index_Prefixes
1389 self.mh.error(n_name.location,
1390 "expression '%s' has type %s, "
1391 "which is not a tuple or record" %
1392 (n_name.to_string(),
1393 n_name.typ.name))
1394 self.match("DOT")
1395 t_dot = self.ct
1396 self.match("IDENTIFIER")
1397 n_field = n_name.typ.components.lookup(self.mh,
1398 self.ct,
1399 ast.Composite_Component)
1400 n_field.set_ast_link(self.ct)
1401 n_name = ast.Field_Access_Expression(
1402 mh = self.mh,
1403 location = self.ct.location,
1404 n_prefix = n_name,
1405 n_field = n_field)
1406 n_name.set_ast_link(t_dot)
1408 elif self.peek("S_BRA"): 1408 ↛ 1384line 1408 didn't jump to line 1384 because the condition on line 1408 was always true
1409 if not isinstance(n_name.typ, ast.Array_Type):
1410 self.mh.error(n_name.location,
1411 "expression '%s' has type %s, "
1412 "which is not an array" %
1413 (n_name.to_string(),
1414 n_name.typ.name))
1416 self.match("S_BRA")
1417 t_bracket = self.ct
1418 n_index = self.parse_expression(scope)
1419 self.match("S_KET")
1420 a_binary = ast.Binary_Operator.INDEX
1421 t_bracket.ast_link = a_binary
1422 self.ct.ast_link = a_binary
1424 n_name = ast.Binary_Expression(
1425 mh = self.mh,
1426 location = t_bracket.location,
1427 typ = n_name.typ.element_type,
1428 operator = a_binary,
1429 n_lhs = n_name,
1430 n_rhs = n_index)
1432 return n_name
1434 def parse_check_block(self):
1435 # lobster-trace: LRM.Check_Block
1436 t_severity = None
1437 self.match_kw("checks")
1438 t_checks = self.ct
1439 self.match("IDENTIFIER")
1440 # lobster-trace: LRM.Applicable_Types
1441 # lobster-trace: LRM.Applicable_Components
1442 n_ctype = self.cu.package.symbols.lookup(self.mh,
1443 self.ct,
1444 ast.Composite_Type)
1445 n_check_block = ast.Check_Block(location = self.ct.location,
1446 n_typ = n_ctype)
1447 n_check_block.set_ast_link(t_checks)
1448 n_ctype.set_ast_link(self.ct)
1449 scope = ast.Scope()
1450 scope.push(self.stab)
1451 scope.push(self.cu.package.symbols)
1452 scope.push(n_ctype.components)
1453 self.match("C_BRA")
1454 n_check_block.set_ast_link(self.ct)
1455 while not self.peek("C_KET"):
1456 c_expr = self.parse_expression(scope)
1457 if not isinstance(c_expr.typ, ast.Builtin_Boolean): 1457 ↛ 1458line 1457 didn't jump to line 1458 because the condition on line 1457 was never true
1458 self.mh.error(c_expr.location,
1459 "check expression must be Boolean")
1461 self.match("COMMA")
1462 t_first_comma = self.ct
1463 if self.peek("KEYWORD"):
1464 self.match("KEYWORD")
1465 t_severity = self.ct
1466 if self.ct.value not in ("warning", "error", "fatal"): 1466 ↛ 1467line 1466 didn't jump to line 1467 because the condition on line 1466 was never true
1467 self.mh.error(self.ct.location,
1468 "expected warning|error|fatal")
1469 c_sev = self.ct.value
1470 else:
1471 c_sev = "error"
1473 self.match("STRING")
1474 if "\n" in self.ct.value:
1475 # lobster-trace: LRM.No_Newlines_In_Message
1476 self.mh.error(self.ct.location,
1477 "error message must not contain a newline",
1478 fatal = False)
1479 t_msg = self.ct
1481 has_extrainfo = False
1482 has_anchor = False
1483 if self.peek("COMMA"):
1484 self.match("COMMA")
1485 t_second_comma = self.ct
1486 if self.peek("IDENTIFIER"):
1487 has_anchor = True
1488 elif self.peek("STRING"): 1488 ↛ 1491line 1488 didn't jump to line 1491 because the condition on line 1488 was always true
1489 has_extrainfo = True
1490 else:
1491 self.mh.error(self.nt.location,
1492 "expected either a details string or"
1493 " identifier to anchor the check message")
1495 if has_extrainfo:
1496 self.match("STRING")
1497 t_extrainfo = self.ct
1498 c_extrainfo = self.ct.value
1500 if self.peek("COMMA"): 1500 ↛ 1501line 1500 didn't jump to line 1501 because the condition on line 1500 was never true
1501 self.match("COMMA")
1502 t_third_comma = self.ct
1503 has_anchor = True
1505 else:
1506 c_extrainfo = None
1508 if has_anchor:
1509 self.match("IDENTIFIER")
1510 t_anchor = self.ct
1511 c_anchor = n_ctype.components.lookup(self.mh,
1512 self.ct,
1513 ast.Composite_Component)
1514 else:
1515 c_anchor = None
1517 n_check = ast.Check(n_type = n_ctype,
1518 n_expr = c_expr,
1519 n_anchor = c_anchor,
1520 severity = c_sev,
1521 t_message = t_msg,
1522 extrainfo = c_extrainfo)
1524 # pylint: disable=possibly-used-before-assignment
1525 # pylint: disable=used-before-assignment
1527 n_check.set_ast_link(t_first_comma)
1528 if t_severity:
1529 n_check.set_ast_link(t_severity)
1530 n_check.set_ast_link(t_msg)
1531 if c_extrainfo or c_anchor:
1532 n_check.set_ast_link(t_second_comma)
1533 if c_extrainfo:
1534 n_check.set_ast_link(t_extrainfo)
1535 if c_anchor:
1536 c_anchor.set_ast_link(t_anchor)
1537 if c_anchor and c_extrainfo: 1537 ↛ 1538line 1537 didn't jump to line 1538 because the condition on line 1537 was never true
1538 n_check.set_ast_link(t_third_comma)
1540 n_ctype.add_check(n_check)
1541 n_check_block.add_check(n_check)
1543 assert scope.size() == 3
1545 self.match("C_KET")
1546 n_check_block.set_ast_link(self.ct)
1548 return n_check_block
1550 def parse_section_declaration(self):
1551 # lobster-trace: LRM.Section_Declaration
1552 self.match_kw("section")
1553 t_section = self.ct
1554 self.match("STRING")
1555 sec = ast.Section(name = self.ct.value,
1556 location = self.ct.location,
1557 parent = self.section[-1] if self.section else None)
1558 sec.set_ast_link(self.ct)
1559 sec.set_ast_link(t_section)
1560 self.section.append(sec)
1561 self.match("C_BRA")
1562 sec.set_ast_link(self.ct)
1563 while not self.peek("C_KET"):
1564 self.parse_trlc_entry()
1565 self.match("C_KET")
1566 sec.set_ast_link(self.ct)
1567 self.section.pop()
1569 def parse_boolean(self):
1570 # lobster-trace: LRM.Boolean_Values
1571 self.match("KEYWORD")
1572 if self.ct.value in ("true", "false"): 1572 ↛ 1575line 1572 didn't jump to line 1575 because the condition on line 1572 was always true
1573 return ast.Boolean_Literal(self.ct, self.builtin_bool)
1574 else:
1575 self.mh.error(self.ct.location,
1576 "expected boolean literal (true or false)")
1578 def parse_value(self, typ):
1579 # lobster-trace: LRM.Tuple_Syntax_Correct_Form
1580 assert isinstance(typ, ast.Type)
1582 if isinstance(typ, ast.Builtin_Numeric_Type):
1583 # lobster-trace: LRM.Integer_Values
1584 # lobster-trace: LRM.Decimal_Values
1585 if self.peek("OPERATOR") and \
1586 self.nt.value in Parser.ADDING_OPERATOR:
1587 self.match("OPERATOR")
1588 t_op = self.ct
1589 e_op = (ast.Unary_Operator.PLUS
1590 if t_op.value == "+"
1591 else ast.Unary_Operator.MINUS)
1592 t_op.ast_link = e_op
1593 else:
1594 t_op = None
1596 if isinstance(typ, ast.Builtin_Decimal):
1597 self.match("DECIMAL")
1598 rv = ast.Decimal_Literal(self.ct, self.builtin_decimal)
1599 rv.set_ast_link(self.ct)
1600 elif isinstance(typ, ast.Builtin_Integer):
1601 self.match("INTEGER")
1602 rv = ast.Integer_Literal(self.ct, self.builtin_int)
1603 rv.set_ast_link(self.ct)
1604 else:
1605 assert False
1607 if t_op:
1608 rv = ast.Unary_Expression(mh = self.mh,
1609 location = t_op.location,
1610 typ = rv.typ,
1611 operator = e_op,
1612 n_operand = rv)
1614 return rv
1616 elif isinstance(typ, ast.Builtin_Markup_String):
1617 # lobster-trace: LRM.Markup_String_Values
1618 return self.parse_markup_string()
1620 elif isinstance(typ, ast.Builtin_String):
1621 # lobster-trace: LRM.String_Values
1622 self.match("STRING")
1623 rv = ast.String_Literal(self.ct, self.builtin_str)
1624 rv.set_ast_link(self.ct)
1625 return rv
1627 elif isinstance(typ, ast.Builtin_Boolean):
1628 rv = self.parse_boolean()
1629 rv.set_ast_link(self.ct)
1630 return rv
1632 elif isinstance(typ, ast.Array_Type):
1633 self.match("S_BRA")
1634 rv = ast.Array_Aggregate(self.ct.location,
1635 typ)
1636 rv.set_ast_link(self.ct)
1637 while not self.peek("S_KET"):
1638 array_elem = self.parse_value(typ.element_type)
1639 rv.append(array_elem)
1640 if self.peek("COMMA"):
1641 self.match("COMMA")
1642 rv.set_ast_link(self.ct)
1643 elif self.peek("S_KET") or self.nt is None: 1643 ↛ anywhereline 1643 didn't jump anywhere: it always raised an exception.
1644 break
1645 else:
1646 self.mh.error(self.ct.location,
1647 "comma separating array elements is "
1648 "missing",
1649 fatal = False)
1651 self.match("S_KET")
1652 rv.set_ast_link(self.ct)
1654 if len(rv.value) < typ.lower_bound:
1655 self.mh.error(self.ct.location,
1656 "this array requires at least %u elements "
1657 "(only %u provided)" %
1658 (typ.lower_bound,
1659 len(rv.value)),
1660 fatal=False)
1661 if typ.upper_bound and len(rv.value) > typ.upper_bound:
1662 self.mh.error(rv.value[typ.upper_bound].location,
1663 "this array requires at most %u elements "
1664 "(%u provided)" %
1665 (typ.upper_bound,
1666 len(rv.value)),
1667 fatal=False)
1669 return rv
1671 elif isinstance(typ, ast.Enumeration_Type):
1672 enum = self.parse_qualified_name(self.default_scope,
1673 ast.Enumeration_Type)
1674 enum.set_ast_link(self.ct)
1675 if enum != typ:
1676 self.mh.error(self.ct.location,
1677 "expected %s" % typ.name)
1678 self.match("DOT")
1679 enum.set_ast_link(self.ct)
1680 self.match("IDENTIFIER")
1681 lit = enum.literals.lookup(self.mh,
1682 self.ct,
1683 ast.Enumeration_Literal_Spec)
1684 return ast.Enumeration_Literal(self.ct.location,
1685 lit)
1687 elif isinstance(typ, ast.Record_Type):
1688 self.match("IDENTIFIER")
1689 t_name = self.ct
1690 if self.peek("DOT"):
1691 self.match("DOT")
1692 t_dot = self.ct
1693 self.match("IDENTIFIER")
1694 the_pkg = self.stab.lookup(self.mh, t_name, ast.Package)
1695 the_pkg.set_ast_link(t_name)
1696 the_pkg.set_ast_link(t_dot)
1697 if not self.cu.is_visible(the_pkg): 1697 ↛ 1698line 1697 didn't jump to line 1698 because the condition on line 1697 was never true
1698 self.mh.error(self.ct.location,
1699 "package must be imported before use")
1700 t_name = self.ct
1701 else:
1702 the_pkg = self.cu.package
1704 rv = ast.Record_Reference(location = t_name.location,
1705 name = t_name.value,
1706 typ = typ,
1707 package = the_pkg)
1708 rv.set_ast_link(t_name)
1710 # We can do an early lookup if the target is known
1711 if the_pkg.symbols.contains(t_name.value):
1712 rv.resolve_references(self.mh)
1714 return rv
1716 elif isinstance(typ, ast.Tuple_Type) and typ.has_separators():
1717 # lobster-trace: LRM.Tuple_Separator_Form
1718 rv = ast.Tuple_Aggregate(self.nt.location, typ)
1720 next_is_optional = False
1721 for n_item in typ.iter_sequence():
1722 if isinstance(n_item, ast.Composite_Component):
1723 if next_is_optional and n_item.optional:
1724 break
1725 value = self.parse_value(n_item.n_typ)
1726 rv.assign(n_item.name, value)
1728 elif n_item.token.kind in ("AT", "COLON", "SEMICOLON"):
1729 if self.peek(n_item.token.kind):
1730 self.match(n_item.token.kind)
1731 n_item.set_ast_link(self.ct)
1732 else:
1733 next_is_optional = True
1735 elif n_item.token.kind == "IDENTIFIER":
1736 if self.peek("IDENTIFIER") and \
1737 self.nt.value == n_item.token.value:
1738 self.match("IDENTIFIER")
1739 n_item.set_ast_link(self.ct)
1740 else:
1741 next_is_optional = True
1743 else:
1744 assert False
1746 return rv
1748 elif isinstance(typ, ast.Tuple_Type) and not typ.has_separators():
1749 # lobster-trace: LRM.Tuple_Generic_Form
1750 self.match("BRA")
1751 rv = ast.Tuple_Aggregate(self.ct.location, typ)
1752 rv.set_ast_link(self.ct)
1754 first = True
1755 for n_field in typ.iter_sequence():
1756 if first:
1757 first = False
1758 else:
1759 self.match("COMMA")
1760 rv.set_ast_link(self.ct)
1761 rv.assign(n_field.name,
1762 self.parse_value(n_field.n_typ))
1764 self.match("KET")
1765 rv.set_ast_link(self.ct)
1766 return rv
1768 else:
1769 self.mh.ice_loc(self.ct.location,
1770 "logic error: unexpected type %s" %
1771 typ.__class__.__name__)
1773 def parse_markup_string(self):
1774 # lobster-trace: LRM.Markup_String_Values
1775 self.match("STRING")
1776 rv = ast.String_Literal(self.ct, self.builtin_mstr)
1777 mpar = Markup_Parser(self, rv)
1778 mpar.parse_all_references()
1779 return rv
1781 def parse_record_object_declaration(self):
1782 # lobster-trace: LRM.Section_Declaration
1783 # lobster-trace: LRM.Record_Object_Declaration
1784 # lobster-trace: LRM.Valid_Record_Types
1785 # lobster-trace: LRM.Valid_Components
1786 # lobster-trace: LRM.Valid_Enumeration_Literals
1787 # lobster-trace: LRM.Mandatory_Components
1788 # lobster-trace: LRM.Evaluation_Of_Checks
1789 # lobster-trace: LRM.Single_Value_Assignment
1791 r_typ = self.parse_qualified_name(self.default_scope,
1792 ast.Record_Type)
1793 r_typ.set_ast_link(self.ct)
1794 if r_typ.is_abstract:
1795 self.mh.error(self.ct.location,
1796 "cannot declare object of abstract record type %s" %
1797 r_typ.name)
1799 self.match("IDENTIFIER")
1800 obj = ast.Record_Object(
1801 name = self.ct.value,
1802 location = self.ct.location,
1803 n_typ = r_typ,
1804 section = self.section.copy() if self.section else None,
1805 n_package = self.cu.package)
1806 self.cu.package.symbols.register(self.mh, obj)
1807 obj.set_ast_link(self.ct)
1809 self.match("C_BRA")
1810 obj.set_ast_link(self.ct)
1811 while not self.peek("C_KET"):
1812 self.match("IDENTIFIER")
1813 comp = r_typ.components.lookup(self.mh,
1814 self.ct,
1815 ast.Composite_Component)
1816 if obj.is_component_implicit_null(comp):
1817 self.mh.error(self.ct.location,
1818 "component '%s' already assigned at line %i" %
1819 (comp.name,
1820 obj.field[comp.name].location.line_no))
1821 comp.set_ast_link(self.ct)
1822 if r_typ.is_frozen(comp):
1823 self.mh.error(self.ct.location,
1824 "cannot overwrite frozen component %s" %
1825 comp.name)
1826 self.match("ASSIGN")
1827 comp.set_ast_link(self.ct)
1828 value = self.parse_value(comp.n_typ)
1829 if not self.ct.ast_link:
1830 value.set_ast_link(self.ct)
1831 obj.assign(comp, value)
1833 # Check that each non-optional component has been specified
1834 for comp in r_typ.all_components():
1835 if isinstance(obj.field[comp.name], ast.Implicit_Null):
1836 if r_typ.is_frozen(comp):
1837 obj.assign(comp, r_typ.get_freezing_expression(comp))
1838 elif not comp.optional:
1839 self.mh.error(
1840 obj.location,
1841 "required component %s (see %s) is not defined" %
1842 (comp.name,
1843 self.mh.cross_file_reference(comp.location)))
1845 self.match("C_KET")
1846 obj.set_ast_link(self.ct)
1848 return obj
1850 def parse_trlc_entry(self):
1851 # lobster-trace: LRM.TRLC_File
1852 if self.peek_kw("section"):
1853 self.parse_section_declaration()
1854 else:
1855 self.cu.add_item(self.parse_record_object_declaration())
1857 def parse_preamble(self, kind):
1858 assert kind in ("rsl", "trlc")
1859 # lobster-trace: LRM.Layout
1860 # lobster-trace: LRM.Preamble
1862 # First, parse package indication, declaring the package if
1863 # needed
1864 self.match_kw("package")
1865 t_pkg = self.ct
1866 self.match("IDENTIFIER")
1868 if kind == "rsl":
1869 declare_package = True
1870 else:
1871 # lobster-trace: LRM.Late_Package_Declarations
1872 declare_package = not self.stab.contains(self.ct.value)
1874 if declare_package:
1875 # lobster-trace: LRM.Package_Declaration
1876 pkg = ast.Package(name = self.ct.value,
1877 location = self.ct.location,
1878 builtin_stab = self.stab,
1879 declared_late = kind == "trlc")
1880 self.stab.register(self.mh, pkg)
1881 else:
1882 pkg = self.stab.lookup(self.mh, self.ct, ast.Package)
1884 pkg.set_ast_link(t_pkg)
1885 pkg.set_ast_link(self.ct)
1887 # lobster-trace: LRM.Current_Package
1888 self.cu.set_package(pkg)
1890 self.default_scope.push(self.cu.package.symbols)
1892 # Second, parse import list (but don't resolve names yet)
1893 # lobster-trace: LRM.Import_Visibility
1894 if kind != "check": 1894 ↛ exitline 1894 didn't return from function 'parse_preamble' because the condition on line 1894 was always true
1895 while self.peek_kw("import"):
1896 self.match_kw("import")
1897 pkg.set_ast_link(self.ct)
1898 self.match("IDENTIFIER")
1899 self.cu.add_import(self.mh, self.ct)
1901 def parse_rsl_file(self):
1902 # lobster-trace: LRM.RSL_File
1903 assert self.cu.package is not None
1905 ok = True
1906 while not self.peek_eof():
1907 try:
1908 if self.peek_kw("checks"):
1909 self.cu.add_item(self.parse_check_block())
1910 else:
1911 self.cu.add_item(self.parse_type_declaration())
1912 except TRLC_Error as err:
1913 if not self.error_recovery or err.kind == "lex error": 1913 ↛ 1914line 1913 didn't jump to line 1914 because the condition on line 1913 was never true
1914 raise
1916 ok = False
1918 # Recovery strategy is to scan until we get the next
1919 # relevant keyword
1920 self.skip_until_newline()
1921 while not self.peek_eof():
1922 if self.peek_kw("checks") or \
1923 self.peek_kw("type") or \
1924 self.peek_kw("abstract") or \
1925 self.peek_kw("final") or \
1926 self.peek_kw("tuple") or \
1927 self.peek_kw("enum"):
1928 break
1929 self.advance()
1930 self.skip_until_newline()
1932 self.match_eof()
1934 for tok in self.lexer.tokens:
1935 if tok.kind == "COMMENT":
1936 self.cu.package.set_ast_link(tok)
1938 return ok
1940 def parse_trlc_file(self):
1941 # lobster-trace: LRM.TRLC_File
1942 assert self.cu.package is not None
1944 ok = True
1946 while self.peek_kw("section") or self.peek("IDENTIFIER"):
1947 try:
1948 self.parse_trlc_entry()
1949 except TRLC_Error as err:
1950 if not self.error_recovery or err.kind == "lex error": 1950 ↛ 1951line 1950 didn't jump to line 1951 because the condition on line 1950 was never true
1951 raise
1953 ok = False
1955 # Recovery strategy is to keep going until we find an
1956 # identifier that is a package or type, or section, or
1957 # EOF
1958 self.skip_until_newline()
1959 while not self.peek_eof():
1960 if self.peek_kw("section"): 1960 ↛ 1961line 1960 didn't jump to line 1961 because the condition on line 1960 was never true
1961 break
1962 elif not self.peek("IDENTIFIER"):
1963 pass
1964 elif self.stab.contains(self.nt.value): 1964 ↛ 1965line 1964 didn't jump to line 1965 because the condition on line 1964 was never true
1965 n_sym = self.stab.lookup_assuming(self.mh,
1966 self.nt.value)
1967 if isinstance(n_sym, ast.Package):
1968 break
1969 elif self.cu.package.symbols.contains(self.nt.value):
1970 n_sym = self.cu.package.symbols.lookup_assuming(
1971 self.mh,
1972 self.nt.value)
1973 if isinstance(n_sym, ast.Record_Type):
1974 break
1975 self.advance()
1976 self.skip_until_newline()
1978 self.match_eof()
1980 for tok in self.lexer.tokens:
1981 if tok.kind == "COMMENT":
1982 self.cu.package.set_ast_link(tok)
1984 return ok