Merge pull request #9812 from iNavFlight/dzikuvx-fix-ez-tune-cli
[inav.git] / src / utils / settings.rb
blobbbc76f9ebd5ef21a6992d89c873b713715e1ea00
1 #!/usr/bin/env ruby
3 # This file is part of INAV.
5 # author: Alberto Garcia Hierro <alberto@garciahierro.com>
7 # This Source Code Form is subject to the terms of the Mozilla Public
8 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
9 # You can obtain one at http://mozilla.org/MPL/2.0/.
11 # Alternatively, the contents of this file may be used under the terms
12 # of the GNU General Public License Version 3, as described below:
14 # This file is free software: you may copy, redistribute and/or modify
15 # it under the terms of the GNU General Public License as published by the
16 # Free Software Foundation, either version 3 of the License, or (at your
17 # option) any later version.
19 # This file is distributed in the hope that it will be useful, but
20 # WITHOUT ANY WARRANTY; without even the implied warranty of
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
22 # Public License for more details.
24 # You should have received a copy of the GNU General Public License
25 # along with this program. If not, see http://www.gnu.org/licenses/.
27 require 'fileutils'
28 require 'getoptlong'
29 require 'json'
30 require 'set'
31 require 'stringio'
32 require 'tmpdir'
33 require 'yaml'
35 require_relative 'compiler'
37 DEBUG = false
38 INFO = false
40 SETTINGS_WORDS_BITS_PER_CHAR = 5
42 def dputs(s)
43     puts s if DEBUG
44 end
46 def lputs(s)
47     puts if DEBUG or INFO
48 end
50 class Object
51     def is_number_kind?
52         self.kind_of?(Integer) || self.kind_of?(Float)
53     end
54 end
56 class String
57     def is_number?
58       true if Float(self) rescue false
59     end
60 end
62 class StringIO
63     def write_byte(b)
64         self << [b].pack("C*")
65     end
67     def write_uvarint(x)
68         while x >= 0x80
69             write_byte((x & 0xFF) | 0x80)
70             x >>= 7
71         end
72         write_byte(x)
73     end
75     def to_carr
76         return string.bytes.to_s.sub('[', '{').sub(']', '}')
77     end
78 end
80 class NameEncoder
81     attr_reader :max_length
82     attr_reader :max_word_length
84     def initialize(names, max_length)
85         @names = names
86         @max_length = max_length
87         # Key is word, value is number of uses
88         @words = Hash.new(0)
89         # Most used words first
90         @words_by_usage = []
91         # Words that shouldn't be split because
92         # their encoding is too long
93         @non_split = Set.new
94         # Key is the name, value is its encoding
95         @encoded = Hash.new
97         @max_word_length = 0;
99         update_words
100         encode_names
101     end
103     def uses_byte_indexing
104         @words.length < 255
105     end
107     def words
108         @words_by_usage
109     end
111     def estimated_size(settings_count)
112         size = 0
113         @max_word_length = 0
114         @words.each do |word, count|
115             size += (word.length + 1) * (5/8.0)
116             if word.length > @max_word_length
117                 @max_word_length = word.length
118             end
119         end
120         return size.to_i + @max_length * settings_count
121     end
123     def format_encoded_name(name)
124         encoded = @encoded[name]
125         raise "Name #{name} was not encoded" if encoded == nil
126         return encoded.to_carr
127     end
129     private
130     def split_words(name)
131         if @non_split.include?(name)
132             return [name]
133         end
134         return name.split('_')
135     end
137     def update_words
138         @words.clear
139         @names.each do |name|
140             split_words(name).each do |word|
141                 @words[word] += 1
142             end
143         end
144         # Sort by usage, then alphabetically
145         @words_by_usage = @words.keys().sort do |x, y|
146             ux = @words[x]
147             uy = @words[y]
148             if ux != uy
149                 uy <=> ux
150             else
151                 x <=> y
152             end
153         end
154     end
156     def encode_names
157         @encoded.clear()
158         @names.each do |name|
159             buf = StringIO.new
160             split_words(name).each do |word|
161                 pos = @words_by_usage.find_index(word)
162                 raise "Word #{word} not found in words array" if pos == nil
163                 # Zero indicates end of words, first word in
164                 # the array starts at 1 ([0] is NULL).
165                 p = pos + 1
166                 if uses_byte_indexing
167                     buf.write_byte(p)
168                 else
169                     buf.write_uvarint(p)
170                 end
171             end
172             if buf.length > @max_length
173                 # TODO: print in verbose mode
174                 # fmt.Printf("encoding %q took %d bytes (>%d), adding it as single word\n", v, len(data), e.MaxEncodedLength)
175                 @non_split << name
176                 update_words
177                 return encode_names
178             end
179             while buf.length < @max_length
180                 buf.write_byte(0)
181             end
182             @encoded[name] = buf
183         end
184     end
187 class ValueEncoder
188     attr_reader :values
190     def initialize(values, constants)
191         min = 0
192         max = 0
194         valuesHash = Hash.new(0)
195         values.each do |v|
196             min = [min, v].min
197             max = [max, v].max
198             valuesHash[v] += 1
199         end
201         # Sorted by usage, most used first
202         @values = valuesHash.keys().sort do |x, y|
203             ux = valuesHash[x]
204             uy = valuesHash[y]
205             if ux != uy
206                 uy <=> ux
207             else
208                 x <=> y
209             end
210         end
212         @constants = constants
214         @min = min
215         @max = max
216     end
218     def min_type
219         [8, 16, 32].each do |x|
220             if @min >= -(2**(x-1))
221                 return "int#{x}_t"
222             end
223         end
224         raise "cannot represent minimum value #{@min} with int32_t"
225     end
227     def max_type
228         [8, 16, 32].each do |x|
229             if @max < 2**x
230                 return "uint#{x}_t"
231             end
232         end
233         raise "cannot represent maximum value #{@max} with uint32_t"
234     end
236     def index_bytes
237         bits = Math.log2(@values.length).ceil
238         bytes = (bits / 8.0).ceil.to_i
239         if bytes > 1
240             raise "too many bytes required for value index: #{bytes}"
241         end
242         return bytes
243     end
245     def encode_values(min, max)
246         buf = StringIO.new
247         encode_value(buf, min)
248         encode_value(buf, max)
249         return buf.to_carr
250     end
252     def resolve_value(val)
253         v = val || 0
254         if !v.is_number_kind?
255             v = @constants[val]
256             if v == nil
257                 raise "Could not resolve constant #{val}"
258             end
259         end
260         return v
261     end
263     private
264     def encode_value(buf, val)
265         v = resolve_value(val)
266         pos = @values.find_index(v)
267         if pos < 0
268             raise "Could not encode value not in array #{v}"
269         end
270         buf.write_byte(pos)
271     end
274 OFF_ON_TABLE = Hash["name" => "off_on", "values" => ["OFF", "ON"]]
276 class Generator
277     def initialize(src_root, settings_file, output_dir, use_host_gcc)
278         @src_root = src_root
279         @settings_file = settings_file
280         @output_dir = output_dir || File.dirname(settings_file)
282         @compiler = Compiler.new(use_host_gcc)
284         @count = 0
285         @max_name_length = 0
286         @tables = Hash.new
287         @used_tables = Set.new
288         @enabled_tables = Set.new
289     end
291     def write_files
293         # Remove the old files first, if any
294         [header_file, impl_file].each do |file|
295             if File.file?(file)
296                 File.delete(file)
297             end
298         end
300         load_data
302         check_member_default_values_presence
303         sanitize_fields
304         resolv_min_max_and_default_values_if_possible
305         initialize_name_encoder
306         initialize_value_encoder
307         validate_default_values
309         write_header_file(header_file)
310         write_impl_file(impl_file)
311     end
313     def write_json(jsonFile)
314         load_data
315         sanitize_fields(true)
317         settings = Hash.new
319         foreach_member do |group, member|
320             name = member["name"]
321             s = {
322                 "type" => member["type"],
323             }
324             table = member["table"]
325             if table
326                 s["table"] = @tables[table]
327             end
328             settings[name] = s
329         end
331         File.open(jsonFile, "w") do |f|
332             f.write(JSON.pretty_generate(settings))
333         end
334     end
336     def print_stats
337         puts "#{@count} settings"
338         puts "words table has #{@name_encoder.words.length} words"
339         word_idx = @name_encoder.uses_byte_indexing ? "byte" : "uvarint"
340         puts "name encoder uses #{word_idx} word indexing"
341         puts "each setting name uses #{@name_encoder.max_length} bytes"
342         puts "#{@name_encoder.estimated_size(@count)} bytes estimated for setting name storage"
343         values_size = @value_encoder.values.length * 4
344         puts "min/max value storage uses #{values_size} bytes"
345         value_idx_size = @value_encoder.index_bytes * 2
346         value_idx_total = value_idx_size * @count
347         puts "value indexing uses #{value_idx_size} per setting, #{value_idx_total} bytes total"
348         puts "#{value_idx_size+value_idx_total} bytes estimated for value+indexes storage"
350         buf = StringIO.new
351         buf << "#include \"fc/settings.h\"\n"
352         buf << "char (*dummy)[sizeof(setting_t)] = 1;\n"
353         stderr = compile_test_file(buf)
354         puts "sizeof(setting_t) = #{/char \(\*\)\[(\d+)\]/.match(stderr)[1]}"
355     end
357     private
359     def load_data
360         @data = YAML.load_file(@settings_file)
362         initialize_tables
363         initialize_constants
364         check_conditions
365     end
367     def header_file
368         File.join(@output_dir, "settings_generated.h")
369     end
371     def impl_file
372         File.join(@output_dir, "settings_generated.c")
373     end
375     def write_file_header(buf)
376         buf << "// This file has been automatically generated by utils/settings.rb\n"
377         buf << "// Don't make any modifications to it. They will be lost.\n\n"
378     end
380     def write_header_file(file)
381         buf = StringIO.new
382         write_file_header(buf)
383         buf << "#pragma once\n"
384         # Write setting_t size constants
385         buf << "#define SETTING_MAX_NAME_LENGTH #{@max_name_length+1}\n" # +1 for the terminating '\0'
386         buf << "#define SETTING_MAX_WORD_LENGTH #{@name_encoder.max_word_length+1}\n" # +1 for the terminating '\0'
387         buf << "#define SETTING_ENCODED_NAME_MAX_BYTES #{@name_encoder.max_length}\n"
388         if @name_encoder.uses_byte_indexing
389             buf << "#define SETTING_ENCODED_NAME_USES_BYTE_INDEXING\n"
390         end
391         buf << "#define SETTINGS_WORDS_BITS_PER_CHAR #{SETTINGS_WORDS_BITS_PER_CHAR}\n"
392         buf << "#define SETTINGS_TABLE_COUNT #{@count}\n"
393         offset_type = "uint16_t"
394         if can_use_byte_offsetof
395             offset_type = "uint8_t"
396         end
397         buf << "typedef #{offset_type} setting_offset_t;\n"
398         pgn_count = 0
399         foreach_enabled_group do |group|
400             pgn_count += 1
401         end
402         buf << "#define SETTINGS_PGN_COUNT #{pgn_count}\n"
403         # Write type definitions and constants for min/max values
404         buf << "typedef #{@value_encoder.min_type} setting_min_t;\n"
405         buf << "typedef #{@value_encoder.max_type} setting_max_t;\n"
406         buf << "#define SETTING_MIN_MAX_INDEX_BYTES #{@value_encoder.index_bytes*2}\n"
407         # Write lookup table constants
408         table_names = ordered_table_names()
409         buf << "enum {\n"
410         table_names.each do |name|
411             buf << "\t#{table_constant_name(name)},\n"
412         end
413         buf << "\tLOOKUP_TABLE_COUNT,\n"
414         buf << "};\n"
415         # Write table pointers
416         table_names.each do |name|
417             buf << "extern const char * const #{table_variable_name(name)}[];\n"
418         end
420         # Write setting constants from settings file
421         @constants.each do |name, value|
422             buf << "#define SETTING_CONSTANT_#{name.upcase} #{value.inspect}\n"
423         end
425         # Write #define'd constants for referencing each setting
426         ii = 0
427         foreach_enabled_member do |group, member|
428             name = member["name"]
429             type = member["type"]
430             default_value = member["default_value"]
432             case
433             when %i[ zero target ].include?(default_value)
434                 default_value = nil
436             when member.has_key?("table")
437                 table_name = member["table"]
438                 table_values = @tables[table_name]["values"]
439                 if table_name == 'off_on' and [false, true].include? default_value
440                     default_value = { false => '0', true => '1' }[default_value]
441                 else
442                     default_value = table_values.index default_value
443                 end
445             when type == "string"
446                 default_value = "{ #{[*default_value.bytes, 0] * ', '} }"
448             when default_value.is_a?(Float)
449                 default_value = default_value.to_s + ?f
451             end
453             min, max = resolve_range(member)
454             setting_name = "SETTING_#{name.upcase}"
455             buf << "#define #{setting_name}_DEFAULT #{default_value}\n" unless default_value.nil?
456             buf << "#define #{setting_name} #{ii}\n"
457             buf << "#define #{setting_name}_MIN #{min}\n"
458             buf << "#define #{setting_name}_MAX #{max}\n"
459             ii += 1
460         end
462         File.open(file, 'w') {|file| file.write(buf.string)}
463     end
465     def write_impl_file(file)
466         buf = StringIO.new
467         write_file_header(buf)
468         add_header = ->(h) {
469             buf << "#include \"#{h}\"\n"
470         }
471         add_header.call("platform.h")
472         add_header.call("config/parameter_group_ids.h")
473         add_header.call("fc/settings.h")
475         foreach_enabled_group do |group|
476             (group["headers"] || []).each do |h|
477                 add_header.call(h)
478             end
479         end
481         # When this file is compiled in unit tests, some of the tables
482         # are not used and generate warnings, causing the test to fail
483         # with -Werror. Silence them
485         buf << "#pragma GCC diagnostic ignored \"-Wunused-const-variable\"\n"
487         # Write PGN arrays
488         pgn_steps = []
489         pgns = []
490         foreach_enabled_group do |group|
491             count = 0
492             group["members"].each do |member|
493                 if is_condition_enabled(member["condition"])
494                     count += 1
495                 end
496             end
497             pgn_steps << count
498             pgns << group["name"]
499         end
501         buf << "const pgn_t settingsPgn[] = {\n"
502         pgns.each do |p|
503             buf << "\t#{p},\n"
504         end
505         buf << "};\n"
506         buf << "const uint8_t settingsPgnCounts[] = {\n"
507         pgn_steps.each do |s|
508             buf << "\t#{s},\n"
509         end
510         buf << "};\n"
512         # Write word list
513         buf << "static const uint8_t settingNamesWords[] = {\n"
514         word_bits = SETTINGS_WORDS_BITS_PER_CHAR
515         # We need 27 symbols for a-z + null
516         rem_symbols = 2 ** word_bits - 27
517         symbols = Array.new
518         acc = 0
519         acc_bits = 0
520         encode_byte = lambda do |c|
521             if c == 0
522                 chr = 0 # XXX: Remove this if we go for explicit lengths
523             elsif c >= 'a'.ord && c <= 'z'.ord
524                 chr = 1 + (c - 'a'.ord)
525             elsif c >= 'A'.ord && c <= 'Z'.ord
526                 raise "Cannot encode uppercase character #{c.ord} (#{c})"
527             else
528                 idx = symbols.index(c)
529                 if idx.nil?
530                     if rem_symbols == 0
531                         raise "Cannot encode character #{c.ord} (#{c}), no symbols remaining"
532                     end
533                     rem_symbols -= 1
534                     idx = symbols.length
535                     symbols.push(c)
536                 end
537                 chr = 1 + ('z'.ord - 'a'.ord + 1) + idx
538             end
539             if acc_bits >= (8 - word_bits)
540                 # Write
541                 remaining = 8 - acc_bits
542                 acc |= chr << (remaining - word_bits)
543                 buf << "0x#{acc.to_s(16)},"
544                 acc = (chr << (8 - (word_bits - remaining))) & 0xff
545             else
546                 # Accumulate for next byte
547                 acc |= chr << (3 - acc_bits)
548             end
549             acc_bits = (acc_bits + word_bits) % 8
550         end
551         @name_encoder.words.each do |w|
552             buf << "\t"
553             w.each_byte {|c| encode_byte.call(c)}
554             encode_byte.call(0)
555             buf << " /* #{w.inspect} */ \n"
556         end
557         if acc_bits > 0
558             buf << "\t0x#{acc.to_s(16)},"
559             if acc_bits > (8 - word_bits)
560                 buf << "0x00"
561             end
562             buf << "\n"
563         end
564         buf << "};\n"
566         # Output symbol array
567         buf << "static const char wordSymbols[] = {"
568         symbols.each { |s| buf << "'#{s.chr}'," }
569         buf << "};\n"
570         # Write the tables
571         table_names = ordered_table_names()
572         table_names.each do |name|
573             buf << "const char * const #{table_variable_name(name)}[] = {\n"
574             tbl = @tables[name]
575             raise "values not found for table #{name}" unless tbl.has_key? 'values'
576             tbl["values"].each do |v|
577                 buf << "\t#{v.inspect},\n"
578             end
579             buf << "};\n"
580         end
582         buf << "static const lookupTableEntry_t settingLookupTables[] = {\n"
583         table_names.each do |name|
584             vn = table_variable_name(name)
585             buf << "\t{ #{vn}, sizeof(#{vn}) / sizeof(char*) },\n"
586         end
587         buf << "};\n"
589         # Write min/max values table
590         buf << "static const uint32_t settingMinMaxTable[] = {\n"
591         @value_encoder.values.each do |v|
592             buf <<  "\t#{v},\n"
593         end
594         buf << "};\n"
596         case @value_encoder.index_bytes
597         when 1
598             buf << "typedef uint8_t setting_min_max_idx_t;\n"
599             buf << "#define SETTING_INDEXES_GET_MIN(val) (val->config.minmax.indexes[0])\n"
600             buf << "#define SETTING_INDEXES_GET_MAX(val) (val->config.minmax.indexes[1])\n"
601         else
602             raise "can't encode indexed values requiring #{@value_encoder.index_bytes} bytes"
603         end
605         # Write setting_t values
606         buf << "static const setting_t settingsTable[] = {\n"
608         last_group = nil
609         foreach_enabled_member do |group, member|
610             if group != last_group
611                 last_group = group
612                 buf << "\t// #{group["name"]}\n"
613             end
615             name = member["name"]
616             buf << "\t{ #{@name_encoder.format_encoded_name(name)}, "
617             buf << "#{var_type(member["type"])} | #{value_type(group)}"
618             tbl = member["table"]
619             if tbl
620                 buf << " | MODE_LOOKUP"
621                 buf << ", .config.lookup = { #{table_constant_name(tbl)} }"
622             else
623                 min, max = resolve_range(member)
624                 if min > max
625                     raise "Error encoding #{name}: min (#{min}) > max (#{max})"
626                 end
627                 enc = @value_encoder.encode_values(min, max)
628                 buf <<  ", .config.minmax.indexes = #{enc}"
629             end
630             buf << ", (setting_offset_t)offsetof(#{group["type"]}, #{member["field"]}) },\n"
631         end
632         buf << "};\n"
634         File.open(file, 'w') {|file| file.write(buf.string)}
635     end
637     def var_type(typ)
638         case typ
639         when "uint8_t", "bool"
640             return "VAR_UINT8"
641         when "int8_t"
642             return "VAR_INT8"
643         when "uint16_t"
644             return "VAR_UINT16"
645         when "int16_t"
646             return "VAR_INT16"
647         when "uint32_t"
648             return "VAR_UINT32"
649         when "float"
650             return "VAR_FLOAT"
651         when "string"
652             return "VAR_STRING"
653         else
654             raise "unknown variable type #{typ.inspect}"
655         end
656     end
658     def value_type(group)
659         return group["value_type"] || "MASTER_VALUE"
660     end
662     def resolve_range(member)
663         min = @value_encoder.resolve_value(member["min"])
664         max = @value_encoder.resolve_value(member["max"])
665         return min, max
666     end
668     def is_condition_enabled(cond)
669         return !cond || @true_conditions.include?(cond)
670     end
672     def foreach_enabled_member &block
673         enum = Enumerator.new do |yielder|
674             groups.each do |group|
675                 if is_condition_enabled(group["condition"])
676                     group["members"].each do |member|
677                         if is_condition_enabled(member["condition"])
678                             yielder.yield group, member
679                         end
680                     end
681                 end
682             end
683         end
684         block_given? ? enum.each(&block) : enum
685     end
687     def foreach_enabled_group &block
688         enum = Enumerator.new do |yielder|
689             last = nil
690             foreach_enabled_member do |group, member|
691                 if last != group
692                     last = group
693                     yielder.yield group
694                 end
695             end
696         end
697         block_given? ? enum.each(&block) : enum
698     end
700     def foreach_member &block
701         enum = Enumerator.new do |yielder|
702             @data["groups"].each do |group|
703                 group["members"].each do |member|
704                     yielder.yield group, member
705                 end
706             end
707         end
708         block_given? ? enum.each(&block) : enum
709     end
711     def groups
712         @data["groups"]
713     end
715     def initialize_tables
716         @data["tables"].each do |tbl|
717             name = tbl["name"]
718             if @tables.key?(name)
719                 raise "Duplicate table name #{name}"
720             end
721             @tables[name] = tbl
722         end
723     end
725     def initialize_constants
726         @constants = @data["constants"]
727     end
729     def ordered_table_names
730         @enabled_tables.to_a().sort()
731     end
733     def table_constant_name(name)
734         upper = name.upcase()
735         "TABLE_#{upper}"
736     end
738     def table_variable_name(name)
739         "table_#{name}"
740     end
742     def can_use_byte_offsetof
743         buf = StringIO.new
744         foreach_enabled_member do |group, member|
745             typ = group["type"]
746             field = member["field"]
747             buf << "static_assert(offsetof(#{typ}, #{field}) < 255, \"#{typ}.#{field} is too big\");\n"
748         end
749         stderr = compile_test_file(buf)
750         return !stderr.include?("static assertion failed")
751     end
753     def mktmpdir
754         # Use a temporary dir reachable by relative path
755         # since g++ in cygwin fails to open files
756         # with absolute paths
757         tmp = File.join(@output_dir, "tmp")
758         FileUtils.mkdir_p(tmp) unless File.directory?(tmp)
759         value = yield(tmp)
760         if File.directory?(tmp)
761             FileUtils.remove_dir(tmp)
762         end
763         value
764     end
766     def compile_test_file(prog)
767         buf = StringIO.new
768         # cstddef for offsetof()
769         headers = ["platform.h", "cstddef"]
770         @data["groups"].each do |group|
771             gh = group["headers"]
772             if gh
773                 headers.concat gh
774             end
775         end
776         headers.each do |h|
777             if h
778                 buf << "#include \"#{h}\"\n"
779             end
780         end
781         buf << "\n"
782         buf << prog.string
783         mktmpdir do |dir|
784             file = File.join(dir, "test.cpp")
785             File.open(file, 'w') {|file| file.write(buf.string)}
786             dputs "Compiling #{buf.string}"
787             stdout, stderr = @compiler.run(file, File.join(dir, "test"), '-c', noerror: true)
788             dputs "Output: #{stderr}"
789             stderr
790         end
791     end
793     def check_conditions
794         buf = StringIO.new
795         conditions = Set.new
796         add_condition = -> (c) {
797             if c && !conditions.include?(c)
798                 conditions.add(c)
799                 buf << "#if "
800                 in_word = false
801                 c.split('').each do |ch|
802                     if in_word
803                         if !ch.match(/^[a-zA-Z0-9_]$/)
804                             in_word = false
805                             buf << ")"
806                         end
807                         buf << ch
808                     else
809                         if ch.match(/^[a-zA-Z_]$/)
810                             in_word = true
811                             buf << "defined("
812                         end
813                         buf << ch
814                     end
815                 end
816                 if in_word
817                     buf << ")"
818                 end
819                 buf << "\n"
820                 buf << "#pragma message(#{c.inspect})\n"
821                 buf << "#endif\n"
822             end
823         }
825         foreach_member do |group, member|
826             add_condition.call(group["condition"])
827             add_condition.call(member["condition"])
828         end
830         stderr = compile_test_file(buf)
831         @true_conditions = Set.new
832         stderr.scan(/#pragma message\(\"(.*)\"\)/).each do |m|
833             @true_conditions << m[0]
834         end
835     end
837     def sanitize_fields(all=false)
838         pending_types = Hash.new
839         has_booleans = false
841         block  = Proc.new do |group, member|
842             if !group["name"]
843                 raise "Missing group name"
844             end
846             if !member["name"]
847                 raise "Missing member name in group #{group["name"]}"
848             end
850             table = member["table"]
851             if table
852                 if !@tables[table]
853                     raise "Member #{member["name"]} references non-existing table #{table}"
854                 end
856                 @used_tables << table
857             end
859             if !member["field"]
860                 member["field"] = member["name"]
861             end
863             typ = member["type"]
864             if typ == "bool"
865                 has_booleans = true
866                 member["table"] = OFF_ON_TABLE["name"]
867             end
868         end
870         all ? foreach_member(&block) : foreach_enabled_member(&block)
872         if has_booleans
873             @tables[OFF_ON_TABLE["name"]] = OFF_ON_TABLE
874             @used_tables << OFF_ON_TABLE["name"]
875         end
877         resolve_all_types
878         foreach_enabled_member do |group, member|
879             @count += 1
880             @max_name_length = [@max_name_length, member["name"].length].max
881             if member["table"]
882                 @enabled_tables << member["table"]
883             end
884         end
885     end
887     def validate_default_values
888         foreach_enabled_member do |_, member|
889             name = member["name"]
890             type = member["type"]
891             min = member["min"] || 0
892             max = member["max"]
893             default_value = member["default_value"]
895             next if %i[ zero target ].include? default_value
897             case
898             when type == "bool"
899                 raise "Member #{name} has an invalid default value" unless [ false, true ].include? default_value
901             when member.has_key?("table")
902                 table_name = member["table"]
903                 table_values = @tables[table_name]["values"]
904                 raise "Member #{name} has an invalid default value" unless table_values.include? default_value
906             when type =~ /\A(?<unsigned>u?)int(?<bitsize>8|16|32|64)_t\Z/
907                 unsigned = !$~[:unsigned].empty?
908                 bitsize = $~[:bitsize].to_i
909                 type_range = unsigned ? 0..(2**bitsize-1) : (-2**(bitsize-1)+1)..(2**(bitsize-1)-1)
910                 min = type_range.min if min.to_s =~ /\AU?INT\d+_MIN\Z/
911                 max = type_range.max if max.to_s =~ /\AU?INT\d+_MAX\Z/
912                 raise "Member #{name} default value has an invalid type, integer or symbol expected" unless default_value.is_a? Integer or default_value.is_a? Symbol
913                 raise "Member #{name} default value is outside type's storage range, min #{type_range.min}, max #{type_range.max}" unless default_value.is_a? Symbol or type_range === default_value
914                 raise "Numeric member #{name} doesn't have maximum value defined" unless member.has_key? 'max'
915                 raise "Member #{name} default value is outside of the allowed range" if default_value.is_a? Numeric and min.is_a? Numeric and max.is_a? Numeric and not (min..max) === default_value
917             when type == "float"
918                 raise "Member #{name} default value has an invalid type, numeric or symbol expected" unless default_value.is_a? Numeric or default_value.is_a? Symbol
919                 raise "Numeric member #{name} doesn't have maximum value defined" unless member.has_key? 'max'
920                 raise "Member #{name} default value is outside of the allowed range" if default_value.is_a? Numeric and min.is_a? Numeric and max.is_a? Numeric and not (min..max) === default_value
922             when type == "string"
923                 max = member["max"].to_i
924                 raise "Member #{name} default value has an invalid type, string expected" unless default_value.is_a? String
925                 raise "Member #{name} default value is too long (max #{max} chars)" if default_value.bytesize > max
927             else
928                 raise "Unexpected type for member #{name}: #{type.inspect}"
929             end
930         end
931     end
933     def scan_types(stderr)
934         types = Hash.new
935         # gcc 6-9
936         stderr.scan(/var_(\d+).*?['’], which is of non-class type ['‘](.*)['’]/).each do |m|
937             member_idx = m[0].to_i
938             type = m[1]
939             types[member_idx] = type
940         end
941         # clang
942         stderr.scan(/member reference base type '(.*?)'.*?is not a structure or union.*? var_(\d+)/m).each do |m|
943             member_idx = m[1].to_i
944             type = m[0]
945             types[member_idx] = type
946         end
947         return types
948     end
950     def resolve_all_types()
951         loop do
952             pending = Hash.new
953             foreach_enabled_member do |group, member|
954                 if !member["type"]
955                     pending[member] = group
956                 end
957             end
959             if pending.empty?
960                 # All types resolved
961                 break
962             end
964             resolve_types(pending)
965         end
966     end
968     def resolve_types(pending)
969         prog = StringIO.new
970         prog << "int main() {\n"
971         ii = 0
972         members = Hash.new
973         pending.each do |member, group|
974             var = "var_#{ii}"
975             members[ii] = member
976             ii += 1
977             gt = group["type"]
978             mf = member["field"]
979             prog << "#{gt} #{var}; #{var}.#{mf}.__type_detect_;\n"
980         end
981         prog << "return 0;\n"
982         prog << "};\n"
983         stderr = compile_test_file(prog)
984         types = scan_types(stderr)
985         if types.empty?
986             raise "No types resolved from #{stderr}"
987         end
988         types.each do |idx, type|
989             member = members[idx]
990             case type
991             when /^bool/
992                 typ = "bool"
993             when /^int8_t/ # {aka signed char}"
994                 typ = "int8_t"
995             when /^uint8_t/ # {aka unsigned char}"
996                 typ = "uint8_t"
997             when /^int16_t/ # {aka short int}"
998                 typ = "int16_t"
999             when /^uint16_t/ # {aka short unsigned int}"
1000                 typ = "uint16_t"
1001             when /^uint32_t/ # {aka long unsigned int}"
1002                 typ = "uint32_t"
1003             when "float"
1004                 typ = "float"
1005             when /^char\s*\[(\d+)\]/
1006                 # Substract 1 to show the maximum string size without the null terminator
1007                 member["max"] = $1.to_i - 1;
1008                 typ = "string"
1009             else
1010                 raise "Unknown type #{type} when resolving type for setting #{member["name"]}"
1011             end
1012             dputs "#{member["name"]} type is #{typ}"
1013             member["type"] = typ
1014         end
1015     end
1017     def initialize_name_encoder
1018         names = []
1019         foreach_enabled_member do |group, member|
1020             names << member["name"]
1021         end
1022         best = nil
1023         (3..7).each do |v|
1024             enc = NameEncoder.new(names, v)
1025             if best == nil || best.estimated_size(@count) > enc.estimated_size(@count)
1026                 best = enc
1027             end
1028         end
1029         dputs "Using name encoder with max_length = #{best.max_length}"
1030         @name_encoder = best
1031     end
1033     def initialize_value_encoder
1034         values = []
1035         constants = []
1036         add_value = -> (v) {
1037             v = v || 0
1038             if v.is_number_kind? || (v.class == String && v.is_number?)
1039                 values << v.to_i
1040             else
1041                 constants << v
1042             end
1043         }
1044         foreach_enabled_member do |group, member|
1045             add_value.call(member["min"])
1046             add_value.call(member["max"])
1047         end
1048         constantValues = resolve_constants(constants)
1049         # Count values used by constants
1050         constants.each do |c|
1051             values << constantValues[c]
1052         end
1053         @value_encoder = ValueEncoder.new(values, constantValues)
1054     end
1056     def check_member_default_values_presence
1057         missing_default_value_names = foreach_member.inject([]) { |names, (_, member)| member.has_key?("default_value") ? names : names << member["name"] }
1058         raise "Missing default value for #{missing_default_value_names.count} member#{"s" unless missing_default_value_names.one?}: #{missing_default_value_names * ", "}" unless missing_default_value_names.empty?
1059     end
1061     def resolv_min_max_and_default_values_if_possible
1062         foreach_member do |_, member|
1063             %w[ min max default_value ].each do |value_type|
1064                 member_value = member[value_type]
1065                 if member_value.is_a? String
1066                     constant_value = @constants[member_value]
1067                     member[value_type] = constant_value unless constant_value.nil?
1068                 end
1069             end
1070         end
1071     end
1073     def resolve_constants(constants)
1074         return nil unless constants.length > 0
1075         s = Set.new
1076         constants.each do |c|
1077             s << c
1078         end
1079         dputs "#{constants.length} constants to resolve"
1080         # Since we're relying on errors rather than
1081                 # warnings to find these constants, the compiler
1082                 # might reach the maximum number of errors and stop
1083                 # compilation, so we might need multiple passes.
1084         gcc_re = /required from ['‘]class expr_(.*?)<(.*?)>['’]/ # gcc 6-9
1085         clang_re = / template class 'expr_(.*?)<(.*?)>'/ # clang
1086         res = [gcc_re, clang_re]
1087         values = Hash.new
1088                 while s.length > 0
1089             buf = StringIO.new
1090             buf << "template <int64_t V> class Fail {\n"
1091             # Include V in the static_assert so it's shown
1092             # in the error condition.
1093             buf << "static_assert(V == 42 && 0 == 1, \"FAIL\");\n"
1094             buf << "public:\n"
1095             buf << "Fail() {};\n"
1096             buf << "int64_t v = V;\n"
1097             buf << "};\n"
1098             ii  = 0
1099             s.each do |c|
1100                 cls = "expr_#{c}"
1101                 buf << "template <int64_t V> class #{cls}: public Fail<V> {};\n"
1102                 buf << "#{cls}<#{c}> var_#{ii};\n"
1103                 ii += 1
1104             end
1105             stderr = compile_test_file(buf)
1106             matches = []
1107             res.each do |re|
1108                 if matches.length == 0
1109                     matches = stderr.scan(re)
1110                 end
1111             end
1112                         if matches.length == 0
1113                 puts stderr
1114                 raise "No more matches looking for constants"
1115             end
1116             matches.each do |m|
1117                 c = m[0]
1118                                 v = m[1]
1119                                 # gcc 6.3 includes an ul or ll prefix after the
1120                                 # constant expansion, while gcc 7.1 does not
1121                 v = v.tr("ul", "")
1122                 nv = v.to_i
1123                 values[c] = nv
1124                 s.delete(c)
1125                 dputs "Constant #{c} resolved to #{nv}"
1126             end
1127         end
1128         values
1129     end
1132 def usage
1133     puts "Usage: ruby #{__FILE__} <source_dir> <settings_file> [--use_host_gcc] [--json <json_file>]"
1136 if __FILE__ == $0
1138     verbose = ENV["V"] == "1"
1140     src_root = ARGV[0]
1141     settings_file = ARGV[1]
1142     if src_root.nil? || settings_file.nil?
1143         usage()
1144         exit(1)
1145     end
1148     opts = GetoptLong.new(
1149         [ "--output-dir", "-o", GetoptLong::REQUIRED_ARGUMENT ],
1150         [ "--help", "-h", GetoptLong::NO_ARGUMENT ],
1151         [ "--json", "-j", GetoptLong::REQUIRED_ARGUMENT ],
1152         [ "--use_host_gcc", "-g", GetoptLong::NO_ARGUMENT ]
1153     )
1155     jsonFile = nil
1156     output_dir = nil
1157     use_host_gcc = nil
1159     opts.each do |opt, arg|
1160         case opt
1161         when "--output-dir"
1162             output_dir = arg
1163         when "--help"
1164             usage()
1165             exit(0)
1166         when "--json"
1167             jsonFile = arg
1168         when "--use_host_gcc"
1169             use_host_gcc = true
1170         end
1171     end
1173     gen = Generator.new(src_root, settings_file, output_dir, use_host_gcc)
1175     if jsonFile
1176         gen.write_json(jsonFile)
1177     else
1178         gen.write_files()
1179         if verbose
1180             gen.print_stats()
1181         end
1182     end