Contributing

Thank you for your interest in contributing to Hologram! This guide will help you get started with contributing to the project.

Current Focus: Porting Erlang Functions

The most impactful way to contribute right now is by helping port Erlang functions to JavaScript. This work expands Elixir standard library coverage in the browser, enabling more of the Elixir you know and love to run client-side.

Hologram automatically transpiles Elixir code to JavaScript, but many Elixir standard library functions depend on underlying Erlang functions that must be manually ported. Currently we're focusing on functions that matter for web development: those supporting state management, data transformation, working with strings and collections, handling dates and times, and other common application needs. Modules like Process, System, File, Port, Code, and Node are deferred to later development phases.

Good news: You don't need deep knowledge of Hologram internals or Erlang to contribute! You just need to understand what the Erlang function does and follow the patterns established in existing ports. This makes it an excellent opportunity whether you're new to open source or an experienced contributor.

Prerequisites

What You Need to Know

What You Need to Have

What You DON'T Need

Finding a Function to Port

Step 1: Browse Available Functions

Visit the Erlang Functions page (part of the Client Runtime reference) to see Erlang functions porting status. Look for functions marked as todo - these are ready to be ported.

Step 2: Check GitHub Issues

Before starting, search the Hologram GitHub issues to make sure nobody is already working on your chosen function.

Step 3: Create Your Issue

Create a new GitHub issue with a clear title in this format: Port :lists.filter/2 to JS. This reserves the function and lets others know you're working on it.

Setting Up Your Development Environment

Fork and Clone

First, fork the Hologram repository on GitHub, then clone your fork locally:

$ git clone https://github.com/YOUR-USERNAME/hologram.git
$ cd hologram

Create Your Branch

Important: Branch from dev, not master. Ports are treated as enhancements, and all enhancements target the dev branch.

$ git checkout dev
$ git checkout -b port-lists-filter-2

Scope Your Work

One function per pull request (or functions that are directly related - same module/name, different arity). This allows for faster review and keeps the codebase moving forward efficiently.

Understanding the Basics

Boxed Types

Hologram uses value boxing to maintain Elixir type information in JavaScript. Each transpiled value is wrapped in an object with a type field that specifies its Elixir type. For example, the integer 42 becomes {type: "integer", value: 42n}.

You can explore the type system in assets/js/type.mjs in the Hologram repository. The Type class provides utilities like Type.boolean(), Type.isTuple(), and Type.encodeMapKey() that you'll use when porting functions.

Function Structure

Let's examine :lists.keymember/3 as an example (from lists.mjs). All ported functions follow this pattern:

// Start keymember/3
"keymember/3": (value, index, tuples) => {
  return Type.boolean(
    Type.isTuple(Erlang_Lists["keyfind/3"](value, index, tuples))
  );
},
// End keymember/3
// Deps: [:lists.keyfind/3]

Key components:

Step-by-Step Porting Process

1. Understand the Function Behavior

Before writing any code, thoroughly understand what the Erlang function does. The Erlang Functions page lists all Erlang functions - click on a function to view its details page, which includes a link to the official Erlang documentation for that specific function. You can also use IEx:

iex> h :lists.filter/2

Or browse the official Erlang documentation directly: https://www.erlang.org/doc (navigate to the appropriate module).

2. Locate the Target File

Manually ported Erlang functions are located in assets/js/erlang/ directory, organized by module. For example, :lists functions go in assets/js/erlang/lists.mjs.

3. Look at Similar Functions

Browse already-ported functions in the same module or similar modules. Use these as templates and follow the same conventions, even if they seem non-DRY or unusual. These patterns will be refactored later once common patterns are identified across all ports.

4. Implement Your Function

Write your implementation following these principles:

5. Validate Inputs

Most ported functions need input validation. Follow existing validation patterns from other ported functions.

Type validation (most common case) - Check that parameters have the expected types:

if (!Type.isList(list)) {
  Interpreter.raiseArgumentError(
    Interpreter.buildArgumentErrorMsg(3, "not a list")
  );
}

Value validation - Check that parameter values are within acceptable ranges or match expected values:

if (index.value < 1) {
  Interpreter.raiseArgumentError(
    Interpreter.buildArgumentErrorMsg(2, "out of range")
  );
}

Deferred functionality - If a particular option or code path doesn't make sense for Phase 1 (e.g., UTF16 encoding), add a clear error message or TODO comment explaining why it's deferred:

// TODO: implement other encodings for inputEncoding param
if (!Interpreter.isStrictlyEqual(inputEncoding, Type.atom("utf8"))) {
  throw new HologramInterpreterError(
    "encodings other than utf8 are not yet implemented in Hologram"
  );
}

6. Register Dependencies

If your function has dependencies on other Erlang functions, you need to register them in two places:

This duplication is temporary - both are required for now, but eventually there will be a single source of truth.

Writing Tests

Every ported function requires two types of tests to ensure correctness and consistency with OTP behavior.

JavaScript Unit Tests

Add comprehensive test cases in a dedicated test file for each ported module. For example, tests for :lists functions are in test/javascript/erlang/lists_test.mjs, while the implementation is in assets/js/erlang/lists.mjs.

Your tests should:

Server-Side Consistency Tests

Create matching tests in Elixir that verify your JavaScript implementation behaves identically to OTP. These go in test/elixir/hologram/ex_js_consistency/erlang/.

For example, if you ported :lists.filter/2, create tests in test/elixir/hologram/ex_js_consistency/erlang/lists_test.exs that mirror your JavaScript tests.

Performance Considerations

While the general rule is to never mutate parameters (see Implement Your Function above), you can use internal mutation for performance when building collections. This is an optimization that doesn't violate immutability from the caller's perspective:

Example: Building Collections Efficiently

When porting functions like :erlang.--/2 (list subtraction), it's fine to mutate the internal data structure as you build it:

// Create a mutable array from input data
const result = Utils.shallowCloneArray(left.data);

for (const rightElem of right.data) {
  for (let i = 0; i < result.length; ++i) {
    if (Interpreter.isStrictlyEqual(rightElem, result[i])) {
      // Mutating result is fine - it's internal
      result.splice(i, 1);
      break;
    }
  }
}

return Type.list(result); // Return the boxed value

Note: These manual performance optimizations are temporary measures. Hologram will implement Structural Sharing in the future to automatically address performance issues related to immutability. Structural sharing will reduce memory usage and improve performance by avoiding unnecessary data copying while maintaining immutability.

Code Review & Communication

Pull Request Guidelines

Communication Channels

Review Process

Once you submit your PR, we'll review it as quickly as possible. We may suggest:

Tips & Best Practices

Using AI Tools

AI tools can be very helpful for porting! They're good at:

Just remember to verify the AI's output against actual OTP behavior and existing Hologram conventions!

Common Pitfalls to Avoid

Alphabetical Ordering

Order imports (including classes like Type, Interpreter, etc.) and functions alphabetically within each module file. Similarly, order test sections for each function alphabetically by function name within test files. This keeps the codebase organized and makes it easier to find functions and tests.

Resources

Questions?

If you have questions about porting, contribution process, or anything else, please reach out:

Thank you for contributing to Hologram! Every function you port brings us closer to full Elixir standard library coverage in the browser and helps the entire Elixir community build better full-stack applications.

Previous
← Roadmap