fprime-gds 4.0.2a3__py3-none-any.whl → 4.0.2a4__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.
Files changed (28) hide show
  1. fprime_gds/common/distributor/distributor.py +9 -6
  2. fprime_gds/common/fpy/README.md +190 -42
  3. fprime_gds/common/fpy/SPEC.md +153 -39
  4. fprime_gds/common/fpy/bytecode/assembler.py +62 -0
  5. fprime_gds/common/fpy/bytecode/directives.py +620 -294
  6. fprime_gds/common/fpy/codegen.py +687 -910
  7. fprime_gds/common/fpy/grammar.lark +44 -9
  8. fprime_gds/common/fpy/main.py +27 -4
  9. fprime_gds/common/fpy/model.py +799 -0
  10. fprime_gds/common/fpy/parser.py +29 -34
  11. fprime_gds/common/fpy/test_helpers.py +119 -0
  12. fprime_gds/common/fpy/types.py +563 -0
  13. fprime_gds/executables/cli.py +9 -0
  14. fprime_gds/executables/run_deployment.py +2 -0
  15. fprime_gds/flask/app.py +17 -2
  16. fprime_gds/flask/default_settings.py +1 -0
  17. fprime_gds/flask/static/index.html +7 -4
  18. fprime_gds/flask/static/js/config.js +9 -1
  19. fprime_gds/flask/static/js/vue-support/channel.js +16 -0
  20. fprime_gds/flask/static/js/vue-support/event.js +1 -0
  21. fprime_gds/flask/static/js/vue-support/fp-row.js +25 -1
  22. {fprime_gds-4.0.2a3.dist-info → fprime_gds-4.0.2a4.dist-info}/METADATA +1 -1
  23. {fprime_gds-4.0.2a3.dist-info → fprime_gds-4.0.2a4.dist-info}/RECORD +28 -24
  24. {fprime_gds-4.0.2a3.dist-info → fprime_gds-4.0.2a4.dist-info}/entry_points.txt +2 -1
  25. {fprime_gds-4.0.2a3.dist-info → fprime_gds-4.0.2a4.dist-info}/WHEEL +0 -0
  26. {fprime_gds-4.0.2a3.dist-info → fprime_gds-4.0.2a4.dist-info}/licenses/LICENSE.txt +0 -0
  27. {fprime_gds-4.0.2a3.dist-info → fprime_gds-4.0.2a4.dist-info}/licenses/NOTICE.txt +0 -0
  28. {fprime_gds-4.0.2a3.dist-info → fprime_gds-4.0.2a4.dist-info}/top_level.txt +0 -0
@@ -1,44 +1,98 @@
1
1
  from __future__ import annotations
2
2
  from abc import ABC
3
3
  import inspect
4
- from dataclasses import dataclass, field, fields
4
+ from dataclasses import astuple, dataclass, field, fields
5
+ from pathlib import Path
6
+ import struct
5
7
  import traceback
6
- from typing import Callable
7
8
  import typing
9
+ from typing import Union, get_origin, get_args
10
+ import zlib
11
+
12
+ from fprime_gds.common.fpy.types import (
13
+ SPECIFIC_FLOAT_TYPES,
14
+ SPECIFIC_INTEGER_TYPES,
15
+ MACROS,
16
+ MAX_DIRECTIVE_SIZE,
17
+ MAX_DIRECTIVES_COUNT,
18
+ SPECIFIC_NUMERIC_TYPES,
19
+ SIGNED_INTEGER_TYPES,
20
+ UNSIGNED_INTEGER_TYPES,
21
+ CompileException,
22
+ CompileState,
23
+ FieldReference,
24
+ FppTypeClass,
25
+ FpyCallable,
26
+ FpyCmd,
27
+ FpyMacro,
28
+ FpyReference,
29
+ FpyScope,
30
+ FpyTypeCtor,
31
+ FpyVariable,
32
+ InternalIntType,
33
+ InternalStringType,
34
+ NothingType,
35
+ TopDownVisitor,
36
+ Visitor,
37
+ create_scope,
38
+ get_ref_fpp_type_class,
39
+ is_instance_compat,
40
+ )
41
+
42
+ # In Python 3.10+, the `|` operator creates a `types.UnionType`.
43
+ # We need to handle this for forward compatibility, but it won't exist in 3.9.
44
+ try:
45
+ from types import UnionType
46
+
47
+ UNION_TYPES = (Union, UnionType)
48
+ except ImportError:
49
+ UNION_TYPES = (Union,)
8
50
 
9
51
  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,
52
+ BINARY_STACK_OPS,
53
+ BOOLEAN_OPERATORS,
54
+ NUMERIC_OPERATORS,
55
+ UNARY_STACK_OPS,
56
+ AllocateDirective,
57
+ BinaryStackOp,
58
+ ConstCmdDirective,
59
+ FloatMultiplyDirective,
60
+ FloatTruncateDirective,
61
+ IntMultiplyDirective,
62
+ MemCompareDirective,
63
+ NoOpDirective,
64
+ StackOpDirective,
65
+ IntegerTruncate64To16Directive,
66
+ IntegerTruncate64To32Directive,
67
+ IntegerTruncate64To8Directive,
68
+ FloatLogDirective,
69
+ IntegerSignedExtend16To64Directive,
70
+ IntegerSignedExtend32To64Directive,
71
+ IntegerSignedExtend8To64Directive,
72
+ StackCmdDirective,
73
+ StorePrmDirective,
74
+ IntegerZeroExtend16To64Directive,
75
+ IntegerZeroExtend32To64Directive,
76
+ IntegerZeroExtend8To64Directive,
20
77
  Directive,
21
78
  FloatExtendDirective,
22
- IntEqualDirective,
23
79
  ExitDirective,
24
- FloatEqualDirective,
25
- FloatNotEqualDirective,
26
- GetPrmDirective,
27
- GetTlmDirective,
80
+ LoadDirective,
81
+ StoreTlmValDirective,
28
82
  GotoDirective,
29
83
  IfDirective,
30
84
  NotDirective,
31
- IntNotEqualDirective,
32
- OrDirective,
33
- SetSerRegDirective,
34
- SetRegDirective,
85
+ PushValDirective,
35
86
  SignedIntToFloatDirective,
87
+ StoreDirective,
88
+ UnaryStackOp,
36
89
  UnsignedIntToFloatDirective,
37
90
  WaitAbsDirective,
38
91
  WaitRelDirective,
39
92
  )
40
93
  from fprime_gds.common.loaders.ch_json_loader import ChJsonLoader
41
94
  from fprime_gds.common.loaders.cmd_json_loader import CmdJsonLoader
95
+ from fprime_gds.common.loaders.event_json_loader import EventJsonLoader
42
96
  from fprime_gds.common.loaders.prm_json_loader import PrmJsonLoader
43
97
  from fprime_gds.common.templates.ch_template import ChTemplate
44
98
  from fprime_gds.common.templates.cmd_template import CmdTemplate
@@ -47,15 +101,13 @@ from fprime.common.models.serialize.time_type import TimeType
47
101
  from fprime.common.models.serialize.enum_type import EnumType
48
102
  from fprime.common.models.serialize.serializable_type import SerializableType
49
103
  from fprime.common.models.serialize.array_type import ArrayType
