-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathcompiler.py
More file actions
1378 lines (1166 loc) · 51.2 KB
/
Copy pathcompiler.py
File metadata and controls
1378 lines (1166 loc) · 51.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# The heart of the FTL -> Python compiler. See the architecture docs in
# ARCHITECTURE.rst for the big picture, and comments on compile_expr below.
import builtins
import contextlib
from collections import OrderedDict
from functools import singledispatch
import attr
import babel
from fluent.syntax import FluentParser
from fluent.syntax.ast import (
Attribute,
BaseNode,
FunctionReference,
Identifier,
Junk,
Message,
MessageReference,
NumberLiteral,
Pattern,
Placeable,
SelectExpression,
StringLiteral,
Term,
TermReference,
TextElement,
VariableReference,
)
from . import codegen, runtime
from .builtins import BUILTINS
from .errors import (
FluentCyclicReferenceError,
FluentDuplicateMessageId,
FluentFormatError,
FluentJunkFound,
FluentReferenceError,
)
from .escapers import EscaperJoin, RegisteredEscaper, escaper_for_message, escapers_compatible, identity, null_escaper
from .types import FluentDateType, FluentNone, FluentNumber, FluentType
from .utils import (
ATTRIBUTE_SEPARATOR,
TERM_SIGIL,
args_match,
ast_to_id,
attribute_ast_to_id,
display_location,
inspect_function_args,
reference_to_id,
span_to_position,
)
# Unicode bidi isolation characters.
FSI = "\u2068"
PDI = "\u2069"
BUILTIN_NUMBER = "NUMBER"
BUILTIN_DATETIME = "DATETIME"
BUILTIN_RETURN_TYPES = {
BUILTIN_NUMBER: FluentNumber,
BUILTIN_DATETIME: FluentDateType,
}
# Function argument and global names::
MESSAGE_ARGS_NAME = "message_args"
ERRORS_NAME = "errors"
MESSAGE_FUNCTION_ARGS = [MESSAGE_ARGS_NAME, ERRORS_NAME]
LOCALE_NAME = "locale"
PLURAL_FORM_FOR_NUMBER_NAME = "plural_form_for_number"
CLDR_PLURAL_FORMS = {
"zero",
"one",
"two",
"few",
"many",
"other",
}
PROPERTY_EXTERNAL_ARG = "PROPERTY_EXTERNAL_ARG"
@attr.s
class CurrentEnvironment:
# The parts of CompilerEnvironment that we want to mutate (and restore)
# temporarily for some parts of a call chain.
message_id = attr.ib(default=None)
ftl_resource = attr.ib(default=None)
term_args = attr.ib(default=None)
in_select_expression = attr.ib(default=False)
escaper = attr.ib(default=null_escaper)
@attr.s
class CompilerEnvironment:
locale = attr.ib()
plural_form_function = attr.ib()
use_isolating = attr.ib()
message_mapping = attr.ib(factory=dict)
errors = attr.ib(factory=list)
escapers = attr.ib(default=None)
functions = attr.ib(factory=dict)
function_renames = attr.ib(factory=dict)
functions_arg_spec = attr.ib(factory=dict)
message_ids_to_ast = attr.ib(factory=dict)
term_ids_to_ast = attr.ib(factory=dict)
current = attr.ib(factory=CurrentEnvironment)
def add_current_message_error(self, error):
self.errors.append((self.current.message_id, error))
def escaper_for_message(self, message_id=None):
return escaper_for_message(self.escapers, message_id=message_id)
@contextlib.contextmanager
def modified(self, **replacements):
"""
Context manager that modifies the 'current' attribute of the
environment, restoring the old data at the end.
"""
# CurrentEnvironment only has immutable args at the moment, so the
# shallow copy returned by attr.evolve is fine.
old_current = self.current
self.current = attr.evolve(old_current, **replacements)
yield self
self.current = old_current
def modified_for_term_reference(self, term_args=None):
return self.modified(term_args=term_args if term_args is not None else {})
class FtlSource:
"""
Object used to specify the origin of a chunk of FTL
"""
def __init__(self, ast_node, ftl_resource):
self.ast_node = ast_node
self.ftl_resource = ftl_resource
self.filename = self.ftl_resource.filename
self.row, self.column = span_to_position(ast_node.span, ftl_resource.text)
@attr.s
class CompiledFtl:
# A dictionary of message IDs to Python functions. This is the primary
# output that is needed to execute the FTL - the functions simply need to be
# called with a dictionary of external arguments, and a list to which
# runtime errors will be added.
message_functions = attr.ib(factory=dict)
# A list of parsing and compilation errors, where each item is
# (message_id or None, exception object)
errors = attr.ib(factory=list)
# Compiled output as Python AST.
module_ast = attr.ib(default=None)
locale = attr.ib(default=None)
def compile_messages(locale, resources, use_isolating=True, functions=None, escapers=None):
"""
Compile a list of FtlResource to a Python module,
and returns a CompiledFtl objects
"""
_functions = BUILTINS.copy()
if functions:
_functions.update(functions)
messages, parsing_issues = _parse_resources(resources)
babel_locale = babel.Locale.parse(locale.replace("-", "_"))
module, message_mapping, module_globals, compilation_errors = messages_to_module(
messages,
babel_locale,
use_isolating=use_isolating,
functions=_functions,
escapers=escapers,
)
# A hack below to allow `.ftl` files to appear in tracebacks, should that
# ever be needed, rather than '<string>' which is rather confusing.
# To do this, we split the module into multiple modules, to allow each
# function to have it's own filename associated with it, because the
# original FTL may come from different sources.
for module_ast in module.as_multiple_module_ast():
if hasattr(module_ast.body[0], "filename"):
filename = module_ast.body[0].filename
else:
filename = "<string>"
code_obj = compile(module_ast, filename, "exec")
exec(code_obj, module_globals)
message_functions = {}
for key, val in message_mapping.items():
if key.startswith(TERM_SIGIL):
# term, shouldn't be in publicly available messages
continue
message_functions[str(key)] = module_globals[val]
return CompiledFtl(
message_functions=message_functions,
errors=parsing_issues + compilation_errors,
module_ast=module.as_ast(),
locale=locale,
)
def _parse_resources(ftl_resources):
parsing_issues = []
output_dict = OrderedDict()
for ftl_resource in ftl_resources:
parser = FluentParser()
resource = parser.parse(ftl_resource.text)
for item in resource.body:
if isinstance(item, (Message, Term)):
full_id = ast_to_id(item)
if full_id in output_dict:
parsing_issues.append(
(
full_id,
FluentDuplicateMessageId(f"Additional definition for '{full_id}' discarded."),
)
)
else:
# Decorate with ftl_resource for better error messages later
item.ftl_resource = ftl_resource
for attribute in item.attributes:
attribute.ftl_resource = ftl_resource
output_dict[full_id] = item
elif isinstance(item, Junk):
parsing_issues.append(
(
None,
FluentJunkFound(
"Junk found:\n"
+ "\n".join(
" {}: {}".format(
display_location(
ftl_resource.filename,
span_to_position(a.span, ftl_resource.text),
),
a.message,
)
for a in item.annotations
),
item.annotations,
),
)
)
return output_dict, parsing_issues
def messages_to_module(messages, locale, use_isolating=True, functions=None, escapers=None):
"""
Compile a set of {id: Message/Term objects} to a Python module, returning a tuple:
(codegen.Module object, dictionary mapping message IDs to Python functions,
module globals dictionary, errors list)
"""
if functions is None:
functions = {}
message_ids_to_ast = OrderedDict(get_message_function_ast(messages))
term_ids_to_ast = OrderedDict(get_term_ast(messages))
# Plural form function
plural_form_for_number_main = babel.plural.to_python(locale.plural_form)
def plural_form_for_number(number):
try:
return plural_form_for_number_main(number)
except TypeError:
# This function can legitimately be passed strings if we incorrectly
# guessed it was a CLDR category. So we ignore silently
return None
function_arg_errors = []
compiler_env = CompilerEnvironment(
locale=locale,
plural_form_function=plural_form_for_number,
use_isolating=use_isolating,
functions=functions,
functions_arg_spec={
name: inspect_function_args(func, name, function_arg_errors) for name, func in functions.items()
},
message_ids_to_ast=message_ids_to_ast,
term_ids_to_ast=term_ids_to_ast,
)
for err in function_arg_errors:
compiler_env.add_current_message_error(err)
if escapers:
if len({e.name for e in escapers}) < len(escapers):
raise ValueError("Every escaper must have a unique 'name' attribute'")
compiler_env.escapers = [RegisteredEscaper(escaper, compiler_env) for escaper in escapers]
# Setup globals, and reserve names for them
module_globals = {k: getattr(runtime, k) for k in runtime.__all__}
module_globals.update(builtins.__dict__)
module_globals[LOCALE_NAME] = locale
# Return types of known functions.
known_return_types = {}
known_return_types.update(BUILTIN_RETURN_TYPES)
known_return_types.update(runtime.RETURN_TYPES)
module_globals[PLURAL_FORM_FOR_NUMBER_NAME] = plural_form_for_number
known_return_types[PLURAL_FORM_FOR_NUMBER_NAME] = str
def get_name_properties(name):
properties = {}
if name in known_return_types:
properties[codegen.PROPERTY_RETURN_TYPE] = known_return_types[name]
return properties
module = codegen.Module()
for k in module_globals:
name = module.scope.reserve_name(k, properties=get_name_properties(k), is_builtin=k in builtins.__dict__)
# We should have chosen all our module_globals to avoid name conflicts:
assert name == k, f"Expected {name}=={k}"
# Reserve names for escapers
if compiler_env.escapers is not None:
for escaper in compiler_env.escapers:
for name, func, properties in escaper.get_reserved_names_with_properties():
assigned_name = module.scope.reserve_name(name, properties=properties)
# We've chosen the names to not clash with anything that
# we've already set up.
assert assigned_name == name
assert assigned_name not in module_globals
module_globals[assigned_name] = func
# Reserve names for function arguments, so that we always
# know the name of these arguments without needing to do
# lookups etc.
for arg in MESSAGE_FUNCTION_ARGS:
module.scope.reserve_function_arg_name(arg)
# -- User defined names
# functions from context
for name, func in functions.items():
# These might clash, because we can't control what the user passed in,
# so we make a record in 'function_renames'
assigned_name = module.scope.reserve_name(name, properties=get_name_properties(name))
compiler_env.function_renames[name] = assigned_name
module_globals[assigned_name] = func
# Pass one, find all the names, so that we can populate message_mapping,
# which is needed for compilation.
for msg_id, msg in message_ids_to_ast.items():
escaper = compiler_env.escaper_for_message(message_id=msg_id)
function_name = module.scope.reserve_name(
suggested_function_name_for_msg_id(msg_id),
properties={codegen.PROPERTY_RETURN_TYPE: escaper.output_type},
)
compiler_env.message_mapping[msg_id] = function_name
# Pass 2, actual compilation
for msg_id, msg in message_ids_to_ast.items():
with compiler_env.modified(
message_id=msg_id,
ftl_resource=msg.ftl_resource,
escaper=compiler_env.escaper_for_message(message_id=msg_id),
):
function_name = compiler_env.message_mapping[msg_id]
function = compile_message(msg, msg_id, function_name, module, compiler_env)
module.add_function(function_name, function)
module = codegen.simplify(module, Simplifier(compiler_env))
return (module, compiler_env.message_mapping, module_globals, compiler_env.errors)
def get_message_function_ast(message_dict):
for msg_id, msg in message_dict.items():
if isinstance(msg, Term):
continue
if msg.value is not None: # has a body
yield (msg_id, msg)
for attribute in msg.attributes:
yield (attribute_ast_to_id(attribute, msg), attribute)
def get_term_ast(message_dict):
for term_id, term in message_dict.items():
if isinstance(term, Message):
pass
if term.value is not None: # has a body
yield (term_id, term)
for attribute in term.attributes:
yield (attribute_ast_to_id(attribute, term), attribute)
def suggested_function_name_for_msg_id(msg_id):
# Scope.reserve_name does further sanitising of name, which we don't need to
# worry about. It also ensures we don't get dupes. So the fact that this
# method will produce occasional collisions is not an issue - here we are
# aiming for an easy method than will produce nice obvious names (for the
# sake of tests) with a low chance of collision in the normal case (so that
# we don't hit worst cases in Scope.reserve_name for normal FTL files).
return msg_id.replace(ATTRIBUTE_SEPARATOR, "__").replace("-", "_")
def compile_message(msg, msg_id, function_name, module, compiler_env):
msg_func = codegen.Function(
parent_scope=module.scope,
name=function_name,
args=MESSAGE_FUNCTION_ARGS,
source=FtlSource(msg, compiler_env.current.ftl_resource),
)
function_block = msg_func.body
if contains_reference_cycle(msg, compiler_env):
error = FluentCyclicReferenceError(f"{display_ast_location(msg, compiler_env)}: Cyclic reference in {msg_id}")
add_static_msg_error(function_block, error)
compiler_env.add_current_message_error(error)
return_expression = finalize_expr_as_output_type(
make_fluent_none(None, module.scope), function_block, compiler_env
)
else:
return_expression = compile_expr(msg, function_block, compiler_env)
# > return $return_expression
msg_func.add_return(return_expression)
return msg_func
def traverse_ast(node, func, exclude_attributes=None):
"""
Postorder-traverse this node and apply `func` to all child nodes.
exclude_attributes is a list of (node type, attribute name) tuples
that should not be recursed into.
"""
def visit(value):
"""Call `func` on `value` and its descendants."""
if isinstance(value, BaseNode):
return traverse_ast(value, func, exclude_attributes=exclude_attributes)
if isinstance(value, list):
return func(list(map(visit, value)))
return func(value)
# Use all attributes found on the node
parts = vars(node).items()
for name, value in parts:
if exclude_attributes is not None and (type(node), name) in exclude_attributes:
continue
visit(value)
return func(node)
def contains_reference_cycle(msg, compiler_env):
"""
Returns True if the message 'msg' contains a cyclic reference,
in the context of the other messages provided in compiler_env
"""
# We traverse the AST starting from message, jumping to other messages and
# terms as necessary, and seeing if a path through the AST loops back to
# previously visited nodes at any point.
# This algorithm has some bugs compared to the runtime method in resolver.py
# For example, a pair of conditionally mutually recursive messages:
# foo = Foo { $arg ->
# [left] { bar }
# *[right] End
# }
# bar = Bar { $arg ->
# *[left] End
# [right] { foo }
# }
# These messages are rejected as containing cycles by this checker, when in
# fact they cannot go into an infinite loop.
# It is pretty difficult to come up with a compelling use case
# for this kind of thing though... so we are not too worried
# about fixing this bug, since we are erring on the conservative side.
message_ids_to_ast = compiler_env.message_ids_to_ast
term_ids_to_ast = compiler_env.term_ids_to_ast
# We exclude recursing into certain attributes, because we already cover
# these recursions explicitly by jumping to a subnode for the case of
# references.
exclude_attributes = [
# Message and Term attributes have already been loaded into the message_ids_to_ast dict,
(Message, "attributes"),
(Term, "attributes"),
# for speed
(Message, "comment"),
(Term, "comment"),
]
# We need to keep track of visited nodes. If we use just a single set for
# each top level message, then things like this would be rejected:
#
# message = { -term } { -term }
#
# because we would visit the term twice.
#
# So we have a stack of sets:
visited_node_stack = [set()]
# The top of this stack represents the set of nodes in the current path of
# visited nodes. We push a copy of the top set onto the stack when we
# traverse into a sub-node, and pop it off when we come back.
checks = []
def checker(node):
if isinstance(node, BaseNode):
node_id = id(node)
if node_id in visited_node_stack[-1]:
checks.append(True)
return
visited_node_stack[-1].add(node_id)
else:
return
# The logic below duplicates the logic that is used for 'jumping' to
# different nodes (messages via a runtime function call, terms via
# inlining), including the fallback strategies that are used.
sub_node = None
if isinstance(node, (MessageReference, TermReference)):
ref_id = reference_to_id(node)
if ref_id in message_ids_to_ast:
sub_node = message_ids_to_ast[ref_id]
elif ref_id in term_ids_to_ast:
sub_node = term_ids_to_ast[ref_id]
elif node.attribute:
# No match for attribute, but compiler falls back to parent ref
# in this situation, so we have to as well.
parent_ref_id = reference_to_id(node, ignore_attributes=True)
if parent_ref_id in message_ids_to_ast:
sub_node = message_ids_to_ast[parent_ref_id]
elif parent_ref_id in term_ids_to_ast:
sub_node = term_ids_to_ast[parent_ref_id]
if sub_node is not None:
visited_node_stack.append(visited_node_stack[-1].copy())
traverse_ast(sub_node, checker, exclude_attributes=exclude_attributes)
if any(checks):
return
visited_node_stack.pop()
return
traverse_ast(msg, checker, exclude_attributes=exclude_attributes)
return any(checks)
# ----------------- Begin 'compile_expr' implementation ---------------------
#
# The `compile_expr_XXXX functions` form the heart of handling all FTL syntax.
# They convert FTL AST nodes (as created by fluent.syntax parser)
# into Python expressions (in the form of our `codegen.PythonAst` objects).
#
# The first `compile_expr` function is decorated with `@singledispatch`,
# so we can then dispatch to other functions based on the type of the first
# argument. This is instead of a huge switch statement consisting of
# `if isinstance(ast, XXX): handle_XXX(...)`, or other similar visitor patterns.
#
# The basic structure is that each `compile_expr` returns a single
# codegen.PythonAst object that corresponds to the passed in FTL AST (the first
# argument). That is, the overall strategy is to compile each FTL AST object to
# a single Python expression.
#
# The simplest example is compile_expr_text, because we can simply convert an
# FTL string to a Python string.
#
# However, some FTL expressions cannot really be implemented in this way. For
# example, the "selectors" Fluent feature needs control structures. To support
# this, each `compile_expr` implementation may also modify the passed in
# `block`, which represents the block of Python code already built up.
#
# So, for example, `compile_expr_select_expression` adds an `if/elif/else`
# clause to the current block. This does the control flow we need, and each
# branch assigns to a temporary variable. The final returned expression is just
# that temporary variable as a VariableReference object. This allows us to stay
# within the paradigm of one FTL expression -> one Python expression - each
# `compile_expr` method still returns a single expression, but it may also
# mutate the passed in `block` in order to add the code needed to support that
# single expression.
#
# Other statements are also added to the block for other purposes e.g. error
# logging.
#
# The return value expressions will be used by code further up the chain, right
# back to the top level code creating the message function, which will use a
# single final expression as a return value.
#
# Example:
#
# foo = Foo
# bar = X { foo }
#
# These messages will be compiled to Python functions like these:
#
# def foo(message_args, errors):
# return 'Foo'
#
# def bar(message_args, errors):
# return f'X {foo(message_args, errors)}'
#
# Here:
#
# The function definitions and signatures:
# - come from `compile_message` function above
#
# `return `
# - comes from `compile_message` function above
#
# `Foo` and `'X '`
# - come from `compile_expr_text` below
#
# `foo(message_args, errors)`
# - comes from `compile_expr_message_reference` below
#
# f'' (f-string)
# - comes from `compile_expr_pattern` below
#
# For `bar` the call chain looks like this (with various intermediate calls
# omitted):
#
# compile_message
# -> compile_expr_pattern
# -> compile_expr_text
# -> compile_expr_message_reference
#
#
# Note that some of the codegen.PythonAst objects can simplify themselves as
# they are being built or finalised, and further transformations (i.e.
# simplifications and optimizations) are done after we've built up a complete
# Python AST for the function. So the easy one-to-one correspondence above will
# not always apply.
#
# Note also that many functions are complicated by the need for 'escaper'
# functions, which will be no-ops (and compile to nothing) if escapers
# are not in use for the message.
#
# In some functions we use comments starting with `>` to try to indicate
# generated code, with $ for interpolations (interpreted loosely)
@singledispatch
def compile_expr(element, block, compiler_env):
"""
Compiles a Fluent expression into a Python one, return
an object of type codegen.Expression.
This may also add statements into block, which is assumed
to be a function that returns a message, or a branch of that
function.
"""
raise NotImplementedError(f"Cannot handle object of type {type(element).__name__}")
@compile_expr.register(Message)
def compile_expr_message(message, block, compiler_env):
return compile_expr(message.value, block, compiler_env)
@compile_expr.register(Term)
def compile_expr_term(term, block, compiler_env):
return compile_expr(term.value, block, compiler_env)
@compile_expr.register(Attribute)
def compile_expr_attribute(attribute, block, compiler_env):
return compile_expr(attribute.value, block, compiler_env)
@compile_expr.register(Pattern)
def compile_expr_pattern(pattern, block, compiler_env):
parts = []
subelements = pattern.elements
use_isolating = compiler_env.use_isolating and len(subelements) > 1
for element in pattern.elements:
wrap_this_with_isolating = use_isolating and not isinstance(element, TextElement)
if wrap_this_with_isolating:
parts.append(wrap_with_escaper(codegen.String(FSI), block, compiler_env))
parts.append(compile_expr(element, block, compiler_env))
if wrap_this_with_isolating:
parts.append(wrap_with_escaper(codegen.String(PDI), block, compiler_env))
# > f'$[p for p in parts]'
return EscaperJoin.build(
[finalize_expr_as_output_type(p, block, compiler_env) for p in parts],
compiler_env.current.escaper,
block.scope,
)
@compile_expr.register(TextElement)
def compile_expr_text(text, block, compiler_env):
return wrap_with_mark_escaped(codegen.String(text.value), block, compiler_env)
@compile_expr.register(StringLiteral)
def compile_expr_string_expression(expr, block, compiler_env):
return codegen.String(expr.parse()["value"])
@compile_expr.register(NumberLiteral)
def compile_expr_number_expression(expr, block, compiler_env):
number_expr = codegen.Number(numeric_to_native(expr.value))
# > NUMBER($number_expr)
return codegen.FunctionCall(BUILTIN_NUMBER, [number_expr], {}, block.scope)
@compile_expr.register(Placeable)
def compile_expr_placeable(placeable, block, compiler_env):
return compile_expr(placeable.expression, block, compiler_env)
@compile_expr.register(MessageReference)
def compile_expr_message_reference(reference, block, compiler_env):
return handle_message_reference(reference, block, compiler_env)
def compile_term(term, block, compiler_env, new_escaper, term_args=None):
current_escaper = compiler_env.current.escaper
if not escapers_compatible(current_escaper, new_escaper):
term_id = ast_to_id(term)
error = TypeError(
"Escaper {} for term {} cannot be used from calling context with {} escaper".format(
new_escaper.name, term_id, current_escaper.name
)
)
add_static_msg_error(block, error)
compiler_env.add_current_message_error(error)
return make_fluent_none(term_id, block.scope)
else:
with compiler_env.modified(escaper=new_escaper):
with compiler_env.modified_for_term_reference(term_args=term_args):
return compile_expr(term.value, block, compiler_env)
@compile_expr.register(TermReference)
def compile_expr_term_reference(reference, block, compiler_env):
term, new_escaper, err_obj = lookup_term_reference(reference, block, compiler_env)
if term is None:
return err_obj
if reference.arguments:
args = [compile_expr(arg, block, compiler_env) for arg in reference.arguments.positional]
kwargs = {
kwarg.name.name: compile_expr(kwarg.value, block, compiler_env) for kwarg in reference.arguments.named
}
if args:
args_err = FluentFormatError(
"{}: Ignored positional arguments passed to term '{}'".format(
display_ast_location(reference.arguments, compiler_env),
reference_to_id(reference),
)
)
add_static_msg_error(block, args_err)
compiler_env.add_current_message_error(args_err)
else:
kwargs = None
return compile_term(term, block, compiler_env, new_escaper, term_args=kwargs)
@compile_expr.register(SelectExpression)
def compile_expr_select_expression(select_expr, block, compiler_env):
with compiler_env.modified(in_select_expression=True):
key_value = compile_expr(select_expr.selector, block, compiler_env)
static_retval = resolve_select_expression_statically(select_expr, key_value, block, compiler_env)
if static_retval is not None:
return static_retval
if_statement = codegen.If(block.scope, parent_block=block)
key_tmp_name = reserve_and_assign_name(block, "_key", key_value)
return_tmp_name = block.scope.reserve_name("_ret")
need_plural_form = any(is_cldr_plural_form_key(variant.key) for variant in select_expr.variants)
if need_plural_form:
plural_form_value = codegen.FunctionCall(
PLURAL_FORM_FOR_NUMBER_NAME,
[block.scope.variable(key_tmp_name)],
{},
block.scope,
)
# > $plural_form_tmp_name = plural_form_for_number($key_tmp_name)
plural_form_tmp_name = reserve_and_assign_name(block, "_plural_form", plural_form_value)
assigned_types = []
first = True
for variant in select_expr.variants:
if variant.default:
# This is the default, so gets chosen if nothing else matches, or
# there was no requested variant. Therefore we use the final 'else'
# block with no condition.
cur_block = if_statement.else_block
else:
# For cases like:
# { $arg ->
# [one] X
# [other] Y
# }
# we can't be sure whether $arg is a string, and the 'one' and 'other'
# keys are just strings, or whether $arg is a number and we need to
# do a plural category comparison. So we have to do both. We can use equality
# checks because they implicitly do a type check
# > $key_tmp_name == $variant.key
condition1 = codegen.Equals(
block.scope.variable(key_tmp_name),
compile_expr(variant.key, block, compiler_env),
)
if is_cldr_plural_form_key(variant.key):
# > $plural_form_tmp_name == $variant.key
condition2 = codegen.Equals(
block.scope.variable(plural_form_tmp_name),
compile_expr(variant.key, block, compiler_env),
)
condition = codegen.Or(condition1, condition2)
else:
condition = condition1
cur_block = if_statement.add_if(condition)
assigned_value = compile_expr(variant.value, cur_block, compiler_env)
cur_block.add_assignment(return_tmp_name, assigned_value, allow_multiple=not first)
first = False
assigned_types.append(assigned_value.type)
if assigned_types:
first_type = assigned_types[0]
if all(t == first_type for t in assigned_types):
block.scope.set_name_properties(return_tmp_name, {codegen.PROPERTY_TYPE: first_type})
block.add_statement(if_statement.finalize())
return block.scope.variable(return_tmp_name)
@compile_expr.register(Identifier)
def compile_expr_variant_name(name, block, compiler_env):
# TODO - handle numeric literals here?
return codegen.String(name.name)
@compile_expr.register(VariableReference)
def compile_expr_variable_reference(argument, block, compiler_env):
name = argument.id.name
if compiler_env.current.term_args is not None:
# We are in a term, all args are passed explicitly, not inherited from
# external args.
if name in compiler_env.current.term_args:
return compiler_env.current.term_args[name]
return make_fluent_none(name, block.scope)
# Otherwise we are in a message, lookup at runtime.
# We might have already looked it up:
existing = block.scope.find_names_by_property(PROPERTY_EXTERNAL_ARG, name)
# Name reservation is done at scope level. We also need to check that it has
# been defined in this block, or a parent block to this one.
if existing and block.has_assignment_for_name(existing[0]):
arg_tmp_name = existing[0]
else:
arg_tmp_name = block.scope.reserve_name("_arg", properties={PROPERTY_EXTERNAL_ARG: name})
# Arguments we get out of the args dictionary should be wrapped
# into 'native' Fluent types using `handle_argument`.
# Except, in a select expression, we only care about matching against a selector, so
# don't need to do this wrapping
wrap_with_handle_argument = not compiler_env.current.in_select_expression
if wrap_with_handle_argument:
arg_handled_tmp_name = block.scope.reserve_name("_arg_h")
# > $tmp_name = handle_argument_with_escaper($tmp_name, "$name", output_type, locale, errors)
# or
# > $tmp_name = handle_argument($tmp_name, "$name", locale, errors)
escaper = compiler_env.current.escaper
if escaper is null_escaper:
handle_argument_func_call = codegen.FunctionCall(
"handle_argument",
[
block.scope.variable(arg_tmp_name),
codegen.String(name),
block.scope.variable(LOCALE_NAME),
block.scope.variable(ERRORS_NAME),
],
{},
block.scope,
)
else:
handle_argument_func_call = codegen.FunctionCall(
"handle_argument_with_escaper",
[
block.scope.variable(arg_tmp_name),
codegen.String(name),
block.scope.variable(escaper.output_type_name()),
block.scope.variable(LOCALE_NAME),
block.scope.variable(ERRORS_NAME),
],
{},
block.scope,
)
if block.scope.has_assignment(arg_tmp_name): # already assigned to this, can re-use
if not wrap_with_handle_argument:
return block.variable(arg_tmp_name)
block.add_assignment(arg_handled_tmp_name, handle_argument_func_call)
return block.scope.variable(arg_handled_tmp_name)
# Add try/except/else to lookup variable.
try_except = codegen.Try(
[
block.scope.variable("LookupError"),
block.scope.variable("TypeError"), # for when args=None
],
block.scope,
)
block.add_statement(try_except)
# Try block
# > $arg_tmp_name = message_args[$name]
try_except.try_block.add_assignment(
arg_tmp_name,
codegen.DictLookup(block.scope.variable(MESSAGE_ARGS_NAME), codegen.String(name)),
)
# Except block
add_static_msg_error(
try_except.except_block,
FluentReferenceError(f"{display_ast_location(argument, compiler_env)}: Unknown external: {name}"),
)
# > $arg_tmp_name = FluentNone("$name")
try_except.except_block.add_assignment(arg_tmp_name, make_fluent_none(name, block.scope), allow_multiple=True)
if not wrap_with_handle_argument:
return block.scope.variable(arg_tmp_name)
# We can use except/else blocks to do wrapping.
# Except block:
# We don't want to add 'handle_argument' round FluentNone instances,
# it does the wrong thing.
# > $arg_handled_tmp_name = $arg_tmp_name
try_except.except_block.add_assignment(arg_handled_tmp_name, block.scope.variable(arg_tmp_name))
# else block:
# > $handled_tmp_name = handle_argument($arg_tmp_name, "$name", locale, errors)
try_except.else_block.add_assignment(arg_handled_tmp_name, handle_argument_func_call, allow_multiple=True)
return block.scope.variable(arg_handled_tmp_name)
@compile_expr.register(FunctionReference)
def compile_expr_function_reference(expr, block, compiler_env):
args = [compile_expr(arg, block, compiler_env) for arg in expr.arguments.positional]
kwargs = {kwarg.name.name: compile_expr(kwarg.value, block, compiler_env) for kwarg in expr.arguments.named}
# builtin or custom function
function_name = expr.id.name
if function_name in compiler_env.functions:
match, sanitized_args, sanitized_kwargs, errors = args_match(
function_name, args, kwargs, compiler_env.functions_arg_spec[function_name]
)
for error in errors:
add_static_msg_error(block, error)
compiler_env.add_current_message_error(error)
if match:
function_name_in_module = compiler_env.function_renames[function_name]
return codegen.FunctionCall(function_name_in_module, sanitized_args, sanitized_kwargs, block.scope)
return make_fluent_none(function_name + "()", block.scope)
error = FluentReferenceError(f"Unknown function: {function_name}")
add_static_msg_error(block, error)
compiler_env.add_current_message_error(error)
return make_fluent_none(function_name + "()", block.scope)
# if isinstance(expr.callee, (TermReference, AttributeExpression)):
# if args:
# args_err = FluentFormatError("Ignored positional arguments passed to term '{0}'"
# .format(reference_to_id(expr.callee)))
# add_static_msg_error(block, args_err)
# compiler_env.add_current_message_error(args_err)
# term, err = lookup_term_reference(expr.callee, block, compiler_env)
# if term is None:
# return err
# return compile_term(term, block, compiler_env, term_args=kwargs)
# End compile_expr implementations
# Compiler utilities and common code:
def add_msg_error_with_expr(block, exception_expr):
block.add_statement(codegen.MethodCall(block.scope.variable(ERRORS_NAME), "append", [exception_expr]))