mirror of
https://github.com/iNavFlight/inav.git
synced 2025-07-19 22:35:19 +03:00
- Don't include target.h explicitely, it's already included by platform.h - Fix an error in compiled file as a test for discovering contstants. Since we rely on compiler errors to give us the resolved value, that lets us get one extra value on each run - Always compile the test files for setting discovery in c++11 even if no -std=XXX argument is passed from the caller.
1006 lines
29 KiB
Ruby
1006 lines
29 KiB
Ruby
#!/usr/bin/env ruby
|
|
|
|
# This file is part of INAV.
|
|
#
|
|
# author: Alberto Garcia Hierro <alberto@garciahierro.com>
|
|
#
|
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
# You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
#
|
|
# Alternatively, the contents of this file may be used under the terms
|
|
# of the GNU General Public License Version 3, as described below:
|
|
#
|
|
# This file is free software: you may copy, redistribute and/or modify
|
|
# it under the terms of the GNU General Public License as published by the
|
|
# Free Software Foundation, either version 3 of the License, or (at your
|
|
# option) any later version.
|
|
#
|
|
# This file is distributed in the hope that it will be useful, but
|
|
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
|
# Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program. If not, see http://www.gnu.org/licenses/.
|
|
|
|
require 'fileutils'
|
|
require 'getoptlong'
|
|
require 'json'
|
|
require 'set'
|
|
require 'stringio'
|
|
require 'tmpdir'
|
|
require 'yaml'
|
|
|
|
require_relative 'compiler'
|
|
|
|
DEBUG = false
|
|
INFO = false
|
|
|
|
SETTINGS_WORDS_BITS_PER_CHAR = 5
|
|
|
|
def dputs(s)
|
|
puts s if DEBUG
|
|
end
|
|
|
|
def lputs(s)
|
|
puts if DEBUG or INFO
|
|
end
|
|
|
|
class Object
|
|
def is_number_kind?
|
|
self.kind_of?(Integer) || self.kind_of?(Float)
|
|
end
|
|
end
|
|
|
|
class String
|
|
def is_number?
|
|
true if Float(self) rescue false
|
|
end
|
|
end
|
|
|
|
class StringIO
|
|
def write_byte(b)
|
|
self << [b].pack("C*")
|
|
end
|
|
|
|
def write_uvarint(x)
|
|
while x >= 0x80
|
|
write_byte((x & 0xFF) | 0x80)
|
|
x >>= 7
|
|
end
|
|
write_byte(x)
|
|
end
|
|
|
|
def to_carr
|
|
return string.bytes.to_s.sub('[', '{').sub(']', '}')
|
|
end
|
|
end
|
|
|
|
class NameEncoder
|
|
attr_reader :max_length
|
|
attr_reader :max_word_length
|
|
|
|
def initialize(names, max_length)
|
|
@names = names
|
|
@max_length = max_length
|
|
# Key is word, value is number of uses
|
|
@words = Hash.new(0)
|
|
# Most used words first
|
|
@words_by_usage = []
|
|
# Words that shouldn't be split because
|
|
# their encoding is too long
|
|
@non_split = Set.new
|
|
# Key is the name, value is its encoding
|
|
@encoded = Hash.new
|
|
|
|
@max_word_length = 0;
|
|
|
|
update_words
|
|
encode_names
|
|
end
|
|
|
|
def uses_byte_indexing
|
|
@words.length < 255
|
|
end
|
|
|
|
def words
|
|
@words_by_usage
|
|
end
|
|
|
|
def estimated_size(settings_count)
|
|
size = 0
|
|
@max_word_length = 0
|
|
@words.each do |word, count|
|
|
size += (word.length + 1) * (5/8.0)
|
|
if word.length > @max_word_length
|
|
@max_word_length = word.length
|
|
end
|
|
end
|
|
return size.to_i + @max_length * settings_count
|
|
end
|
|
|
|
def format_encoded_name(name)
|
|
encoded = @encoded[name]
|
|
raise "Name #{name} was not encoded" if encoded == nil
|
|
return encoded.to_carr
|
|
end
|
|
|
|
private
|
|
def split_words(name)
|
|
if @non_split.include?(name)
|
|
return [name]
|
|
end
|
|
return name.split('_')
|
|
end
|
|
|
|
def update_words
|
|
@words.clear
|
|
@names.each do |name|
|
|
split_words(name).each do |word|
|
|
@words[word] += 1
|
|
end
|
|
end
|
|
# Sort by usage, then alphabetically
|
|
@words_by_usage = @words.keys().sort do |x, y|
|
|
ux = @words[x]
|
|
uy = @words[y]
|
|
if ux != uy
|
|
uy <=> ux
|
|
else
|
|
x <=> y
|
|
end
|
|
end
|
|
end
|
|
|
|
def encode_names
|
|
@encoded.clear()
|
|
@names.each do |name|
|
|
buf = StringIO.new
|
|
split_words(name).each do |word|
|
|
pos = @words_by_usage.find_index(word)
|
|
raise "Word #{word} not found in words array" if pos == nil
|
|
# Zero indicates end of words, first word in
|
|
# the array starts at 1 ([0] is NULL).
|
|
p = pos + 1
|
|
if uses_byte_indexing
|
|
buf.write_byte(p)
|
|
else
|
|
buf.write_uvarint(p)
|
|
end
|
|
end
|
|
if buf.length > @max_length
|
|
# TODO: print in verbose mode
|
|
# fmt.Printf("encoding %q took %d bytes (>%d), adding it as single word\n", v, len(data), e.MaxEncodedLength)
|
|
@non_split << name
|
|
update_words
|
|
return encode_names
|
|
end
|
|
while buf.length < @max_length
|
|
buf.write_byte(0)
|
|
end
|
|
@encoded[name] = buf
|
|
end
|
|
end
|
|
end
|
|
|
|
class ValueEncoder
|
|
attr_reader :values
|
|
|
|
def initialize(values, constants)
|
|
min = 0
|
|
max = 0
|
|
|
|
valuesHash = Hash.new(0)
|
|
values.each do |v|
|
|
min = [min, v].min
|
|
max = [max, v].max
|
|
valuesHash[v] += 1
|
|
end
|
|
|
|
# Sorted by usage, most used first
|
|
@values = valuesHash.keys().sort do |x, y|
|
|
ux = valuesHash[x]
|
|
uy = valuesHash[y]
|
|
if ux != uy
|
|
uy <=> ux
|
|
else
|
|
x <=> y
|
|
end
|
|
end
|
|
|
|
@constants = constants
|
|
|
|
@min = min
|
|
@max = max
|
|
end
|
|
|
|
def min_type
|
|
[8, 16, 32].each do |x|
|
|
if @min >= -(2**(x-1))
|
|
return "int#{x}_t"
|
|
end
|
|
end
|
|
raise "cannot represent minimum value #{@min} with int32_t"
|
|
end
|
|
|
|
def max_type
|
|
[8, 16, 32].each do |x|
|
|
if @max < 2**x
|
|
return "uint#{x}_t"
|
|
end
|
|
end
|
|
raise "cannot represent maximum value #{@max} with uint32_t"
|
|
end
|
|
|
|
def index_bytes
|
|
bits = Math.log2(@values.length).ceil
|
|
bytes = (bits / 8.0).ceil.to_i
|
|
if bytes > 1
|
|
raise "too many bytes required for value index: #{bytes}"
|
|
end
|
|
return bytes
|
|
end
|
|
|
|
def encode_values(min, max)
|
|
buf = StringIO.new
|
|
encode_value(buf, min)
|
|
encode_value(buf, max)
|
|
return buf.to_carr
|
|
end
|
|
|
|
def resolve_value(val)
|
|
v = val || 0
|
|
if !v.is_number_kind?
|
|
v = @constants[val]
|
|
if v == nil
|
|
raise "Could not resolve constant #{val}"
|
|
end
|
|
end
|
|
return v
|
|
end
|
|
|
|
private
|
|
def encode_value(buf, val)
|
|
v = resolve_value(val)
|
|
pos = @values.find_index(v)
|
|
if pos < 0
|
|
raise "Could not encode value not in array #{v}"
|
|
end
|
|
buf.write_byte(pos)
|
|
end
|
|
end
|
|
|
|
OFF_ON_TABLE = Hash["name" => "off_on", "values" => ["OFF", "ON"]]
|
|
|
|
class Generator
|
|
def initialize(src_root, settings_file, output_dir)
|
|
@src_root = src_root
|
|
@settings_file = settings_file
|
|
@output_dir = output_dir || File.dirname(settings_file)
|
|
|
|
@compiler = Compiler.new
|
|
|
|
@count = 0
|
|
@max_name_length = 0
|
|
@tables = Hash.new
|
|
@used_tables = Set.new
|
|
@enabled_tables = Set.new
|
|
end
|
|
|
|
def write_files
|
|
|
|
# Remove the old files first, if any
|
|
[header_file, impl_file].each do |file|
|
|
if File.file?(file)
|
|
File.delete(file)
|
|
end
|
|
end
|
|
|
|
load_data
|
|
|
|
sanitize_fields
|
|
initialize_name_encoder
|
|
initialize_value_encoder
|
|
|
|
write_header_file(header_file)
|
|
write_impl_file(impl_file)
|
|
end
|
|
|
|
def write_json(jsonFile)
|
|
load_data
|
|
sanitize_fields(true)
|
|
|
|
settings = Hash.new
|
|
|
|
foreach_member do |group, member|
|
|
name = member["name"]
|
|
s = {
|
|
"type" => member["type"],
|
|
}
|
|
table = member["table"]
|
|
if table
|
|
s["table"] = @tables[table]
|
|
end
|
|
settings[name] = s
|
|
end
|
|
|
|
File.open(jsonFile, "w") do |f|
|
|
f.write(JSON.pretty_generate(settings))
|
|
end
|
|
end
|
|
|
|
def print_stats
|
|
puts "#{@count} settings"
|
|
puts "words table has #{@name_encoder.words.length} words"
|
|
word_idx = @name_encoder.uses_byte_indexing ? "byte" : "uvarint"
|
|
puts "name encoder uses #{word_idx} word indexing"
|
|
puts "each setting name uses #{@name_encoder.max_length} bytes"
|
|
puts "#{@name_encoder.estimated_size(@count)} bytes estimated for setting name storage"
|
|
values_size = @value_encoder.values.length * 4
|
|
puts "min/max value storage uses #{values_size} bytes"
|
|
value_idx_size = @value_encoder.index_bytes * 2
|
|
value_idx_total = value_idx_size * @count
|
|
puts "value indexing uses #{value_idx_size} per setting, #{value_idx_total} bytes total"
|
|
puts "#{value_idx_size+value_idx_total} bytes estimated for value+indexes storage"
|
|
|
|
buf = StringIO.new
|
|
buf << "#include \"fc/settings.h\"\n"
|
|
buf << "char (*dummy)[sizeof(setting_t)] = 1;\n"
|
|
stderr = compile_test_file(buf)
|
|
puts "sizeof(setting_t) = #{/char \(\*\)\[(\d+)\]/.match(stderr)[1]}"
|
|
end
|
|
|
|
private
|
|
|
|
def load_data
|
|
@data = YAML.load_file(@settings_file)
|
|
|
|
initialize_tables
|
|
check_conditions
|
|
end
|
|
|
|
def header_file
|
|
File.join(@output_dir, "settings_generated.h")
|
|
end
|
|
|
|
def impl_file
|
|
File.join(@output_dir, "settings_generated.c")
|
|
end
|
|
|
|
def write_file_header(buf)
|
|
buf << "// This file has been automatically generated by utils/settings.rb\n"
|
|
buf << "// Don't make any modifications to it. They will be lost.\n\n"
|
|
end
|
|
|
|
def write_header_file(file)
|
|
buf = StringIO.new
|
|
write_file_header(buf)
|
|
buf << "#pragma once\n"
|
|
# Write setting_t size constants
|
|
buf << "#define SETTING_MAX_NAME_LENGTH #{@max_name_length+1}\n" # +1 for the terminating '\0'
|
|
buf << "#define SETTING_MAX_WORD_LENGTH #{@name_encoder.max_word_length+1}\n" # +1 for the terminating '\0'
|
|
buf << "#define SETTING_ENCODED_NAME_MAX_BYTES #{@name_encoder.max_length}\n"
|
|
if @name_encoder.uses_byte_indexing
|
|
buf << "#define SETTING_ENCODED_NAME_USES_BYTE_INDEXING\n"
|
|
end
|
|
buf << "#define SETTINGS_WORDS_BITS_PER_CHAR #{SETTINGS_WORDS_BITS_PER_CHAR}\n"
|
|
buf << "#define SETTINGS_TABLE_COUNT #{@count}\n"
|
|
offset_type = "uint16_t"
|
|
if can_use_byte_offsetof
|
|
offset_type = "uint8_t"
|
|
end
|
|
buf << "typedef #{offset_type} setting_offset_t;\n"
|
|
pgn_count = 0
|
|
foreach_enabled_group do |group|
|
|
pgn_count += 1
|
|
end
|
|
buf << "#define SETTINGS_PGN_COUNT #{pgn_count}\n"
|
|
# Write type definitions and constants for min/max values
|
|
buf << "typedef #{@value_encoder.min_type} setting_min_t;\n"
|
|
buf << "typedef #{@value_encoder.max_type} setting_max_t;\n"
|
|
buf << "#define SETTING_MIN_MAX_INDEX_BYTES #{@value_encoder.index_bytes*2}\n"
|
|
# Write lookup table constants
|
|
table_names = ordered_table_names()
|
|
buf << "enum {\n"
|
|
table_names.each do |name|
|
|
buf << "\t#{table_constant_name(name)},\n"
|
|
end
|
|
buf << "\tLOOKUP_TABLE_COUNT,\n"
|
|
buf << "};\n"
|
|
# Write table pointers
|
|
table_names.each do |name|
|
|
buf << "extern const char * const #{table_variable_name(name)}[];\n"
|
|
end
|
|
|
|
# Write #define'd constants for referencing each setting
|
|
ii = 0
|
|
foreach_enabled_member do |group, member|
|
|
name = member["name"]
|
|
min, max = resolve_range(member)
|
|
setting_name = "SETTING_#{name.upcase}"
|
|
buf << "#define #{setting_name} #{ii}\n"
|
|
buf << "#define #{setting_name}_MIN #{min}\n"
|
|
buf << "#define #{setting_name}_MAX #{max}\n"
|
|
ii += 1
|
|
end
|
|
|
|
File.open(file, 'w') {|file| file.write(buf.string)}
|
|
end
|
|
|
|
def write_impl_file(file)
|
|
buf = StringIO.new
|
|
write_file_header(buf)
|
|
add_header = ->(h) {
|
|
buf << "#include \"#{h}\"\n"
|
|
}
|
|
add_header.call("platform.h")
|
|
add_header.call("config/parameter_group_ids.h")
|
|
add_header.call("fc/settings.h")
|
|
|
|
foreach_enabled_group do |group|
|
|
(group["headers"] || []).each do |h|
|
|
add_header.call(h)
|
|
end
|
|
end
|
|
|
|
# Write PGN arrays
|
|
pgn_steps = []
|
|
pgns = []
|
|
foreach_enabled_group do |group|
|
|
count = 0
|
|
group["members"].each do |member|
|
|
if is_condition_enabled(member["condition"])
|
|
count += 1
|
|
end
|
|
end
|
|
pgn_steps << count
|
|
pgns << group["name"]
|
|
end
|
|
|
|
buf << "const pgn_t settingsPgn[] = {\n"
|
|
pgns.each do |p|
|
|
buf << "\t#{p},\n"
|
|
end
|
|
buf << "};\n"
|
|
buf << "const uint8_t settingsPgnCounts[] = {\n"
|
|
pgn_steps.each do |s|
|
|
buf << "\t#{s},\n"
|
|
end
|
|
buf << "};\n"
|
|
|
|
# Write word list
|
|
buf << "static const uint8_t settingNamesWords[] = {\n"
|
|
word_bits = SETTINGS_WORDS_BITS_PER_CHAR
|
|
# We need 27 symbols for a-z + null
|
|
rem_symbols = 2 ** word_bits - 27
|
|
symbols = Array.new
|
|
acc = 0
|
|
acc_bits = 0
|
|
encode_byte = lambda do |c|
|
|
if c == 0
|
|
chr = 0 # XXX: Remove this if we go for explicit lengths
|
|
elsif c >= 'a'.ord && c <= 'z'.ord
|
|
chr = 1 + (c - 'a'.ord)
|
|
elsif c >= 'A'.ord && c <= 'Z'.ord
|
|
raise "Cannot encode uppercase character #{c.ord} (#{c})"
|
|
else
|
|
idx = symbols.index(c)
|
|
if idx.nil?
|
|
if rem_symbols == 0
|
|
raise "Cannot encode character #{c.ord} (#{c}), no symbols remaining"
|
|
end
|
|
rem_symbols -= 1
|
|
idx = symbols.length
|
|
symbols.push(c)
|
|
end
|
|
chr = 1 + ('z'.ord - 'a'.ord + 1) + idx
|
|
end
|
|
if acc_bits >= (8 - word_bits)
|
|
# Write
|
|
remaining = 8 - acc_bits
|
|
acc |= chr << (remaining - word_bits)
|
|
buf << "0x#{acc.to_s(16)},"
|
|
acc = (chr << (8 - (word_bits - remaining))) & 0xff
|
|
else
|
|
# Accumulate for next byte
|
|
acc |= chr << (3 - acc_bits)
|
|
end
|
|
acc_bits = (acc_bits + word_bits) % 8
|
|
end
|
|
@name_encoder.words.each do |w|
|
|
buf << "\t"
|
|
w.each_byte {|c| encode_byte.call(c)}
|
|
encode_byte.call(0)
|
|
buf << " /* #{w.inspect} */ \n"
|
|
end
|
|
if acc_bits > 0
|
|
buf << "\t0x#{acc.to_s(16)},"
|
|
if acc_bits > (8 - word_bits)
|
|
buf << "0x00"
|
|
end
|
|
buf << "\n"
|
|
end
|
|
buf << "};\n"
|
|
|
|
# Output symbol array
|
|
buf << "static const char wordSymbols[] = {"
|
|
symbols.each { |s| buf << "'#{s.chr}'," }
|
|
buf << "};\n"
|
|
# Write the tables
|
|
table_names = ordered_table_names()
|
|
table_names.each do |name|
|
|
buf << "const char * const #{table_variable_name(name)}[] = {\n"
|
|
tbl = @tables[name]
|
|
tbl["values"].each do |v|
|
|
buf << "\t#{v.inspect},\n"
|
|
end
|
|
buf << "};\n"
|
|
end
|
|
|
|
buf << "static const lookupTableEntry_t settingLookupTables[] = {\n"
|
|
table_names.each do |name|
|
|
vn = table_variable_name(name)
|
|
buf << "\t{ #{vn}, sizeof(#{vn}) / sizeof(char*) },\n"
|
|
end
|
|
buf << "};\n"
|
|
|
|
# Write min/max values table
|
|
buf << "static const uint32_t settingMinMaxTable[] = {\n"
|
|
@value_encoder.values.each do |v|
|
|
buf << "\t#{v},\n"
|
|
end
|
|
buf << "};\n"
|
|
|
|
case @value_encoder.index_bytes
|
|
when 1
|
|
buf << "typedef uint8_t setting_min_max_idx_t;\n"
|
|
buf << "#define SETTING_INDEXES_GET_MIN(val) (val->config.minmax.indexes[0])\n"
|
|
buf << "#define SETTING_INDEXES_GET_MAX(val) (val->config.minmax.indexes[1])\n"
|
|
else
|
|
raise "can't encode indexed values requiring #{@value_encoder.index_bytes} bytes"
|
|
end
|
|
|
|
# Write setting_t values
|
|
buf << "static const setting_t settingsTable[] = {\n"
|
|
|
|
last_group = nil
|
|
foreach_enabled_member do |group, member|
|
|
if group != last_group
|
|
last_group = group
|
|
buf << "\t// #{group["name"]}\n"
|
|
end
|
|
|
|
name = member["name"]
|
|
buf << "\t{ #{@name_encoder.format_encoded_name(name)}, "
|
|
buf << "#{var_type(member["type"])} | #{value_type(group)}"
|
|
tbl = member["table"]
|
|
if tbl
|
|
buf << " | MODE_LOOKUP"
|
|
buf << ", .config.lookup = { #{table_constant_name(tbl)} }"
|
|
else
|
|
min, max = resolve_range(member)
|
|
if min > max
|
|
raise "Error encoding #{name}: min (#{min}) > max (#{max})"
|
|
end
|
|
enc = @value_encoder.encode_values(min, max)
|
|
buf << ", .config.minmax.indexes = #{enc}"
|
|
end
|
|
buf << ", offsetof(#{group["type"]}, #{member["field"]}) },\n"
|
|
end
|
|
buf << "};\n"
|
|
|
|
File.open(file, 'w') {|file| file.write(buf.string)}
|
|
end
|
|
|
|
def var_type(typ)
|
|
case typ
|
|
when "uint8_t", "bool"
|
|
return "VAR_UINT8"
|
|
when "int8_t"
|
|
return "VAR_INT8"
|
|
when "uint16_t"
|
|
return "VAR_UINT16"
|
|
when "int16_t"
|
|
return "VAR_INT16"
|
|
when "uint32_t"
|
|
return "VAR_UINT32"
|
|
when "float"
|
|
return "VAR_FLOAT"
|
|
when "string"
|
|
return "VAR_STRING"
|
|
else
|
|
raise "unknown variable type #{typ.inspect}"
|
|
end
|
|
end
|
|
|
|
def value_type(group)
|
|
return group["value_type"] || "MASTER_VALUE"
|
|
end
|
|
|
|
def resolve_range(member)
|
|
min = @value_encoder.resolve_value(member["min"])
|
|
max = @value_encoder.resolve_value(member["max"])
|
|
return min, max
|
|
end
|
|
|
|
def is_condition_enabled(cond)
|
|
return !cond || @true_conditions.include?(cond)
|
|
end
|
|
|
|
def foreach_enabled_member
|
|
@data["groups"].each do |group|
|
|
if is_condition_enabled(group["condition"])
|
|
group["members"].each do |member|
|
|
if is_condition_enabled(member["condition"])
|
|
yield group, member
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def foreach_enabled_group
|
|
last = nil
|
|
foreach_enabled_member do |group, member|
|
|
if last != group
|
|
last = group
|
|
yield group
|
|
end
|
|
end
|
|
end
|
|
|
|
def foreach_member
|
|
@data["groups"].each do |group|
|
|
group["members"].each do |member|
|
|
yield group, member
|
|
end
|
|
end
|
|
end
|
|
|
|
def foreach_group
|
|
last = nil
|
|
foreach_member do |group, member|
|
|
if last != group
|
|
last = group
|
|
yield group
|
|
end
|
|
end
|
|
end
|
|
|
|
def initialize_tables
|
|
@data["tables"].each do |tbl|
|
|
name = tbl["name"]
|
|
if @tables.key?(name)
|
|
raise "Duplicate table name #{name}"
|
|
end
|
|
@tables[name] = tbl
|
|
end
|
|
end
|
|
|
|
def ordered_table_names
|
|
@enabled_tables.to_a().sort()
|
|
end
|
|
|
|
def table_constant_name(name)
|
|
upper = name.upcase()
|
|
"TABLE_#{upper}"
|
|
end
|
|
|
|
def table_variable_name(name)
|
|
"table_#{name}"
|
|
end
|
|
|
|
def can_use_byte_offsetof
|
|
buf = StringIO.new
|
|
foreach_enabled_member do |group, member|
|
|
typ = group["type"]
|
|
field = member["field"]
|
|
buf << "static_assert(offsetof(#{typ}, #{field}) < 255, \"#{typ}.#{field} is too big\");\n"
|
|
end
|
|
stderr = compile_test_file(buf)
|
|
return !stderr.include?("static assertion failed")
|
|
end
|
|
|
|
def mktmpdir
|
|
# Use a temporary dir reachable by relative path
|
|
# since g++ in cygwin fails to open files
|
|
# with absolute paths
|
|
tmp = File.join(@output_dir, "tmp")
|
|
FileUtils.mkdir_p(tmp) unless File.directory?(tmp)
|
|
value = yield(tmp)
|
|
if File.directory?(tmp)
|
|
FileUtils.remove_dir(tmp)
|
|
end
|
|
value
|
|
end
|
|
|
|
def compile_test_file(prog)
|
|
buf = StringIO.new
|
|
# cstddef for offsetof()
|
|
headers = ["platform.h", "cstddef"]
|
|
@data["groups"].each do |group|
|
|
gh = group["headers"]
|
|
if gh
|
|
headers.concat gh
|
|
end
|
|
end
|
|
headers.each do |h|
|
|
if h
|
|
buf << "#include \"#{h}\"\n"
|
|
end
|
|
end
|
|
buf << "\n"
|
|
buf << prog.string
|
|
mktmpdir do |dir|
|
|
file = File.join(dir, "test.cpp")
|
|
File.open(file, 'w') {|file| file.write(buf.string)}
|
|
dputs "Compiling #{buf.string}"
|
|
stdout, stderr = @compiler.run(file, File.join(dir, "test"), '-c', noerror: true)
|
|
dputs "Output: #{stderr}"
|
|
stderr
|
|
end
|
|
end
|
|
|
|
def check_conditions
|
|
buf = StringIO.new
|
|
conditions = Set.new
|
|
add_condition = -> (c) {
|
|
if c && !conditions.include?(c)
|
|
conditions.add(c)
|
|
buf << "#ifdef #{c}\n"
|
|
buf << "#pragma message(#{c.inspect})\n"
|
|
buf << "#endif\n"
|
|
end
|
|
}
|
|
|
|
foreach_member do |group, member|
|
|
add_condition.call(group["condition"])
|
|
add_condition.call(member["condition"])
|
|
end
|
|
|
|
stderr = compile_test_file(buf)
|
|
@true_conditions = Set.new
|
|
stderr.scan(/#pragma message\(\"(.*)\"\)/).each do |m|
|
|
@true_conditions << m[0]
|
|
end
|
|
end
|
|
|
|
def sanitize_fields(all=false)
|
|
pending_types = Hash.new
|
|
has_booleans = false
|
|
|
|
block = Proc.new do |group, member|
|
|
if !group["name"]
|
|
raise "Missing group name"
|
|
end
|
|
if !member["name"]
|
|
raise "Missing member name in group #{group["name"]}"
|
|
end
|
|
table = member["table"]
|
|
if table
|
|
if !@tables[table]
|
|
raise "Member #{member["name"]} references non-existing table #{table}"
|
|
end
|
|
@used_tables << table
|
|
end
|
|
if !member["field"]
|
|
member["field"] = member["name"]
|
|
end
|
|
typ = member["type"]
|
|
if !typ
|
|
pending_types[member] = group
|
|
elsif typ == "bool"
|
|
has_booleans = true
|
|
member["type"] = "uint8_t"
|
|
member["table"] = OFF_ON_TABLE["name"]
|
|
end
|
|
end
|
|
|
|
all ? foreach_member(&block) : foreach_enabled_member(&block)
|
|
|
|
if has_booleans
|
|
@tables[OFF_ON_TABLE["name"]] = OFF_ON_TABLE
|
|
@used_tables << OFF_ON_TABLE["name"]
|
|
end
|
|
|
|
resolve_types pending_types unless !pending_types
|
|
foreach_enabled_member do |group, member|
|
|
@count += 1
|
|
@max_name_length = [@max_name_length, member["name"].length].max
|
|
if member["table"]
|
|
@enabled_tables << member["table"]
|
|
end
|
|
end
|
|
end
|
|
|
|
def resolve_types(pending)
|
|
# TODO: Loop to avoid reaching the maximum number
|
|
# of errors printed by the compiler.
|
|
prog = StringIO.new
|
|
prog << "int main() {\n"
|
|
ii = 0
|
|
members = Hash.new
|
|
pending.each do |member, group|
|
|
var = "var_#{ii}"
|
|
members[ii] = member
|
|
ii += 1
|
|
gt = group["type"]
|
|
mf = member["field"]
|
|
prog << "#{gt} #{var}; #{var}.#{mf}.__type_detect_;\n"
|
|
end
|
|
prog << "return 0;\n"
|
|
prog << "};\n"
|
|
stderr = compile_test_file(prog)
|
|
stderr.scan(/var_(\d+).*?', which is of non-class type '(.*)'/).each do |m|
|
|
member = members[m[0].to_i]
|
|
case m[1]
|
|
when /^int8_t/ # {aka signed char}"
|
|
typ = "int8_t"
|
|
when /^uint8_t/ # {aka unsigned char}"
|
|
typ = "uint8_t"
|
|
when /^int16_t/ # {aka short int}"
|
|
typ = "int16_t"
|
|
when /^uint16_t/ # {aka short unsigned int}"
|
|
typ = "uint16_t"
|
|
when /^uint32_t/ # {aka long unsigned int}"
|
|
typ = "uint32_t"
|
|
when "float"
|
|
typ = "float"
|
|
when /^char \[(\d+)\]/
|
|
# Substract 1 to show the maximum string size without the null terminator
|
|
member["max"] = $1.to_i - 1;
|
|
typ = "string"
|
|
else
|
|
raise "Unknown type #{m[1]} when resolving type for setting #{member["name"]}"
|
|
end
|
|
dputs "#{member["name"]} type is #{typ}"
|
|
member["type"] = typ
|
|
end
|
|
# Make sure all types have been resolved
|
|
foreach_enabled_member do |group, member|
|
|
if !member["type"]
|
|
raise "Could not resolve type for member #{member["name"]} in group #{group["name"]}"
|
|
end
|
|
end
|
|
end
|
|
|
|
def initialize_name_encoder
|
|
names = []
|
|
foreach_enabled_member do |group, member|
|
|
names << member["name"]
|
|
end
|
|
best = nil
|
|
(3..7).each do |v|
|
|
enc = NameEncoder.new(names, v)
|
|
if best == nil || best.estimated_size(@count) > enc.estimated_size(@count)
|
|
best = enc
|
|
end
|
|
end
|
|
dputs "Using name encoder with max_length = #{best.max_length}"
|
|
@name_encoder = best
|
|
end
|
|
|
|
def initialize_value_encoder
|
|
values = []
|
|
constants = []
|
|
add_value = -> (v) {
|
|
v = v || 0
|
|
if v.is_number_kind? || (v.class == String && v.is_number?)
|
|
values << v.to_i
|
|
else
|
|
constants << v
|
|
end
|
|
}
|
|
foreach_enabled_member do |group, member|
|
|
add_value.call(member["min"])
|
|
add_value.call(member["max"])
|
|
end
|
|
constantValues = resolve_constants(constants)
|
|
# Count values used by constants
|
|
constants.each do |c|
|
|
values << constantValues[c]
|
|
end
|
|
@value_encoder = ValueEncoder.new(values, constantValues)
|
|
end
|
|
|
|
def resolve_constants(constants)
|
|
return nil unless constants.length > 0
|
|
s = Set.new
|
|
constants.each do |c|
|
|
s << c
|
|
end
|
|
dputs "#{constants.length} constants to resolve"
|
|
# Since we're relying on errors rather than
|
|
# warnings to find these constants, the compiler
|
|
# might reach the maximum number of errors and stop
|
|
# compilation, so we might need multiple passes.
|
|
re = /required from 'class expr_(.*?)<(.*)>'/
|
|
values = Hash.new
|
|
while s.length > 0
|
|
buf = StringIO.new
|
|
buf << "template <int64_t V> class Fail {\n"
|
|
# Include V in the static_assert so it's shown
|
|
# in the error condition.
|
|
buf << "static_assert(V == 42 && 0 == 1, \"FAIL\");\n"
|
|
buf << "public:\n"
|
|
buf << "Fail() {};\n"
|
|
buf << "int64_t v = V;\n"
|
|
buf << "};\n"
|
|
ii = 0
|
|
s.each do |c|
|
|
cls = "expr_#{c}"
|
|
buf << "template <int64_t V> class #{cls}: public Fail<V> {};\n"
|
|
buf << "#{cls}<#{c}> var_#{ii};\n"
|
|
ii += 1
|
|
end
|
|
stderr = compile_test_file(buf)
|
|
matches = stderr.scan(re)
|
|
if matches.length == 0
|
|
puts stderr
|
|
raise "No more matches looking for constants"
|
|
end
|
|
matches.each do |m|
|
|
c = m[0]
|
|
v = m[1]
|
|
# gcc 6.3 includes an ul or ll prefix after the
|
|
# constant expansion, while gcc 7.1 does not
|
|
v = v.tr("ul", "")
|
|
nv = v.to_i
|
|
values[c] = nv
|
|
s.delete(c)
|
|
dputs "Constant #{c} resolved to #{nv}"
|
|
end
|
|
end
|
|
values
|
|
end
|
|
end
|
|
|
|
def usage
|
|
puts "Usage: ruby #{__FILE__} <source_dir> <settings_file> [--json <json_file>]"
|
|
end
|
|
|
|
if __FILE__ == $0
|
|
|
|
verbose = ENV["V"] == "1"
|
|
|
|
src_root = ARGV[0]
|
|
settings_file = ARGV[1]
|
|
if src_root.nil? || settings_file.nil?
|
|
usage()
|
|
exit(1)
|
|
end
|
|
|
|
|
|
opts = GetoptLong.new(
|
|
[ "--output-dir", "-o", GetoptLong::REQUIRED_ARGUMENT ],
|
|
[ "--help", "-h", GetoptLong::NO_ARGUMENT ],
|
|
[ "--json", "-j", GetoptLong::REQUIRED_ARGUMENT ],
|
|
)
|
|
|
|
jsonFile = nil
|
|
output_dir = nil
|
|
|
|
opts.each do |opt, arg|
|
|
case opt
|
|
when "--output-dir"
|
|
output_dir = arg
|
|
when "--help"
|
|
usage()
|
|
exit(0)
|
|
when "--json"
|
|
jsonFile = arg
|
|
end
|
|
end
|
|
|
|
gen = Generator.new(src_root, settings_file, output_dir)
|
|
|
|
if jsonFile
|
|
gen.write_json(jsonFile)
|
|
else
|
|
gen.write_files()
|
|
if verbose
|
|
gen.print_stats()
|
|
end
|
|
end
|
|
end
|