104
+ from fprime.common.models.serialize.type_exceptions import TypeException
50
105
  from fprime.common.models.serialize.numerical_types import (
51
106
  U32Type,
52
107
  U16Type,
53
108
  U64Type,
54
109
  U8Type,
55
- I16Type,
56
- I32Type,
57
110
  I64Type,
58
- I8Type,
59
111
  F32Type,
60
112
  F64Type,
61
113
  FloatType,
@@ -65,469 +117,29 @@ from fprime.common.models.serialize.numerical_types import (
65
117
  from fprime.common.models.serialize.string_type import StringType
66
118
  from fprime.common.models.serialize.bool_type import BoolType
67
119
  from fprime_gds.common.fpy.parser import (
68
- AstAnd,
120
+ AstBinaryOp,
69
121
  AstBoolean,
70
- AstComparison,
71
122
  AstElif,
72
123
  AstElifs,
73
124
  AstExpr,
74
125
  AstGetAttr,
75
126
  AstGetItem,
76
- AstNot,
77
127
  AstNumber,
78
- AstOr,
128
+ AstOp,
79
129
  AstReference,
130
+ AstScopedBody,
80
131
  AstString,
81
132
  Ast,
82
- AstTest,
83
133
  AstBody,
84
134
  AstLiteral,
85
135
  AstIf,
86
136
  AstAssign,
87
137
  AstFuncCall,
138
+ AstUnaryOp,
88
139
  AstVar,
89
140
  )
90
141
  from fprime.common.models.serialize.type_base import BaseType as FppType
91
142
 
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
143
 
532
144
  class AssignIds(TopDownVisitor):
533
145
  """assigns a unique id to each node to allow it to be indexed in a dict"""
@@ -554,7 +166,7 @@ class CreateVariables(Visitor):
554
166
  )
555
167
  return
556
168
 
557
- var = FpyVariable(node.var_type, None)
169
+ var = FpyVariable(node.var_type, node)
558
170
  # new var. put it in the table under this scope
559
171
  state.variables[node.variable.var] = var
560
172
  state.runtime_values[node.variable.var] = var
@@ -565,7 +177,7 @@ class CreateVariables(Visitor):
565
177
  return
566
178
 
567
179
 
568
- class ResolveReferences(Visitor):
180
+ class ResolveReferences(TopDownVisitor):
569
181
  """for each reference, resolve it in a specific scope based on its
570
182
  syntactic position, or fail if could not resolve"""
571
183
 
@@ -604,7 +216,7 @@ class ResolveReferences(Visitor):
604
216
  return None
605
217
  return attr
606
218
 
607
- # parent is a ch, prm, const, var or field
219
+ # parent is a ch, prm, const, or field
608
220
 
609
221
  value_type = get_ref_fpp_type_class(parent)
610
222
 
@@ -656,7 +268,7 @@ class ResolveReferences(Visitor):
656
268
  state.err("Invalid syntax", node)
657
269
  return None
658
270
 
659
- # parent is a ch, prm, const, var or field
271
+ # parent is a ch, prm, const, or field
660
272
 
661
273
  value_type = get_ref_fpp_type_class(parent)
662
274
 
@@ -672,7 +284,7 @@ class ResolveReferences(Visitor):
672
284
 
673
285
  if not self.is_type_constant_size(value_type):
674
286
  state.err(
675
- f"{value_type} has non-constant sized members, cannot access members",
287
+ f"{value_type.__name__} has non-constant sized members, cannot access members",
676
288
  node,
677
289
  )
678
290
  return None
@@ -689,18 +301,17 @@ class ResolveReferences(Visitor):
689
301
  )
690
302
  return None
691
303
 
692
- def resolve_if_ref(
693
- self, node: AstExpr, ns: FpyScope, state: CompileState
694
- ) -> bool:
304
+ def resolve_if_ref(self, node: AstExpr, ns: FpyScope, state: CompileState) -> bool:
695
305
  """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):
306
+ otherwise, if it is not a reference, return true as it doesn't need to be resolved
307
+ """
308
+ if not is_instance_compat(node, AstReference):
698
309
  return True
699
310
 
700
311
  return self.resolve_ref_in_ns(node, ns, state) is not None
701
312
 
702
313
  def resolve_ref_in_ns(
703
- self, node: AstExpr, ns: FpyScope, state: CompileState
314
+ self, node: AstReference, ns: FpyScope, state: CompileState
704
315
  ) -> FpyReference | None:
705
316
  """recursively resolves a reference in a scope, returning the resolved ref
706
317
  or none if none could be found."""
@@ -735,19 +346,19 @@ class ResolveReferences(Visitor):
735
346
  return
736
347
 
737
348
  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)
349
+ # arg value refs must have values at runtime
350
+ if not self.resolve_if_ref(arg, state.runtime_values, state):
351
+ state.err("Unknown runtime value", arg)
741
352
  return
742
353
 
743
- def visit_AstIf_AstElif(self, node: AstIf | AstElif, state: CompileState):
354
+ def visit_AstIf_AstElif(self, node: Union[AstIf, AstElif], state: CompileState):
744
355
  # if condition expr refs must be "runtime values" (tlm/prm/const/etc)
745
356
  if not self.resolve_if_ref(node.condition, state.runtime_values, state):
746
357
  state.err("Unknown runtime value", node.condition)
747
358
  return
748
359
 
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"
360
+ def visit_AstBinaryOp(self, node: AstBinaryOp, state: CompileState):
361
+ # lhs/rhs side of stack op, if they are refs, must be refs to "runtime vals"
751
362
  if not self.resolve_if_ref(node.lhs, state.runtime_values, state):
752
363
  state.err("Unknown runtime value", node.lhs)
753
364
  return
@@ -755,15 +366,9 @@ class ResolveReferences(Visitor):
755
366
  state.err("Unknown runtime value", node.rhs)
756
367
  return
757
368
 
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)
369
+ def visit_AstUnaryOp(self, node: AstUnaryOp, state: CompileState):
370
+ if not self.resolve_if_ref(node.val, state.runtime_values, state):
371
+ state.err("Unknown runtime value", node.val)
767
372
  return
768
373
 
769
374
  def visit_AstAssign(self, node: AstAssign, state: CompileState):
@@ -779,118 +384,201 @@ class ResolveReferences(Visitor):
779
384
  return
780
385
  var.type = type
781
386
 
782
- if not self.resolve_if_ref(node.value, state.consts, state):
783
- state.err("Unknown const", node.value)
387
+ if not self.resolve_if_ref(node.value, state.runtime_values, state):
388
+ state.err("Unknown runtime value", node.value)
784
389
  return
785
390
 
391
+ def visit_AstReference(self, node: AstReference, state: CompileState):
392
+ # make sure that all refs are resolved when we get to them
393
+ if node not in state.resolved_references:
394
+ state.err("Unknown variable", node)
395
+ return
786
396
 
787
- class CalculateExprTypes(Visitor):
788
- """stores in state the fprime type of each expression, or NothingType if the expr had no type"""
789
397
 
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
398
+ class CheckUseBeforeDeclare(Visitor):
798
399
 
799
- def visit_AstString(self, node: AstString, state: CompileState):
800
- state.expr_types[node] = StringType
400
+ def __init__(self):
401
+ self.currently_declared_vars: list[FpyVariable] = []
801
402
 
