fprime-gds 4.0.0a8__py3-none-any.whl → 4.0.0a9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1687 @@
1
+ from __future__ import annotations
2
+ from abc import ABC
3
+ import inspect
4
+ from dataclasses import dataclass, field, fields
5
+ import traceback
6
+ from typing import Callable
7
+ import typing
8
+
9
+ from fprime_gds.common.fpy.bytecode.directives import (
10
+ FLOAT_INEQUALITY_DIRECTIVES,
11
+ MAX_SERIALIZABLE_REGISTER_SIZE,
12
+ INT_SIGNED_INEQUALITY_DIRECTIVES,
13
+ INT_UNSIGNED_INEQUALITY_DIRECTIVES,
14
+ AndDirective,
15
+ CmdDirective,
16
+ DeserSerReg1Directive,
17
+ DeserSerReg2Directive,
18
+ DeserSerReg4Directive,
19
+ DeserSerReg8Directive,
20
+ Directive,
21
+ FloatExtendDirective,
22
+ IntEqualDirective,
23
+ ExitDirective,
24
+ FloatEqualDirective,
25
+ FloatNotEqualDirective,
26
+ GetPrmDirective,
27
+ GetTlmDirective,
28
+ GotoDirective,
29
+ IfDirective,
30
+ NotDirective,
31
+ IntNotEqualDirective,
32
+ OrDirective,
33
+ SetSerRegDirective,
34
+ SetRegDirective,
35
+ SignedIntToFloatDirective,
36
+ UnsignedIntToFloatDirective,
37
+ WaitAbsDirective,
38
+ WaitRelDirective,
39
+ )
40
+ from fprime_gds.common.loaders.ch_json_loader import ChJsonLoader
41
+ from fprime_gds.common.loaders.cmd_json_loader import CmdJsonLoader
42
+ from fprime_gds.common.loaders.prm_json_loader import PrmJsonLoader
43
+ from fprime_gds.common.templates.ch_template import ChTemplate
44
+ from fprime_gds.common.templates.cmd_template import CmdTemplate
45
+ from fprime_gds.common.templates.prm_template import PrmTemplate
46
+ from fprime.common.models.serialize.time_type import TimeType
47
+ from fprime.common.models.serialize.enum_type import EnumType
48
+ from fprime.common.models.serialize.serializable_type import SerializableType
49
+ from fprime.common.models.serialize.array_type import ArrayType
50
+ from fprime.common.models.serialize.numerical_types import (
51
+ U32Type,
52
+ U16Type,
53
+ U64Type,
54
+ U8Type,
55
+ I16Type,
56
+ I32Type,
57
+ I64Type,
58
+ I8Type,
59
+ F32Type,
60
+ F64Type,
61
+ FloatType,
62
+ IntegerType,
63
+ NumericalType,
64
+ )
65
+ from fprime.common.models.serialize.string_type import StringType
66
+ from fprime.common.models.serialize.bool_type import BoolType
67
+ from fprime_gds.common.fpy.parser import (
68
+ AstAnd,
69
+ AstBoolean,
70
+ AstComparison,
71
+ AstElif,
72
+ AstElifs,
73
+ AstExpr,
74
+ AstGetAttr,
75
+ AstGetItem,
76
+ AstNot,
77
+ AstNumber,
78
+ AstOr,
79
+ AstReference,
80
+ AstString,
81
+ Ast,
82
+ AstTest,
83
+ AstBody,
84
+ AstLiteral,
85
+ AstIf,
86
+ AstAssign,
87
+ AstFuncCall,
88
+ AstVar,
89
+ )
90
+ from fprime.common.models.serialize.type_base import BaseType as FppType
91
+
92
+ NUMERIC_TYPES = (
93
+ U32Type,
94
+ U16Type,
95
+ U64Type,
96
+ U8Type,
97
+ I16Type,
98
+ I32Type,
99
+ I64Type,
100
+ I8Type,
101
+ F32Type,
102
+ F64Type,
103
+ )
104
+ INTEGER_TYPES = (
105
+ U32Type,
106
+ U16Type,
107
+ U64Type,
108
+ U8Type,
109
+ I16Type,
110
+ I32Type,
111
+ I64Type,
112
+ I8Type,
113
+ )
114
+ SIGNED_INTEGER_TYPES = (
115
+ I16Type,
116
+ I32Type,
117
+ I64Type,
118
+ I8Type,
119
+ )
120
+ UNSIGNED_INTEGER_TYPES = (
121
+ U32Type,
122
+ U16Type,
123
+ U64Type,
124
+ U8Type,
125
+ )
126
+ FLOAT_TYPES = (
127
+ F32Type,
128
+ F64Type,
129
+ )
130
+
131
+
132
+ # a value of type FppTypeClass is a Python `type` object representing
133
+ # the type of an Fprime value
134
+ FppTypeClass = type[FppType]
135
+
136
+
137
+ class NothingType(ABC):
138
+ """a type which has no valid values in fprime. used to denote
139
+ a function which doesn't return a value"""
140
+ @classmethod
141
+ def __subclasscheck__(cls, subclass):
142
+ return False
143
+
144
+
145
+ # the `type` object representing the NothingType class
146
+ NothingTypeClass = type[NothingType]
147
+
148
+
149
+ class CompileException(BaseException):
150
+ def __init__(self, msg, node: Ast):
151
+ self.msg = msg
152
+ self.node = node
153
+ self.stack_trace = "\n".join(traceback.format_stack(limit=8)[:-1])
154
+
155
+ def __str__(self):
156
+ return f'{self.stack_trace}\nAt line {self.node.meta.line} "{self.node.node_text}": {self.msg}'
157
+
158
+
159
+ @dataclass
160
+ class FpyCallable:
161
+ return_type: FppTypeClass | NothingTypeClass
162
+ args: list[tuple[str, FppTypeClass]]
163
+
164
+
165
+ @dataclass
166
+ class FpyCmd(FpyCallable):
167
+ cmd: CmdTemplate
168
+
169
+
170
+ @dataclass
171
+ class FpyMacro(FpyCallable):
172
+ instantiate_macro: Callable[[list[FppType]], list[Directive]]
173
+ """a function which instantiates the macro given the argument values"""
174
+
175
+
176
+ MACROS: dict[str, FpyMacro] = {
177
+ "sleep": FpyMacro(
178
+ NothingType, [("seconds", F64Type)], lambda args: [WaitRelDirective(int(args[0].val), int(args[0].val * 1000000) % 1000000)]
179
+ ),
180
+ "sleep_until": FpyMacro(
181
+ NothingType, [("wakeup_time", TimeType)], lambda args: [WaitAbsDirective(args[0])]
182
+ ),
183
+ "exit": FpyMacro(NothingType, [("success", BoolType)], lambda args: [ExitDirective(args[0].val)]),
184
+ }
185
+
186
+
187
+ @dataclass
188
+ class FpyTypeCtor(FpyCallable):
189
+ type: FppTypeClass
190
+
191
+
192
+ @dataclass
193
+ class FieldReference:
194
+ """a reference to a field/index of an fprime type"""
195
+
196
+ parent: "FpyReference"
197
+ """the qualifier"""
198
+ type: FppTypeClass
199
+ """the fprime type of this reference"""
200
+ offset: int
201
+ """the constant offset in the parent type at which to find this field"""
202
+ name: str = None
203
+ """the name of the field, if applicable"""
204
+ idx: int = None
205
+ """the index of the field, if applicable"""
206
+
207
+ def get_from(self, parent_val: FppType) -> FppType:
208
+ """gets the field value from the parent value"""
209
+ assert isinstance(parent_val, self.type)
210
+ assert self.name is not None or self.idx is not None
211
+ value = None
212
+ if self.name is not None:
213
+ if isinstance(parent_val, SerializableType):
214
+ value = parent_val.val[self.name]
215
+ elif isinstance(parent_val, TimeType):
216
+ if self.name == "seconds":
217
+ value = parent_val.__secs
218
+ elif self.name == "useconds":
219
+ value = parent_val.__usecs
220
+ elif self.name == "time_base":
221
+ value = parent_val.__timeBase
222
+ elif self.name == "time_context":
223
+ value = parent_val.__timeContext
224
+ else:
225
+ assert False, self.name
226
+ else:
227
+ assert False, parent_val
228
+
229
+ else:
230
+
231
+ assert isinstance(parent_val, ArrayType), parent_val
232
+
233
+ value = parent_val._val[self.idx]
234
+
235
+ assert isinstance(value, self.type), (value, self.type)
236
+ return value
237
+
238
+
239
+ # named variables can be tlm chans, prms, callables, or directly referenced consts (usually enums)
240
+ @dataclass
241
+ class FpyVariable:
242
+ """a mutable, typed value referenced by an unqualified name"""
243
+
244
+ type_ref: AstExpr
245
+ """the expression denoting the var's type"""
246
+ type: FppTypeClass | None = None
247
+ """the resolved type of the variable. None if type unsure at the moment"""
248
+ sreg_idx: int | None = None
249
+ """the index of the sreg it is stored in"""
250
+
251
+
252
+ # a scope
253
+ FpyScope = dict[str, "FpyReference"]
254
+
255
+
256
+ def create_scope(
257
+ references: dict[str, "FpyReference"],
258
+ ) -> FpyScope:
259
+ """from a flat dict of strs to references, creates a hierarchical, scoped
260
+ dict. no two leaf nodes may have the same name"""
261
+
262
+ base = {}
263
+
264
+ for fqn, ref in references.items():
265
+ names_strs = fqn.split(".")
266
+
267
+ ns = base
268
+ while len(names_strs) > 1:
269
+ existing_child = ns.get(names_strs[0], None)
270
+ if existing_child is None:
271
+ # this scope is not defined atm
272
+ existing_child = {}
273
+ ns[names_strs[0]] = existing_child
274
+
275
+ if not isinstance(existing_child, dict):
276
+ # something else already has this name
277
+ print(
278
+ f"WARNING: {fqn} is already defined as {existing_child}, tried to redefine it as {ref}"
279
+ )
280
+ break
281
+
282
+ ns = existing_child
283
+ names_strs = names_strs[1:]
284
+
285
+ if len(names_strs) != 1:
286
+ # broke early. skip this loop
287
+ continue
288
+
289
+ # okay, now ns is the complete scope of the attribute
290
+ # i.e. everything up until the last '.'
291
+ name = names_strs[0]
292
+
293
+ existing_child = ns.get(name, None)
294
+
295
+ if existing_child is not None:
296
+ # uh oh, something already had this name with a diff value
297
+ print(
298
+ f"WARNING: {fqn} is already defined as {existing_child}, tried to redefine it as {ref}"
299
+ )
300
+ continue
301
+
302
+ ns[name] = ref
303
+
304
+ return base
305
+
306
+
307
+ def union_scope(lhs: FpyScope, rhs: FpyScope) -> FpyScope:
308
+ """returns the two scopes, joined into one. if there is a conflict, chooses lhs over rhs"""
309
+ lhs_keys = set(lhs.keys())
310
+ rhs_keys = set(rhs.keys())
311
+ common_keys = lhs_keys.intersection(rhs_keys)
312
+
313
+ only_lhs_keys = lhs_keys.difference(common_keys)
314
+ only_rhs_keys = rhs_keys.difference(common_keys)
315
+
316
+ new = FpyScope()
317
+
318
+ for key in common_keys:
319
+ if not isinstance(lhs[key], dict) or not isinstance(rhs[key], dict):
320
+ # cannot be merged cleanly. one of the two is not a scope
321
+ print(f"WARNING: {key} is defined as {lhs[key]}, ignoring {rhs[key]}")
322
+ new[key] = lhs[key]
323
+ continue
324
+
325
+ new[key] = union_scope(lhs[key], rhs[key])
326
+
327
+ for key in only_lhs_keys:
328
+ new[key] = lhs[key]
329
+ for key in only_rhs_keys:
330
+ new[key] = rhs[key]
331
+
332
+ return new
333
+
334
+
335
+ FpyReference = (
336
+ ChTemplate
337
+ | PrmTemplate
338
+ | FppType
339
+ | FpyCallable
340
+ | FppTypeClass
341
+ | FpyVariable
342
+ | FieldReference
343
+ | dict # FpyReference
344
+ )
345
+ """some named concept in fpy"""
346
+
347
+
348
+ def get_ref_fpp_type_class(ref: FpyReference) -> FppTypeClass:
349
+ """returns the fprime type of the ref, if it were to be evaluated as an expression"""
350
+ if isinstance(ref, ChTemplate):
351
+ result_type = ref.ch_type_obj
352
+ elif isinstance(ref, PrmTemplate):
353
+ result_type = ref.prm_type_obj
354
+ elif isinstance(ref, FppType):
355
+ # constant value
356
+ result_type = type(ref)
357
+ elif isinstance(ref, FpyCallable):
358
+ # a reference to a callable isn't a type in and of itself
359
+ # it has a return type but you have to call it (with an AstFuncCall)
360
+ # consider making a separate "reference" type
361
+ result_type = NothingType
362
+ elif isinstance(ref, FpyVariable):
363
+ result_type = ref.type
364
+ elif isinstance(ref, type):
365
+ # a reference to a type doesn't have a value, and so doesn't have a type,
366
+ # in and of itself. if this were a function call to the type's ctor then
367
+ # it would have a value and thus a type
368
+ result_type = NothingType
369
+ elif isinstance(ref, FieldReference):
370
+ result_type = ref.type
371
+ elif isinstance(ref, dict):
372
+ # reference to a scope. scopes don't have values
373
+ result_type = NothingType
374
+ else:
375
+ assert False, ref
376
+
377
+ return result_type
378
+
379
+
380
+ @dataclass
381
+ class CompileState:
382
+ """a collection of input, internal and output state variables and maps"""
383
+
384
+ types: FpyScope
385
+ """a scope whose leaf nodes are subclasses of BaseType"""
386
+ callables: FpyScope
387
+ """a scope whose leaf nodes are FpyCallable instances"""
388
+ tlms: FpyScope
389
+ """a scope whose leaf nodes are ChTemplates"""
390
+ prms: FpyScope
391
+ """a scope whose leaf nodes are PrmTemplates"""
392
+ consts: FpyScope
393
+ """a scope whose leaf nodes are instances of subclasses of BaseType"""
394
+ variables: FpyScope = field(default_factory=dict)
395
+ """a scope whose leaf nodes are FpyVariables"""
396
+ runtime_values: FpyScope = None
397
+ """a scope whose leaf nodes are tlms/prms/consts/variables, all of which
398
+ have some value at runtime."""
399
+
400
+ def __post_init__(self):
401
+ self.runtime_values = union_scope(
402
+ self.tlms,
403
+ union_scope(self.prms, union_scope(self.consts, self.variables)),
404
+ )
405
+
406
+ resolved_references: dict[AstReference, FpyReference] = field(
407
+ default_factory=dict, repr=False
408
+ )
409
+ """reference to its singular resolution"""
410
+
411
+ expr_types: dict[AstExpr, FppTypeClass | NothingTypeClass] = field(
412
+ default_factory=dict
413
+ )
414
+ """expr to its fprime type, or nothing type if none"""
415
+
416
+ expr_values: dict[AstExpr, FppType | NothingType | None] = field(
417
+ default_factory=dict
418
+ )
419
+ """expr to its fprime value, or nothing if no value, or None if unsure at compile time"""
420
+
421
+ expr_registers: dict[AstExpr, int] = field(default_factory=dict)
422
+ """expr to the register it's stored in"""
423
+
424
+ directives: dict[Ast, list[Directive] | None] = field(default_factory=dict)
425
+ """a list of code generated by each node, or None/empty list if no directives"""
426
+
427
+ node_dir_counts: dict[Ast, int] = field(default_factory=dict)
428
+ """node to the number of directives generated by it"""
429
+
430
+ next_register: int = 0
431
+ """the index of the next free register"""
432
+ next_sreg: int = 0
433
+ """the index of the next free serializable register"""
434
+
435
+ start_line_idx: dict[Ast, int] = field(default_factory=dict)
436
+ """the line index at which each node's directives will be included in the output"""
437
+
438
+ errors: list[CompileException] = field(default_factory=list)
439
+ """a list of all compile exceptions generated by passes"""
440
+
441
+ def err(self, msg, n):
442
+ """adds a compile exception to internal state"""
443
+ self.errors.append(CompileException(msg, n))
444
+
445
+
446
+ class Visitor:
447
+ """visits each class, calling a custom visit function, if one is defined, for each
448
+ node type"""
449
+
450
+ def _find_custom_visit_func(self, node: Ast):
451
+ for name, func in inspect.getmembers(type(self), inspect.isfunction):
452
+ if not name.startswith("visit") or name == "visit_default":
453
+ # not a visitor, or the default visit func
454
+ continue
455
+ signature = inspect.signature(func)
456
+ params = list(signature.parameters.values())
457
+ assert len(params) == 3
458
+ assert params[1].annotation is not None
459
+ annotations = typing.get_type_hints(func)
460
+ param_type = annotations[params[1].name]
461
+ if isinstance(node, param_type):
462
+ return func
463
+ else:
464
+ # call the default
465
+ return type(self).visit_default
466
+
467
+ def _visit(self, node: Ast, state: CompileState):
468
+ visit_func = self._find_custom_visit_func(node)
469
+ visit_func(self, node, state)
470
+
471
+ def visit_default(self, node: Ast, state: CompileState):
472
+ pass
473
+
474
+ def run(self, start: Ast, state: CompileState):
475
+ """runs the visitor, starting at the given node, descending depth-first"""
476
+
477
+ def _descend(node: Ast):
478
+ if not isinstance(node, Ast):
479
+ return
480
+ children = []
481
+ for field in fields(node):
482
+ field_val = getattr(node, field.name)
483
+ if isinstance(field_val, list):
484
+ children.extend(field_val)
485
+ else:
486
+ children.append(field_val)
487
+
488
+ for child in children:
489
+ if not isinstance(child, Ast):
490
+ continue
491
+ _descend(child)
492
+ if len(state.errors) != 0:
493
+ break
494
+ self._visit(child, state)
495
+ if len(state.errors) != 0:
496
+ break
497
+
498
+ _descend(start)
499
+ self._visit(start, state)
500
+
501
+
502
+ class TopDownVisitor(Visitor):
503
+
504
+ def run(self, start: Ast, state: CompileState):
505
+ """runs the visitor, starting at the given node, descending breadth-first"""
506
+
507
+ def _descend(node: Ast):
508
+ if not isinstance(node, Ast):
509
+ return
510
+ children = []
511
+ for field in fields(node):
512
+ field_val = getattr(node, field.name)
513
+ if isinstance(field_val, list):
514
+ children.extend(field_val)
515
+ else:
516
+ children.append(field_val)
517
+
518
+ for child in children:
519
+ if not isinstance(child, Ast):
520
+ continue
521
+ self._visit(child, state)
522
+ if len(state.errors) != 0:
523
+ break
524
+ _descend(child)
525
+ if len(state.errors) != 0:
526
+ break
527
+
528
+ self._visit(start, state)
529
+ _descend(start)
530
+
531
+
532
+ class AssignIds(TopDownVisitor):
533
+ """assigns a unique id to each node to allow it to be indexed in a dict"""
534
+
535
+ def __init__(self):
536
+ self.next_id = 0
537
+
538
+ def visit_default(self, node, state):
539
+ node.id = self.next_id
540
+ self.next_id += 1
541
+
542
+
543
+ class CreateVariables(Visitor):
544
+ """finds all variable declarations and adds them to the variable scope"""
545
+
546
+ def visit_AstAssign(self, node: AstAssign, state: CompileState):
547
+ existing = state.variables.get(node.variable.var, None)
548
+ if not existing:
549
+ # idk what this var is. make sure it's a valid declaration
550
+ if node.var_type is None:
551
+ # error because this isn't an annotated assignment. right now all declarations must be annotated
552
+ state.err(
553
+ "Must provide a type annotation for new variables", node.variable
554
+ )
555
+ return
556
+
557
+ var = FpyVariable(node.var_type, None)
558
+ # new var. put it in the table under this scope
559
+ state.variables[node.variable.var] = var
560
+ state.runtime_values[node.variable.var] = var
561
+
562
+ if existing and node.var_type is not None:
563
+ # redeclaring an existing variable
564
+ state.err(f"{node.variable.var} already declared", node)
565
+ return
566
+
567
+
568
+ class ResolveReferences(Visitor):
569
+ """for each reference, resolve it in a specific scope based on its
570
+ syntactic position, or fail if could not resolve"""
571
+
572
+ def is_type_constant_size(self, type: FppTypeClass) -> bool:
573
+ """return true if the type is statically sized"""
574
+ if issubclass(type, StringType):
575
+ return False
576
+
577
+ if issubclass(type, ArrayType):
578
+ return self.is_type_constant_size(type.MEMBER_TYPE)
579
+
580
+ if issubclass(type, SerializableType):
581
+ for _, arg_type, _, _ in type.MEMBER_LIST:
582
+ if not self.is_type_constant_size(arg_type):
583
+ return False
584
+ return True
585
+
586
+ return True
587
+
588
+ def get_attr_of_ref(
589
+ self, parent: FpyReference, node: AstGetAttr, state: CompileState
590
+ ) -> FpyReference | None:
591
+ """resolve a GetAttr node relative to a given FpyReference. return the
592
+ resolved ref, or None if none could be found. Will raise errors if not found"""
593
+
594
+ if isinstance(parent, (FpyCallable, type)):
595
+ # right now we don't support resolving something after a callable/type
596
+ state.err("Invalid syntax", node)
597
+ return None
598
+
599
+ if isinstance(parent, dict):
600
+ # parent is a scope
601
+ attr = parent.get(node.attr, None)
602
+ if attr is None:
603
+ state.err("Unknown attribute", node)
604
+ return None
605
+ return attr
606
+
607
+ # parent is a ch, prm, const, var or field
608
+
609
+ value_type = get_ref_fpp_type_class(parent)
610
+
611
+ assert value_type != NothingType
612
+
613
+ if not issubclass(value_type, (SerializableType, TimeType)):
614
+ # trying to do arr.x, but arr is not a struct
615
+ state.err(
616
+ "Invalid syntax (tried to access named member of a non-struct type)",
617
+ node,
618
+ )
619
+ return None
620
+
621
+ if not self.is_type_constant_size(value_type):
622
+ state.err(
623
+ f"{value_type} has non-constant sized members, cannot access members",
624
+ node,
625
+ )
626
+ return None
627
+
628
+ member_list: list[tuple[str, FppTypeClass]] = None
629
+ if issubclass(value_type, SerializableType):
630
+ member_list = [t[0:2] for t in value_type.MEMBER_LIST]
631
+ else:
632
+ # if it is a time type, there are some "implied" members
633
+ member_list = []
634
+ member_list.append(("time_base", U16Type))
635
+ member_list.append(("time_context", U8Type))
636
+ member_list.append(("seconds", U32Type))
637
+ member_list.append(("useconds", U32Type))
638
+
639
+ offset = 0
640
+ for arg_name, arg_type in member_list:
641
+ if arg_name == node.attr:
642
+ return FieldReference(parent, arg_type, offset, name=arg_name)
643
+ offset += arg_type.getMaxSize()
644
+
645
+ state.err(f"Unknown member {node.attr}", node)
646
+ return None
647
+
648
+ def get_item_of_ref(
649
+ self, parent: FpyReference, node: AstGetItem, state: CompileState
650
+ ) -> FpyReference | None:
651
+ """resolve a GetItem node relative to a given FpyReference. return the
652
+ resolved ref, or None if none could be found. Will raise errors if not found"""
653
+
654
+ if isinstance(parent, (FpyCallable, type, dict)):
655
+ # right now we don't support resolving index after a callable/type/scope
656
+ state.err("Invalid syntax", node)
657
+ return None
658
+
659
+ # parent is a ch, prm, const, var or field
660
+
661
+ value_type = get_ref_fpp_type_class(parent)
662
+
663
+ assert value_type != NothingType
664
+
665
+ if not issubclass(value_type, ArrayType):
666
+ # trying to do struct[0], but struct is not an array
667
+ state.err(
668
+ "Invalid syntax (tried to access indexed member of a non-array type)",
669
+ node.item,
670
+ )
671
+ return None
672
+
673
+ if not self.is_type_constant_size(value_type):
674
+ state.err(
675
+ f"{value_type} has non-constant sized members, cannot access members",
676
+ node,
677
+ )
678
+ return None
679
+
680
+ offset = 0
681
+ for i in range(0, value_type.LENGTH):
682
+ if i == node.item.value:
683
+ return FieldReference(parent, value_type.MEMBER_TYPE, offset, idx=i)
684
+ offset += value_type.MEMBER_TYPE.getMaxSize()
685
+
686
+ state.err(
687
+ f"Array access out-of-bounds (access: {node.item}, array size: {value_type.LENGTH})",
688
+ node.item,
689
+ )
690
+ return None
691
+
692
+ def resolve_if_ref(
693
+ self, node: AstExpr, ns: FpyScope, state: CompileState
694
+ ) -> bool:
695
+ """if the node is a reference, try to resolve it in the given scope, and return true if success.
696
+ otherwise, if it is not a reference, return true as it doesn't need to be resolved"""
697
+ if not isinstance(node, AstReference):
698
+ return True
699
+
700
+ return self.resolve_ref_in_ns(node, ns, state) is not None
701
+
702
+ def resolve_ref_in_ns(
703
+ self, node: AstExpr, ns: FpyScope, state: CompileState
704
+ ) -> FpyReference | None:
705
+ """recursively resolves a reference in a scope, returning the resolved ref
706
+ or none if none could be found."""
707
+ if isinstance(node, AstVar):
708
+ if not isinstance(ns, dict):
709
+ return None
710
+ ref = ns.get(node.var, None)
711
+ if ref is None:
712
+ return None
713
+ state.resolved_references[node] = ref
714
+ return ref
715
+
716
+ parent = self.resolve_ref_in_ns(node.parent, ns, state)
717
+ if parent is None:
718
+ # couldn't resolve parent
719
+ return None
720
+
721
+ if isinstance(node, AstGetItem):
722
+ ref = self.get_item_of_ref(parent, node, state)
723
+ state.resolved_references[node] = ref
724
+ return ref
725
+
726
+ assert isinstance(node, AstGetAttr)
727
+ ref = self.get_attr_of_ref(parent, node, state)
728
+ state.resolved_references[node] = ref
729
+ return ref
730
+
731
+ def visit_AstFuncCall(self, node: AstFuncCall, state: CompileState):
732
+ # function refs must be callables
733
+ if not self.resolve_ref_in_ns(node.func, state.callables, state):
734
+ state.err("Unknown callable", node.func)
735
+ return
736
+
737
+ for arg in node.args if node.args is not None else []:
738
+ # arg value refs must be consts
739
+ if not self.resolve_if_ref(arg, state.consts, state):
740
+ state.err("Unknown const", arg)
741
+ return
742
+
743
+ def visit_AstIf_AstElif(self, node: AstIf | AstElif, state: CompileState):
744
+ # if condition expr refs must be "runtime values" (tlm/prm/const/etc)
745
+ if not self.resolve_if_ref(node.condition, state.runtime_values, state):
746
+ state.err("Unknown runtime value", node.condition)
747
+ return
748
+
749
+ def visit_AstComparison(self, node: AstComparison, state: CompileState):
750
+ # lhs/rhs side of comparison, if they are refs, must be refs to "runtime vals"
751
+ if not self.resolve_if_ref(node.lhs, state.runtime_values, state):
752
+ state.err("Unknown runtime value", node.lhs)
753
+ return
754
+ if not self.resolve_if_ref(node.rhs, state.runtime_values, state):
755
+ state.err("Unknown runtime value", node.rhs)
756
+ return
757
+
758
+ def visit_AstAnd_AstOr(self, node: AstAnd | AstOr, state: CompileState):
759
+ for val in node.values:
760
+ if not self.resolve_if_ref(val, state.runtime_values, state):
761
+ state.err("Unknown runtime value", val)
762
+ return
763
+
764
+ def visit_AstNot(self, node: AstNot, state: CompileState):
765
+ if not self.resolve_if_ref(node.value, state.runtime_values, state):
766
+ state.err("Unknown runtime value", node.value)
767
+ return
768
+
769
+ def visit_AstAssign(self, node: AstAssign, state: CompileState):
770
+ var = self.resolve_ref_in_ns(node.variable, state.variables, state)
771
+ if not var:
772
+ state.err("Unknown variable", node.variable)
773
+ return
774
+
775
+ if node.var_type is not None:
776
+ type = self.resolve_ref_in_ns(node.var_type, state.types, state)
777
+ if not type:
778
+ state.err("Unknown type", node.var_type)
779
+ return
780
+ var.type = type
781
+
782
+ if not self.resolve_if_ref(node.value, state.consts, state):
783
+ state.err("Unknown const", node.value)
784
+ return
785
+
786
+
787
+ class CalculateExprTypes(Visitor):
788
+ """stores in state the fprime type of each expression, or NothingType if the expr had no type"""
789
+
790
+ def visit_AstNumber(self, node: AstNumber, state: CompileState):
791
+ if isinstance(node.value, float):
792
+ result_type = FloatType
793
+ elif isinstance(node.value, int):
794
+ result_type = IntegerType
795
+ else:
796
+ assert False, node.value
797
+ state.expr_types[node] = result_type
798
+
799
+ def visit_AstString(self, node: AstString, state: CompileState):
800
+ state.expr_types[node] = StringType
801
+
802
+ def visit_AstBoolean(self, node: AstBoolean, state: CompileState):
803
+ state.expr_types[node] = BoolType
804
+
805
+ def visit_AstReference(self, node: AstReference, state: CompileState):
806
+ ref = state.resolved_references[node]
807
+ state.expr_types[node] = get_ref_fpp_type_class(ref)
808
+
809
+ def visit_AstFuncCall(self, node: AstFuncCall, state: CompileState):
810
+ ref = state.resolved_references[node.func]
811
+ assert isinstance(ref, FpyCallable)
812
+ state.expr_types[node] = ref.return_type
813
+
814
+ def visit_AstOr_AstAnd_AstNot_AstComparison(
815
+ self, node: AstOr | AstAnd | AstNot | AstComparison, state: CompileState
816
+ ):
817
+ state.expr_types[node] = BoolType
818
+
819
+ def visit_default(self, node, state):
820
+ # coding error, missed an expr
821
+ assert not isinstance(node, AstExpr), node
822
+
823
+
824
+ class CheckAndResolveArgumentTypes(Visitor):
825
+ """for each syntactic node with arguments (ands/ors/nots/cmps/funcs), check that the argument
826
+ types are right"""
827
+
828
+ def is_literal_convertible_to(
829
+ self, node: AstLiteral, to_type: FppTypeClass, state: CompileState
830
+ ) -> bool:
831
+
832
+ if isinstance(node, AstBoolean):
833
+ return to_type == BoolType
834
+
835
+ if isinstance(node, AstString):
836
+ return issubclass(to_type, StringType)
837
+
838
+ if isinstance(node, AstNumber):
839
+ if isinstance(node.value, float):
840
+ return issubclass(to_type, FloatType)
841
+ if isinstance(node.value, int):
842
+ # int literal can be converted into float or int
843
+ return issubclass(to_type, (FloatType, IntegerType))
844
+
845
+ assert False, node.value
846
+
847
+ assert False, node
848
+
849
+ def visit_AstComparison(self, node: AstComparison, state: CompileState):
850
+
851
+ lhs_type = state.expr_types[node.lhs]
852
+ rhs_type = state.expr_types[node.rhs]
853
+
854
+ if not issubclass(lhs_type, NumericalType):
855
+ state.err(f"Cannot compare non-numeric type {lhs_type}", node.lhs)
856
+ return
857
+ if not issubclass(rhs_type, NumericalType):
858
+ state.err(f"Cannot compare non-numeric type {rhs_type}", node.rhs)
859
+ return
860
+
861
+ # args are both numeric
862
+
863
+ # if either is generic float, pick F64. we want F64 cuz otherwise we need
864
+ # an FPEXT to convert to F64
865
+ if lhs_type == FloatType:
866
+ state.expr_types[node.lhs] = F64Type
867
+ if rhs_type == FloatType:
868
+ state.expr_types[node.rhs] = F64Type
869
+
870
+ if lhs_type == IntegerType and rhs_type == IntegerType:
871
+ # use i64
872
+ state.expr_types[node.lhs] = I64Type
873
+ state.expr_types[node.rhs] = I64Type
874
+ return
875
+
876
+ if lhs_type == IntegerType:
877
+ # try to interpret it as the rhs_type if rhs_type is integer
878
+ if issubclass(rhs_type, IntegerType):
879
+ state.expr_types[node.lhs] = state.expr_types[node.rhs]
880
+ else:
881
+ # otherwise rhs is a float. just use i64
882
+ state.expr_types[node.lhs] = I64Type
883
+
884
+ if rhs_type == IntegerType:
885
+ # try to interpret it as the rhs_type if rhs_type is integer
886
+ if issubclass(lhs_type, IntegerType):
887
+ state.expr_types[node.rhs] = state.expr_types[node.lhs]
888
+ else:
889
+ # otherwise lhs is a float. just use i64
890
+ state.expr_types[node.rhs] = I64Type
891
+
892
+ def visit_AstFuncCall(self, node: AstFuncCall, state: CompileState):
893
+ func = state.resolved_references[node.func]
894
+ func_args = func.args
895
+ node_args = node.args if node.args else []
896
+
897
+ if len(node_args) < len(func_args):
898
+ state.errors.append(
899
+ CompileException(
900
+ f"Missing arguments (expected {len(func_args)} found {len(node_args)})",
901
+ node,
902
+ )
903
+ )
904
+ return
905
+ if len(node_args) > len(func_args):
906
+ state.errors.append(
907
+ CompileException(
908
+ f"Too many arguments (expected {len(func_args)} found {len(node_args)})",
909
+ node,
910
+ )
911
+ )
912
+ return
913
+
914
+ for value_expr, arg in zip(node_args, func_args):
915
+ arg_name, arg_type = arg
916
+
917
+ value_expr_type = state.expr_types[value_expr]
918
+
919
+ if value_expr_type == arg_type or (
920
+ isinstance(value_expr, AstLiteral)
921
+ and self.is_literal_convertible_to(value_expr, arg_type, state)
922
+ ):
923
+ # arg type is good!
924
+ state.expr_types[value_expr] = arg_type
925
+ continue
926
+
927
+ # it is not. these are not compatible
928
+ state.errors.append(
929
+ CompileException(
930
+ f"Cannot convert {value_expr} ({value_expr_type}) to {arg_type}",
931
+ value_expr,
932
+ )
933
+ )
934
+ return
935
+
936
+ # got thru all args successfully
937
+
938
+ def visit_AstOr_AstAnd(self, node: AstOr | AstAnd, state: CompileState):
939
+ # "or/and" can have as many args as you want. they all need to be bools tho
940
+ for val in node.values:
941
+ val_type = state.expr_types[val]
942
+ if val_type != BoolType:
943
+ state.err(f"Arguments to 'and'/'or' must be booleans", val)
944
+ return
945
+ state.expr_types[val] = BoolType
946
+
947
+ def visit_AstNot(self, node: AstNot, state: CompileState):
948
+ val_type = state.expr_types[node.value]
949
+ if val_type != BoolType:
950
+ state.err(f"Argument to 'not' must be boolean", node.value)
951
+ return
952
+ state.expr_types[node.value] = BoolType
953
+
954
+ def visit_AstAssign(self, node: AstAssign, state: CompileState):
955
+ var_type = state.resolved_references[node.variable].type
956
+ value_type = state.expr_types[node.value]
957
+ if var_type != value_type:
958
+ if not (
959
+ isinstance(node.value, AstLiteral)
960
+ and self.is_literal_convertible_to(node.value, var_type, state)
961
+ ):
962
+ state.err(f"Cannot interpret {node.value} as {var_type}", node.value)
963
+ return
964
+
965
+ state.expr_types[node.value] = var_type
966
+
967
+ if var_type.getMaxSize() > MAX_SERIALIZABLE_REGISTER_SIZE:
968
+ state.err(f"{var_type} is too big to fit in a variable", node)
969
+ return
970
+
971
+ def visit_AstGetItem(self, node: AstGetItem, state: CompileState):
972
+ # the node of the index number has no expression value, it's an arg
973
+ # but only at syntax level
974
+ state.expr_types[node.item] = NothingType
975
+
976
+
977
+ class CalculateExprValues(Visitor):
978
+ """for each expr, try to calculate its constant value and store it in a map. stores None if no value could be
979
+ calculated at compile time, and NothingType if the expr had no value"""
980
+
981
+ def visit_AstLiteral(self, node: AstLiteral, state: CompileState):
982
+ literal_type = state.expr_types[node]
983
+ if literal_type != NothingType:
984
+ assert (
985
+ literal_type in NUMERIC_TYPES
986
+ or issubclass(literal_type, StringType)
987
+ or literal_type == BoolType
988
+ ), literal_type
989
+ state.expr_values[node] = literal_type(node.value)
990
+ else:
991
+ state.expr_values[node] = literal_type()
992
+
993
+ def visit_AstReference(self, node: AstReference, state: CompileState):
994
+ ref = state.resolved_references[node]
995
+
996
+ if isinstance(ref, (ChTemplate, PrmTemplate, FpyVariable)):
997
+ # we do not try to calculate or predict these values at compile time
998
+ expr_value = None
999
+ elif isinstance(ref, FieldReference):
1000
+ if isinstance(ref.parent, FppType):
1001
+ # ref to a field of a constant
1002
+ # get the field
1003
+ expr_value = ref.get_from(ref.parent)
1004
+ else:
1005
+ # ref to a field of smth else. no runtime val
1006
+ expr_value = None
1007
+ elif isinstance(ref, FppType):
1008
+ # constant value
1009
+ expr_value = ref
1010
+ elif isinstance(ref, FpyCallable):
1011
+ # a reference to a callable doesn't have a value, you have to actually
1012
+ # call the func
1013
+ expr_value = NothingType()
1014
+ elif isinstance(ref, type):
1015
+ # a reference to a type doesn't have a value, and so doesn't have a type,
1016
+ # in and of itself. if this were a function call to the type's ctor then
1017
+ # it would have a value
1018
+ expr_value = NothingType()
1019
+ elif isinstance(ref, dict):
1020
+ # a ref to a scope doesn't have a value
1021
+ expr_value = NothingType()
1022
+ else:
1023
+ assert False, ref
1024
+
1025
+ assert expr_value is None or isinstance(expr_value, state.expr_types[node]), (
1026
+ expr_value,
1027
+ state.expr_types[node],
1028
+ )
1029
+ state.expr_values[node] = expr_value
1030
+
1031
+ def visit_AstFuncCall(self, node: AstFuncCall, state: CompileState):
1032
+ func = state.resolved_references[node.func]
1033
+ assert isinstance(func, FpyCallable)
1034
+ # gather arg values
1035
+ arg_values = [
1036
+ state.expr_values[e] for e in (node.args if node.args is not None else [])
1037
+ ]
1038
+ unknown_value = any(v for v in arg_values if v is None)
1039
+ if unknown_value:
1040
+ state.expr_values[node] = None
1041
+ return
1042
+
1043
+ if isinstance(func, FpyTypeCtor):
1044
+ # actually construct the type
1045
+ if issubclass(func.type, SerializableType):
1046
+ instance = func.type()
1047
+ # pass in args as a dict
1048
+ # t[0] is the arg name
1049
+ arg_dict = {t[0]: v for t, v in zip(func.type.MEMBER_LIST, arg_values)}
1050
+ instance._val = arg_dict
1051
+ state.expr_values[node] = instance
1052
+
1053
+ elif issubclass(func.type, ArrayType):
1054
+ instance = func.type()
1055
+ instance._val = arg_values
1056
+ state.expr_values[node] = instance
1057
+
1058
+ elif func.type == TimeType:
1059
+ state.expr_values[node] = TimeType(*arg_values)
1060
+
1061
+ else:
1062
+ # no other FppTypeClasses have ctors
1063
+ assert False, func.return_type
1064
+ else:
1065
+ # don't try to calculate the value of this function call
1066
+ # it's something like a cmd or macro
1067
+ state.expr_values[node] = None
1068
+
1069
+ def visit_AstTest(self, node: AstTest, state: CompileState):
1070
+ # we do not calculate compile time value of or/and/nots/cmps at the moment
1071
+ state.expr_values[node] = None
1072
+
1073
+ def visit_default(self, node, state):
1074
+ # coding error, missed an expr
1075
+ assert not isinstance(node, AstExpr), node
1076
+
1077
+
1078
+ class GenerateVariableDirectives(Visitor):
1079
+ """for each variable assignment or declaration, check the rhs was known
1080
+ at compile time, and generate a directive"""
1081
+
1082
+ def visit_AstAssign(self, node: AstAssign, state: CompileState):
1083
+ existing_var = state.resolved_references[node.variable]
1084
+ # should already have been put in var table
1085
+ assert existing_var is not None
1086
+
1087
+ # we should have type info about the variable
1088
+ assert existing_var.type is not None
1089
+
1090
+ value_type = state.expr_types[node.value]
1091
+ value = state.expr_values[node.value]
1092
+
1093
+ # already type checked
1094
+ assert value_type == type(value), (value_type, type(value))
1095
+
1096
+ if value is None:
1097
+ # expr value is unknown at this point in compile
1098
+ state.err(
1099
+ f"Cannot assign {node.variable.var}: {existing_var.type} to {node.value}, as its value was not known at compile time",
1100
+ node.value,
1101
+ )
1102
+ return
1103
+
1104
+ sreg_idx = existing_var.sreg_idx
1105
+ if sreg_idx is None:
1106
+ # doesn't have an sreg idx, allocate one
1107
+ sreg_idx = state.next_sreg
1108
+ state.next_sreg += 1
1109
+ existing_var.sreg_idx = sreg_idx
1110
+ val_bytes = value.serialize()
1111
+ assert len(val_bytes) == value.getMaxSize(), (
1112
+ len(val_bytes),
1113
+ value.getMaxSize(),
1114
+ value,
1115
+ )
1116
+ assert len(val_bytes) <= MAX_SERIALIZABLE_REGISTER_SIZE, len(val_bytes)
1117
+ state.directives[node] = [SetSerRegDirective(sreg_idx, val_bytes)]
1118
+
1119
+
1120
+ class GenerateConstCmdDirectives(Visitor):
1121
+ """for each command or macro whose arguments were const at runtime (should be all at the moment),
1122
+ generate a directive"""
1123
+ def visit_AstFuncCall(self, node: AstFuncCall, state: CompileState):
1124
+ func = state.resolved_references[node.func]
1125
+ if isinstance(func, FpyCmd):
1126
+ arg_bytes = bytes()
1127
+ for arg_node in node.args if node.args is not None else []:
1128
+ arg_value = state.expr_values[arg_node]
1129
+ if arg_value is None:
1130
+ state.err(
1131
+ f"Only constant arguments to commands are allowed", arg_node
1132
+ )
1133
+ return
1134
+ arg_bytes += arg_value.serialize()
1135
+ state.directives[node] = [CmdDirective(func.cmd.get_op_code(), arg_bytes)]
1136
+ elif isinstance(func, FpyMacro):
1137
+ arg_values = []
1138
+ for arg_node in node.args if node.args is not None else []:
1139
+ arg_value = state.expr_values[arg_node]
1140
+ if arg_value is None:
1141
+ state.err(
1142
+ f"Only constant arguments to macros are allowed", arg_node
1143
+ )
1144
+ return
1145
+ arg_values.append(arg_value)
1146
+
1147
+ state.directives[node] = func.instantiate_macro(arg_values)
1148
+ else:
1149
+ state.directives[node] = None
1150
+
1151
+
1152
+ def put_sreg_in_nreg(
1153
+ sreg_idx: int, sreg_offset: int, nreg_idx: int, size: int
1154
+ ) -> list[Directive]:
1155
+ if size > 4:
1156
+ return [DeserSerReg8Directive(sreg_idx, sreg_offset, nreg_idx)]
1157
+ elif size > 2:
1158
+ return [DeserSerReg4Directive(sreg_idx, sreg_offset, nreg_idx)]
1159
+ elif size > 1:
1160
+ return [DeserSerReg2Directive(sreg_idx, sreg_offset, nreg_idx)]
1161
+ elif size == 1:
1162
+ return [DeserSerReg1Directive(sreg_idx, sreg_offset, nreg_idx)]
1163
+ else:
1164
+ assert False, size
1165
+
1166
+
1167
+ class AssignExprRegisters(Visitor):
1168
+ """assign each expr a unique register"""
1169
+ def visit_AstExpr(self, node: AstExpr, state: CompileState):
1170
+ state.expr_registers[node] = state.next_register
1171
+ state.next_register += 1
1172
+
1173
+
1174
+ class GenerateConstExprDirectives(Visitor):
1175
+ """for each expr with a constant compile time value, generate
1176
+ directives for how to put it in its register"""
1177
+ def visit_AstExpr(self, node: AstExpr, state: CompileState):
1178
+ expr_type = state.expr_types[node]
1179
+
1180
+ if node in state.directives:
1181
+ # already have directives associated with this node
1182
+ return
1183
+
1184
+ if expr_type == NothingType:
1185
+ # impossible. nothing type has no value
1186
+ state.directives[node] = None
1187
+ return
1188
+
1189
+ if expr_type.getMaxSize() > 8:
1190
+ # bigger than 8 bytes
1191
+ # impossible. can't fit in a register
1192
+ state.directives[node] = None
1193
+ return
1194
+
1195
+ # okay, it is not nothing and it is smaller than 8 bytes.
1196
+ # should be able to put it in a reg
1197
+
1198
+ register = state.expr_registers[node]
1199
+
1200
+ expr_value = state.expr_values[node]
1201
+
1202
+ if expr_value is None:
1203
+ # no const value
1204
+ return
1205
+
1206
+ # it has a constant value at compile time
1207
+ serialized_expr_value = expr_value.serialize()
1208
+ assert len(serialized_expr_value) <= 8, len(serialized_expr_value)
1209
+ val_as_i64_bytes = bytes(8 - len(serialized_expr_value))
1210
+ val_as_i64_bytes += serialized_expr_value
1211
+
1212
+ # reinterpret as an I64
1213
+ val_as_i64 = I64Type()
1214
+ val_as_i64.deserialize(val_as_i64_bytes, 0)
1215
+
1216
+ state.directives[node] = [SetRegDirective(register, val_as_i64.val)]
1217
+
1218
+
1219
+ class GenerateNonConstExprDirectives(Visitor):
1220
+ """for each expr whose value is not known at compile time, but can be calculated at run time,
1221
+ generate directives to calculate the value and put it in its register"""
1222
+
1223
+ def visit_AstReference(self, node: AstReference, state: CompileState):
1224
+ if node in state.directives:
1225
+ # already know how to put it in reg, or it is impossible
1226
+ return
1227
+
1228
+ expr_type = state.expr_types[node]
1229
+ ref = state.resolved_references[node]
1230
+
1231
+ directives = []
1232
+
1233
+ # does not have a constant compile time value
1234
+
1235
+ # all references that don't have a compile time value have to go into an sreg first
1236
+ # and then into an nreg
1237
+
1238
+ sreg_idx = None
1239
+
1240
+ offset = 0
1241
+
1242
+ base_ref = ref
1243
+
1244
+ # if it's a field ref, find the parent and the offset in the parent
1245
+ while isinstance(base_ref, FieldReference):
1246
+ offset += base_ref.offset
1247
+ base_ref = base_ref.parent
1248
+
1249
+ if isinstance(base_ref, FpyVariable):
1250
+ # already in an sreg
1251
+ sreg_idx = base_ref.sreg_idx
1252
+ else:
1253
+ sreg_idx = state.next_sreg
1254
+ state.next_sreg += 1
1255
+
1256
+ if isinstance(base_ref, ChTemplate):
1257
+ tlm_time_sreg_idx = state.next_sreg
1258
+ state.next_sreg += 1
1259
+ directives.append(
1260
+ GetTlmDirective(sreg_idx, tlm_time_sreg_idx, base_ref.get_id())
1261
+ )
1262
+
1263
+ elif isinstance(base_ref, PrmTemplate):
1264
+ directives.append(GetPrmDirective(sreg_idx, base_ref.get_id()))
1265
+
1266
+ else:
1267
+ assert (
1268
+ False
1269
+ ), base_ref # ref should either be impossible to put in a reg or should have a compile time val
1270
+
1271
+ # pull from sreg into nreg
1272
+ directives.extend(
1273
+ put_sreg_in_nreg(
1274
+ sreg_idx, offset, state.expr_registers[node], expr_type.getMaxSize()
1275
+ )
1276
+ )
1277
+
1278
+ state.directives[node] = directives
1279
+
1280
+ def visit_AstAnd_AstOr(self, node: AstAnd | AstOr, state: CompileState):
1281
+ if node in state.directives:
1282
+ # already know how to put it in reg, or know that it's impossible
1283
+ return
1284
+
1285
+ expr_reg = state.expr_registers[node]
1286
+ directives = []
1287
+
1288
+ registers_to_compare = []
1289
+ for arg_value_expr in node.values:
1290
+ arg_value_dirs = state.directives[arg_value_expr]
1291
+ assert arg_value_dirs is not None
1292
+ directives.extend(arg_value_dirs)
1293
+ registers_to_compare.append(state.expr_registers[arg_value_expr])
1294
+
1295
+ assert len(registers_to_compare) >= 2, len(registers_to_compare)
1296
+
1297
+ # okay, now we have to "or" or "and" together all of the registers
1298
+ # "or/and" the first two together, put in res.
1299
+ # from then on, "or/and" the next with res
1300
+
1301
+ dir_type = OrDirective if isinstance(node, AstOr) else AndDirective
1302
+
1303
+ directives.append(
1304
+ dir_type(registers_to_compare[0], registers_to_compare[1], expr_reg)
1305
+ )
1306
+
1307
+ for i in range(2, len(registers_to_compare)):
1308
+ directives.append(dir_type(expr_reg, registers_to_compare[i], expr_reg))
1309
+
1310
+ state.directives[node] = directives
1311
+
1312
+ def visit_AstNot(self, node: AstNot, state: CompileState):
1313
+ if node in state.directives:
1314
+ # already know how to put it in reg
1315
+ return
1316
+
1317
+ expr_reg = state.expr_registers[node]
1318
+ directives = []
1319
+ arg_value_dirs = state.directives[node.value]
1320
+ assert arg_value_dirs is not None
1321
+ directives.extend(arg_value_dirs)
1322
+ directives.append(NotDirective(state.expr_registers[node.value], expr_reg))
1323
+
1324
+ state.directives[node] = directives
1325
+
1326
+ def visit_AstComparison(self, node: AstComparison, state: CompileState):
1327
+ if node in state.directives:
1328
+ # already know how to put it in reg
1329
+ return
1330
+
1331
+ directives = []
1332
+
1333
+ lhs_type = state.expr_types[node.lhs]
1334
+ rhs_type = state.expr_types[node.rhs]
1335
+
1336
+ lhs_reg = state.expr_registers[node.lhs]
1337
+ rhs_reg = state.expr_registers[node.rhs]
1338
+ res_reg = state.expr_registers[node]
1339
+
1340
+ directives.extend(state.directives[node.lhs])
1341
+ directives.extend(state.directives[node.rhs])
1342
+
1343
+ fp = False
1344
+ if issubclass(lhs_type, FloatType) or issubclass(rhs_type, FloatType):
1345
+ fp = True
1346
+
1347
+ if fp:
1348
+ # need to convert both lhs and rhs into F64
1349
+ # modify them in place
1350
+
1351
+ # convert int to float
1352
+ if issubclass(lhs_type, IntegerType):
1353
+ if lhs_type in UNSIGNED_INTEGER_TYPES:
1354
+ directives.append(UnsignedIntToFloatDirective(lhs_reg, lhs_reg))
1355
+ else:
1356
+ directives.append(SignedIntToFloatDirective(lhs_reg, lhs_reg))
1357
+ if issubclass(rhs_type, IntegerType):
1358
+ if rhs_type in UNSIGNED_INTEGER_TYPES:
1359
+ directives.append(UnsignedIntToFloatDirective(rhs_reg, rhs_reg))
1360
+ else:
1361
+ directives.append(SignedIntToFloatDirective(rhs_reg, rhs_reg))
1362
+
1363
+ # convert F32 to F64
1364
+ if lhs_type == F32Type:
1365
+ directives.append(FloatExtendDirective(lhs_reg, lhs_reg))
1366
+ if rhs_type == F32Type:
1367
+ directives.append(FloatExtendDirective(rhs_reg, rhs_reg))
1368
+
1369
+ if node.op.value == "==":
1370
+ if fp:
1371
+ directives.append(FloatEqualDirective(lhs_reg, rhs_reg, res_reg))
1372
+ else:
1373
+ directives.append(IntEqualDirective(lhs_reg, rhs_reg, res_reg))
1374
+ elif node.op.value == "!=":
1375
+ if fp:
1376
+ directives.append(FloatNotEqualDirective(lhs_reg, rhs_reg, res_reg))
1377
+ else:
1378
+ directives.append(IntNotEqualDirective(lhs_reg, rhs_reg, res_reg))
1379
+ else:
1380
+
1381
+ if fp:
1382
+ dir_type = FLOAT_INEQUALITY_DIRECTIVES[node.op.value]
1383
+ else:
1384
+ # if either is signed, consider both as signed
1385
+ signed = (
1386
+ lhs_type in SIGNED_INTEGER_TYPES or rhs_type in SIGNED_INTEGER_TYPES
1387
+ )
1388
+
1389
+ if signed:
1390
+ dir_type = INT_SIGNED_INEQUALITY_DIRECTIVES[node.op.value]
1391
+ else:
1392
+ dir_type = INT_UNSIGNED_INEQUALITY_DIRECTIVES[node.op.value]
1393
+
1394
+ directives.append(dir_type(lhs_reg, rhs_reg, res_reg))
1395
+
1396
+ state.directives[node] = directives
1397
+
1398
+
1399
+ class CountNodeDirectives(Visitor):
1400
+ """count the number of directives that will be generated by each node"""
1401
+
1402
+ def visit_AstIf(self, node: AstIf, state: CompileState):
1403
+ count = 0
1404
+ # include the condition
1405
+ count += state.node_dir_counts[node.condition]
1406
+ # include if stmt
1407
+ count += 1
1408
+ # include body
1409
+ count += state.node_dir_counts[node.body]
1410
+ # include a goto end of if
1411
+ count += 1
1412
+
1413
+ if node.elifs is not None:
1414
+ count += state.node_dir_counts[node.elifs]
1415
+ if node.els is not None:
1416
+ count += state.node_dir_counts[node.els]
1417
+
1418
+ state.node_dir_counts[node] = count
1419
+
1420
+ def visit_AstElifs(self, node: AstElifs, state: CompileState):
1421
+ count = 0
1422
+ for case in node.cases:
1423
+ count += state.node_dir_counts[case]
1424
+
1425
+ state.node_dir_counts[node] = count
1426
+
1427
+ def visit_AstElif(self, node: AstElif, state: CompileState):
1428
+ count = 0
1429
+ # include the condition
1430
+ count += state.node_dir_counts[node.condition]
1431
+ # include if stmt
1432
+ count += 1
1433
+ # include body
1434
+ count += state.node_dir_counts[node.body]
1435
+ # include a goto end of if
1436
+ count += 1
1437
+
1438
+ state.node_dir_counts[node] = count
1439
+
1440
+ def visit_AstBody(self, node: AstBody, state: CompileState):
1441
+ count = 0
1442
+ for stmt in node.stmts:
1443
+ count += state.node_dir_counts[stmt]
1444
+
1445
+ state.node_dir_counts[node] = count
1446
+
1447
+ def visit_default(self, node, state):
1448
+ state.node_dir_counts[node] = (
1449
+ len(state.directives[node]) if state.directives.get(node) is not None else 0
1450
+ )
1451
+
1452
+
1453
+ class CalculateStartLineIdx(TopDownVisitor):
1454
+ """based on the number of directives generated by each node, calculate the start line idx
1455
+ of each node's directives"""
1456
+ def visit_AstBody(self, node: AstBody, state: CompileState):
1457
+ if node not in state.start_line_idx:
1458
+ state.start_line_idx[node] = 0
1459
+
1460
+ start_idx = state.start_line_idx[node]
1461
+
1462
+ line_idx = start_idx
1463
+ for stmt in node.stmts:
1464
+ state.start_line_idx[stmt] = line_idx
1465
+ line_idx += state.node_dir_counts[stmt]
1466
+
1467
+ def visit_AstIf(self, node: AstIf, state: CompileState):
1468
+ line_idx = state.start_line_idx[node]
1469
+ state.start_line_idx[node.condition] = line_idx
1470
+ line_idx += state.node_dir_counts[node.condition]
1471
+ # include if stmt
1472
+ line_idx += 1
1473
+ state.start_line_idx[node.body] = line_idx
1474
+ line_idx += state.node_dir_counts[node.body]
1475
+ # include goto stmt
1476
+ line_idx += 1
1477
+ if node.elifs is not None:
1478
+ state.start_line_idx[node.elifs] = line_idx
1479
+ line_idx += state.node_dir_counts[node.elifs]
1480
+ if node.els is not None:
1481
+ state.start_line_idx[node.els] = line_idx
1482
+ line_idx += state.node_dir_counts[node.els]
1483
+
1484
+ def visit_AstElifs(self, node: AstElifs, state: CompileState):
1485
+ line_idx = state.start_line_idx[node]
1486
+ for case in node.cases:
1487
+ state.start_line_idx[case] = line_idx
1488
+ line_idx += state.node_dir_counts[case]
1489
+
1490
+ def visit_AstElif(self, node: AstElif, state: CompileState):
1491
+ line_idx = state.start_line_idx[node]
1492
+ state.start_line_idx[node.condition] = line_idx
1493
+ line_idx += state.node_dir_counts[node.condition]
1494
+ # include if dir
1495
+ line_idx += 1
1496
+ state.start_line_idx[node.body] = line_idx
1497
+ line_idx += state.node_dir_counts[node.body]
1498
+ # include a goto end of if
1499
+ line_idx += 1
1500
+
1501
+
1502
+ class GenerateBodyDirectives(Visitor):
1503
+ """concatenate all directives together for each AstBody"""
1504
+
1505
+ def visit_AstIf(self, node: AstIf, state: CompileState):
1506
+ start_line_idx = state.start_line_idx[node]
1507
+
1508
+ all_dirs = []
1509
+
1510
+ cases: list[tuple[AstExpr, AstBody]] = []
1511
+ goto_ends: list[GotoDirective] = []
1512
+
1513
+ cases.append((node.condition, node.body))
1514
+
1515
+ if node.elifs is not None:
1516
+ for case in node.elifs.cases:
1517
+ cases.append((case.condition, case.body))
1518
+
1519
+ for case in cases:
1520
+ case_dirs = []
1521
+ # include the condition
1522
+ case_dirs.extend(state.directives[case[0]])
1523
+ # include if stmt (update the end idx later)
1524
+ if_dir = IfDirective(state.expr_registers[case[0]], -1)
1525
+
1526
+ case_dirs.append(if_dir)
1527
+ # include body
1528
+ case_dirs.extend(state.directives[case[1]])
1529
+ # include a temporary goto end of if, will be refined later
1530
+ goto_dir = GotoDirective(-1)
1531
+ case_dirs.append(goto_dir)
1532
+ goto_ends.append(goto_dir)
1533
+
1534
+ # if false, skip the body and goto
1535
+ if_dir.false_goto_stmt_index = (
1536
+ start_line_idx + len(all_dirs) + len(case_dirs)
1537
+ )
1538
+
1539
+ all_dirs.extend(case_dirs)
1540
+
1541
+ if node.els is not None:
1542
+ all_dirs.extend(state.directives[node.els])
1543
+
1544
+ for goto in goto_ends:
1545
+ goto.statement_index = start_line_idx + len(all_dirs)
1546
+
1547
+ state.directives[node] = all_dirs
1548
+
1549
+ def visit_AstBody(self, node: AstBody, state: CompileState):
1550
+ dirs = []
1551
+ for stmt in node.stmts:
1552
+ stmt_dirs = state.directives.get(stmt, None)
1553
+ if stmt_dirs is not None:
1554
+ dirs.extend(stmt_dirs)
1555
+
1556
+ state.directives[node] = dirs
1557
+
1558
+
1559
+ def get_base_compile_state(dictionary: str) -> CompileState:
1560
+ """return the initial state of the compiler, based on the given dict path"""
1561
+ cmd_json_dict_loader = CmdJsonLoader(dictionary)
1562
+ (cmd_id_dict, cmd_name_dict, versions) = cmd_json_dict_loader.construct_dicts(
1563
+ dictionary
1564
+ )
1565
+
1566
+ ch_json_dict_loader = ChJsonLoader(dictionary)
1567
+ (ch_id_dict, ch_name_dict, versions) = ch_json_dict_loader.construct_dicts(
1568
+ dictionary
1569
+ )
1570
+ prm_json_dict_loader = PrmJsonLoader(dictionary)
1571
+ (prm_id_dict, prm_name_dict, versions) = prm_json_dict_loader.construct_dicts(
1572
+ dictionary
1573
+ )
1574
+ # the type name dict is a mapping of a fully qualified name to an fprime type
1575
+ # here we put into it all types found while parsing all cmds, params and tlm channels
1576
+ type_name_dict: dict[str, FppTypeClass] = cmd_json_dict_loader.parsed_types
1577
+ type_name_dict.update(ch_json_dict_loader.parsed_types)
1578
+ type_name_dict.update(prm_json_dict_loader.parsed_types)
1579
+
1580
+ # enum const dict is a dict of fully qualified enum const name (like Ref.Choice.ONE) to its fprime value
1581
+ enum_const_name_dict: dict[str, FppType] = {}
1582
+
1583
+ # find each enum type, and put each of its values in the enum const dict
1584
+ for name, typ in type_name_dict.items():
1585
+ if issubclass(typ, EnumType):
1586
+ for enum_const_name, val in typ.ENUM_DICT.items():
1587
+ enum_const_name_dict[name + "." + enum_const_name] = typ(
1588
+ enum_const_name
1589
+ )
1590
+
1591
+ # insert the implicit types into the dict
1592
+ type_name_dict["Fw.Time"] = TimeType
1593
+ for typ in NUMERIC_TYPES:
1594
+ type_name_dict[typ.get_canonical_name()] = typ
1595
+ type_name_dict["bool"] = BoolType
1596
+ # note no string type at the moment
1597
+
1598
+ callable_name_dict: dict[str, FpyCallable] = {}
1599
+ # add all cmds to the callable dict
1600
+ for name, cmd in cmd_name_dict.items():
1601
+ cmd: CmdTemplate
1602
+ args = []
1603
+ for arg_name, _, arg_type in cmd.arguments:
1604
+ args.append((arg_name, arg_type))
1605
+ # cmds are thought of as callables with a "NothingType" return value
1606
+ callable_name_dict[name] = FpyCmd(NothingType, args, cmd)
1607
+
1608
+ # for each type in the dict, if it has a constructor, create an FpyTypeCtor
1609
+ # object to track the constructor and put it in the callable name dict
1610
+ for name, typ in type_name_dict.items():
1611
+ args = []
1612
+ if issubclass(typ, SerializableType):
1613
+ for arg_name, arg_type, _, _ in typ.MEMBER_LIST:
1614
+ args.append((arg_name, arg_type))
1615
+ elif issubclass(typ, ArrayType):
1616
+ for i in range(0, typ.LENGTH):
1617
+ args.append(("e" + str(i), typ.MEMBER_TYPE))
1618
+ elif issubclass(typ, TimeType):
1619
+ args.append(("time_base", U16Type))
1620
+ args.append(("time_context", U8Type))
1621
+ args.append(("seconds", U32Type))
1622
+ args.append(("useconds", U32Type))
1623
+ else:
1624
+ # bool, enum, string or numeric type
1625
+ # none of these have callable ctors
1626
+ continue
1627
+
1628
+ callable_name_dict[name] = FpyTypeCtor(typ, args, typ)
1629
+
1630
+ # for each macro function, add it to the callable dict
1631
+ for macro_name, macro in MACROS.items():
1632
+ callable_name_dict[macro_name] = macro
1633
+
1634
+ state = CompileState(
1635
+ tlms=create_scope(ch_name_dict),
1636
+ prms=create_scope(prm_name_dict),
1637
+ types=create_scope(type_name_dict),
1638
+ callables=create_scope(callable_name_dict),
1639
+ consts=create_scope(enum_const_name_dict),
1640
+ )
1641
+ return state
1642
+
1643
+
1644
+ def compile(body: AstBody, dictionary: str) -> list[Directive]:
1645
+ state = get_base_compile_state(dictionary)
1646
+ passes: list[Visitor] = [
1647
+ AssignIds(),
1648
+ # based on assignment syntax nodes, we know which variables exist where
1649
+ CreateVariables(),
1650
+ # now that variables have been defined, all names/attributes/indices (references)
1651
+ # should be defined
1652
+ ResolveReferences(),
1653
+ # now that we know what all refs point to, we should be able to figure out the type
1654
+ # of every expression
1655
+ CalculateExprTypes(),
1656
+ # now that we know the type of each expr, we can type check all function calls
1657
+ # and also narrow down ambiguous argument types
1658
+ CheckAndResolveArgumentTypes(),
1659
+ # okay, now that we're sure we're passing in all the right args to each func,
1660
+ # we can calculate values of type ctors etc etc
1661
+ CalculateExprValues(),
1662
+ # now that we know variable values, we can generate directives for vars
1663
+ GenerateVariableDirectives(),
1664
+ # give each expr its own register
1665
+ AssignExprRegisters(),
1666
+ # for cmds, which have constant arguments, generate the corresponding directives
1667
+ GenerateConstCmdDirectives(),
1668
+ # for expressions which have constant values, generate corresponding directives
1669
+ # to put the expr in its register
1670
+ GenerateConstExprDirectives(),
1671
+ # for expressions which don't have constant values, generate directives to
1672
+ # calculate the expr at runtime and put it in its register
1673
+ GenerateNonConstExprDirectives(),
1674
+ # count the number of directives generated by each node
1675
+ CountNodeDirectives(),
1676
+ # calculate the index that the node will correspond to in the output file
1677
+ CalculateStartLineIdx(),
1678
+ # generate directives for each body node, including the root
1679
+ GenerateBodyDirectives(),
1680
+ ]
1681
+
1682
+ for compile_pass in passes:
1683
+ compile_pass.run(body, state)
1684
+ for error in state.errors:
1685
+ raise error
1686
+
1687
+ return state.directives[body]