Technical Documentation: facebook/hermes
ℹ️ Provenance: Multi-Tier Ingestion · CodeWiki Reference
Overview (`README.md`)
Hermes JS Engine
Hermes is a JavaScript engine optimized for fast start-up of React Native apps. It features ahead-of-time static optimization and compact bytecode.
If you're only interested in using pre-built Hermes in a new or existing React Native app, you do not need to follow this guide or have direct access to the Hermes source. Instead, just follow these instructions to enable Hermes.
Noted that each Hermes release is aimed at a specific RN version. The rule of thumb is to always follow Hermes releases strictly. Version mismatch can result in instant crash of your apps in the worst case scenario.
If you want to know how to build and hack on Hermes directly, and/or integrate Hermes built from source into a React Native app then read on.
Hermes Blog - Technical articles and deep dives into Hermes internals.
The instructions here very briefly cover steps to build the Hermes CLI. They assume you have typical native development tools setup for your OS, and support for cmake and Ninja. For more details of required dependencies, building Hermes with different options, etc. follow these links instead:
To build a local debug version of the Hermes CLI tools the following steps should get you started on macOS/Linux:
mkdir hermes_workingdir
cd hermes_workingdir
git clone https://github.com/facebook/hermes.git
cmake -S hermes -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug
cmake --build ./buildOr if you're using Windows, the following should get you going in a Git Bash shell:
mkdir hermes_workingdir
cd hermes_workingdir
git -c core.autocrlf=false clone https://github.com/facebook/hermes.git
cmake -S hermes -B build -G 'Visual Studio 16 2019' -A x64
cmake --build ./buildYou will now be in a directory with the output of building Hermes into CLI tools. From here you can run a piece of JavaScript as follows:
echo "'use strict'; function hello() { print('Hello World'); } hello();" | ./bin/hermesContributing
The main purpose of this repository is to continue to evolve Hermes, making it faster and more efficient. We are grateful to the community for contributing bugfixes and improvements. Read below to learn how you can take part in improving Hermes.
Code of Conduct
Facebook has adopted a Code of Conduct that we expect project participants to adhere to. Please read the full text so that you can understand what actions will and will not be tolerated.
Contributing Guide
Read our contributing guide to learn about our development process, how to propose bugfixes and improvements, and how to build and test your changes to Hermes.
License
Hermes is MIT licensed.
Overview (`agent-perf/README.md`)
Static Hermes Agent Performance Infrastructure
Tools for measuring, analyzing, and comparing Static Hermes performance.
All tools output compact (single-line) structured JSON (--json) for
programmatic parsing and human-readable text by default. Compact JSON
minimizes token consumption when AI agents ingest tool output. Designed
for both human engineers and AI agents.
Quick Start
# Build shermes with cmake
cmake -B cmake-build-release -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build cmake-build-release --target shermes
# Run benchmarks
agent-perf/tools/run_benchmarks.sh <shermes> micro --json
# A/B performance comparison (statistical)
agent-perf/tools/benchmark_compare.sh <shermes_a> <shermes_b> --suite all
# A/B binary size comparison
agent-perf/tools/binary_size_compare.sh --baseline <shermes_a> --optimized <shermes_b> --suite octane
# Analyze generated C code
shermes -emit-c -o /tmp/out.c test.js
agent-perf/tools/generated_code_analyzer.py -i /tmp/out.c --json
# Quick performance health check
agent-perf/tools/perf_proxies.sh <shermes> benchmarks/octane/richards.jsDirectory Structure
agent-perf/
├── tools/ 24 analysis and measurement tools
├── benchmarks/
│ ├── micro/ 9 targeted subsystem benchmarks (<5s each)
│ │ ├── arithmetic.js
│ │ ├── array_operations.js
│ │ ├── closures_scopes.js
│ │ ├── control_flow.js
│ │ ├── function_calls.js
│ │ ├── object_creation.js
│ │ ├── property_access.js
│ │ ├── string_operations.js
│ │ └── typed_operations.js
│ └── macro/ 4 app-representative workloads
│ ├── app_startup.js
│ ├── component_tree.js
│ ├── json_processing.js
│ └── module_system.js
├── schemas/
│ └── perf_schema.json JSON output format documentation
└── tests/ 13 unit test files (250+ tests)Additionally, Octane benchmarks live at benchmarks/octane/ in the SH root.
CLI Conventions
All tools follow these conventions:
--help/-h— prints usage, arguments, and examples--json— outputs compact (single-line) structured JSON, optimized for minimal token consumption by AI agents--typed— compiles with Static Hermes typed mode--compare <shermes_b>— A/B comparison mode (shell tools)- Exit code 0 on success, non-zero on error or regression detection
- Shell tools accept positional arguments; Python tools use named flags
Tool Reference
Benchmarking and Comparison
`run_benchmarks.sh` — Benchmark Runner
Compiles and runs the standard benchmark suites, extracting results fromRESULT: lines (micro/macro) or Octane score lines.
Questions answered:
- How fast is this shermes build on the standard benchmarks?
- What are the ops/sec for each micro/macro benchmark?
- What is the Octane score?
Usage: run_benchmarks.sh <shermes_binary> [micro|macro|octane|all] [options]
Arguments:
<shermes_binary> Path to the shermes compiler binary (required)
[micro|macro|octane|all] Benchmark suite to run (default: all)
Options:
--json Output structured JSON
--typed Compile with --typed flag
--iterations <N> Number of iterations per benchmark (default: 5)
--octane Also run benchmarks/octane/ suite
--shermes-args "ARGS" Extra arguments to pass to shermes`benchmark_compare.sh` — Statistical A/B Comparison
Statistical A/B comparison of two shermes binaries. Uses interleaved execution
for bias reduction and Mann-Whitney U test for significance.
Questions answered:
- Is binary B faster or slower than binary A?
- Are performance differences statistically significant?
- What is the geometric mean delta across benchmarks?
Usage: benchmark_compare.sh <shermes_a> <shermes_b> [options]
Arguments:
<shermes_a> Path to baseline shermes binary (required)
<shermes_b> Path to candidate shermes binary (required)
Options:
--suite <name> Benchmark suite: micro, macro, all (default: all)
--json Output structured JSON
--typed Compile with --typed flag
--iterations <N> Iterations per benchmark (default: 11)
--threshold <PCT> Regression threshold percentage (default: 2.0)Exit code 1 if any benchmark regresses beyond threshold.
`count_instructions.sh` — Deterministic Instruction Counts
Measures retired CPU instruction counts using perf stat -e instructions.
Instructions are the most deterministic performance metric — not affected by
system load or thermal throttling.
Questions answered:
- How many instructions does this workload execute?
- Did a compiler change increase or decrease instruction count?
- How many instructions does compilation itself take?
Usage: count_instructions.sh <js_file> [options]
Arguments:
<js_file> JavaScript file to measure (required)
Options:
--shermes <path> Path to shermes binary (auto-detected if not set)
--compare <a> <b> A/B comparison mode with two shermes binaries
--iterations <N> Number of iterations (default: 11)
--typed Compile with --typed flag
--compile-only Measure compilation instructions only (no execution)
--json Output structured JSONBinary Size Analysis
`binary_size_compare.sh` — A/B Binary Size Comparison
End-to-end binary size comparison for Static Hermes compiled JS programs.
Compiles benchmarks with two shermes builds and compares .text section sizes,
call site counts, and per-call-site overhead. Optionally uses bloaty for
deeper section/symbol/compilation-unit analysis when available.
Questions answered:
- How much does a compiler/header change affect generated binary size?
- How many
_sh_ljs_get_by_val_rjs(or other) call sites exist? - What is the per-call-site overhead of an inlined fast path?
- (with
--bloaty) Which sections, symbols, and compilation units grew most?
Usage: binary_size_compare.sh --baseline <shermes_a> --optimized <shermes_b> [options]
Options:
--baseline <path> Path to baseline shermes binary (required)
--optimized <path> Path to optimized shermes binary (required)
--js <file.js> JS file to compile (can specify multiple times)
--suite <name> Benchmark suite: micro, macro, octane, all
--typed Compile with --typed flag
--strip Compare stripped binaries (removes debug info)
--bloaty Use bloaty for deeper symbol/section analysis (if installed)
--json Output structured JSONWhen --bloaty is specified and bloaty is installed, each benchmark's output
includes additional section-level, symbol-level (top 20), and compilation-unit
(top 10) breakdowns from bloaty's DWARF-aware analysis. Falls back to the
default nm/objdump analysis if bloaty is not available.
`binary_size_analyzer.py` — Symbol-Level Size Analysis
Parses nm/objdump output from two binaries to produce a detailed
symbol-level size comparison. Categorizes symbols and identifies the
largest contributors to size growth.
Questions answered:
- How much did the
.textsection grow/shrink? - Which individual symbols changed size the most?
- Are there new inlined functions present in B but not A?
- What is the
get_by_val-related symbol growth?
Usage: binary_size_analyzer.py [options]
Options:
--baseline <path> Path to baseline binary
--optimized <path> Path to optimized binary
--nm-a <path> Path to pre-captured nm output for baseline
--nm-b <path> Path to pre-captured nm output for optimized
--json Output JSON
Either --baseline/--optimized or --nm-a/--nm-b is required.Symbol categories: get_by_val, get_by_id, property_put, calls,arithmetic, generated_js, runtime, other.
Generated Code Analysis
`generated_code_analyzer.py` — Runtime Call Analysis
The primary diagnostic tool for understanding generated C code. Categorizes
every _sh_* runtime call, reports per-function statistics, and detects
anti-patterns.
Questions answered:
- How many runtime calls are in the generated code?
- Which category dominates (property access, GC, arithmetic, etc.)?
- Which functions have the highest call density?
- Are there redundant type checks or excessive GC scoping?
Usage: generated_code_analyzer.py -i <generated.c> [options]
Options:
-i, --input <path> Path to .c file from shermes -emit-c (required)
--json Output JSONCategories tracked: property_access, type_check, gc, arithmetic,comparison, call, object, string, other.
Detected patterns: redundant_type_check, excessive_gc_scoping,inline_opportunity, high_call_density.
`generated_code_antipatterns.py` — C Compiler Optimization Inhibitors
Scans generated .c files for patterns that prevent the C compiler from
optimizing well — artifacts of code generation that indicate improvement
opportunities in shermes.
Questions answered:
- Does the generated C code have patterns that prevent optimization?
- Are there unnecessary GC root operations? Redundant casts?
- Is there excessive pointer aliasing?
Usage: generated_code_antipatterns.py -i <generated.c> [options]
Options:
-i, --input <path> Path to generated .c file (required)
--json Output JSONDetected patterns (by severity): excessive_pointer_aliasing,redundant_null_check, excessive_function_calls, unnecessary_gc_root,redundant_cast, large_stack_frame, unreachable_after_throw.
`generated_code_report.sh` — Per-Function Code Metrics
Generates C code and reports per-function metrics: lines of C, runtime call
count, and complexity indicators (branch count).
Questions answered:
- How many functions did the compiler generate?
- How large is each function? What is its runtime call count?
- How does generated code differ between two shermes builds?
Usage: generated_code_report.sh <shermes_binary> <workload.js> [options]
Options:
--typed Compile with --typed flag
--json Output structured JSON
--compare <shermes_b> Compare generated code from two shermes binaries`sh_source_mapper.py` — JS-to-C Line Mapping
Maps between JS source, generated C, and native code by parsing #line
directives in C code generated by shermes -emit-c. Essential for correlating
native code hotspots with original JS source.
Questions answered:
- Which JS source line does C line 150 correspond to?
- Which C lines were generated from JS line 25?
- What is the full source map between JS and generated C?
Usage: sh_source_mapper.py -i <generated.c> [options]
Options:
-i, --input <path> Path to .c file from shermes -emit-c (required)
--c-line <N> Query: find JS source line for this C line
--js-line <N> Query: find all C lines for this JS line
--js-file <name> Filter --js-line results to a specific JS file
--json Output JSON`type_coverage_analyzer.py` — Typed Mode Effectiveness
Compares two generated .c files (typed vs. untyped) to measure how
effectively typed mode eliminates or reduces runtime calls.
Questions answered:
- How effective is typed mode at eliminating runtime calls?
- Which
_sh_*calls are fully eliminated by type information? - What is the per-function type coverage percentage?
- Are there calls that increased in typed mode (regressions)?
Usage: type_coverage_analyzer.py -t <typed.c> -u <untyped.c> [options]
Options:
-t, --typed <path> Path to typed-mode generated .c file (required)
-u, --untyped <path> Path to untyped-mode generated .c file (required)
--json Output JSONProfiling and Hotspot Analysis
`collect_pmu.sh` — Hardware PMU Counter Collection
Collects hardware Performance Monitoring Unit counters using perf stat.
Computes derived ratios (IPC, miss rates). Requires Linux perf.
Questions answered:
- What is the IPC for this workload?
- What are the cache, branch, and TLB miss rates?
- How do two shermes builds compare on hardware counters?
Usage: collect_pmu.sh <shermes_binary> <workload.js> [options]
Options:
--typed Compile with --typed flag
--json Output structured JSON
--compare <shermes_b> A/B comparison with second shermes binary
--shermes-args "ARGS" Extra arguments to pass to shermesCounters: instructions, cycles, L1-dcache-load-misses,L1-icache-load-misses, branch-misses, dTLB-load-misses.
`pmu_anomalies.py` — PMU Anomaly Detection
Accepts PMU counter data as JSON and flags values that exceed known-good
thresholds. Pairs with collect_pmu.sh --json output.
Questions answered:
- Are there microarchitectural performance anomalies?
- Is IPC suspiciously low?
- Are cache or branch misprediction rates abnormal?
Usage: pmu_anomalies.py [-i <pmu_data.json>] [options]
Options:
-i, --input <path> Path to PMU JSON data (default: stdin)
--json Output JSONThresholds: IPC < 1.0, branch miss > 10%, L1-icache miss > 5%,
L1-dcache miss > 10%, dTLB miss > 3%.
`profile_to_json.py` — Perf Report Parser
Converts perf report --stdio output to structured JSON, extracting
per-function profiling data.
Questions answered:
- What are the hottest functions in a profiled binary?
- How are samples distributed across modules?
Usage: profile_to_json.py [-i <perf_report.txt>] [options]
Options:
-i, --input <path> Path to perf report output (default: stdin)
-n, --top <N> Show only top N entries
-f, --filter <substr> Filter to entries whose module contains substring
--json Output JSON`annotate_disasm.py` — Instruction-Level Hotspot Annotation
Produces hotness-annotated disassembly from perf annotate output, marking
hot instructions (>=1.0% samples) with >>>.
Questions answered:
- Which assembly instructions are the hottest?
- Where is the CPU spending the most time at the instruction level?
Usage: annotate_disasm.py [-i <perf_annotate.txt>] [options]
Options:
-i, --input <path> Path to perf annotate --stdio output (default: stdin)
-f, --function <name> Filter to a specific function (substring match)
--json Output JSON`annotate_source.py` — Source-Level Hotspot Annotation
Overlays line-level profile data onto source code, mapping perf annotate
sample counts back to source lines and marking hot lines with [HOT X.X%].
Questions answered:
- Which source code lines consume the most CPU time?
- Where are the hotspots in the source (not assembly) view?
Usage: annotate_source.py [-i <perf_annotate.txt>] [options]
Options:
-i, --input <path> Path to perf annotate --stdio output (default: stdin)
-f, --function <name> Filter to a specific function (substring match)
--json Output JSON`runtime_call_profiler.py` — Runtime Function Profiling
Profiles _sh_* runtime function usage from perf report output. Ranks by
sample count, groups by category, and identifies top optimization candidates.
Questions answered:
- Which
_sh_*runtime functions consume the most CPU? - Which category (property access, GC, arithmetic) dominates?
- What are the top optimization candidates?
Usage: runtime_call_profiler.py [-i <perf_report.txt>] [options]
Options:
-i, --input <path> Path to perf report --stdio output (default: stdin)
-n, --top <N> Number of top candidates to show (default: 20)
--json Output JSONAnti-Pattern Scanners
`js_antipattern_scan.py` — JavaScript Anti-Patterns
Scans JavaScript source for patterns that exercise slow engine paths in
Hermes/Static Hermes, preventing effective AOT compilation.
Questions answered:
- Does this JS code use patterns that are slow in Hermes?
- Are there uses of
eval,with,arguments,delete? - Are there sparse arrays or megamorphic property accesses?
Usage: js_antipattern_scan.py -i <file_or_dir> [options]
Options:
-i, --input <path> JS file or directory to scan (required)
--json Output JSONDetected patterns: eval_usage, with_statement,arguments_materialization, megamorphic_access, dynamic_property_name,prototype_pollution, delete_operator, try_catch_in_loop,sparse_array, implicit_global.
`antipattern_scan.py` — C++ Performance Anti-Patterns
Scans C++ source files for common performance anti-patterns. Optionally
cross-references with profiling data to focus on hot functions.
Questions answered:
- Does this C++ code contain known performance anti-patterns?
- Are large types being passed by value?
- Are there redundant container lookups?
Usage: antipattern_scan.py -i <file_or_dir> [options]
Options:
-i, --input <path> C++ source file or directory (required)
-p, --profile <path> Profile JSON for cross-referencing hot functions
--json Output JSONDetected patterns: pass_by_value, repeated_lookup,string_concat_loop, virtual_dispatch_hot, unnecessary_copy,redundant_find_access.
Compiler Analysis
`compiler_stats.sh` — Pass Statistics
Captures compiler pass statistics from shermes --print-stats, including
LLVM STATISTIC() counters and pass timing.
Questions answered:
- How many inlining decisions were made?
- How much time does each pass take?
- What are the DCE/CSE/type inference counters?
Usage: compiler_stats.sh <shermes_binary> <workload.js> [options]
Options:
--typed Compile with --typed flag
--json Output structured JSON`compilation_profiler.sh` — Pipeline Stage Timing
Measures time for each compilation stage independently: JS-to-C, C-to-object,
and full pipeline (including linking).
Questions answered:
- Which compilation stage is the bottleneck?
- How long does JS-to-C take vs. C compilation vs. linking?
- What are the file sizes at each stage?
Usage: compilation_profiler.sh <shermes_binary> <workload.js> [options]
Options:
--typed Compile with --typed flag
--json Output structured JSON`pass_experiment.sh` — Optimization Pass Marginal Value
Measures the marginal value of a specific optimization pass by comparing the
full pipeline vs. pipeline with that pass excluded.
Questions answered:
- How much does a specific pass (inlining, DCE, CSE) contribute?
- What happens to code size and performance if we disable it?
Usage: pass_experiment.sh <shermes_binary> <workload.js> --disable <pass> [options]
Arguments:
<shermes_binary> Path to shermes (required)
<workload.js> JavaScript file (required)
Options:
--disable <pass_name> Pass to exclude (required)
--typed Compile with --typed flag
--json Output structured JSONAvailable passes: dce, cse, mem2reg, instsimplify, simplifycfg,stackpromotion, typeinference, inlining, codemotion, funcsigopts,tdzdedup.
`regalloc_report.py` — Register Allocation Quality
Analyzes register allocation quality from shermes -dump-ra output. Reports
per-function register count, spill count, and max register pressure.
Questions answered:
- How many registers does each function use?
- Which functions have the most spills?
- Are there register allocation quality problems?
Usage: regalloc_report.py [-i <dump_ra.txt>] [options]
Options:
-i, --input <path> Path to shermes -dump-ra output (default: stdin)
--json Output JSONGC Analysis
`gc_stats.sh` — Garbage Collection Statistics
Compiles JS, runs the native binary, and captures GC statistics from stderr.
Computes allocation rate, GC overhead %, and survival ratio.
Questions answered:
- What is the GC overhead for this workload?
- What is the allocation rate?
- How does GC behavior differ between two builds?
Usage: gc_stats.sh <shermes_binary> <workload.js> [options]
Options:
--typed Compile with --typed flag
--json Output structured JSON
--compare <shermes_b> A/B comparison with second shermes binaryMulti-Metric
`perf_proxies.sh` — Quick Performance Health Check
Fast multi-dimensional performance assessment without requiring hardware
counters. Measures code size, call density, and compilation latency.
Questions answered:
- What is the overall "performance health" of a shermes build?
- How do code size, call density, and compilation latency compare?
Usage: perf_proxies.sh <shermes> <js_file_or_dir> [options]
Options:
--typed Compile with --typed flag
--compare <shermes_b> A/B comparison mode
--json Output structured JSONMetrics: generated C size (lines/bytes), native binary size, runtime call
density (_sh_* calls), compilation latency (ms per stage), function count
and average size.
Recommended Workflows
Workflow 1: Performance Regression Check
Quick check for performance regressions between two shermes builds.
# Statistical A/B comparison with significance testing
agent-perf/tools/benchmark_compare.sh <baseline> <candidate> --suite all --json
# Deterministic instruction count comparison (most stable metric)
agent-perf/tools/count_instructions.sh benchmarks/octane/richards.js \
--compare <baseline> <candidate> --iterations 21Workflow 2: Binary Size Impact Assessment
Quantify code size cost of an optimization that inlines code into generated C.
# Step 1: A/B binary size across Octane
agent-perf/tools/binary_size_compare.sh \
--baseline <shermes_a> --optimized <shermes_b> --suite octane
# Step 2: Symbol-level drill-down on a specific benchmark
agent-perf/tools/binary_size_analyzer.py \
--baseline baseline_richards.o --optimized optimized_richards.oWorkflow 3: Generated Code Quality Analysis
Understand what the compiler produces and find optimization opportunities.
# Generate C code
shermes -emit-c -o /tmp/out.c workload.js
# Analyze runtime calls by category
agent-perf/tools/generated_code_analyzer.py -i /tmp/out.c --json
# Find code patterns that inhibit C compiler optimization
agent-perf/tools/generated_code_antipatterns.py -i /tmp/out.c
# Map hotspots back to JS source
agent-perf/tools/sh_source_mapper.py -i /tmp/out.c --c-line 150Workflow 4: Typed Mode Effectiveness
Measure how much type information improves generated code.
# Generate both typed and untyped C code
shermes -emit-c -o /tmp/untyped.c workload.js
shermes -emit-c --typed -o /tmp/typed.c workload.js
# Compare runtime call elimination
agent-perf/tools/type_coverage_analyzer.py -t /tmp/typed.c -u /tmp/untyped.c --jsonWorkflow 5: Deep Profiling Investigation
Full profiling workflow from PMU counters to source-level hotspots.
# Step 1: Collect PMU counters
agent-perf/tools/collect_pmu.sh <shermes> workload.js --json > /tmp/pmu.json
# Step 2: Check for anomalies
agent-perf/tools/pmu_anomalies.py -i /tmp/pmu.json
# Step 3: Profile with perf
perf record -g -- ./compiled_binary
perf report --stdio > /tmp/report.txt
# Step 4: Parse profile and find hot runtime functions
agent-perf/tools/profile_to_json.py -i /tmp/report.txt --json
agent-perf/tools/runtime_call_profiler.py -i /tmp/report.txt --json
# Step 5: Annotate source with hotspots
perf annotate --stdio > /tmp/annotate.txt
agent-perf/tools/annotate_source.py -i /tmp/annotate.txtWorkflow 6: Compiler Pass Analysis
Evaluate the contribution of individual optimization passes.
# Measure marginal value of inlining
agent-perf/tools/pass_experiment.sh <shermes> workload.js --disable inlining --json
# Compare pass statistics between builds
agent-perf/tools/compiler_stats.sh <shermes> workload.js --jsonTool Summary Table
| Tool | Category | Questions Answered |
|---|---|---|
run_benchmarks.sh |
Benchmark | How fast is this build? What are the ops/sec? |
benchmark_compare.sh |
Benchmark | Is B faster/slower than A? Statistically significant? |
count_instructions.sh |
Benchmark | How many instructions? Deterministic delta? |
binary_size_compare.sh |
Binary Size | .text growth? Call sites? Per-site overhead? |
binary_size_analyzer.py |
Binary Size | Which symbols grew? What category? |
generated_code_analyzer.py |
Code Analysis | Runtime call categories? Call density? Patterns? |
generated_code_antipatterns.py |
Code Analysis | C optimization inhibitors? GC root waste? |
generated_code_report.sh |
Code Analysis | Per-function metrics? A/B code diff? |
sh_source_mapper.py |
Code Analysis | JS line for C line? C lines for JS line? |
type_coverage_analyzer.py |
Code Analysis | Type coverage %? Eliminated calls? |
collect_pmu.sh |
Profiling | IPC? Cache/branch/TLB miss rates? |
pmu_anomalies.py |
Profiling | Microarch anomalies? Abnormal rates? |
profile_to_json.py |
Profiling | Hottest functions? Sample distribution? |
annotate_disasm.py |
Profiling | Hot assembly instructions? |
annotate_source.py |
Profiling | Hot source lines? |
runtime_call_profiler.py |
Profiling | Hot _sh_* functions? Top optimization targets? |
js_antipattern_scan.py |
Anti-Pattern | Slow JS patterns? eval/with/arguments? |
antipattern_scan.py |
Anti-Pattern | C++ anti-patterns? Pass-by-value? |
compiler_stats.sh |
Compiler | Pass counters? DCE/CSE/inline stats? |
compilation_profiler.sh |
Compiler | Stage bottleneck? JS-to-C vs C vs link? |
pass_experiment.sh |
Compiler | Marginal value of a pass? |
regalloc_report.py |
Compiler | Register pressure? Spill count? |
gc_stats.sh |
GC | GC overhead %? Allocation rate? |
perf_proxies.sh |
Multi-Metric | Overall health? Code size + density + latency? |
Related Files
.llms/rules/sh_perf_benchmarking.md— Performance benchmarking rules (opt-mode, metrics, statistics).llms/rules/sh_backend.md— SH code generation rules.llms/rules/sh_runtime.md— SH runtime library rules.llms/rules/sh_type_system.md— Typed mode rules.llms/rules/sh_compiler_pipeline.md— Compilation pipeline rules.llms/skills/sh_perf_investigation.md— Performance investigation workflowschemas/perf_schema.json— JSON output format documentation
Testing
All Python tools have unit tests in tests/ using Python's built-in unittest
module. Tests use synthetic input data — no shermes binary, perf counters, or
build system required.
# Run ALL tests (from xplat/static_h/)
python3 -m unittest discover -s agent-perf/tests -p 'test_*.py' -v
# Run a single test file
python3 -m unittest agent-perf/tests/test_generated_code_analyzer.py -v
# Run a specific test class
python3 -m unittest agent-perf.tests.test_generated_code_analyzer.TestAnalyzeGeneratedCode -v
# Run a specific test method
python3 -m unittest agent-perf.tests.test_generated_code_analyzer.TestAnalyzeGeneratedCode.test_basic_analysis -v
# Validate all shell scripts (syntax check only)
for f in agent-perf/tools/*.sh; do bash -n "$f"; doneImportant: Always run from the xplat/static_h/ directory. Tests import
tools via relative path (sys.path.insert to agent-perf/tools/).
Overview (`API/hermes_abi/README.md`)
Experimental C based stable ABI for Hermes
This directory contains ongoing work to develop a stable C-based ABI for Hermes and an accompanying C++ JSI wrapper. It is a work in progress and is not supported for general use.
The goal of this ABI is to allow Hermes to be updated independently of the rest of a React Native application. Note that this does not immediately solve the general problem of ABI stability for RN extensions, since they are still written against the C++ JSI, and consume a C++ jsi::Runtime provided by RN. However, that is an eventual goal of this work.
Overview (`API/hermes_sandbox/README.md`)
Hermes Sandboxed Runtime implementation
This directory contains a sandboxed version of Hermes that is produced by compiling Hermes to wasm, and then compiling the resulting wasm to C using wasm2c. The sandboxed code exposes Hermes' C-API, and JSI is re-implemented on top of it.
The generated C is pre-generated and checked in to simplify the build. It will need to be periodically updated to incorporate bug fixes, and especially when there are changes to JSI or the bytecode version.
Re-generating the sandbox
Install emsdk by following the instructions here. Then download wasm2c from wabt.
Build hermesc for the host platform, in order to generate bytecode that is used by the Hermes build. The commands below use build_host to refer to this host build.
Generate the debug build with the following command. Note that it is built with O2 and without slow asserts to avoid slowing down execution too dramatically, since the generated C code for the debug build is then typically built without optimisations.
cmake -S <path to hermes> -B build_wasm_dbg \
-DCMAKE_TOOLCHAIN_FILE=<path to emsdk>/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake \
-DIMPORT_HERMESC=build_host/ImportHermesc.cmake \
-DCMAKE_BUILD_TYPE=Debug -DHERMES_UNICODE_LITE=ON \
-DCMAKE_CXX_FLAGS=-O2 -DCMAKE_C_FLAGS=-O2 \
-DCMAKE_EXE_LINKER_FLAGS="-sALLOW_MEMORY_GROWTH=1 -sSTACK_SIZE=256KB -sGLOBAL_BASE=16384" \
-DHERMES_ENABLE_DEBUGGER=OFF -DHERMES_SLOW_DEBUG=OFF \
-DHERMES_IS_MOBILE_BUILD=ON -G NinjaThen generate the C artefact by running the following:
cmake --build build_wasm_dbg --target hermesSandboxImpl \
&& wasm2c build_wasm_dbg/API/hermes_sandbox/hermesSandboxImpl.wasm -n hermes --num-outputs 8 \
-o <path to hermes>/API/hermes_sandbox/external/hermes_sandbox_impl_dbg_compiled.cLikewise for the release build. Note that the release build is still built with -g2 to preserve function names. Since the generated C is going to be compiled anyway, this doesn't have any effect on the final size of release builds.
cmake -S <path to hermes> -B build_wasm_opt \
-DCMAKE_TOOLCHAIN_FILE=<path to emsdk>/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake \
-DIMPORT_HERMESC=build_host/ImportHermesc.cmake \
-DCMAKE_BUILD_TYPE=Release -DHERMES_UNICODE_LITE=ON \
-DCMAKE_EXE_LINKER_FLAGS="-sALLOW_MEMORY_GROWTH=1 -sSTACK_SIZE=256KB -sGLOBAL_BASE=16384 -g2" \
-DHERMES_ENABLE_DEBUGGER=OFF -DHERMES_IS_MOBILE_BUILD=ON -G Ninja
cmake --build build_wasm_opt --target hermesSandboxImpl \
&& wasm2c build_wasm_opt/API/hermes_sandbox/hermesSandboxImpl.wasm -n hermes --num-outputs 8 \
-o <path to hermes>/API/hermes_sandbox/external/hermes_sandbox_impl_opt_compiled.cThe currently generated artefact was generated using emsdk 3.1.39, and wasm2c 1.0.33. Note that using a newer wasm2c will also require updating the checked in wasm-rt files in the external directory, which provide the runtime library for the generated code.
Overview (`API/napi/README.md`)
Hermes Node-API
This is an implementation of Node-API
(N-API) v10 for the Hermes JavaScript engine. It allows native addons written
against the Node-API ABI to run on Hermes without modification.
The implementation is built directly on Hermes VM internals (not JSI), covering
all non-experimental APIs through NAPI_VERSION 10. See
COMPATIBILITY.md for the full API support matrix.
Architecture
napi_value is a pointer to a PinnedHermesValue slot in a handle scope
block pool. Handle scopes, references, and other NAPI-managed state live in thenapi_env__ struct, which registers a custom GC roots function with the
Hermes Runtime.
Native addon callbacks are bridged through NativeConstructor trampolines.
Async work and thread-safe functions use a pluggable host integration interface
(hermes_napi_host) that the host application provides.
Source Files
| File | Purpose |
|---|---|
hermes_napi.h |
Core env struct, handle scopes, refs, macros |
hermes_napi_internal.h |
Internal helpers and trampoline declarations |
hermes_napi.cpp |
Env lifecycle, GC root marking, module loading |
hermes_napi_value.cpp |
Primitives, typeof, coercion, comparison |
hermes_napi_string.cpp |
String create/extract (UTF-8, UTF-16, Latin-1) |
hermes_napi_object.cpp |
Property operations, define_properties, freeze/seal |
hermes_napi_function.cpp |
Functions, call, new_instance, define_class |
hermes_napi_error.cpp |
Error creation and throwing |
hermes_napi_reference.cpp |
Strong/weak references |
hermes_napi_external.cpp |
External values (DecoratedObject) |
hermes_napi_wrap.cpp |
Object wrapping and finalizers |
hermes_napi_scope.cpp |
Handle scopes, instance data, cleanup hooks |
hermes_napi_arraybuffer.cpp |
ArrayBuffer create, external, detach |
hermes_napi_typedarray.cpp |
TypedArray (all 11 types) |
hermes_napi_dataview.cpp |
DataView |
hermes_napi_buffer.cpp |
Buffer (as Uint8Array) |
hermes_napi_promise.cpp |
Promise create/resolve/reject |
hermes_napi_bigint.cpp |
BigInt create/extract |
hermes_napi_typetag.cpp |
128-bit type tags |
hermes_napi_tsfn.cpp |
Thread-safe functions |
hermes_napi_async_work.cpp |
Async work |
hermes_napi_async_context.cpp |
Async context and callback scopes |
Building
# Build the NAPI library
cmake --build <build-dir> --target hermesNapi
# Build and run unit tests
cmake --build <build-dir> --target NapiTests && <build-dir>/unittests/napi/NapiTests
# Run all NAPI test suites (CTS + vendored Node.js tests)
cmake --build <build-dir> --target check-napiTesting
Tests are organized in three layers:
- Unit tests (
unittests/napi/) — 31 GTest files testing individual APIs
in isolation. - Node.js NAPI tests (
external/node-api-tests/) — ~65 test files
vendored from the Node.js source tree. 33 pass, 31 skipped (due to Node.js
module dependencies, experimental APIs, or Hermes-specific limitations).
Zero failures. - node-api-cts (
external/node-api-cts/) — The official conformance test
suite (currently a WIP upstream project with minimal coverage).
Known Limitations
- External strings (
node_api_create_external_string_*) always copy the
data. Hermes does not support external string storage. napi_adjust_external_memorytracks the counter but does not influence
GC scheduling.napi_get_uv_event_loopreturns the host-provideduv_loopif set inhermes_napi_host, otherwise returnsnapi_generic_failure.- Weak reference shutdown ordering — all weak references use a
WeakRefSlotmanaged by the GC. Destroying refs during Runtime teardown
can trigger assertions in debug builds because the ref lifetime outlives
the Runtime. A few Node.js tests are skipped for this reason. - Experimental APIs (
NAPI_EXPERIMENTAL) are not included.
Overview (`benchmarks/bench-runner/README.md`)
benchmark_runner
Introduction
bench-runner is a tool to run benchmarks and compare results, between different engines, and between different versions of the same engine. It is loosely based on gc_bench, but:
- Doesn't try to get detailed GC statistics -- it just uses elapsed time, as reported by the benchmarks themselves. (NB: any benchmark run in this framework must report it's time, printing a line of the form "Time:
". That is, the difference between a Date() created at the start and end of execution.) - Does allow the benchmark to be run by multiple different engines.
- Does allow runs with the same engine, before/after some change, to be compared.
Basic operation
To run the benchmarks:
python3 xplat/static_h/benchmarks/bench-runner/bench-runner.py --hermes -b ~/sworkspace/build_release/bin/hermes --cats v8The result of this will be something like:
Ran each benchmark 1 times
Runtime: hermes
General Stats:
Benchmark total
(time)
--------------------------------------------------------
v8-crypto 552.0 s +- 0.0%
v8-deltablue 787.0 s +- 0.0%
v8-raytrace 163.0 s +- 0.0%
v8-regexp 329.0 s +- 0.0%
v8-richards 854.0 s +- 0.0%
v8-splay 139.0 s +- 0.0%Arguments
bench_runner shares many arguments with gc_bench.
You can specify the set of benchmarks to run, either individually:
- --benchmarks (or --bm) benchmark1 ... benchmarkN
or as a whole category:
- --categories (or --cats) cat1 ... catN
The benchmark and categories names are available via --help.
Other arguments:
- --count (-c) N: run each benchmark N times.
- --keep-tmp (-k): retain bytecode files for hermes.
- --verbose (-v): verbose output.
- --debug (-d): debug output.
- --hermes: use the hermes vm.
- --synth: use the synthetic benchmark interpreter.
Some arguments are new in bench-runner, or have added options:
--binary (-b): path to the binary to run as the engine. Before was just hermes or synthtrace; now can be an arbitrary engine, like v8 or jsc.
--output-format (-f): format of the output. Was "ascii" or "tab-separated". To this we add "json": write the results as a json file, suitable as input for the merger program, described below.
--out file: write the output to the specified file. This is especially useful with "-f json".
--v8, --v8jitless: as with --hermes, specify that you want to run with v8 (or v8 with the --jitless flag). Note that this requires a --binary specification; it doesn't have a default for where v8 lives.
Using bench-merge
Say you want to compare v8 and hermes at some point in time, or hermes before and after some modification. Let's consider the latter case. I built a release build of hermes in the "before" revision, and then ran, from the bench-runner directory:
python3 bench-runner.py --hermes -b ~/sworkspace/build_release/bin/hermes --cats v8 octane -l before -f json --out ~/tmp/before.json -c 5Then I moved to the "after" revision, again built hermes, and ran:
python3 bench-runner.py --hermes -b ~/sworkspace/build_release/bin/hermes --cats v8 octane -l after -f json --out ~/tmp/after.json -c 5
python3 bench-merge.py ~/tmp/before.json ~/tmp/after.json > ~/tmp/before-after.txtThe last output file, before-after.txt, has output like:
runner benchmark Total time ratio
================================================================================================================
hermes (before) box2d 1677.4 s +- 0.8% 1.000
hermes (after) box2d 1653.4 s +- 0.3% 0.986
----------------------------------------------------------------------------------------------------------------
hermes (before) code-load 2376.0 s +- 1.5% 1.000
hermes (after) code-load 2433.0 s +- 4.8% 1.024
...
hermes (before) v8-splay 115.4 s +- 0.8% 1.000
hermes (after) v8-splay 113.8 s +- 1.7% 0.986
----------------------------------------------------------------------------------------------------------------
hermes (before) geomean 1.000
hermes (after) geomean 0.988TBD:
- Benchmarks must be modified to measure and print their own elapsed time, in a known engine-independent way. Will add more benchmarks.
- Add jsc.
- We should probably make the heap-size configs apply in some reasonable way to non-Hermes engines.
- Add labels for comparisons between different versions of the same runtime. (The output above is from a later version that already has this.)
- Might want a way to run, say, JSC vs Hermes in one command. (That might be a composite script of the commands in this diff.)
Overview (`benchmarks/imgui-demo/README.md`)
ImGUI Demo
Imported and modified from https://github.com/tmikov/gpt-scroller/tree/c-impl.
Overview (`benchmarks/jit-benches/README.md`)
Random benchmarks that we found useful while working on the JIT.
They should eventually be moved to appropriate directories.
Overview (`benchmarks/many-subclasses/README.md`)
This tests performance when accessing properties in a parent class via
instances of different child classes. The same function is called instances
of different child classes, but only accessed fields in their common parent
class.
many.js
This is the JS version of the code.
many-sh-1.js
This is the SH version of the code, with type annotation. It can be compiled
and executed with the following command:
shermes -typed many-sh-1.js && ./a.outmany-sh-2.js
Is a version of the benchmark where we have prevented inlinling of the bench()
function by returning it, which creates a second reference. The goal is to
examine the quality of the code in the non-inlined function.
A problem immediately become apparent: the types of N and res are notnumber, which forces emitting of slower out-of-line instructions.
Another problem is that N is not treated as a constant.
The reason for this are two-fold:
- We still don't fully type variables. That code is coming soon.
- Type inference can't do its job, because the variables are initialized too
late - in the middle of the function. That means there is a period of time
where both are initialized toundefined(without TDZ; TDZ doesn't change
the picture here). We can't provde thatbench()isn't called at that time
and doesn't see theundefined.
many-sh-3.js
It improves on many-sh-2.js by moving the initialization of res and N to
the top. Now N is finally treated as a constant, and we don't see undefined
as a valid type for res.
However another problem becomes apparent: we still infer that res can be a
string.
many-sh-4.js
It improves on the previous one by marking bench() as inline. This allows us
to inline the invocation, while also keeping the body around. This is slightly
faster, but still nowhere as performant as the fully inlined version.
Overview (`benchmarks/nbody/README.md`)
nbody benchmark
The Computer Language Benchmarks Game
https://salsa.debian.org/benchmarksgame-team/benchmarksgame/
contributed by Isaac Gouy
modified by Andrey Filatkin
License is BSD 3-Clause, see LICENSE.md
Tests floating point numerical computation and reading properties from an object.
There are type-annotated versions in fully-typed directory.
- fully-typed/nbody.js: the original file was type-annotated with Flow types.
Bodywas turned into ES6 class. - fully-typed/nbody.ts: copied from the flow-annotated file. The type declaration for built-in functions (e.g.
print()) are not yet available, so the use site wasts-ignored.
Overview (`benchmarks/octane/README.md`)
Octane
Obtained from this fork of Octane at 2bfcf3dcec9353fe836028964569eca1c3dd876c.
The files in this directory are modified by putting the related source files together so that each file is individually complete and executable. Each file is prefixed with the base.js that was originally available in the repository.
The following files are are further concatenated:
- gbemu.js includes both gbemu-part1.js and gbemu-part2.js
- typescript.js includes all of typescript.js, typescript-input.js and typescript-compiler.js
- zlib.js include both zlib.js and zlib-data.js.
Furthermore, original zlib benchmark's code is entirely contained in a single string that is then passed to eval in zlib-data.js. The original content in zlib-data.js has been modified with the following transformation:
- Get the body of the benchmark by replacing eval() with print ().
- Apply prettier.
- Expose a dummy function read(), which isn't used but is referred to.
Finally, at the end of each file, a call to BenchmarkSuite.RunSuites is invoked with the corresponding Print functions.
Overview (`benchmarks/raytracer/README.md`)
Raytrace Benchmark
Copied from https://github.com/microsoft/TypeScriptSamples/tree/b675f11d4ef8b7f94aa563586332415407bfe168/raytracer .
Overview (`benchmarks/sqlite-read/README.md`)
Reading an SQLite3 DB into memory
This benchmark compares the time it takes to read an sqlite3 database into memory in C++ and Static Hermes.
The actual DB API is mocked using a very simple implementation in sqmock.cpp, implementing only a few
SQLite3 APIs.
Overview (`benchmarks/widgets/README.md`)
widgets benchmark
Features used:
- classes (inheritance, methods, field lookups)
- array methods and
Map
Directories:
original/: the original Flow annotated benchmark.single-file/: packaged in a single file.single-file/cpp/: C++ version.simple-classes/: uses only Flow classes, no other complex types.
The /stripped/ subdirectory in every case contains the JS without Flow type
annotations.
The /es5/ subdirectory, where present, contains a version of the code lowered
to ES5. When lowering a single file only Babel is used, but when lowering
multuple modules we use Webpack.
Overview (`doc/blog/README.md`)
Hermes Blog
Technical articles and deep dives into Hermes internals.
Posts
2026
- Jun 5 - What's New in the Next Stable Release 260318099.0.0 - New JS features, faster JSON/Map, Unicode 17, and bug fixes.
2025
- Dec 12 - New Way to Contribute to Hermes - JSI-based extensions make contributing easier and safer.
- Dec 2 - ISerialization: Efficient Binary Encoding for JS Values - New serialization API for passing values between runtimes.
- Nov 24 - JSON Parsing Performance: 2.7x-3.4x Faster - Major JSON.parse() optimizations now in static_h.
- Nov 6 - Hermes Memory Modes - HV64 vs HV32 and how Hermes handles different platforms.
- Nov 2 - Hermes Compilation and Runtime Modes - Overview of AOT, bytecode, JIT, and how they can be mixed.
- Jul 15 - Static_h Branch Performance: June 2025 Update - hv32+jit and hv32+interpreter benchmarks vs QuickJS and Hermes.
- Jun 9 - Introducing JSI's New Runtime Data APIs - New setRuntimeData/getRuntimeData methods for per-runtime storage.
2024
- Dec 23 - Compiling Full-Featured JavaScript to Wasm - How Static Hermes compiles ES6 to WebAssembly.
- Dec 19 - Static Hermes Update, December 2024 - ES6 classes, block scoping, and Octane benchmark improvements.
- Nov 9 - Octane Benchmark Results: Hermes JIT Progress - Comparing original Hermes, static_h branch, and JIT on Raspberry Pi.
Overview (`examples/ffi/README.md`)
FFI Examples
This directory contains examples of using the FFI to call C functions from Static Hermes.
These examples are not meant to be production quality, but rather to demonstrate
the basic usage of the FFI. They don't necessarily have robust error checking.
Only ASCII strings are supported (no UTF-8, though it would not be difficult to add).
For now these examples have only been tested on MacOS, but they should work on Linux.
getenv.js
A very simple example reading the PATH environment variable and printing it.
To run it:
shermes -typed -exec getenv.jsfopen.js
An example demonstrating fopen(), fread() and fclose(). To run use:
shermes -typed -exec fopen.jssqlite.js
Demonstrates using libsqlite3. Opens a databas, executes a query, and prints the results.
This example requires that libsqlite3 be installed on your system. On MacOS, it is already installed.
On Linux you may need to install it. You may need to supply -L and -Wc,-I flags to shermes.
# First create the database
./init_db.sh
# Then run the example
shermes -typed -exec sqlite.js -lsqlite3Overview (`external/asmjit/README.md`)
Imported AsmJIt from https://github.com/asmjit/asmjit.git
2024-08-11
330aa64386f394e090eb1062c645f9d021a761bc
Deleted .git, .github and .gitignore
Overview (`external/boost/README.md`)
This directory contains a slightly modified partial copy of Boost needed by Boost.Context.
- Downloaded https://archives.boost.io/release/1.86.0/source/boost_1_86_0.tar.
bz2. - Copied only the parts needed by Boost.Context.
- Deleted
boost/context/continuation*.hppsince onlyfiberis needed. - Deleted
boost/context/pooled_fixedsize_stack.hppbecause of its
dependency onintrusive_ptr. - Removed unused include of
intrusive_ptr.hppfromboost/context/fiber_fcontext.hpp. - Minimal updates to
libs/context/CMakeLists.txtto remove other library
dependencies, remove library alias, supportCMAKE_OSX_ARCHITECTURES - Rename the
boostnamespace tohoostusing a preprocessor define. - Rename the
xxx_fcontext()globals to have ahoost_prefix. - Change the include path from
boost/tohoost/.
Overview (`external/dragonbox/README.md`)
Downloaded dragonbox.h from https://github.com/jk-jeon/dragonbox/blob/master/include/dragonbox/dragonbox.h, and placed under external/dragonbox/dragonbox/
Downloaded 2025-12-26
Git commit: beeeef91cf6fef89a4d4ba5e95d47ca64ccb3a44
Patches
Worked around an MSVC VS2019 (vs16.11.31) bug with template template
parameter forwarding in a class template's default non-type argument. The
upstream SFINAE pattern bool = is_in_range(0) as a default
template argument of compute_impl triggers MSVC errors C2672/C3207 because
the compiler fails to resolve Info as a class template in that context.
Wrapped the call in a helper struct is_in_range_v and changed the default
to is_in_range_v<...>::value; MSVC handles the struct instantiation
correctly. Clang and GCC compile both forms.
Patch: patches/msvc-is-in-range-wrapper.patch.
Overview (`external/esprima/README.md`)
Esprima (esprima.org, BSD license) is a high performance,
standard-compliant ECMAScript
parser written in ECMAScript (also popularly known as
JavaScript).
Esprima is created and maintained by Ariya Hidayat,
with the help of many contributors.
Features
- Full support for ECMAScript 2017 (ECMA-262 8th Edition)
- Sensible syntax tree format as standardized by ESTree project
- Experimental support for JSX, a syntax extension for React
- Optional tracking of syntax node location (index-based and line-column)
- Heavily tested (~1500 unit tests with full code coverage)
API
Esprima can be used to perform lexical analysis (tokenization) or syntactic analysis (parsing) of a JavaScript program.
A simple example on Node.js REPL:
> var esprima = require('esprima');
> var program = 'const answer = 42';
> esprima.tokenize(program);
[ { type: 'Keyword', value: 'const' },
{ type: 'Identifier', value: 'answer' },
{ type: 'Punctuator', value: '=' },
{ type: 'Numeric', value: '42' } ]
> esprima.parseScript(program);
{ type: 'Program',
body:
[ { type: 'VariableDeclaration',
declarations: [Object],
kind: 'const' } ],
sourceType: 'script' }For more information, please read the complete documentation.
Overview (`external/fast_float/README.md`)
Downloaded fast_float.h from https://github.com/fastfloat/fast_float/releases, and placed under external/fast_float/fast_float/
Version 8.1.0, downloaded 2025-11-5
88b1e5321c6918cbe091afd76728296290668403
Overview (`external/flowtest/README.md`)
Flow git commit: 0d843c455cf38fb059d1b693afe59c4f4360b5d4
Overview (`external/llvh/README.md`)
LLVH
This is a copy of LLVM libraries and headers that are used by Hermes.
The copy was made from https://github.com/llvm-mirror/llvm.git with
hash c179d7b006348005d2da228aed4c3c251590baa3 .
gtest patches
There's a bug in googletest-1.8.0 when compiling on Windows, with clang,
with exceptions disabled: GTEST_HAS_EXCEPTIONS isn't defined to the correct
value.
Pulled in this fix: https://github.com/google/googletest/commit/3498a1ac52deb83f30b8170c78bfba9dc6227198
from googletest-1.10.0 into googletest-1.8.0.
It only modifies ./utils/unittest/googletest/include/gtest/internal/gtest-port.h.
Fixed cxxabi.h detection for MSVC under Wine. MSVC doesn't have cxxabi.h, and__has_include is buggy under Wine (see https://bugs.winehq.org/show_bug.cgi?id=54130).
Patch: ./utils/unittest/googletest/patches/gtest-port-msvc-cxxabi.patch.
Added UniversalTersePrinter specialization for std::nullptr_t to fix MSVC
ambiguous operator<< error.
Patch: ./utils/unittest/googletest/patches/gtest-printers-nullptr.patch.
Added UniversalTersePrinter specialization for char16_t*. MSVC explicitly
deletes operator<< for char16_t* in <ostream>. Uses proper C-style
escaping consistent with gtest's char*/wchar_t* string printing.
Patch: ./utils/unittest/googletest/patches/gtest-printers-char16.patch.
Suppressed -Wundef warnings in gtest.h and gmock.h by wrapping the
header contents with #pragma GCC diagnostic push/ignored/pop. These headers
use #if on macros that may not be defined, which is harmless but noisy under-Wundef. Files modified:
./utils/unittest/googletest/include/gtest/gtest.h./utils/unittest/googlemock/include/gmock/gmock.h
LIT patches
In order to fix some Windows python3 problems, it was necessary to pull in some
updates to the LIT testing tool from LLVM.
The version used was LLVM release v9.0.1,
commit hash c1a0a213378a458fbea1a5c77b315c7dce08fd05, from November 27, 2019.
This was done like so:
git clone https://github.com/llvm/llvm-project.git
cd llvm-project
git checkout c1a0a213378a458fbea1a5c77b315c7dce08fd05
cp -r ./llvm/utils/lit/* /path/to/hermes/external/llvh/utils/litSome copied files were omitted because we don't use them
(like lit self-tests and examples).
Update: pull in the run_under argument in googletest.py, from LLVM release
v20.1.8, commit hash 87f0227cb60147a26a1eeb4fb06e3b505e9c7261.
Skip empty RUN lines after substitution in TestRunner.py. When a
substitution replaces a RUN line with an empty string, the line is dropped.
If all RUN lines are dropped, the test is marked UNSUPPORTED. This allowslit.cfg to disable specific tools (e.g. %shermes) by substituting them
with a marker and then replacing the entire line with an empty string.
Patch: patches/lit-skip-empty-run-lines.patch.
Other imports from v9.0.1
Support/JSON and Support/ToolOutputFile.
Rename to LLVH
To avoid conflicts when linking with mainline LLVM, the namespace and include directories were
manually renamed to llvh. That left renaming some C public symbols. Unused files under llvh-c/
were removed and lastly a manual rename of the remaining global symbols was performed. The last
step is recorded in patches/rename-c-interface.patch.
abi-breaking.h
Upstream LLVM generates abi-breaking.h via CMake's configure_file(),
defining LLVM_ENABLE_ABI_BREAKING_CHECKS and LLVM_ENABLE_REVERSE_ITERATION.
LLVH does not use the LLVM build system, so include/llvh/Config/abi-breaking.h
is a hand-written header that defines both macros to 0. Without these
definitions, #if LLVM_ENABLE_ABI_BREAKING_CHECKS silently evaluates to 0
(correct behavior, but produces -Wundef warnings).
Overview (`external/node-api-cts/README.md`)
Node-API Conformance Test Suite (`node-api-cts`)
A test suite for Node-API implementors across different JS engines and runtimes.
Written in ECMAScript & C/C++ with an implementor customizable harness.
[!IMPORTANT]
This repository is currently a work-in-progress and shouldn't yet be relied on by anyone.
Overview
Tests
The tests are divided into three buckets:
tests/harness/*exercising the implementor's test harness.
and two parts based on the two header files declaring the Node-API functions:
tests/js-native-api/*testing the engine-specific part of Node-API defined in thejs_native_api.hheader.tests/node-api/*testing the runtime-specific part of Node-API defined in thenode_api.hheader.
Implementors
This repository offers an opportunity for implementors of Node-API to maintain (parts of) their implementor-specific harness inside this repository, in a sub-directory of the implementors directory. We do this in hope of increased velocity from faster iteration and potentials for reuse of code across the harnesses.
We maintain a list of other runtimes implementing Node-API in doc/node-api-engine-bindings.md of the nodejs/abi-stable-node repository.
Overview (`lib/InternalJavaScript/README.md`)
Internal Bytecode
Promise
The Promise Internal Bytecode is generated on the fly by running:
$ utils/gen-promise-internal-bc.shIterator helpers
The Iterator Helper polyfill is adapted from https://github.com/es-shims/iterator-helpers/ at commit fbd6ffee76f28c0aa1f99180d6971caa6a36f39c. The Iterator.concat polyfill was later adapted at commit b790e7409435f3bd3f43edbf9dc1d962818d8f46. The required dependencies are taken and flattened in 05-IteratorHelperDependencies.js and the actual Iterator implementation are followed after the file. The dependencies may be simplified from the original source. Assertions from the polyfill, which threw TypeErrors, are removed.
Note the original polyfill's dependencies included many polyfills for features that are supported by Hermes. In those cases, they are may be removed and use Hermes implementation directly.
Examples (non-exhausive):
Errors are supported so its polyfill implementation is removed.- Checks for
Symbols support are removed since Hermes supportSymbols. - Hermes implements
Function.prototype.bind, so the polyfill for this is removed.
The implementation for Iterator.prototype.constructor and Iterator.prototype [ %Symbol.toStringTag% ] differs from the original polyfill in order to adhere to spec compliance. The changes uses the polyfill for the abstract operation SetterThatIgnoresPrototypeProperties to help with the implementation.
At the time of adaptation, the original polyfill is based off the proposal at https://tc39.es/proposal-iterator-helpers/. This differs slightly from the spec, and the polyfill reflects some of these changes:
- The original polyfill uses
this instanceof methodNameto guard against constructor calls. This triggers[[GetPrototypeOf]]on the receiver, which is observable via Proxy and violates the spec's expected side-effect ordering. Replaced withnew.target !== undefined, which is not observable and correctly detects constructor calls. - An
IteratorRecordwith undefined[[NextMethod]]is created prior to argument checking. The original polyfill checks the argument first, then obtains theIteratorRecorddirectly viaGetIteratorDirect. - The
ToPrimiveAbstract Operation in the polyfill is not spec compliant. It should check%Symbol.toPrimitive%, which has been added in our adaption of the polyfill. - The
SLOT.assertfunction from theinternal-slotpackage callschannel.assert, which usesobjectInspectto build its error message.objectInspectcallsObject.prototype.toString.call(obj), which accessesobj[Symbol.toStringTag]— an observable side effect on Proxy objects. Removed thechannel.assertcall since the subsequentSLOT.hascheck is sufficient and does not trigger any observable property accesses.
Overview (`tools/hcdp/README.md`)
Usage
In the hcdp directory:
- Build
hcdp binaryusing BUCK or CMake - Install npm packages
npm install- Run
hcdp.js, passing it the path to thehcdp binary, and the script to be debugged.
e.g.:
node ./hcdp.js ~/hcdp ~/loop.jsImplementation
The hcdp tool carries out Hermes debugging via CDP. There are
two main parts to the tool: a JavaScript component and a C++ component.
C++
The C++ component uses the Hermes API to host the objects required to carry out a debugging session (Hermes Runtime, CDP Debug API, CDP Agents). It accepts IPC messages instructing it when to create/destroy a CDP Agent or handle a message. It emits IPC messages for responses and notifications emitted from CDP Agents.
JavaScript
The JavaScript component hosts a websocket server to communicate with debug clients, prints CDP messages as they flow back and forth, and handles keyboard input from the user.
Incoming websocket messages from the debug client are converted to IPC messages to the C++ component. IPC messages from the C++ component are converted to websocket messages back to the debug client.
IPC Messages
IPC messages carry a type, agentId, and optionally message. The following message types are supported:
C- Connect.agentIdindicates the unique ID of the new agent to be created.messageis unused.M- Message.agentIdindicates the ID of the previously-created agent that should handle themessage.D- Disconnect.agentIdindicates the ID of the previously-created agent to be destroyed.messageis unused.
Overview (`tools/secdiff/README.md`)
secdiff
Section-by-section interactive diff tool using Python's curses.
Input:
A file with some number of sections that have lines delimiting the sections.
Usage:
secdiff.py [-h] -s SEPARATOR filenameProvide a separator marker with '-s' and the file to diff the sections of.
Keybindings:
- l/h: Go to the next/prev pair of sections to diff.
- j/k: Scroll down/up in the current diff.
Overview (`unsupported/juno/README.md`)
Juno
Use of Juno is currently unsupported
Juno is an optimizing compiler which consumes and outputs JavaScript.
Overview (`utils/testsuite/README.md`)
Hermes Test Runner
How to run
The runner requires two paths:
- Path to the test suite directory that we support (i.e., test262, mjsunit,
esprima, flow and a CVE testsuite). This can be
a list separated by whitespace, so you can run multiple test suites at once. - Path to the directory of Hermes binaries, including
hermes(andshermes
depending on extra flags).
Example usage:
# ./build is the cmake build directory.
# flow
./utils/test_runner.py external/flowtest/test/flow -b ./build/bin
# esprima
.utils/test_runner.py external/esprima/test_fixtures -b ./build/binUse --compile-args and --vm-args if you want to apply extra compliler or VM
flags to run the testsuite. By default, we use the below flags for test262 and
the CVE testsuite:
# For hermes compiler
-test262 -fno-static-builtins -Xes6-block-scoping -Xenable-tdz
# For hermes VM (running the bytecode)
-Xes6-proxy -Xhermes-internal-test-methods -Xmicrotask-queueSince esprima testsuite includes only AST tests, we simply use -dump-ast. For
flow testsuite, we use additionally below flags:
-parse-flow -Xparse-component-syntax -parse-jsx -Xinclude-empty-ast-nodes -Xparse-flow-matchSkiplist
skiplist.json is a config file that contains a list of tests that we want to
skip by default. It has several sections:
- "manual_skip_list": This is managed manually. We usually put tests that we plan
to support soon here and remove them manually once it's supported. - "skip_list": We put all tests that currently fail due to spec incompliance or
unsupported feature here. - "lazy_skip_list": Tests we want to skip when "--lazy" is passed.
- "permanent_skip_list": Tests we do not plan to fix or support.
- "handlesan_skip_list": Tests that should be run with "-gc-sanitize-handles=0".
- "unsupported_features": Tests with unsupported features in test262. We may
support them in the future. - "permanent_unsupported_features": Tests with unsupported features in 262. We
don't have a plan to support them. - "intl_tests": Tests requires INTL to be enabled.
- "platform_skip_list": Tests that need to be skipped depending on platforms.
Run with flag --test-skiplist to force running tests that are included in theskiplist.json config (all other sections, and "lazy_skip_list" if --lazy is
given, "intl_tests" if --test-intl is given).
After it finishes, all passed tests included in the skiplist config file will be
printed, and you can tell the runner to remove these tests from the config by
following the prompt (we will only update "skip_list" and "permanent_skip_list"
section for now). Note that if some tests are covered by a folder path in the
config, the test runner will first find all tests under that folder, exclude
those passed, and insert the rest into the config file. If you don't want this
behavior for a specific folder, please add it to the manual list with a comment
explaining the reason.
Overview (`API/hermes/extensions/README.md`)
Hermes Extensions
This directory contains JSI-based extensions that add functionality to the Hermes JavaScript runtime.
Why JSI?
Extensions use JSI (JavaScript Interface) rather than internal Hermes APIs. This provides:
- Stability: JSI is a stable public API. Extensions won't break when Hermes internals change.
- Portability: JSI works with any JSI-compatible runtime, not just Hermes.
- Simplicity: JSI is easier to use than internal VM APIs.
Directory Structure
extensions/
├── 00-header.js # IIFE wrapper (do not modify)
├── 99-footer.js # IIFE wrapper (do not modify)
├── 20-Dummy.js # Example core extension (JS)
├── Dummy.h/cpp # Example core extension (C++)
├── Extensions.h/cpp # Extension installation orchestration
├── Intrinsics.h/cpp # Captures built-in constructors
├── JSIUtils.h/cpp # Shared utility functions for extensions
├── ExtensionsBytecode.* # Generated bytecode embedding
└── contrib/ # Community-contributed extensions
└── README.md # Contributor guidelinesHow Extensions Work
Each extension has two parts:
JavaScript file (
NN-ExtensionName.js): Defines a setup function assigned toextensions.ExtensionName. The numeric prefix controls load order.C++ files (
ExtensionName.h/cpp): Calls the JS setup function, optionally passing native helper functions.
At runtime:
- All JS files are concatenated and compiled to bytecode at build time
- The bytecode is embedded in the binary and executed at startup
- Each extension's C++ installer retrieves and calls its JS setup function
Adding an Extension
Core Extensions
Core extensions are maintained by the Hermes team. To add one:
- Create
NN-ExtensionName.jswith your setup function - Create
ExtensionName.handExtensionName.cpp - Add the C++ file to
EXTENSIONS_CPP_SOURCESinCMakeLists.txt - Call your installer from
Extensions.cpp
Community Contributions
See contrib/README.md for guidelines on contributing extensions.
Intrinsics
The Intrinsics.h/cpp files capture references to built-in constructors (like TypeError, Uint8Array) before any user code runs. Extensions should use these captured intrinsics rather than reading from globals, which users might have replaced.
#include "Intrinsics.h"
// Use captured TypeError instead of global
throwTypeError(rt, "Expected a string");JSI Utilities
The JSIUtils.h header provides common utilities for working with JSI, such as TypedArray buffer access and safe integer conversion. Include it to get Intrinsics.h as well. See the header for documentation.
Overview (`benchmarks/MiniReact/no-deps/README.md`)
This is the MiniReact benchmark with the import statements deleted.
The relevant dependencies have been inlined when possible, e.g. invariant.
Overview (`benchmarks/MiniReact/no-objects/README.md`)
This is the no-objects MiniReact benchmark with objects and other unsupported
features removed.
Setup
(cd ~/builds; cmake -S ~/fbsource/xplat/static_h/benchmarks/ -B benchmarksdebug -G Ninja -DHERMES_BUILD=~/builds/shdebug -DHERMES_SRC=~/fbsource/xplat/static_h) && (cd ~/fbsource/xplat/static_h/benchmarks/build-helpers/flow-bundler; yarn install)Run with Static Hermes
~/fbsource/xplat/static_h/benchmarks/MiniReact/no-objects/build.sh && (cd ~/builds/shdebug/; ninja hermesvm shermes && ./bin/shermes -typed -exec -g ~/fbsource/xplat/static_h/benchmarks/MiniReact/no-objects/out/simple.js)Run with Hermes
~/fbsource/xplat/static_h/benchmarks/MiniReact/no-objects/build.sh && (cd ~/builds/shdebug/; ninja hermesvm hermes && ./bin/hermes -exec -g ~/fbsource/xplat/static_h/benchmarks/MiniReact/no-objects/out/simple-lowered.js)Overview (`benchmarks/build-helpers/flow-bundler/README.md`)
Flow Bundler
This bundler allows providing any number of entry points then producing a single
"bundle" file that includes all entrypoints and any imported files. The bundler
does not do any lowering so maintains all Flow typing needed for Static Hermes.
Usage
First install the needed npm dependencies:
yarn install
// or
npm installBasic usage:
./benchmarks/build-helpers/flow-bundler/bin/flow-bundler \
--out output/path/of/bundle.js
--root project/root
path/to/entrypoint1.js
path/to/entrypoint2.jsFor more detailed usage see "help":
./benchmarks/build-helpers/flow-bundler/bin/flow-bundler --helpOverview (`benchmarks/raytracer/original/README.md`)
TypeScript Sample: Raytracer
Overview
This sample shows a raytracer implementation in TypeScript.
Running
tsc --sourcemap raytracer.ts
start raytracer.htmlOverview (`benchmarks/widgets/original/README.md`)
Original Widgets Benchmark
main.js is a copy of main.mjs used only by Webpack.
Overview (`benchmarks/widgets/simple-classes/README.md`)
simple classes version
- no
static - no tuple or object types
Overview (`benchmarks/widgets/single-file/README.md`)
single file widgets benchmark
Concatenate all the files in original together in an order which typechecks with flow.
Remove all uses of import/export.
Overview (`external/asmjit/asmjit/README.md`)
AsmJit
AsmJit is a lightweight library for machine code generation written in C++ language.
- Official Home Page (asmjit.com)
- Official Repository (asmjit/asmjit)
- Public Chat Channel
- Zlib License
See asmjit.com page for more details, examples, and documentation.
Documentation
Contributing
- See CONTRIBUTING page for more details
Breaking Changes
Breaking the API is sometimes inevitable, what to do?
- See Breaking Changes Guide, which is now part of AsmJit documentation
- See asmjit tests, they always compile and provide implementation of many use-cases:
- asmjit_test_emitters.cpp - Tests that demonstrate the purpose of emitters
- asmjit_test_assembler_x86.cpp - Tests targeting AsmJit's Assembler (x86/x64)
- asmjit_test_compiler_x86.cpp - Tests targeting AsmJit's Compiler (x86/x64)
- asmjit_test_instinfo.cpp - Tests that query instruction information
- asmjit_test_x86_sections.cpp - Multiple sections test.
- Visit our Gitter Chat if you need a quick help
Project Organization
/- Project root- src - Source code
- asmjit - Source code and headers (always point include path in here)
- core - Core API, backend independent except relocations
- arm - ARM specific API, used only by ARM and AArch64 backends
- x86 - X86 specific API, used only by X86 and X64 backends
- asmjit - Source code and headers (always point include path in here)
- test - Unit and integration tests (don't embed in your project)
- tools - Tools used for configuring, documenting, and generating files
- src - Source code
Ports
- 32-bit ARM/Thumb port (work in progress)
- RISC-V port (not in progress, help welcome)
Support
- AsmJit project has both community and commercial support, see AsmJit's Support Page
- You can help the development and maintenance through Petr Kobalicek's GitHub sponsors Profile
Notable Donors List:
Authors & Maintainers
- Petr Kobalicek [email protected]
Overview (`external/boost/boost_1_86_0/README.md`)
Boost C++ Libraries
The Boost project provides free peer-reviewed portable C++ source libraries.
We emphasize libraries that work well with the C++ Standard Library. Boost
libraries are intended to be widely useful, and usable across a broad spectrum
of applications. The Boost license encourages both commercial and non-commercial use
and does not require attribution for binary use.
The project website is www.boost.org, where you can obtain more information and
download the current release.
Overview (`external/node-api-tests/hermes/README.md`)
Hermes Node.js NAPI Test Harness
This directory contains the harness for running the Node.js NAPI test
suite under Hermes. The test sources in ../js-native-api/ and../node-api/ are vendored from Node.js.
Patches to Vendored Tests
The following vendored test files have been patched for Hermes
compatibility. All patches are marked with [Hermes patch] comments.
V8-specific error message regexes
Hermes and V8 use different error message formats for strict-mode
TypeError. For example:
V8: "Cannot assign to read only property 'x' of object '#<Object>'"
Hermes: "Cannot assign to read-only property 'x'"
V8: "Cannot set property x of #<Object> which has only a getter"
Hermes: "Cannot assign to property 'x' which has only a getter"The assert.throws() regexes in these tests were relaxed to accept both
formats while still verifying the correct error type and property name:
js-native-api/test_properties/test.jsjs-native-api/test_constructor/test.jsjs-native-api/6_object_wrap/test.js
Strict Mode Detection
The harness prepends assert/common/require code before each test, which
moves 'use strict' directives out of top-of-script position. To
compensate, run-node-tests.cmake detects 'use strict' at the top of
the original test source (after leading comments) and passes -strict
to hermes.
Overview (`public/hermes/Public/README.md`)
The Public directory contains type declarations made available to both the
Hermes runtime and the Hermes API. The idea is to give both projects a shared
vocabulary of types to reduce the need for bridging and glue code.
The Public directory may not use types from the Hermes, VM, or API layers, or
LLVM types. It may use std types and its own types only.
Configuration
Most Runtime constructor parameters are passed in through configuration structs
created using the macros defined in CtorConfig.h. Below is an example:
#define FOO_FIELDS(F) \
F(size_t, Bar, 0) \
F(std::string, Baz, "Hello") \
/* FOO_FIELDS END */
_HERMES_CTORCONFIG_STRUCT(FooConfig, FOO_FIELDS, {
Bar_ = std::min(Bar_, Baz_.size());
});This defines a FooConfig struct, with two fields, with the following types,
names and default values:
size_t Bar_ = 0;
std::string Baz_ = "Hello";FooConfig's default constructor produces a config with all default values.
Accessors
Each parameter has an associated getter, in this example FooConfig has:
size_t FooConfig::getBar() const;
std::string FooConfig::getBaz() const;Building
Each config also has a corresponding Builder type -- in this caseFooConfig::Builder -- which is the preferred way to override defaults:
auto fooConfig =
FooConfig::Builder()
.withBar(10)
.withBaz("Hello, world!")
.build();This defines a new config, fooConfig, where Bar = 10 and Baz = "Hello, world!". Note that
- every field is optional,
- if omitted a field retains its default value,
- and the underlying
FooConfigis obtained through a call toFooConfig::Builder::build().
It is possible to query a builder on whether a given field has been overridden:
A call to bool FooConfig::Builder::hasBar() const returns true on a given
builder if and only if the builder has had withBar(...) called on it
previously.
Validation and Consistency
The config definition can also include arbitrary logic that is run when the
config is built (The last parameter to _HERMES_CTORCONFIG_STRUCT). This is
typically used to enforce constraints between parameters. If this is not
required, an empty block can be supplied (i.e. {}). In the example it
ensures the value of Bar never exceeds the length of the string in Baz.
The body of the logic has mutable access to all the variables in the config.
However, note that their names are suffixed with underscores.. It also
provides access to the builder (useful for querying whether the fields were
explicitly overridden or not). The body should not return anything.
NB The default values of the config are assumed to be unchanged by the
supplied logic. As a result, the logic is not run as part of the config's
default constructor.
Rebuild
It is possible to modify the contents of an existing config, but only by
converting it back into a builder, using
FooConfig::Builder FooConfig::rebuild() const;which returns a fresh builder with fields populated from the config it was
called on.
NB Fields in the fresh builder are not considered overridden, even if their
values diverge from the config's defaults.
Overview (`test/SourceMap/translator/README.md`)
Merge Input Source Map with debug info Tests
To update these tests, first update the .js file that you want to update.
throw.js
Tests exception stack traces.
Note: make sure each function occupies a separate line so that line numbers
in the symbolicated traces will match the original trace.
Then, run uglify (can be installed via npm install uglify-js)
For example:
uglifyjs throw.js --source-map -o throw.min.jsThis will minify the files and place a source map which can be used for testing
in the directory.
prog1.js
Tests compile time errors.
To update this test, run:
../../../benchmarks/build-helpers/flow-bundler/bin/flow-bundler --root=prog1 prog1/index.js --out=prog1.jsprog2/ directory.
Tests automatic source map detection. To regenerate, from within that directory run npm install as
needed and run ./gen.sh
Overview (`test/hermes/contrib/README.md`)
Contrib Extension Tests
This directory contains tests for community-contributed extensions.
Automatic Exclusion
Tests in this directory are automatically skipped when building with:
cmake -B build -DHERMES_ENABLE_CONTRIB_EXTENSIONS=OFFThis is handled by lit.local.cfg which checks for the contrib_extensions feature.
Adding Tests
When adding a new contrib extension, place its tests in this directory following the standard Hermes test format:
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
// RUN: %hermes %s | %FileCheck %s --match-full-lines
print("Hello from my extension");
// CHECK: Hello from my extensionTests here use the same Lit infrastructure as other Hermes tests. See test/hermes/ for more examples.
Overview (`tools/fuzzers/fuzzilli/README.md`)
Fuzzing with Fuzzilli
Get Fuzzilli
git clone https://github.com/googleprojectzero/fuzzilli.gitSetup Profiles for Hermes
mv profile/*.swift $FUZZILLI_LOCATION/Sources/FuzzilliCli/Profiles/Build Instrumented Binary
mkdir fuzzilli_build && cd fuzzilli_build
cmake .. -DHERMES_ENABLE_FUZZILLI=ON -DHERMES_ENABLE_TRACE_PC_GUARD=ON $OTHER_FLAGS
make fuzzilliStart Fuzzing:
cd $FUZZILLI_LOCATION
swift run FuzzilliCli --profile=hermes $BINARY_LOCATIONOverview (`API/hermes/extensions/contrib/README.md`)
Contributing Extensions to Hermes
This directory contains community-contributed extensions. These extensions are maintained by their contributors and the community, not by the Hermes team.
What This Means
- Community Support: If you encounter issues with a contrib extension, reach out to its author or the community rather than the Hermes team.
- Opt-Out Available: Contrib extensions can be disabled for critical deployments with
-DHERMES_ENABLE_CONTRIB_EXTENSIONS=OFF. - Promotion Path: Popular, well-maintained extensions may be promoted to core if they become widely adopted.
Requirements for Acceptance
- JSI Only: Extensions must use only JSI APIs, not internal Hermes APIs.
- Tests Required: Include
.jstest files using Lit (same format as Hermes tests). - Follow Patterns: Match the existing extension structure (see
ContribDummyexample). - Documentation: Provide clear documentation of what your extension does.
How to Add a Contrib Extension
Step 1: Create the JavaScript File
Create NN-YourExtension.js where NN is a two-digit number controlling load order:
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
// YourExtension description
extensions.YourExtension = function(nativeHelpers) {
// Setup code here
// nativeHelpers contains any native functions passed from C++
};Step 2: Create the C++ Header
Create YourExtension.h:
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#ifndef HERMES_EXTENSIONS_CONTRIB_YOUREXTENSION_H
#define HERMES_EXTENSIONS_CONTRIB_YOUREXTENSION_H
#include <jsi/jsi.h>
namespace facebook {
namespace hermes {
void installYourExtension(jsi::Runtime &runtime, jsi::Object &extensions);
} // namespace hermes
} // namespace facebook
#endifStep 3: Create the C++ Implementation
Create YourExtension.cpp:
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include "YourExtension.h"
#include "../Intrinsics.h" // If you need intrinsics
namespace facebook {
namespace hermes {
void installYourExtension(jsi::Runtime &rt, jsi::Object &extensions) {
// Create native helpers object if needed
jsi::Object nativeHelpers = jsi::Object(rt);
// Add native functions (optional)
nativeHelpers.setProperty(rt, "myNativeFunc",
jsi::Function::createFromHostFunction(rt,
jsi::PropNameID::forAscii(rt, "myNativeFunc"),
1, // argument count
[](jsi::Runtime &rt, const jsi::Value &,
const jsi::Value *args, size_t count) -> jsi::Value {
// Implementation
return jsi::Value::undefined();
}));
// Get and call the setup function
jsi::Function setup = extensions.getPropertyAsFunction(rt, "YourExtension");
setup.call(rt, nativeHelpers);
}
} // namespace hermes
} // namespace facebookStep 4: Update Build and Registration
All updates are within the contrib/ directory:
a) Add to CMakeLists.txt:
set(CONTRIB_EXTENSIONS_CPP_SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/ContribExtensions.cpp
${CMAKE_CURRENT_SOURCE_DIR}/ContribDummy.cpp
${CMAKE_CURRENT_SOURCE_DIR}/YourExtension.cpp # Add this
PARENT_SCOPE
)b) Add to ContribExtensions.cpp:
#include "ContribExtensions.h"
#include "ContribDummy.h"
#include "YourExtension.h" // Add this
void installContribExtensions(jsi::Runtime &rt, jsi::Object &extensions) {
installContribDummy(rt, extensions);
installYourExtension(rt, extensions); // Add this
}Step 5: Add Tests
Create test files in test/hermes/contrib/ following the Lit test format:
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
// RUN: %hermes %s | %FileCheck %s --match-full-lines
// Test your extension here
print("Expected output");
// CHECK: Expected outputTests in test/hermes/contrib/ are automatically skipped when contrib extensions are disabled.
Disabling Contrib Extensions
To build Hermes without contrib extensions:
cmake -B build -DHERMES_ENABLE_CONTRIB_EXTENSIONS=OFFThis is useful for:
- Critical production deployments requiring minimal attack surface
- Debugging issues to isolate core vs contrib behavior
- Builds with strict binary size requirements
Overview (`benchmarks/lib/libgui/cimgui/README.md`)
cimgui [](https://travis-ci.org/cimgui/cimgui)
This is a thin c-api wrapper programmatically generated for the excellent C++ immediate mode gui Dear ImGui.
All imgui.h functions are programmatically wrapped.
Generated files are: cimgui.cpp, cimgui.h for C compilation. Also for helping in bindings creation, definitions.lua with function definition information and structs_and_enums.lua.
This library is intended as a intermediate layer to be able to use Dear ImGui from other languages that can interface with C (like D - see D-binding)
History:
Initially cimgui was developed by Stephan Dilly as hand-written code but lately turned into an auto-generated version by sonoro1234 in order to keep up with imgui more easily (letting the user select the desired branch and commit)
Notes:
- currently this wrapper is based on version [1.79 of Dear ImGui with internal api]
- only functions, structs and enums from imgui.h (an optionally imgui_internal.h) are wrapped.
- if you are interested in imgui backends you should look LuaJIT-ImGui project.
- All naming is algorithmic except for those names that were coded in cimgui_overloads table (https://github.com/cimgui/cimgui/blob/master/generator/generator.lua#L60). In the official version this table is empty.
- Current overloaded function names can be found in (https://github.com/cimgui/cimgui/blob/master/generator/output/overloads.txt)
compilation
clone
git clone --recursive https://github.com/cimgui/cimgui.gitgit submodule update
compile
- using makefile on linux/macOS/mingw (Or use CMake to generate project)
- cmake options are IMGUI_STATIC (compiling as static library), IMGUI_FREETYPE (for using Freetype2) and FREETYPE_PATH (Freetype2 cmake install location)
- or as in https://github.com/sonoro1234/LuaJIT-ImGui/tree/master/build
For compiling with backends there is now an example with SDL2 and opengl3 in folder backend_test. It will generate a cimgui_sdl module and a test_sdl executable. You only need to provide SDL_PATH telling cmake where to look for SDL2 cmake installation.
using generator
- this is only needed (before compilation) if you want an imgui version different from the one provided, otherwise generation is already done.
- you will need LuaJIT (https://github.com/LuaJIT/LuaJIT.git better 2.1 branch) or precompiled for linux/macOS/windows in https://luapower.com/luajit/download
- you need to use also a C++ compiler for doing preprocessing: gcc (In windows MinGW-W64-builds for example), clang or cl (MSVC). (this repo was done with gcc)
- update
imguifolder to the version you desire. - edit
generator/generator.baton windows, orgenerator/generator.shon linux, to choose between gcc, clang, or cl and to choose desired backends and whether imgui_internal is generated or not and Freetype2 is used or not. - the defaults of generator are gcc as compiler, imgui_internal included and sdl, glfw, opengl2 and opengl3 as backends.
- edit config_generator.lua for adding includes needed by your chosen backends (vulkan needs that).
- Run generator.bat or generator.sh with gcc, clang or cl and LuaJIT on your PATH.
- as a result some files are generated:
cimgui.cpp,cimgui.handcimgui_impl.hfor compiling and some lua/json files with information about the binding:definitions.jsonwith function info,structs_and_enums.jsonwith struct and enum info,impl_definitions.jsonwith functions from the backends info. - You can pass compiler flags to generator.sh or generator.bat at the end of the call to further specify the compiler behavior. (e.g. -DIMGUI_USER_CONFIG or -DIMGUI_USE_WCHAR32)
generate binding
- C interface is exposed by cimgui.h when you define CIMGUI_DEFINE_ENUMS_AND_STRUCTS
- with your prefered language you can use the lua or json files generated as in:
definitions description
- It is a collection in which key is the cimgui name that would result without overloadings and the value is an array of overloadings (may be only one overloading)
- Each overloading is a collection. Some relevant keys and values are:
- stname : the name of the struct the function belongs to (will be "" if it is top level in ImGui namespace)
- ov_cimguiname : the overloaded cimgui name (if absent it would be taken from cimguiname)
- cimguiname : the name without overloading (this should be used if there is not ov_cimguiname)
- ret : the return type
- retref : is set if original return type is a reference. (will be a pointer in cimgui)
- argsT : an array of collections (each one with type: argument type and name: the argument name, when the argument is a function pointer also ret: return type and signature: the function signature)
- args : a string of argsT concatenated and separated by commas
- call_args : a string with the argument names separated by commas for calling imgui function
- defaults : a collection in which key is argument name and value is the default value.
- manual : will be true if this function is hand-written (not generated)
- skipped : will be true if this function is not generated (and not hand-written)
- isvararg : is set if some argument is a vararg
- constructor : is set if the function is a constructor for a class.
- destructor : is set if the function is a destructor for a class but not just a default destructor.
- realdestructor : is set if the function is a destructor for a class
- templated : is set if the function belongs to a templated class (ImVector)
- templatedgen: is set if the function belongs to a struct generated from template (ImVector_ImWchar)
- nonUDT : if present the original function was returning a user defined type so that signature has been changed to accept a pointer to the UDT as first argument.
- location : name of the header file and linenumber this function comes from. (imgui:000, internal:123, imgui_impl_xxx:123)
- is_static_function : is setted when it is an struct static function.
structs_and_enums description
- Is is a collection with three items:
- under key enums we get the enums collection in which each key is the enum tagname and the value is an array of the ordered values represented as a collection with keys
- name : the name of this enum value
- value : the C string
- calc_value : the numeric value corresponding to value
- under key structs we get the structs collection in which the key is the struct name and the value is an array of the struct members. Each one given as a collection with keys
- type : the type of the struct member
- template_type : if type has a template argument (as ImVector) here will be
- name : the name of the struct member
- size : the number of array elements (when it is an array)
- bitfield : the bitfield width (in case it is a bitfield)
- under key locations we get the locations collection in which each key is the enum tagname or the struct name and the value is the name of the header file and line number this comes from.
- under key enums we get the enums collection in which each key is the enum tagname and the value is an array of the ordered values represented as a collection with keys
usage
- use whatever method is in ImGui c++ namespace in the original imgui.h by prepending
ig - methods have the same parameter list and return values (where possible)
- functions that belong to a struct have an extra first argument with a pointer to the struct.
- where a function returns UDT (user defined type) by value some compilers complain so the function is generated accepting a pointer to the UDT type as the first argument (or second if belongs to a struct).
usage with backends
- look at backend_test folder for a cmake module building with SDL and opengl3.
example bindings based on cimgui
- LuaJIT-ImGui
- ImGui.NET
- nimgl/imgui
- kotlin-imgui
- CImGui.jl
- odin-imgui
- DerelictImgui
- BindBC-CimGui
- imgui-rs
- imgui-pas
- crystal-imgui
C examples based on cimgui
Overview (`benchmarks/widgets/original/es5/README.md`)
ES5 Packaged with Webpack
The pre-generated file is widgets.js. To regenerate it:
npm install
npm run buildOverview (`benchmarks/widgets/original/stripped/README.md`)
The original benchmark stripped of types so it can run with v8. The conversion
was performed by the script strip.sh using Juno.
To run the benchmark, use v8 main.mjs.
It can probably run with NodeJS too, but appears that all files need to be renamed
to have a .mjs extension as well as the imports referring to them.
Overview (`benchmarks/widgets/single-file/es5/README.md`)
Single File ES5
The pre-generated file is widgets.js. To regenerate it:
npm install
npm run buildOverview (`external/asmjit/asmjit/db/README.md`)
AsmJit Instruction Database
This is a database of instructions that is used by AsmJit to generate its internal database and also assembler implementations. This project started initially as AsmDB, but was merged to AsmJit later to make the maintenance easier. The database was created in a way so that each instruction definition would only need a single line in JSON data file. The data is then processed by architecture specific data readers that make the data canonical and ready for processing.
AsmJit database provides the following ISAs:
isa_x86.json- provides X86 instruction data (both 32-bit and 64-bit)isa_aarch32.json- provides AArch32 instruction data (A32/T16/T32 encoding)isa_aarch64.json- provides AArch64 instruction data (A64 encoding)isa_aarch64_sme.json- provides AArch64 SME instruction data (work-in-progress)
To Be Documented
This project will be refactored and documented in the future.
License
AsmJit database is dual licensed under Zlib (AsmJit license) or public domain. The database can be used for any purpose, not just by AsmJit.
Overview (`tools/hermes-parser/js/babel-plugin-syntax-hermes-parser/README.md`)
babel-plugin-syntax-hermes-parser
Hermes parser plugin for Babel. This plugin switches Babel to use hermes-parser instead of the @babel/parser. Since Hermes parser uses C++ compiled to WASM it is significantly faster and provides full syntax support for Flow.
Install
Using npm:
npm install --save-dev babel-plugin-syntax-hermes-parseror using yarn:
yarn add babel-plugin-syntax-hermes-parser --devUsage
The plugin can be enabled via:
// babel.config.json
{
"plugins": ["babel-plugin-syntax-hermes-parser"]
}If parser options need to be provide you can do so via the parserOpts config:
// babel.config.json
{
"plugins": ["babel-plugin-syntax-hermes-parser"],
"parserOpts": {
"allowReturnOutsideFunction": true
}
}Overview (`tools/hermes-parser/js/flow-api-translator/README.md`)
flow-api-translator
Development of flow-api-translator has moved to thefacebook/flow repository.
Overview (`tools/hermes-parser/js/hermes-eslint/README.md`)
hermes-eslint
hermes-eslint is a custom parser for ESLint. It is the recommended parser for use for linting with Flow code.
Usage
Flat config (`eslint.config.js`)
To use hermes-eslint with ESLint flat config, set it aslanguageOptions.parser:
module.exports = [
{
languageOptions: {
parser: require('hermes-eslint'),
},
},
];You can configure parser options withlanguageOptions.parserOptions:
module.exports = [
{
languageOptions: {
parser: require('hermes-eslint'),
parserOptions: {
sourceType: 'module',
},
},
},
];Legacy config (`.eslintrc`)
If you are still using legacy ESLint config, specify"hermes-eslint" as the "parser" in your .eslintrc file:
{
"parser": "hermes-eslint"
}The ESLint documentation provides more information about how to configure ESLint, including how to specify a custom parser.
Options
You may provide additional configuration for hermes-eslint by passing an
object containing configuration options as parserOptions in your ESLint
configuration file. In flat config, this is languageOptions.parserOptions.
This object may contain the following properties:
type ParserOptions = {
/**
* The identifier that's used for JSX Element creation (after transpilation).
* This should not be a member expression - just the root identifier (i.e. use "React" instead of "React.createElement").
*
* To use the new global JSX transform function, you can explicitly set this to `null`.
*
* Defaults to `"React"`.
*/
jsxPragma?: string | null,
/**
* The identifier that's used for JSX fragment elements (after transpilation).
* If `null`, assumes transpilation will always use a member on `jsxFactory` (i.e. React.Fragment).
* This should not be a member expression - just the root identifier (i.e. use "h" instead of "h.Fragment").
*
* Defaults to `null`.
*/
jsxFragmentName?: string | null,
/**
* The source type of the script.
*
* Defaults to `"module"`.
*/
sourceType?: 'script' | 'module',
/**
* Ignore <fbt /> JSX elements when adding references to the module-level `React` variable.
* FBT is JSX that's transformed to non-JSX and thus references differently
*
* https://facebook.github.io/fbt/
*/
fbt?: boolean,
};{
"parser": "hermes-eslint",
"parserOptions": {
"sourceType": "module"
}
}Overview (`tools/hermes-parser/js/hermes-estree/README.md`)
hermes-estree
Flow types for the Flow-ESTree spec produced by the hermes parser
Overview (`tools/hermes-parser/js/hermes-parser/README.md`)
hermes-parser
A JavaScript parser built from the Hermes engine's parser compiled to WebAssembly. Can parse ES6, Flow, and JSX syntax.
API
The Hermes parser exposes a single parse(code, [options]) function, where code is the source code to parse as a string, and options is an optional object that may contain the following properties:
- babel:
boolean, defaults tofalse. Iftrue, output an AST conforming to Babel's AST format. Iffalse, output an AST conforming to the ESTree AST format. - allowReturnOutsideFunction:
boolean, defaults tofalse. Iftrue, do not error on return statements found outside functions. - flow:
"all"or"detect", defaults to"detect". If"detect", only parse syntax as Flow syntax where it is ambiguous whether it is a Flow feature or regular JavaScript when the@flowpragma is present in the file. Otherwise if"all", always parse ambiguous syntax as Flow syntax regardless of the presence of an@flowpragma. For examplefoo<T>(x)in a file without an@flowpragma will be parsed as two comparisons if set to"detect", otherwise if set to"all"or the@flowpragma is included it will be parsed as a call expression with a type argument. - sourceFilename:
string, defaults tonull. The filename corresponding to the code that is to be parsed. If non-null, the filename will be added to all source locations in the output AST. - sourceType:
"module","script", or"unambiguous"(default). If"unambiguous", source type will be automatically detected and set to"module"if any ES6 imports or exports are present in the code, otherwise source type will be set to"script". - tokens:
boolean, defaults tofalse. Iftrue, add all tokens to atokensproperty on the root node. - transformOptions:
object. Options to be supplied to relevant transforms.TransformEnumSyntaxenable:boolean- Whether to enable the transform. By default,false.getRuntime(optional):() => Expression- The expression which should be a reference to the Flow Enums runtime. By default, isrequire('flow-enums-runtime').
Overview (`tools/hermes-parser/js/hermes-transform/README.md`)
hermes-transform
A package for traversing and transforming a hermes AST.
This package is pieced together and inspired by code from:
Overview (`tools/hermes-parser/js/prettier-plugin-hermes-parser/README.md`)
prettier-plugin-hermes-parser
Hermes parser plugin for Prettier. Unless you want to be on the bleeding edge, you should use the official @prettier/plugin-hermes instead.
Usage
More details on using Prettier plugins: https://prettier.io/docs/en/plugins.html#using-plugins
To then use the parser you will need to instruct Prettier to use hermes as the parser for your required files:
// .prettierrc
{
"plugins": ["prettier-plugin-hermes-parser"],
"overrides": [
{
"files": ["*.js", "*.jsx", "*.flow"],
"options": {
"parser": "hermes"
}
}
]
}More details on configuring Prettier parsers: https://prettier.io/docs/en/configuration.html#setting-the-parserdocsenoptionshtmlparser-option
Overview (`unittests/API/synthtest/tests/README.md`)
Synthetic benchmark replay tests
Each file in this directory is a single integration test for synthetic benchmark
parsing and replay.
Adding a new test
Let each test file have a unique identifier, call it testName.
Each test file defines two functions:
const char *testNameTrace();
const char *testNameSource();testNameTrace should return a string of JSON that represents a synthetic trace
of a hypothetical native interaction with JS. This interaction can do things
like define native functions that are called from JS, or make native objects, or
simply call functions in JS.
NOTE: The object ids used in the trace string is only meant to be used as a
unique identifier, it doesn't have any special meaning. Most tests start at 10
just because of convention. You can use object ids like 3, 400000, or 1345, as
long as they are unique integers.
NOTE: We use these test traces for two tests each: one uses Strings to
represent property names, the other PropNameIDs. To make this
possible, we have a "fake" CreatePropNameRecord type. The string
substitution in Driver.cpp (where this convention is further
explained) leaves changes CreatePropNameRecord into either
CreateString or CreatePropNameID, depending on which test variant
we're doing. Note that there are CreateStringRecords for strings that
are not used as property names; these are left unchanged.
testNameSource is some JS source code that interacts with the native
environment created by the trace. It should throw exceptions if the native code
does not behave as expected, this will cause the test to fail.
Make sure testName is added to the macro list in TestFunctions.h.
Then Driver.cpp will call these new functions for you!
Debugging a broken test
Use gdb, and put a breakpoint on TraceInterpreter::execFunction. Then step
through each event that's executed by the interpreter. Currently it's not
particularly easy to also debug the JS, that would require a hybrid debugger.
Overview (`unsupported/hermes/crates/hermes_diagnostics/README.md`)
hermes_diagnostics
Types for representing compiler diagnostics. Includes a general-purpose representation
of diagnostics with related information which can be converted into miette::Diagnostic to exploit miette's pretty printing of errors.
Unlike miette, lsp_types, and other diagnostic libraries, the error severities are categorized
to allow different tools to report them at different levels. For example, a tool may choose to
report todo errors or ignore them.
Overview (`unsupported/hermes/crates/hermes_estree/README.md`)
hermes_estree
This crate is a Rust representation of the ESTree format and
popular extenions including JSX and (eventually) Flow and TypeScript.
Because this crate is intended to support usage in both Rust toolchains and for interop in JavaSCript-based toolchains, the format is designed to support accurate serialization to/from estree-compatible JSON.
Overview (`unsupported/hermes/crates/hermes_estree_codegen/README.md`)
hermes_estree_codegen
This crate is a build dependency for hermes_estree, and contains codegen logic to produce Rust code to describe the ESTree format
given a JSON schema.
Overview (`unsupported/hermes/crates/hermes_parser/README.md`)
hermes_parser
Wrapper around the Hermes parser that exposes parse results as a hermes_estree AST.
Overview (`unsupported/hermes/crates/hermes_semantic_analysis/README.md`)
hermes_semantic_analysis
Implements semantic analysis of JavaScript name resolution and scope information over the hermes_estree AST. Eventually this
analysis will be extended to cover Flow and TypeScript in addition to the core JS and JSX language.
NOTE: this analsyis assumes strict mode and does not support legacy non-strict semantics.
Overview (`unsupported/hermes/crates/hermes_utils/README.md`)
hermes_utils
This is a catch-all crate for utilities and helper code that doesn't have an obvious home elsewhere.
It is expected that this crate will be depended on by lots of other crates in the project. However,
this crate should generally not depend on other workspace crates — that's an indication that the
utility you're adding belongs with the crate that uses the utility.
Overview (`API/hermes/cdp/tools/hermes-inspector-msggen/README.md`)
@react-native/hermes-inspector-msggen
Installation
yarn add --dev @babel/cli @babel/core @babel/preset-env @babel/preset-flow jest @react-native/hermes-inspector-msggenNote: We're using yarn to install deps. Feel free to change commands to use npm 3+ and npx if you like
Testing
To run the tests in this package, run the following commands from the React Native root folder:
yarnto install the dependencies. You just need to run this onceyarn jest packages/hermes-inspector-msggen.
Overview (`external/boost/boost_1_86_0/libs/context/README.md`)
boost.context
boost.context is a foundational library that provides a sort of cooperative multitasking on a single thread.
By providing an abstraction of the current execution state in the current thread, including the stack (with
local variables) and stack pointer, all registers and CPU flags, and the instruction pointer, a execution_context
instance represents a specific point in the application's execution path. This is useful for building
higher-level abstractions, like coroutines, cooperative threads (userland threads) or an equivalent to
C# keyword yield in C++.
A fiber provides the means to suspend the current execution path and to transfer execution control,
thereby permitting another fiber to run on the current thread. This state full transfer mechanism
enables a fiber to suspend execution from within nested functions and, later, to resume from where it
was suspended. While the execution path represented by a fiber only runs on a single thread, it can be
migrated to another thread at any given time.
A context switch between threads requires system calls (involving the OS kernel), which can cost more than
thousand CPU cycles on x86 CPUs. By contrast, transferring control among fibers requires only fewer than
hundred CPU cycles because it does not involve system calls as it is done within a single thread.
boost.context requires C++11!
Overview (`tools/hermes-parser/js/hermes-eslint/__tests__/eslint-scope/README.md`)
eslint-scope tests
These tests are forked from eslint-scope. The original license for eslint-scope can be found in the ESLINT_SCOPE_LICENSE file in the root of this package. The tests have been reformatted and modified to work with Jest.
Overview (`tools/hermes-parser/js/hermes-eslint/src/scope-manager/README.md`)
Hermes ESLint Scope Manager
This is a fork of typescript-eslint's scope manager, enhanced to support Flow.
The original license for typescript-eslint's scope manager can be found in the TYPESCRIPT_ESLINT_SCOPE_MANAGER_LICENSE file in the root of this package.
Overview (`tools/hermes-parser/js/hermes-transform/src/transform/comments/prettier/README.md`)
Comment Attachment
This is a fork of prettier's comment attachment algorithm. The original license for prettier can be found in the PRETTIER_LICENCE file in the root of this package.
Specifically this fork contains the attach method from src/main/comments.js and the minimum set of code required for it.
The folder structure has been maintained to make merging future updates easier.
Doc BuildingAndRunning (`doc/BuildingAndRunning.md`)
id: building-and-running title: Building and Running
This document describes how to build and run Hermes as a standalone compiler and VM. To use Hermes in the context of a React Native app, see the React Native documentation.
Dependencies
Hermes is a C++17 project. clang, gcc, and Visual C++ are supported. Hermes also requires cmake, git, Python. It builds with CMake and ninja.
The Hermes REPL will also use libreadline, if available.
ICU is not required for a default build: Unicode case conversion, normalization, collation and date formatting are all self-contained. ICU is needed only for -DHERMES_ENABLE_INTL=ON (off by default), and even then only on Linux and Windows, since Apple and Android builds use their platform's own Intl implementation. Install libicu-dev (Ubuntu) or icu (Arch) if you are enabling Intl.
To install dependencies on Ubuntu:
apt install build-essential cmake git ninja-build python3 tzdata libreadline-devOn Arch Linux:
pacman -S cmake git ninja python zip readlineOn Mac via Homebrew:
brew install cmake git ninjaBuilding on Linux and macOS
Hermes will place its build files in the current directory by default.
You can also give explicit source and build directories, use --help on the build scripts to see how.
Create a base directory to work in, e.g. ~/workspace, and cd into it.
(Tip: avoid naming it hermes, as hermes will be one of several subdirectories in the workspace).
After cding, follow the steps below to generate the Hermes build system:
git clone https://github.com/facebook/hermes.git
cmake -S hermes -B build -G Ninja -DCMAKE_BUILD_TYPE=DebugThe build system has now been generated in the build directory. To perform the build:
cmake --build ./buildRelease Build
The above instructions create an unoptimized debug build. The -DCMAKE_BUILD_TYPE=Release flag will create a release build:
cmake -S hermes -B build_release -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build ./build_releaseBuilding on Windows
To build on Windows using Visual Studio with a checkout in the hermes directory:
cmake -S hermes -B build -G 'Visual Studio 16 2019'
cmake --build ./buildRunning Hermes
The primary binary is the hermes tool, which will be found at build/bin/hermes. This tool compiles JavaScript to Hermes bytecode. It can also execute JavaScript, from source or bytecode or be used as a REPL.
Executing JavaScript with Hermes
hermes test.jsCompiling and Executing JavaScript with Bytecode
hermes -emit-binary -out test.hbc test.js
hermes test.hbcRunning Tests
To run the Hermes test suite:
cmake --build ./build --target check-hermesTo run Hermes against the test262 suite, you need to have a Hermes binary built
already and a clone of the test262 repo:
hermes/utils/testsuite/run_testsuite.py -b <hermes_build> <test262>E.g. if we configured at ~/hermes_build (i.e. ~/hermes_build/bin/hermes is
an executable) and cloned test262 at ~/test262, then perform:
hermes/utils/testsuite/run_testsuite.py -b ~/hermes_build ~/test262/testNote that you can also only test against part of a test suite, e.g. to run the
Intl402 subset of the test262, you can specifiy a subdir:
hermes/utils/testsuite/run_testsuite.py -b ~/hermes_build ~/test262/test/intl402Formatting Code
To automatically format all your changes, you will need clang-format, then
simply run:
hermes/utils/format.shAddressSanitizer (ASan) Build
The -HERMES_ENABLE_ADDRESS_SANITIZER=ON flag will create a ASan build:
git clone https://github.com/facebook/hermes.git
cmake -S hermes -B asan_build -G Ninja -D HERMES_ENABLE_ADDRESS_SANITIZER=ON -DCMAKE_BUILD_TYPE=Debug
cmake --build ./asan_buildYou can verify the build by looking for asan symbols in the hermes binary:
nm asan_build/bin/hermes | grep asanOther Tools
In addition to hermes, the following tools will be built:
hdb: JavaScript command line debuggerhbcdump: Hermes bytecode disassemblerhermesc: Standalone Hermes compiler. This can compile JavaScript to Hermes bytecode, but does not support executing it.hvm: Standalone Hermes VM. This can execute Hermes bytecode, but does not support compiling it.
Doc CodingStandards (`doc/CodingStandards.md`)
id: coding-standards title: Coding Standards
Hermes Coding Standards
This document provides guidance for the kind of code that should go in to
the Hermes project. The rules in this document will allow us to scale the
project and ensure that the code base remains readable and maintainable.
Code Quality
Code quality standards in Hermes are very strict. While we welcome and
support community contributions, expect scrutiny on changes that do not
meet the following standards:
- Contributions must be well understood and explained in PRs. We cannot accept
and maintain contributions that we don't fully understand. - Contributions should not use shortcuts or hacks in the name of expediency.
Hermes is a critical piece of infrastructure for many apps and systems, and
such code creates ongoing work for maintainers.
Language Features
Hermes uses C++14.
Exceptions or RTTI are prohibited, except where needed for
interfacing with other software that requires them.
GCC language extensions are discouraged for compatibility with MSVC, or they
should be conditionally enabled.
Code Style
TODO: This should be sorted and expanded.
- Every declaration must have a doc-comment.
Naming
- Member variables use a "_" suffix unless they are public, where it is
acceptable to omit the suffix. - Classes use pascal case (
MyClass). - Functions and methods use camel case (
myFunc). - Variables generally use camel case (
myVar). We also allow "abbreviated
case": "basic block" can be abbreviated asBB, "instruction" can be
abbreviated asI, etc. - Constants use either capitalized snake case (
MY_CONST) or camel case with
"k" prefix (kMyConst).
Misc
structshould be used only when it is a true POD - there are no constructors
or destructors of any kind. For everything else useclass.- Inlining: Only completely trivial (one line) methods can be defined in the
class body. The rest should be declared asinlineboth at the declaration and
the definition.
Small incremental changes
The project is developed using small incremental changes. These changes can be
small bug fixes or minor tweaks. Other times, these changes are small steps
along the path to reaching larger stated goals. Long-term development branches
suffer from many problems, including the lack of visibility, difficulty of code
review, lack of testing of the branch and merge difficulty.
Commits that go into the project need to be reviewable. This means that commits
need to be relatively small, well documented and self contained.
Add tests
Functional changes to the compiler need to include a testcase. Unit tests and
regression tests are critical to the qualification of the compiler. Every bug
fix needs to include a testcase.
Reduce test cases as much as possible! It is unacceptable to commit big programs
because they do not describe the essence of the failure, they are fragile, and
they slow testing down. Tests need to be short and focused.
Format your code
clang-format is required to enforce code style and formatting.
Commits that only change the formatting of code should go in independent of
functional changes.
Commit messages
Here are some guidelines about the format of the commit message:
Separate the commit message into a single-line title and a separate body that
describes the change. Make the title short (80 chars) and readable. In changes
that are restricted to a specific part of the code, include a [tag] at the start
of the line in square brackets—for example, “[docs] ... ”.
If the commit fixes an issue in the bug tracking system, include a link or a
task number.
When reverting a change make sure to add a short note that describes why the
patch is being reverted.
Code review
The project relies heavily on code review to maintain the software quality.
Review other people’s changes! Anybody is allowed to review code and comment
on patches.
All changes, by all developers, must be reviewed before they are committed to
the repository. Smaller changes (if the developer is the de-facto owner of the
code base) can be reviewed after being committed.
Doc CrossCompilation (`doc/CrossCompilation.md`)
id: cross-compilation title: Cross Compilation
This document describes how to build Hermes in a cross compilation setting, e.g.
building for Android, WASM (with Emscripten), or any other platforms different
than the host development machines.
Android compilation
Edit paths:
CMAKE_TOOLCHAIN_FILEshould point to whateverandroid_ndkyou need.IMPORT_HOST_COMPILERSshould point to whatever SH host build you want to use for InternalBytecode compilation.-Bpath is to your build directory, or omit it to build in current directory.
Ensure that CMAKE_CXX_FLAGS_RELEASE is -O3 -DNDEBUG if you're in Release mode.
Then you can use adb push to copy any files into /data/local/tmp/ on an Android device and run it there using 'adb shell'.
cmake ~/fbsource/xplat/static_h \
-B ~/static_h/build_android \
-DHERMES_FACEBOOK_BUILD=OFF \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_TOOLCHAIN_FILE=/opt/android_ndk/r17fb2/build/cmake/android.toolchain.cmake \
-DHERMES_UNICODE_LITE=ON \
-DIMPORT_HOST_COMPILERS=~/static_h/build_release/ImportHostCompilers.cmake \
-DHERMES_ENABLE_DEBUGGER=OFF \
-DANDROID_ABI=arm64-v8a -G NinjaHERMES_UNICODE_LITE=ON selects the self-contained no-op Unicode backend.
Without it, an Android build uses the Java backend, which needs a JVM at
runtime and so is not usable from a binary pushed with adb push and run
under adb shell.
No ICU is needed here, or in any cross-compilation target. Hermes no
longer depends on ICU at all. The platform_unicode layer -- case
conversion, normalization, collation and date formatting -- is
self-contained, and the ICU-backed Intl backend has been removed.-DHERMES_ENABLE_INTL=ON is now supported only on Apple and Android,
which have their own native Intl implementations.
Emscripten
For Emscripten, you can find an example from the test-emscripten job from hermes/.circleci/config.yml. Also see more details at Building with Emscripten
Compiling and Running ARM32 Static Hermes from ARM64
On the ARM64 Linux machine you need clang-16 and lld-16.
On the ARM64 Linux machine on which you're compiling:
# Add architecture
dpkg --add-architecture armhf
# Update
sudo apt update
# For compiling
sudo apt install gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf binutils-arm-linux-gnueabihf libc6:armhf libstdc++6:armhfIf you're executing on an ARM64 machine, set it up as well:
# Add architecture
dpkg --add-architecture armhf
# Update
sudo apt update
# For running
sudo apt install libc6:armhf libstdc++6:armhfThere's a CMake toolchain file in the source directory that will then set up Clang to compile for ARM32. To use it:
CC=clang-16 CXX=clang++-16 cmake ${SRC_DIR?} -GNinja -DCMAKE_TOOLCHAIN_FILE=${SRC_DIR?}/armhf/arm-linux-gnueabihf.toolchain.cmake -DCMAKE_BUILD_TYPE=ReleaseThen you can run ninja and build 32-bit binaries and libraries. They'll work on the machine on which you're executing.
Doc Design (`doc/Design.md`)
id: design title: Design Overview
This is the design document for the Hermes Engine.
JSON estree parser
JavaScript parser
IRGen
Hermes Bytecode Generator
The Hermes bytecode generator is responsible for translating the
Hermes high-level IR into Hermes bytecode. This section only describes the
generation of the opcode stream, not the whole bundle file format that is
described in a separate document.
The bytecode is a register-based bytecode. The number of registers is
infinite, but there are some restrictions on the registers. For example,
registers that are allocated to call instructions must be consecutive, and most
instructions only accept an 8-bit register index.
The first phase of bytecode generation is lowering of some instructions to
target-specific instructions. Next, the register allocator allocates
registers for each instruction in the lowered IR. Notice that the Hermes IR
has a single return value per instruction. We model multiple return values or
in-out variables using alloc-stack instructions.
One interesting design decision here is that register allocation happens on the
IR prior to instruction selection. Performing register allocation prior to
lowering is often done in JIT compilers where the lowering phase is trivial and
close to a 1:1 translation between high-level IR and the low-level target IR.
The lowered Hermes IR is very close to the bytecode format with a few
exceptions: the handling of variables, the representation of constants and the
representation of stack-allocated variables. Stack allocated registers are
implemented as values that are pinned into a specific registers (at least during
the lifetime of the stack allocation). Load/Store instructions from the stack
allocation values are lowered into MOV instructions.
The current register allocation implementation is a very simple linear scan. The
allocation has four main steps. First, we number the instructions in the
functions, and traverse the basic blocks in the function in reverse-post-order
scan. Second, we calculate the liveness graph of the result of each
instruction in the function. We propagate this information across the function
with a simple data-flow scan. Third, we calculate the liveness interval of each
instruction. Finally, we scan the instructions in the function one by one and
assign registers. We maintain a list of currently live intervals that correspond
to the register file. We represent the register file with a simple bit vector
and prefer to allocate registers from the beginning of the file. When we run out
of registers we simply allocate new registers. When we reach instructions that
require target-specific handling we call the backend to fill in the details. For
example, the Hermes backend handles call instructions in a special way. At the
moment we do not coalesce values and our PHI nodes generate two MOVs.
After register allocation we lower the Hermes IR into pseudo opcodes using a
one-to-one or one-to-many translation. There are a few interesting optimizations
during this translation. The first interesting optimization is the optimization
that eliminates redundant constant jumps. It is possible to eliminate many
branches by scheduling basic blocks in a way that maximizes the number of
fall-through opportunities. The current algorithm uses a greedy approach where
we emit basic blocks in some order (that starts with the entry block), and
attempt to place the fall-through destination right after terminators that
branch (conditional and unconditional branches) if the block has not been
generated already. In the future we may need to investigate if this greedy
algorithm is optimal.
When we generate the opcode stream we need to encode jump targets. However, when
we emit opcodes that refer to jump destinations that we have not yet emitted
then we do not know the address of the destination. We solve this problem by
emitting a dummy value and keeping a side-table that saves the location that we
need to patch. When we finish emitting all of the opcodes we scan the side table
and patch all of the locations that refer to previously unresolved addresses.
Hermes Bytecode Instructions
Hermes bytecode adopts variable-length instructions. Each operand to a bytecode
instruction has a fixed-type and width, defined by the opcode. For instance,
Jmp takes a 1-byte offset as the jump target, while JmpLong takes a 4-byte
offset as the jump target. Fixed-type/width instructions allow us to decode
them efficiently in the interpreter.
However we are trading off with an increasing number of
opcodes to handle different operand widths (e.g. two Jmp opcodes instead of
one). We believe that we are able to avoid opcode explosion by generating the
code smartly. A full list of Hermes bytecode opcodes can be found in
BytecodeList.def. There are a few interesting design decisions worth mentioning
here:
- Registers: We discovered that in all of the Facebook mobile JS code as well as
majority of external benchmarks, no function ever uses more than 256 registers.
Hence we always use 1-byte to represent register index, which will be most
efficient for the normal cases. Spilling is implemented via MovLong which
supports 32 bit register indices. - Constants: we achieve constant loading fully through instructions.
For fixed-value constants such as undefined, null, true and false, we introduce
a corresponding load opcode for each of them into a register (e.g.
LoadConstUndefineddstReg); For 32-bit integers, we introduce LoadConstInt
opcode, which takes a 4-bytes immediate value and load it into a register; for
doubles, we introduce LoadConstDouble, which takes a 8-bytes immediate value
and load it into a register; finally for strings, we introduce LoadConstString,
which takes an index to the string table from which to load into a register.
Doing so can significantly reduce the size of the bytecode, however it does
introduce a few more opcodes which could slow down the interpreter. - Non-local Variable Access: Local variables are translated to registers.
Non-local variables are variables from different scopes/environments. Without a
compiler, accessing non-local variables in JavaScript usually means a scope
lookup (i.e. locating the closest scope in the scope chain that defines the
variable), followed by a symbol lookup in that scope.
However with a compiler, it is possible to statically determine the scope of
every variable, and hence there is no need for a real scope lookup. In Hermes
backend, for each non-local variable access, we simply calculate the delta
between the defining scope and the current scope, and locate such scope using
the delta (i.e. number of times the VM needs to follow the scope chain)
directly. Furthermore, since we know exactly what and how many variables are
defined in each scope (except the global scope), we skip the symbol lookup in
the scope but instead using a direct index access to retrieve such variable in
the located scope. We believe that skipping both scope lookups and symbol
lookups can significantly improve the runtime performance.
Bytecode File Format
The bytecode file contains the bytecode as well as necessary metadata and
auxiliary data sections for the VM to execute properly. The file format is
defined in BytecodeFileFormat.h, and structures as following:
- FILE HEADER: The file header contains the MAGIC, the current format version,
and a list of global metadata, including the file size, offset of the function
header table, offset of the string table, index of the global code and number
of functions. - FUNCTION HEADER TABLE: This is a list of function headers. Each function header
contains metadata of a function, such as the offset of the function bytecode in
the file, number of parameters, size of the frame/environment, size of the
bytecode and etc. This list also naturally assigns an index to each function in
the file, which makes access to each function convenient in the VM. - STRING TABLE&STORAGE: All the strings used in all functions are uniqued and
stored in the section to avoid redundant string storage. This section contains
two parts: The string storage, which is a long sequence of raw characters; the
string table, which is a list of pairs, each pair represents a string through
the offset to the string storage and the length of the string. The string table
also naturally assigns an index to each unique string, which makes it
convenient to refer to the strings in the bytecode. - FUNCTION BYTECODES: This section is the core section of the bytecode file,
containing a list of compiled function body. The function body contains its
executable bytecode, along with a few tables that are used by the bytecode,
including the exception handler table (tells where to jump to when exception
happens), the array buffer (used to initialize constant arrays). Likely there
will be a few more tables coming to support RegExp and debug information.
Serialization / Deserialization
Because the Hermes backend is in the same codebase as the VM, there are
opportunities for us to share code between them. In particular, we want the
serialization in the backend to be able to share the same target data structure
as the deserialization in the VM. This introduces some interesting questions:
- How can we share data/code without having to link too much code on each side?
- How to avoid data copies during both serialization and deserialization?
We introduce two design pieces to achieve code sharing efficiently:
- Generator: During serialization, we often need many auxiliary data and
functionality to aid the process, though many of that will not be needed in the
end. To ensure the separation of the complexity and allow the deserialization
to share data/code most efficiently, we use a BytecodeModuleGenerator and a
list of BytecodeFunctionGenerator for the purpose of generating bytecode and
serialization. After all the processing, they will eventually generate a
BytecodeModule and a list of BytecodeFunction that contains minimum amount of
data/functionality required to generate the bytecode file. Hence we can share
this minimum data structure between the backend and the VM. - StreamVector: During serialization, we need to move/copy the part of the
auxiliary data from the generator to the shared minimum data structure; during
deserialization, we need to move/copy the content of the file into the shared
minimum data structure. Both can be expensive if not managed properly. To
minimize the copying overhead, we abstracted the complexity using a class named
StreamVector. During serialization, StreamVector allows us to move the data out
of the generator without copying (through std::vector::swap); during
deserialization, StreamVector allows us to take in a raw data pointer to the
memory buffer from the file directly, without having to copy them too.
Interaction with the VM
At runtime, the VM will deserialize the bytecode from the file and interpret
it. A few components are involved:
BytecodeModule: This is the in-memory representation of the whole bytecode
file, containing all the bytecode functions. During both serialization and
deserialization, this data structure is generated, as a static representation
of the whole file.
- BytecodeFunction: This is the in-memory representation of a function’s
bytecode. - RuntimeModule: This is the dynamic version of the bytecode module, containing
necessary runtime information for interpretation. - CodeBlock: This is the dynamic version of the bytecode function, containing
necessary runtime information to execute a function. - Domain: A GC-managed proxy which references a set of RuntimeModules, acting as
a bridge between the GC heap and the C++ heap. - JSFunction: This is the Function object in JavaScript.
It is important (and tricky) to efficiently manage the memory and ownership of
these objects properly at runtime. The following figure demonstrates it. Own
means one manages the memory of the other through unique_ptr; Pointer means one
has a raw pointer to the other, without any ownership. Indirectly Own is a
special kind of ownership, which will be explained below. To summarize the
ownership:
- JSFunction is a JavaScript object, and hence managed by the heap/garbage
collector directly. - JSFunction owns a GC-visible reference to a Domain, and a pointer to the
corresponding CodeBlock to execute. - CodeBlock contains a pointer to both the RuntimeModule to access runtime
information, as well as a pointer to the corresponding BytecodeFunction which
contains the static function bytecode to execute. - BytecodeModule owns a list of BytecodeFunction.
- RuntimeModule owns a list of CodeBlock, as well as the corresponding
BytecodeModule. - Domain owns one or more RuntimeModule. This is the mechanism by which an extant
JSFunction keeps the backing bytecode alive.
Interpreter
Garbage Collector
Doc Emscripten (`doc/Emscripten.md`)
id: emscripten title: Building with Emscripten
Compiling to Wasm
Prerequisites
This guide assumes you are familiar with building Hermes locally and have the necessary
tools installed (e.g., CMake, Ninja, and a compatible toolchain). The instructions are
written for macOS but should work on Linux with minimal adjustments.
Install Emscripten
Install emsdk by following the directions on the Emscripten website for emsdk.
Then, inside the installed directory, we need to make sure we have the latest toolchain:
./emsdk install latest
./emsdk activate latestCreate a Parent Builds Directory
We need a directory to contain the different Hermes builds:
mkdir ~/hermes-builds
cd ~/hermes-buildsSetup Environment Variables
For convenience, we rely on two environment variables throughout this document:
export Emsdk=<path-to-emsdk-directory>
export HermesSourcePath=<path-to-hermes-source-directory>Build Hermes for the Host
Hermes uses a two stage build process because parts of Hermes need to be
built using Hermes itself. For the first stage, we need to build Hermes for
the host. We will also use the host build later to compile .js to .c.
# From within the hermes-builds directory
cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -S ${HermesSourcePath?} -B build-host
# Build hermes and shermes for the host
cmake --build build-host --target hermesc --target hermes --target shermes --parallelCompile the Hermes VM to Wasm
# From within the hermes-builds directory
cmake -G Ninja \
-S ${HermesSourcePath?} -B build-wasm \
-DCMAKE_TOOLCHAIN_FILE=${Emsdk?}/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake \
-DCMAKE_BUILD_TYPE=Release \
-DIMPORT_HOST_COMPILERS=build-host/ImportHostCompilers.cmake \
-DHAVE_SYS_IOCTL_H=0 \
-DCMAKE_EXE_LINKER_FLAGS="-sNODERAWFS=1 -sALLOW_MEMORY_GROWTH=1 -sSTACK_SIZE=256KB"
# Build the VM and libraries
cmake --build build-wasm --target hermes --target shermes --target shermes-dep --parallelImportant options:
-G Ninjais optional but recommended. It instructs CMake to use Ninja as a build tool.CMAKE_BUILD_TYPE: set it to one of CMake's build modes:Debug,Release,MinSizeRel, etc.-DHAVE_SYS_IOCTL_H=0is needed for compatibility with the Emscripten runtime environment.NODERAWFS: set to1if you will be running Hermes directly with Node. It
enables direct access to the filesystem.ALLOW_MEMORY_GROWTH: whether to pre-allocate all memory, or let it grow over time
Under Emscripten, Hermes relies on a small amount of JavaScript
to be executed by the Wasm host (like Node.js or a browser). If you intend to run it under a "pure" Wasm host, consider
using this flag:
-DHERMES_UNICODE_LITE=if set to ON, provides a minimal mostly stubbed-out Unicode implementation.
Note that running under a "pure" Wasm host is not described here and will likely require more tweaks.
Now that the VM is compiled to Wasm, we can examine it:
ls -l build-wasm/bin/hermes.*Execute Some JavaScript with the Wasm Hermes VM
Let's create a small .js file:
echo 'var x = "hello"; console.log(`${x} world`);' > hello.jsLet's run the example with the Wasm VM:
node ./build-wasm/bin/hermes.js hello.jsLet's compile the example to bytecode and then run the bytecode:
node ./build-wasm/bin/hermes.js hello.js --emit-binary -out hello.hbc
node ./build-wasm/bin/hermes.js hello.hbcFinally, let's compare the performance of the Wasm VM with the native one
by running one of the micro-benchmarks that come with Hermes.
# Run the micro-benchmark with the host VM.
./build-host/bin/hermes -w ${HermesSourcePath?}/benchmarks/bench-runner/resource/test-suites/micros/interp-dispatch.js
# Run it with the Wasm VM
node ./build-wasm/bin/hermes.js -w ${HermesSourcePath?}/benchmarks/bench-runner/resource/test-suites/micros/interp-dispatch.jsCompile JavaScript to Wasm
Now, let's compile JavaScript to Wasm! This is still not directly supported by
the Hermes CLI tools, so we will need a manual step. But no worries, it is easy.
First, we must make sure we have activated the Emscripten SDK in the current shell.
emcc --helpIf that runs fine, the SDK is already active. Otherwise, we need to activate it:
source ${Emsdk?}/emsdk_env.shNote that we didn't need to activate it when we were building with CMake, because
we used a CMake toolchain file.
Now we are ready to compile the example js file we created earlier directly to Wasm.wasm-compile.sh is a helper script in the Hermes utils/ directory.
${HermesSourcePath?}/utils/wasm-compile.sh build-host build-wasm hello.jsThe compilation generates two files:
hello-wasm.wasm: the compiled Wasm module. Note that this also contains
the entire JS library.hello-wasm.js: this is the Node.js wrapper to load the Wasm module.
We can run it:
node ./hello-wasm.jsNow let's try compiling the micro-benchmark to Wasm:
${HermesSourcePath?}/utils/wasm-compile.sh build-host build-wasm \
${HermesSourcePath?}/benchmarks/bench-runner/resource/test-suites/micros/interp-dispatch.jsDoc Features (`doc/Features.md`)
id: language-features title: Language Features
Hermes is a JavaScript engine optimized for fast start-up. It executes JavaScript using a combination of ahead-of-time (AOT) compilation to bytecode and runtime interpretation.
Hermes aims to support the latest ECMAScript specification, prioritizing language features over new library functions. However, some features are intentionally excluded or de-prioritized if they conflict with Hermes' design goals of performance, low memory use, and efficient AOT compilation.
Supported ECMAScript Features
Hermes provides broad support for ECMAScript features, including:
Full support for ES2015 (ES6) Language Features:
letandconstdeclarations (including Temporal Dead Zone behavior)- Arrow Functions (
=>) - Classes (Basic syntax:
class,extends,staticmethods,super,constructor) - Enhanced Object Literals (shorthand properties, computed names, methods)
- Template Literals (tagged and untagged)
- Destructuring Assignment (arrays and objects, including rest
...) - Default, Rest, and Spread (
...) operators - Iterators and Generators (
Symbol.iterator,for..of,function*) - Symbols (primitive type,
Symbol(), well-known symbols) - And all other ES2015 language constructs (unless listed below).
Key Post-ES2015 Language Features:
async/await(ES2017)- Object Rest/Spread Properties (ES2018)
- RegExp Named Capture Groups (ES2018)
- Optional Chaining (
?.) (ES2020) - Nullish Coalescing (
??) (ES2020) BigInt(ES2020)- Class Fields & Static Blocks (ES2022):
- Public and Private Instance Fields (property declarations and initializers, e.g.,
myField = 1;,#privateField = 2;) - Public and Private Static Fields (e.g.,
static staticField = 3;,static #privateStatic = 4;) - Static Initialization Blocks (
static {})
- Public and Private Instance Fields (property declarations and initializers, e.g.,
- RegExp Match Indices (
/dflag) (ES2022)
Key Library Features:
- ES2015 standard built-ins (
Promise,Set,Map,WeakSet,WeakMap, TypedArrays,Reflect,Proxy, updated methods onArray,String,Object, etc.) - Extended
PromiseAPI (see "Known Deviations" for the limits ofSymbol.speciesand microtask timing):Promise.prototype.finally(ES2018)Promise.allSettled(ES2020)Promise.any(ES2021)Promise.withResolvers(ES2024)Promise.try(ES2025)
Symbol.prototype.description(ES2019 - see "Known Deviations")WeakRef(ES2021 - see "Planned Features" regardingFinalizationRegistry)IntlAPI (BasicDateTimeFormat,NumberFormatsupport; see "Planned Features")- Note: Support for the latest standard library features may lag behind language feature support.
- ES2015 standard built-ins (
Planned Features
Features Hermes intends to support in the future. Active development or implementation hasn't started or completed yet.
FinalizationRegistry: The complementary API toWeakRef.- Expanded
IntlFunctionality: Support for APIs such asDisplayNames,ListFormat,PluralRules,RelativeTimeFormat,Locale, etc. - Other Standard Library Features: Newer library additions (e.g., Iterator Helpers, Array Grouping methods) are considered but may be lower priority.
Intentionally Excluded / De-prioritized Features
These features are not supported, often due to incompatibility with Hermes' AOT compilation strategy, performance concerns, limited utility, or complexity costs.
- Local
eval():eval()cannot access or modify local variables in the surrounding lexical scope (neither in strict nor non-strict mode). It operates only on the global scope. This restriction is fundamental to Hermes' AOT compilation and optimization strategy; supporting localevalwould severely degrade performance across the engine. Use ofevalis strongly discouraged. withStatements: Not supported (throws aSyntaxError). It hinders performance and optimization and is disallowed in strict mode.- ES Modules (
import/export): Hermes does not currently provide a runtime module loader. While an implementation existed previously, it was removed as the React Native ecosystem relies heavily on bundlers (like Metro) which provide their own module systems. A future goal is to potentially parse module syntax to give the AOT compiler better visibility for cross-module optimizations, but not necessarily to replace bundler functionality at runtime. Symbol.species: Generally not supported for built-in methods. While some specific methods might incidentally behave compatibly, Hermes does not guarantee or commit to supporting theSymbol.speciespattern due to its complexity and performance overhead, particularly for an AOT-focused engine.- Non-Strict
argumentsObject Behavior: See "Known Deviations" for details on how Hermes deviates from the spec regarding parameter syncing, assignment, andvarshadowing for theargumentsobject in non-strict mode. These deviations simplify implementation and improve performance. Symbol.unscopables: Not relevant aswithis not supported.
Known Deviations & Implementation Details
Specific behaviors where Hermes differs from the ECMAScript specification or has notable implementation characteristics. These deviations are often deliberate choices prioritizing performance, simplicity, or compatibility with Hermes' AOT compilation model, especially concerning rarely used or complex legacy features.
argumentsObject Behavior (Non-Strict Mode): Hermes simplifiesargumentsobject handling in non-strict ("loose") mode compared to the specification:- No Parameter Syncing: Assigning to indices of the
argumentsobject does not dynamically update the corresponding named parameters, and vice-versa (e.g., settingarguments[0] = xdoes not change the value of the first named parameter). This matches strict mode behavior. (Motivation: High cost and complexity for a rare feature). - Assignment Forbidden: Direct assignment to the
argumentsidentifier itself (e.g.,arguments = ...;) is disallowed, unlike in spec-compliant loose mode. (Motivation: Rare, complex, little practical benefit). var argumentsShadows: Declaringvar arguments;inside a function creates a new variable that shadows the arguments object (likelet arguments;) rather than aliasing it. (Motivation: Extremely rare, no known practical uses).- These
argumentsdeviations are considered very low priority to align with the spec due to the reasons mentioned.
- No Parameter Syncing: Assigning to indices of the
Function Declaration Hoisting (Non-Strict Mode Blocks): The exact semantics of how function declarations within nested blocks are hoisted ("promoted") in non-strict mode may differ from the spec in some complex corner cases. For example, a function declared in an inner block might incorrectly overwrite a function declared in an outer block at the function scope level. (Motivation: Affects rare edge cases primarily in non-strict mode; other engines may exhibit similar behavior. Low priority to fix).
Function.prototype.toString(): Due to AOT compilation to bytecode, this method does not return the original JavaScript source code. It typically returns a placeholder like"[native code]"or"[bytecode]".PromiseImplementation: Promises are implemented using an internally bundled polyfill compiled to bytecode rather than as a native intrinsic. The polyfill covers the full ES2025 surface (then,catch,finally,all,allSettled,any,race,resolve,reject,try,withResolvers,Symbol.toStringTag), but the following corners differ from the spec:- No
Symbol.speciesdispatch. Per spec, the prototype methodsPromise.prototype.then,Promise.prototype.catch, andPromise.prototype.finallyuseSpeciesConstructor(this, %Promise%)to determine the constructor of the chained promise. The polyfill substitutesthis.constructor(and takes the built-inPromisedirectly on the common-casethis.constructor === Promisefast path), so a subclass that overridesSymbol.specieswill not see its override applied (consistent with the engine-wide "Symbol.species not supported" stance noted above). The static methods (all,allSettled,any,race,resolve,reject) usethisviaNewPromiseCapabilityper spec —Symbol.speciesis not involved there. - Extra microtask hop on subclass
.then. Whenthis.constructor !== Promise, the polyfill bridges the user's reaction to the subclass capability through an intermediate corePromise, costing one additional microtask hop compared to spec'sPerformPromiseThen, which attaches the reaction directly to the capability. The fast path (this.constructor === Promise) is unaffected. Promise.all1-microtask fast path (non-spec, default). When an input is an already-fulfilled corePromise, the polyfill synchronously invokes the resolve element and collapses the spec-mandatedPerformPromiseAllstep 8.e microtask hop. This is a deliberate performance default; the spec-compliant.then-only dispatch is enabled under the--test262runtime flag.- Microtask timing in
await. Hermes'async functionmachinery lowers to a generator wrapped with the polyfill'sthen, and a few test262 tick-counting tests (e.g.await-non-promise-thenable, thefor-await-of/ticks-with-…-constructor-lookupfamily) observe a different microtask sequence than spec because the polyfill does one fewer.constructorread on each await hop. - See the polyfill source.
- No
Symbol.prototype.descriptionConformance: WhileSymbol('desc').descriptionworks as expected,Symbol().descriptioncurrently returns''(empty string) instead of the spec-compliantundefined.
Doc GCSafeCoding (`doc/GCSafeCoding.md`)
C++ Coding With GC
Hermes is a JavaScript engine with a garbage collector (GC). Various entities like
JS objects, hidden classes, property maps, and so on are allocated in the GC heap
and their lifetime is managed by the garbage collector — they can be freed when the
GC discovers that they are no longer referenced. Additionally, the GC can
automatically move GC-managed entities for heap compaction or other reasons.
Writing C++ code that uses GC-managed objects can be difficult because of this —
objects that the C++ code is accessing can be freed or moved. This document explains
the rules for writing correct and performant GC-safe code.
The fundamental rule of GC-safe native code
The GC can only determine that objects are unreachable or move objects at
GC safepoints. Without that guarantee, it would be impossible to write GC-safe
C++ code. So, the fundamental requirement for GC-safe code is that all "live"
pointers to GC-managed entities used by the C++ code must be stored in locations
known to the GC before a GC safepoint, and must be reloaded from those locations
after the GC safepoint.
Storing the pointers in a location known to the GC ensures that the GC will consider
those objects reachable and will not free them. Reloading the pointers after the safe
point guarantees that if the GC moved the objects, the native code will use the
updated pointer value.
This is all very abstract. In the next sections we will talk about more concrete
patterns.
GC safepoints
In practice, a GC safepoint is either an allocation, or a function call that might
transitively reach a GC safepoint. Not all C++ calls can reach a GC safepoint.
In the Hermes code base, we use naming conventions, parameter types, and explicit
doc-comments to communicate this. The rules are:
- A function that takes a
Runtime &orPointerBase ¶meter (both of which
provide access to the GC) is assumed to be able to reach GC safepoints, unless
explicitly documented otherwise, or named with a suffix like_noallocor_nogc. - A function with an
_RJSsuffix may invoke JavaScript code recursively
("Recursive JavaScript"), so it definitely reaches GC safepoints.
For example:JSObject::getNamed_RJS(),toString_RJS(),toNumber_RJS(). - A function that does not take
Runtime &orPointerBase &, and is not_RJS, is generally safe to call without protecting pointers.
These rules must be followed absolutely by C++ code — all pointers to heap values
must be stored before and reloaded after. (In practice, the values simply "live" in
locations known to the GC and the C++ code loads them every time. The C++ compiler is
smart enough to optimize out consecutive loads if the underlying value couldn't have
changed.)
GC roots vs heap values
The GC needs a starting point to discover which objects are alive. That starting
point is the set of roots — pointers to GC-managed objects that live outside
the GC heap, in locations the GC is explicitly told about. Roots include:
- The register stack — the VM's operand stack used by the bytecode interpreter.
- Locals — stack-allocated
PinnedValue<>fields registered viaLocalsRAII. - GCScope chains — dynamically allocated
PinnedHermesValueslots managed byGCScope(legacy). - Runtime fields —
PinnedHermesValuefields inside theRuntimeobject
itself, such as well-known prototypes (arrayPrototype,objectPrototype, etc.).
None of these live in the GC heap. They live on the C++ stack or in fixed locations
in the Runtime. The GC knows about each category and walks them all during
collection (see Runtime::markRoots() in Runtime.cpp).
Starting from these roots, the GC traces through every pointer it finds. An object
in the GC heap is alive if and only if it is transitively reachable from at least
one root. Any object not reachable from any root can be freed. If the GC moves an
object (for compaction), it updates every pointer it knows about — both roots and
pointers stored inside other GC heap objects.
The key insight for C++ code: if you hold a raw pointer to a GC object and that
pointer is not stored in a root, the GC does not know about it. The pointed-to
object could be freed (use-after-free) or moved (dangling pointer). This is why all
live pointers must be stored in roots before any GC safepoint.
HermesValue
HermesValue is the fundamental value type exposed to the GC — it is a NaN-boxed
64-bit value that can contain JS primitives (numbers, booleans, undefined, null,
symbols) and, importantly, pointers to GC-managed objects.
The GC works on HermesValues, using the NaN-boxing tag bits to determine whether
each value is a pointer or a primitive. When a value is a pointer, the GC traces
through it, and updates it if the pointed-to object moves.
PinnedHermesValue
PinnedHermesValue is a subclass of HermesValue. The only purpose of the subclass
is to indicate that this HermesValue instance is a GC root — it lives in
memory that is not in the GC heap (and is not movable by the GC), and is known to
the GC as a root.
Note that declaring something as PinnedHermesValue does not magically register
it as a root. The declaration is a semantic marker: if a value is stored outside
the GC heap and is registered with the GC as a root (through Locals, GCScope, orRuntime fields), it should be typed as PinnedHermesValue to indicate its
"root-ness".
PinnedHermesValue is essential for writing GC-safe code. The "locations known to
the GC" mentioned in the first section are all PinnedHermesValue roots.
Conceptually, C++ code stores all pointers to GC objects it needs intoPinnedHermesValue instances before a safepoint and reloads them after. In
practice, there are convenient C++ abstractions built on top ofPinnedHermesValue.
Handle and MutableHandle
A Handle<T> is a wrapper around a pointer to an immutable PinnedHermesValue
(const PinnedHermesValue *), where the PinnedHermesValue is known to contain a
value of type T (a number, bool, JSObject, etc.). Most internal APIs acceptHandles instead of direct pointers.
A Handle<> (i.e. Handle<HermesValue>) is an untyped handle that can hold anyHermesValue.
A MutableHandle<T> is similar to a Handle<T>, but it points to a mutablePinnedHermesValue and allows updating the stored value.
Handle<T> is trivially copyable (it is just a pointer) and is designed to be
passed by value.
An important property: a PinnedValue<T> implicitly converts to Handle<T>. This
means that when a function accepts Handle<T>, you can pass a PinnedValue<T>
directly.
PseudoHandle
PseudoHandle<T> holds a GC-managed value without protecting it via a root. It
is a move-only type — once moved from, the original is invalidated (in debug mode,
accessing an invalidated PseudoHandle asserts).
PseudoHandle exists for performance: in many cases a function produces a value and
the caller immediately stores it in a root. Wrapping the value in a PseudoHandle
encodes in the type system that the value is unrooted and must be stored somewhere
safe before any GC safepoint. Many internal APIs returnCallResult> for this reason.
Common patterns with PseudoHandle:
// Storing into a PinnedValue (moves and invalidates the PseudoHandle):
lv.obj = std::move(*result);
// Converting to a Handle (allocates in GCScope — legacy):
auto handle = runtime.makeHandle(std::move(pseudoHandle));CallResult and error handling
CallResult<T> is the standard return type for operations that can throw a JS
exception. It is either a value of type T or ExecutionStatus::EXCEPTION. The
typical usage pattern is:
auto result = someOperation_RJS(runtime, args);
if (LLVM_UNLIKELY(result == ExecutionStatus::EXCEPTION)) {
return ExecutionStatus::EXCEPTION;
}
// Use *result or result.getValue() to get the value.
lv.obj = std::move(*result);Most functions that can trigger GC return CallResult> orCallResult<HermesValue>. The caller must check for exceptions before using the
value, and must store the value into a root before the next GC safepoint.
GCScope (legacy)
GCScope is a RAII-based variable-sized container of PinnedHermesValue. GCScopes
are always instantiated in a stack-like manner. They keep themselves in a singly
linked list — the root of this list is known to the GC. The GC crawls all GCScopes
during garbage collection, making all PinnedHermesValue stored there roots.
A GCScope is an efficient dynamic container — it dynamically allocates newPinnedHermesValue slots internally and returns instances of Handle<> orMutableHandle<>. However, to prevent unlimited growth, it is configured with a
fixed limit (48 by default in debug builds). This limit can be changed per scope.
The typical usage pattern is to declare a GCScope at function entry, allocate values
inside it as needed (keeping the handles). The GCScope is destroyed automatically
when the function exits.
IMPORTANT: Values allocated in a GCScope must not be used after the scope is
destroyed. This mistake is rare but it happens — typically by attempting to return aHandle<> that was allocated in a GCScope that is about to be destroyed.
Implicit GCScope allocation
runtime.makeHandle() and runtime.makeMutableHandle(), as well as theHandle<> and MutableHandle<> constructors, implicitly allocate a slot in the
topmost (i.e. most recently created) GCScope. This means:
- If a function creates
Handle<>orMutableHandle<>values (and does not
return them to the caller), it likely needs its ownGCScope, or at the very
least aGCScopeMarkerRAIIto free the slots when they are no longer needed.
AGCScopeMarkerRAIIis preferred when only one or two handles are allocated. - If a function calls APIs that return a
Handle<>, those handles are also
allocated in the topmostGCScope, so the same logic applies. Note that this
only applies to functions that takeRuntime &orPointerBase &— those are
the ones that can access the GCScope and allocate new slots. Functions likevmcast<>that do not take these parameters simply cast an existing handle
without allocating a new one.
Without a local GCScope or GCScopeMarkerRAII, handles accumulate in the
caller's GCScope, which can cause it to exceed its slot limit.
The problem with loops and GCScopeMarkerRAII (legacy)
A common problem when using GCScope is allocating handles in a loop, causing
potentially unbounded growth of rooted PinnedHermesValue slots. This is why
GCScope has a limit on the maximum number of handles.
One solution is to create a GCScope in the loop body. This works, but is
heavyweight — a GCScope preallocates space for 16 handles and must register and
deregister itself from the linked list every iteration.
GCScopeMarkerRAII is a lightweight alternative. It saves the state of a GCScope on
creation and frees all subsequently allocated handles on destruction. A typical
pattern is to place it at the top of the loop body:
GCScope gcScope(runtime);
// ...
for (...) {
GCScopeMarkerRAII marker(gcScope);
// Handles allocated here are freed at end of each iteration.
}Or, more efficiently, create the marker outside the loop and call flush()
explicitly:
GCScope gcScope(runtime);
// ...
auto marker = gcScope.createMarker();
for (...) {
gcScope.flushToMarker(marker);
// ...
}Locals and PinnedValue\ (preferred API)
All new code must use Locals + PinnedValue<T> instead of GCScope +makeHandle() for rooting local GC values. Do not introduce new GCScope
instances or makeHandle() calls. Existing code is being migrated incrementally.
Locals with PinnedValue<T> is the preferred way to create rooted storage for
local GC values. Handle<T> remains the standard type for passing GC values between
functions — PinnedValue<T> implicitly converts to Handle<T>, so the two work
together naturally.
How it works
You declare an anonymous struct inheriting from Locals, with PinnedValue<T>
fields for each GC value you need to keep alive. Then create a LocalsRAII to
register the struct with the runtime. LocalsRAII pushes the struct onto a linked
list (runtime.vmLocals). During GC, Runtime::markRoots() walks this list and
marks every PinnedHermesValue in every Locals struct as a root.
// Locals struct lives on the C++ stack. The PinnedValue fields are
// PinnedHermesValues that the GC will mark as roots.
struct : public Locals {
PinnedValue<JSObject> obj;
PinnedValue<StringPrimitive> str;
PinnedValue<> genericValue; // untyped — can hold any HermesValue
} lv;
LocalsRAII lraii(runtime, &lv);Why Locals is better than GCScope
| GCScope + Handle | Locals + PinnedValue |
|---|---|
Dynamically allocates PinnedHermesValue slots |
Fields are part of the struct — no dynamic allocation |
Handle<T> is a pointer to a PinnedHermesValue — one level of indirection |
PinnedValue<T> is the PinnedHermesValue — direct access |
Easy to accidentally allocate handles in a loop, requiring GCScopeMarkerRAII |
Fixed set of fields — loops cannot cause unbounded growth |
| 48-handle limit per scope (debug assertion) | No limit — number of fields is determined at compile time |
Assignment patterns
// From a PseudoHandle (the typical case after a CallResult):
lv.obj = std::move(*callResult);
// From a CallResult with a HermesValue that you know is a specific type:
lv.obj.castAndSetHermesValue<JSObject>(callResult.getValue());
// From a raw pointer:
lv.obj = someJSObjectPtr;
// From a GCPointer:
lv.obj = someGCPointer.get(runtime);
// Clearing (so it doesn't keep an object alive unnecessarily):
lv.obj = nullptr;Passing PinnedValue where Handle is expected
PinnedValue<T> implicitly converts to Handle<T>, so you can pass it directly
to functions that accept handles:
struct : public Locals {
PinnedValue<JSObject> O;
} lv;
LocalsRAII lraii(runtime, &lv);
lv.O.castAndSetHermesValue<JSObject>(*objRes);
// getPrototypeOf takes Handle<JSObject>, but PinnedValue<JSObject> converts:
return getPrototypeOf(runtime, lv.O);Reading from PinnedValue
// Get the underlying typed value:
JSObject *rawPtr = lv.obj.get(); // or *lv.obj
JSObject *rawPtr = *lv.obj;
// Dereference to access members:
lv.obj->someMethod();
// Get as HermesValue:
HermesValue hv = lv.obj.getHermesValue();Template functions
When calling PinnedValue member function templates like castAndSetHermesValue<T>
from a template context where the PinnedValue type is dependent, C++ requires thetemplate keyword before the member name:
template <typename T>
void doSomething(Runtime &runtime, HermesValue value) {
struct : public Locals {
PinnedValue<T> obj;
} lv;
LocalsRAII lraii(runtime, &lv);
// "template" is required because PinnedValue<T> is a dependent type:
lv.obj.template castAndSetHermesValue<T>(value);
}Without the template keyword, the compiler parses the < as a less-than operator
instead of the start of a template argument list, causing a compile error. This
applies to any template member function called on a PinnedValue with a dependent
type parameter.
Real-world example
From arrayConstructor in Array.cpp:
CallResult<HermesValue> arrayConstructor(void *, Runtime &runtime) {
NativeArgs args = runtime.getCurrentFrame().getNativeArgs();
struct : public Locals {
PinnedValue<JSObject> selfParent;
PinnedValue<JSArray> self;
} lv;
LocalsRAII lraii(runtime, &lv);
if (LLVM_LIKELY(!args.isConstructorCall() || ...)) {
CallResult<PseudoHandle<JSArray>> selfRes =
JSArray::create(runtime, runtime.arrayPrototype);
if (LLVM_UNLIKELY(selfRes == ExecutionStatus::EXCEPTION))
return ExecutionStatus::EXCEPTION;
lv.self = std::move(*selfRes);
} else {
CallResult<PseudoHandle<JSObject>> thisParentRes =
NativeConstructor::parentForNewThis_RJS(runtime, ...);
if (LLVM_UNLIKELY(thisParentRes == ExecutionStatus::EXCEPTION))
return ExecutionStatus::EXCEPTION;
lv.selfParent = std::move(*thisParentRes);
auto arrRes = JSArray::create(runtime, lv.selfParent);
if (LLVM_UNLIKELY(arrRes == ExecutionStatus::EXCEPTION))
return ExecutionStatus::EXCEPTION;
lv.self = std::move(*arrRes);
}
// ...use lv.self throughout the rest of the function...
return lv.self.getHermesValue();
}Locals and loops
Locals solves the loop problem inherently — the PinnedValue fields are a fixed
set, so you simply reuse them each iteration without needing GCScopeMarkerRAII.
However, some existing code uses a GCScope + GCScopeMarkerRAII alongsideLocals for functions that call APIs returning Handle<> (which require aGCScope). In such cases, the Locals hold the long-lived values, while theGCScope manages short-lived temporaries created by those API calls:
struct : Locals {
PinnedValue<JSObject> O;
PinnedValue<> elem;
PinnedValue<StringPrimitive> sep;
} lv;
LocalsRAII lraii(runtime, &lv);
GCScope gcScope(runtime);
// ...
auto marker = gcScope.createMarker();
for (uint32_t i = 0; i < len; gcScope.flushToMarker(marker), ++i) {
// Temporary handles created inside the loop are flushed each iteration.
// Long-lived values stored in lv.* persist across iterations.
auto strRes = toString_RJS(runtime, lv.elem);
if (LLVM_UNLIKELY(strRes == ExecutionStatus::EXCEPTION))
return ExecutionStatus::EXCEPTION;
lv.sep = std::move(*strRes);
}Common mistakes
Holding raw pointers across GC safepoints
// WRONG — rawPtr may dangle after the allocation:
JSObject *rawPtr = someHandle->getObject();
auto newObj = JSObject::create(runtime); // GC safepoint!
rawPtr->doSomething(); // rawPtr may be invalid!
// CORRECT — store in a PinnedValue, reload after:
struct : public Locals {
PinnedValue<JSObject> obj;
} lv;
LocalsRAII lraii(runtime, &lv);
lv.obj = someHandle->getObject();
auto newObj = JSObject::create(runtime); // GC safepoint
lv.obj->doSomething(); // Safe: PinnedValue is a rootHolding PseudoHandles across GC safepoints
PseudoHandle<T> is not a GC root — it holds an unrooted value just like a
raw pointer. It is equally dangerous to use a PseudoHandle after a GC
safepoint. This is easy to miss because PseudoHandle looks like a "safe" smart
pointer type, but it is not rooted.
Watch especially for multi-step creation patterns where two ::create()
calls are made in sequence:
// WRONG — ctor PseudoHandle is stale after the second create():
auto ctor = NativeConstructor::create(runtime, ...);
auto proto = JSObject::create(runtime); // GC safepoint — ctor is stale!
lv.ctor = std::move(ctor); // Too late — ctor is already stale!
// CORRECT — root the PseudoHandle before the next allocation:
lv.ctor = NativeConstructor::create(runtime, ...); // Root immediately.
lv.proto = JSObject::create(runtime); // GC safepoint — lv.ctor is safe.This bug often goes undetected because the stale value is still valid most of
the time — the GC only moves objects during compaction, which is rare. EnableHERMESVM_SANITIZE_HANDLES (enabled by default in ASAN builds) to catch these
bugs deterministically by moving the heap after every allocation.
Returning a Handle from a destroyed GCScope or Locals
// WRONG — the Handle points into the destroyed GCScope:
Handle<JSObject> createThing(Runtime &runtime) {
GCScope gcScope(runtime);
auto handle = runtime.makeHandle(JSObject::create(runtime));
return handle; // gcScope destroyed here — handle dangles!
}
// WRONG — returning a Handle from a PinnedValue that is about to be destroyed:
Handle<JSObject> createThing(Runtime &runtime) {
struct : public Locals {
PinnedValue<JSObject> obj;
} lv;
LocalsRAII lraii(runtime, &lv);
lv.obj = ...;
return lv.obj; // lraii destroyed here — Handle dangles!
}Instead, return a PseudoHandle (which copies the value out, not a pointer to the
root):
// CORRECT — return a PseudoHandle:
CallResult<PseudoHandle<JSObject>> createThing(Runtime &runtime) {
struct : public Locals {
PinnedValue<JSObject> obj;
} lv;
LocalsRAII lraii(runtime, &lv);
lv.obj = ...;
return PseudoHandle<JSObject>::create(*lv.obj);
}
// CORRECT — write into a caller-provided MutableHandle:
void createThing(Runtime &runtime, MutableHandle<JSObject> result) {
// No local rooting needed — we write directly into the caller's root.
auto res = JSObject::create(runtime);
result = res.get();
}
// CORRECT — return a HermesValue (caller roots it, so risky):
CallResult<HermesValue> createThing(Runtime &runtime) {
struct : public Locals {
PinnedValue<JSObject> obj;
} lv;
LocalsRAII lraii(runtime, &lv);
lv.obj = ...;
return lv.obj.getHermesValue();
}Forgetting to check for exceptions
// WRONG — using the value without checking for exception:
auto result = someOperation_RJS(runtime, args);
lv.obj = std::move(*result); // May dereference an exception!
// CORRECT:
auto result = someOperation_RJS(runtime, args);
if (LLVM_UNLIKELY(result == ExecutionStatus::EXCEPTION))
return ExecutionStatus::EXCEPTION;
lv.obj = std::move(*result);Null prototype handling
When traversing prototype chains, always check for null:
auto protoRes = JSObject::getPrototypeOf(obj, runtime);
if (LLVM_UNLIKELY(protoRes == ExecutionStatus::EXCEPTION))
return ExecutionStatus::EXCEPTION;
if (!*protoRes) {
lv.O = nullptr; // End of prototype chain
} else {
lv.O.castAndSetHermesValue<JSObject>(protoRes->getHermesValue());
}Doc GenGC (`doc/GenGC.md`)
id: gengc title: The GenGC Garbage Collector
GenGC used to be the default garbage collector for Hermes, and aims to provide a
generational garbage collector that returns memory to the OS aggressively and
frequently. The newer GC is called Hades, and it has much lower
pause times than GenGC. We recommend most users use Hades instead.
Throughout this document, we will refer to the heap, which in this context
means the memory space where JS objects reside. This is separate from the C and
C++ malloc heap, and in general if this document says "heap" it means the
space for JS objects.
Heap Segments
The heap is made out of many fixed-size regions of memory known as
heap segments (or more briefly as segments). Currently these segments
are 4 MiB, but this can be changed with the CMake build configuration variable-DHERMESVM_LOG_HEAP_SEGMENT_SIZE=number. Memory is acquired and released on a
per-segment basis. Segments are allocated using mmap on POSIX systems andVirtualAlloc on Windows systems. These functions allow virtual memory to be
requested for some space, and allows us to unmap subregions so that alignment
can be guaranteed. This is done in separate regions because we found that a lot
of devices have trouble allocating enough virtual memory upfront to have the
entire heap be contiguous. By allowing it to grow virtual address space usage
with the size of the heap (and shrink as well), it works on a broader set of
devices. We chose 4 MiB as the size of segments because it was small enough to
not exhaust virtual address space, while also being large enough to not have a
lot of overhead in managing the segments.
Each heap segment is aligned to begin on a pointer address that is a multiple
of its size. For example, if segments are 4 MiB wide, then their start
addresses are aligned on a 4 MiB boundary (the low 22 bits are all 0). This
means for any pointer into the JS heap, you can get the segment start address
with a simple bitwise-and operation. All metadata for the segment is stored at
the front of the segment.
At the beginning of a segment, there is a small amount of memory reserved for
some metadata about the heap. Specifically, the following things are kept there:
- Segment ID: explained in Compressed Pointers
- CardTable: explained in Generations
- MarkBitArray: explained in Mark Phase
- Guard page: a page of memory that is protected from reads and writes to ensure
bugs in the VM or GC don't accidentally overwrite metadata.
The rest of the segment is free space available for JS values to be allocated
into. SeeAlignedHeapSegment::Contents
for details about the layout of a heap segment.
Object Types
In order to determine reachability, we need to be able to discover
what objects point to. To do this for the variety of types in Hermes, we
define a system of metadata for each object, associated to its type which is
encoded as a VTable. Metadata is built once for the first Runtime created, and
is then re-used for every subsequent runtime. The metadata describes at what
offsets from the head of an object there are pointers. It also describes for
any array type where the length of the array can be found, so a dynamic number
of pointers can be found. This design allows pointers to be marked without
performing any type checks, branches, or virtual dispatch.
The VTable * is embedded in the header of everything allocated on the heap.
This is represented by the C++ superclass of all heap objects, called GCCell.
This way, given a pointer to something in the heap, its type (and pointer
metadata) can be found. The type is stored as a CellKind enum. The VTable
also includes function pointers for dynamic dispatch. This is used to implement
type-based dispatch for things like property accesses and function calls.
Generations
GenGC is so named because it is generational, meaning it manages groups of
allocated objects based on their age. Age is determined by how many collections
an object has survived. GenGC has two generations: the Young Generation (YG)
and Old Generation (OG).
Allocations go initially into YG, and if they survive the first YG collection
cycle, they are promoted to OG. The OG is collected less frequently and over a
larger number of objects. YG is collected frequently because it is assumed that
there are a lot of objects that were allocated but became garbage quickly. This
is known as the
Generational Hypothesis:
young objects are more likely to die than old objects.
The young generation of GenGC is a single segment, regardless of the size
of the heap as a whole. Since YG is much smaller than OG, this is also a much
shorter pause to collect YG. Since the YG is also a single segment, it's very
fast to check if a pointer is a YG pointer: get the segment start address and
compare to the YG start address. The allocation algorithm is as follows:
- Attempt to allocate into the YG segment by bumping a pointer called the
"level" of the segment up. If the level would go past the end of the segment, it
fails - If the bump allocation succeeds, the allocation is complete and control
returns to the VM - If the bump allocation fails, start a YG collection cycle. This will evacuate
the YG into the OG, and leave some empty space. - Try bump allocation again after the YG collection cycle completes. If it
succeeds, return. - Else, try to allocate directly into the OG.
- If the OG is full, start a full collection cycle (collect garbage in both the
YG and OG). Once that completes, try allocating directly into OG again. - If all that fails, that means there's not enough space for the allocation,
raise an Out of Memory (OOM) error
The old generation of GenGC is a list of segments, each of which is created
after an allocation failed in the previous segment. The OG maintains an
allocation context which remembers the most recent active segment, and this is
where promoted YG objects are allocated into. Once the OG has reached its
configured size limit, it will do a full collection, trying to free garbage
inside the OG.
There are more details on collection cycles in the
Collection Cycles section.
Allocate directly into the Old Geneneration (Pre-Tenuring)
Sometimes there are applications where the generational hypothesis doesn't hold
true, or in other words they allocate more objects that are long-lived than
short-lived. This typically occurs during the initialization period of an
application. If this rule applies to your application, you can request this
behavior from Hermes by changing the GCConfig that's used at Runtime
construction time:
hermes::vm::GCConfig::Builder gcConfigBuilder{};
gcConfigBuilder
.withAllocInYoung(false)
.withRevertToYGAtTTI(true);
std::unique_ptr<jsi::Runtime> runtime = makeHermesRuntime(
hermes::vm::RuntimeConfig::Builder()
.withGCConfig(gcConfigBuilder.build())
);The AllocInYoung parameter defaults to true, and controls whether
allocations go into YG by default or OG.
The RevertToYGAtTTI parameter defaults to false, and is unused unlessAllocInYoung is true.
See the full documentation of GCConfig for
more details and other configuration options.
Collection Cycles
GenGC determines garbage that can be collected in a program based on what is
reachable in the graph of objects. The graph has roots that define the
entrypoint to the graph. These are typically things like variables on the
JS call stack. From the roots, the entire graph of objects is traversed by
following each property to another object. Think of it as similar to a
traditional graph search algorithm, although we make many optimizations specific
to the heap structure.
GenGC has two different types of garbage collection cycles
(abbreviated as "GC"). There is a Young Generation Collection or YG GC, and a
Full Collection or full GC.
A YG GC collects only objects present in the YG, and promotes objects that are
reachable to the OG so they are collected less frequently.
A full GC collects both the OG and YG simultaneously, and determines the
reachability of every object in the heap.
There are two common concepts across both types of collections:
- Mark: Marking means setting some state related to an object to say that it was
found to be reachable during the traversal of the heap. In GenGC's case, this
is often referred to as a "mark bit" - Sweep: Sweeping means reclaiming unused space so that it can be re-used by the
allocator. This can look very different depending on the generation
Young Generation Collection
When a YG collection is triggered, it first needs to mark the roots of the
object graph. Because YG collections occur very frequently, we want the set of
roots to be small. To achieve this we ignore any roots that are known to be
only in the OG. When each pointer is discovered for the first time, an
allocation is made in the OG for the same size, the YG object's contents are
copied, and a forwarding pointer is left in the header of the YG object pointing
to the new OG object. After that, if we see the same pointer again, we replace
it with the value of the forwarding pointer.
We also need to consider any OG objects that point into YG as part of the root
set. That is handled by a separate mechanism during JS execution, detailed below
in the section on Write Barriers.
To find all the reachable objects, we take advantage of the linear nature of
OG, and scan linearly from the original end of the allocated region, until
there aren't any more promoted objects. Each promoted object is scanned for its
own pointers. This is how all YG objects are discovered. It's similar to a
semi-space algorithm where the second space is the OG.
Once the objects are all moved, any weak references pointing to YG objects are
updated if the object is still alive, and then any finalizers are run by
iterating over a list of finalizers.
Write Barriers
As part of the root set, we also consider any OG objects that point into YG.
To determine where these exist without iterating over the whole OG, we use a
write barrier whenever a pointer value is written to during JS execution.
If it would create an old -> young pointer, we want to mark the source object
as part of the root set of YG.
The way we do this is by classifying a small region of memory in the OG as a
card, currently 512 bytes. We determine the card for a pointer using the
same alignment trick to get the base address of the segment, then use division
to figure out the card index. The card table is a list of bytes stored in
the header of the segment, and when we find an object pointing into the YG, we
"dirty" its card by setting the byte value to 1. We don't use a bitmap because
the card table is only 8 KiB, and shrinking it further isn't worth it.
During YG collection time, we can quickly scan each card table for the set of
dirtied cards, then we mark all of the objects on that card. We use a separate
card object table to find the first object for a card. That table contains
another byte per card, except it is signed. If the sign is positive, it means
"go back that many bytes to find the first object on this card". If the sign
is negative, it means "go back that many cards and check the table at that
index". This encoding scheme allows a low memory cost way to jump across very
large objects.
Note that the way cards are dirtied means there's a chance of promoting some
YG objects that aren't actually live. This tradeoff is chosen so the cards take
up less memory and can be scanned quickly. Smaller cards are more precise, but
take up more memory and require more scanning.
Full Collection
A full collection is much larger than a YG collection, so it happens less
frequently. It needs to determine the reachability of every object in the heap.
A full collection is triggered when the OG doesn't have enough space to satisfy
an allocation from a YG promotion or a direct-to-OG allocation.
Mark Phase
The collection begins with a call to markRoots, although this time
additionally marking roots known to only point into the OG, such as theIdentifierTable. The first time an object is encountered, it sets a bit in the
mark bit array, so future accesses know that it has already been found. Then we
push the newly discovered object on top of a mark stack.
The mark stack is drained by popping off the top object, scanning all of its
pointers, and pushing objects it discovers onto the mark stack.
There are two changes implemented to prevent the mark stack from growing without
bound:
- If a pointer to an object is discovered that is after the current address, it
isn't pushed on the mark stack. Instead, we jump to the next marked bit when
the mark stack is empty. If the pointer is before the current address, it has to
be pushed on the stack because we won't go backwards in the heap normally - If the mark stack overflows a limit, we drop the whole stack and re-scan from
the beginning of the heap. Since many objects have already been marked, this
will go much faster than previous times. It's guaranteed to terminate because
in the worst case eventually every object will be marked and there's nothing
to collect.
Once the marking is complete this way, full reachability is known, and sweeping
can begin.
Weak Map resolution
However, one problem to be handled are weak references, which are pointers
that do not keep what they point to alive. Now that we know reachability, we
know which weak pointers to make null.
But there's a special case of the JS type WeakMap that has to be handled:
the keys are weak, and the values are strong, unless they keep the key itself
alive. This means the values can't be marked until the key is determined
reachable through some other means.
We delay marking any WeakMap key/value pairs until the end of marking. Then for
each WeakMap, and for each key that is reachable in the weak map, we then mark
the value. If marking the value ends up making another WeakMap key reachable,
we then need to mark that. We repeat this for every key that becomes reachable
until there's nothing more to mark. At that point we can finally null out keys
that were never found to be reachable.
Sweep & Compact Phases
Now that full reachability has been computed, we know which objects are garbage.
We want to reclaim that memory so that it can be allocated again. To do so, we
will compact all live memory down so that there's no space between them,
overwriting the space that was used by dead objects.
To do this, we iterate through every live object in the heap using the mark
bits, and for each one we compute its new address. We place that new address
as a forwarding pointer in the header of the object (displacing the VTable *
to a temporary vector to avoid losing the type information). Once a forwarding
pointer has been determined for everything in the heap, we do a second pass to
fixup pointers within objects to use the forwarded value. Finally, we copy the
memory from the previous location to the new location, overwriting whatever used
to be there.
The end result of this is that all live memory sits right next to each other in
the heap segments, and all dead memory has now been reclaimed for the allocator.
We use madvise on POSIX systems to tell the operating system that the leftover
memory is now not needed by the program, and it's free to be paged out and zero
filled if the OS needs to.
At this point control returns to the JS engine, typically to finish the
allocation that started the collection.
Doc Hades (`doc/Hades.md`)
id: hades title: The Hades Garbage Collector
Hades is a garbage collector for Hermes that aims to improve pause times by an
order of magnitude over GenGC. The main principle Hades uses to achieve those
low pause times is that most of the garbage collection work happens in a
background thread concurrently with the interpreter running JavaScript code.
This is distinct from GenGC, which only runs on a single thread which is shared
with the interpreter.
Enabling Hades
In local builds on the command line using CMake (which forwards to CMake), Hades
is the default GC used, and currently the only GC supported for production use.
The GC being used is controlled by the CMake variable -DHERMESVM_GCKIND=value.
To use a pre-built package of Hermes with Hades enabled, check the
Releases page on Github.
As of right now, there aren't any available, but we'll be making one available
with v0.8 and later.
Check Which GC is Used
If you want to know what GC is being used in your application, you can find out
with some JS:
const gcName = HermesInternal.getRuntimeProperties().GC;
// If you're running Hermes on the command line, use print.
print(gcName);
// If you're running Hermes in some kind of framework like React Native,
// console.log should exist.
console.log(gcName);This will print one of:
"hades (concurrent)": You're using Hades in concurrent mode"hades (incremental)": You're using Hades in incremental mode
Basics
Most of the basic heap structure of Hades is similar to GenGC, so it is
recommended to read the GenGC Documentation first, in particular
the following sections:
Generations
Similarly to GenGC, Hades also has two generations: the Young Generation
(YG) and Old Generation (OG). Allocations go initially into YG, and if they
survive the first collection they go into OG. YG works exactly the same as
GenGC, but OG has a different allocation strategy that allows for gaps.
Freelist Allocator
Hades's OG is a list of heap segments, and each heap segment maintains a
Free List of empty space. Each Free List Cell points to the next cell,
called an explicit free list. This is opposed to an implicit free list, where
the length is used to traverse both free and used cells. A free list is used
because it allows empty space to be left where it is, without requiring
compaction. This is a requirement for concurrent allocations and sweeping.
Furthermore, the free list is size-segregated, meaning each size class gets
a separate free list. In other words, cells of size N only point to other
cells of size N. This allows an allocation of size N to be satisfied
instantly with the head of the free list. Each free list head is stored at an
index in a fixed-size array for small cell sizes. These are known as
buckets.
Hades does not do any rounding up of sizes beyond the required heap alignment
of 8 bytes. This means there is one bucket for each multiple of 8, up to 2048
bytes. From 2048 bytes to the maximum heap segment size (4 MiB) the buckets go
by powers of 2. For large buckets, we store cells that are greater than or equal
to the size bucket, but less than the next power of 2. Cells that need less than
the size of the free list cell carve out a small piece of the cell, and put
the remaining piece on a the free list corresponding to its new size.
Due to having these free lists be per-segment, we need a quick way to find which
segment has free space for a given size. We do this with a series of bit arrays,
where bits are flipped to 0 as a free list is exhausted in a segment, and
flipped back to 1 when sweeping frees some cells. We have per-segment free lists
so that sweeping and compaction (which also operate on a per-segment basis) can
destroy them efficiently and create a new list. It can do this to easily
coalesce adjacent free regions.
Collection Cycles
Hades has two different types of collections: a YG collection (YG GC) and an
OG collection (OG GC). The former is almost exactly the same as GenGC's, so we
won't repeat it here. The OG GC is very different though, because it runs
concurrently in a background thread.
For the purposes of distinguishing these two threads, we'll name them as
follows:
- Mutator Thread: The thread running the JS interpreter
- GC Thread: The thread running any GC operations such as marking or
sweeping
Note that currently there is only ever a single GC thread at any point in time.
We also cache the thread and reuse it instead of making a new one for each
collection.
There are three different locks used throughout the GC:
- The GC Mutex is used to protect structures like mark bits, card tables,
and the free lists - The WeakRef Mutex is used to protect structures used during weak ref
marking - The Write Barrier Mutex is used to protect a small buffer used by write
barriers and concurrent marking
The GC mutex is used to protect most things, as they tend to all be accessed at
the same time. There was no need for finer grained locks yet, with the exception
of the write barrier mutex because write barriers are executed all the time.
An OG GC is started once the OG is about 75% full. We start it a bit early so
that it can complete sweeping before reaching 100% full and avoid blocking any
allocations.
Mark Phase
The first step of an OG GC is to mark all of the roots of the object graph.
We only ever start an OG GC when YG is empty, so there's no need to mark any of
YG.
Marking an object consists of the following steps:
- Using mark bits, check if an object has been visited already
- If it has been visited already, there's nothing to do
- If not, push it onto a mark stack that will be drained later
- Set its mark bit
- If the object pointed to is a WeakMap, put it onto a separate stack
(see Weak Map Resolution)
Draining the mark stack works as follows:
- Acquire a lock on the GC mutex
- Check if the write barrier buffer has objects that need to be marked, if so,
add them to the mark stack - While we've marked fewer than a certain number of bytes, defaults to 8 KiB
- Pull one object off the mark stack
- Get its type metadata (see Object Types)
- Use the metadata to find pointers to other objects
- Those other objects will be pushed on the stack
- Release the lock on the GC mutex
This can run almost entirely uninterrupted on the background thread since very
few things need to acquire the GC mutex. The most common way to interrupt
marking is when YG fills up, as it requires the GC mutex in order to evacuate
YG.
Write Barriers
There's an important race condition to consider when thinking about concurrent
marking: what happens if a pointer is modified while we're reading it?
There are two different races that are possible here:
- A non-atomic read of the pointer might race with a non-atomic write, and the
reads or writes might "tear" (meaning you see only part of the write) - You might miss marking the old value or the new value
The first is handled on 64-bit platforms because all of the reads are of a 64
bit value, which can be atomically handled cheaply on a 64-bit CPU. See the
Incremental Mode section for what we do on a 32-bit CPU.
The second problem is harder to solve. If we see the old value, and the new
object isn't marked anywhere else, we would accidentally think it's garbage and
collect it! Alternatively, if we see the new value, the old value won't be
marked. This would be a problem if the old pointer was moved from one object to
another, but we had already marked the second object.
In order to fix this, we need to know when a pointer is modified during
concurrent marking. Hades implements this through an additional write barrier.
This write barrier is based on a principle called "Snapshot at the Beginning"
(SATB). The principle is that we want to collect the OG based on a snapshot
of the heap when the collection began. Which means if a pointer is changed, we
want to make sure we mark the old value instead of the new value.
This might feel counter-intuitive compared to the more common alternative
approach known as "Incremental Update" (IU), where the new value is marked. The
reason Hades uses SATB instead is that it has a nice guarantee: you'll never
need to revisit any object you have already marked. This means there is a finite
upper bound on the amount of work marking has to do. IU write barrier based
systems often have a race near the end, where the GC thread needs to pause the
mutator thread to try and complete marking as fast as possible. If it exceeds
a time quota, it resumes the mutator and tries again later. We avoid this
complexity with our SATB barrier.
A second benefit of SATB is that we can treat any allocations made into OG
between the start and end of the collection as alive by default, without
needing to mark them.
And the final benefit of SATB is that there is never a need to mark the roots
again to finish a collection, as their old values were handled at the start
of the collection while the mutator was paused.
The barrier works by pushing the old value onto a small fixed size buffer, which
has space for 128 elements. Once it fills up, the Write Barrier Lock is taken to
"flush" the buffer into a separate mark stack used by the concurrent marker.
This means a lock is only taken every 128 write barriers. It uses a separate
mutex from the GC mutex to ensure a write barrier is not blocked for very long
if the GC thread happens to be reading from the separate mark stack.
Complete Marking
Once the mark stack is empty, there are a few details that need to be handled in
order to complete marking and move on to sweeping:
- Flush any remaining write barrier pointers left
- Handle WeakMap resolution
- Fix weak references (WeakRefs)
Handling these things can be very tricky concurrently, so in order to prevent
bugs and infinite loops, we pause the mutator during this time. Even though SATB
write barriers don't require the mutator to pause, these other operations do
require a pause, so unfortunately this is still required.
Flushing the remaining write barrier pointers just means copying the pointers
into the mark stack and draining it one more time. This could potentially take
a very long time, but in practice that is exceedingly rare.
WeakMap resolution is handled in the same way that GenGC handles it. We do this
during a mutator pause mostly because we didn't want to rewrite the algorithm
to work in a concurrent context, as it's already very complicated on its own.
Weak References also need to be cleared if they point to something that is
now garbage, and this is much easier to do with the mutator paused. Otherwise
the mutator would need a lock to read a weak reference value. This could be
improved to simply be an atomic operation in the future, but for now this can't
be atomic.
Once that's all taken care of, we can move on to sweeping.
WeakRef Read Barriers
There's a caveat to mention about WeakRefs and the SATB barrier. If you read a
pointer out of a weak reference and store it in an object on the heap, SATB
won't record the change, and the object might not be found reachable. Something
similar can happen if a weak ref is read and placed into a root.
To fix this, we have WeakRef reads perform a barrier on the pointer being read.
This conservatively assumes the pointer being read is alive. WeakRefs are not
read from that often, so this was deemed an acceptable cost.
Note that there also weak roots, such as the HiddenClass cache stored in each
CodeBlock. These do not perform a read barrier, specifically because they are
only ever used for comparisons. They never produce a pointer that was otherwise
dead. A possible simplification of this in the future could use a HiddenClass ID
instead of a pointer, as it achieves the same effect without requiring a special
case.
Sweep Phase
Once complete reachability information is known, the OG GC turns off the
SATB barriers. The sweeper iterates over one segment at a time, allowing the
mutator to interleave. This is specifically allowed because the sweeper only
ever modifies garbage objects that aren't used, therefore there's no races.
It holds the GC mutex to prevent YG from allocating into the OG while it's
being swept.
The process works as follows:
- Acquire a lock on the GC mutex
- Clear one heap segment's free list
- Iterate linearly over cells, using the embedded length to skip over live cells
- Check if a cell is new garbage using its type tag
- If it is not new garbage, continue to the next cell
- If it is new garbage, turn it into a free list cell.
- Contiguous unused regions are added as a single region onto the new free list
- Once all cells in the segment have been processed, release the lock
Once that process is completed for every heap segment, sweeping completes and
the OG collection is over.
Compact Phase
Compacting live memory to be closer together is still a beneficial concept in
Hades, as it allows us to return unused memory to the OS, and reduces the
fragmentation of the free lists for mostly empty heap segments. Implementing it
is more tricky than GenGC though, as we can't modify pointers concurrently with
the mutator thread.
Due to these restrictions, we can currently only compact a single segment
(called the compactee) for each full collection. Compaction runs as part of the
collection cycle and flows as follows:
- At the start of an OG collection, determine whether the heap is currently
larger than its target size. If so, select and record a segment to compact. - Write barriers start dirtying cards for pointers pointing into the
compaction candidate. This will continue until the compaction is fully complete. - Marking begins. During marking, we dirty cards in the card table
corresponding to any on-heap pointers that point into the compaction candidate.
Any YG collection that occurs during marking needs special care. Promoted
objects will not be scanned by the OG since they are allocated as marked, so
they need to be scanned for compactee pointers after they have been promoted.
Furthermore, the card table cannot be cleared at the end of the YG collection,
since that would erase information from the ongoing compaction. - During the STW pause, the internal state of the GC is updated to signal that
all pointers into the compactee have been marked, and that the next YG
collection should complete the compaction. - Sweeping. The segment identified for compaction will not be swept, however
compaction may take place during sweeping if the next YG collection starts
before sweeping is complete. Note that write barriers will continue to be active
until compaction is complete, since new pointers from the OG into the compactee
may be added. - Compaction. The next young gen collection evacuates both the YG and the
compactee. It will mark long lived roots and update pointers based on the
previously dirtied cards. Combining compaction with YG collections lets us
share the overhead of updating roots, and lets us avoid tracking pointers from
the YG into the compactee. - The now empty segment is released by the GC and returned to the OS.
Incremental Mode
Hades's concurrent marking relies on being able to read a 64-bit value
atomically at the same time it might be modified by the mutator. If the
underlying hardware supports this natively, then we use it.
However, some hardware does not support doing those atomic reads in a lock-free
manner, primarily 32-bit ARM CPUs. Since Hermes's main target is mobile devices,
it's important for us to still support them, and have some of the fast pause
time guarantees that Hades gives.
In order to do this, on 32-bit platforms we don't use any other threads, and
instead run Hades in "incremental mode". This means instead of marking objects
concurrently on the GC thread, we use a portion of each YG GC to do some OG GC
work. This means the OG GC is completed incrementally on each individual YG GC.
Each YG GC takes a little bit longer while an OG GC is active, but the penalty
is small enough to still have better guarantees than running a fully blocking OG
GC.
The concurrent mode of Hades has faster pauses and is preferred to be used if
possible, but incremental mode has to be used on most 32-bit CPUs. You can also
use incremental mode if threads aren't supported on your platform, or if you
prefer to not use threads for some other reason.
Doc HighLevelOptimizations (`doc/HighLevelOptimizations.md`)
id: high-level-optimizations title: High Level Optimizations
JS authors frequently use high level library functions which they expect to perform efficiently.
They call these in conjunction with other library calls to write code which has similarities to
functional programming languages.
Executing this kind of code in Hermes can be slow due to the compiler not having proper visibility
into the JS library. However, we can get some information about what is being called in certain
scenarios, such as with -fstatic-builtins or in actual typed SH code.
This document is a place to put some ideas on how to make specific scenarios more efficient
in the future. It is possible we'll just happen to optimize some cases here as a result of good
type information in SH and a solid optimization pipeline.
Spreading and modifying existing structures
An example of some sample code found in input to Hermes to clone a Map and add an element:
return new Map([...Array.from(origMap), [id, obj]]);- Iterates
origMapand creates a new array%tmp1, element by element. - Creates a new array
%tmp2, iterates%tmp1, and copies the elements. - Adds the new array
[id, obj]to%tmp2. - Iterates
%tmp2and populates a newly createdMap.
Some potential ways to simplify things:
...Array.from(a)is the same as...aprovided that the array iterator has not been modified.new Map([...oldMap, [x, y]])is the same asconst %tmp = new Map(oldMap); %tmp.set(x, y)
provided that theMapiterator has not been modified.
If we can enforce that iterators haven't been modified, then we can do step 1 potentially even in
legacy code, and step 2 in typed code.
Doc IR (`doc/IR.md`)
id: ir title: Design of the IR
Introduction
This document is a reference manual for the Hermes High-level IR. The IR is a
Static Single Assignment (SSA) based representation that captures the JavaScript
language semantics. It features optional types (values may be annotated with
types).
The IR representation is designed to be used as an in-memory form. The IR can be
dumped to human readable assembly-like format.
Well-Formedness
This section describes the rules that define a valid IR.
Instructions must be dominated by their operands.
Basic block must end with a terminator, which is a branch instruction or a
return instruction.There has to be exactly one terminator for each basic block.
AllocStack instructions must appear at the 'entry' basic block (the first basic
block in the function).PHI Nodes must appear at the beginning of the basic block and must contain
an entry for each predecessor.
Frames:
Every JavaScript function is a closure that can capture variables from its
declaration scope. The declaration scope may be the global scope or another
function. A closure is a pair of function code, which contains the code of the
function, and a context which saves the environment that the closure may access.
Function variables that are captured by a closure are allocated in a scope on
the heap and the scope is passed as part of the context to the closure.
Closures may access variables at all nest-levels, which means that they can
access multiple scopes. We implement this feature by nesting scopes and linking
them together. Each scope (the set of captured variables) also has a reference
to the scope of the caller function. Closures can access variables at different
nesting levels by loading the parent scope for each scope.
At the IR Level we define a few instructions that can load and store frame
variables. Functions own variables, that are storage units that represent
variables at the source level. These variables can be optimized away or
promoted to the stack, etc. We define the AllocStack instruction for allocating
variable storage. The only instructions that can access the storage values
(Variable and AllocStack) are the Load and Store instructions. It is not
possible to save the address of the allocation itself. Depending on the
implementation of the virtual machine, the heap allocations may be packed into a
single frame.
Types:
The Hermes high-level IR is optionally typed. Values may be annotated with the
expected type of the value. The types are optional and untyped programs are
correct. The types represent primitive JavaScript types, or refinement of these
types. All values can be annotated with types: Functions, Instructions and
Parameters. Annotations of functions represent the type of the returned value.
The type annotations are not a suggestion or a hint. They must be correct and
consistent or else the program may fail at run time. Programs are expected to
run and have the same semantics when the type annotations are stripped.
Some instructions in the IR expect operands of specific types, or produce
specific types. For example, the CondBranch instruction expects the condition
operand to be annotated as a boolean type. The ToBool instruction produces
values that are annotated as boolean type.
The optimizer may optimize the IR based on type annotations. For example, if the
operand of the instruction ToBool is annotated with the boolean type then the
optimizer is free to remove the instruction and replace all uses with the
operand of the conversion instruction.
Example:
This is a short example of a valid program in textual IR. The function contains
four basic blocks with a few instructions. The code below follow the
requirements of a well-formed function defined in the previous section.
function forEach(cond : boolean, value : number)
%BB0:
%0 = BranchInst %BB1
%BB1:
%2 = CondBranchInst %cond, %BB2, %BB3
%BB2:
%3 = ReturnInst %cond
%BB3:
%4 = ReturnInst %valueSide Effects:
The instructions in the IR are annotated with side effect labels. The
labels describe the kind of effect the instruction has on memory, IO
state (for example, hardware IO such as drawing to a screen or sending
a network packet), and whether they can throw. The two kind of memory
side effects are 'read' and 'write'. ('write' actually indicates
"read and/or write".) These side effect specifications indicate that
the instruction does not have IO effects or throw. The 'unknown' side
effect is maximally conservative: it indicates that the instruction
may read, write, have IO effects, and/or throw. These side effect
annotations allow the optimizer to decide which optimizations are
legal. For example, it is not legal to sink instructions that can
write to memory past each other. It is not legal to hoist instructions
that read to memory outside of a loop if there is another instruction
that writes to memory in that loop, because the writer instruction may
clobber memory that the reader may read.
Some instructions that allocate memory are marked as not having any side
effects. This is because the instructions only touch the newly allocated memory
and do not influence the rest of the heap. Notice that we don't model the effect
of instructions on the garbage collector or anything like that, just the content
of the allocated memory.
Register allocation
In Hermes, register allocation is performed on the IR. An infinite number of
virtual registers are allocated while preserving the constraints of the IR and
of the low-level target. Registers are allocated in an attempt to reduce the
number of registers, values across PhiNodes are coalesced, AllocStack
instructions are assigned with a dedicated register and arguments to call
instructions are placed in consecutive registers.
While in SSA form, the IR does not support all of the things that can be done
with registers. This is why the Register Allocator introduces MOV instructions
that represent a copy of one register to another. We lower some of the
load/store instructions into MOVs, and spill registers with MOV instructions.
Generator Overview
When generating the IR for a generator function, we make two functions:
an outer GeneratorFunction and an inner function.
The outer function calls CreateGenerator on the inner function,
and returns the resultant generator.
The inner function assumes that it can store and retrieve state from
its own closure. As such, it contains instructions to start, save/yield,
and resume generators.
CreateGenerator:
An instruction to create a generator given a Function.
First, it creates an inner GeneratorInnerFunction, then it wraps it in a Generator object.
Used by the GeneratorFunction to create the generator which is returned upon calling it.
StartGenerator:
Always the first instruction executed when an inner function is called.
Restores values for all local variables in the generator,
and jumps to the resume point of the generator if it's suspended.
If the generator hasn't been started yet, simply continues execution.
SaveAndYield:
Saves necessary state to the closure and yields execution.
In practice, this will save state and use the return opcode to
allow the caller to get the yield result.
Emitting a SaveAndYield also emits its corresponding ResumeGenerator.
ResumeGenerator:
Placed at the start of the block following the SaveAndYield to which it
corresponds. Resumes execution by loading context from the closure,
and then using state stored in the closure, does one of 3 things:
- Continues execution with the result of the instruction being the
argument to.next(). - Throws a value immediately
- Sets an
%isReturnflag to true, which later instructions may branch on to execute thefinally
if necessary, and then return.
Instruction semantics
This section describes the semantic of the instruction that are defined in the
IR. Please make sure to update this section as new instructions are added to the
compiler.
BranchInst
| BranchInst | _ |
|---|---|
| Description | Jumps to a different basic block. |
| Example | %0 = BranchInst %BB1 |
| Arguments | A single operand which is the target basic block. |
| Semantics | Terminates a basic block and 'jumps' to a different basic block. |
| Effects | Does not read or write from memory. |
ReturnInst
| ReturnInst | _ |
|---|---|
| Description | Leaves the function and returns a value. |
| Example | %0 = ReturnInst %17 |
| Arguments | A single operand which is the returned value. Notice the functions that return without an explicit value return the 'undefined' value. |
| Semantics | Terminates a basic block and transfer the control to the caller of the current function. |
| Effects | Does not read or write from memory. |
AllocStackInst
| AllocStackInst | _ |
|---|---|
| Description | Allocates a variable on the stack. |
| Example | %0 = AllocStackInst $name |
| Arguments | $name is the textual representation of the variable at the sourcecode level. |
| Semantics | AllocStack allocates a variable on the stack. Depending on the implementation of the VM, the variables may be packed into a single frame. AllocStack values may be used by instructions in different functions that represent closures created by the current functions. AllocStack values are used to represent local and captured variables. The AllocStack itself needs to be used directly. It is not possible to save a reference to the reference. The lifetime of the AllocStack may not exceed the lifetime of the allocating function. |
| Effects | Does not read or write from memory. |
LoadFrameInst
| LoadFrameInst | _ |
|---|---|
| Description | Loads a value from a variable. |
| Example | %1 = LoadFrameInst %scope, %variable |
| Arguments | %variable is a location in %scope to load from. |
| Semantics | The the instruction reads from a variable. The variable must be valid and found in the given scope. |
| Effects | Reads from memory. |
LoadStackInst
| LoadStackInst | _ |
|---|---|
| Description | Loads a value from a stack allocated memory pointed by a reference. |
| Example | %1 = LoadInst %0 |
| Arguments | The address from which the instruction loads. |
| Semantics | The the instruction reads from memory. The address must be a valid stack address. |
| Effects | Reads from memory. |
StoreFrameInst
| StoreFrameInst | _ |
|---|---|
| Description | Stores a value to a frame variable. |
| Example | %1 = StoreFrameInst %scope, %value, %variable |
| Arguments | %value is the value to be stored. %variable is a variable in %scope where where the value will be stored. |
| Semantics | The the instruction saves a value to memory. The variable must be a real variable found in the scope. |
| Effects | Writes to memory. |
StoreStackInst
| StoreStackInst | _ |
|---|---|
| Description | Stores a value to a stack allocated memory. |
| Example | %1 = StoreStackInst %value, %stack_allocated |
| Arguments | %value is the value to be stored. %address is the reference to stack allocation. |
| Semantics | The the instruction saves a value to memory. The address must be a valid stack allocation. |
| Effects | Writes to memory. |
ToPropertyKeyInst
| ToPropertyKeyInst | _ |
|---|---|
| Description | Converts a JavaScript value into a property key. |
| Example | %1 = ToPropertyKeyInst %input |
| Arguments | The value to cast. |
| Semantics | Implements ES15 7.1.19 ToPropertyKey ( argument ). |
| Effects | May execute. |
AsNumberInst
| AsNumberInst | _ |
|---|---|
| Description | Casts a JavaScript value into a number value. |
| Example | %1 = AsNumberInst %input |
| Arguments | The value to cast. |
| Semantics | The instruction follows the JavaScript rules for converting types into numbers. |
| Effects | May read or write to memory. |
AsInt32Inst
| AsInt32Inst | _ |
|---|---|
| Description | Casts a JavaScript value into a signed 32-bit integer value. |
| Example | %1 = AsInt32Inst %input |
| Arguments | The value to cast. |
| Semantics | The instruction follows the JavaScript rules for converting types into 32-bit signed integers. |
| Effects | May read or write to memory. |
AsUint32Inst
| AsUint32Inst | _ |
|---|---|
| Description | Casts a JavaScript value into an unsigned 32-bit integer value. |
| Example | %1 = AsUint32Inst %input |
| Arguments | The value to cast. |
| Semantics | The instruction follows the JavaScript rules for converting types into 32-bit unsigned integers (equivalent to >>> 0). |
| Effects | May read or write to memory. |
AddEmptyStringInst
| AddEmptyStringInst | _ |
|---|---|
| Description | Convert a value to string as if evaluating value + '' |
| Example | %1 = AddEmptyStringInst %input |
| Arguments | The value to cast. |
| Semantics | The instruction follows the JavaScript rules for adding an empty string to a value (ES5.1 11.6.1). |
| Effects | May read or write to memory or throw. |
CreatePrivateNameInst
| CreatePrivateNameInst | _ |
|---|---|
| Description | Create a globally unique private value, settings its internal [[Description]] to the given string literal. Practically speakig, this will result in a symbol value created at runtime with its [[Description]] set. |
| Example | %1 = CreatePrivateNameInst %descString |
| Arguments | %descString is the string description of the name. It must be a string literal. |
| Semantics | Allocate a new globally unique private name. |
| Effects | Does not read or write to memory. |
CondBranchInst
| CondBranchInst | _ |
|---|---|
| Description | Jumps to one of two blocks depending on a condition value. |
| Example | %1 = CondBranchInst %cond, %BB1, %BB2 |
| Arguments | %cond is the condition variable, %BB1 is the 'True' block, %BB2 is the 'False' block. |
| Semantics | The instruction observes the value of a typed value and jumps to one of two basic blocks. If the condition is evaluated as 'True' the program jumps to the 'True' block. Otherwise the program jumps to the 'False' block. |
| Effects | Does not read or write from memory. |
HBCCompareBranchInst
| HBCCompareBranchInst | _ |
|---|---|
| Description | Performs a binary comparison of the two operands and a conditional branch depending on the result. |
| Example | %0 = CompareBranch %x, %y, %BB1, %BB2 |
| Arguments | %x and %y are the operands of the binary operation, %BB1 is the 'True' block, %BB2 is the 'False' block. |
| Semantics | The instruction follows the rules of JavaScript for each one of the binary operators defined in the instruction. If the condition is evaluated as 'True' the program jumps to the 'True' block. Otherwise the program jumps to the 'False' block. |
| Effects | May read and write memory. |
HBCFCompareBranchInst
| HBCFCompareBranchInst | _ |
|---|---|
| Description | Performs a binary comparison of the two numbers and a conditional branch depending on the result. |
| Example | %0 = HBCFCompareBranch %x, %y, %BB1, %BB2 |
| Arguments | %x and %y are the operands of the binary operation, %BB1 is the 'True' block, %BB2 is the 'False' block. |
| Semantics | Performs a numeric comparison on the two doubles. If the condition is evaluated as 'True' the program jumps to the 'True' block. Otherwise the program jumps to the 'False' block. |
| Effects | Does not read or write to memory. |
HBCCmpBrTypeOfIsInst
| HBCCmpBrTypeOfIsInst | _ |
|---|---|
| Description | Compare the type of a value against known types and branch. |
| Example | %0 = HBCCmpBrTypeOfIsInst %val, %types, %trueBlock, %falseBlock |
| Arguments | %val is the argument to the typeof comparison, %types is a TypeOfIsTypes (see Typeof.h), %trueBlock and %falseBlock are the targets |
| Semantics | Branch to true if the type of %val matches the bit in %types, false otherwise. |
| Effects | Does not read or write memory. |
GetParentScopeInst
| GetParentScopeInst | _ |
|---|---|
| Description | Retrieve the parent scope of the current function. |
| Example | %0 = GetParentScopeInst %varScope, %parentScopeParam |
| Arguments | %varScope is the VariableScope that describes the parent environment. %parentScopeParam is dummy parameter used to model usage of the parent environment. |
| Semantics | The instruction returns the enclosing environment of the currently executing function. |
| Effects | Does not read or write to memory. |
CreateScopeInst
| CreateScopeInst | _ |
|---|---|
| Description | Creates a new scope which can be used to store variables. |
| Example | %0 = CreateScopeInst %variablescope, %parentScope |
| Arguments | %variablescope is a VariableScope describing the variables stored in the produced scope. %parentScope is the scope to use as the parent of the new scope, or EmptySentinel if the scope does not have a parent. |
| Semantics | The instruction creates a new scope which can be used to store/retrieve variables, and allow inner functions to access variables in an enclosing scope. |
| Effects | Does not read or write to memory. |
ResolveScopeInst
| ResolveScopeInst | _ |
|---|---|
| Description | Traverse the scope chain to retrieve an enclosing scope. |
| Example | %0 = ResolveScopeInst %variablescope, %startVarScope, %startScope |
| Arguments | %variablescope is the VariableScope corresponding to the enclosing scope to retrieve. %startScope is the scope from which to start traversing. %startVarScope is the VariableScope associated with %startScope. |
| Semantics | The instruction retrieves the requested scope, which must be reachable from the starting scope. |
| Effects | Does not read or write to memory. |
LIRResolveScopeInst
| LIRResolveScopeInst | _ |
|---|---|
| Description | Traverse the scope chain to retrieve an enclosing scope. |
| Example | %0 = LIRResolveScopeInst %variablescope, %startScope, %numLevels |
| Arguments | %variablescope is the VariableScope corresponding to the enclosing scope to retrieve. %startScope is the scope from which to start traversing. %numLevels is the number of levels to walk up the chain. |
| Semantics | The instruction retrieves the requested scope, which must be %numLevels up from the starting scope. |
| Effects | Does not read or write to memory. |
GetClosureScopeInst
| GetClosureScopeInst | _ |
|---|---|
| Description | Retrieve the scope from the given closure. |
| Example | %0 = GetClosureScopeInst %varScope, %function, %closure |
| Arguments | %varScope is the VariableScope that describes the resulting scope. %function is the IR function that the closure operand is known to refer to. %closure is the closure from which to read the scope. |
| Semantics | The instruction returns the scope stored in the given closure. |
| Effects | Does not read or write to memory. |
CreateFunctionInst
| CreateFunctionInst | _ |
|---|---|
| Description | Constructs a new function into the current scope from its code representation. |
| Example | %0 = CreateFunction %scope, %varScope, %function |
| Arguments | %function is the function that represents the code of the generated closure. %scope is the surrounding environment. %varScope is the VariableScope that describes %scope. |
| Semantics | The instruction creates a new closure that may access the lexical scope of the current function |
| Effects | Does not read or write to memory. |
CreateClassInst
| CreateClassInst | _ |
|---|---|
| Description | Constructs a new class into the current scope from its constructor code representation. |
| Example | %0 = CreateClassInst %scope, %varScope, %function, %superClass, %homeObjectOutput |
| Arguments | %function is the function that represents the code of the constructor. %scope is the surrounding environment. %varScope is the VariableScope that describes %scope. %superClass is the class to inherit from; in base classes this is an empty sentinel value. %homeObjectOutput is an out parameter which will contain the home object (.prototype) of the class. |
| Semantics | The instruction creates a new class that may access the lexical scope of the current function, and an inherit from a given super class. (ES2023 15.7.14) This results in the creation of 2 objects: the class function object itself, and the "home" object. The home object is where methods are put. The home object can be found on the .prototype of the class, and the class can be found on the .constructor of the home object. |
| Effects | Writes to stack memory. May execute JS if it's a derived class. |
BinaryOperatorInst
| BinaryOperatorInst | _ |
|---|---|
| Description | Performs the binary operation on the two operands. |
| Example | %0 = BinaryOperatorInst %x, %y |
| Arguments | %x and %y are the operands of the binary operation. |
| Semantics | The instruction follows the rules of JavaScript for each one of the binary operators defined in the instruction. |
| Effects | May read and write memory. |
DirectEvalInst
| DirectEvalInst | _ |
|---|---|
| Description | Implement direct eval. |
| Example | %0 = DirectEvalInst %%evalText, %strictCaller |
| Arguments | %evalArg is the value which will be evaluated. |
| Semantics | Implement the semantics of ES6 PerformEval(%evalText, evalRealm, strictCaller, direct=true) (ES6 18.2.1.1). |
| Effects | Unknown |
CreateThisInst
| CreateThisInst | _ |
|---|---|
| Description | Creates the object to be used as the this parameter of a construct call. |
| Example | %0 = CreateThisInst %closure, %newtarget, %functionCode |
| Arguments | %closure is the closure that will be invoked as a constructor, it may be any value, and the instruction will throw if it is not a valid callable. %newtarget is the new.target value to use for the call, it must either be the same value as %closure, or a valid callable. %functionCode is the IR function that is known to be associated with the closure operand, or EmptySentinel. |
| Semantics | The instruction is responsible for preparing the this parameter of a construct call. If %closure is a legacy class constructor, the instruction does not do anything and immediately produces undefined, since those constructors handle creating this internally. Otherwise, this the instruction determines the new.target to use for retrieving the .prototype (which may involve iterating over bound functions), reads the .prototype from it, and creates an object with that value as its parent. |
| Effects | May execute JS if %closure is not a legacy class constructor. |
CallInst
| CallInst | _ |
|---|---|
| Description | Calls another function with some arguments. |
| Example | %0 = CallInst %callee, %target, %calleeIsAlwaysClosure, %env, %newtarget %this, %arg0, %arg1, %arg2, ... |
| Arguments | %callee is the closure to execute. %target is either EmptySentinel or the only possible Function this call can invoke. %calleeIsAlwaysClosure indicates whether the callee needs to be checked for being a function. %env is the environment for the function or EmptySentinel. %newtarget is the new.target value to use for the call. %this is a reference to the 'this' value. Arguments %arg0 ... %argN are the arguments passed to the function. |
| Semantics | The instruction passes the control to the callee, that must be of closure type. The arguments are mapped to the parameters. Unmapped parameters are initialized to 'undefined'. |
| Effects | May read and write memory. |
CallBuiltinInst
| CallBuiltinInst | _ |
|---|---|
| Description | Calls a builtin function passing "undefined" for this |
| Example | %0 = CallBuiltinInst %builtinNumber, %undefined, %undefined, %arg0, %arg1, %arg2, ... |
| Arguments | %builtinNumber is the builtin to execute. Arguments %arg0 ... %argN are the arguments passed to the function. |
| Semantics | The instruction passes the control to the builtin in a VM-specific way. The arguments are mapped to the parameters. Unmapped parameters are initialized to 'undefined'. |
| Effects | May read and write memory. |
CallIntrinsicInst
| CallIntrinsicInst | _ |
|---|---|
| Description | Calls an unsafe compiler intrinsic, passing "undefined" for this |
| Example | %0 = CallIntrinsicInst %intrinsicsIndex, %undefined, %arg0, %arg1, %arg2, ... |
| Arguments | %intrinsicsIndex is the intrinsic to execute. Arguments %arg0 ... %argN are the arguments passed to the function. |
| Semantics | The instruction passes the control to the intrinsics in a VM-specific way. The arguments are mapped to the parameters. |
| Effects | May read and write memory. |
GetBuiltinClosureInst
| GetBuiltinClosureInst | _ |
|---|---|
| Description | Get a closure of a builtin function |
| Example | %0 = GetBuiltinClosureInst %builtinNumber |
| Arguments | %builtinNumber is the builtin to return the closure of. |
| Semantics | |
| Effects | Reads from memory. |
LoadPropertyInst
| LoadPropertyInst | _ |
|---|---|
| Description | Loads the value of a field from a JavaScript object. |
| Example | %0 = LoadPropertyInst %object, %property |
| Arguments | %object is the object to load from. %property is the name of the field. |
| Semantics | The instruction follows the rules of JavaScript property access in ES5.1 sec 11.2.1. The operation GetValue (ES5.1. sec 8.7.1) is then applied to the returned Reference. |
| Effects | May read and write memory or throw. |
LoadOwnPrivateFieldInst
| LoadOwnPrivateFieldInst | _ |
|---|---|
| Description | Loads the value of a private name from a JavaScript object. |
| Example | %0 = LoadOwnPrivateFieldInst %object, %property : symbol |
| Arguments | %object is the object to load from. %property is the private name symbol. |
| Semantics | The instruction honors ES2024 7.3.30 PrivateGet. It looks up a private name on the object and throws if it does not exist. However, this does not have to deal with accessors or methods because those are stored outside of the instance. |
| Effects | May execute JS. |
| TryLoadGlobalPropertyInst | _ |
|---|---|
| Description | Loads the value of an existing field from the global object or throw if it doesn't exist. |
| Example | %0 = TryLoadGlobalPropertyInst %object, %property |
| Arguments | %object is the global object. %property is the name of the field, which must be a string literal. |
| Semantics | Similar to LoadPropertyInst, but throw if the field doesn't exist. |
| Effects | May read and write memory or throw. |
LoadPropertyWithReceiverInst
| LoadPropertyWithReceiverInst | _ |
|---|---|
| Description | Loads the value of a field from a JavaScript object. |
| Example | %0 = LoadPropertyWithReceiverInst %object, %property, %receiver |
| Arguments | %object is the object to load from. %property is the name of the field. %receiver is receiver of the operation. |
| Semantics | The instruction implements ES2024 6.2.5.5 GetValue. This is used for super references. |
| Effects | May read and write memory or throw. |
DeletePropertyInst
| DeletePropertyInst | _ |
|---|---|
| Description | Deletes the value of a field from a JavaScript object. |
| Example | %0 = DeletePropertyInst %object, %property |
| Arguments | %object is the object to modify. %property is the name of the field. |
| Semantics | The instruction follows the rules of JavaScript property access. |
| Effects | May read and write memory. |
StorePropertyInst
| StorePropertyInst | _ |
|---|---|
| Description | Stores a value to field in a JavaScript object. |
| Example | %4 = StorePropertyInst %value, %object, %property |
| Arguments | %value is the value to be stored. %object is the object where the field %property will be created or modified. |
| Semantics | The instruction follows the rules of JavaScript property access in ES5.1 sec 11.2.1. The operation PutValue (ES5.1. sec 8.7.2) is then applied to the returned Reference. |
| Effects | May read and write memory or throw. |
StorePropertyWithReceiverInst
| StorePropertyWithReceiverInst | _ |
|---|---|
| Description | Stores a value to field in a JavaScript object, with a specified receiver object. |
| Example | %4 = StorePropertyWithReceiverInst %value, %object, %property, %receiver, %isStrict |
| Arguments | %value is the value to be stored. %object is the object where the field %property will be created or modified. %receiver will be the receiver operand when invoking the [[Set]] internal method. If %isStrict is true, the operation will fail if there is an incompatible property descriptor found during the attempted store.. |
| Semantics | This follows the same semantics as StorePropertyInst, except the receiver is explicitly specified. |
| Effects | May read and write memory or throw. |
TryStoreGlobalPropertyInst
| TryStoreGlobalPropertyInst | _ |
|---|---|
| Description | Attempt to store a value into an existing field of the global object and throw if it doesn't exist. |
| Example | %4 = TryStoreGlobalPropertyInst %value, %object, %property |
| Arguments | %value is the value to be stored. %object is the global object, where the field %property will be stored. %property must be a string literal. |
| Semantics | Similar to StorePropertyInst, but throw if the field doesn't exist. |
| Effects | May read and write memory or throw. |
DefineOwnPropertyInst
| DefineOwnPropertyInst | _ |
|---|---|
| Description | Define an own property of JavaScript object. Will throw if the property write was not successful (e.g. trying to store to a non-writable property.) |
| Example | %4 = DefineOwnPropertyInst %value, %object : object, %property, %enumerable : boolean |
| Arguments | %value is the value to be stored. %object must be of an object type; it's the object where the field with name %property will be created or modified. %enumerable determines whether a new property will be created as enumerable or not. |
| Semantics | Implements ES15 7.3.8 DefinePropertyOrThrow. The instruction follows the rules of JavaScript own property access. The property is created or updated in the instance of the object, regardless of whether the same property already exists earlier in the prototype chain. |
| Effects | May read and write memory. |
DefineOwnInDenseArrayInst
| DefineOwnInDenseArrayInst | _ |
|---|---|
| Description | Define an own property in a dense JavaScript array at a specific index. This is an optimized version for dense array property assignment. Used only when populating newly created array literals. |
| Example | %0 = DefineOwnInDenseArrayInst %value, %array : object, %arrayIndex : LiteralNumber |
| Arguments | %value is the value to be stored. %array is the dense array object where the property will be defined. %arrayIndex is a literal number representing the array index where the property will be stored. |
| Semantics | This instruction is used for efficient property definition. It requires that the array is dense and that the ArrayStorage underlying it has a size which is greater than the arrayIndex operand. The property is defined directly at the specified index without prototype chain traversal. |
| Effects | Writes to heap. |
StoreOwnPrivateFieldInst
| StoreOwnPrivateFieldInst | _ |
|---|---|
| Description | Store a private own property to an object. |
| Example | %4 = StoreOwnPrivateFieldInst %value, %object, %property : symbol, %enumerable : boolean |
| Arguments | %value is the value to be stored. %object should be an object, if not this throws. %property must be the value of a symbol. %enumerable must always be false. |
| Semantics | This instruction implements ES2024 7.3.31 PrivateSet. This instruction will throw if the private property does not already exist on %object. |
| Effects | May read and write memory. |
AddOwnPrivateFieldInst
| AddOwnPrivateFieldInst | _ |
|---|---|
| Description | Add a new private own field. |
| Example | %4 = AddOwnPrivateFieldInst %value, %object, %property : symbol, %enumerable : boolean |
| Arguments | %value is the value to be stored. %object must be an object. %property must be the value of a symbol. %enumerable should always be false. |
| Semantics | This instruction is used to initialize private fields on objects. As such it implements ES2024 7.3.27 PrivateFieldAdd- except this instruction will assume that the property being added does not already exist. |
| Effects | May read and write memory. |
PrivateBrandCheckInst
| PrivateBrandCheckInst | _ |
|---|---|
| Description | Check that an object has a private brand installed. |
| Example | PrivateBrandCheckInst %object, %brand : privateName |
| Arguments | %object is the object to check. %brand must be a private name representing the brand to look for. |
| Semantics | This instruction implements the brand check for private methods and accessors. It throws a TypeError if the brand is not present on the object. |
| Effects | May read memory and throw. |
DefineOwnGetterSetterInst
| DefineOwnGetterSetterInst | _ |
|---|---|
| Description | Associates a pair of getter and setter with an own field in a JavaScript object, replacing the previous value. |
| Example | %4 = DefineOwnGetterSetterInst %getter, %setter, %object, %property, %enumerable |
| Arguments | %getter is a getter accessor, or undefined. %setter is a setter accessor, or undefined. %object is the object where the field %property will be created or modified. %enumerable determines whether a new property will be created as enumerable or not. |
| Semantics | The instruction follows the rules of JavaScript property access. The property is created or updated in the instance of the object, regardless of whether the same property already exists earlier in the prototype chain. It replaces both accessors even if one or both of the parameters are undefined. |
| Effects | May read and write memory. |
AllocObjectInst
| AllocObjectInst | _ |
|---|---|
| Description | Allocates a new JavaScript object on the heap. |
| Example | %0 = AllocObjectInst %sizeHint : LiteralNumber, %parent : EmptySentinel or null or Value |
| Arguments | *%sizeHint% indicates that the object will need at least that many property slots. %parent is the optional parent to create the object with: EmptySentinel means use Object.prototype, null means no parent, or otherwise use the specified value. |
| Semantics | The instruction creates a new JavaScript object on the heap. If the parent is invalid (not EmptySenyinel, null or object), it is silently ignored. |
| Effects | Does not read or write to memory. |
AllocObjectLiteralInst
| AllocObjectLiteralInst | _ |
|---|---|
| Description | Allocates a new JavaScript object on the heap. During lowering pass it will be lowered to either an AllocObjectInst or a LIRAllocObjectFromBufferInst. |
| Example | %0 = AllocObjectLiteralInst %parent, "prop1" : string, 10 : number |
| Arguments | %parent is the parent of the new object. %prop_map is a vector of (Literal*, value*) pairs which represents the properties and their keys in the object literal. |
| Semantics | The instruction creates a new JavaScript object on the heap with an initial list of properties. |
| Effects | Does not read or write to memory. |
AllocTypedObjectInst
| AllocTypedObjectInst | _ |
|---|---|
| Description | Allocates a new typed object on the heap. During lowering pass it will be lowered to either an AllocObjectInst or a LIRAllocObjectFromBufferInst. |
| Example | %0 = AllocTypedObjectInst %parent, "prop1" : string, 10 : number |
| Arguments | %parent is the parent of the new object, and the other operands are alternating (Literal*, value*) pairs which represent the properties and their keys in the typed class. |
| Semantics | The instruction creates a new JavaScript object on the heap with an initial list of properties, which may include 'uninit' values. Properties are enumerable. |
| Effects | Does not read or write to memory. |
AllocTypedNonEnumObjectInst
| AllocTypedNonEnumObjectInst | _ |
|---|---|
| Description | Allocates a new typed object on the heap with non-enumerable properties. Used for home objects (class prototypes with methods). During lowering pass it will be lowered to a LIRAllocTypedNonEnumObjectFromBufferInst. |
| Example | %0 = AllocTypedNonEnumObjectInst %parent, "prop1" : string, 10 : number |
| Arguments | %parent is the parent of the new object, and the other operands are alternating (Literal*, value*) pairs which represent the properties and their keys in the typed class. |
| Semantics | The instruction creates a new JavaScript object on the heap with an initial list of non-enumerable properties, which may include 'uninit' values. |
| Effects | Does not read or write to memory. |
AllocArrayInst
| AllocArrayInst | _ |
|---|---|
| Description | Allocates a new JavaScript array on the heap. |
| Example | %0 = AllocArrayInst %sizeHint, %value0, %value1, ... |
| Arguments | sizeHint tells the size of the array that the VM should allocate. It must be equal or larger than the initial list of elements in this instruction. The rest of the values are all literal values as the initial elements of the array. Non-literal values or values after elision will be inserted into the array separately. |
| Semantics | The instruction creates a new JavaScript array on the heap with a hinted size and initial list of elements. |
| Effects | Does not read or write to memory. |
AllocFastArrayInst
| AllocFastArrayInst | _ |
|---|---|
| Description | Allocates a new FastArray on the heap. |
| Example | %0 = AllocFastArrayInst %capacity |
| Arguments | %capacity is a hint to the VM for how large the initial capacity should be. |
| Semantics | The instruction creates a new empty FastArray on the heap. |
| Effects | Does not read or write to memory. |
GetTemplateObjectInst
| GetTemplateObjectInst | _ |
|---|---|
| Description | Gets the object to pass to the tagged template function. |
| Example | %0 = GetTemplateObjectInst %templateObjID, %dup, %string1, ... |
| Arguments | %templateObjID is the cache key for the template. %dup indicates whether the raw strings are duplicates of the cooked strings. If %dup is true, then all %string operands are raw. Otherwise, some number of raw %string operands are followed by the same number of cooked %string operands. |
| Semantics | If cached, retrieves from the template cache. Otherwise, updates the cache with a new template object with the cooked strings, which also contains an object containing the raw strings. Returns the resultant object. |
| Effects | Does not read or write to program memory. Updates template cache. |
TypeOfInst
| TypeOfInst | _ |
|---|---|
| Description | The JS typeof operator |
| Example | %0 = TypeOfInst %val |
| Arguments | %val is the value whose type we want to obtain. |
| Semantics | Obtains a string representing the type of the operand. |
| Effects | Does not read or write to memory. |
TypeOfIsInst
| TypeOfIsInst | _ |
|---|---|
| Description | Compare the type of a value against known types |
| Example | %0 = TypeOfIsInst %val, %types |
| Arguments | %val is the value whose type we want to compare, %types is the TypeOfIsTypes (see Typeof.h) to compare against. |
| Semantics | Returns true if the type of %val matches the bit in the TypeOfIsTypes, false otherwise. |
| Effects | Does not read or write to memory. |
CreateArgumentsInst
| CreateArgumentsInst | _ |
|---|---|
| Description | Allocates the JavaScript arguments array-like object on the heap. |
| Example | %0 = CreateArgumentsInst |
| Arguments | None. |
| Semantics | The instruction creates the arguments object, populates it with copies of the values of the arguments (according to "strict mode" semantics) and sets arguments.length to the number of arguments (this isn't copied or counted). There should be only one CreateArgumentsInst in a function. |
| Effects | Does not read or write to memory. |
CreateRegExpInst
| CreateRegExpInst | _ |
|---|---|
| Description | Construct a RegExp object from a regexp literal. |
| Example | %0 = CreateRegExpInst "pattern", "flags" |
| Arguments | pattern: LiteralString and flags: LiteralString |
| Semantics | It is equivalent to calling RegExp(pattern, flags), except that it calls the built-in constructor, even if RegExp has been overridden. |
| Effects | Does not read or write to memory. |
SwitchInst
| SwitchInst | _ |
|---|---|
| Description | The ‘switch‘ instruction is used to transfer control to one of different places. |
| Example | %0 = SwitchInst %input, %default, [%val0, %block0], [%val1, %block1] .. |
| Arguments | The instruction accepts an input, a default block, and one or more pairs of value-destination values. The value must be a primitive JS type, and the destination must be a basic block within the current function. |
| Semantics | The semantic of the instruction is identical to a sequence of 'if' statements that compare the value of the input to each of the case statements. Repeating the same value is not allowed. |
| Effects | May read and write memory. |
GetPNamesInst
| GetPNamesInt | _ |
|---|---|
| Description | Generates the property enumerator, which is a collection of registers that hold the state of the enumerator (iterator, object base, index, size, etc). |
| Example | %0 = GetPNamesInt %propertyAddr, %baseAddr, %indexAddr, %sizeAddr, %iteratorAddr, %onEmpty, %onLast |
| Arguments | The first 5 parameters are addresses (stack allocated addresses) that represent the state of the property enumerator. The last two argument are jump destination for the two cases: empty object and object with some properties. |
| Semantics | This instruction is a terminator instruction and prepares the enumerator for the GetNextPNameInst instruction to consume. |
| Effects | May read and write memory. |
GetNextPNameInst
| GetNextPNameInst | _ |
|---|---|
| Description | Loads the next property from the object property enumerator. |
| Example | %0 = GetNextPNameInst %propertyAddr, %baseAddr, %indexAddr, %sizeAddr, %iteratorAddr, %onLast, %onSome |
| Arguments | The first argument is the destination where the name of the property is written into. The next 4 arguments are the state of the property enumerator. The last two arguments are the destination blocks for: no next property, or some property available. |
| Semantics | This instruction is a terminator instruction that uses the state that was prepared by the GetPNamesInst instruction. |
| Effects | May read and write memory. |
CatchInst
| CatchInst | _ |
|---|---|
| Description | This instruction catches an exception, and returns that exception. |
| Example | %0 = CatchInst |
| Arguments | This instruction does not have arguments. |
| Semantics | This instruction will be generated for each catch block and for each finally block. The current exception will be returned. CatchInst can only show up at the beginning of a basic block. The coverage and depth information for the CatchInst will be constructed dynamically later during bytecode generation. |
| Effects | May read and write memory. |
ThrowInst
| ThrowInst | _ |
|---|---|
| Description | This instruction will throw an exception. |
| Example | %0 = ThrowInst %e, %catchTarget |
| Arguments | This instruction takes one required operand, which is the register that contains the exception value. The second operand is optional and indicates the closest surrounding catch block; it must be present if there is one, and must be missing otherwise. |
| Semantics | This instruction is a terminator instruction that will transition the control to the CatchInst that covers this instruction with closest scope. |
| Effects | May read and write memory. |
ThrowTypeErrorInst
| ThrowTypeErrorInst | _ |
|---|---|
| Description | This instruction will create and throw a TypeError. |
| Example | %0 = ThrowTypeErrorInst %message |
| Arguments | This instruction takes one argument, %message, which will be converted to a string and used as the message for the TypeError. |
| Semantics | This instruction is a terminator instruction that will transition the control to the CatchInst that covers this instruction with closest scope. |
| Effects | Will throw. |
TryStartInst
| TryStartInst | _ |
|---|---|
| Description | Mark the beginning of the try blocks. |
| Example | %0 = TryStartInst %catchTargetBlock, %tryBodyBlock |
| Arguments | This instruction takes 2 arguments: %tryBodyBlock is the block where the body of Try starts, %catchTargetBlock is the basic block that contains the CatchInst which covers this try. Both %tryBodyBlock and %catchTargetBlock are successors of this instruction. |
| Semantics | This is a nop, used only for tracking the beginning of try blocks. |
| Effects | Does not read or write memory. |
TryEndInst
| TryEndInst | _ |
|---|---|
| Description | A terminator that marks the end of the try blocks. |
| Example | %0 = TryEndInst %CatchBlock, %BranchDest |
| Arguments | The active catch block and the destination block to branch to. |
| Semantics | This is a nop, used only for tracking the end of try blocks. |
| Effects | Technically this instruction itself does not touch memory, however we mark it as may write to prevent optimizations going pass this instruction. |
BranchIfBuiltinInst
| BranchIfBuiltinInst | _ |
|---|---|
| Description | Check if a value is the same as a known builtin value. |
| Example | %0 = BranchIfBuiltin %builtinNumber, %arg, %trueDest, %falseDest |
| Arguments | The builtin number, the value to check, and the blocks to branch to. |
| Semantics | Branch to %trueDest if %arg is the same pointer as the builtin with %builtinNumber, else branch to %falseDest. |
| Effects | None |
PhiInst
| PhiInst | _ |
|---|---|
| Description | This is a Phi node instruction. |
| Example | %0 = PhiInst %value0, %block0, [%value1, %block1] |
| Arguments | A list of pairs of value and incoming basic block. |
| Semantics | The PhiNode needs to have a single entry for each incoming basic block of the block the PHI is located in. The incoming value must dominate the last instruction in the incoming block. |
| Effects | Does not read or write memory. |
MovInst
| MovInst | _ |
|---|---|
| Description | The MOV inst represents a low-level operation of moving one register to another. |
| Example | %0 = MovInst %value0 |
| Arguments | Any value. |
| Semantics | The Mov instruction is only valid after Register Allocation in bytecode as we move away from SSA form. |
| Effects | Does not read or write memory. |
ImplicitMovInst
| ImplicitMovInst | _ |
|---|---|
| Description | The ImplicitMov inst represents moving one register to another, except the mov will be performed implicitly by an immediately-subsequent instruction. This is used to express to the optimizer instructions which modify registers other than their destination. |
| Example | %0 = ImplicitMovInst %value0 |
| Arguments | Any value. |
| Semantics | The ImplicitMov instruction is only valid after Register Allocation in bytecode as we move away from SSA form. |
| Effects | Does not read or write memory. |
DebuggerInst
| DebuggerInst | _ |
|---|---|
| Description | This instruction corresponds to the JavaScript debugger statement. |
| Example | %0 = DebuggerInst |
| Arguments | It takes no arguments and returns no values. |
| Semantics | Its behavior is implementation-dependent. |
| Effects | Does not read or write to memory. |
GetNewTargetInst
| GetNewTargetInst | _ |
|---|---|
| Description | Obtains the value of new.target in the current function or constructor. |
| Example | %0 = GetNewTargetInst, %param |
| Arguments | %param is a dummy JSDynamicParam used to quickly find usages of new.target. |
| Semantics | It must only be called from a ES6 class constructor or ES5 function. If the callee was invoked from new, it returns the function object of the direct constructor, otherwise undefined. |
| Effects | Does not read or write memory |
ThrowIfInst
| ThrowIfInst | _ |
|---|---|
| Description | Check whether the value belongs to one of the "rejected" types, and if it is, throw ReferenceError, otherwise return it. |
| Example | %_ = ThrowIfInst %value, %rejectedTypesUnion |
| Arguments | %value is the value to check. %rejectedTypesUnion is a union of types that should be rejected. |
| Semantics | It is used to implement ES6 TDZ functionality. Variables declared with let are poisoned with empty until they are initialized. |
| Effects | Potentially throws an exception. Has no other side effects. |
ThrowIfThisInitializedInst
| ThrowIfThisInitializedInst | _ |
|---|---|
| Description | Check whether the value of this in a derived class constructor is already initialized. If so, throw. |
| Example | %_ = ThrowIfThisInitializedInst %thisVal |
| Arguments | %thisVal is the value to check. |
| Semantics | This is used to guard against double-initialization of this, which happens by invoking super() multiple times. |
| Effects | Potentially throws an exception. Has no other side effects. |
CoerceThisNS
| CoerceThisNS | _ |
|---|---|
| Description | Coerces its argument using the rules of "this" coercion to object in non-strict mode. |
| Example | %0 = CoerceThisNS %value0 |
| Arguments | Any value. |
| Semantics | |
| Effects | Does not read or write memory (it potentially creates a new object) |
CreateGenerator
| CreateGenerator | _ |
|---|---|
| Description | Constructs a new GeneratorInnerFunction from its code representation, and wraps it in a Generator object. |
| Example | %0 = CreateFunction %scope, %varScope, %function |
| Arguments | %function is the function that represents the code of the generator's inner function. %scope is the surrounding environment. %varScope is the VariableScope that describes %scope. |
| Semantics | Creates a new GeneratorInnerFunction closure that may access the environment and wraps it in a generator |
| Effects | Does not read or write to memory (creates a new object). |
SaveAndYield
| SaveAndYield | _ |
|---|---|
| Description | Saves information needed to resume generator execution and yield. |
| Example | %0 = SaveAndYield %value, %isDelegated, %next |
| Arguments | %value is the value to yield, %isDelegated determines if the value to be yielded should be wrapped (true when inside a yield*), %next is the next BasicBlock to execute upon resuming, which must begin with a ResumeGeneratorInst (generated alongside SaveAndYield). |
| Semantics | Saves the frame variables and the next IP to the closure, and yield execution. |
| Effects | Reads and writes to memory, may throw or execute. |
ResumeGenerator
| ResumeGenerator | _ |
|---|---|
| Description | Perform the user-requested action on resuming a generator. |
| Example | %0 = ResumeGenerator %isReturn |
| Arguments | %isReturn is an output argument set to true if the user requested a return, false otherwise. |
| Semantics | If the user requested next(), continue on. If the user requested throw(), throw. If the user requested return(), set %isReturn to true and continue. Subsequent instructions will check %isReturn and execute any finally handlers, for example, before returning. |
| Effects | May read and write memory. (may throw) |
IteratorBegin
| IteratorBegin | _ |
|---|---|
| Description | Begins array destructuring on a given iterable source. |
| Example | %0 = IteratorBegin %sourceOrNext |
| Arguments | %sourceOrNext[in/out] is the stack location for source to destructure from. Is set to source if performing array iteration, else set to the .next() method of the iterator. |
| Semantics | If %sourceOrNext is an Array then it remains unmodified and the instruction returns 0, but if it is not, it is replaced with the 'next' method so that it can be called on each step of the iteration and the instruction returns the iterator object. If the [Symbol.iterator] function throws, this instruction will throw. |
| Effects | May read and write memory, may throw or execute. |
IteratorNext
| IteratorNext | _ |
|---|---|
| Description | Destructures the next value from a given iterator. |
| Example | %0 = IteratorNext %iterator %sourceOrNext |
| Arguments | %iterator is the index or the iterator. %sourceOrNext is the input stack location (source to destructure from) or the next method. |
| Semantics | If %iterator is an index: if %iterator is less than %sourceOrNext.length, reads the value from %sourceOrNext and increments the index, else sets %iterator to undefined and returns undefined. If %iterator is an actual iterator, calls %sourceOrNext as a next method and evaluates to the result value. When iteration is complete, sets %iterator to undefined as a signal that we're done. |
if (typeof %iterator === 'undefined') {
return undefined;
}
if (typeof %iterator === 'number') {
if (%iterator >= %sourceOrNext.length) {
%iterator = undefined;
return undefined;
}
return %sourceOrNext[%iterator];
}
var iterResult = %sourceOrNext();
if (iterResult.done) {
%iterator = undefined;
return undefined;
}
return iterResult.value;Effects | May read and write memory, may throw or execute.
IteratorClose
| IteratorClose | _ |
|---|---|
| Description | Closes an iterator if it exists. |
| Example | %0 = IteratorClose %iterator %ignoreInnerException |
| Arguments | %iterator is the index or the iterator. %ignoreInnerException is a boolean literal. |
| Semantics | If %iterator is an iterator, calls .return() on it to close it. Otherwise, this is a no-op. If .return() throws, the exception is ignored when %ignoreInnerException is true. |
| Effects | May read and write memory, may throw or execute. |
UnreachableInst
| UnreachableInst | _ |
|---|---|
| Description | Crashes the VM (ifndef NDEBUG). |
| Example | %0 = UnreachableInst |
| Arguments | None. |
| Semantics | Can be added to stubs and similar to verify that they are never executed. |
| Effects | Marked as reading/writing memory to avoid reordering. |
Target Instructions
Some high-level IR instructions are lowered into a sequence of low-level machine
instructions. In order to perform register allocation on these instructions we
need to perform lowering, which is a form of instruction selection. The semantic
of these instructions are identical to the semantic of the relevant target
instructions.
LIRGetGlobalObjectInst
| LIRGetGlobalObjectInst | _ |
|---|---|
| Description | Obtain the "global" object |
| Example | %0 = LIRGetGlobalObjectInst |
| Arguments | None. |
| Semantics | The instruction returns a reference to the "global" object. |
| Effects | Does not read or write to memory. |
HBCCreateFunctionEnvironmentInst
| HBCCreateFunctionEnvironmentInst | _ |
|---|---|
| Description | Create a new environment with the function's parent environment as its parent. |
| Example | %0 = HBCCreateFunctionEnvironmentInst %varScope, %parentScopeParam |
| Arguments | %varScope is the variable scope that this instruction will produce. %parentScopeParam is dummy parameter used to model usage of the parent environment. |
| Semantics | The instruction creates a new environment for a function. |
| Effects | Does not read or write to memory. |
HBCResolveParentEnvironmentInst
| HBCResolveParentEnvironmentInst | _ |
|---|---|
| Description | Traverse the chain of environments starting at the current function's parent to find a given environment. |
| Example | %0 = HBCResolveParentEnvironmentInst %varScope, %numLevels, %parentScopeParam |
| Arguments | %varScope is the variable scope to resolve to. %numLevels is the number of scopes up from the current function's parent that the result will be found. %parentScopeParam is dummy parameter used to model usage of the parent environment. |
| Semantics | The instruction resolves an environment that is a parent of the current function's environment. |
| Effects | Does not read or write to memory. |
LIRAllocObjectFromBufferInst
| LIRAllocObjectFromBufferInst | _ |
|---|---|
| Description | Allocates a new JavaScript object on the heap, and initializes it with values from the object buffer. |
| Example | %0 = LIRAllocObjectFromBufferInst %parent, %value0, %value1, ... |
| Arguments | %parent is the parent of the new object. The subsequent values are all literal values, with alternating keys and values. Non-serializable values will be inserted into the array separately. |
| Semantics | The instruction creates a new JavaScript object on the heap with an initial list of properties. |
| Effects | Does not read or write to memory. |
LIRAllocTypedObjectFromBufferInst
| LIRAllocTypedObjectFromBufferInst | _ |
|---|---|
| Description | Allocates a new typed class instance on the heap, and initializes it with values from the buffer. Properties are enumerable. |
| Example | %0 = LIRAllocTypedObjectFromBufferInst %parent, %key0, %value0, ... |
| Arguments | The %parent is the parent of the result object. The remaining operands are all literal values, with alternating keys and values. Non-literal values will be inserted into the array separately. |
| Semantics | The instruction creates a new JavaScript typed object on the heap with an initial list of enumerable properties. |
| Effects | Does not read or write to memory. |
LIRAllocTypedNonEnumObjectFromBufferInst
| LIRAllocTypedNonEnumObjectFromBufferInst | _ |
|---|---|
| Description | Allocates a new typed class instance on the heap with non-enumerable properties, and initializes it with values from the buffer. Used for home objects (class prototypes). |
| Example | %0 = LIRAllocTypedNonEnumObjectFromBufferInst %parent, %key0, %value0, ... |
| Arguments | The %parent is the parent of the result object. The remaining operands are all literal values, with alternating keys and values. Non-literal values will be inserted into the array separately. |
| Semantics | The instruction creates a new JavaScript typed object on the heap with an initial list of non-enumerable properties. |
| Effects | Does not read or write to memory. |
HBCCallNInst
| HBCCallNInst | _ |
|---|---|
| Description | Calls a function with a fixed number of arguments (from 1 to 4, inclusive). |
| Example | %0 = HBCCallNInst %callee, %this, %arg0, %arg1, %arg2 |
| Arguments | %callee is the function to execute. %this is a reference to the 'this' value. Arguments %arg0 ... %argN are the arguments passed to the function. |
| Semantics | The instruction copies its arguments (starting from this) into the parameter-passing registers at the end of the frame, and passes the control to the callee, which must be of closure type. The arguments are mapped to the parameters. Unmapped parameters are initialized to 'undefined'. |
| Effects | May read and write memory. |
HBCCallWithArgCountInst
| HBCCallWithArgCountInst | _ |
|---|---|
| Description | This instruction contains the same operands as CallInst, in addition to explicitly passing in the argument count as an operand. |
| Example | %0 = HBCCallWithArgCountInst %callee, %target, %calleeIsAlwaysClosure, %env, %newtarget, $argcount, %this, %arg0, %arg1, %arg2, ... |
| Arguments | %argcount is the number of arguments to the function, including 'this'. |
| Semantics | The instruction passes the control to the callee, that must be of closure type. The arguments are mapped to the parameters. Unmapped parameters are initialized to 'undefined'. |
| Effects | May read and write memory. |
prload
| prload | _ |
|---|---|
| Description | Load a typed object property by index |
| Example | %0 = prload (:type) %object, %propIndex, %propName |
| Arguments | %object is the object to load from. %propIndex is the property index. %propName is the property name (which isn't actually used). type is the type of the property. |
| Semantics | Load the property without any checking. |
| Effects | May read memory. |
prstore
| prstore | _ |
|---|---|
| Description | Store a typed object property by index |
| Example | prstore %value, %object, %propIndex, %propName, %nonPointer |
| Arguments | %value is the value to store. %object is the object to store into. %propIndex is the property index. %propName is the property name (which isn't actually used). %nonPointer is a boolean indicating whether the property is a non-pointer type (meaning that both the old and the new value are not pointers). |
| Semantics | Store the property without any checking. |
| Effects | May read memory. |
FastArrayLoad
| FastArrayLoad | _ |
|---|---|
| Description | Loads an element from a FastArray, validates the index. |
| Example | FastArrayLoad %array (:object), %index (:number) |
| Arguments | %array is the array, %index is the index to read |
| Semantics | Throw if the index is not an unsigned integer within range, otherwise, load the value at %index from %array. |
| Effects | May read memory or throw. |
FastArrayStore
FastArrayStore
--- | --- |
Description | Stores an element to an array, validates the index.
Example | FastArrayStore %storedValue, %array, %index
Arguments | %array is the array, %index is the index to write to, %storedValue is the value to write
Semantics | Throw if the index is not an unsigned integer within range, otherwise, store %storedValue to %array.
Effects | May write memory or throw.
FastArrayLength
| FastArrayLength | _ |
|---|---|
| Description | Read the length of a FastArray |
| Example | FastArrayLength (:number) %array |
| Arguments | %array is the array |
| Semantics | Read the length property of the array and return it. |
| Effects | May read memory. |
FastArrayPushInst
| FastArrayPushInst | _ |
|---|---|
| Description | Pushes an element onto a FastArray. |
| Example | FastArrayPushInst %pushedValue, %array |
| Arguments | %pushedValue is the value to be pushed, %array is the array we are pushing onto. |
| Semantics | Push an element onto the target array, increasing its length by 1. If accommodating the additional element causes a reallocation past the maximum allowable allocation size, throw an exception. |
| Effects | May write memory or throw. |
FastArrayAppendInst
| FastArrayAppendInst | _ |
|---|---|
| Description | Appends the elements of one FastArray to another. |
| Example | FastArrayAppendInst %other, %array |
| Arguments | %other is array from which elements will be copied, %array is the array onto which elements will be appended. |
| Semantics | Copy the elements from %other into the end of %array, increasing its length by the number of elements in %other. If accommodating the additional elements causes a reallocation past the maximum allowable allocation size, throw an exception. |
| Effects | May write memory or throw. |
LoadParentNoTraps
| LoadParentNoTraps | _ |
|---|---|
| Description | Loads the parent for a legacy, ordinary untyped object. The given object cannot be proxy. Returns null or an object. |
| Example | %0 = LoadParentNoTraps %object |
| Arguments | %object must be of type object. |
| Semantics | Read the parent. |
| Effects | May read memory. |
TypedLoadParent
| TypedLoadParent | _ |
|---|---|
| Description | Loads the parent (the vtable) for a typed object instance of a class |
| Example | %0 = TypedLoadParent %object |
| Arguments | %object is an instance of a typed class |
| Semantics | Read the parent without any checks. |
| Effects | May read memory. |
FUnaryMath
| FUnaryMath | _ |
|---|---|
| Description | Instruction class for a floating point unary math operation. |
| Example | FNegate (:number), %operand (:number) |
| Arguments | %operand is the input value |
| Semantics | Perform the specified op on a floating point number, return a number. |
| Effects | None |
FBinaryMath
| FBinaryMath | _ |
|---|---|
| Description | Instruction class for a floating point binary math operation. |
| Example | FAdd (:number), %left (:number), %right (:number) |
| Arguments | %left and %right are the input values |
| Semantics | Perform the specified op on two floating point numbers (same type), return a number. |
| Effects | None |
FCompare
| FCompare | _ |
|---|---|
| Description | Instruction class for comparing floating point numbers |
| Example | FLessThan (:bool), %left (:number), %right (:number) |
| Arguments | %left and %right are the input values |
| Semantics | Perform the specified compare on two floating point numbers (same type), return bool. Note that if any operand is NaN, comparison always returns false. |
| Effects | None |
StringConcat
| StringConcat | _ |
|---|---|
| Description | Concatenate N strings and return the resultant string. |
| Example | StringConcat (:string), %op1 (:string), ..., %opN (:string) |
| Arguments | %op1 to %opN are the input values which must be strings. |
| Semantics | Perform the specified concatenation on strings. |
| Effects | None |
HBCStringConcat
| HBCStringConcat | _ |
|---|---|
| Description | Concatenate 2 strings and return the resultant string. |
| Example | StringConcat (:string), %left (:string), %right (:string) |
| Arguments | %left and %right are the input values which must be strings. |
| Semantics | Perform the specified concatenation on strings. |
| Effects | None |
UnionNarrowTrusted
| UnionNarrowTrusted | _ |
|---|---|
| Description | Narrow a union type when the compiler has proven that the operand is more specific. |
| Example | UnionNarrowTrusted (:number), %operand (:number | empty) |
| Arguments | %operand is the input value |
| Semantics | Narrow the type, but doesn't change the value. |
| Effects | None |
CheckedTypeCast
| CheckedTypeCast | _ |
|---|---|
| Description | Attempt to cast to the result type, throw if unable. |
| Example | CheckedTypeCast (:U), %operand (:T), %type |
| Arguments | %operand is the value to cast, %type is the type to cast to |
| Semantics | Cast from type T to U, throw when the cast is not valid. The result type may be narrower than the |
| specified cast type, if the compiler can prove that the input value is more specific. | |
| Effects | May throw. |
NativeCall
| NativeCall | _ |
|---|---|
| Description | Call a native function. |
| Example | NativeCall (:type) %nativeFunctionPtr, %nativeSignature, %arg1, %arg2, ... |
| Arguments | %nativeFunctionPtr is the pointer to the native function. %nativeSignature is the signature of the native function. Arguments %arg1 ... %argN are the arguments passed to the function encoded as JS values. |
| Semantics | The JS arguments are converted to native types. The result is converted to the JS type. |
| Effects | Unknown. |
GetNativeRuntime
| GetNativeRuntime | _ |
|---|---|
| Description | Get a native pointer to the native runtime to be passed to a native function. |
| Example | GetNativeRuntime (:number) |
| Arguments | None |
| Semantics | Get the native runtime pointer. |
| Effects | None |
LIRDeadValue
| LIRDeadValue | _ |
|---|---|
| Description | Create a "dead value" of the specified type. This instruction is created during lowering in code that will never execute, but is needed to satisfy the type constraints of downstream instruction that will also never execute. |
| Example | LIRDeadValue (:number) |
| Arguments | None |
| Semantics | Create a value of the specified type. |
| Effects | None |
LazyCompilationData
NOTE: LazyCompilationData relies on the fact that we don't delete Variables during the lazy compilation pipeline. That means no stack promotion, and the existing full optimization pipeline cannot run, because we haven't yet figured out which variables are captured by child functions.
| LazyCompilationData | _ |
|---|---|
| Description | Data needed for lazy compilation, including the VariableScope chain used to access captured variables. |
| Example | LazyCompilationData %capturedThis, %capturedNewTarget, %capturedArguments, %capturedHomeObject, %parentVS, %parentParentVS, ... |
| Arguments | The captured values are used in case the lazy function may have arrow functions as children which need to capture the values as variables. In the case of %capturedHomeObject, this is needed even for non-arrow functions. %parentVS the immediately enclosing VariableScope. The remaining operands are successively the ancestors of %parentVS, which are kept as operands to ensure the VariableScopes aren't deleted across lazy compilation calls. |
| Semantics | Information needed for lazy compilation. Deleted after use. |
| Effects | None |
EvalCompilationData
NOTE: EvalCompilationData relies on the fact that we don't delete Variables during the eval compilation pipeline. That means no stack promotion, and the existing full optimization pipeline cannot run, because we haven't yet figured out which variables are captured by child functions.
| EvalCompilationData | _ |
|---|---|
| Description | Data needed for eval compilation within this function, including the VariableScope chain used to access captured variables. |
| Example | EvalCompilationData %capturedThis, %capturedNewTarget, %capturedArguments, %VS, %parentVS, ... |
| Arguments | The captured values are used in case the eval function may have arrow functions as children which need to capture the values as variables. %VS the VariableScope for the function (block scoping not supported). The remaining operands are successively the ancestors of %VS, which are kept as operands to ensure the VariableScopes aren't deleted across compilation calls. |
| Semantics | Information needed for eval compilation. |
| Effects | None |
Doc IntlAPIs (`doc/IntlAPIs.md`)
id: intl title: Internationalization APIs
This document describes the current state of the Android and iOS implementation of the ECMAScript Internationalization API Specification (ECMA-402, or Intl). ECMA-402 is still evolving and the latest iteration that was taken into account is 7th edition which was published in June 2020. Each new edition is built on top of the last one and adds new capabilities typically as,
- New
Intlservice constructors (e.g.Intl.Collator,Intl.NumberFormatetc.) or extending existing ones by accepting more parameters - New functions or properties in
Intlobjects (e.g.Intl.Collator.prototype.compare) - New locale aware functions in standard Javascript object prototypes (e.g.
String.prototype.localeCompare)
One popular implementation strategy followed by other engines, is to bundle an internationalization framework (typically ICU) along with the application package. This guarantees deterministic behaviours at the cost of applications package bloat. We decided to consume the Android and iOS platform provided facilities for space efficiency, but at the cost of some variance in behaviours across Android and iOS platforms. This also includes behavioural variations between different versions of Android.
ECMA-402 Compliance
Supported on both platforms
Intl.CollatorIntl.Collator.supportedLocalesOfIntl.Collator.prototype.compareIntl.Collator.prototype.resolvedOptions
Intl.NumberFormat*Intl.NumberFormat.supportedLocalesOfIntl.NumberFormat.prototype.formatIntl.NumberFormat.prototype.resolvedOptions
Intl.DateTimeFormat*Intl.DateTimeFormat.supportedLocalesOfIntl.DateTimeFormat.prototype.formatIntl.DateTimeFormat.prototype.resolvedOptions
Intl.getCanonicalLocalesString.prototypelocaleComparetoLocaleLowerCasetoLocaleUpperCase
Array.prototypetoLocaleString
Number.prototypetoLocaleString
Date.prototypetoLocaleStringtoLocaleDateStringtoLocaleTimeString
Supported on Android only
Intl.NumberFormatIntl.NumberFormat.prototype.formatToParts
* Limitations on property support
Limited iOS property support
Intl.NumberFormatimplementation does not support the following properties,notation: 'compact'notation: 'engineering'compactDisplaysignDisplay
Intl.DateTimeFormatimplementation does not support the following properties,numberingSystemformatMatcher
Limited Android property support
Intl.DateTimeFormatimplementation does not support the following properties,dayPeriodfractionalSecondDigitsformatMatcherThe property enables the implementation to pick the best display format when it supports only a subset of all possible formats. ICU library in Android platform and hence our implementation allows all subsets and formats which makes thisformatMatcherproperty unnecessary.
Limitations across Android SDKs
Android 11
- The keys of the object returned by
resolvedOptionsfunction in allIntlservices are not deterministically ordered as prescribed by spec. - DateFormat: ECMAScript beginning of time (-8,640,000,000,000,000), is formatted as
November 271817, instead of expectedApril 271822. Intl.NumberFormatimplementation has some rough edges in supporting the following properties,style: 'unit'notation: 'compact'signDisplaycurrencyFormat
Android 10 and older (SDK < 30)
Intl.NumberFormat: Scientific notation formatting has issues on some cases. e.g.-Infinitymay get formatted as '-∞E0' instead of expected '-∞'. Another manifestation of the issues is that the formatToParts may return 4 parts instead of 2.Intl.NumberFormat: Compact notationformatToPartsdoesn't identify unit, hence we report unit as 'literal'. For e.g. the second part of "100ac" gets reported as "literal" instead of "compact"
Android 9 and older (SDK < 29)
- There are some failures likely due to older Unicode and CLDR version, which are hard to generalize. Some examples are,
Intl.NumberFormat: 'Percent' is not accepted as a unit.Intl.NumberFormat: unit symbols difference, kph vs km/h- Some issue in significant digit precision, which is not yet looked into the details.
Android 8.0 – 8.1 and older (SDK < 28)
Intl.getCanonicalLocales: Unicode/CLDR version differences results in some variances. e.g. und-u-tz-utc vs. und-u-tz-gmt.Intl.NumberFormat: CompactFormatter doesn't respect the precision inputs.
Android 7.0 - 7.1 and older (SDK < 26)
Intl.getCanonicalLocales: Unicode/CLDR version differences results in some variances. e.g. und-u-ms-imperial vs. und-u-ms-uksystem.
Android 7.0 - 7.1 and older (SDK < 24)
Intl.Collator: Doesn't canonically decompose the input strings. Canonically equivalent string with non-identical code points may not match.Intl.getCanonicalLocales: Unicode/CLDR version differences results in some variances. e.g. und-u-ca-ethiopic-amete-alem vs. und-u-ca-ethioaa, und-u-ks-primary vs. und-u-ks-level1.Intl.NumberFormat: Unit style does not work.Intl.NumberFormat: There are issues in the precision configuration due to lack of APIs.Intl.DateFormat: There are issues with the calendar configuration which needs to be dug into.dateStyle/timeStyleis not implemented.
SDK < 21 and older
On platforms before 21, Locale.forLanguageTag() is not available, hence we can't construct java.util.Locale object from locale tag. Hence, we fallback to English for any locale input.
Internationalization framework in Android Platform
Our implementation is essentially projecting the Android platform provided internationalization facilities through the ECMA-402 specified services. It implies that the results of running the same code may vary between devices running different versions of Android.
Android platform internationalization libraries have been based on ICU4j project. Version of ICU4j and the backing CLDR data varies across Android platform versions. Also, the ICU APIs were never exposed directly, but only through wrappers and aliases. This results in significant variance in internationalization API surface and data across platform versions.
The following table summarizes ICU, CLDR and Unicode versions available on the Android platforms.
Platform 24+ where ICU4j APIs are available.
| Android Platform Version | ICU | Unicode | CLDR |
|---|---|---|---|
| Android 11 (API level 30) | ICU4J 66.1 (ref) | Unicode 13 beta | CLDR 36.1 |
| Android 10 (API level 29) | ICU4j 63.2 (ref) | CLDR 34 | Unicode 11.0 |
| Android 9 (API level 28) | ICU4j 60.2 (ref) | CLDR 32.0.1 | Unicode 10.0 |
| Android 8.0 - 8.1 (API levels 26 - 27) | ICU4j 58.2( ref) | CLDR 30.0.3 | Unicode 9.0 |
| Android 7.0 - 7.1 (API levels 24 - 25) | ICU4j 56 (ref) | CLDR 28 | Unicode 8.0 |
Pre-24 platforms
| Android Platform Version | ICU | Unicode | CLDR |
|---|---|---|---|
| Android 6.0 (API level 23) | ICU4j 55.1 (ref) | CLDR 27.0.1 | Unicode 7.0 |
| Android 5.0 (API levels 21–22) | ICU4j 53 (ref) | CLDR 25 | Unicode 6.3 |
| Android 4.4 (API levels 19–20) | ICU4j 51 (ref) | CLDR 23 | Unicode 6.2 |
| Android 4.3 (API level 18) | ICU4j 50 (ref) | CLDR 22.1 | Unicode 6.2 |
| Android 4.1 (API levels 16–17) | ICU4j 4.8 (ref) | CLDR 2.0 | Unicode 6.0 |
In summary,
Platforms >= 24 have much better internationalization support than earlier, as many ICU classes are available as is.
Platforms 21-24 still has reasonable internationalization support, by allowing creation of Locale objects and enabling selected ICU services through java.text namespace.
Platforms < 21 doesn't allow creation of Locale objects from tags, severely limiting general purpose international code.
Platform 30 has introduced classes under
android.icu.numbernamespace which will majorly improve ourIntl.NumberFormatimplementation
Impact on Android Application Size
The following numbers are measured using a test application which takes dependency on the Hermes library to evaluate a JavaScript snippet. Essentially, enabling Intl APIs adds 57-62K per ABI.
| Product APK Size | NOINTL | INTL | DIFF | PERC |
|---|---|---|---|---|
| ARM64 | 1,672,235 | 1,729,579 | 57,344 | 3.43% |
| ARM | 1,471,539 | 1,528,883 | 57,344 | 3.90% |
| X86_64 | 1,844,255 | 1,901,599 | 57,344 | 3.11% |
| X86 | 1,950,739 | 2,012,179 | 61,440 | 3.15% |
The overhead is contributed by both compiled native C++ and Java bits
The uncompressed size of the Hermes shared library got bigger as follows,
| libhermes.so Size | NOINTL | INTL | DIFF | PERC |
|---|---|---|---|---|
| ARM64 | 2,473,760 | 2,551,592 | 77,832 | 3.15% |
| ARM | 1,696,672 | 1,754,016 | 57,344 | 3.38% |
| X86_64 | 2,633,528 | 2,711,368 | 77,840 | 2.96% |
| X86 | 2,859,916 | 2,945,936 | 86,020 | 3.01% |
And the Java bits got bigger as well,
| Java Size | NOINTL | INTL | DIFF | PERC |
|---|---|---|---|---|
| classes.jar(in hermes.aar) | 559 | 120975 | 120,416 | 21541.32% |
| classes.dex(replapp.apk) | 160708 | 234808 | 74,100 | 46.11% |
Please note that the application dex file contains non-hermes class files too.
And finally, this is the increase in the final npm package,
| NPM Package | NOINTL | INTL | DIFF | PERC |
|---|---|---|---|---|
| hermes | 214447973 | 219291220 | 4,843,247 | 2.26% |
Doc LazyEvalCompilation (`doc/LazyEvalCompilation.md`)
id: lazy-eval-compilation title: Lazy/Eval Compilation
This is a high level overview of how we handle compiling code while the VM is executing, using existing Environments. Used for lazy compilation (deferring full compilation of functions until they are executed) and debugger local eval (used by the debugger REPL). For more detailed explanations about each of the parts, look at comments in the code for each of the compiler pipeline phases.
The concepts of lazy and eval compilation have conceptual overlap, but use of one doesn't technically require use of the other. It's possible to run lazy compilation but disallow debugger local eval and vice versa. The code supplied to debugger local eval may also be lazily compiled, but it doesn't have to be.
NOTE: All of the below currently only works when executing from source, as it relies on BCProviderFromSrc. Lazy compilation doesn't make sense when running from an HBC file (everything's already compiled) and debugging HBC hasn't been implemented yet. We also rely on optimizations not running, to avoid deleting variables/promoting them to the stack, because we rely on everything being in Environments at runtime.
Parsing
The parser has 3 phases when running lazy compilation:
PreParse: Run the parser over the whole file, but discard the AST. The only information retained isPreParsedData, which has information on function locations and metadata (arrow functions, strict mode, etc.).LazyParse: Run the parser using thePreParsedDatato skip over sufficiently large function bodies. The function bodies are stubbed out and theisLazyFunctionBodyflag on theirBlockStatementNodeis set totrue.FullParse: A regular parse. Produces a full AST for the input JS source.
In lazy compilation, the PreParse and LazyParse passes run before execution begins. Then FullParse is run when the program attempts to actually execute a lazy function.
Data Storage
The data we need to run both lazy compilation and debugger eval is stored in BCProviderFromSrc's CompilationData. It points to all the information needed to run semantic resolution, IRGen, and HBC BCGen.
BCProviderFromSrc uses shared_ptr to store the IR Module and SemContext, because it's possible for us to use the same Module and SemContext for multiple BCProviderFromSrcs. That happens during debugger eval when the SemContext and IR are reused (to be able to access captured variables) but the source is new so we use a new BCProvider (because one BCProvider corresponds to one RuntimeModule, which corresponds to a single JS source).
The fact that we persist Module and SemContext allows us to run the compiler with new code almost as if it existed the whole time.
We introduce two IR instructions: LazyCompilationDataInst and EvalCompilationDataInst. These store data needed for lazy/eval compilation respectively and ensure that relevant Variables are kept alive by storing every parent VariableScope as an operand (the VariableScopes have users, so they won't get destroyed).
We can optionally have a pointer to an IR Function from the functionIR_ field of BytecodeFunction, which allows us to get to the relevant CompilationDataInst. The functionIR_ will be destroyed when the BytecodeFunction is itself destroyed along with its BytecodeModule when the GC cleans up the corresponding RuntimeModule.
Semantic Resolution
First pass
The first time the resolver runs with lazy compilation or in preparation for allowing debugger eval, it stores extra data to the FunctionInfo by populating the bindingTableScope field. In lazy compilation mode, it doesn't have to enter lazy functions because they're empty stub BlockStatementNodes.
Actual Resolution
SemanticResolver::runLazy is the entrypoint into SemanticResolver for lazy compilation. It uses an existing SemContext and adds new information to it. After resolving a lazy function, it cleans up binding table state unless we still need it for debugger eval in the future.
SemanticResolver::runInScope allows for local eval, and operates similarly, though it requires a ProgramNode as input. It doesn't free any preexisting SemContext data. It is called with a new SemContext, which is created as a child of the original SemContext to share the binding table but to allow it to be freed separately from the root SemContext.
These paths just run the SemanticResolver as usual after some setup, because they can restore the binding table state and internal SemanticResolver state based on their input.
IRGen
First pass
When IRGen encounters a lazy function body stub, it calls setupLazyFunction, which makes an IR Function with a LazyCompilationDataInst and an UnreachableInst terminator.
When IRGen actually compiles a function, if debugger eval is to be supported, it replaces the body of the IR Function with an EvalCompilationDataInst.
We also now have two lists of Functions in the Module: one ordinary list of functions to be compiled, and a compiledFunctions_ list which only holds functions that have already passed through the whole pipeline once. This is a performance optimization to avoid reiterating through these functions needlessly every single time we rerun IRGen.
Actual IRGen
Running IRGen on a lazy Function can produce many new IR Functions, because there may be many small functions inside the lazy function. These new functions will be some mix of lazy and eagerly compiled. They are not added to the compiledFunctions_ list yet.
BCGen
This part interacts heavily with the IR data structures.
First pass
On trying to run BCGen for a lazy Function, assign it a bcFunctionID. This allows it to get a dummy CodeBlock created at runtime, and we can use the ID to replace the lazy bytecode with real bytecode. Then, assign it to the functionIR_ field on the new BytecodeFunction so that we can find it later. Move the function to the compiledFunctions_ list so that we no longer treat it as a member of the regular function list in the IR lowering pipeline. Generate a mostly empty function header and empty bytecode, so we have enough information to run actual compilation later. So an IR Function used in lazy compilation can be: not compiled yet (and containing a LazyCompilationDataInst), assigned a BC ID but not actually compiled (and in compiledFunctions_), or destroyed and replaced with a real Function with real instructions.
Similarly for functions with EvalCompilationDataInst. They're already in the compiledFunctions_ list, but we store them in functionIR_.
Actual compilation
When the function is called during lazy compilation, generateLazyFunctions is issued to recompile every Function in the Module that isn't a lazy/eval function. This assigns bcFunctionIDs to all newly discovered functions. They are then generated as usual, and the BCProviderFromSrc is modified to point to the updated values for string tables, bytecode function tables, etc. by calling setBytecodeModuleRefs.
generateForEval works similarly but the key distinction is that eval creates a new BCProvider, so the existing one is not updated, and there's no existing BytecodeFunction with an ID that has to be replaced.
After either of these functions consumes the newly generated functions in the Module, we call resetForMoreCompilation which cleans upany Functions we don't need any more or replaces their body with just EvalCompilationDataInst if they're needed still for eval. It also deletes any now-unused VariableScopes.
Running the pipeline
Lazy compilation is called via CodeBlock::lazyCompile, which detects whether there's no bytecode on a CodeBlock and calls hbc::compileLazyFunction. It modifies the existing BCProviderFromSrc, and CodeBlock replaces its own internal state with the new information.
Debugger eval runs through the standard directEval pipeline, which uses evalInEnvironment. If evalInEnvironment is provided a Handle<Environment> then it won't run the pipeline from scratch but rather go through hbc::compileEvalModule.
Doc MemoryProfilers (`doc/MemoryProfilers.md`)
id: memory-profilers title: Memory Profilers
This page describes the various tools you can use to diagnose memory issues in
your app using Hermes. The tools can be used in a variety of ways based on
your needs.
The main goal of these profilers are to help you identify ways your app can
reduce its memory footprint. This is important for improving the user experience
in many ways:
- Your app can run faster and have fewer, less expensive garbage collection
cycles - Your app can avoid running out of memory entirely, and crashing with an Out of Memory (OOM) error
- Your app can avoid being killed by the operating system for using too much memory
Heap Snapshots
A Heap Snapshot is a view of the entire JavaScript heap at a point in time. It
represents the data as a graph, where nodes are the values in the heap, and the
edges are pointers from one node to another. The snapshot can answer these types
of questions:
- What types of values is my app using?
- How much memory do those values use?
- What memory is being retained unnecessarily?
The nodes in the snapshot can be any value usable in JS, such as:
- Objects:
{a: 1, b: 2} - Arrays:
[1, 2, 3] - Functions:
function foo(args) { ... } - Strings:
"hello world" - Numbers:
3.14 - Native host memory, injected into the heap via
jsi::HostFunctionandjsi::HostObjectin the JSI API - Hermes-specific internal memory, such as
Environment,ArrayStorage, andHiddenClass, and the backing storage of ArrayBuffers.
Each node has a list of edges to other nodes, which can be used to determine
what nodes retain other nodes. If node A retains node B, that means if A
is reachable by your program, B will also remain reachable. Therefore, B cannot
be garbage collected until A is garbage collected.
Many heap snapshot viewers will define the difference between shallow size
and retained size. Shallow size is the size of just that node, whereas
retained size is the size of all the nodes the current node retains.
The snapshot format is a JSON file ending in .heapsnapshot, and is the same
format used by the Chrome Developer Tools and the V8 JavaScript engine. This
means all tools that work with heap snapshots taken from V8, Chrome, or Node.js
are also compatible with snapshots taken from Hermes.
In particular, you can take memory snapshots from the Chrome Developer Tools
connected to Hermes!
Taking a heap snapshot with the Chrome DevTools
Before reading this section, make sure you can
connect the Chrome DevTools to Hermes
to debug some JavaScript. Once you have that set up you can proceed.
- Open the "Memory" tab of the DevTools, and make sure the "Heap snapshot" radio
button is selected - (Optional): Click on the garbage can icon in the top left to collect any
unreachable objects. This will prevent them from showing up in the snapshot. - Click on "Take snapshot"
Chrome will alert Hermes that a snapshot is requested, and Hermes will stream
the snapshot JSON file back to Chrome. This may take some time if you have a
large heap. Once it's completed, you can go to the
Using the Chrome DevTools Heap Snapshot Explorer
section to start reading it.
You can save the snapshot to disk by pressing the "Save" button on your snapshot
in the Chrome menu:
Taking a heap snapshot from C++
Using the Chrome DevTools only applies when you have attached a debugger. For
various reasons, it might be hard to do that for your app. In those cases, you
can write some C++ code to ask Hermes for a heap snapshot.
Before reading this section, make sure to read the JSI documentation which
explains all of the basics of the API.
Now, say you have some Native Module in React Native, and you want to create
a heap snapshot. Here's what you do:
void myFunc(jsi::Runtime &rt) {
// If you want to write to a file name, use createSnapshotToFile.
rt.instrumentation().createSnapshotToFile("/tmp/filename.heapsnapshot");
// If you already have a C++ std::ostream set up, use createSnapshotToStream.
rt.instrumentation().createSnapshotToStream(std::cout);
}This method gives you a lot of flexibility of when and where you want to set up
your heap snapshots. You can also expose the functionality above through ajsi::HostFunction and call it from JS. The downside is you'll need to add a
new native module and build React Native from source for your app.
Taking a heap snapshot from JavaScript with the Hermes CLI
Currently, it isn't possible to take a heap snapshot from JavaScript unless you
are running hermes via the command line interface.
If you are running from the command line interface, there's a function defined
on the global object called createHeapSnapshot, used like so:
// Calling with no arguments will print to stdout
createHeapSnapshot();
// Calling with a single string argument will write the snapshot to that file
// path. If it isn't a valid file, or the permissions won't allow Hermes to
// write to it, it'll throw a TypeError
createHeapSnapshot("/tmp/filename.heapsnapshot");Note that createHeapSnapshot does not exist if you are running Hermes in
React Native, or if Hermes was built without memory instrumentation, and
you will get an exception if you try to use it.
Loading a heap snapshot from disk
If after any of the above methods you have a filename.heapsnapshot file saved
somewhere, you can load that into Chrome without needing to connect to any
running app. From the "Memory" tab, next to "Take snapshot" there is a "Load"
button which will open a file browser. Navigate to the file you want to load
and open it.
Using the Chrome DevTools Heap Snapshot Explorer
Now you have a heap snapshot taken and you want to find a memory problem. Here's
an explanation of what you can do in Chrome.
Sorting
You can sort by any of the following categories by clicking on the category
name:
- Retained Size: size of all nodes pointed to by this node
- Shallow Size: size of just the node itself
- Constructor: The name of the constructor of the node. Can think of it as a
type name - Distance: The number of edges needed to traverse from a root to this node
Click a second time to reverse the order of the sort.
Type names
You can search for a type name, which uses the name of the constructor function
by default. For example, if you have code like the following:
function MyObject() {}
var obj = new MyObject();The node for obj in the snapshot will have the constructor name MyObject.
Expanding the type category will show all objects that are in the heap of that
type.
Hermes tries to be as specific as it can with type names for objects. For
objects created without a constructor, such as object literals
({a: 1, b: "hello"}), Hermes will display its type name as Object(a, b). If
an object has more than 5 properties, it will be displayed asObject(a, b, c, d, e, ...). If an object has more thanHiddenClass::kDictionaryThreshold properties (currently 64), it will be
displayed as Object(Dictionary).
Warning: If you change the name of the constructor function dynamically, or if
the .name property of a function is an accessor, Hermes may only report the
original name of the function as defined in the source file.
Retainers
If you click on a particular object in your snapshot, a drawer will pop up below
saying "Retainers". This drawer will show a tree of all nodes that have a
reference to this node.
If you click on the dropdown arrow for one node, you'll see all of the other
nodes which retain that node, and so on until you reach what's called a "root"
of the heap. You can also right-click on a node in the retainers drawer and
click on "Reveal in Summary view" to jump to that node in the summary box.
In this example, you can see that an object (who has an "a" property), is
retained by the property "x" in an instance of MyObject. That object is retained
by the 1,000th element in an array. That array is retained by (Registers),
which is the root category for all local variables on the JS stack.
Below you can see some more of the root categories that Hermes uses to describe
some things that can retain objects:
The most important ones are:
(Registers): all local variables on the JS stack(IdentifierTable): a table of strings used as object properties and symbol
descriptions(GCScopes): the equivalent of the(Registers)group for native code inside
the Hermes engine. These hold onto JS values currently in use by native code(Prototypes): lists of prototypes and constructors of system objects likeFunctionand array iterators(Custom): these are any roots created by native code outside of Hermes. In
the case of any embedder using JSI (such as React Native), this corresponds to
thejsi::Values being used(SymbolRegistry): this is a table of all symbols made bySymbol.for("foo")
Knowing which root anchors your node is an important step to knowing why it is
being retained, and if it shouldn't be. If an object doesn't have a retaining
path to the root, it means there is nothing keeping it alive, and it will be
collected at the next garbage collection cycle.
Outside of root categories, there is another frequent retainer of objects:
the Environment type. An Environment in Hermes is a simple array that is
used to store captured variables of a closure. In this example you can see that
the variable x is captured:
function makeCallback(x) {
return () => x;
}The closure returned from makeCallback will capture x, and Hermes implements
this by placing x into an Environment pointed to by the closure.
Environments can be shared if multiple closures capture the same variable:
function makeManyCallbacks(x) {
return [() => x = {foo: 1}, () => x.foo];
}In this case, both closures will capture the same environment pointing to x,
so the second closure will see updates made by the first closure.
Taking a snapshot of some objects created by calling makeManyCallbacks shows
the following:
The [1] in Array shows this was the second closure stored in the array.
Comparing Snapshots
If you have two heap snapshots from the same execution you can compare them to
each other and see which objects were allocated or destroyed between those
points in time.
First, load two snapshots into your Chrome workspace. Make sure they are from
the same process. Two snapshots from two separate runs of your app will not be
comparable, and the output might not make sense.
Then, click on the "Summary" dropdown box at the top and change it to
"Comparison". If you don't see the "Comparison" option that means you don't have
two snapshots loaded into Chrome yet. The following video shows how to do this:
Once you're in a comparison view, you can see what objects were created since
the last snapshot represented by a + icon, and what objects were destroyed
represented by a - icon. The columns are as follows:
# New: Number of new objects created of a particular type# Deleted: Number of objects deleted (garbage collected)# Delta: New - Deleted. Positive means more were created than deleted.Alloc. Size: Size of all newly created objects of that type added togetherFreed Size: Size of all deleted objects of that type added togetherSize Delta: Alloc. Size - Freed Size. Positive means more bytes were created
than deleted.
Typically sorting by Size Delta is the most useful for hunting down memory
regressions. A large positive value means a lot of extra bytes were allocated
for that type of object.
Expanding the type category will show for each object ID, whether it was new
or deleted, and the size it was.
Statistics View
From the same dropdown menu for selecting "Summary" or "Comparison", you can
also select "Statistics", which shows a pie chart of various memory categories.
This view only accounts for a few limited categories of memory, and doesn't have
any way to drill down into the category to see more, so this view is of limited
use.
Heap Timelines
TODO: Fill out this section.
Sampling Heap Profiler
TODO: Fill out this section.
Doc Modules (`doc/Modules.md`)
id: modules title: Modules
In which we describe the module system and metadata input.
Metadata Format
In order to provide a directory or zipfile to Hermes,
it must contain a metadata.json file at the root.
The metadata.json file must be a JSON object, with the following fields:
segments- An object with string keys representing integers, which are the segments
IDs. The IDs are generally sequential but there can be gaps (ex. having IDs
"6" and "8" and no "7"). ID zero has special meaning, this is the "main
segment" that gets loaded on startup (ex. when React Native starts). - Each value must be an array containing file paths
of the files to place into that segment, expressed relative to the root of
the ZIP file (ex.subdir/foo.js). File paths may include./at the start,
but they are not required to. - The first element of
segments["0"]is the first modulerequired at run time.
- An object with string keys representing integers, which are the segments
resolutionTable(Optional)- An object for which the keys are relative file names
(the same file names as insegments). - Values are objects which map from strings given to
require()
to their actual resolved file path relative to the directory or zip file root.
These relative paths should be the same as insegments, with leading./.
- An object for which the keys are relative file names
Example Metadata File
{
"segments": {
"0": [
"./cjs-subdir-main.js",
"cjs-subdir-2.js",
"bar/cjs-subdir-bar.js",
"foo/cjs-subdir-foo.js"
]
},
"resolutionTable": {
"./cjs-subdir-main.js": {
"foo": "./foo/cjs-subdir-foo.js"
},
"./foo/cjs-subdir-foo.js": {
"bar": "./bar/cjs-subdir-bar.js"
}
}
}Require system
Modules can require each other using the require function provided as a local
variable by the runtime. The argument to require must be an absolute path
which root is the root of the ZIP file or input directory. For example:
const Foo = require('/subdir/foo.js');
Foo.doSmth();Source maps
Each JavaScript file can optionally provide a corresponding source map, which is
the name of the source file with the suffix .map. For example subdir/foo.js
might provide a subdir/foo.js.map file.
Complete design
Module mode is currently activated via passing -commonjs to Hermes while compiling.
Hermes can then be given a directory and a metadata.json file,
or a simple list of files with the first being the entry point.
First, each file is parsed just like any other JS file.
Then, a call to hermes::wrapCJSModule puts each file in its own "module function",
which binds exports, require, module as parameters.
IR generation relies on this wrapping of the AST.
Every CJS module file generates IR into a shared hermes::Module
(it's called M in generateIRForSourcesAsCJSModules).
Each of these generated CJS modules are now hermes::Function *s.
These are registered in the cjsModules_ field using the hermes::Module::addCJSModule function,
which stores information in the CJSModule struct regarding filename, id, and IR function.
This allows lookup of the CJS modules either via hermes::Function * or by string literal (file path).
If the caller of the Hermes CLI passes -static-require,
then we attempt to resolve all require calls at compilation time.
This occurs in ResolveStaticRequire.cpp, which is able to resolve files if they were povided
by the user at invocation time and if all require calls only take string literals as arguments.
If every require call is able to be resolved, every one of these require calls is replaced
with a call to HermesBuiltin_requireFast with an ID for the CJS module,
and that function does no string work and is therefore very fast.
After all CJS modules have generated IR and all the requires have been resolved,
we generate one or more bytecode files from our IR::Module M.
This requires two special bits of logic in hbc::generateBytecodeModule.
- We add a mapping from a CJS module to the HBC function ID.
This allows us to actually runrequirewhen JS execution demands it.
Ifrequires have been resolved, we add to thecjsModulesStatic_table in the HBC file,
else we add to to thecjsModules_table in the HBC file (which maps from strings instead of IDs). - To accommodate bundle splitting, we also pass a
SegmentRangeto the function.
This allows us to only compile the functions which are needed by the CJS modules in the segment.
We then set acjsModuleOffset_field in the HBC file,
which allows us to know how far into the complete set of CJS modules this segment is.
Because every HBC file contains a contiguous set of CJS modules, each of which are unique,
it is not possible for multiple segments to contain the same CJS module.
Having built 1 or more HBC files in the compiler, we can now execute them.
The vm::Domain data structure is used for keeping track of all HBC files which were compiled together.
In particular, the Domain owns a CJS module table.
This table is an array of CJS modules, indexed by the CJS module's ID.
If requires were not resolved at compile time, there's also a mapping from file path to that ID.
The first HBC file to be loaded must have a cjsModuleOffset_ of 0.
CJSModule 0 is the entry point, and will be executed first.
Loading of other segments is then done via a loadSegment call in the runtime;
the user can call it via the ConsoleHost loadSegment or more commonly via the Hermes API.loadSegment does need a requireContext, which allows us to determine which Domain to actually
load the new CJS modules from the segment into.
This is registered into require.context (recall that require was bound as a param in all CJS modules).
Finally, we can require.
When require is called, one of two things happens:
- If we DID resolve all static
requires, then calls torequirewere turned into Hermes builtin
calls, so the actual value ofrequires (the parameter) is not used.
We simply callHermesBuiltin_requireFastvia theCallBuiltininstruction. - If we DID NOT resolve all static
requires, we call therequirefunction.
The runtime will have set up that function to also pass along the base path (to allow relativerequire),
sorequireis now a BoundFunction (as if we'd donerequire = require.bind(currentPath)).
Note that this is transparent to the application developer, and doesn't change the wayrequire
is actually called in JS source.
In either case, we dorunRequireCallas implemented inrequire.cpp.
It performs any necessary checks, calls the target CJS module, and caches and readsexportsback.
Hermes also partially supports ECMAScript modules.
Currently, the interop of ESM and CJS modules is defined simply by turning import into require calls.
This precludes full correctness of the ESM module system, which requires live bindings;
when a value is changed in the imported module, the local binding to that value must also be updated.
Tests for the current emitted code can be found in test/hermes/esm/.
Doc Optimizer (`doc/Optimizer.md`)
id: optimizer title: Design of the Optimizer
Introduction
This document describes the high-level design of the Hermes optimizer. The
Hermes optimizer transforms the Hermes IR into a more efficient representation
that preserves the original semantics of the program. The IR.md document describes
the design of the Hermes IR.
Key concepts
This section describes a few key concepts and ideas:
The optimizer is responsible for optimizing the IR. IRGen and BytecodeGen
are not the right place for implementing optimizations. The parts of the
compiler that translate from one representation to another are inherently
complex because they require the understanding of the semantics of both
representations. Moreover, translators are not designed like optimizers.
They do not have good access to analysis and do not allow the separation
of the optimizer from the translation, which makes debugging more
difficult.Passes: Optimizations are organized in passes. There are two kinds of
passes: function passes and module passes. Function passes can modify only
the functions that they operate on, while module passes operate on the whole
module. Function passes are allowed to read the whole module but only
touch the current function.Analysis: Analyses are caches in front of a computation of some property.
For example, the dominator analysis is a cache that helps reduce compile
time by removing the need to recompute the dominator tree for each
function. Analyses are all about caching and invalidating pre-computed
properties.Optimizations do one thing: Optimizations are designed to be simple and
this means that they do only one thing. For example, the common
subexpression elimination optimization does not delete dead code
"on the way" just because it can.Optimizations are predictable: Sometimes there are several legal
representations of the program, but the optimizer should never
randomize the output of the compiler. Randomization of the output
happens when the output depends on runtime information such as the order
of elements in a set or map. Randomizing the output of the compiler
makes it very difficult to write tests and reproduce bugs. LLVM has
data structures that provide guaranteed order - use them!Write compile-time efficient algorithms: The compile time of a compiler is a
very important metric and we attempt to minimize compile time as much as
possible. Do not write exponential algorithms (or polynomial algorithm with
a high degree). If you are writing a quadratic algorithm make sure to
implement a sliding-window or other techniques that will allows to limit
the quadratic search to a small subset of the graph. Always assume that
there exist a function with hundreds of consecutive basic blocks or a basic
block with thousands of instructions. If you are writing a "solver" then
you are probably doing it wrong.There are three kinds of transformations: canonicalization,
simplification and lowering. Make sure that you know exactly what kind of
transformation you are doing and why. Canonicalizations are transformations that
expose opportunities for other transformations. Re-association (reducing tree
height, placing constants on the RHS, etc.) is a canonicalization because it
organizes things in predictable patterns and makes the life of future
optimizations simpler by reducing the number of possible inputs. Inlining is
another example of effective canonicalization because it exposes opportunities
for optimizations in the caller function (by providing more information).
Another example is loop rotation, which is a canonical representation of all
loops. In canonicalization we strive to clean up the program as much as
possible and reach a pure representation of the program. Simplification is what
we normally think of as optimizations, like removing redundancy by deleting dead
code and optimizing arithmetic, etc. Canonicalization can allow simplification
that can allow more canonicalization. For example, de-virtualization unblocks
inlining that may allow some transformations that enable more de-virtualization.
Lowering transformations are the opposite of canonicalization. In Lowering
transformations we generate patterns that are closer to the target
representation. We may not be able to recover from lowering transformations. One
example for lowering transformation is loop strength reduction where the
optimizer transforms loop indices into non-consecutive accesses that fit with
the hardware instruction set. Another example is loop versioning where the body
of the loop is duplicated and versioned multiple times .
Doc PerfProfiling (`doc/PerfProfiling.md`)
id: perf-profiling title: perf Profiling
This document describes how to profile the performance of Static Hermes.
Profiling JIT
Currently, we rely on Linux perf tool for profiling JIT'ed code, and we only
enable JIT on ARM64 platform. So you need to have a Linux machine with ARM64.
How To Profile
- Recording the trace.
-Xperf-profis to enable perf profiling,-Xperf-prof-dir
is to specify the output directory (by default it's/tmpso if you don't want
to look into those generated files, you can skip this flag).-k monomeans we
use CLOCK_MONOTONIC for clock id, as it's used in jitdump. Here we only sample
user space events, but you could also sample kernel events and also specify
different flags (e.g., -g for call graph). - Injecting the jitdump into the perf.data. This will create an ELF binary for
each JIT'ed function. - Reporting the trace.
perf record -g -k mono -e cycles:u ./bin/hermes ~/js/test.js -Xjit=force \
-Xperf-prof -Xperf-prof-dir ./
perf inject -j -i perf.data -o perf.data.jitted
perf report -i perf.data.jitted --call-graph=fractal,callee --childrenDoc ProhibitInvoke (`doc/ProhibitInvoke.md`)
id: prohibit-invoke title: Valid-invocation enforcement (prohibitInvoke and the constructor rule)
Introduction
Hermes enforces two kinds of "you cannot invoke this function that way" rules:
prohibitInvoke— a per-JS-function flag that says the function is
constructor-only or call-only (e.g. an ES6 class constructor cannot be
called withoutnew; an arrow function, method, generator, or async
function cannot be called withnew).- The "a
NativeFunctioncannot be used as a constructor" rule — aCellKind-based rule that rejectsnewing a plain C++ builtin that is not
aNativeConstructor.
These two mechanisms are orthogonal and fire at different times: theprohibitInvoke check runs at the callee entry and is keyed on a per-function
flag; the constructor rule runs caller-side at the new site (when this is
created) and is keyed on the callee's CellKind. This document describes both,
and how each is realized across the four execution paths: the bytecode
interpreter, the JIT, C++ builtins, and the Static Hermes (SH) native C backend.
Part 1 — `prohibitInvoke` (the per-function flag)
The two enums
There are two ProhibitInvoke enums, with deliberately different value
orderings, bridged by computeProhibitInvoke().
IR-level (include/hermes/IR/IR.h):
enum class ProhibitInvoke {
ProhibitNone, // = 0
ProhibitConstruct, // = 1
ProhibitCall, // = 2
};Backend/runtime (include/hermes/BCGen/FunctionInfo.h):
enum ProhibitInvoke {
Call = 0, // regular (non-new) calls prohibited => constructor-only
Construct = 1, // construct (new) calls prohibited => call-only
None = 2,
};Gotcha: in the backend enum the names denote which kind of invocation is
prohibited, and the numeric values are inverted relative to the IR enum.
Always convert viacomputeProhibitInvoke()(FunctionInfo.h); never assume
the numeric values of the two enums line up.
How the restriction is decided
The restriction is derived from the function's definition kind inFunction::getProhibitInvoke() (lib/IR/IR.cpp):
- ES6 base/derived constructors →
ProhibitCall(must usenew). - Generators, async functions, arrow functions, and object/class methods →
ProhibitConstruct(cannot usenew). - Everything else →
ProhibitNone.
Where it is stored
The value lives in one of two places, depending on the function kind:
- Bytecode functions: a 2-bit field in the
FunctionHeaderFlagbyte of the
code block header (include/hermes/BCGen/HBC/BytecodeFileFormat.h), written
during bytecode generation (lib/BCGen/HBC/BytecodeGenerator.cpp). Bit
layout within the flags byte (LSB first): bits 0-1ProhibitInvoke, bit 2StrictMode, bit 3HasExceptionHandler, bit 4HasDebugInfo, bit 5Overflowed, bits 6-7Kind. - SH/native-compiled functions (
NativeJSFunction): a 2-bitprohibit_invokefield inSHNativeFuncInfo(include/hermes/VM/static_h.h),
emitted into the generated C function-info table (lib/BCGen/SH/SH.cpp). - C++ builtins (
NativeFunction/NativeConstructor): no flag at all —
for these, invocation validity is handled by the constructor rule (Part 2)
and by hand-written checks in the builtin body.
The central predicate for the flag (BytecodeFileFormat.h):
bool isCallProhibited(bool construct) const {
return getProhibitInvoke() == (uint8_t)construct;
}This exploits the backend enum values: for a construct call
(construct == true == 1) it matches Construct(1); for a plain call
(construct == false == 0) it matches Call(0); None(2) never matches
either, so one comparison covers both directions.
Enforcement — a callee-entry check
The prohibitInvoke flag is checked at the callee entry, keyed on the
callee's own flags. There is no central gate: Callable::call (the vtable
funnel), Interpreter::handleCallSlowPath, and the SH doCall / _sh_ljs_call
helpers all deliberately do not check the flag; they only dispatch. Being a
callee-entry check keyed on the callee is exactly what makes it caller-agnostic
for bytecode functions.
| Callee kind | Flag enforced at entry? | Where |
|---|---|---|
| Bytecode (interpreted) | Yes | interpretFunction entry preamble and inline Call fast-path (lib/VM/Interpreter.cpp) |
| Bytecode (JIT-compiled) | Yes | Inline in the compiled prologue (lib/VM/JIT/arm64/JitEmitter.cpp) → shared slow-path helpers |
| C++ builtin | N/A (no flag) | Constructor rule (Part 2) + manual body checks |
SH-compiled (NativeJSFunction) |
No | Flag is stored but never checked at call time |
Interpreter (bytecode callees)
Two sites, both raising the same two errors:
- Entry preamble of
Interpreter::interpretFunction(lib/VM/Interpreter.cpp):
checkscurCodeBlock->getHeaderFlags().isCallProhibited(newFrame.isConstructorCall()).
Done before the callee frame is fully set up, so the exception appears to
come from the call site. - Inline Call fast-path (
lib/VM/Interpreter.cpp): checkscalleeBlock->getHeaderFlags().isCallProhibited(isCtorCall), whereisCtorCallis (new.targetis not undefined). - Messages:
"Function is not a constructor"/"Class constructor invoked without new".
The inline fast path is taken only for JSFunction callees without a JIT
pointer; when the callee is JIT-compiled, the interpreter skips its own check
because the JIT prologue self-checks.
JIT (bytecode callees; arm64 only)
Emitted inline at the compiled function's own prologue
(lib/VM/JIT/arm64/JitEmitter.cpp): readsgetHeaderFlags().getProhibitInvoke(), loads new.target, compares to
undefined, and branches to an out-of-line slow path that calls the shared
helpers _sh_throw_invalid_call / _sh_throw_invalid_construct
(lib/VM/JIT/arm64/JitHandlers.cpp) — same two error messages. Complete and
caller-agnostic. (arm64 is the only JIT backend present.)
SH native backend (`NativeJSFunction` callees) — flag enforcement gap
The SH C backend records prohibit_invoke in SHNativeFuncInfo and uses it
for isConstructor() (lib/VM/Operations.cpp) and .prototype suppression
(lib/VM/Callable.cpp), but emits no callee-entry guard for it:
- The generated function prologue (
lib/BCGen/SH/SH.cpp) emits
stack-overflow / frame / try setup but nonew.target/prohibit check. - Call lowering →
_sh_ljs_call→doCall→NativeJSFunction::_legacyCall(justfunctionPtr_(shr)) — no flag check. - The code flags this as unfinished:
TODO(T168592126) standardize on where we perform function call validation for the native backend. - Consistent with the gap, there is no SH-backend test for
prohibitInvoke;
the existingtest/hermes/prohibit-invoke.jsruns under the interpreter.
Note this gap is specifically about the flag. The caller-side constructor rule
(Part 2) is present in the SH backend via _sh_ljs_create_this.
Part 2 — The constructor rule (`NativeFunction` cannot be `new`ed)
This is a separate, CellKind-based rule enforced caller-side at the new
site, when this is created — before the callee frame is set up. It rejectsnewing a plain C++ builtin (NativeFunction) that is not a NativeConstructor.
It does not consult the prohibitInvoke flag.
Interpreter: `CreateThisForNew` / `CreateThisForSuper` → `createThisImpl`
For new X(), the compiler emits a CreateThisForNew opcode (andCreateThisForSuper for super() in derived constructors) before the actualConstruct. The interpreter handlers (lib/VM/Interpreter.cpp) callInterpreter::createThisImpl (lib/VM/Interpreter-slowpaths.cpp), which
classifies the callee by CellKind:
>= CallableExpectsThisKind_first(JSFunction,NativeJSFunction): the
callee wants a pre-madethis→ allocate the object.>= CallableMakesThisKind_first(NativeConstructor,FinalizableNativeFunction,JSCallableProxy,NativeJSClass,JSClass):
the callee makes its ownthis→ returnundefined.>= CallableUnknownMakesThisKind_first(BoundFunction,NativeFunction):
walk the bound-function target chain, then re-check. If after unwrapping the
target is still a plainNativeFunction(not promoted into the "makes this"
range whereNativeConstructorlives), throw"This function cannot be used as a constructor."- else (not a
Callable/ not an object): throw viaraiseTypeErrorForValue
with" cannot be used as a constructor.".
The ordered CellKind ranges that make the >= comparisons work are defined ininclude/hermes/VM/CellKinds.def (CallableUnknownMakesThis = {BoundFunction, NativeFunction}, CallableMakesThis starts at NativeConstructor,CallableExpectsThis = {JSFunction, NativeJSFunction}), with contiguity enforced
by static_asserts in include/hermes/VM/CellKind.h.
SH backend: `_sh_ljs_create_this`
_sh_ljs_create_this (lib/VM/StaticH.cpp) is the SH analog with identicalCellKind logic and the same "This function cannot be used as a constructor."
throw. It additionally validates up front that new.target is a Callable
(" invalid new.target."). It is emitted by generateCreateThisInst
(lib/BCGen/SH/SH.cpp), and the JIT emits it too
(lib/VM/JIT/arm64/JitEmitter.cpp). So the constructor rule is present in all
backends — this is the caller-side construct gate the SH prohibitInvoke gap
does not affect.
C++ builtins also self-check
Beyond the CellKind rule, dual/opposite-mode builtins enforce their own
contract in their body by inspecting args.isConstructorCall(), e.g.:
if (args.isConstructorCall())
return runtime.raiseTypeError("BigInt is not a constructor"); // BigInt.cpp
if (!args.isConstructorCall())
return runtime.raiseTypeError("ArrayBuffer() called ..."); // ArrayBuffer.cppDual-mode builtins (Array, Date, Error, Boolean) branch onargs.isConstructorCall() internally. NativeConstructor itself is largely a
marker subclass (its _callImpl only asserts invariants in debug builds).
How the two mechanisms combine
During new X() the two checks fire in order:
- Caller-side (constructor rule).
CreateThisForNew/createThisImpl
(interpreter) or_sh_ljs_create_this(SH/JIT) runs first, classifies the
callee byCellKind, and either allocatesthis, returnsundefined, or
throws"This function cannot be used as a constructor."for a plainNativeFunction. No call frame exists yet. - Callee-entry (
prohibitInvokeflag). The subsequentConstructperforms
the call; on entry the callee'sprohibitInvokeflag is checked viaisCallProhibited(isCtorCall), throwing"Function is not a constructor"/"Class constructor invoked without new".
They catch different things and are complementary rather than redundant:
| Caller-side constructor rule | Callee-entry prohibitInvoke |
|
|---|---|---|
| Mechanism | CellKind range check |
prohibit_invoke / header-flag bit |
| Catches | plain NativeFunction used with new |
JS/native function whose flag forbids that invocation (arrow/method/generator/async/class ctor) |
| Timing | at the new site, before the frame exists |
when the callee frame is set up |
| Message | "This function cannot be used as a constructor." |
"Function is not a constructor" / "Class constructor invoked without new" |
| Sites | createThisImpl (interp), _sh_ljs_create_this (SH/JIT) |
Interpreter.cpp entry/fast-path, JIT prologue |
`isConstructor` — the unified predicate
isConstructor (lib/VM/Operations.cpp) is the one place that layers both
mechanisms together. It walks the JSCallableProxy and BoundFunction target
chains to the eventual target, then:
- for a
JSFunction(bytecode), consults the flag via!isCallProhibited(/*construct=*/true); - for a
NativeJSFunction, consultsprohibit_invoke != ProhibitInvoke::Construct; - otherwise applies the
CellKindrule: a plainNativeFunctionis not a
constructor unless it is aFinalizableNativeFunctionorNativeConstructor.
Note that createThisImpl / _sh_ljs_create_this do not callisConstructor and do not check the flag — they only do the CellKind
check. isConstructor is used by RJS/spec operations (e.g. Reflect.construct,instanceof helpers), not as the new-site gate.
Cross-calling behavior
Because the prohibitInvoke check is at the callee entry and keyed on the
callee's own flags, and the constructor rule is at the new site keyed on the
callee's CellKind, both hold uniformly regardless of the caller:
- Interpreter → bytecode: flag enforced (interpreter entry / fast path);
constructor rule enforced atCreateThisForNew. - JIT → bytecode, and anyone → JIT-compiled bytecode: flag enforced (JIT
prologue self-checks); constructor rule enforced via emitted_sh_ljs_create_this. - SH → bytecode: flag enforced —
doCallroutesJSFunctioncallees to their
JIT pointer or_interpret, both of which carry the callee-entry check;
constructor rule enforced via_sh_ljs_create_this. - Anyone → C++ builtin: the constructor rule (caller-side
CellKindcheck)
rejectsnewing a non-constructor builtin; call/construct contracts beyond
that rely on the builtin's own manual checks. - Anyone → SH-compiled
NativeJSFunction: the constructor rule still applies
caller-side, but theprohibitInvokeflag is not enforced at
invocation time. This is the one real hole (TODO(T168592126)).
Other consumers (not enforcement)
.prototypesetup:lib/VM/Callable.cppsuppresses.prototypeon
call-only (ProhibitInvoke::Construct) non-generator functions.- Inliner:
lib/Optimizer/Scalar/Inlining.cpprefuses to inline a construct
call into aProhibitConstructfunction, and vice versa forProhibitCall. - InstSimplify:
lib/Optimizer/Scalar/InstSimplify.cppcan drop an unusednew.targetwhenProhibitNone. - Disassembler:
lib/BCGen/HBC/BytecodeDisassembler.cppprintsConstructor
forProhibitInvoke::CallandNCFunctionforProhibitInvoke::Construct.
Quick reference
prohibitInvoke (per-function flag, callee-entry):
- Enums:
include/hermes/IR/IR.h,include/hermes/BCGen/FunctionInfo.h - Decision logic:
lib/IR/IR.cpp(Function::getProhibitInvoke()) - Predicate:
include/hermes/BCGen/HBC/BytecodeFileFormat.h(isCallProhibited) - Set:
lib/BCGen/HBC/BytecodeGenerator.cpp(bytecode),lib/BCGen/SH/SH.cpp(SH) - Interpreter enforcement:
lib/VM/Interpreter.cpp - JIT enforcement:
lib/VM/JIT/arm64/JitEmitter.cpp,lib/VM/JIT/arm64/JitHandlers.cpp - SH flag gap:
lib/BCGen/SH/SH.cpp,lib/VM/StaticH.cpp(TODO(T168592126))
Constructor rule (CellKind-based, caller-side):
- Interpreter:
CreateThisForNew/CreateThisForSuperinlib/VM/Interpreter.cpp
→createThisImplinlib/VM/Interpreter-slowpaths.cpp - SH/JIT:
_sh_ljs_create_thisinlib/VM/StaticH.cpp, emitted bylib/BCGen/SH/SH.cppandlib/VM/JIT/arm64/JitEmitter.cpp - CellKind ranges:
include/hermes/VM/CellKinds.def,include/hermes/VM/CellKind.h - Manual builtin checks:
lib/VM/JSLib/{BigInt,ArrayBuffer,DataView,Boolean,Array,Date,Error}.cpp
Unified predicate:
isConstructor:lib/VM/Operations.cpp
Doc ReactNativeIntegration (`doc/ReactNativeIntegration.md`)
id: react-native-integration title: React Native Integration
Using a custom Hermes build in a React Native app
Since React Native 0.69, Hermes is shipped as part of the React Native distribution. We call this bundled hermes.
If you wish to customize Hermes and use it inside your React Native app, you can do so by using the REACT_NATIVE_OVERRIDE_HERMES_DIR environment variable. This variable allows you to specify a folder where you can store your custom copy of the Hermes repository.
export REACT_NATIVE_OVERRIDE_HERMES_DIR=/path/to/your/hermes/repoIf you set this variable, React Native will use the Hermes build from the specified directory instead of the one that comes with the React Native distribution.
[!NOTE]
If you are making changes to the compiler in Hermes, be sure to make sure you test your app in release mode as this enables bytecode compilation in advance.
Instructions for Android
On Android, once you set the REACT_NATIVE_OVERRIDE_HERMES_DIR environment variable, make sure you also:
- Enable a build from source following the instructions in the React Native website.
- Re-run the Android app with the
yarn androidcommand.
Instructions for iOS
On iOS, once you set the REACT_NATIVE_OVERRIDE_HERMES_DIR environment variable, make sure you also:
- Disable React Native's Default Hermes Pod: Ensure that the
:hermes_enabledflag is set tofalseinios/Podfile.
use_react_native!(
:path => config[:reactNativePath],
:hermes_enabled => false,
# ... other configurations
)- Enable a build from source by installing the pods as follows:
BUILD_FROM_SOURCE=true bundle exec pod install- Re-run the iOS app with the
yarn ioscommand.
Reporting native crashes
If Hermes causes a native crash in your application, a stack trace is critical for us to be able to understand where the crash occurred.
If you have a native crash to report, please be aware that in most cases, it is not feasible for us to debug crashes in earlier versions of Hermes. Please update your app to the latest React Native version, as that contains the latest hermes-engine versions. Specifically since React Native 0.69, Hermes is now coming bundled together with React Native.
Including the symbolicated stack trace will make it easier for us to address your bug report more quickly.
How to symbolicate a native crash on Android
To symbolicate a native crash on Android, you need to use the ndk-stack command.
This command is part of the Android SDK and you should be able to access with $ANDROID_HOME/ndk/<ndk_version>/ndk-stack (provided you replace the URL with your NDK version like $ANDROID_HOME/ndk/21.4.7075529/ndk-stack).
To use the ndk-stack command you will need the debug symbols (check the paragraph below to see where to find them).
You can then symbolicate a stacktrace as follows:
$ANDROID_HOME/ndk/21.4.7075529/ndk-stack -sym ./<path-to-debug-symbols>/obj/local/arm64-v8a < crash.txtprovided that crash.txt contains your stacktrace in plain text, and you replace armeabi-v7a with the correct architecture you used when the native crash was recorded.
As an alternative, you can trigger a crash and have a symbolicated logcat in real time with the following command:
adb logcat | $ANDROID_HOME/ndk/21.4.7075529/ndk-stack -sym ./<path-to-debug-symbols>/obj/local/arm64-v8aWhere to find the debug symbols
React Native < 0.69
For React Native versions before 0.69 (i.e. hermes-engine 0.11.0 and previous versions), you can find the debug symbols in the GitHub release corresponding to the Hermes NPM you used to build your application.
Download the hermes-runtime-android-vX.Y.Z.tar.gz file for your version, unpack the tar file, then run ndk-stack using the contained directory as described in the paragraph above.
React Native 0.69 and 0.70
For React Native versions 0.69 and 0.70, you can find the debug symbols in the GitHub release corresponding to the React Native version you used to build your application.
For instance for React Native 0.70.6, you can find the hermes-native-symbols-v0.70.6.zip file with the debug symbols inside.
React Native >= 0.71
For React Native versions 0.71 and above, the debug symbols are uploaded to Maven Central together with the react-native and hermes-engine artifacts. You don't need to manually download the debug symbols at all, as they will be available to you when you build your apps locally.
You can just invoke the ndk-stack command suggested above and the symbolication will just work.
Doc RegExp (`doc/RegExp.md`)
id: regexp title: RegExp
RegExp
The Hermes regexp engine is a traditional engine using a backtracking stack. It compiles a regexp into bytecode which can be executed efficiently. For regexp literals like /abc/, this occurs at compile time: the regexp bytecode is embedded into the Hermes bytecode file. Note regexp bytecode is distinct from Hermes bytescode.
The regexp engine proceeds as follows:
- Parse phase. The regexp parser emits a tree of nodes, effectively an IR.
- Optimization phase. The node tree is traversed and optimized in various ways.
- Emitting phase. The node tree is traversed and emits regexp bytecode.
- Execution phase. The bytecode is executed against an input string.
Supported Syntax
As of this writing, Hermes regexp supports
- All of ES6, including global, case-insensitive, multiline, sticky, and Unicode (and legacy).
- ES9 lookbehinds.
- Named capture groups.
- Unicode property escapes.
Doc SpecIncompat (`doc/SpecIncompat.md`)
Incompatibilities With The JavaScript Spec
This documents aims to collect and explain deliberate differences between Static Hermes and the JavaScript spec.
Arguments Object
"Mapped" Arguments Object in Loose Mode
In loose mode, updates to the arguments object are not reflected in the function parameter values and vice versa.
function foo(param) {
arguments[0] = 10;
print(param, arguments[0]); // Prints different values in SH
}Motivation: this is a very rare case. Implementing it correctly is expensive and would impose cost on the majority of usages which do not depend on this behavior. Optimizing it, so it doesn't regress legitimate cases is possible, but complex.
This is "implementable", but with very low priority.
Assigning to Arguments in Loose Mode
Assignment to arguments is prohibited in loose mode.
function foo() {
arguments = 0; // Allowed by the spec, but prohibited in Static Hermes
}Motivation: this is a very rare case, not generally useful. Implementing it correctly is possible, but complex, with very little benefit.
This is "implementable", but with very low priority.
Aliasing Arguments With a Var Declaration in Loose Mode
In loose mode declaring var arguments does not alias the arguments object. Instead it simply creates a new declaration that shadows the arguments object, similar to let.
function foo() {
var arguments;
print(arguments); // Prints undefined in Static Hermes
}Motivation: similar to assigning to arguments, this is a very rare case, with no uses that we are aware of.
This is "implementable", but with very low priority.
`String.prototype.localeCompare` Ignores the Locale
localeCompare ignores its locales and options arguments. Its ordering is the DUCET root collation of Unicode Technical Standard #10, not the host locale's.
// Swedish sorts "z" before "ä"; the root collation does not.
print("ä".localeCompare("z", "sv")); // Prints -1 in Static HermesECMA-262 permits this: without ECMA-402, localeCompare need only be a consistent comparison function. It is still worth knowing about, and it is a change from earlier versions, which forwarded to ICU and so did pick up the host locale on Linux and Windows.
Two further properties of the ordering:
- It is plain DUCET, not the CLDR root that ICU and the platform collators use. CLDR retailors the root in a few places, so some pairs sort differently on Android and Apple, which still use their platform collators.
- Comparison stops at the tertiary level; there is no identical level. Canonically equivalent strings compare equal, as do strings differing only by a completely ignorable character such as U+0000.
Motivation: locale-tailored collation means shipping CLDR tailoring data and a locale database. The root collation table is about 110 KB, needs no system library, and removes the last dependency on ICU for string comparison.
Full Scoped Function Promotion Semantics in Loose Mode
Static Hermes implements most of the scoped function promotion semantics in loose mode, but some corner cases are not spec compliant yet.
function g() {
{
function f() { return 1; }
{
function f() { return 2; }
}
}
print(f());
}
g();Apparently the spec requires this example to print "1", because the second f() shouldn't be promoted to function scope. Shermes prints "2". In our (quick and incomplete) tests, other major engines also print "2".
Motivation: these are very rare cases. Loose mode itself is rare. While we intend to address them, the priority is low.
Doc Strings (`doc/Strings.md`)
id: strings title: Strings
Hermes Bytecode String Kinds
Hermes bytecode has three kinds of string. The simple JS program Object.foo = "bar"; contains examples of all of them.
Objectis Predefined because it is being used directly to access a property (i.e. it is an identifier) -- in this case on the global object -- and both the compiler and runtime are already aware of it. Seehermes/VM/PredefinedStrings.deffor a full list of strings that could be Predefined (if they were used for property access).foois an Identifier because it is being used directly to access a property but it is not built-in to the VM or compiler.baris a String, because it is referred to as a literal but neither of the previous conditions apply.
The distinction between these is important during bytecode initialisation, particularly "Identifier Table Initialisation", detailed in the section below.
String Table Format
In the Hermes bytecode format, the string table is split into the following consecutive sections of the file:
- String Kinds. A sequence which describes the kinds of the strings in the table (as described in "Hermes Bytecode String Kinds"). Abstractly, the i-th element in the sequence is the kind of the i-th string in the table. Represented as a run-length encoding.
- Identifier Hashes. This sequence has an element corresponding to each identifier in the table. The i-th element of this sequence corresponds to the i-th identifier in the table. If the string is an Identifier, the element contains a hash of the identifier's string representation.
- Small String Table. The primary index into the string table. When a bytecode instruction refers to a string by its index, it uses the offset into this data structure. Represents the string with three pieces of information:
- Whether it is UTF16 or not.
- Its offset into the character storage.
- Its length.
Each entry is packed in 32 bits: 1 bit for the UTF16 flag, 23 bits for the offset and 8 for the length. If the length or the offset do not fit in the available space, the entry spills into the overflow table (the next section). In this case the small entry's length is 255 and its offset field is interpreted as the offset into the overflow table.
- Overflow String Table. Contains the offset and length information (32 bits for each) for strings that could not fit into a small entry.
- ASCII String Storage. A contiguous sequence of ASCII character data. Character data for individual strings can potentially overlap.
- UTF16 String Storage. As above, but for UTF16 strings.
Identifier Table
To speed up property accesses the Runtime interns property names, assigning a number to refer to them by. Although bytecode instructions also refer to property names as numbers, the same string -> number mapping can't be used because a property name's number in a bytecode file stems from that file's string table and will not in general agree across all bytecode files (the Runtime could be running code from multiple files).
The Runtime's intern table is managed by the IdentifierTable which maintains a two-way mapping between strings and their numbers. It is known to the GC which will clean up entries corresponding to unused names.
Importing String Table IDs
The Runtime representation of a bytecode file maintains a mapping between the numbers that strings are referred to by in the bytecode (in instructions) and the numbers that strings are referred to by in the IdentifierTable, this is initialised when the bytecode is loaded. Identifier Translations are used to speed this process up:
- Only the entries in the mapping corresponding to identifiers are assigned.
- If the identifier is Predefined its Identifier Translation is its IdentifierTable ID and the mapping can be initialised without querying the IdentifierTable. This is because the compiler and Runtime agree ahead of time on the IdentifierTable IDs that Predefined strings will be assigned to.
- If the identifier is not predefined, its Identifier Translation is its precomputed hash, this can be passed to the IdentifierTable when inserting it to avoid having to page in the contents of the string to calculate the hash during initialisation.
Lowering Strings to Hermes Bytecode
When a string is mentioned as a literal in the source code, this is always represented by an instance of LiteralString in the IR (Intermediate Representation). This is a pointer to a UTF8 string that has been interned in the compiler's memory.
In HBC (Hermes bytecode) opcodes refer to literal strings by an index into a string table (part of the bytecode file). See "String Table Format", above, for a detailed description.
- Before the IR is lowered, all the mentioned strings are gathered into a table and assigned indices.
- Indices are not assigned until all strings are gathered, at which point the table is frozen. It will remain immutable, but can now be queried: Given the UTF8 representation of a string, it will return its ID.
- The frozen table is used during lowering to emit String IDs in bytecode instructions.
- Finally the table is serialized into the bytecode, following the format laid out above.
Gathering is handled in hbc::generateBytecodeModule. The UniquingStringLiteralAccumulator class is responsible for assigning indices to strings and functions in include/BCGen/HBC/TraverseLiteralStrings.h are responsible for visiting each string in the IR. Before lowering instructions, the accumulator is converted into a StringLiteralTable which is an immutable representation of the mapping.
NB Every potential occurrence of a string must be visited by a function declared in TraverseLiteralStrings.h, otherwise they will not appear in the string table. Similarly, every instance where a string could be used as an identifier should be enumerated in isIdOperand in TraverseLiteralStrings.cpp. The compiler will throw an assertion failure in debug builds if it encounters a string without an ID or an identifier that has not been marked as such during lowering.
String Table Index Ordering
The order of entries in the String Table is significant for two reasons:
- Instructions that access properties have variants. E.g.
GetByIdShort,GetByIdandGetByIdLong. They differ in the number of bytes they have available to encode a string table index (1, 2 and 4 respectively) and their own size increases correspondingly. The compiler emits the narrowest instruction that can fit the ID being accessed, so by arranging for strings that are accessed more often to have smaller IDs, the space taken by instructions can be saved. - Because the String Kind section (see "String Table Format" above) is a run-length encoding, its size can be minimised by grouping together strings of the same kind.
In order to make good use of these properties, the compiler counts the occurrences of strings as it is gathering them. The top 2^8 most accessed strings are given the first 2^8 IDs and so on, to minimise instruction size with respect to the first constraint. Then within each category of ID (short, regular and long) strings are grouped by kind with all the Strings coming first, then Identifiers, and finally Predefineds.
Delta Optimizing Mode
In delta-optimizing mode Hermes tries to minimise the effect of incremental changes to the source on the bytecode bundle. Outside of this mode, small changes to string usage can cause the output bytecode's string table to be re-ordered. This has knock-on effects throughout the instruction stream wherever strings are referred to. When delta-optimizing mode is enabled, the order of strings that existed in the bundle before the change is preserved.
How Hermes Packs Strings
This is a description of the string packing algorithm employed by Hermes. Its design is guided by the Hermes optimization principles: it must be predictable and compile-time efficient.
A Hermes bytecode file represents strings as a pair (offset, length) in a big character buffer, which necessarily contains all strings as substrings. By being clever about arranging the strings in the character buffer, we can take advantage of shared substrings. This reduces the size of the bytecode file and the memory footprint of the app.
There are two types of relationships between strings:
- A full containment relationship. For example,
once upon a timecontainsupon. In this case, we would like to emit only the characters of the first string, and have the second string reference a substring of the first. We say that the first string is a parent of the second. - An overlapping relationship:
splitpeaandpeasoup. Herepeais a suffix of the first string and a prefix of the second, and so we would like to emitsplitpeasoupwhere thepeais shared between strings. We say that the pair (splitpea,peasoup) has overlap of length 3.
The challenge is to find a superstring of all our strings which is shorter than simply concatenating them. Finding the shortest such string is the "Shortest Common Superstring" problem, and it is NP-Hard. We employ a greedy heuristic. The output is not as small as the Set Cover approach, but the Set Cover approach appears necessarily quadratic while ours is N log N. (Input size is on the order of 35k strings, so quadratic algorithms are not tolerable.)
The basic approach is inspired by "A greedy approximation algorithm for constructing shortest common superstrings" by Tarhio and Ukkonen. Given a set of strings S:
- Consider S to be vertices of a weighted directed graph.
- Let there be an edge from s1 to s2 if s1 overlaps s2, that is, some string is both a suffix of s1 and a prefix of s2. The weight of the edge is the amount of overlap.
- Construct a Hamiltonian Path through the graph by greedily choosing edges with the maximum weight, but ignoring edges that would result in a cycle.
- Output the strings in the order of the path, applying the overlap.
The key bottleneck of the algorithm is step 2: identifying overlapping strings. Tarhio and Ukkonen suggest using KMP string matching on all pairs of strings, which is quadratic. We instead build a generalized suffix array on all strings, which allows both finding parent and overlap relationships via binary search.
Computing Parents and Overlap
Recall that an overlap is a pair (left, right) where there is some string that is simultaneously a suffix of left, and a prefix of right. For example, splitpea and peasoup has overlap of 3. We wish to find all parents and all overlaps. We are armed with a suffix array: a sorted list of suffixes of all our strings, where each suffix points back to the string(s) that contain it.
We loop over the right strings, and find all overlapping left mates. For each right string, we loop over its prefixes in increasing length. For example, given peasoup we loop over p, pe, pea... Call this the test prefix. We use binary search in our suffix array to identify suffixes that are prefixed by the the test prefix. This set of suffixes is necessarily contiguous, because the suffix array is sorted. If a suffix is exactly equal to that prefix, then we have an overlap. This is necessarily the leftmost element of our range, because our range is sorted.
A key idea is that moving to the next prefix can only narrow the matching suffixes. For example, only suffixes that have pe as a prefix may have pea as a prefix. Therefore we do not need to reset the binary search range across iterations. Furthermore, for each iteration, we only need to consider one character in each suffix, since we know all previous characters necessarily match. This is key to performance.
When we find nonzero overlap, we add an Arc (left -> right) representing edges in our graph. The Arcs are maintained sorted by weight for our greedy algorithm.
Example
Say we wish to find overlaps with left string splitpea and right string peasoup (of course, there is only one overlap of length 3). Our proposed right string is peasoup and our suffix array is (considering only the suffixes of splitpea):
[a, ea, itpea, litpea, pea, plitpea, splitpea, tpea]
We are going to iterate the prefixes of our right string: p, pe, pea, peas, peaso, peasou, peasoup, but we will see that we exit early.
Start with
p. Binary search at index 0 to get the contiguous range[pea, plitpea]We then check if the first element is exactly equal top(which we can do simply by checking its length); if it were, we would add an arc of weight 1.Next is
pe. Binary search on index 2 withe, yielding a one-element range[pea]Again we would add an arc if the first element were equal tope.Next is
pea. Binary search on index 3 witha, again yielding[pea]The first string of this range is indeed equal to our prefixpea, so we add an Arcpeasoup -> splitpeawith overlap 3.Next is
peas. Binary search to narrow our range by the fourth characters. This yields an empty range, so we exit the loop.
After exhausting our prefixes, it may happen that our range of suffixes is not empty. For example, if we searched for lit we would end up with one suffix litpea. This is how we identify substring containment relationships. In this case we don't add an arc (since there's not actually any overlap) but we do set a pointer from lit to the parent string that contains it, and the offset within that parent.
Suffix Array Construction Optimizations
We construct the suffix array by uniquing suffixes via a hash set, and then sorting them. This was found to be faster than using a sorted data structure (such as std::map) because comparing long strings lexicographically is expensive.
There are some optimizations we take advantage of:
- We are only interested in suffixes that share a prefix with some string in our set. For example, for the set [splitpea, peasoup] we only need to look at suffixes that start with s or p. We take this a bit further by making a hash set of three-character prefixes of our strings ("trigrams"), and only constructing suffixes prefixed by one of those trigrams. This causes us to miss overlaps of length 1 and 2, but massively reduces the number of suffixes that we have to consider.
- There are many long strings with shared suffixes. When constructing the suffix array, using a three-way radix quicksort instead of
std::sort()is a substantial win, because it avoids comparing known-matching prefixes.
Further Optimization
- Computing overlap is a natural candidate to be done in parallel.
- It's possible that we could employ a faster algorithm for suffix array construction such as SA-IS, but it is unclear how we would unique our suffixes with that approach.
References
- "A greedy approximation algorithm for constructing shortest common superstrings" by Tarhio and Ukkonen
- 3-way radix quicksort
Doc TypedLanguage (`doc/TypedLanguage.md`)
Typed Language
The typed language implementation is in progress and considered unstable. This is a usage guide that explains how to use it and what features currently work.
Type System
The Flow type syntax is supported (use -typed).
The TypeScript type syntax is partially supported (use -parse-ts to convert TS to Flow before compilation).
Primitive Types
void(undefined)null(null)boolean(true,false)string("abc")number(1,3.5,Infinity,NaN)bigint(1n)
Native Types
c_ptr(raw C pointer)
Any/Mixed
anyis a supertype of all types, casts implicitly to other types with a runtime type check.mixedis a supertype of all types, but does not cast implicitly.
Empty
emptyhas no values. It is the subtype of all other types.
Unions
A | B indicates a union between two types A and B. Values that are either A or B are valid values of A | B. Unions may be cyclic, but cycles must include another type. For example: type A = A is invalid, but type A = [A, B] | null is valid.
Arrays
The typed language implements Array as a generic: Array<T> or T[] both work, and mean the same thing.
These soundly typed arrays are incompatible with untyped JS arrays and are more restrictive. They do not allow holes or empty spaces: every element of an array a in the range [0, a.length) must be of type T. Indexed access to arrays is bounds-checked and throws on out-of-bounds access.
Tuple
Tuples are shown with brackets: [A, B, C].
When instantiating a tuple, it is typically necessary to specify the type to avoid confusion with arrays:
var x: [number, string] = [1, "abc"];Objects
All object types in the typed language are exact objects.
var obj: {a: number, b: string} = {a: 1, b: "xyz"};They must have exactly the set of properties listed in their type (no extra properties allowed). Properties must be in the same order as the type requires. This allows the compiler to make access extremely efficient.
Variance
Object properties (and indexers, below) may carry a variance sigil:
var obj: {+ro: number, -wo: string} = {ro: 1, wo: "x"};+ or readonly makes a property read-only (covariant); - or writeonly makes it write-only (contravariant). The default (no sigil) is invariant: readable and writable, and subtyping requires the property type to match exactly.
Indexers
An object type may declare a single indexer (index signature) instead of named properties, describing a dictionary with a uniform value type:
var dict: {[string]: number} = {};
var n: number = dict["x"]; // read
dict["y"] = 3; // write
var m: number = dict.x; // dot access works too
dict.y = 3;An object type has at most one indexer, and an indexer cannot be combined with named properties. The key type is checked against the index expression.
Reads yield T but are checked by a checked cast at runtime (so a read of a missing key throws if undefined is not in T). Writes require the value type T.
Non-computed access (dict.x) routes through the indexer using a string key, so it requires a string-keyed indexer.
Spreading an object that has an indexer produces an object with an indexer of the same key type. If the result also has named properties (literal properties, or fields from another spread source), they fold into the indexer and its value type becomes the union of all the value types:
var d: {[string]: number} = {};
var a = {...d}; // {[string]: number}
var b = {...d, x: "s"}; // {[string]: number | string}Functions
Function types can be quite complex:
type F = (this: string, x: number, y?: boolean) => void;The type can contain the type of this inside the function, as well as optional arguments.
If a function has NO type annotations, it is considered to be an "untyped" function, and calls to it will not be typechecked. This allows for convenient calling to untyped JS from typed JS without extra effort or modification.
Classes
Classes are declared as in JS:
class C {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}Classes in the typed language are nominally typed. This means that two classes that happen to have the same field names cannot be used interchangeably.
Class methods can be decorated as final, meaning they cannot be overridden by a subclass:
class A {
@Hermes.final
foo(): number { return 1; }
}
class B extends A {
foo(): number { return 2; } // ERROR
}Optional Types
The ? indicates that a type may also be either null or void:
type A = ?number;Type Aliases
Types can be aliased using the type keyword:
type A = number;Generics
Classes and type aliases can both be generic.
class Wrapper<T> { x: T }
type NumberTuple<T> = [T, number];Typechecking
Compile-time Typechecking
The compiler will emit errors for any type errors which can be detected ahead of time.
Runtime Typechecking
Certain operations will result in checked casts which must happen at runtime.
function foo(a: any) {
var b: number = a; // Checked cast inserted, will throw if 'a' is not a number.
}Type Inference
Variable Declarations
If the declarator has an annotation, it wins.
Otherwise the initializer is visited and its type becomes the declaration's type. Globals and catch variables are always any.
Reassignments to a let/var variable must obey the type inferred/declared at the variable's declaration.
Array and Tuple Literals
An array literal becomes a tuple or an array based on the constraint:
- Tuple constraint: each element is checked against the matching slot.
- Array constraint: each element is checked against
T, with checked casts inserted as needed. - No constraint specified: element types are unioned and the literal becomes
Array. - Empty literal, no constraint: Warns and assumes 'any'. Annotate the declaration for a proper type.
Tuples are never inferred without context — [1,"x"] alone is Array<number|string>, not a tuple.
Generic Calls
When a generic call omits type arguments, each type parameter is attempted to be inferred. Arguments are processed in two passes: first those whose parameter type is a bare type variable, then those with complex types like (T) => U from left to right. Inference succeeds only if every placeholder is resolved.
Matching is structural: same-kind containers (Array, Tuple, ExactObject, function types) recurse into their components, a union constraint tries each arm against a non-union concrete, etc.
Limitations:
- First-write wins:
[T, T]against[number, string]fixesT = number, and the second slot then fails the regular flow check. - Single forward pass: fixpoint iteration.
Type Refinement
Typed language doesn't have full proper type refinement support yet but it supports a partial implementation that allows for certain use cases by implicitly checking null or void at usage time.
function foo(a: number | void): number {
var b: number = a; // Typechecks at compile time, but will throw if 'a' is not a number.
return b;
}Decorations on Function Declarations
JS decorator syntax (@Hermes.foo) does not apply to function declarations, so the typed language uses a Hermes.decorate(...) call to attach decorations:
function charAt(this: string, pos: number): string { return this[pos]; }
Hermes.decorate(charAt, Hermes.builtin);- The call must appear as a statement immediately following the
FunctionDeclarationit decorates. - The first argument must be an identifier matching the preceding function's name.
- The second argument must be the decorator.
- Exactly two arguments are required.
JS Language Features
The typed language is a subset of JavaScript. Most common ES2015+ constructs work, with the notable exceptions listed under "Not yet supported" below.
Function Parameters
Function parameters can be typed, optional, defaulted, or destructured.
// Typed parameter.
function f(x: number): void {}
// Optional parameter (must come last). Inside the body, type is `T | void`.
function g(x: number, y?: string): void {}
// Default parameter.
function h(x: number = 1): number { return x; }
// Object destructuring parameter, with optional default.
function i({x, y}: {x: number, y: string} = {x: 0, y: ""}): void {}
// Array (tuple) destructuring parameter, including nested patterns.
function j([a, b]: [number, string]): void {}
function k({a: {b}}: {a: {b: number}}): void {}Destructuring
Destructuring is supported in variable declarations and parameters, including nested patterns and default values. Type annotations inside destructuring patterns are not allowed; annotate the whole pattern instead.
const [x, y]: [number, string] = [1, "abc"];
const {a, b}: {a: number, b: number} = obj;Spread in Array Literals
Spread of an array into an array literal is supported.
const xs: number[] = [1, 2, ...other];Overloading
Class methods can be overloaded using the @Hermes.overload decoration. Overloaded methods must be final.
class C {
@Hermes.final
@Hermes.overload
foo(x: number): string { return 'a'; }
@Hermes.final
@Hermes.overload
foo(x: string): void {}
}Overloads are resolved at compile time, and are not stored on the home object.
Both public and private methods may be overloaded, and an overload set may mix non-generic and generic overloads.
Restrictions:
- No overloading constructors/accessors
- Overloaded methods must be called (
var x = obj.overloadedMethodis an error)
Comparisons
Strict equality (=== and !==) is type-checked. An expression a === b (or a !== b) is legal only when one operand's type flows into the other's without a checked cast.
Loose equality (== and !=) is generally disallowed. To compare two values of the same type, use === / !==. It exists for the x == null / x == undefined idiom used to test nullability. Specifically, a == b is allowed only when either operand has type any, or one operand has type null or void and null or void flows into the other type.
Other Features
- Arrow functions (without type parameters).
for-ofloops over arrays.try/catch/throw(the catch binding is typedany).- Template literals.
- Classes with static members, private fields and methods (
#name),
inheritance,supercalls, andsuper.method()access. - Getters and setters on classes (must be marked
@Hermes.final). - Rest parameters in function declarations and function types
(function f(...args)).
Not supported (yet)
- Spread arguments in calls (
f(...args)). - Rest elements in destructuring (
{a, ...rest},[a, ...rest]). - Spread elements in tuple literals.
asyncmethods and generator methods.- Computed property names in class definitions.
- Optional call expressions (
f?.()). - Type parameters on function expressions and arrow functions.
Doc VM (`doc/VM.md`)
id: vm title: VM Overview
Value Representation
HermesValue
The VM uses a class called HermesValue to encapsulate JS values efficiently,
preserving their type while still allowing them to fit in a register.
NaN-tagging is used to store different types of values;
we store values in the lower bits of a uint64_t.
Thus, when the uint64_t is interpreted as a double,
tagged NaN values can hold non-double types.
After we reserve the canonical quiet NaN to be used as the NaN representation
in the VM, we have 51 remaining bits we can set. Even on 64-bit systems, we are
able to accommodate a full pointer and type tag, since pointers are never more
than 48 bits in practice.
A HermesValue can take on any of the following types of values, which are
distinguished by their type tag:
- Empty
- Undefined
- Null
- Boolean
- Symbol
- Native value (used to store an int or pointer for bookkeeping)
- String pointer
- Object pointer
HermesValue is used across the VM to store and pass JS values.PinnedHermesValue is used in non-moveable memory, primarily for the register
stack and other GC roots in known locations in memory. GCHermesValues are
used on the GC managed heap.
HermesValue32
When compressed pointers are enabled, we also encode some values in a compact
32-bit representation called HermesValue32. Because compressing pointers to
store in this format requires additional work, we also avoid usingHermesValue32 for frequently accessed values (like the register stack).
Instead we selectively use HermesValue32 for objects that are known to
consume a large percentage of heap memory, but that are unlikely to affect
performance.
HermesValue32 supports storing all of the types that HermesValue does,
except for native values, which were dropped because they are relatively rare
and because we do not have a mechanism for compressing native pointers outside
the GC managed heap.
Instead of using NaN boxing, it takes advantage of the 8-byte alignment of the
Hermes heap to store tags, since the lowest 3 bits of a pointer are always
guaranteed to be zero (even after compression).
HermesValue32 also requires special handling for doubles. It is able to store
small integers up to 29 bits inline, but anything that cannot be represented in
that form must be stored as a separate double object on the heap. Based on the
workloads we have looked at, actual doubles are very rarely used, so the
overhead of this approach is small.
Strings
StringPrimitive is used to store immutable UTF16 encoded strings,
and StringPrimitive * can be stored in HermesValue to make JS String values.
Internally, StringPrimitive can be
DynamicStringPrimitive(stored in the GC heap)ExternalStringPrimitive(stored as a pointer outside the VM, such as into a bytecode file)
Runtime
The Runtime class is the primary driver of the VM.
It contains the current environment and heap, as well as the code to execute.Runtime is used to execute RuntimeModules,
which are constructed from BytecodeModules using Runtime::runModule().
Runtime Module
A RuntimeModule is the VM representation into a bytecode file.RuntimeModules are stored outside the GC heap and are constructed via new.
To allow for segmentation of bytecode files and requireing modules between
separate segments, we collect RuntimeModules in a class called Domain.
You may think of the Domain as the collection of bytecode files which were
all compiled in the same invocation of the compiler.
Every JSFunction shares ownership of a Domain, and the Domain owns
the RuntimeModules which provide those functions. In this way, when allJSFunctions which require the files in a Domain are collected,
the Domain and the RuntimeModules are also collected.
Runtime Identifiers
The Runtime contains an IdentifierTable,
which is used for getting unique IDs for strings.
The table is used to go from StringPrimitive to SymbolID and back.
It's prepopulated with some "predefined strings",
the set of strings that are required by built in functions,
which can be seen in PredefinedStrings.def.
Garbage Collection
Currently, the VM uses HadesGC by default, a concurrent garbage collector aimed at dramatically lowering pause times over our previous collector GenGC. The heap in Hermes is non-contiguous, which allows us to avoid reserving large regions of address space upfront, and allows us to return memory to the OS at a finer granularity. Garbage collection in Hermes is precise, which means that the GC always knows which values contain valid pointers.
See the documentation for Hades for details
on how it works.
The garbage collector moves objects to different place on the heap,
invalidating HermesValues,
so there are a couple classes which allow updating them automatically.Handle<> and Handle<T> are garbage collector-aware handles;
they are moved if a collection occurs in between two successive accesses.
So, to ensure correctness in the VM,
use the handles instead of passing raw HermesValue between functions.
A GCScope is used to keep track of all the current HermesValue handles.
Any GCScope must be constructed on the stack,
whence it tracks any scoped handles that are used until it falls out of scope.
The GCScope allocates space in chunks,
and when it is destroyed (falls out of scope) it frees any chunks it allocated.
The GCScope is used to internally generate PinnedHermesValues,
which are then stored in Handle<> and Handle<T>.
We also provide PseudoHandle<T> classes which are explicitly not handles.
These are used to be explicit about storage of raw pointers and HermesValue.PseudoHandle should be used as an argument in place of a raw pointer to
functions which may want to turn that argument into a Handle,
but in which it's not necessary to always incur the cost of handle allocation.PseudoHandle also does not have a copy constructor,
and moving out of one invalidates it.
This prevents the reuse of PseudoHandle after an allocating function call.
Rules for GC-safe code
See GCSafeCoding.md for the complete guide to writing correct
and performant GC-safe C++ code, including the preferred Locals + PinnedValue
API, handle usage, error handling patterns, and common mistakes.
Object Model
Currently the object model is a VTable-based scheme,
in which all possible JS values inherit from a base garbage collector VTable.
These are called "cells", and all the cells are defined in CellKinds.def.
Objects have a special ObjectVTable, Callables have a CallableVTable, etc.
Objects
Each JS object is represented by Object (or a class derived from Object).
JS objects have a set of name/value pairs, and some optional "indexed storage".
Read more about how Object works in ObjectModel.h.
The Runtime contains a global object which is used to store in global scope.
Arrays
Arrays, the arguments object, etc. inherit from Object directly,
but simply provide their own implementations of *OwnIndexed using the VTable.
Functions
Functions and native functions inherit from Callable.
This allows them to call executeCall* to run functions using the internal API.
Boxed Primitives
The VM has classes used to contain Booleans, Strings, and Numbers,
when they are constructed using their respective JS constructors.JSString is a boxed String object, etc.
REPL
The HermesVM provides a REPL in bin/hermes,
which calls through to the eval() global function in the Runtime.
Doc Icu License (`doc/icu_license.txt`)
Some binary releases of Hermes may include binary copies of the International
Components for Unicode (ICU), either as separate libraries or statically linked
into executables.
COPYRIGHT AND PERMISSION NOTICE (ICU 58 and later)
Copyright © 1991-2019 Unicode, Inc. All rights reserved.
Distributed under the Terms of Use in https://www.unicode.org/copyright.html.
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Unicode data files and any associated documentation
(the "Data Files") or Unicode software and any associated documentation
(the "Software") to deal in the Data Files or Software
without restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, and/or sell copies of
the Data Files or Software, and to permit persons to whom the Data Files
or Software are furnished to do so, provided that either
(a) this copyright and permission notice appear with all copies
of the Data Files or Software, or
(b) this copyright and permission notice appear in associated
Documentation.
THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT OF THIRD PARTY RIGHTS.
IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS
NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL
DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THE DATA FILES OR SOFTWARE.
Except as contained in this notice, the name of a copyright holder
shall not be used in advertising or otherwise to promote the sale,
use or other dealings in these Data Files or Software without prior
written authorization of the copyright holder.
Third-Party Software Licenses
This section contains third-party software notices and/or additional
terms for licensed third-party software components included within ICU
libraries.
- ICU License - ICU 1.8.1 to ICU 57.1
COPYRIGHT AND PERMISSION NOTICE
Copyright (c) 1995-2016 International Business Machines Corporation and others
All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, and/or sell copies of the Software, and to permit persons
to whom the Software is furnished to do so, provided that the above
copyright notice(s) and this permission notice appear in all copies of
the Software and that both the above copyright notice(s) and this
permission notice appear in supporting documentation.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
HOLDERS INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY
SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER
RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
Except as contained in this notice, the name of a copyright holder
shall not be used in advertising or otherwise to promote the sale, use
or other dealings in this Software without prior written authorization
of the copyright holder.
All trademarks and registered trademarks mentioned herein are the
property of their respective owners.
- Chinese/Japanese Word Break Dictionary Data (cjdict.txt)
The Google Chrome software developed by Google is licensed under
the BSD license. Other software included in this distribution is
provided under other licenses, as set forth below.
The BSD License
http://opensource.org/licenses/bsd-license.php
Copyright (C) 2006-2008, Google Inc.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided with
the distribution.
Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
The word list in cjdict.txt are generated by combining three word lists
listed below with further processing for compound word breaking. The
frequency is generated with an iterative training against Google web
corpora.
* Libtabe (Chinese)
- https://sourceforge.net/project/?group_id=1519
- Its license terms and conditions are shown below.
* IPADIC (Japanese)
- http://chasen.aist-nara.ac.jp/chasen/distribution.html
- Its license terms and conditions are shown below.
---------COPYING.libtabe ---- BEGIN--------------------
/*
* Copyright (c) 1999 TaBE Project.
* Copyright (c) 1999 Pai-Hsiang Hsiao.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* . Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* . Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
* . Neither the name of the TaBE Project nor the names of its
* contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
* OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/*
* Copyright (c) 1999 Computer Systems and Communication Lab,
* Institute of Information Science, Academia
* Sinica. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* . Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* . Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
* . Neither the name of the Computer Systems and Communication Lab
* nor the names of its contributors may be used to endorse or
* promote products derived from this software without specific
* prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
* OF THE POSSIBILITY OF SUCH DAMAGE.
*/
Copyright 1996 Chih-Hao Tsai @ Beckman Institute,
University of Illinois
[email protected] http://casper.beckman.uiuc.edu/~c-tsai4
---------------COPYING.libtabe-----END--------------------------------
---------------COPYING.ipadic-----BEGIN-------------------------------
Copyright 2000, 2001, 2002, 2003 Nara Institute of Science
and Technology. All Rights Reserved.
Use, reproduction, and distribution of this software is permitted.
Any copy of this software, whether in its original form or modified,
must include both the above copyright notice and the following
paragraphs.
Nara Institute of Science and Technology (NAIST),
the copyright holders, disclaims all warranties with regard to this
software, including all implied warranties of merchantability and
fitness, in no event shall NAIST be liable for
any special, indirect or consequential damages or any damages
whatsoever resulting from loss of use, data or profits, whether in an
action of contract, negligence or other tortuous action, arising out
of or in connection with the use or performance of this software.
A large portion of the dictionary entries
originate from ICOT Free Software. The following conditions for ICOT
Free Software applies to the current dictionary as well.
Each User may also freely distribute the Program, whether in its
original form or modified, to any third party or parties, PROVIDED
that the provisions of Section 3 ("NO WARRANTY") will ALWAYS appear
on, or be attached to, the Program, which is distributed substantially
in the same form as set out herein and that such intended
distribution, if actually made, will neither violate or otherwise
contravene any of the laws and regulations of the countries having
jurisdiction over the User or the intended distribution itself.
NO WARRANTY
The program was produced on an experimental basis in the course of the
research and development conducted during the project and is provided
to users as so produced on an experimental basis. Accordingly, the
program is provided without any warranty whatsoever, whether express,
implied, statutory or otherwise. The term "warranty" used herein
includes, but is not limited to, any warranty of the quality,
performance, merchantability and fitness for a particular purpose of
the program and the nonexistence of any infringement or violation of
any right of any third party.
Each user of the program will agree and understand, and be deemed to
have agreed and understood, that there is no warranty whatsoever for
the program and, accordingly, the entire risk arising from or
otherwise connected with the program is assumed by the user.
Therefore, neither ICOT, the copyright holder, or any other
organization that participated in or was otherwise related to the
development of the program and their respective officials, directors,
officers and other employees shall be held liable for any and all
damages, including, without limitation, general, special, incidental
and consequential damages, arising out of or otherwise in connection
with the use or inability to use the program or any product, material
or result produced or otherwise obtained by using the program,
regardless of whether they have been advised of, or otherwise had
knowledge of, the possibility of such damages at any time during the
project or thereafter. Each user will be deemed to have agreed to the
foregoing by his or her commencement of use of the program. The term
"use" as used herein includes, but is not limited to, the use,
modification, copying and distribution of the program and the
production of secondary products from the program.
In the case where the program, whether in its original form or
modified, was distributed or delivered to or received by a user from
any person, organization or entity other than ICOT, unless it makes or
grants independently of ICOT any specific warranty to the user in
writing, such person, organization or entity, will also be exempted
from and not be held liable to the user for any such damages as noted
above as far as the program is concerned.
---------------COPYING.ipadic-----END----------------------------------
- Lao Word Break Dictionary Data (laodict.txt)
Copyright (c) 2013 International Business Machines Corporation
and others. All Rights Reserved.
Project: http://code.google.com/p/lao-dictionary/
Dictionary: http://lao-dictionary.googlecode.com/git/Lao-Dictionary.txt
License: http://lao-dictionary.googlecode.com/git/Lao-Dictionary-LICENSE.txt
(copied below)
This file is derived from the above dictionary, with slight
modifications.
----------------------------------------------------------------------
Copyright (C) 2013 Brian Eugene Wilson, Robert Martin Campbell.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification,
are permitted provided that the following conditions are met:
Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer. Redistributions in
binary form must reproduce the above copyright notice, this list of
conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------
- Burmese Word Break Dictionary Data (burmesedict.txt)
Copyright (c) 2014 International Business Machines Corporation
and others. All Rights Reserved.
This list is part of a project hosted at:
github.com/kanyawtech/myanmar-karen-word-lists
--------------------------------------------------------------------------
Copyright (c) 2013, LeRoy Benjamin Sharon
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met: Redistributions of source code must retain the above
copyright notice, this list of conditions and the following
disclaimer. Redistributions in binary form must reproduce the
above copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
Neither the name Myanmar Karen Word Lists, nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
--------------------------------------------------------------------------
- Time Zone Database
ICU uses the public domain data and code derived from Time Zone
Database for its time zone support. The ownership of the TZ database
is explained in BCP 175: Procedure for Maintaining the Time Zone
Database section 7.
7. Database Ownership
The TZ database itself is not an IETF Contribution or an IETF
document. Rather it is a pre-existing and regularly updated work
that is in the public domain, and is intended to remain in the
public domain. Therefore, BCPs 78 [RFC5378] and 79 [RFC3979] do
not apply to the TZ Database or contributions that individuals make
to it. Should any claims be made and substantiated against the TZ
Database, the organization that is providing the IANA
Considerations defined in this RFC, under the memorandum of
understanding with the IETF, currently ICANN, may act in accordance
with all competent court orders. No ownership claims will be made
by ICANN or the IETF Trust on the database or the code. Any person
making a contribution to the database or code waives all rights to
future claims in that contribution or in the TZ Database.
- Google double-conversion
Copyright 2006-2011, the V8 project authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Doc Typescript Stripping (`doc/typescript-stripping.md`)
TypeScript Type Stripping
Hermes supports stripping erasable TypeScript syntax from source files via the--transform-ts flag, similar to Node.js's --strip-types. This allows
running TypeScript files directly without a separate transpilation step, as long
as the code uses only erasable type syntax.
Usage
# Run a TypeScript file directly
hermes --transform-ts file.ts
# Compile to bytecode
hermesc --transform-ts -emit-binary -out file.hbc file.ts
# With shermes
shermes --transform-ts file.tsThe --transform-ts flag implies --parse-ts (TypeScript parsing is enabled
automatically).
Supported Erasable Syntax
The following TypeScript constructs are erased (removed or unwrapped) during
the AST transformation pass:
- Type annotations on variables, parameters, and return types
(let x: number,function f(a: string): void) - Type aliases (
type Foo = string) - Interfaces (
interface Bar { ... }) - Generic type parameters (
function f<T>(x: T): T,class C<T>) - Type assertions (
x as T,<T>x) - Type-only imports (
import type { Foo } from "bar") - Type-only exports (
export type { Foo }) implementsclauses on classes (class Foo implements Bar)- Type arguments on call/new expressions (
foo<T>(x),new C<T>()) - TS modifiers on class properties (
readonly, access modifiers when used
only as type-level annotations on properties) superTypeArgumentson classes (class Foo extends Bar<T>)
Unsupported Non-Erasable Syntax
The following TypeScript constructs have runtime semantics and cannot be
simply erased. Using them with --transform-ts produces an error:
- Enums (
enum Color { Red, Green, Blue }) - Namespaces / modules (
namespace Foo { ... }) - Parameter properties (
constructor(public x: number))
Restrictions
--transform-tsis incompatible with--typed(typed mode). Using both
together produces an error.
How It Works
The stripping pass runs as an AST transformation before semantic analysis,
in the same pipeline slot as the async generator transform. It operates by:
- Walking the AST using the recursive visitor framework.
- Nulling out type annotation fields on nodes (identifiers, functions,
classes, patterns, call expressions). - Removing type-only declaration nodes from statement lists (type aliases,
interfaces, type-only imports/exports). - Unwrapping type assertion expressions (
x as Tbecomesx). - Reporting errors for non-erasable constructs.
Deep TS type nodes (e.g., TSAnyKeyword, TSFunctionType, TSUnionType)
only appear as descendants of type annotation fields. Nulling those fields at
the parent level makes them unreachable, so no explicit handlers are needed.
Comparison with Node.js
| Feature | Hermes --transform-ts |
Node.js --strip-types |
|---|---|---|
| Type annotations | Stripped | Stripped |
| Interfaces / type aliases | Stripped | Stripped |
| Enums | Error | Error (unless --experimental-transform-types) |
| Namespaces | Error | Error (unless --experimental-transform-types) |
| Parameter properties | Error | Error (unless --experimental-transform-types) |
import type |
Stripped | Stripped |
Per-specifier import { type X } |
Not yet supported | Stripped |
Future Work
- Per-specifier type import stripping (
import { type X, Y } from "bar") - Enum lowering (transform enums to JavaScript objects)
- Namespace lowering (transform namespaces to IIFEs)
Doc Blog 2024 11 09 Octane Benchmark Jit Progress (`doc/blog/2024-11-09-octane-benchmark-jit-progress.md`)
Octane Benchmark Results: Hermes JIT Progress
November 9, 2024 · tmikov
In parallel with typed JS, we have also been making progress on untyped (legacy) JS performance.
These are Octane benchmark results comparing original Hermes, the static_h branch, and static_h branch with JIT. I also added QuickJS for reference. The TypeScript benchmark was removed because it is too high.
The benchmarks were run on Raspberry Pi 3 and Pi 5 to encompass a wide range of performance.
This is all untyped, unannotated JS, not using the native backend.
To be clear, Octane is not representative these days, but we can't have good performance in more important cases if we completely suck at Octane. Higher scores are better.
Doc Blog 2024 12 19 Static Hermes Update Dec 2024 (`doc/blog/2024-12-19-static-hermes-update-dec-2024.md`)
Static Hermes Update, December 2024
December 19, 2024 · tmikov
Throughout much of 2024, the Hermes team has been focused on improving both the performance and ECMAScript compliance of untyped JavaScript. While the primary goal of the Static Hermes project is to achieve native-like performance with typed JavaScript, we recognize that a significant amount of untyped code must also execute correctly and efficiently.
This post highlights some of the key improvements we've made in the static_h branch of Hermes. The static_h branch supports both typed and untyped compilation and execution. We are working toward making the static_h branch opt-in initially, and later the default React Native engine next year.
Major Advances in ECMAScript Compliance
Full spec-compliant implementation of ES6 classes. This does not include private fields and static blocks, but support is already under way.
Our classes implementation is ~2.3x faster at constructing instances of a class, and ~4.5x faster in super invocations compared to Babel's strict transform. Additionally, we are ~1.35x and ~1.15x, respectively, faster than Babel's loose transform, all while remaining fully spec-compliant.
Full block scoping support for let, const and Temporal Dead Zone (TDZ), with an option to disable TDZ for increased performance.
These features have been a long time coming and represent a significant step forward in closing the gap in Hermes' functionality—though this gap has largely been mitigated by React Native's Babel pipeline. Now, we are finally here.
Untyped JS Performance Wins
The table below shows the "times x" performance improvements in Octane benchmarks of the static_h branch compared to the main Hermes branch. The first column represents performance with the regular interpreter, while the second column shows results with JIT enabled.
The last three benchmarks are custom additions by our team. These benchmarks highlight, among other things, the dramatic performance improvements of the JIT when more type information is available.
In the InterpDispatch2 and Mandelbrot benchmarks, our compiler was able to statically infer many types, resulting in significant speed-ups. With typed JavaScript, these numbers are expected to climb even higher.
| Benchmark | Normal | JIT |
|---|---|---|
| Richards | 1.24 | 1.78 |
| DeltaBlue | 1.29 | 1.86 |
| Crypto | 1.34 | 1.91 |
| RayTrace | 1.72 | 2.07 |
| EarleyBoyer | 1.49 | 1.71 |
| RegExp | 1.20 | 1.20 |
| Splay | 1.22 | 1.33 |
| SplayLatency | 1.09 | 1.15 |
| NavierStokes | 1.50 | 1.87 |
| PdfJS | 1.68 | 1.78 |
| Mandreel | 1.38 | 1.69 |
| MandreelLatency | 1.36 | 1.42 |
| Gameboy | 1.36 | 1.68 |
| Box2D | 1.41 | 1.93 |
| zlib | 1.65 | 2.04 |
| Typescript | 1.28 | 1.46 |
| InterpDispatch | 1.66 | 2.97 |
| InterpDispatch2 | 1.58 | 7.90 |
| Mandelbrot | 1.25 | 3.29 |
Doc Blog 2024 12 23 Compiling Javascript To Wasm (`doc/blog/2024-12-23-compiling-javascript-to-wasm.md`)
Compiling Full-Featured JavaScript to Wasm
December 23, 2024 · tmikov
Compiling JavaScript to Wasm ahead of time has always been a fascinating topic. However, to my knowledge, there is no close-to-production-quality solution currently capable of statically compiling the full ES6 JavaScript language to Wasm without relying on an interpreter.
I would like to demonstrate how easily Static Hermes enables us to do precisely that.
This article has been a long time coming, since I have posted more than once that Static Hermes can produce Wasm, but I never actually had the time to sit down, write the instructions, and fix a couple of minor problems that appeared.
NOTE: Porffor definitely deserves a mention here: it is an exciting emerging project exploring this space while taking a different approach. While it is not production quality yet, it shows real promise.
But Why Do It at All?
Compiling JS to Wasm, while perhaps not a game-changer for everyday JavaScript execution, opens up a world of intriguing possibilities. For example:
- It provides sandbox isolation.
- It allows Wasm hosts that lack a JS engine to still run JavaScript.
- It allows standalone applications to embed the runtime, built-ins, and compiled code into a single Wasm binary.
What is Static Hermes (and Hermes)
Hermes is an optimized JavaScript engine designed to execute JavaScript efficiently on resource-constrained devices, particularly phones. It is the default JS engine of React Native, a cross-platform mobile development framework. Hermes has been deployed to hundreds of millions of devices, including desktop computers and servers.
Static Hermes (which lives in the static_h branch) is the code name for the next generation of Hermes, which, among other things, focuses on optional sound typing and optional ahead-of-time (AOT) compilation of JavaScript to native code.
The native compilation aspect of Static Hermes is what makes it particularly suitable to the task at hand - compiling JavaScript to Wasm.
Static Hermes already supports compiling the full JavaScript language to native code. By leveraging Clang and LLVM for code generation, it can treat WebAssembly (Wasm) as just another compiler target. This approach allows Static Hermes to compile the complete ES6 JavaScript language into Wasm while preserving the language's flexibility and dynamic nature.
The resulting Wasm binaries are highly comprehensive and contain:
- Compiled JavaScript: The input JavaScript translated directly into Wasm instructions for efficient execution, without the JS interpreter. We will look at a detailed example later in this article.
- JavaScript Library: All standard built-ins, such as Object, Array, and Math, are included to ensure compatibility.
- Embedded Interpreter: Dynamic features like
eval()andnew Function()are supported through an integrated interpreter, activated only when required.
In the next section, we will see how to build Static Hermes and use it to compile JavaScript to Wasm and how to run the resulting Wasm binaries.
Getting Started with Static Hermes and Wasm
For those interested in exploring Static Hermes for compiling JavaScript to WebAssembly, the process is straightforward and involves a few key steps. Rather than detailing them all here, I have created a guide to walk through:
- Setting up the development environment with Emscripten and the static_h branch of Hermes.
- Building the Hermes JSVM for your host platform and for Wasm.
- Running simple JavaScript applications and benchmarks using the Hermes VM Wasm binary. This involves running the Hermes VM in Wasm mode, which functions as an interpreter.
The final step, compiling JavaScript applications to independent Wasm binaries, highlights Static Hermes's unique capabilities. This process produces a fully self-contained Wasm binary.
The guide includes the necessary commands, configurations, and options to begin experimenting with Static Hermes and Wasm. It can be found here: doc/Emscripten.md.
If you find inaccuracies in the guide, or have ideas how to improve it, PRs and comments are welcome.
Please, keep in mind that while the Static Hermes interpreter has already been shipped in production, the native compilation functionality is still considered experimental (but is not expected to have major bugs).
Exploring the Generated Wasm Code
To understand the results of Static Hermes's compilation, let's take a closer look at a simple example. The following JavaScript function computes the sum of integers from 0 to a given number to:
function sum(to) {
to = +to;
let sum = 0;
for(let i = 0; i < to; ++i)
sum += i;
return sum;
}The statement to = +to is interesting. It ensures that to is explicitly converted to a number, enabling us to infer types throughout the function. Type inference allows Static Hermes to optimize the generated Wasm code by treating to, i, and sum as numbers consistently, avoiding the overhead of type checks and conversions during execution.
NOTE: The example would work without that, but would be slower and much more verbose.
After compilation with Static Hermes, this function is transformed into WebAssembly (Wasm) code. The assembly listing is too long to reproduce here, but we can examine the most important part - the loop.
loop ;; label = @2
local.get 5 ;; Get the current sum
local.get 6 ;; Get the current value of `i`
f64.add ;; Add `i` to the sum
local.set 6 ;; Store the result back into `sum`
local.get 5 ;; Increment `i`
f64.const 0x1p+0 ;; Load the constant 1 (as a floating point)
f64.add ;; Add 1 to `i`
local.tee 5 ;; Store the incremented value into `i` and leave it on the stack
local.get 4 ;; Get the upper bound `to`
f64.lt ;; Compare `i` with `to`
br_if 0 (;@2;) ;; If `i >= to`, break
endThis Wasm loop corresponds directly to the JavaScript for loop. Key operations such as updating the sum, incrementing the loop counter, and checking the loop condition are reflected in the Wasm instructions:
local.getandlocal.setmanage local variables likesumandi.f64.addhandles floating-point addition, used here to incrementiand updatesum.f64.ltcomparesito the loop limitto.br_ifprovides the conditional branching mechanism to terminate the loop.
Observations
- Explicitness: The Wasm representation is verbose, reflecting its low-level nature. Each operation (e.g., addition, comparison) is explicitly represented, enabling precise control over execution.
- Efficiency: The use of floating-point instructions (
f64.*) reflects the type inference the compiler is able to perform, turning dynamic typing into static. - Direct Mapping: The loop in the JavaScript code maps fairly cleanly to the Wasm code. This is not always the case with more complex code and optimizations, but is fun to see.
Advanced JavaScript
I have prepared a small script, demo.js, demonstrating some more advanced JavaScript features, including closures, TDZ behavior, and prototype manipulation, which are faithfully supported by Static Hermes when compiled to Wasm.
Compile and Run demo.js
Assuming you have just successfully completed the "Getting Started" instructions, make sure demo.js is in the current directory and compile it.
${HermesSourcePath?}/utils/wasm-compile.sh build-host build-wasm demo.jsThe command should output something like:
Using shermes to compile demo.js... to demo.c
Using emcc to compile demo.c to demo.o
Using emcc to link demo.o to demo-wasm.js/.wasm
-rw-r--r-- 1 user group 73K Dec 23 17:35 demo-wasm.js
-rwxr-xr-x 1 user group 2.6M Dec 23 17:35 demo-wasm.wasmNow we can run the result and enjoy the awesome power of Wasm!
node ./demo-wasm.jsClosures
=========
Generating 5 increments with default step size:
1
... and so onThe following sections provide a brief tour of demo.js, showcasing that Static Hermes handles various JavaScript features.
Closures and Generators
function createCounterWithGenerator() {
let count = 0; // Shared mutable variable
// Default parameter.
const increment = (by = 1) => count += by;
// Generator captures a peer closure (increment).
function* doSteps(steps) {
for (let i = 0; i < steps; i++)
yield increment();
}
// Shorthand object literal.
return {
increment,
doSteps
};
}
const counter = createCounterWithGenerator();
// Tagged template literal
console.log(`Generating ${steps} increments with default step size:`);
// For-of with a generator
for (const value of counter.doSteps(steps))
console.log(value);Temporal Dead Zone (TDZ)
The show_tdz() function illustrates TDZ errors arising from improper use of let.
function show_tdz() {
function getval() {
return val;
}
// Reading val before it is initialized.
let val = getval() + 1;
}
console.log("\nTDZ\n=========");
try {
show_tdz();
} catch (e) {
console.log("TDZ Error!", e.stack);
}Prototypical Inheritance
The example uses prototype chains and dynamic getters, showcasing inheritance and dynamic property manipulation.
const prototypeObj = {
first: "I am in the prototype"
};
const obj = {
get second() {
// Add and increment the `third` property
if (!this.third) {
this.third = 1; // Initialize if it doesn't exist
} else {
this.third++;
}
return `Getter executed, third is now ${this.third}`;
},
__proto__: prototypeObj // Set prototype using object literal syntax
};
console.log("First property:", obj.first); // Inherited from prototype
console.log("Second property:", obj.second); // Triggers the getter
console.log("Third property:", obj.third); // Dynamically added and incremented
console.log("Second property again:", obj.second); // Getter increments third
console.log("Third property now:", obj.third); // Reflects incremented valueES6 Classes
Demonstrates inheritance and property handling with ES6 classes.
class PrototypeClass {
constructor() {}
// Define `first` as a getter in the prototype
get first() {
return "I am in the prototype";
}
}
class DerivedClass extends PrototypeClass {
constructor() {
super();
}
get second() {
// Add and increment the `third` property
if (!this.third) {
this.third = 1; // Initialize if it doesn't exist
} else {
this.third++;
}
return `Getter executed, third is now ${this.third}`;
}
}
const clInst = new DerivedClass();
console.log("First property:", clInst.first); // Inherited from PrototypeClass
console.log("Second property:", clInst.second); // Triggers the getter
console.log("Third property:", clInst.third); // Dynamically added and incremented
console.log("Second property again:", clInst.second); // Getter increments third
console.log("Third property now:", clInst.third); // Reflects incremented valueConclusion
Static Hermes is an ambitious project that pushes the boundaries of what JavaScript can do. The ability to compile the full ES6 standard to Wasm, while preserving JavaScript's flexibility, opens up some fascinating possibilities.
If this kind of thing interests you, check out the Static Hermes source code. Try it out, poke around, and feel free to ask questions. Contributions and discussions are always welcome.
Doc Blog 2025 06 09 Jsi Runtime Data Apis (`doc/blog/2025-06-09-jsi-runtime-data-apis.md`)
Introducing JSI's New Runtime Data APIs
June 9, 2025 · tmikov
We noticed a common pattern emerging in React Native (RN) libraries - a map from jsi::Runtime * to some per-runtime data. This approach has proven clumsy and problematic, because of unclear map ownership and lifetime. To address this, we recently introduced two new methods to jsi::Runtime: setRuntimeData() and getRuntimeData().
/// Stores the pointer \p data with the \p uuid in the runtime. This can be
/// used to store some custom data within the runtime. When the runtime is
/// destroyed, or if an entry at an existing key is overwritten, the runtime
/// will release its ownership of the held object.
void setRuntimeData(const UUID& uuid, const std::shared_ptr<void>& data);
/// Returns the data associated with the \p uuid in the runtime. If there's no
/// data associated with the uuid, return a null pointer.
std::shared_ptr<void> getRuntimeData(const UUID& uuid);The setRuntimeData() method allows storage of a std::shared_ptr<void> associated with a UUID, with the runtime managing ownership and releasing it upon destruction or overwrite. The getRuntimeData() method retrieves this data for a given UUID, returning a null pointer if none exists. This approach eliminates the need for custom maps and eliminates any uncertainty about ownership and lifetime.
An immediate practical application would be storing cached instances of jsi::PropNameID, which are runtime-specific uniqued strings.
Why UUIDs? UUIDs are 128-bit values designed to be globally unique, even without central coordination. They are necessary here to avoid conflicts between independent libraries using the same runtime. You could use one for all your per-runtime data, or multiple for different things - it is up to you.
Doc Blog 2025 07 15 Static H Performance June 2025 (`doc/blog/2025-07-15-static-h-performance-june-2025.md`)
Static_h Branch Performance: June 2025 Update
July 15, 2025 · tmikov
static_h branch performance at end of June 2025, relative to Hermes and QuickJS. Numbers are relative to QuickJS (1.2 means 1.2x faster); higher is better.
The measured Hermes v2 configuration is hv32+jit.
- hv32 means the runtime uses compressed pointers and boxed doubles, when they don't fit in a 32-bit tagged value.
- jit means JIT 😅
QuickJS is used as a performance reference, since it is high quality, small, and spec compliant.
static_h branch hv32+interpreter performance, relative to QuickJS and Hermes (higher is better).
Doc Blog 2025 11 02 Hermes Compilation Runtime Modes (`doc/blog/2025-11-02-hermes-compilation-runtime-modes.md`)
Hermes Compilation and Runtime Modes
November 2, 2025 · tmikov
Hermes has the following compilation "modes":
- AOT compilation of typed or untyped JS to bytecode
- AOT compilation of typed or untyped JS to native code
Plus the following runtime modes:
- Native execution
- Bytecode interpreter
- Lazy compilation (source to bytecode) of each function the first time it is called
- Baseline JIT (bytecode to native) of frequently executed functions
All of these modes can be mixed and exist in the same runtime simultaneously. An app could use all of these:
- Some code compiled to native
- Some code compiled to bytecode (to save space)
- Some source (perhaps generated at runtime)
If the JIT is enabled, parts of (2) and (3) will be JIT-ed to native if they become hot enough.
Doc Blog 2025 11 06 Hermes Memory Modes (`doc/blog/2025-11-06-hermes-memory-modes.md`)
Hermes Memory Modes
November 6, 2025 · tmikov
Hermes has the following memory modes:
HV64 (all platforms)
This is usually the simplest mode - all JS values, pointers, and numbers are encoded as 64-bit values using NaN-boxing.
HV32 on 32-bit devices
JS values and pointers are encoded as 32-bit values. Some numbers are encoded as 32-bit values. The ones that do not fit in 32-bit are boxed in the heap. Fortunately, in most apps this is a really small percentage.
This mode has a small overhead, but it saves a lot of memory, obviously, and can be a performance win because of smaller cache footprint.
HV32 for 64-bit platforms (except iOS)
JS values and pointers are encoded as 32-bit values. All memory is allocated in a (up to) 4GB reserved contiguous memory block and all pointers are 32-bit offsets inside it.
This mode similarly has some overhead but saves a lot of memory, and can be a performance win because of smaller cache footprint.
HV32 for iOS (64-bit)
iOS is special, since it doesn't by default allow an app to allocate a lot of virtual address space (without a special entitlement). So, Hermes allocates memory in 4MB segments. Each segment can be anywhere in the 64-bit address space, so there is no way to directly encode a 64-bit address in a 32-bit pointer.
So, Hermes maintains a table with one entry per segment. 32-bit pointers are a combination of index in the table and offset in the segment (I am simplifying a bit, but the principle remains).
This mode also saves a lot of memory like the other HV32 ones, but has more overhead. iOS devices usually are faster, so it is not as visible.
As you can see, Hermes is very configurable. A lot of work has gone into supporting different kinds of systems.
With Hermes V1, by default we are going to ship HV32 on Android devices and HV64 on iOS devices in order to get best performance from the latter.
Doc Blog 2025 11 24 Json Parsing Performance (`doc/blog/2025-11-24-json-parsing-performance.md`)
JSON Parsing Performance: 2.7x-3.4x Faster
November 24, 2025 · tmikov
Big news for Hermes JSON parsing. Michael from our team worked on speeding it up for the last month - the result is between 2.7x-3.4x faster! It is available in the static_h branch now and will be included in the next stable releases.
Incomplete list of improvements:
- Switch to an iterative parser
- Cache object hidden classes
- Optimize string parsing
- Use @lemire's fast_float
- Add an even faster number path for integers
- Avoid unnecessary allocations
- Use a per-character lookup and function dispatch table
Coming up next: JSON.stringify().
Doc Blog 2025 12 02 Iserialization (`doc/blog/2025-12-02-iserialization.md`)
ISerialization: Efficient Binary Encoding for JS Values
December 2, 2025 · tmikov
ISerialization just landed in the static_h branch and will be out in the next stable release.
ISerialization encodes a JS value into an efficient opaque binary representation, which can then be de-serialized (deeply cloned) in one or more runtime instances.
It also optionally supports serialization with ownership transfer.
It is intended for efficient passing of values between runtimes and will be the base of message passing in our implementation of Web Workers.
Doc Blog 2025 12 12 New Way To Contribute (`doc/blog/2025-12-12-new-way-to-contribute.md`)
New Way to Contribute to Hermes
December 12, 2025 · tmikov
Contributing to Hermes has been... let's say "challenging." I've been thinking about how to fix that.
The Problem
The core problem is that Hermes contributions are written against internal APIs. Those APIs are designed for efficiency, not ergonomics, and they're genuinely hard to use correctly. Mistakes can easily lead to serious memory errors - crashes, corruption, security issues. Getting code to production quality against these APIs takes a lot of back-and-forth.
Internal APIs are also unstable - they change all the time as we optimize and refactor. So when we accept a PR, we're not just accepting the code, we're accepting responsibility for maintaining it forever. Every internal API change means we have to update that code too.
And then there's the domain expertise problem. A contribution that implements Intl or the Streams API requires reviewers to understand those specs deeply enough to verify correctness. That expertise often doesn't exist on the team, so either we spend a lot of time building it, or we're not confident in what we're shipping.
The result: PRs sit for months, we ask for endless revisions, and sometimes we just have to say no to perfectly reasonable code because the long-term cost is too high. That sucks for everyone.
A Different Path: JSI Extensions
Hermes extensions can now be written using JSI - the stable JavaScript Interface API - instead of internal APIs. JSI is designed to be safe and easy to use. And because it doesn't change, code won't break when we refactor Hermes internals.
We've already migrated TextEncoder from internal APIs to JSI as the first real extension - it's a good example of how this works in practice.
Two Tiers
There are two tiers of JSI extensions. Core extensions live in extensions/ and are fully maintained by the Hermes team. Community contributions live in extensions/contrib/.
For contrib, our commitment is limited: we guarantee that extensions will continue to build and pass the tests they were submitted with, but that's it. Bug reports and support come from the community, not from us.
Because the Hermes team isn't taking on maintenance burden, the review process for contrib is lighter. We focus on structural review - is this good C++, is it using JSI correctly, is the code reasonable? But we're not doing functional review - we're not verifying that an Intl implementation handles every locale correctly, or that a Streams implementation matches the WHATWG spec. We trust contributors to get that right; that's what their tests are for.
Graduation
If a contrib extension proves itself - gets popular, stays stable, fills a real need - it can graduate to core. That doesn't mean rewriting against internal APIs; it means the extension moves out of contrib and the Hermes team takes over maintenance and support.
How It Works: TextEncoder Under the Hood
The JavaScript side handles the stuff that's natural in JS - setting up the constructor, defining the prototype, installing on globalThis:
extensions.TextEncoder = function(nativeInit, nativeEncode, nativeEncodeInto) {
function TextEncoder() {
if (!new.target) {
throw new TypeError('TextEncoder must be called as a constructor');
}
nativeInit(this);
}
TextEncoder.prototype.encode = nativeEncode;
TextEncoder.prototype.encodeInto = nativeEncodeInto;
// ... property definitions ...
globalThis.TextEncoder = TextEncoder;
};The C++ side provides the native helper functions that do the actual work - in this case, UTF-8 encoding. These get passed into the JS setup function, which wires them onto the prototype.
This split plays to each language's strengths. JS is great for prototype plumbing and property definitions. C++ handles the performance-critical byte manipulation. And because everything goes through JSI, the C++ side is straightforward and safe - no manual GC interaction, no risk of dangling handles.
The old internal-API implementation of TextEncoder was about 340 lines of intricate VM code. The JSI version is simpler, easier to review, and won't break when we refactor Hermes internals.
Come Build Something
Docs and a working example are up now. If you've ever wanted to add something to Hermes but got discouraged by the process, this is the path.
Doc Blog 2026 06 05 New Hermes Stable Release (`doc/blog/2026-06-05-new-hermes-stable-release.md`)
What's New in the Next Stable Release 260318099.0.0
June 5, 2026 · zhaogang
A roundup of the JavaScript features, performance wins, and fixes that have landed in the static_h branch and will be shipped in the next Hermes stable release 260318099.0.0.
New Supported Features
- Set operations:
union,intersection,difference,symmetricDifference,isSubsetOf,isSupersetOf,isDisjointFrom. - Grouping:
Object.groupBy()andMap.groupBy(). - Iterator Helpers:
.map(),.filter(),.take(),.drop(),.flatMap(),.reduce(), and more directly on iterators, plusIterator.from(). - Async iteration: ,
for await...of(withSymbol.asyncIterator) and async generator (not tested in production, don't use except for testing). Promise.withResolvers()— create a promise together with itsresolve/rejectfunctions, without the executor boilerplate.FinalizationRegistry, andWeakRef/WeakMap/WeakSetnow accept symbols.TextDecodernow ships with the engine (no polyfill needed).- Strings:
String.prototype.isWellFormed()andtoWellFormed(). - More:
Error.isError(),Math.sumPrecise(),ArrayBuffer.prototype.detached, and BigInt keys in object literals.
TypeScript
- Strip type-only TypeScript syntax directly with the
--transform-tsflag (type erasure, similar to Node.js's--strip-types) — no separate Babel pass required.
Bug fixes
A batch of correctness and stability fixes also landed, including:
- TypedArray and DataView access on detached
ArrayBuffers Array.prototype.sortandtoSplicedon very large arrays- RegExp
/iuUnicode case-folding Proxyinvariant checksSymbol.descriptionspec compliance
Faster
JSON.parse/JSON.stringifyare significantly faster — see JSON Parsing Performance: 2.7x-3.4x Faster.MapandSetare faster — the backing hash table (OrderedHashMap) was reworked around a contiguous data table, with the index moved off the GC heap, for quicker iteration and insertion and less GC overhead.- Faster object/property access and array operations, plus expanded JIT coverage.
Also
- Unicode updated to 17.0 — newer emoji and scripts across string and RegExp APIs.
- Larger heaps: the GC can now manage more than 4 GB on 64-bit devices with HV64, lifting the old ~4 GB ceiling for memory-heavy workloads. (The 32-bit compressed-pointer configuration stays bounded by design.)
JSI updates for native modules
- Runtime data APIs —
setRuntimeData()/getRuntimeData()give native modules per-runtime storage without fragileRuntime*-to-data maps. See Introducing JSI's New Runtime Data APIs. ISerialization— efficient binary encoding of JS values for passing between runtimes, including serialize-with-transfer and external (zero-copy)ArrayBufferdata. See ISerialization: Efficient Binary Encoding for JS Values.
Bytecode version changed (98 → 99)
We bumped the bytecode version to 99. So if you use prebuilt bytecode, you'll need to rebuild it with the new version of Hermes.
Doc Plans Ir Type Flow Type System (`doc/plans/ir-type/flow-type-system.md`)
Flow Type System (FlowContext)
Overview
The Flow type system in Hermes is a full semantic type system used during static
analysis and type checking. Unlike the IR bitmask type system (13 bits, 2 bytes),
it supports nominal classes, generics, function signatures, tuples, exact objects,
union types with recursive structures, and subtyping with variance rules.
Key files:
include/hermes/Sema/FlowContext.h— all type class definitionslib/Sema/FlowContext.cpp— comparison, hashing, canonicalizationlib/Sema/FlowChecker.cpp— type checking,canAFlowIntoB(), annotation parsinglib/Sema/FlowChecker-scopetypes.cpp— scope resolution, alias resolution, looping type detection
Memory Representation
Types use a three-layer architecture:
Type— a lightweight wrapper holding aTypeInfo *infopointer, anESTree::Node *node(source location), and anisLoopingflag for recursive
structures.TypeInfo— abstract base class for type backing storage. Carries:TypeKind kind_— discriminator enum- Lazy-cached hash (
hashed_,hashValue_) - Virtual
_compareImpl(),_equalsImpl(),_hashImpl()
Concrete type classes —
UnionType,ClassType,TypedFunctionType, etc.
All types are allocated in std::deque containers within FlowContext, providing
stable pointers without invalidation. Singleton types (Void, Null, Number, etc.)
exist as single instances; complex types are interned via hash-consing where
appropriate.
Complete Type Kinds
Singleton Types (immutable, one instance each)
| Type | Meaning |
|---|---|
Void |
JS undefined / absence of value |
Null |
JS null |
Boolean |
true or false |
String |
JS string |
Number |
IEEE 754 double |
BigInt |
Arbitrary-precision integer |
CPtr |
Internal C pointer (native interop) |
Any |
Supertype of everything; outputs require checked casts |
Mixed |
Bidirectional any (flows both in and out freely) |
Generic |
Placeholder for unspecialized generic class/alias (must not escape checker) |
InferencePlaceholder |
Used during generic type argument inference |
Complex (Structural) Types
| Type | Description |
|---|---|
Union |
A | B | C — canonical sorted union with looping-arm support |
Array |
Array<T> — single element type, invariant |
Tuple |
[A, B, C] — fixed-length, ordered, typed |
ExactObject |
{x: A, y: B} — named fields, exact (no extra properties) |
TypedFunction |
(p1: A, p2: B) => R — full signature with this-param, optional params |
UntypedFunction |
Function with no type information (only async/generator flags) |
NativeFunction |
C++ native function with a NativeSignature |
Class |
Nominal class with fields, methods, constructor, superclass chain |
ClassConstructor |
Type of a class variable/declaration (distinct from instance type) |
Singleton Types
Created once in FlowContext and accessed via getters (getVoid(), getNull(),
etc.). Comparison is pointer identity (O(1)). Hash is the TypeKind ordinal.
`Any` vs `Mixed`
Any: accepts all inputs. When a value typedanyflows out to a
concrete type, a runtime checked cast is required (needCheckedCast = true).Mixed: flows both directions without checked casts. Used where the type
system cannot determine the type but no runtime check is desired.
Union Types
Represents A | B | C. Encoded as a vector of arms with a dual-mode
canonicalization strategy.
Canonicalization
- Nested unions are flattened (one level).
- Arms are partitioned into non-looping (sorted, uniqued via structural
comparison) and looping (recursive arms, deduplicated via O(n²) matching). numNonLoopingTypes_tracks the boundary.- Single-arm unions collapse:
maybeCreateUnion()returns the arm directly.
Optional / Nullable
No separate Optional type. These are represented as unions:
type?→Void | Null | type?type(nullable) →Void | Null | type
createPopulatedNullable(type) creates the canonical union.
Looping (Recursive) Types
Structural recursive types like type Tree = null | {val: number, children: Tree[]}
cause cycles. The FindLoopingTypes pass marks all arms on a cycle withisLooping = true. Looping arms use slow O(n²) matching during comparison since
they cannot be deterministically sorted. Nominal types (classes) stop recursion
traversal — they are compared by identity, not structure.
Array Types
Array<T> with a single element type. Invariant: Array<A> does NOT flow toArray even if A <: B, because arrays are mutable at runtime.
Tuple Types
[A, B, C] — fixed length, positional types. Invariant: element count and
types must match exactly.
Exact Object Types
{x: A, y: B} — named, typed fields with exact semantics (no extra properties
allowed). Field order matters for comparison. Each field has:
Identifier nameType *type
A fieldNameMap_ provides O(1) lookup by name.
Note: Inexact objects ({x: number, ...}) are not yet implemented.
Function Types
Base: `BaseFunctionType`
Carries isAsync_ and isGenerator_ flags. Async/generator status must match
exactly for subtyping.
`TypedFunctionType`
Full signature: return type, optional this-param, parameter list where each
parameter has a name, type, and optional flag.
`UntypedFunctionType`
No signature — only async/generator flags. Flows to other untyped functions
regardless of actual parameters.
`NativeFunctionType`
Points to a NativeSignature from NativeContext. Flows only if signatures are
identical (pointer equality).
Function Subtyping
Functions use standard variance rules:
- Contravariant in parameters: callee's parameter types must accept the
caller's argument types. - Covariant in return type: callee's return type must be accepted where the
caller's return type is expected. - Async/generator status must match exactly.
- Function kind must match (Typed ≠ Untyped ≠ Native).
Class Types (Nominal)
Classes are nominal — subtyping is based on the inheritance chain, not
structure. Each class has a unique ID (TypeWithId base class).
Fields
struct Field {
Identifier name;
Type *type;
size_t layoutSlotIR; // maps to PrLoad/PrStore IR slots
bool isPrivate;
ESTree::MethodDefinitionNode *method; // nullptr for data fields
bool overridden; // can be modified by subclasses
};fields_— direct (non-inherited) fields only.fieldNameMap_— all accessible fields including inherited (name → index).privateFieldNameMap_— private fields (own class only).
Other Properties
constructorType_— function type of the constructor.homeObjectType_— the class representing[[HomeObject]]prototype.superClass_— parent class (nullable).
Class Subtyping
ClassA flows to ClassB iff ClassB is in ClassA's superclass chain.
No structural matching — two classes with identical fields but different
declarations are incompatible.
`ClassConstructorType`
The type of a class declaration/variable (e.g., const C = class { ... }).
Wraps a ClassType. Distinct from the instance type.
Generics
Representation
template <class Spec>
class GenericInfo {
ESTree::Node *originalNode;
DenseMap<ArrayRef<TypeInfo*>, Spec*> specializations; // memoized
};Specialization
- Type arguments are matched to type parameters.
- A specialization cache maps type argument tuples to instantiated types/AST
clones, preventing duplicate work. validateAndBindTypeParameters()populates a binding table mapping parameter
names to concrete types.
`InferencePlaceholder`
Used during generic type argument inference. When a generic function is called
without explicit type arguments, placeholders are created and filled in bymatchConstraintToType() based on argument types.
Subtyping (`canAFlowIntoB`)
The core function returns CanFlowResult { bool canFlow, bool needCheckedCast }.
Rules (in priority order)
- Identity:
a == b→ flows. - Top types:
_ → anyflows;_ → mixedflows;any → _flows but
requires checked cast. - Unions:
(A | B) → Cflows iffA → CANDB → C.A → (B | C)flows iffA → BORA → C.
- Structural types (all invariant):
- Arrays: element types must be equal.
- Tuples: length must match, element-wise equal.
- Objects: field names, order, and types must match exactly.
- Nominal types: class chain lookup.
- Functions: contravariant params, covariant return, matching kind and
async/generator flags.
Checked Casts
When any flows into a concrete type, needCheckedCast = true signals that
IRGen must insert a runtime type check instruction (CheckedTypeCastInst).
Type Comparison, Equality, and Hashing
Comparison (`TypeInfo::compare`)
Lexicographic structural ordering. Tracks visited pairs in CompareState to
detect cycles (returns 0 for revisited pairs). Results are cached.
Equality (`TypeInfo::equals`)
Structural equality with cycle tolerance. Special handling for incomplete unions
(created during alias resolution before canonicalization).
Hashing (`TypeInfo::hash`)
Intentionally shallow — unions hash on (kind, armCount), arrays ignore element
type, tuples/objects hash on size only. This avoids O(n) cost in hash tables while
maintaining the invariant that equals() implies equal hashes.
Type Resolution Pipeline
Three stages during scope processing:
Forward declarations — create
ClassTypeinstances with IDs but no fields.
Record type alias names without resolving their RHS.Alias resolution — resolve type alias RHS annotations. Detect and reject
union-only cycles. Populate forward-declared aliases.Complete declarations — parse class bodies (fields, methods, constructor,
superclass). InitializeClassTypefields. Canonicalize unions.
This staging allows mutual references between classes and type aliases.
Type Narrowing
`getNonOptionalSingleType(type)`
Extracts the single non-void, non-null arm from a union. Returns nullptr if
multiple non-optional arms exist. Used in control flow after null checks.
`tryNarrowType(exprType, targetType)`
Attempts to narrow a union to a target type. Returns the narrowed type and whether
a checked cast is needed. Used after type guards and conditional branches.
Relationship to the IR Type System
The Flow type system and IR type system are separate:
- Flow types operate during semantic analysis (
FlowChecker). They are rich,
structural/nominal, and support generics. - IR types operate during optimization and codegen. They are a 16-bit bitmask
of 13 primitive kinds.
Connection points:
ClassType::Field::layoutSlotIRmaps Flow fields to IRPrLoad/PrStoreslot
indices.- IRGen converts Flow types to IR types and inserts
CheckedTypeCastInstwhereneedCheckedCastwas flagged. - Flow's
Number,String,Boolean, etc. map directly to IR type bits. - Flow's
Classmaps to IR'sObjectbit. - Flow's
Unionmaps to the IR union of its arms' IR types.
Key Design Properties
- Nominal classes, structural everything else: classes use identity; arrays,
tuples, objects, functions use structure. - Invariant containers: arrays, tuples, and objects are invariant because JS
values are mutable. - Cycle-tolerant: comparison, equality, and hashing all handle recursive types
via visited-pair tracking. - Lazy hashing: avoids expensive recursive hashing until a type enters a hash
table. - Stable pointers:
std::dequeallocation ensures type pointers never
invalidate. - Forward declaration support: types can be created with an ID before their
fields are known, enabling mutual references.
Doc Plans Ir Type Ir Type System V2 Design (`doc/plans/ir-type/ir-type-system-v2-design.md`)
IR Type System v2 — Design Proposal
Problem
The current IR Type is a 16-bit bitmask of 13 primitive kinds. It can express
"this value might be a number or a string" but cannot express "this value is an
instance of class Foo" or "this is an array of numbers". The Flow type system
(FlowContext) has full nominal classes, typed arrays, function signatures, tuples,
and exact objects — but none of that information survives lowering into IR.
We want to enrich the IR type system so it can carry Flow-level type information
through optimization and into codegen. The type system will also generate runtime
type representations for soundness enforcement, checked casts, and type guards.
Design Goals
Expressive: can represent everything the Flow type system expresses —
nominal classes, typed arrays, function signatures, tuples, exact objects,
and arbitrary unions thereof.Correct: every type operation (union, intersect, subset, etc.) is
well-defined and sound for all type combinations. Every operation
produces a valid result for every input — no "returns garbage" fallback
paths. Some operations (notablysubtract) may return a conservative
approximation when the exact result is not representable.Orthogonal: Union is the single, uniform composition mechanism for all
types. The type hierarchy has clean, independent dimensions with no ad-hoc
encoding distinctions.Backwards-compatible: the existing IR type system is a proper subset.
All current types and operations continue to work. Flow types lower to IR
types.Suitable for runtime: types can be lowered to compact runtime type
descriptors for checked casts and type guards.Efficient (deferred): performance optimizations (bitmask fast paths,
compact encoding) can be layered on without changing the type algebra.
Type Hierarchy
A Type represents a set of possible runtime values. Every runtime value
belongs to exactly one base type. A Type is either a base type, a union of
types, or the empty type (NoType).
Type
├── NoType -- empty set (bottom, unreachable)
├── Primitives
│ ├── Undefined
│ ├── Null
│ ├── Boolean
│ ├── Number (fp64)
│ │ ├── Int32 -- fp64, value in int32 range
│ │ └── Uint32 -- fp64, value in uint32 range
│ ├── BigInt
│ ├── String
│ └── Symbol
├── Object -- any object, no refinement
│ ├── ClassInstance(classId) -- nominal, with inheritance
│ ├── Array(elemType) -- known element type
│ ├── Tuple(elemTypes...) -- fixed-length, positional
│ ├── Function(signature) -- known param/return types
│ └── ExactObject(fields...) -- known exact field set
├── Internal
│ ├── Empty -- TDZ
│ ├── Uninit -- declared, not initialized
│ ├── Environment
│ ├── PrivateName
│ ├── FunctionCode
│ └── bits32 -- physical 32-bit integer, signless
└── Union(Type, Type, ...) -- union of two or more typesPrimitive Types
Primitives correspond to JS primitive values. Each is a distinct kind — a
runtime value is exactly one primitive kind.
| Type | Description |
|---|---|
| Undefined | The JS undefined value |
| Null | The JS null value |
| Boolean | true or false |
| Number | IEEE 754 double-precision float |
| BigInt | Arbitrary-precision integer |
| String | JS string |
| Symbol | ES2015 symbol |
Number subtypes: Int32 and Uint32 are subtypes of Number. They
represent values that are still fp64 at runtime but are known to be integers
within a specific range. They are not separate runtime kinds — they are
refinements that enable integer-specific optimizations.
Int32: integer value in [-2³¹, 2³¹ - 1].Uint32: integer value in [0, 2³² - 1].Int32andUint32overlap (both contain [0, 2³¹ - 1]) but neither is a
subtype of the other.
Object Types
Object is the base type for all JS objects. It represents "some object, no
further information." Object refinements narrow this to specific categories:
| Refinement | Description |
|---|---|
| ClassInstance(classId) | Instance of a specific nominal class. Subtyping follows the class inheritance chain. |
| Array(elemType) | Array with a known element type. Legacy arrays use Array(any). Invariant (not covariant) because arrays are mutable. |
| Tuple(elemTypes...) | Fixed-length array with known positional types. Invariant. |
| Function(signature) | Callable with known parameter and return types. Signature is immutable (derived from FunctionCode). Legacy functions use Function(any, ...) => any. |
| ExactObject(fields...) | Object with a known exact set of typed fields. No extra properties. |
All object refinements are subtypes of Object. Different refinement categories
are disjoint — a value cannot be both a ClassInstance and an Array.
Legacy vs typed objects: the type system does NOT distinguish between
"legacy" (dynamic shape) and "typed" (fixed shape) objects. Legacy JS objects
can be typed with their known type information (e.g., a legacy function's
signature comes from its FunctionCode). The distinction between dynamic and
fixed shapes is encoded in the instructions that access them (PrLoad/PrStore
for typed access, LoadProperty/StoreProperty for legacy), not in the type
system.
Internal Types
Internal types represent values that exist in the IR but are not JS-visible:
| Type | Description |
|---|---|
| Empty | Temporal Dead Zone — variable before its declaration |
| Uninit | After declaration, before initialization (maps to undefined at runtime) |
| Environment | Lexical scope |
| PrivateName | Private class field name |
| FunctionCode | IR function reference (the code, not a closure). Will carry the precise function signature in the future; currently untyped for backwards compatibility. |
| bits32 | Physical 32-bit integer, signless. Sign is a property of the consuming operation. Fits in a NaN-boxed register. |
bits32 is fundamentally different from Number and its subtypes. Number
(including Int32 and Uint32) is a JS value with fp64 representation.bits32 is a physical 32-bit integer with integer representation.
Specialized instructions handle conversion between bits32 and Number.
Union Types
Union is the single, uniform composition mechanism. Union(A, B, ...)
represents a value that could be any one of its member types.
Union(Number, String) -- number or string
Union(ClassA, ClassB) -- instance of ClassA or ClassB
Union(Number, ClassA, Null) -- number, ClassA instance, or nullThere is no conceptual distinction between unions of primitives and unions of
refined types. The bitmask encoding in the current IR is an implementation
optimization of primitive unions, not a separate concept.
Canonical form: unions are maintained in canonical form:
- Nested unions are flattened:
Union(A, Union(B, C))=Union(A, B, C). - Single-element unions collapse:
Union(A)=A. - Duplicate arms are removed.
- Subsumed arms are removed:
Union(Int32, Number)=Number. - Arms are sorted for deterministic comparison.
Special Types
- NoType: the empty set. No possible values. Bottom of the type lattice.
Represents unreachable code. - AnyType: the union of all JS-observable types (all primitives + Object).
Top of the JS type lattice. Equivalent to today'sTYPE_ANY_MASK.
any is not a distinct type kind — it is the maximal union. When Flow's any
type requires checked casts on output, that is an instruction-level decision
made during IRGen, not a property of the IR type.
Subtype Relationships
The subtype relation A <: B means every value of type A is also a value of
type B (set inclusion).
Rules
Primitives:
Int32 <: Number.Uint32 <: Number.- All other primitive kinds are pairwise disjoint.
Objects:
- Every object refinement
<: Object. ClassB <: ClassAif ClassB extends ClassA (nominal inheritance chain).- Two ClassInstances with no inheritance relationship are disjoint.
- Different refinement categories are disjoint (ClassInstance ∩ Array = ∅).
Unions:
A <: Union(..., A, ...)— every type is a subtype of any union containing it.Union(A₁, ..., Aₙ) <: BiffAᵢ <: Bfor all i.
General:
NoType <: Tfor all T.T <: AnyTypefor all JS-observable T.- Reflexive:
T <: T. - Transitive:
A <: BandB <: CimpliesA <: C.
Type Operations
union(A, B)
Returns the smallest type containing all values of both A and B.
union(A, B):
if A == B: return A
if A == NoType: return B
if B == NoType: return A
if A <: B: return B
if B <: A: return A
return canonical Union(A, B)For object refinements:
union(ClassA, ClassB)where A extends B → B (the superclass).union(ClassA, ClassB)unrelated →Union(ClassA, ClassB).union(Array(T), Array(T))→Array(T).union(Array(S), Array(T))where S ≠ T →Union(Array(S), Array(T)).
NOTArray(union(S, T))— arrays are invariant because they are mutable.
intersect(A, B)
Returns the largest type whose values are in both A and B.
intersect(A, B):
if A == B: return A
if A <: B: return A
if B <: A: return B
if A and B are disjoint: return NoType
distribute over unions:
intersect(Union(A₁,...,Aₙ), B) = union(intersect(A₁,B), ..., intersect(Aₙ,B))Key cases:
intersect(Number, Int32)= Int32.intersect(Object, ClassA)= ClassA.intersect(ClassA, ClassB)unrelated = NoType.intersect(Number, String)= NoType.
subtract(A, B)
Removes from A all values that are in B. Returns a conservative approximation
(superset of the true result) when the exact difference is not representable.
subtract(A, B):
if A <: B: return NoType
if A and B are disjoint: return A
distribute over unions:
subtract(Union(A₁,...,Aₙ), B) = union(subtract(A₁,B), ..., subtract(Aₙ,B))Key cases:
subtract(Union(Number, String), Number)= String (exact).subtract(Union(ClassA, ClassB), ClassA)= ClassB (exact).subtract(Object, ClassA)= Object (conservative — "all objects except
ClassA" is not representable).subtract(Number, Int32)= Number (conservative).
isSubsetOf(A, B)
Returns true iff A <: B. See Subtype Relationships above.
Function Signatures
FunctionSignature:
returnType : Type
thisType : Type -- NoType if no `this` parameter
restType : Type -- NoType if no rest parameter
params : [(Type, bool)] -- (type, isOptional) per parameter
isAsync : bool
isGenerator : boolNote: restType represents the element type of a trailing rest parameter
(e.g., ...args: string[] has restType = String). Flow'sTypedFunctionType does not currently represent rest parameters; this field
is forward-looking.
Subtyping uses standard variance:
- Contravariant in parameters (callee accepts at least what caller provides).
- Covariant in return type (callee returns at most what caller expects).
- Async/generator flags must match exactly.
Two Function types with different signatures are disjoint unless related
by the variance rules above.
Class Types and Inheritance
Classes are nominal — identity is the type table index, not structure. Two
classes with identical fields are different types if they come from different
class declarations.
A ClassInstance type entry stores the superclass relationship, class name,
and field layout directly in its TypeEntry payload:
ClassInstance payload:
superClassTypeId : uint32 -- type table ID of parent ClassInstance
(kNoTypeId if no parent)
className : Identifier -- for runtime type descriptors and
error messages
fields : [(name, Type)] -- direct fieldsThere is no separate class registry or class ID namespace. The type table
entry's own id_ serves as the class identity. The createClassInstance()
method on TypeContext creates a new (non-interned) entry for each
class declaration.
Subtyping: ClassInstance(A) <: ClassInstance(B) iff B is an ancestor
of A in the inheritance chain. The chain is walked via thesuperClassTypeId field in the type table entries.
Disjointness: two classes with no inheritance relationship are disjoint.
This enables precise intersection (intersect(ClassA, ClassB) = NoType for
unrelated classes).
Relationship to Flow Types
| Flow Type | IR Type |
|---|---|
void |
Undefined |
null |
Null |
number |
Number |
string |
String |
boolean |
Boolean |
bigint |
BigInt |
symbol |
Symbol |
any |
AnyType |
mixed |
AnyType (mixed is a compilation-level concept, not present in IR) |
ClassFoo |
ClassInstance(classFooId) |
Array<number> |
Array(Number) |
[number, string] |
Tuple(Number, String) |
(x: number) => string |
Function({params: [Number], return: String}) |
{x: number, y: string} |
ExactObject({x: Number, y: String}) |
ClassA | null |
Union(ClassInstance(A), Null) |
ClassA | ClassB |
Union(ClassInstance(A), ClassInstance(B)) |
number | string |
Union(Number, String) |
Not lowered to IR (resolved before IR):
- Generics (monomorphized or erased).
- Type aliases (resolved to concrete types).
InferencePlaceholder(resolved during checking).
Lowered indirectly:
ClassConstructorType→Function(constructorSignature). A class
constructor is a callable with a known signature; until now lowered to
unrefinedObjectdue to limitations of the old type system.
Relationship to Current IR Type System
Every type in the current 16-bit bitmask system maps directly:
| Current IR | New IR |
|---|---|
Number bit |
Number |
String bit |
String |
Boolean bit |
Boolean |
Object bit |
Object |
Null bit |
Null |
Undefined bit |
Undefined |
BigInt bit |
BigInt |
Symbol bit |
Symbol |
Empty bit |
Empty |
Uninit bit |
Uninit |
Environment bit |
Environment |
PrivateName bit |
PrivateName |
FunctionCode bit |
FunctionCode |
| Multiple bits set | Union of corresponding types |
notype (0) |
NoType |
any mask |
AnyType |
The new system is a strict superset. All existing type queries (canBeNumber(),isPrimitive(), isSubsetOf(), etc.) have direct equivalents with identical
semantics for the primitive-only case.
Runtime Type Representation
The type system must produce compact runtime type descriptors for:
- Checked casts:
CheckedTypeCastInstcarries a target type. The runtime
verifies the value matches. - Type guards: conditional branches narrow types; the narrowed type may
need runtime verification. - Soundness enforcement: typed function parameters are checked at call
boundaries when called from untyped code.
Runtime type checks by category:
| Check | Mechanism |
|---|---|
| Primitive kind (number, string, ...) | Runtime type tag comparison |
| ClassInstance(id) | Class ID lookup in instance's class chain |
| Array(elemType) | Tag check (is array) + element type from array metadata |
| Function(sig) | Tag check (is callable) + signature from FunctionCode metadata |
| Union | Check each arm in sequence |
The specific encoding of runtime type descriptors is an implementation detail
to be determined when codegen support is added.
Representation (Implementation Sketch)
The following is a sketch — specific encoding decisions are deferred until
performance requirements are clearer.
Type Value
A Type is a small, copyable value (ideally 4 bytes). It acts as an opaque
handle into a type table owned by the Module.
TypeContext
Owned by each Module. Stores all interned types and the class inheritance
information needed for subtype checks. Type operations that involve refined
types (union, intersect, isSubsetOf on non-primitive types) access the context
to consult the type table.
Primitive Optimization (deferred)
As an optimization, the common case of primitive-only types (unions of
primitives with no object refinement) can be encoded directly in the handle
without a table lookup — e.g., via a bitmask embedded in the handle. This is
purely an implementation optimization and does not affect the type algebra.
Interning Algorithm: Flow Types to IR Types
When lowering Flow types to IR types, each unique Flow type must be assigned
a unique IRTypeID. Structurally identical Flow types (even with different
pointers) must receive the same ID, so that IR type equality is a simple
integer comparison.
Challenge: Looping Types
Flow types can be recursive (looping). For example:
type Tree = null | {val: number, left: Tree, right: Tree}This creates a cycle: the union contains an ExactObject whose fields refer
back to the union. Flow's FindLoopingTypes pass marks every type node on
a cycle with isLooping = true — not just union arms, but every intermediate
node (the ExactObject, Array, etc.).
Looping types cannot be deeply hashed or structurally compared by normal
recursion — the traversal would never terminate. Flow already solves this:
TypeInfo::hash()uses shallow hashing for looping types (kind + arity).TypeInfo::equals()uses co-inductive comparison with a visited-pairs set.
Flow also provides TypesDenseMapInfo (in FlowTypesDumper.h), aDenseMapInfo<const TypeInfo *> specialization that delegates to hash() andequals(). This can be reused as the basis for interning tables.
Data Structures
Three tables:
Identity table:
DenseMap<const TypeInfo *, IRTypeID>— keyed by
pointer identity. This is the fast path: once a Flow type has been
processed, its pointer maps directly to an IR ID with no hashing or
structural comparison.Non-looping dedup table:
DenseMap<const TypeInfo *, IRTypeID, TypesDenseMapInfo>— keyed by deep structural hash + structural equals.
Ensures that two differentTypeInfo *pointers representing structurally
identical non-looping types receive the same IR ID.Looping dedup table:
DenseMap<const TypeInfo *, IRTypeID, LoopingTypesDenseMapInfo>— keyed by shallow hash + co-inductive equals.
Ensures that structurally identical cycles receive the same IR IDs.
The non-looping and looping dedup tables are separate because their
hash/equality semantics differ: deep structural comparison terminates for
non-looping types but would recurse infinitely for looping types. Mixing them
in one table would require every lookup to handle both cases, complicating theDenseMapInfo and risking subtle bugs.
Algorithm
intern(type) → IRTypeID:
// 1. Fast path: already processed this exact pointer?
Look up type in identity table (pointer equality).
If found → return existing ID.
// 2. Non-looping type.
if !type->isLooping:
Look up type in non-looping dedup table (deep structural hash + equals).
If found → record in identity table, return existing ID.
Recursively intern all sub-types.
Allocate new IRTypeID.
Insert into both identity table and non-looping dedup table.
Return new ID.
// 3. Looping type.
Walk the type graph from type, collecting all reachable looping types
not already in the identity table — the "cycle cluster."
Intern any non-looping sub-types encountered along the way (step 2).
Deduplicate the cluster against the looping dedup table
(shallow hash + co-inductive equals).
If equivalent cycle found → map all nodes to existing IDs.
Otherwise → batch-allocate IRTypeIDs for all nodes in the cluster.
Insert all cluster nodes into both identity table and looping dedup table.
Return the ID for type.Motivation and Design Rationale
Why three tables?
The identity table is an optimization: most lookups during recursive interning
revisit types that have already been processed (e.g., Number appears in many
field types). A pointer-keyed lookup is O(1) with no hashing and avoids the
cost of structural comparison entirely.
The two dedup tables handle the case where different TypeInfo * pointers
represent structurally identical types. Flow's own interning is partial —
it deduplicates within unions but does not globally deduplicate all types.
Without dedup tables, structurally identical types from different parts of
the Flow type graph would receive different IR IDs, breaking the invariant
that IR type equality is an integer comparison.
Why separate dedup tables for looping vs non-looping?
Non-looping types use deep structural hashing: the hash incorporates the
full structure of the type, including all sub-types recursively. This gives
excellent hash distribution and precise equality. It terminates because
there are no cycles.
Looping types cannot use deep hashing — the recursion would not terminate.
They use shallow hashing (kind + arity) which produces more collisions, and
co-inductive equality (with a visited-pairs set to detect cycles). Mixing
these in one table would mean either: (a) using shallow hashing for all types
(poor distribution for non-looping), or (b) checking isLooping inside theDenseMapInfo methods (fragile, since the hash strategy must be consistent
across all entries in the table).
Why batch-allocate looping types?
A cycle like Tree → ExactObject → Array(Tree) contains multiple type nodes
that reference each other. You cannot fully intern any one without the others
having IDs. Batch allocation assigns IDs to all nodes in the cycle
simultaneously, then fills in cross-references using those IDs.
Every node in the cycle needs its own ID — not just the "root." Intermediate
nodes (like Array(Tree)) are real types that can appear independently in
other contexts and must be internable.
Why is the identity table sufficient as a fast path?
During recursive interning, the same TypeInfo * pointer is encountered
repeatedly — once when first processing a type, and again each time it appears
as a sub-type of another type. The identity table caches the result of the
first processing, making subsequent encounters O(1). The dedup tables are
only consulted once per unique pointer, during initial processing.
Leveraging Flow's infrastructure.
Flow already provides TypeInfo::hash(), TypeInfo::equals(), theisLooping flag on every cycle node, and TypesDenseMapInfo. The interning
algorithm builds on these rather than reimplementing structural comparison.
This ensures consistency with Flow's type identity semantics and avoids
duplicating complex cycle-handling logic.
Migration Plan
Phase 1: Type representation and context (API-compatible)
- Define
Typeas a handle intoTypeContext. - Implement
TypeContextwith interning. Owned byModule. - Use a thread-local pointer to maintain the "current"
TypeContext.
An RAII guard (TypeContextRAII) sets the thread-local at pass entry
points. All type operations (unionTy,intersectTy,isSubsetOf, etc.)
useTypeContext::current()when they need the table. - Register all existing primitive types as interned entries.
- All existing type operations delegate to the new implementation but produce
identical results. No call-site changes required. - All existing tests pass unchanged.
- Risk: low. Semantics are unchanged; the thread-local is invisible to
callers.
Phase 2: Explicit context parameter (mechanical)
- Mechanically transform all call sites of
unionTy,intersectTy,subtractTy,isSubsetOf, etc. to accept an explicitTypeContext &
parameter. - Remove the thread-local from
TypeContext. - This is a large but purely mechanical change with no semantic effect.
The thread-local from Phase 1 provides a working system to migrate from
incrementally.
Phase 3: Class types
- Add
ClassInstanceto the type system. - Lower Flow
ClassTypetoClassInstanceduring IRGen. CheckedTypeCastInstcarries class types.- Implement class subtype checking via inheritance chain.
Phase 4: Function signatures and arrays
- Add
Function(signature)andArray(elemType). - Lower Flow function and array types during IRGen.
- Enable signature-aware call-site analysis.
Phase 5: Tuple, ExactObject
- Add remaining object refinements.
- Complete union/intersect/subset logic for all type combinations.
Phase 6: Number subtypes and bits32
- Add
Int32,Uint32subtypes of Number. - Add
bits32internal type.
Phase 7: Runtime type descriptors
- Generate compact runtime type descriptors from IR types.
- Codegen emits type checks using descriptors.
Phase 8: Exploitation in optimization
- Type inference produces and propagates refined types.
- InstSimplify uses refined types for more aggressive simplification.
- Codegen emits specialized code paths for known types.
Open Questions
Number subtype precision:
Int32 ∩ Uint32= non-negative int32 values.
This is not directly representable. Is conservative approximation sufficient,
or do we need finer number subtypes?Per-Module vs shared type context: per-Module is simpler. Shared enables
cross-module type identity but requires index translation.Class inheritance storage: should the type context store the full
superclass chain per class, or just the parent with a separate lookup?
Tradeoff: faster subtype checks vs. compact storage.FunctionCode typing: FunctionCode will carry the precise function
signature in the future. Currently untyped for backwards compatibility.
The migration path is to add an optional signature to FunctionCode's type
representation when ready.
Doc Plans Ir Type Ir Type System (`doc/plans/ir-type/ir-type-system.md`)
Hermes IR Type System
Overview
The IR type system represents the set of possible runtime types a value can have at
a given program point. It uses a bitmask-based union type encoding: each bit in
a 16-bit integer represents one primitive type, and a Type value is the set of
types that a value might be. This makes union, intersection, and subset operations
trivial bitwise operations. The entire Type fits in 2 bytes.
Key files:
include/hermes/IR/IR.h:58-414—Typeclass andTypeKindenumlib/Optimizer/Scalar/TypeInference.cpp— type inference passlib/IR/IR.cpp:1040-1080— type printing
Primitive Types (TypeKind)
Each type occupies one bit in the bitmask (include/hermes/IR/IR.h:61-86):
| Bit | TypeKind | Description |
|---|---|---|
| 0 | Empty | TDZ — variable before its declaration |
| 1 | Uninit | After declaration, before initialization (maps to undefined at runtime) |
| 2 | Undefined | The JS undefined value |
| 3 | Null | The JS null value |
| 4 | Boolean | true or false |
| 5 | String | JS string |
| 6 | Number | IEEE 754 double |
| 7 | BigInt | Arbitrary-precision integer |
| 8 | Symbol | ES2015 symbol |
| 9 | Environment | Lexical scope (internal; not mixed with JS types) |
| 10 | PrivateName | Private class field name (internal) |
| 11 | FunctionCode | IR function reference, not a JS closure |
| 12 | Object | JS object (including arrays, functions, regexps, etc.) |
13 types total. Types 0-8 and 12 can appear in user-facing values; 9-11 are
internal to the compiler.
Composite / Pre-computed Masks
Defined as constants in IR.h:122-134:
| Name | Bits | Meaning |
|---|---|---|
TYPE_ANY_MASK |
all except Empty, Uninit | Standard "any" — all observable JS types |
TYPE_ANY_EMPTY_UNINIT_MASK |
all except Environment, PrivateName, FunctionCode | "any" including TDZ states |
PRIMITIVE_BITS |
Number | String | BigInt | Null | Undefined | Boolean | Symbol | JS primitives |
NONPTR_BITS |
Number | Boolean | Null | Undefined | Non-pointer values (useful for GC) |
Type Operations
All operations are constexpr and work on bitmasks:
// Union: value could be either type
static Type unionTy(Type A, Type B); // A.bitmask_ | B.bitmask_
// Intersection: only types in both
static Type intersectTy(Type A, Type B); // A.bitmask_ & B.bitmask_
// Subtraction: remove types in B from A
static Type subtractTy(Type A, Type B); // A.bitmask_ & ~B.bitmask_Querying
isSubsetOf(Type t)— all bits ofthisare intcanBeType(Type t)— non-empty intersectionisProperSubsetOf(Type t)— strict subsetisPrimitive()— only bits inPRIMITIVE_BITSisNonPtr()— only bits inNONPTR_BITSisKnownPrimitiveType()— exactly one primitive bit set
Special Values
createNoType()— empty bitmask (0). Represents "impossible" / unreachable.createAnyType()—TYPE_ANY_MASK. Default for unknown values.
How Types Attach to the IR
Every Value (base class for instructions, literals, parameters, etc.) carries aType valueType member (IR.h:712), defaulting to createAnyType().
Literals Get Exact Types
| Literal class | Type |
|---|---|
LiteralEmpty |
Empty |
LiteralUninit |
Uninit |
LiteralUndefined |
Undefined |
LiteralNull |
Null |
LiteralNumber |
Number (or Int32 if the value fits) |
LiteralString |
String |
LiteralBigInt |
BigInt |
LiteralBool |
Boolean |
GlobalObject |
Object |
Instructions Declare Inherent Types
Instructions can declare static Optional<Type> getInherentTypeImpl() to declare
a type that is always correct regardless of operands:
- Arithmetic/conversion:
AsNumberInst→ Number,AsInt32Inst→ Int32 - Allocation:
AllocArrayInst,AllocObjectLiteralInst,CreateFunctionInst→ Object - Scope:
CreateScopeInst,ResolveScopeInst,GetParentScopeInst→ Environment - Type queries:
TypeOfInst→ String - Coercion:
AddEmptyStringInst→ String,ToPropertyKeyInst→ String | Symbol
Instructions without an inherent type (e.g. LoadPropertyInst, CallInst,CatchInst) default to any and rely on the inference pass.
Type Inference Pass
File: lib/Optimizer/Scalar/TypeInference.cpp
A whole-program, fixed-point, forward type inference that narrows value types.
Algorithm
Partition functions and variables into groups (union-find) based on mutual
use. Functions that call each other or share variables are in the same partition.Clear all instruction types to their inherent type (or NoType). Save
pre-pass types as upper bounds.Fixed-point iteration over each partition:
- Infer parameter types from call sites (union of all actual arguments).
- Infer instruction types from operand types (see rules below).
- Infer return types from
ReturnInstvalues. - Infer variable/stack types from stored values.
- Repeat until no type changes.
Clamp: intersect inferred types with pre-pass types to prevent widening
beyond what was previously known.
Inference Rules for Binary Operations
| Operation | Result Type |
|---|---|
Comparisons (==, <, instanceof, etc.) |
Boolean |
+ with a String operand |
String |
+ of two Numbers |
Number |
+ of two BigInts |
BigInt |
+ of two non-string primitives |
Number | BigInt |
+ general case |
String | Number | BigInt |
-, *, /, ** |
Number | BigInt |
<<, >> |
Number | BigInt |
>>> |
Uint32 (subtype of Number) |
&, |, ^ |
Int32 | BigInt |
% |
Int32 | BigInt |
Inference Rules for Unary Operations
| Operation | Result Type |
|---|---|
typeof |
String |
void |
Undefined |
! |
Boolean |
-, ++, -- on Number |
Number |
-, ++, -- on BigInt |
BigInt |
-, ++, -- general |
Number | BigInt |
~ on Number |
Int32 |
~ on BigInt |
BigInt |
~ general |
Int32 | BigInt |
PHI Nodes
A PHI's type is the union of all its incoming values' types. The algorithm
recursively traverses nested PHIs to collect all non-PHI inputs.
Memory Locations (Variables, Stack Slots)
The inferred type of a memory location is the union of all values stored to it.
Parameters
If all call sites of a function are known, a parameter's type is the union of all
corresponding arguments across call sites (defaulting to Undefined for missing
arguments). If any call site is unknown, the parameter gets any.
Return Types
The return type of a function is the union of all ReturnInst operand types.
Generator functions always return any because .return() can inject any value.
Type Narrowing Instructions
Several instructions refine (narrow) types:
ThrowIfInst
Removes Empty and Uninit from the type — used for TDZ checks. After this
instruction, the value is known not to be in TDZ.
CheckedTypeCastInst
Intersects the input type with a specified target type. If the intersection is
empty, the code is unreachable.
UnionNarrowTrustedInst
Intersects a saved result type with the operand type. Used after conditional
branches to narrow union types (e.g., after a typeof check).
Type Printing
Types are printed as pipe-separated names: number|string|undefined. Special
cases:
- Empty bitmask →
notype - All types except Empty/Uninit →
any - All types including Empty/Uninit →
any|empty|uninit
Design Properties
- Monotonic narrowing: the inference pass can only narrow types, never widen
them beyond the pre-pass upper bound. - Conservative: if in doubt, the type is
any. Soundness is prioritized
over precision. - No nominal object types: Object is a single type — there is no
Array,RegExp,Functiondistinction in the IR type system. - No function signature types:
FunctionCodeis a single type with no
parameter/return type information embedded. - Cheap: 2-byte representation, all operations are bitwise, no allocations.
Doc Plans Ir Type Ir Type V2 Implementation (`doc/plans/ir-type/ir-type-v2-implementation.md`)
IR Type System v2 — Implementation Design
References
- ir-type-system-v2-design.md — conceptual type
hierarchy, type algebra, design goals, interning algorithm, migration phases. - ir-type-system.md — current 2-byte bitmask type system.
- flow-type-system.md — Flow type system that feeds
into IR types during lowering.
This document covers the concrete C++ implementation: data structures, class
layouts, APIs, integration points, and invariants.
1. Type Representation
Current: 2-byte bitmask
class Type {
uint16_t bitmask_; // 13 primitive kinds as bit flags
};
static_assert(sizeof(Type) == 2);New: 4-byte opaque index
class Type {
uint32_t id_; // Index into TypeContext type table
};
static_assert(sizeof(Type) == 4);Type is a pure opaque handle. It carries no type information itself — all
type data lives in the TypeContext type table, indexed by id_. Two
types are equal iff their id_ values are equal.
The size increase from 2 to 4 bytes fits within the existing Value layout
constraint (sizeof(valueType) <= 4, enforced at IR.h:737).
Well-Known Type IDs
The TypeContext constructor pre-allocates entries for all primitive types
and commonly used unions. These have fixed id_ values:
// Pre-allocated type IDs (assigned by TypeContext constructor).
// These are constants, valid for any TypeContext instance.
static constexpr uint32_t kNoTypeId = 0;
static constexpr uint32_t kEmptyId = 1;
static constexpr uint32_t kUninitId = 2;
static constexpr uint32_t kUndefinedId = 3;
static constexpr uint32_t kNullId = 4;
static constexpr uint32_t kBooleanId = 5;
static constexpr uint32_t kStringId = 6;
static constexpr uint32_t kNumberId = 7;
static constexpr uint32_t kBigIntId = 8;
static constexpr uint32_t kSymbolId = 9;
static constexpr uint32_t kEnvironmentId = 10;
static constexpr uint32_t kPrivateNameId = 11;
static constexpr uint32_t kFunctionCodeId = 12;
static constexpr uint32_t kObjectId = 13;
static constexpr uint32_t kBits32Id = 14;
static constexpr uint32_t kAnyTypeId = 15; // Union of all JS-observable types
static constexpr uint32_t kNumericId = 16; // Number | BigInt
static constexpr uint32_t kAnyEmptyUninitId = 17; // any | Empty | Uninit
// ... other common unions as needed ...
static constexpr uint32_t kFirstDynamicId = 32; // IDs below this are reserved
// The well-known ID assignment is frozen: IDs are never removed or reordered.
// New well-known IDs are only appended before kFirstDynamicId.The Type::createX() static methods return these well-known IDs:
static constexpr Type createNoType() { return Type(kNoTypeId); }
static constexpr Type createNumber() { return Type(kNumberId); }
static constexpr Type createString() { return Type(kStringId); }
static constexpr Type createObject() { return Type(kObjectId); }
static constexpr Type createAnyType() { return Type(kAnyTypeId); }
static constexpr Type createBits32() { return Type(kBits32Id); }
// ... etc.These remain static constexpr — no context needed to create primitive types.
Encoding Examples
| Type | id_ | Table entry |
|---|---|---|
NoType |
0 |
{kind: NoType} |
Number |
7 |
{kind: Number} |
Object (unrefined) |
13 |
{kind: Object} |
Number | String |
N |
{kind: Union, arms: [Number, String]} |
ClassA |
N |
{kind: ClassInstance, classId: A} |
Array(Number) |
N |
{kind: Array, elemType: Number} |
Number | ClassA |
N |
{kind: Union, arms: [Number, ClassA]} |
ClassA | ClassB |
N |
{kind: Union, arms: [ClassA, ClassB]} |
AnyType |
15 |
{kind: Union, arms: [all JS-observable types]} |
bits32 |
14 |
{kind: Bits32} |
Note on bits32: bits32 is not yet used in the current codebase. It is
reserved for future use (physical 32-bit integer, signless). Its well-known
ID and TypeKind enum value are defined from the start so they are stable.
2. Type Table Entries
Each entry in the type table describes a type. All entries are immutable once
created.
The TypeKind enum is defined in full from the start, but refined kinds
(ClassInstance, Array, Tuple, Function, ExactObject, Int32,Uint32) are dead code until their respective migration phase activates
them. In Phase 1, only the leaf kinds and Union are populated.
TypeKind
enum class TypeKind : uint8_t {
// --- Leaf kinds (no sub-type data) ---
NoType, // Empty set, bottom
Undefined,
Null,
Boolean,
Number, // Unrefined fp64
BigInt,
String,
Symbol,
Empty, // TDZ
Uninit, // Declared, not initialized
Environment,
PrivateName,
FunctionCode,
Object, // Unrefined JS object
Bits32, // Physical 32-bit integer, signless
// --- Number subtypes (still fp64, value constrained) ---
Int32, // Integer in [-2^31, 2^31 - 1]
Uint32, // Integer in [0, 2^32 - 1]
// --- Refined object types ---
ClassInstance, // Nominal class instance
Array, // Array with known element type
Tuple,