802
- def visit_AstBoolean(self, node: AstBoolean, state: CompileState):
803
- state.expr_types[node] = BoolType
403
+ def visit_AstAssign(self, node: AstAssign, state: CompileState):
404
+ var = state.resolved_references[node.variable]
405
+
406
+ if var.declaration != node:
407
+ # this is not the node that declares this variable
408
+ return
409
+
410
+ # this node declares this variable
411
+
412
+ self.currently_declared_vars.append(var)
804
413
 
805
414
  def visit_AstReference(self, node: AstReference, state: CompileState):
806
415
  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
416
+ if not isinstance(ref, FpyVariable):
417
+ return
813
418
 
814
- def visit_AstOr_AstAnd_AstNot_AstComparison(
815
- self, node: AstOr | AstAnd | AstNot | AstComparison, state: CompileState
816
- ):
817
- state.expr_types[node] = BoolType
419
+ if ref.declaration.variable == node:
420
+ # this is the initial name of the variable. don't crash
421
+ return
818
422
 
819
- def visit_default(self, node, state):
820
- # coding error, missed an expr
821
- assert not isinstance(node, AstExpr), node
423
+ if ref not in self.currently_declared_vars:
424
+ state.err("Variable used before declared", node)
425
+ return
822
426
 
823
427
 
824
- class CheckAndResolveArgumentTypes(Visitor):
825
- """for each syntactic node with arguments (ands/ors/nots/cmps/funcs), check that the argument
826
- types are right"""
428
+ class PickAndConvertTypes(Visitor):
429
+ """stores in state the fprime type of each expression, or NothingType if the expr had no type"""
827
430
 
828
- def is_literal_convertible_to(
829
- self, node: AstLiteral, to_type: FppTypeClass, state: CompileState
431
+ def coerce_expr_type(
432
+ self, node: AstExpr, type: FppTypeClass, state: CompileState
830
433
  ) -> bool:
434
+ node_type = state.expr_types[node]
435
+ if self.can_coerce_type(node_type, type):
436
+ state.type_coercions[node] = type
437
+ return True
438
+ state.err(f"Expected {type.__name__}, found {node_type.__name__}", node)
439
+ return False
440
+
441
+ def can_coerce_type(self, type: FppTypeClass, to_type: FppTypeClass) -> bool:
442
+ if type == to_type:
443
+ return True
444
+ if issubclass(type, IntegerType) and issubclass(to_type, NumericalType):
445
+ # we can coerce any integer into any other number
446
+ return True
447
+ if issubclass(type, FloatType) and issubclass(to_type, FloatType):
448
+ # we can convert any float into any float
449
+ return True
450
+ if type == InternalStringType and issubclass(to_type, StringType):
451
+ # we can convert the internal String type to any string type
452
+ return True
831
453
 
832
- if isinstance(node, AstBoolean):
833
- return to_type == BoolType
454
+ return False
455
+
456
+ def pick_intermediate_type(
457
+ self, arg_types: list[FppTypeClass], op: BinaryStackOp | UnaryStackOp
458
+ ) -> FppTypeClass:
459
+
460
+ if op in BOOLEAN_OPERATORS:
461
+ return BoolType
834
462
 
835
- if isinstance(node, AstString):
836
- return issubclass(to_type, StringType)
463
+ non_numeric = any(not issubclass(t, NumericalType) for t in arg_types)
464
+
465
+ if op == BinaryStackOp.EQUAL or op == BinaryStackOp.NOT_EQUAL:
466
+ if non_numeric:
467
+ if len(set(arg_types)) != 1:
468
+ # can only compare equality between the same types
469
+ return None
470
+ return arg_types[0]
471
+
472
+ # all arguments should be numeric
473
+ if non_numeric:
474
+ # cannot find intermediate type
475
+ return None
837
476
 
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))
477
+ if op == BinaryStackOp.DIVIDE or op == BinaryStackOp.EXPONENT:
478
+ # always do true division over floats, python style
479
+ return F64Type
844
480
 
845
- assert False, node.value
481
+ float = any(issubclass(t, FloatType) for t in arg_types)
482
+ unsigned = any(t in UNSIGNED_INTEGER_TYPES for t in arg_types)
846
483
 
847
- assert False, node
484
+ if float:
485
+ # at least one arg is a float
486
+ return F64Type
848
487
 
849
- def visit_AstComparison(self, node: AstComparison, state: CompileState):
488
+ if unsigned:
489
+ # at least one arg is unsigned
490
+ return U64Type
491
+
492
+ return I64Type
493
+
494
+ def visit_AstNumber(self, node: AstNumber, state: CompileState):
495
+ # give a best guess as to the final type of this node. we don't actually know
496
+ # its bitwidth or signedness yet
497
+ if isinstance(node.value, float):
498
+ result_type = F64Type
499
+ else:
500
+ result_type = InternalIntType
501
+ state.expr_types[node] = result_type
850
502
 
503
+ def visit_AstBinaryOp(self, node: AstBinaryOp, state: CompileState):
851
504
  lhs_type = state.expr_types[node.lhs]
852
505
  rhs_type = state.expr_types[node.rhs]
853
506
 
854
- if not issubclass(lhs_type, NumericalType):
855
- state.err(f"Cannot compare non-numeric type {lhs_type}", node.lhs)
507
+ intermediate_type = self.pick_intermediate_type([lhs_type, rhs_type], node.op)
508
+ if intermediate_type is None:
509
+ state.err(
510
+ f"Op {node.op} undefined for {lhs_type.__name__}, {rhs_type.__name__}",
511
+ node,
512
+ )
513
+ return
514
+
515
+ if not self.coerce_expr_type(node.lhs, intermediate_type, state):
856
516
  return
857
- if not issubclass(rhs_type, NumericalType):
858
- state.err(f"Cannot compare non-numeric type {rhs_type}", node.rhs)
517
+ if not self.coerce_expr_type(node.rhs, intermediate_type, state):
859
518
  return
860
519
 
861
- # args are both numeric
520
+ # okay now find which actual directive we're going to use based on this intermediate
521
+ # type, and save it
522
+
523
+ dir = None
524
+ if (
525
+ node.op == BinaryStackOp.EQUAL or node.op == BinaryStackOp.NOT_EQUAL
526
+ ) and intermediate_type not in SPECIFIC_NUMERIC_TYPES:
527
+ dir = MemCompareDirective
528
+ else:
529
+ dir = BINARY_STACK_OPS[node.op][intermediate_type]
530
+
531
+ state.stack_op_directives[node] = dir
532
+
533
+ result_type = None
534
+ if node.op in NUMERIC_OPERATORS:
535
+ result_type = intermediate_type
536
+ else:
537
+ result_type = BoolType
538
+ state.expr_types[node] = result_type
539
+
540
+ def visit_AstUnaryOp(self, node: AstUnaryOp, state: CompileState):
541
+ val_type = state.expr_types[node.val]
862
542
 
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
543
+ intermediate_type = self.pick_intermediate_type([val_type], node.op)
544
+ if intermediate_type is None:
545
+ state.err(f"Op {node.op} undefined for {val_type.__name__}", node)
546
+ return
869
547
 
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
548
+ if not self.coerce_expr_type(node.val, intermediate_type, state):
874
549
  return
