mirror of
https://github.com/mruby/mruby
synced 2026-06-08 16:11:16 +00:00
9ff1aa9d55
Replace separate float formatting (fmt_fp.c) and parsing (readfloat.c) implementations with a unified fp_uscale.c using 128-bit unrounded scaling. Both mrb_format_float() and mrb_read_float() now share a single pow10 table and uscale() primitive for decimal/binary conversion. This fixes subnormal parsing accuracy (old code returned 0.0 for the smallest subnormals) and corrects %.2f rounding for values like 12345.125. Table size grows from ~5KB to ~11KB in .rodata. Co-authored-by: Claude <noreply@anthropic.com>
568 lines
18 KiB
Ruby
568 lines
18 KiB
Ruby
module MRuby
|
|
class Amalgam
|
|
# Top-level headers to include (internal dependencies are inlined recursively)
|
|
# mruby.h is the main header - it includes value.h, gc.h, version.h with proper macro order
|
|
# Note: internal.h is NOT included here - it goes in mruby.c
|
|
HEADER_ORDER = %w[
|
|
mruby.h
|
|
mruby/array.h
|
|
mruby/string.h
|
|
mruby/hash.h
|
|
mruby/class.h
|
|
mruby/proc.h
|
|
mruby/range.h
|
|
mruby/variable.h
|
|
mruby/numeric.h
|
|
mruby/error.h
|
|
mruby/data.h
|
|
mruby/istruct.h
|
|
mruby/mempool.h
|
|
mruby/debug.h
|
|
mruby/dump.h
|
|
mruby/irep.h
|
|
mruby/opcode.h
|
|
mruby/re.h
|
|
mruby/throw.h
|
|
mruby/khash.h
|
|
mruby/endian.h
|
|
mruby/presym.h
|
|
mruby/compile.h
|
|
].freeze
|
|
|
|
# Boxing headers are conditionally included
|
|
BOXING_HEADERS = %w[
|
|
mruby/boxing_no.h
|
|
mruby/boxing_word.h
|
|
mruby/boxing_nan.h
|
|
].freeze
|
|
|
|
# Core sources in recommended order
|
|
CORE_SOURCE_ORDER = %w[
|
|
allocf.c
|
|
readnum.c
|
|
readint.c
|
|
fp_uscale.c
|
|
state.c
|
|
symbol.c
|
|
class.c
|
|
object.c
|
|
gc.c
|
|
mempool.c
|
|
variable.c
|
|
array.c
|
|
hash.c
|
|
string.c
|
|
range.c
|
|
numeric.c
|
|
numops.c
|
|
proc.c
|
|
kernel.c
|
|
enum.c
|
|
error.c
|
|
backtrace.c
|
|
vm.c
|
|
load.c
|
|
dump.c
|
|
cdump.c
|
|
codedump.c
|
|
print.c
|
|
debug.c
|
|
etc.c
|
|
version.c
|
|
init.c
|
|
].freeze
|
|
|
|
def initialize(build)
|
|
@build = build
|
|
@processed_guards = {}
|
|
@processed_headers = [] # Track header paths for include transformation
|
|
# Pre-collect gem header names for source include transformation
|
|
@gem_header_names = collect_gem_header_names
|
|
end
|
|
|
|
def collect_gem_header_names
|
|
names = []
|
|
library_gems.each do |gem|
|
|
gem_include = "#{gem.dir}/include"
|
|
next unless File.directory?(gem_include)
|
|
Dir.glob("#{gem_include}/**/*.h").each do |path|
|
|
rel_path = path.sub("#{gem_include}/", "")
|
|
names << rel_path
|
|
# Also track basename for simple includes like "io_hal.h"
|
|
names << File.basename(rel_path)
|
|
end
|
|
end
|
|
names.uniq
|
|
end
|
|
|
|
def generate_header(output_path)
|
|
FileUtils.mkdir_p(File.dirname(output_path))
|
|
_pp "GEN", output_path.relative_path
|
|
|
|
File.open(output_path, "w:binary") do |f|
|
|
write_header_preamble(f)
|
|
write_ordered_headers(f)
|
|
# Boxing headers are inlined at their include point in value.h
|
|
# Presym headers are inlined via presym.h -> enable.h -> id.h
|
|
write_gem_headers(f)
|
|
write_header_postamble(f)
|
|
end
|
|
end
|
|
|
|
def generate_source(output_path)
|
|
FileUtils.mkdir_p(File.dirname(output_path))
|
|
_pp "GEN", output_path.relative_path
|
|
|
|
File.open(output_path, "w:binary") do |f|
|
|
write_source_preamble(f)
|
|
write_internal_headers(f)
|
|
write_core_sources(f)
|
|
write_generated_sources(f)
|
|
write_gem_sources(f)
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def include_dir
|
|
"#{MRUBY_ROOT}/include"
|
|
end
|
|
|
|
def build_include_dir
|
|
"#{@build.build_dir}/include"
|
|
end
|
|
|
|
def src_dir
|
|
"#{MRUBY_ROOT}/src"
|
|
end
|
|
|
|
# Filter out binary gems (they have main() functions)
|
|
def library_gems
|
|
@build.gems.reject { |gem| gem.name.start_with?("mruby-bin-") }
|
|
end
|
|
|
|
# ========== Header Generation ==========
|
|
|
|
def write_header_preamble(f)
|
|
f.puts <<~PREAMBLE
|
|
/*
|
|
** mruby amalgamated header
|
|
** Generated from mruby source files
|
|
**
|
|
** This file is auto-generated. Do not edit directly.
|
|
*/
|
|
|
|
#ifndef MRUBY_AMALGAM_H
|
|
#define MRUBY_AMALGAM_H
|
|
|
|
#ifdef __cplusplus
|
|
#define __STDC_LIMIT_MACROS
|
|
#define __STDC_CONSTANT_MACROS
|
|
#define __STDC_FORMAT_MACROS
|
|
#endif
|
|
|
|
#include <stdarg.h>
|
|
#include <stdint.h>
|
|
#include <stddef.h>
|
|
#include <limits.h>
|
|
#include <string.h>
|
|
|
|
PREAMBLE
|
|
|
|
# Add build-level defines from gems (e.g., MRB_USE_TASK_SCHEDULER)
|
|
gem_defines = collect_gem_defines
|
|
unless gem_defines.empty?
|
|
f.puts "/* Gem-required defines */"
|
|
gem_defines.each do |d|
|
|
f.puts "#define #{d}"
|
|
end
|
|
f.puts
|
|
end
|
|
end
|
|
|
|
# Collect defines added by gems that affect core headers
|
|
def collect_gem_defines
|
|
defines = []
|
|
@build.defines.each do |d|
|
|
# Include defines that affect mrb_state or core functionality
|
|
defines << d if d =~ /^MRB_USE_|^MRB_UTF8_|^HAVE_MRUBY_/
|
|
end
|
|
defines.uniq.sort
|
|
end
|
|
|
|
def write_header_postamble(f)
|
|
f.puts <<~POSTAMBLE
|
|
|
|
#endif /* MRUBY_AMALGAM_H */
|
|
POSTAMBLE
|
|
end
|
|
|
|
def write_ordered_headers(f)
|
|
# Process top-level headers; internal includes are recursively inlined
|
|
HEADER_ORDER.each do |header|
|
|
path = "#{include_dir}/#{header}"
|
|
next unless File.exist?(path)
|
|
write_header_content(f, header, path)
|
|
end
|
|
end
|
|
|
|
def write_boxing_headers(f)
|
|
f.puts "\n/* Boxing type selection */"
|
|
f.puts "#if defined(MRB_NAN_BOXING)"
|
|
write_header_content(f, "mruby/boxing_nan.h", "#{include_dir}/mruby/boxing_nan.h")
|
|
f.puts "#elif defined(MRB_WORD_BOXING)"
|
|
write_header_content(f, "mruby/boxing_word.h", "#{include_dir}/mruby/boxing_word.h")
|
|
f.puts "#else"
|
|
write_header_content(f, "mruby/boxing_no.h", "#{include_dir}/mruby/boxing_no.h")
|
|
f.puts "#endif"
|
|
end
|
|
|
|
def write_presym_headers(f)
|
|
presym_dir = "#{@build.build_dir}/include/mruby/presym"
|
|
return unless File.directory?(presym_dir)
|
|
|
|
%w[id.h table.h].each do |header|
|
|
path = "#{presym_dir}/#{header}"
|
|
next unless File.exist?(path)
|
|
write_header_content(f, "mruby/presym/#{header}", path)
|
|
end
|
|
end
|
|
|
|
def write_gem_headers(f)
|
|
library_gems.each do |gem|
|
|
gem_include = "#{gem.dir}/include"
|
|
next unless File.directory?(gem_include)
|
|
|
|
Dir.glob("#{gem_include}/**/*.h").sort.each do |path|
|
|
rel_path = path.sub("#{gem_include}/", "")
|
|
write_header_content(f, "#{gem.name}: #{rel_path}", path)
|
|
# Also track the relative path for source include transformation
|
|
# (handles includes like #include "io_hal.h")
|
|
@processed_headers << rel_path unless @processed_headers.include?(rel_path)
|
|
end
|
|
end
|
|
end
|
|
|
|
def write_header_content(f, name, path)
|
|
return unless File.exist?(path)
|
|
|
|
content = File.read(path, mode: "rb")
|
|
guard = extract_include_guard(content)
|
|
|
|
# Skip if already processed
|
|
if guard && @processed_guards[guard]
|
|
f.puts "/* #{name} - already included */"
|
|
return
|
|
end
|
|
@processed_guards[guard] = true if guard
|
|
@processed_headers << name # Track header path for include transformation
|
|
|
|
f.puts "\n/* ======== #{name} ======== */"
|
|
content = strip_include_guard(content, guard) if guard
|
|
content = transform_includes(content, inline: true)
|
|
f.puts content
|
|
end
|
|
|
|
# ========== Source Generation ==========
|
|
|
|
def write_source_preamble(f)
|
|
f.puts <<~PREAMBLE
|
|
/*
|
|
** mruby amalgamated source
|
|
** Generated from mruby source files
|
|
**
|
|
** This file is auto-generated. Do not edit directly.
|
|
*/
|
|
|
|
#include "mruby.h"
|
|
|
|
PREAMBLE
|
|
end
|
|
|
|
def write_internal_headers(f)
|
|
f.puts "/* ======== Internal headers ======== */"
|
|
|
|
# Forward declarations needed for amalgamation
|
|
# (functions called before defined due to source file ordering)
|
|
# Note: mrb_irep_catch_handler_table is static inline in internal.h, no forward decl needed
|
|
f.puts <<~FORWARD
|
|
/* Forward declarations for amalgamation */
|
|
static void mrb_irep_free(mrb_state *mrb, mrb_irep *irep);
|
|
static mrb_value mrb_class_find_path(mrb_state *mrb, struct RClass *c);
|
|
static void mrb_method_added(mrb_state *mrb, struct RClass *c, mrb_sym mid);
|
|
static void mrb_proc_copy(mrb_state *mrb, struct RProc *a, const struct RProc *b);
|
|
static size_t mrb_gc_mark_range(mrb_state *mrb, struct RRange *r);
|
|
static void mrb_ary_decref(mrb_state *mrb, mrb_shared_array *shared);
|
|
static mrb_int mrb_proc_arity(const struct RProc *p);
|
|
FORWARD
|
|
|
|
# internal.h
|
|
internal_path = "#{include_dir}/mruby/internal.h"
|
|
if File.exist?(internal_path)
|
|
content = File.read(internal_path, mode: "rb")
|
|
content = strip_include_guard(content, extract_include_guard(content))
|
|
content = transform_source_includes(content)
|
|
f.puts "\n/* mruby/internal.h */"
|
|
f.puts content
|
|
end
|
|
|
|
# presym/table.h (generated, needed by symbol.c)
|
|
table_path = "#{build_include_dir}/mruby/presym/table.h"
|
|
if File.exist?(table_path)
|
|
content = File.read(table_path, mode: "rb")
|
|
content = transform_source_includes(content)
|
|
f.puts "\n/* mruby/presym/table.h */"
|
|
f.puts content
|
|
end
|
|
|
|
# value_array.h (internal src header)
|
|
value_array_path = "#{src_dir}/value_array.h"
|
|
if File.exist?(value_array_path)
|
|
content = File.read(value_array_path, mode: "rb")
|
|
content = strip_include_guard(content, extract_include_guard(content))
|
|
content = transform_source_includes(content)
|
|
f.puts "\n/* src/value_array.h */"
|
|
f.puts content
|
|
end
|
|
end
|
|
|
|
def write_core_sources(f)
|
|
f.puts "\n/* ======== Core sources ======== */"
|
|
|
|
CORE_SOURCE_ORDER.each do |source|
|
|
path = "#{src_dir}/#{source}"
|
|
next unless File.exist?(path)
|
|
write_source_content(f, "src/#{source}", path)
|
|
end
|
|
|
|
# Clear potentially conflicting macros from core sources
|
|
write_macro_cleanup(f, "core")
|
|
end
|
|
|
|
def write_generated_sources(f)
|
|
# mrblib.c - compiled Ruby stdlib
|
|
mrblib_path = "#{@build.build_dir}/mrblib/mrblib.c"
|
|
if File.exist?(mrblib_path)
|
|
write_source_content(f, "mrblib.c", mrblib_path)
|
|
end
|
|
end
|
|
|
|
# Macros that may conflict between source files in amalgamation
|
|
# These are #undef-ed after each source file within a gem
|
|
CONFLICTING_MACROS = %w[
|
|
mrb_stat
|
|
mrb_lstat
|
|
mrb_fstat
|
|
lesser
|
|
greater
|
|
CASE
|
|
NEXT
|
|
JUMP
|
|
CALL
|
|
node_type
|
|
push
|
|
pop
|
|
peek
|
|
].freeze
|
|
|
|
def write_gem_sources(f)
|
|
f.puts "\n/* ======== Gem sources ======== */"
|
|
|
|
library_gems.each do |gem|
|
|
# Some gems use 'core/' instead of 'src/' (mruby-compiler, mruby-bigint)
|
|
source_dirs = ["#{gem.dir}/src", "#{gem.dir}/core"].select { |d| File.directory?(d) }
|
|
|
|
# Include C sources if the gem has any
|
|
unless source_dirs.empty?
|
|
sources = source_dirs.flat_map { |d| Dir.glob("#{d}/**/*.c") }.sort
|
|
sources.each_with_index do |path, idx|
|
|
rel_path = path.sub("#{gem.dir}/", "")
|
|
write_source_content(f, "#{gem.name}: #{rel_path}", path)
|
|
# Clear macros between source files to avoid conflicts
|
|
# (e.g., mrb_stat macro in file.c vs function in file_test.c)
|
|
write_macro_cleanup(f, "#{gem.name}/#{File.basename(path)}") if idx < sources.size - 1
|
|
end
|
|
end
|
|
|
|
# Gem's compiled mrblib (Ruby-only gems like mruby-enum-ext have this)
|
|
gem_mrblib = "#{gem.build_dir}/gem_mrblib.c"
|
|
if File.exist?(gem_mrblib)
|
|
write_source_content(f, "#{gem.name}: gem_mrblib.c", gem_mrblib)
|
|
end
|
|
|
|
# Gem's init functions (GENERATED_TMP_mrb_*_gem_init/final)
|
|
# Required for both C and Ruby-only gems
|
|
gem_init = "#{gem.build_dir}/gem_init.c"
|
|
if File.exist?(gem_init)
|
|
write_source_content(f, "#{gem.name}: gem_init.c", gem_init)
|
|
end
|
|
|
|
# Clear potentially conflicting macros after each gem
|
|
write_macro_cleanup(f, gem.name)
|
|
end
|
|
|
|
# gem_init.c - gem registration
|
|
gem_init_path = "#{@build.build_dir}/mrbgems/gem_init.c"
|
|
if File.exist?(gem_init_path)
|
|
write_source_content(f, "gem_init.c", gem_init_path)
|
|
end
|
|
end
|
|
|
|
def write_source_content(f, name, path)
|
|
return unless File.exist?(path)
|
|
|
|
content = File.read(path, mode: "rb")
|
|
# For source files, comment out all mruby includes (they're in the header)
|
|
# and inline local includes (like .cstub files)
|
|
source_dir = File.dirname(path)
|
|
content = transform_source_includes(content, source_dir)
|
|
|
|
f.puts "\n/* ======== #{name} ======== */"
|
|
f.puts content
|
|
end
|
|
|
|
def write_macro_cleanup(f, gem_name)
|
|
f.puts "\n/* Cleanup macros from #{gem_name} to avoid conflicts */"
|
|
CONFLICTING_MACROS.each do |macro|
|
|
f.puts "#ifdef #{macro}"
|
|
f.puts "#undef #{macro}"
|
|
f.puts "#endif"
|
|
end
|
|
end
|
|
|
|
# X-macro pattern headers that must be inlined every time they're included
|
|
# (not commented out) because they expand differently based on macro definitions
|
|
XMACRO_HEADERS = %w[
|
|
mruby/ops.h
|
|
].freeze
|
|
|
|
def transform_source_includes(content, source_dir = nil)
|
|
content.gsub(/^(\s*)(#\s*include\s+([<"])([^>"]+)[>"])/m) do |match|
|
|
prefix = $1
|
|
include_stmt = $2
|
|
quote_type = $3 # < or "
|
|
header = $4
|
|
|
|
# X-macro headers must be inlined every time (not commented out)
|
|
# because they expand differently based on surrounding macro definitions
|
|
if XMACRO_HEADERS.include?(header)
|
|
xmacro_path = "#{include_dir}/#{header}"
|
|
if File.exist?(xmacro_path)
|
|
xmacro_content = File.read(xmacro_path, mode: "rb")
|
|
"#{prefix}/* Inlined X-macro: #{header} */\n#{xmacro_content}"
|
|
else
|
|
match
|
|
end
|
|
# Comment out all mruby-related includes and any header already in amalgam
|
|
elsif mruby_header?(header) || header == "mruby.h" ||
|
|
@processed_headers.include?(header) || @gem_header_names.include?(header)
|
|
"#{prefix}// #{include_stmt} - in amalgam header"
|
|
elsif source_dir && quote_type == '"' && !header.include?("/")
|
|
# Check for local includes like "known_errors_def.cstub"
|
|
local_path = "#{source_dir}/#{header}"
|
|
if File.exist?(local_path)
|
|
local_content = File.read(local_path, mode: "rb")
|
|
"#{prefix}/* Inlined: #{header} */\n#{local_content}"
|
|
else
|
|
match
|
|
end
|
|
else
|
|
match
|
|
end
|
|
end
|
|
end
|
|
|
|
# ========== Content Transformation ==========
|
|
|
|
def extract_include_guard(content)
|
|
# Match #ifndef GUARD_NAME at start of file (after comments)
|
|
if content =~ /\A(?:\/\*.*?\*\/\s*|\/\/[^\n]*\n)*\s*#ifndef\s+(\w+)\s*\n\s*#define\s+\1/m
|
|
$1
|
|
end
|
|
end
|
|
|
|
def strip_include_guard(content, guard)
|
|
return content unless guard
|
|
|
|
# Remove opening #ifndef GUARD but KEEP #define GUARD (needed for #ifdef checks)
|
|
content = content.sub(/\A((?:\/\*.*?\*\/\s*|\/\/[^\n]*\n)*\s*)#ifndef\s+#{guard}\s*\n/m, '\1')
|
|
|
|
# Remove closing #endif (any comment is ok, guard name may not match exactly)
|
|
content = content.sub(/\n#endif\s*(?:\/\*[^*]*\*\/|\/\/[^\n]*)?\s*\z/m, "\n")
|
|
|
|
content
|
|
end
|
|
|
|
def transform_includes(content, inline: false)
|
|
# Match both "#include" and "# include" (preprocessor allows spaces)
|
|
# Only match includes at the start of a line (real preprocessor directives)
|
|
content.gsub(/^(\s*)(#\s*include\s+[<"]([^>"]+)[>"])/m) do |match|
|
|
prefix = $1
|
|
include_stmt = $2
|
|
header = $3
|
|
|
|
if already_included?(header)
|
|
# Keep original whitespace, comment out the include
|
|
"#{prefix}// #{include_stmt} - in amalgam"
|
|
elsif inline && mruby_header?(header)
|
|
# Recursively inline this header
|
|
"#{prefix}#{inline_header(header)}"
|
|
else
|
|
match
|
|
end
|
|
end
|
|
end
|
|
|
|
def already_included?(header)
|
|
# Check if header was already processed
|
|
# Only use end_with? matching for mruby headers to avoid matching
|
|
# system headers like <time.h> against mruby/time.h
|
|
if mruby_header?(header)
|
|
@processed_headers.any? { |h| h == header || h.end_with?("/#{header}") }
|
|
else
|
|
@processed_headers.include?(header)
|
|
end
|
|
end
|
|
|
|
def mruby_header?(header)
|
|
# Check if this is an mruby header that should be inlined/transformed
|
|
header == "mruby.h" || header.start_with?("mruby/") ||
|
|
%w[mrbconf.h boxing_nan.h boxing_word.h boxing_no.h common.h object.h value_array.h].include?(header)
|
|
end
|
|
|
|
def inline_header(header)
|
|
# Find the full path for this header
|
|
if header == "mruby.h" || header == "mrbconf.h" || header.start_with?("mruby/")
|
|
full_header = header
|
|
# Check both source include dir and build include dir (for generated headers)
|
|
path = "#{include_dir}/#{header}"
|
|
path = "#{build_include_dir}/#{header}" unless File.exist?(path)
|
|
else
|
|
# Relative includes like "boxing_word.h" -> "mruby/boxing_word.h"
|
|
full_header = "mruby/#{header}"
|
|
path = "#{include_dir}/#{full_header}"
|
|
path = "#{build_include_dir}/#{full_header}" unless File.exist?(path)
|
|
end
|
|
|
|
return "/* #{header} - not found */" unless File.exist?(path)
|
|
|
|
# Mark as processed to avoid infinite recursion
|
|
@processed_headers << header
|
|
@processed_headers << full_header
|
|
|
|
content = File.read(path, mode: "rb")
|
|
guard = extract_include_guard(content)
|
|
|
|
if guard && @processed_guards[guard]
|
|
return "/* #{header} - already included */"
|
|
end
|
|
@processed_guards[guard] = true if guard
|
|
|
|
content = strip_include_guard(content, guard) if guard
|
|
content = transform_includes(content, inline: true)
|
|
|
|
"\n/* ======== #{full_header} (inlined) ======== */\n#{content}"
|
|
end
|
|
end
|
|
end
|