1#!/usr/bin/env python
2# Copyright 2016 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6import math
7
8import json5_generator
9import template_expander
10import keyword_utils
11import bisect
12
13from blinkbuild.name_style_converter import NameStyleConverter
14from core.css import css_properties
15from core.style.computed_style_fields import DiffGroup, Enum, Group, Field
16
17from itertools import chain
18
19# Heuristic ordering of types from largest to smallest, used to sort fields by
20# their alignment sizes.
21# Specifying the exact alignment sizes for each type is impossible because it's
22# platform specific, so we define an ordering instead.
23# The ordering comes from the data obtained in:
24# https://codereview.chromium.org/2841413002
25# FIXME: Put alignment sizes into code form, rather than linking to a CL
26# which may disappear.
27ALIGNMENT_ORDER = [
28    # Aligns like double
29    'ScaleTransformOperation',
30    'RotateTransformOperation',
31    'TranslateTransformOperation',
32    'base::Optional<IntSize>',
33    'double',
34    # Aligns like a pointer (can be 32 or 64 bits)
35    'NamedGridLinesMap',
36    'OrderedNamedGridLines',
37    'NamedGridAreaMap',
38    'TransformOperations',
39    'Vector<CSSPropertyID>',
40    'Vector<GridTrackSize>',
41    'Vector<AtomicString>',
42    'GridPosition',
43    'GapLength',
44    'AtomicString',
45    'scoped_refptr',
46    'Persistent',
47    'std::unique_ptr',
48    'Vector<String>',
49    'Font',
50    'FillLayer',
51    'NinePieceImage',
52    'IntrinsicLength',
53    # Aligns like float
54    'StyleOffsetRotation',
55    'TransformOrigin',
56    'ScrollPadding',
57    'ScrollMargin',
58    'LengthBox',
59    'LengthSize',
60    'FloatSize',
61    'LengthPoint',
62    'Length',
63    'TextSizeAdjust',
64    'TabSize',
65    'float',
66    # Aligns like int
67    'cc::ScrollSnapType',
68    'cc::ScrollSnapAlign',
69    'BorderValue',
70    'StyleColor',
71    'Color',
72    'LayoutUnit',
73    'LineClampValue',
74    'OutlineValue',
75    'unsigned',
76    'size_t',
77    'int',
78    # Aligns like short
79    'unsigned short',
80    'short',
81    # Aligns like char
82    'StyleSelfAlignmentData',
83    'StyleContentAlignmentData',
84    'uint8_t',
85    'char',
86    # Aligns like bool
87    'bool'
88]
89
90# FIXME: Improve documentation and add docstrings.
91
92
93def _flatten_list(x):
94    """Flattens a list of lists into a single list."""
95    return list(chain.from_iterable(x))
96
97
98def _get_include_paths(properties):
99    """
100    Get a list of paths that need to be included for ComputedStyleBase.
101    """
102    include_paths = set()
103    for property_ in properties:
104        include_paths.update(property_['include_paths'])
105    return list(sorted(include_paths))
106
107
108def _create_groups(properties):
109    """Create a tree of groups from a list of properties.
110
111    Returns:
112        Group: The root group of the tree. The name of the group is set to None.
113    """
114
115    # We first convert properties into a dictionary structure. Each dictionary
116    # represents a group. The None key corresponds to the fields directly stored
117    # on that group. The other keys map from group name to another dictionary.
118    # For example:
119    # {
120    #   None: [field1, field2, ...]
121    #   'groupA': { None: [field3] },
122    #   'groupB': {
123    #      None: [],
124    #      'groupC': { None: [field4] },
125    #   },
126    # }
127    #
128    # We then recursively convert this dictionary into a tree of Groups.
129    # FIXME: Skip the first step by changing Group attributes to methods.
130    def _dict_to_group(name, group_dict):
131        fields_in_current_group = group_dict.pop(None)
132        subgroups = [
133            _dict_to_group(subgroup_name, subgroup_dict)
134            for subgroup_name, subgroup_dict in group_dict.items()
135        ]
136        return Group(name, subgroups, _reorder_fields(fields_in_current_group))
137
138    root_group_dict = {None: []}
139    for property_ in properties:
140        current_group_dict = root_group_dict
141        if property_['field_group']:
142            for group_name in property_['field_group'].split('->'):
143                current_group_dict[group_name] = current_group_dict.get(
144                    group_name, {None: []})
145                current_group_dict = current_group_dict[group_name]
146        current_group_dict[None].extend(_create_fields(property_))
147
148    return _dict_to_group(None, root_group_dict)
149
150
151def _create_diff_groups_map(diff_function_inputs, root_group):
152    diff_functions_map = {}
153
154    for entry in diff_function_inputs:
155        # error handling
156        field_names = entry['fields_to_diff'] + _list_field_dependencies(
157            entry['methods_to_diff'] + entry['predicates_to_test'])
158        for name in field_names:
159            assert name in [
160                field.property_name for field in root_group.all_fields], \
161                "The field '{}' isn't a defined field on ComputedStyle. " \
162                "Please check that there's an entry for '{}' in" \
163                "css_properties.json5 or " \
164                "computed_style_extra_fields.json5".format(name, name)
165        diff_functions_map[entry['name'].original] = _create_diff_groups(
166            entry['fields_to_diff'], entry['methods_to_diff'],
167            entry['predicates_to_test'], root_group)
168    return diff_functions_map
169
170
171def _list_field_dependencies(entries_with_field_dependencies):
172    field_dependencies = []
173    for entry in entries_with_field_dependencies:
174        field_dependencies += entry['field_dependencies']
175    return field_dependencies
176
177
178def _create_diff_groups(fields_to_diff, methods_to_diff, predicates_to_test,
179                        root_group):
180    diff_group = DiffGroup(root_group)
181    field_dependencies = _list_field_dependencies(methods_to_diff +
182                                                  predicates_to_test)
183    for subgroup in root_group.subgroups:
184        if any(
185                field.property_name in (fields_to_diff + field_dependencies)
186                for field in subgroup.all_fields):
187            diff_group.subgroups.append(
188                _create_diff_groups(fields_to_diff, methods_to_diff,
189                                    predicates_to_test, subgroup))
190    for entry in fields_to_diff:
191        for field in root_group.fields:
192            if not field.is_inherited_flag and entry == field.property_name:
193                diff_group.fields.append(field)
194    for entry in methods_to_diff:
195        for field in root_group.fields:
196            if (not field.is_inherited_flag
197                    and field.property_name in entry['field_dependencies']
198                    and entry['method'] not in diff_group.expressions):
199                diff_group.expressions.append(entry['method'])
200    for entry in predicates_to_test:
201        for field in root_group.fields:
202            if (not field.is_inherited_flag
203                    and field.property_name in entry['field_dependencies']
204                    and entry['predicate'] not in diff_group.predicates):
205                diff_group.predicates.append(entry['predicate'])
206    return diff_group
207
208
209def _create_enums(properties):
210    """Returns a list of Enums to be generated"""
211    enums = {}
212    for property_ in properties:
213        # Only generate enums for keyword properties that do not
214        # require includes.
215        if (property_['field_template'] in ('keyword', 'multi_keyword')
216                and len(property_['include_paths']) == 0):
217            enum = Enum(
218                property_['type_name'],
219                property_['keywords'],
220                is_set=(property_['field_template'] == 'multi_keyword'))
221            if property_['field_template'] == 'multi_keyword':
222                assert property_['keywords'][0] == 'none', \
223                    "First keyword in a 'multi_keyword' field must be " \
224                    "'none' in '{}'.".format(property_['name'])
225
226            if enum.type_name in enums:
227                # There's an enum with the same name, check if the enum
228                # values are the same
229                assert set(enums[enum.type_name].values) == set(enum.values), \
230                    "'{}' can't have type_name '{}' because it was used by " \
231                    "a previous property, but with a different set of " \
232                    "keywords. Either give it a different name or ensure " \
233                    "the keywords are the same.".format(
234                        property_['name'], enum.type_name)
235            else:
236                enums[enum.type_name] = enum
237
238    # Return the enums sorted by type name
239    return list(sorted(enums.values(), key=lambda e: e.type_name))
240
241
242def _create_property_field(property_):
243    """
244    Create a property field.
245    """
246    name_for_methods = property_['name_for_methods']
247
248    assert property_['default_value'] is not None, \
249        'MakeComputedStyleBase requires an default value for all fields, ' \
250        'none specified for property ' + property_['name']
251
252    type_name = property_['type_name']
253    if property_['field_template'] == 'keyword':
254        assert property_['field_size'] is None, \
255            ("'" + property_['name'] + "' is a keyword field, "
256             "so it should not specify a field_size")
257        size = int(math.ceil(math.log(len(property_['keywords']), 2)))
258    elif property_['field_template'] == 'multi_keyword':
259        size = len(property_['keywords']) - 1  # Subtract 1 for 'none' keyword
260    elif property_['field_template'] == 'external':
261        size = None
262    elif property_['field_template'] == 'primitive':
263        # pack bools with 1 bit.
264        size = 1 if type_name == 'bool' else property_["field_size"]
265    elif property_['field_template'] == 'pointer':
266        size = None
267    else:
268        assert property_['field_template'] == 'monotonic_flag', \
269            "Please use a valid value for field_template"
270        size = 1
271
272    return Field(
273        'property',
274        name_for_methods,
275        property_name=property_['name'].original,
276        inherited=property_['inherited'],
277        independent=property_['independent'],
278        type_name=property_['type_name'],
279        wrapper_pointer_name=property_['wrapper_pointer_name'],
280        field_template=property_['field_template'],
281        size=size,
282        default_value=property_['default_value'],
283        custom_copy=property_['custom_copy'],
284        custom_compare=property_['custom_compare'],
285        mutable=property_['mutable'],
286        getter_method_name=property_['getter'],
287        setter_method_name=property_['setter'],
288        initial_method_name=property_['initial'],
289        computed_style_custom_functions=property_[
290            'computed_style_custom_functions'],
291    )
292
293
294def _create_inherited_flag_field(property_):
295    """
296    Create the field used for an inheritance fast path from an independent CSS
297    property, and return the Field object.
298    """
299    name_for_methods = NameStyleConverter(
300        property_['name_for_methods']).to_function_name(
301            suffix=['is', 'inherited'])
302    name_source = NameStyleConverter(name_for_methods)
303    return Field(
304        'inherited_flag',
305        name_for_methods,
306        property_name=property_['name'].original,
307        type_name='bool',
308        wrapper_pointer_name=None,
309        field_template='primitive',
310        size=1,
311        default_value='true',
312        custom_copy=False,
313        custom_compare=False,
314        mutable=False,
315        getter_method_name=name_source.to_function_name(),
316        setter_method_name=name_source.to_function_name(prefix='set'),
317        initial_method_name=name_source.to_function_name(prefix='initial'),
318        computed_style_custom_functions=property_[
319            "computed_style_custom_functions"],
320    )
321
322
323def _create_fields(property_):
324    """
325    Create ComputedStyle fields from a property and return a list of Fields.
326    """
327    fields = []
328    # Only generate properties that have a field template
329    if property_['field_template'] is not None:
330        # If the property is independent, add the single-bit sized isInherited
331        # flag to the list of Fields as well.
332        if property_['independent']:
333            fields.append(_create_inherited_flag_field(property_))
334
335        fields.append(_create_property_field(property_))
336
337    return fields
338
339
340def _reorder_bit_fields(bit_fields):
341    # Since fields cannot cross word boundaries, in order to minimize
342    # padding, group fields into buckets so that as many buckets as possible
343    # are exactly 32 bits. Although this greedy approach may not always
344    # produce the optimal solution, we add a static_assert to the code to
345    # ensure ComputedStyleBase results in the expected size. If that
346    # static_assert fails, this code is falling into the small number of
347    # cases that are suboptimal, and may need to be rethought.
348    # For more details on packing bit fields to reduce padding, see:
349    # http://www.catb.org/esr/structure-packing/#_bitfields
350    field_buckets = []
351    # Consider fields in descending order of size to reduce fragmentation
352    # when they are selected. Ties broken in alphabetical order by name.
353    for field in sorted(bit_fields, key=lambda f: (-f.size, f.name)):
354        added_to_bucket = False
355        # Go through each bucket and add this field if it will not increase
356        # the bucket's size to larger than 32 bits. Otherwise, make a new
357        # bucket containing only this field.
358        for bucket in field_buckets:
359            if sum(f.size for f in bucket) + field.size <= 32:
360                bucket.append(field)
361                added_to_bucket = True
362                break
363        if not added_to_bucket:
364            field_buckets.append([field])
365
366    return _flatten_list(field_buckets)
367
368
369def _reorder_non_bit_fields(non_bit_fields):
370    # A general rule of thumb is to sort members by their alignment requirement
371    # (from biggest aligned to smallest).
372    for field in non_bit_fields:
373        assert field.alignment_type in ALIGNMENT_ORDER, \
374            "Type {} has unknown alignment. Please update ALIGNMENT_ORDER " \
375            "to include it.".format(field.name)
376    return list(
377        sorted(
378            non_bit_fields,
379            key=lambda f: ALIGNMENT_ORDER.index(f.alignment_type)))
380
381
382def _reorder_fields(fields):
383    """
384    Returns a list of fields ordered to minimise padding.
385    """
386    # Separate out bit fields from non bit fields
387    bit_fields = [field for field in fields if field.is_bit_field]
388    non_bit_fields = [field for field in fields if not field.is_bit_field]
389
390    # Non bit fields go first, then the bit fields.
391    return _reorder_non_bit_fields(non_bit_fields) + _reorder_bit_fields(
392        bit_fields)
393
394
395def _get_properties_ranking_using_partition_rule(properties_ranking,
396                                                 partition_rule):
397    """Take the contents of the properties ranking file and produce a dictionary
398    of css properties with their group number based on the partition_rule
399
400    Args:
401        properties_ranking: rankings map as read from CSSPropertyRanking.json5
402        partition_rule: cumulative distribution over properties_ranking
403
404    Returns:
405        dictionary with keys are css properties' name values are the group
406        that each css properties belong to. Smaller group number is higher
407        popularity in the ranking.
408    """
409    return dict(
410        zip(properties_ranking, [
411            bisect.bisect_left(partition_rule,
412                               float(i) / len(properties_ranking)) + 1
413            for i in range(len(properties_ranking))
414        ]))
415
416
417def _best_rank(prop, ranking_map):
418    """Return the best ranking value for the specified property.
419
420    This function collects ranking values for not only the property's real name
421    but also its aliases, and returns the best (lower is better) value.
422    If no ranking values for the property is available, this returns -1.
423    """
424    worst_rank = max(ranking_map.values()) + 1
425    best_rank = ranking_map.get(prop["name"].original, worst_rank)
426
427    for alias_name in prop.get("aliases", []):
428        best_rank = min(best_rank, ranking_map.get(alias_name, worst_rank))
429    return best_rank if best_rank != worst_rank else -1
430
431
432def _evaluate_rare_non_inherited_group(properties,
433                                       properties_ranking,
434                                       num_layers,
435                                       partition_rule=None):
436    """Re-evaluate the grouping of RareNonInherited groups based on each
437    property's popularity.
438
439    Args:
440        properties: list of all css properties
441        properties_ranking: map of property rankings
442        num_layers: the number of group to split
443        partition_rule: cumulative distribution over properties_ranking
444                        Ex: [0.3, 0.6, 1]
445    """
446    if partition_rule is None:
447        partition_rule = [
448            1.0 * (i + 1) / num_layers for i in range(num_layers)
449        ]
450
451    assert num_layers == len(partition_rule), \
452        "Length of rule and num_layers mismatch"
453
454    layers_name = [
455        "rare-non-inherited-usage-less-than-{}-percent".format(
456            int(round(partition_rule[i] * 100))) for i in range(num_layers)
457    ]
458    properties_ranking = _get_properties_ranking_using_partition_rule(
459        properties_ranking, partition_rule)
460
461    for property_ in properties:
462        rank = _best_rank(property_, properties_ranking)
463        if (property_["field_group"] is not None
464                and "*" in property_["field_group"]
465                and not property_["inherited"] and rank >= 0):
466
467            assert property_["field_group"] == "*", \
468                "The property {}  will be automatically assigned a group, " \
469                "please put '*' as the field_group".format(property_['name'])
470
471            property_["field_group"] = "->".join(layers_name[0:rank])
472        elif (property_["field_group"] is not None
473              and "*" in property_["field_group"]
474              and not property_["inherited"] and rank < 0):
475            group_tree = property_["field_group"].split("->")[1:]
476            group_tree = [layers_name[0], layers_name[0] + "-sub"] + group_tree
477            property_["field_group"] = "->".join(group_tree)
478
479
480def _evaluate_rare_inherit_group(properties,
481                                 properties_ranking,
482                                 num_layers,
483                                 partition_rule=None):
484    """Re-evaluate the grouping of RareInherited groups based on each property's
485    popularity.
486
487    Args:
488        properties: list of all css properties
489        properties_ranking: map of property rankings
490        num_layers: the number of group to split
491        partition_rule: cumulative distribution over properties_ranking
492                        Ex: [0.4, 1]
493    """
494    if partition_rule is None:
495        partition_rule = [
496            1.0 * (i + 1) / num_layers for i in range(num_layers)
497        ]
498
499    assert num_layers == len(partition_rule), \
500        "Length of rule and num_layers mismatch"
501
502    layers_name = [
503        "rare-inherited-usage-less-than-{}-percent".format(
504            int(round(partition_rule[i] * 100))) for i in range(num_layers)
505    ]
506
507    properties_ranking = _get_properties_ranking_using_partition_rule(
508        properties_ranking, partition_rule)
509
510    for property_ in properties:
511        rank = _best_rank(property_, properties_ranking)
512        if (property_["field_group"] is not None
513                and "*" in property_["field_group"] and property_["inherited"]
514                and rank >= 0):
515            property_["field_group"] = "->".join(layers_name[0:rank])
516        elif (property_["field_group"] is not None
517              and "*" in property_["field_group"] and property_["inherited"]
518              and rank < 0):
519            group_tree = property_["field_group"].split("->")[1:]
520            group_tree = [layers_name[0], layers_name[0] + "-sub"] + group_tree
521            property_["field_group"] = "->".join(group_tree)
522
523
524class ComputedStyleBaseWriter(json5_generator.Writer):
525    def __init__(self, json5_file_paths, output_dir):
526        super(ComputedStyleBaseWriter, self).__init__([], output_dir)
527
528        self._input_files = json5_file_paths
529
530        # Reads css_properties.json5, computed_style_field_aliases.json5,
531        # runtime_enabled_features.json5 and computed_style_extra_fields.json5
532        self._css_properties = css_properties.CSSProperties(
533            json5_file_paths[0:4])
534
535        # We sort the enum values based on each value's position in
536        # the keywords as listed in css_properties.json5. This will ensure that
537        # if there is a continuous
538        # segment in css_properties.json5 matching the segment in this enum then
539        # the generated enum will have the same order and continuity as
540        # css_properties.json5 and we can get the longest continuous segment.
541        # Thereby reduce the switch case statement to the minimum.
542        properties = keyword_utils.sort_keyword_properties_by_canonical_order(
543            self._css_properties.longhands, json5_file_paths[5],
544            self.default_parameters)
545        self._properties = properties + self._css_properties.extra_fields
546
547        self._generated_enums = _create_enums(self._properties)
548
549        # Organise fields into a tree structure where the root group
550        # is ComputedStyleBase.
551        group_parameters = dict(
552            [(conf["name"], conf["cumulative_distribution"])
553             for conf in json5_generator.Json5File.load_from_files(
554                 [json5_file_paths[7]]).name_dictionaries])
555
556        properties_ranking = [
557            x["name"].original for x in json5_generator.Json5File.
558            load_from_files([json5_file_paths[6]]).name_dictionaries
559        ]
560        _evaluate_rare_non_inherited_group(
561            self._properties, properties_ranking,
562            len(group_parameters["rare_non_inherited_properties_rule"]),
563            group_parameters["rare_non_inherited_properties_rule"])
564        _evaluate_rare_inherit_group(
565            self._properties, properties_ranking,
566            len(group_parameters["rare_inherited_properties_rule"]),
567            group_parameters["rare_inherited_properties_rule"])
568        self._root_group = _create_groups(self._properties)
569        self._diff_functions_map = _create_diff_groups_map(
570            json5_generator.Json5File.load_from_files(
571                [json5_file_paths[4]]).name_dictionaries, self._root_group)
572
573        self._include_paths = _get_include_paths(self._properties)
574        self._outputs = {
575            'computed_style_base.h':
576            self.generate_base_computed_style_h,
577            'computed_style_base.cc':
578            self.generate_base_computed_style_cpp,
579            'computed_style_base_constants.h':
580            self.generate_base_computed_style_constants,
581        }
582
583    @template_expander.use_jinja(
584        'core/style/templates/computed_style_base.h.tmpl',
585        tests={
586            'in': lambda a, b: a in b
587        })
588    def generate_base_computed_style_h(self):
589        return {
590            'input_files': self._input_files,
591            'properties': self._properties,
592            'enums': self._generated_enums,
593            'include_paths': self._include_paths,
594            'computed_style': self._root_group,
595            'diff_functions_map': self._diff_functions_map,
596        }
597
598    @template_expander.use_jinja(
599        'core/style/templates/computed_style_base.cc.tmpl',
600        tests={
601            'in': lambda a, b: a in b
602        })
603    def generate_base_computed_style_cpp(self):
604        return {
605            'input_files': self._input_files,
606            'properties': self._properties,
607            'enums': self._generated_enums,
608            'include_paths': self._include_paths,
609            'computed_style': self._root_group,
610            'diff_functions_map': self._diff_functions_map,
611        }
612
613    @template_expander.use_jinja(
614        'core/style/templates/computed_style_base_constants.h.tmpl')
615    def generate_base_computed_style_constants(self):
616        return {
617            'input_files': self._input_files,
618            'properties': self._properties,
619            'enums': self._generated_enums,
620        }
621
622
623if __name__ == '__main__':
624    json5_generator.Maker(ComputedStyleBaseWriter).main()
625