875
550
 
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
551
+ # okay now find which actual directive we're going to use based on this intermediate
552
+ # type, and save it
883
553
 
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
554
+ chosen_dir = UNARY_STACK_OPS[node.op][intermediate_type]
555
+
556
+ result_type = None
557
+ if node.op in NUMERIC_OPERATORS:
558
+ result_type = intermediate_type
559
+ else:
560
+ result_type = BoolType
561
+
562
+ state.stack_op_directives[node] = chosen_dir
563
+ state.expr_types[node] = result_type
564
+
565
+ def visit_AstString(self, node: AstString, state: CompileState):
566
+ state.expr_types[node] = InternalStringType
567
+
568
+ def visit_AstBoolean(self, node: AstBoolean, state: CompileState):
569
+ state.expr_types[node] = BoolType
570
+
571
+ def visit_AstReference(self, node: AstReference, state: CompileState):
572
+ ref = state.resolved_references[node]
573
+ state.expr_types[node] = get_ref_fpp_type_class(ref)
574
+ if isinstance(node, AstGetItem):
575
+ # the node of the index number has no expression value, it's an arg
576
+ # but only at syntax level
577
+ state.expr_types[node.item] = NothingType
891
578
 
892
579
  def visit_AstFuncCall(self, node: AstFuncCall, state: CompileState):
893
580
  func = state.resolved_references[node.func]
581
+ assert isinstance(func, FpyCallable)
894
582
  func_args = func.args
895
583
  node_args = node.args if node.args else []
896
584
 
@@ -914,84 +602,81 @@ class CheckAndResolveArgumentTypes(Visitor):
914
602
  for value_expr, arg in zip(node_args, func_args):
915
603
  arg_name, arg_type = arg
916
604
 
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
605
+ if not self.coerce_expr_type(value_expr, arg_type, state):
606
+ return
935
607
 
936
608
  # got thru all args successfully
609
+ state.expr_types[node] = func.return_type
937
610
 
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
611
+ def visit_AstAssign(self, node: AstAssign, state: CompileState):
612
+ var_type = state.resolved_references[node.variable].type
946
613
 
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)
614
+ if not self.coerce_expr_type(node.value, var_type, state):
951
615
  return
952
- state.expr_types[node.value] = BoolType
953
616
 
617
+ def visit_default(self, node, state):
618
+ # coding error, missed an expr
619
+ assert not is_instance_compat(node, AstExpr), node
620
+
621
+
622
+ class AllocateVariables(Visitor):
954
623
  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
624
+ existing_var = state.resolved_references[node.variable]
964
625
 
965
- state.expr_types[node.value] = var_type
626
+ assert existing_var is not None
627
+ assert existing_var.type is not None
966
628
 
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
629
+ value_size = existing_var.type.getMaxSize()
970
630
 
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
631
+ if existing_var.lvar_offset is None:
632
+ # doesn't have an lvar idx, allocate one
633
+ lvar_offset = state.lvar_array_size_bytes
634
+ state.lvar_array_size_bytes += value_size
635
+ existing_var.lvar_offset = lvar_offset
975
636
 
976
637
 
977
- class CalculateExprValues(Visitor):
638
+ class CalculateConstExprValues(Visitor):
978
639
  """for each expr, try to calculate its constant value and store it in a map. stores None if no value could be
979
640
  calculated at compile time, and NothingType if the expr had no value"""
980
641
 
642
+ def const_coerce_type(self, from_val: FppType, to_type: FppTypeClass) -> FppType:
643
+ if type(from_val) == to_type:
644
+ return from_val
645
+ if issubclass(to_type, StringType):
646
+ assert type(from_val) == InternalStringType, type(from_val)
647
+ return to_type(from_val.val)
648
+ if issubclass(to_type, FloatType):
649
+ assert issubclass(type(from_val), NumericalType), type(from_val)
650
+ return to_type(float(from_val.val))
651
+ if issubclass(to_type, IntegerType):
652
+ assert issubclass(type(from_val), IntegerType), type(from_val)
653
+ return to_type(int(from_val.val))
654
+ assert False, (from_val, type(from_val), to_type)
655
+
981
656
  def visit_AstLiteral(self, node: AstLiteral, state: CompileState):
982
657
  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)
658
+
659
+ if literal_type == NothingType:
660
+ value = NothingType()
990
661
  else:
991
- state.expr_values[node] = literal_type()
662
+ try:
663
+ value = literal_type(node.value)
664
+ except TypeException as e:
665
+ state.err(f"For type {literal_type.__name__}: {e}", node)
666
+ return
667
+
668
+ coerced_type = state.type_coercions.get(node, None)
669
+ if coerced_type is not None:
670
+ try:
671
+ value = self.const_coerce_type(value, coerced_type)
672
+ except TypeException as e:
673
+ state.err(f"For type {coerced_type.__name__}: {e}", node)
674
+ return
675
+ state.expr_values[node] = value
992
676
 
993
677
  def visit_AstReference(self, node: AstReference, state: CompileState):
994
678
  ref = state.resolved_references[node]
679
+ expr_type = state.expr_types[node]
995
680
 
996
681
  if isinstance(ref, (ChTemplate, PrmTemplate, FpyVariable)):
997
682
  # we do not try to calculate or predict these values at compile time
@@ -1022,10 +707,20 @@ class CalculateExprValues(Visitor):
1022
707
  else:
1023
708
  assert False, ref
1024
709
 
1025
- assert expr_value is None or isinstance(expr_value, state.expr_types[node]), (
1026
- expr_value,
1027
- state.expr_types[node],
1028
- )
710
+ if expr_value is None:
711
+ # cannot calculate at compile time
712
+ state.expr_values[node] = None
713
+ return
714
+
715
+ assert isinstance(expr_value, expr_type), (expr_value, expr_type)
716
+
717
+ coerced_type = state.type_coercions.get(node, None)
718
+ if coerced_type is not None:
719
+ try:
720
+ expr_value = self.const_coerce_type(expr_value, coerced_type)
721
+ except TypeException as e:
722
+ state.err(f"For type {expr_type.__name__}: {e}", node)
723
+ return
1029
724
  state.expr_values[node] = expr_value
1030
725
 
1031
726
  def visit_AstFuncCall(self, node: AstFuncCall, state: CompileState):
@@ -1037,9 +732,12 @@ class CalculateExprValues(Visitor):
1037
732
  ]
1038
733
  unknown_value = any(v for v in arg_values if v is None)
1039
734
  if unknown_value:
735
+ # we will have to calculate this at runtime
1040
736
  state.expr_values[node] = None
1041
737
  return
1042
738
 
739
+ expr_value = None
740
+
1043
741
  if isinstance(func, FpyTypeCtor):
1044
742
  # actually construct the type
1045
743
  if issubclass(func.type, SerializableType):
@@ -1048,15 +746,15 @@ class CalculateExprValues(Visitor):
1048
746
  # t[0] is the arg name
1049
747
  arg_dict = {t[0]: v for t, v in zip(func.type.MEMBER_LIST, arg_values)}
1050
748
  instance._val = arg_dict
1051
- state.expr_values[node] = instance
749
+ expr_value = instance
1052
750
 
1053
751
  elif issubclass(func.type, ArrayType):
1054
752
  instance = func.type()
1055
753
  instance._val = arg_values
1056
- state.expr_values[node] = instance
754
+ expr_value = instance
1057
755
 
1058
756
  elif func.type == TimeType:
1059
- state.expr_values[node] = TimeType(*arg_values)
757
+ expr_value = TimeType(*arg_values)
1060
758
 
1061
759
  else:
1062
760
  # no other FppTypeClasses have ctors
@@ -1065,164 +763,170 @@ class CalculateExprValues(Visitor):
1065
763
  # don't try to calculate the value of this function call
1066
764
  # it's something like a cmd or macro
1067
765
  state.expr_values[node] = None
766
+ return
767
+
768
+ assert isinstance(expr_value, func.return_type), (expr_value, func.return_type)
769
+
770
+ coerced_type = state.type_coercions.get(node, None)
771
+ if coerced_type is not None:
772
+ try:
773
+ expr_value = self.const_coerce_type(expr_value, coerced_type)
774
+ except TypeException as e:
775
+ state.err(f"For type {func.return_type.__name__}: {e}", node)
776
+ return
777
+ state.expr_values[node] = expr_value
1068
778
 
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
779
+ def visit_AstOp(self, node: AstOp, state: CompileState):
780
+ # we do not calculate compile time value of operators at the moment
1071
781
  state.expr_values[node] = None
1072
782
 
1073
783
  def visit_default(self, node, state):
1074
784
  # coding error, missed an expr
1075
- assert not isinstance(node, AstExpr), node
785
+ assert not is_instance_compat(node, AstExpr), node
1076
786
 
1077
787
 
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
788
+ class GenerateConstExprDirectives(Visitor):
789
+ """for each expr with a constant compile time value, generate
790
+ directives for how to put it in its register"""
1089
791
 
1090
- value_type = state.expr_types[node.value]
1091
- value = state.expr_values[node.value]
792
+ def visit_AstExpr(self, node: AstExpr, state: CompileState):
793
+ if node in state.directives:
794
+ # already have directives associated with this node
795
+ return
1092
796
 
1093
- # already type checked
1094
- assert value_type == type(value), (value_type, type(value))
797
+ expr_value = state.expr_values[node]
1095
798
 
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
- )
799
+ if expr_value is None:
800
+ # no const value
1102
801
  return
1103
802
 
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)]
803
+ if isinstance(expr_value, NothingType):
804
+ # nothing type has no value
805
+ state.directives[node] = []
806
+ return
1118
807
 
808
+ # it has a constant value at compile time
809
+ serialized_expr_value = expr_value.serialize()
1119
810
 
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
811
+ # push it to the stack
812
+ state.directives[node] = [PushValDirective(serialized_expr_value)]
1172
813
 
1173
814
 
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]
815
+ class GenerateExprMacrosAndCmds(Visitor):
816
+ """for each expr whose value is not known at compile time, but can be calculated at run time,
817
+ generate directives to calculate the value and put it in its register. for each command
818
+ or macro, generate directives for calling them with appropriate arg values"""
819
+
820
+ def get_64_bit_type(self, type: FppTypeClass) -> FppTypeClass:
821
+ assert type in SPECIFIC_NUMERIC_TYPES, type
822
+ return (
823
+ I64Type
824
+ if type in SIGNED_INTEGER_TYPES
825
+ else U64Type if type in UNSIGNED_INTEGER_TYPES else F64Type
826
+ )
1179
827
 
1180
- if node in state.directives:
1181
- # already have directives associated with this node
1182
- return
828
+ def truncate_from_64_bits(
829
+ self, from_type: FppTypeClass, new_size: int
830
+ ) -> list[Directive]:
1183
831
 
1184
- if expr_type == NothingType:
1185
- # impossible. nothing type has no value
1186
- state.directives[node] = None
1187
- return
832
+ assert new_size in (1, 2, 4, 8), new_size
833
+ assert from_type.getMaxSize() == 8, from_type.getMaxSize()
1188
834
 
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
835
+ if new_size == 8:
836
+ # already correct size
837
+ return []
1194
838
 
1195
- # okay, it is not nothing and it is smaller than 8 bytes.
1196
- # should be able to put it in a reg
839
+ if from_type == F64Type:
840
+ # only one option for float trunc
841
+ assert new_size == 4, new_size
842
+ return [FloatTruncateDirective()]
1197
843
 
1198
- register = state.expr_registers[node]
844
+ # must be an int
845
+ assert issubclass(from_type, IntegerType), from_type
1199
846
 
1200
- expr_value = state.expr_values[node]
847
+ if new_size == 1:
848
+ return [IntegerTruncate64To8Directive()]
849
+ elif new_size == 2:
850
+ return [IntegerTruncate64To16Directive()]
1201
851
 
1202
- if expr_value is None:
1203
- # no const value
1204
- return
852
+ return [IntegerTruncate64To32Directive()]
1205
853
 
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
854
+ def extend_to_64_bits(self, type: FppTypeClass) -> list[Directive]:
855
+ if type.getMaxSize() == 8:
856
+ # already 8 bytes
857
+ return []
858
+ if type == F32Type:
859
+ return [FloatExtendDirective()]
1211
860
 
1212
- # reinterpret as an I64
1213
- val_as_i64 = I64Type()
1214
- val_as_i64.deserialize(val_as_i64_bytes, 0)
861
+ # must be an int
862
+ assert issubclass(type, IntegerType), type
1215
863
 
1216
- state.directives[node] = [SetRegDirective(register, val_as_i64.val)]
864
+ from_size = type.getMaxSize()
865
+ assert from_size in (1, 2, 4, 8), from_size
1217
866
 
867
+ if type in SIGNED_INTEGER_TYPES:
868
+ if from_size == 1:
869
+ return [IntegerSignedExtend8To64Directive()]
870
+ elif from_size == 2:
871
+ return [IntegerSignedExtend16To64Directive()]
872
+ else:
873
+ return [IntegerSignedExtend32To64Directive()]
874
+ else:
875
+ if from_size == 1:
876
+ return [IntegerZeroExtend8To64Directive()]
877
+ elif from_size == 2:
878
+ return [IntegerZeroExtend16To64Directive()]
879
+ else:
880
+ return [IntegerZeroExtend32To64Directive()]
881
+
882
+ def convert_type(
883
+ self, from_type: FppTypeClass, to_type: FppTypeClass
884
+ ) -> list[Directive]:
885
+ if from_type == to_type:
886
+ return []
887
+
888
+ # only valid runtime type conversion is between two numeric types
889
+ assert (
890
+ from_type in SPECIFIC_NUMERIC_TYPES and to_type in SPECIFIC_NUMERIC_TYPES
891
+ ), (
892
+ from_type,
893
+ to_type,
894
+ )
895
+ # also invalid to convert from a float to an integer at runtime due to loss of precision
896
+ assert not (
897
+ from_type in SPECIFIC_FLOAT_TYPES and to_type in SPECIFIC_INTEGER_TYPES
898
+ ), (
899
+ from_type,
900
+ to_type,
901
+ )
1218
902
 
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"""
903
+ dirs = []
904
+ # first go to 64 bit width
905
+ dirs.extend(self.extend_to_64_bits(from_type))
906
+ from_64_bit = self.get_64_bit_type(from_type)
907
+ to_64_bit = self.get_64_bit_type(to_type)
908
+
909
+ # now convert from int to float if necessary
910
+ if from_64_bit == U64Type and to_64_bit == F64Type:
911
+ dirs.append(UnsignedIntToFloatDirective())
912
+ from_64_bit = F64Type
913
+ elif from_64_bit == I64Type and to_64_bit == F64Type:
914
+ dirs.append(SignedIntToFloatDirective())
915
+ from_64_bit = F64Type
916
+ elif from_64_bit == U64Type or from_64_bit == I64Type:
917
+ assert to_64_bit == U64Type or to_64_bit == I64Type
918
+ # conversion from signed to unsigned int is implicit, doesn't need code gen
919
+ from_64_bit = to_64_bit
920
+
921
+ assert from_64_bit == to_64_bit, (from_64_bit, to_64_bit)
922
+
923
+ # now truncate back down to desired size
924
+ dirs.extend(self.truncate_from_64_bits(to_64_bit, to_type.getMaxSize()))
925
+ return dirs
1222
926
 
1223
927
  def visit_AstReference(self, node: AstReference, state: CompileState):
1224
928
  if node in state.directives:
1225
- # already know how to put it in reg, or it is impossible
929
+ # already know how to put it on stack, or it is impossible
1226
930
  return
1227
931
 
1228
932
  expr_type = state.expr_types[node]
@@ -1232,168 +936,178 @@ class GenerateNonConstExprDirectives(Visitor):
1232
936
 
1233
937
  # does not have a constant compile time value
1234
938
 
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
939
+ # first, put it in an lvar. then load it from the lvar onto stack
1239
940
 
1240
- offset = 0
941
+ # the offset of the field in the parent type
942
+ offset_in_parent_val = 0
943
+ # the offset of the lvar the parent type is stored in
944
+ offset_in_lvar_array = 0
1241
945
 
1242
946
  base_ref = ref
1243
947
 
1244
948
  # if it's a field ref, find the parent and the offset in the parent
1245
949
  while isinstance(base_ref, FieldReference):
1246
- offset += base_ref.offset
950
+ offset_in_parent_val += base_ref.offset
1247
951
  base_ref = base_ref.parent
1248
952
 
1249
- if isinstance(base_ref, FpyVariable):
1250
- # already in an sreg
1251
- sreg_idx = base_ref.sreg_idx
953
+ if isinstance(base_ref, ChTemplate):
954
+ # put it in an lvar
955
+ offset_in_lvar_array = state.lvar_array_size_bytes
956
+ state.lvar_array_size_bytes += base_ref.get_type_obj().getMaxSize()
957
+ directives.append(
958
+ StoreTlmValDirective(base_ref.get_id(), offset_in_lvar_array)
959
+ )
960
+ elif isinstance(base_ref, PrmTemplate):
961
+ # put it in an lvar
962
+ offset_in_lvar_array = state.lvar_array_size_bytes
963
+ state.lvar_array_size_bytes += base_ref.get_type_obj().getMaxSize()
964
+ directives.append(
965
+ StorePrmDirective(base_ref.get_id(), offset_in_lvar_array)
966
+ )
967
+ elif isinstance(base_ref, FpyVariable):
968
+ # already should be in an lvar
969
+ offset_in_lvar_array = base_ref.lvar_offset
1252
970
  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()))
971
+ assert (
972
+ False
973
+ ), base_ref # ref should either be impossible to put on stack or should have a compile time val
1265
974
 
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()
975
+ # load from the lvar
976
+ directives.append(
977
+ LoadDirective(
978
+ offset_in_lvar_array + offset_in_parent_val, expr_type.getMaxSize()
1275
979
  )
1276
980
  )
981
+ converted_type = state.type_coercions.get(node, None)
982
+ if converted_type is not None:
983
+ directives.extend(self.convert_type(expr_type, converted_type))
1277
984
 
1278
985
  state.directives[node] = directives
1279
986
 
1280
- def visit_AstAnd_AstOr(self, node: AstAnd | AstOr, state: CompileState):
987
+ def visit_AstBinaryOp(self, node: AstBinaryOp, state: CompileState):
1281
988
  if node in state.directives:
1282
- # already know how to put it in reg, or know that it's impossible
989
+ # already know how to put it on stack
1283
990
  return
1284
991
 
1285
- expr_reg = state.expr_registers[node]
1286
992
  directives = []
1287
993
 
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
994
+ expr_type = state.expr_types[node]
1302
995
 
1303
- directives.append(
1304
- dir_type(registers_to_compare[0], registers_to_compare[1], expr_reg)
1305
- )
996
+ lhs_dirs = state.directives[node.lhs]
997
+ rhs_dirs = state.directives[node.rhs]
998
+
999
+ # which variant of the op did we pick?
1000
+ dir = state.stack_op_directives[node]
1001
+
1002
+ # generate the actual op itself
1003
+ directives: list[Directive] = lhs_dirs + rhs_dirs
1004
+ if dir == MemCompareDirective:
1005
+ lhs_type = state.expr_types[node.lhs]
1006
+ rhs_type = state.expr_types[node.rhs]
1007
+ assert lhs_type == rhs_type, (lhs_type, rhs_type)
1008
+ directives.append(dir(lhs_type.getMaxSize()))
1009
+ if node.op == BinaryStackOp.NOT_EQUAL:
1010
+ directives.append(NotDirective())
1011
+ elif dir == NoOpDirective:
1012
+ # don't include no op
1013
+ pass
1014
+ else:
1015
+ directives.append(dir())
1306
1016
 
1307
- for i in range(2, len(registers_to_compare)):
1308
- directives.append(dir_type(expr_reg, registers_to_compare[i], expr_reg))
1017
+ # and convert the result of the op into the desired result of this expr
1018
+ converted_type = state.type_coercions.get(node, None)
1019
+ if converted_type is not None:
1020
+ directives.extend(self.convert_type(expr_type, converted_type))
1309
1021
 
1310
1022
  state.directives[node] = directives
1311
1023
 
1312
- def visit_AstNot(self, node: AstNot, state: CompileState):
1024
+ def visit_AstUnaryOp(self, node: AstUnaryOp, state: CompileState):
1313
1025
  if node in state.directives:
1314
- # already know how to put it in reg
1026
+ # already know how to put it on stack
1315
1027
  return
1316
1028
 
1317
- expr_reg = state.expr_registers[node]
1318
1029
  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
1030
 
1324
- state.directives[node] = directives
1031
+ expr_type = state.expr_types[node]
1325
1032
 
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
1033
+ val_dirs = state.directives[node.val]
1330
1034
 
1331
- directives = []
1035
+ # which variant of the op did we pick?
1036
+ dir = state.stack_op_directives[node]
1037
+ # generate the actual op itself
1038
+ directives: list[Directive] = val_dirs
1332
1039
 
1333
- lhs_type = state.expr_types[node.lhs]
1334
- rhs_type = state.expr_types[node.rhs]
1040
+ if node.op == UnaryStackOp.NEGATE:
1041
+ # in this case, we also need to push -1
1042
+ if dir == FloatMultiplyDirective:
1043
+ directives.append(PushValDirective(F64Type(-1).serialize()))
1044
+ elif dir == IntMultiplyDirective:
1045
+ directives.append(PushValDirective(I64Type(-1).serialize()))
1335
1046
 
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:
1047
+ directives.append(dir())
1048
+ # and convert the result of the op into the desired result of this expr
1049
+ converted_type = state.type_coercions.get(node, None)
1050
+ if converted_type is not None:
1051
+ directives.extend(self.convert_type(expr_type, converted_type))
1380
1052
 
1381
- if fp:
1382
- dir_type = FLOAT_INEQUALITY_DIRECTIVES[node.op.value]
1053
+ state.directives[node] = directives
1054
+
1055
+ def visit_AstFuncCall(self, node: AstFuncCall, state: CompileState):
1056
+ node_args = node.args if node.args is not None else []
1057
+ func = state.resolved_references[node.func]
1058
+ dirs = state.directives.get(node, [])
1059
+ if len(dirs) > 0:
1060
+ # already know how to put this on the stack
1061
+ return
1062
+ if isinstance(func, FpyCmd):
1063
+ const_args = not any(
1064
+ state.expr_values[arg_node] is None for arg_node in node_args
1065
+ )
1066
+ if const_args:
1067
+ # can just hardcode this cmd
1068
+ arg_bytes = bytes()
1069
+ for arg_node in node_args:
1070
+ arg_value = state.expr_values[arg_node]
1071
+ arg_bytes += arg_value.serialize()
1072
+ dirs = [ConstCmdDirective(func.cmd.get_op_code(), arg_bytes)]
1383
1073
  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
1074
+ arg_byte_count = 0
1075
+ # push all args to the stack
1076
+ # keep track of how many bytes total we have pushed
1077
+ for arg_node in node_args:
1078
+ node_dirs = state.directives[arg_node]
1079
+ assert len(node_dirs) >= 1
1080
+ dirs.extend(node_dirs)
1081
+ arg_byte_count = state.expr_types[arg_node].getMaxSize()
1082
+ # then push cmd opcode to stack as u32
1083
+ dirs.append(
1084
+ PushValDirective(U32Type(func.cmd.get_op_code()).serialize())
1387
1085
  )
1086
+ # now that all args are pushed to the stack, pop them and opcode off the stack
1087
+ # as a command
1088
+ dirs.append(StackCmdDirective(arg_byte_count))
1089
+ elif isinstance(func, FpyMacro):
1090
+ # put all arg values on stack
1091
+ for arg_node in node_args:
1092
+ node_dirs = state.directives[arg_node]
1093
+ assert len(node_dirs) >= 1
1094
+ dirs.extend(node_dirs)
1388
1095
 
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]
1096
+ dirs.append(func.dir())
1097
+ else:
1098
+ dirs = None
1393
1099
 
1394
- directives.append(dir_type(lhs_reg, rhs_reg, res_reg))
1100
+ # perform type conversion if called for
1101
+ coerced_type = state.type_coercions.get(node, None)
1102
+ if coerced_type is not None:
1103
+ dirs.extend(self.convert_type(func.return_type, coerced_type))
1104
+ state.directives[node] = dirs
1395
1105
 
1396
- state.directives[node] = directives
1106
+ def visit_AstAssign(self, node: AstAssign, state: CompileState):
1107
+ var = state.resolved_references[node.variable]
1108
+ state.directives[node] = state.directives[node.value] + [
1109
+ StoreDirective(var.lvar_offset, var.type.getMaxSize())
1110
+ ]
1397
1111
 
1398
1112
 
1399
1113
  class CountNodeDirectives(Visitor):
@@ -1437,8 +1151,11 @@ class CountNodeDirectives(Visitor):
1437
1151
 
1438
1152
  state.node_dir_counts[node] = count
1439
1153
 
1440
- def visit_AstBody(self, node: AstBody, state: CompileState):
1154
+ def visit_AstBody(self, node: Union[AstBody, AstScopedBody], state: CompileState):
1441
1155
  count = 0
1156
+ if isinstance(node, AstScopedBody):
1157
+ # add one for lvar array alloc
1158
+ count += 1
1442
1159
  for stmt in node.stmts:
1443
1160
  count += state.node_dir_counts[stmt]
1444
1161
 
@@ -1453,13 +1170,18 @@ class CountNodeDirectives(Visitor):
1453
1170
  class CalculateStartLineIdx(TopDownVisitor):
1454
1171
  """based on the number of directives generated by each node, calculate the start line idx
1455
1172
  of each node's directives"""
1456
- def visit_AstBody(self, node: AstBody, state: CompileState):
1173
+
1174
+ def visit_AstBody(self, node: Union[AstBody, AstScopedBody], state: CompileState):
1457
1175
  if node not in state.start_line_idx:
1458
1176
  state.start_line_idx[node] = 0
1459
1177
 
1460
1178
  start_idx = state.start_line_idx[node]
1461
1179
 
1462
1180
  line_idx = start_idx
1181
+ if isinstance(node, AstScopedBody):
1182
+ # include lvar alloc
1183
+ line_idx += 1
1184
+
1463
1185
  for stmt in node.stmts:
1464
1186
  state.start_line_idx[stmt] = line_idx
1465
1187
  line_idx += state.node_dir_counts[stmt]
@@ -1518,10 +1240,10 @@ class GenerateBodyDirectives(Visitor):
1518
1240
 
1519
1241
  for case in cases:
1520
1242
  case_dirs = []
1521
- # include the condition
1243
+ # put the conditional on top of stack
1522
1244
  case_dirs.extend(state.directives[case[0]])
1523
1245
  # include if stmt (update the end idx later)
1524
- if_dir = IfDirective(state.expr_registers[case[0]], -1)
1246
+ if_dir = IfDirective(-1)
1525
1247
 
1526
1248
  case_dirs.append(if_dir)
1527
1249
  # include body
@@ -1532,7 +1254,7 @@ class GenerateBodyDirectives(Visitor):
1532
1254
  goto_ends.append(goto_dir)
1533
1255
 
1534
1256
  # if false, skip the body and goto
1535
- if_dir.false_goto_stmt_index = (
1257
+ if_dir.false_goto_dir_index = (
1536
1258
  start_line_idx + len(all_dirs) + len(case_dirs)
1537
1259
  )
1538
1260
 
@@ -1542,12 +1264,14 @@ class GenerateBodyDirectives(Visitor):
1542
1264
  all_dirs.extend(state.directives[node.els])
1543
1265
 
1544
1266
  for goto in goto_ends:
1545
- goto.statement_index = start_line_idx + len(all_dirs)
1267
+ goto.dir_idx = start_line_idx + len(all_dirs)
1546
1268
 
1547
1269
  state.directives[node] = all_dirs
1548
1270
 
1549
- def visit_AstBody(self, node: AstBody, state: CompileState):
1271
+ def visit_AstBody(self, node: Union[AstBody, AstScopedBody], state: CompileState):
1550
1272
  dirs = []
1273
+ if isinstance(node, AstScopedBody):
1274
+ dirs.append(AllocateDirective(state.lvar_array_size_bytes))
1551
1275
  for stmt in node.stmts:
1552
1276
  stmt_dirs = state.directives.get(stmt, None)
1553
1277
  if stmt_dirs is not None:
@@ -1556,6 +1280,52 @@ class GenerateBodyDirectives(Visitor):
1556
1280
  state.directives[node] = dirs
1557
1281
 
1558
1282
 
1283
+ HEADER_FORMAT = "!BBBBBHI"
1284
+ HEADER_SIZE = struct.calcsize(HEADER_FORMAT)
1285
+
1286
+
1287
+ @dataclass
1288
+ class Header:
1289
+ majorVersion: int
1290
+ minorVersion: int
1291
+ patchVersion: int
1292
+ schemaVersion: int
1293
+ argumentCount: int
1294
+ statementCount: int
1295
+ bodySize: int
1296
+
1297
+
1298
+ FOOTER_FORMAT = "!I"
1299
+ FOOTER_SIZE = struct.calcsize(FOOTER_FORMAT)
1300
+
1301
+
1302
+ @dataclass
1303
+ class Footer:
1304
+ crc: int
1305
+
1306
+
1307
+ def serialize_directives(dirs: list[Directive], output: Path):
1308
+ output_bytes = bytes()
1309
+
1310
+ for dir in dirs:
1311
+ dir_bytes = dir.serialize()
1312
+ if len(dir_bytes) > MAX_DIRECTIVE_SIZE:
1313
+ raise CompileException(
1314
+ f"Directive {dir} in sequence too large (expected less than {MAX_DIRECTIVE_SIZE}, was {len(dir_bytes)})",
1315
+ None,
1316
+ )
1317
+ output_bytes += dir_bytes
1318
+
1319
+ header = Header(0, 0, 0, 1, 0, len(dirs), len(output_bytes))
1320
+ output_bytes = struct.pack(HEADER_FORMAT, *astuple(header)) + output_bytes
1321
+
1322
+ crc = zlib.crc32(output_bytes) % (1 << 32)
1323
+ footer = Footer(crc)
1324
+ output_bytes += struct.pack(FOOTER_FORMAT, *astuple(footer))
1325
+
1326
+ output.write_bytes(output_bytes)
1327
+
1328
+
1559
1329
  def get_base_compile_state(dictionary: str) -> CompileState:
1560
1330
  """return the initial state of the compiler, based on the given dict path"""
1561
1331
  cmd_json_dict_loader = CmdJsonLoader(dictionary)
@@ -1571,11 +1341,16 @@ def get_base_compile_state(dictionary: str) -> CompileState:
1571
1341
  (prm_id_dict, prm_name_dict, versions) = prm_json_dict_loader.construct_dicts(
1572
1342
  dictionary
1573
1343
  )
1344
+ event_json_dict_loader = EventJsonLoader(dictionary)
1345
+ (event_id_dict, event_name_dict, versions) = event_json_dict_loader.construct_dicts(
1346
+ dictionary
1347
+ )
1574
1348
  # the type name dict is a mapping of a fully qualified name to an fprime type
1575
1349
  # here we put into it all types found while parsing all cmds, params and tlm channels
1576
1350
  type_name_dict: dict[str, FppTypeClass] = cmd_json_dict_loader.parsed_types
1577
1351
  type_name_dict.update(ch_json_dict_loader.parsed_types)
1578
1352
  type_name_dict.update(prm_json_dict_loader.parsed_types)
1353
+ type_name_dict.update(event_json_dict_loader.parsed_types)
1579
1354
 
1580
1355
  # enum const dict is a dict of fully qualified enum const name (like Ref.Choice.ONE) to its fprime value
1581
1356
  enum_const_name_dict: dict[str, FppType] = {}
@@ -1590,11 +1365,12 @@ def get_base_compile_state(dictionary: str) -> CompileState:
1590
1365
 
1591
1366
  # insert the implicit types into the dict
1592
1367
  type_name_dict["Fw.Time"] = TimeType
1593
- for typ in NUMERIC_TYPES:
1368
+ for typ in SPECIFIC_NUMERIC_TYPES:
1594
1369
  type_name_dict[typ.get_canonical_name()] = typ
1595
1370
  type_name_dict["bool"] = BoolType
1596
1371
  # note no string type at the moment
1597
1372
 
1373
+ cmd_response_type = type_name_dict["Fw.CmdResponse"]
1598
1374
  callable_name_dict: dict[str, FpyCallable] = {}
1599
1375
  # add all cmds to the callable dict
1600
1376
  for name, cmd in cmd_name_dict.items():
@@ -1602,8 +1378,8 @@ def get_base_compile_state(dictionary: str) -> CompileState:
1602
1378
  args = []
1603
1379
  for arg_name, _, arg_type in cmd.arguments:
1604
1380
  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)
1381
+ # cmds are thought of as callables with a Fw.CmdResponse return value
1382
+ callable_name_dict[name] = FpyCmd(cmd_response_type, args, cmd)
1607
1383
 
1608
1384
  # for each type in the dict, if it has a constructor, create an FpyTypeCtor
1609
1385
  # object to track the constructor and put it in the callable name dict
@@ -1641,7 +1417,7 @@ def get_base_compile_state(dictionary: str) -> CompileState:
1641
1417
  return state
1642
1418
 
1643
1419
 
1644
- def compile(body: AstBody, dictionary: str) -> list[Directive]:
1420
+ def compile(body: AstScopedBody, dictionary: str) -> list[Directive]:
1645
1421
  state = get_base_compile_state(dictionary)
1646
1422
  passes: list[Visitor] = [
1647
1423
  AssignIds(),
@@ -1650,27 +1426,21 @@ def compile(body: AstBody, dictionary: str) -> list[Directive]:
1650
1426
  # now that variables have been defined, all names/attributes/indices (references)
1651
1427
  # should be defined
1652
1428
  ResolveReferences(),
1429
+ CheckUseBeforeDeclare(),
1653
1430
  # now that we know what all refs point to, we should be able to figure out the type
1654
1431
  # 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(),
1432
+ PickAndConvertTypes(),
1433
+ # now that expr types have been narrowed down, we can allocate lvar space for variables
1434
+ AllocateVariables(),
1659
1435
  # okay, now that we're sure we're passing in all the right args to each func,
1660
1436
  # 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(),
1437
+ CalculateConstExprValues(),
1668
1438
  # for expressions which have constant values, generate corresponding directives
1669
- # to put the expr in its register
1439
+ # to put the expr on the stack
1670
1440
  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(),
1441
+ # generate directives to calculate exprs, macros and cmds at runtime and put them
1442
+ # on the stack
1443
+ GenerateExprMacrosAndCmds(),
1674
1444
  # count the number of directives generated by each node
1675
1445
  CountNodeDirectives(),
1676
1446
  # calculate the index that the node will correspond to in the output file
@@ -1684,4 +1454,11 @@ def compile(body: AstBody, dictionary: str) -> list[Directive]:
1684
1454
  for error in state.errors:
1685
1455
  raise error
1686
1456
 
1687
- return state.directives[body]
1457
+ dirs = state.directives[body]
1458
+ if len(dirs) > MAX_DIRECTIVES_COUNT:
1459
+ raise CompileException(
1460
+ f"Too many directives in sequence (expected less than {MAX_DIRECTIVES_COUNT}, had {len(dirs)})",
1461
+ None,
1462
+ )
1463
+
1464
+ return dirs