← articlesripple internals

Ripple Runtime: Compilation Pipeline

Part 2 of the Ripple internals deep dive series.

Phase 1: Compilation - From Ripple to JavaScript

Before our component can run, Ripple's compiler transforms it into optimized JavaScript. The compilation process happens in three phases: parsing, analysis, and transformation. Understanding this transformation is crucial because it shows how reactive syntax becomes runtime calls that enable dependency tracking.

Compilation Pipeline

Step 1.1: Parser Transformation

The parser reads our Ripple syntax and builds an Abstract Syntax Tree (AST). The key transformation is how @count is handled.

Original Ripple code:

let count = track(0);
let double = track(() => @count * 2);

What the parser sees:

  • @count is a special token (not a regular identifier)
  • The parser strips the @ and marks the identifier as tracked: true

AST Structure:

// First variable declaration
VariableDeclaration {
  declarations: [{
    id: Identifier { name: "count" },
    init: CallExpression {
      callee: Identifier { name: "track" },
      arguments: [Literal { value: 0 }]
    }
  }]
}

// Second variable declaration (note the tracked flag!)
VariableDeclaration {
  declarations: [{
    id: Identifier { name: "double" },
    init: CallExpression {
      callee: Identifier { name: "track" },
      arguments: [ArrowFunctionExpression {
        body: BinaryExpression {
          operator: "*",
          left: Identifier {
            name: "count",
            tracked: true  // ← This flag is crucial!
          },
          right: Literal { value: 2 }
        }
      }]
    }
  }]
}

The tracked: true flag is crucial - it tells the transformer that this identifier needs runtime dependency tracking. Without this flag, the transformer wouldn't know to generate the special _$_.get() call that registers dependencies.

Step 1.2: Analysis Phase

The analyzer walks the AST and builds a scope tree - a map of all variables and where they're used. This helps the transformer generate correct code.

Scope Analysis:

ComponentScope {
  declarations: {
    "count": Binding {
      node: Identifier,
      initial: CallExpression { callee: "track" },
      kind: "let",
      metadata: {
        is_tracked: true  // ← This is a tracked value
      }
    },
    "double": Binding {
      node: Identifier,
      initial: CallExpression {
        callee: "track",
        arguments: [Function]  // ← Function means it's derived
      },
      kind: "let",
      metadata: {
        is_tracked: true,
        is_derived: true  // ← Computed from other values
      }
    },
    "showDouble": Binding {
      node: Identifier,
      initial: CallExpression { callee: "track" },
      kind: "let",
      metadata: { is_tracked: true }
    }
  }
}

What this tells us:

  • count is a simple tracked value (stores a number)
  • double is a derived value (computed from count)
  • showDouble is a simple tracked value (stores a boolean)

The analyzer also tracks where each variable is referenced, which helps with optimization.

Step 1.3: Transformation Phase

The transformer generates optimized JavaScript code. This is where the magic happens - reactive syntax becomes runtime calls.

Transformed Component (simplified):

import * as _$_ from 'ripple/internal/client';
import { track } from 'ripple';

function Counter(props) {
  // Create component context (tracks component state)
  const component_ctx = _$_.create_component_ctx();
  _$_.push_component();

  // Tracked value creation
  // Note: component_ctx is passed as 4th argument
  let count = track(0, undefined, undefined, component_ctx);
  let double = track(() => _$_.get(count) * 2, undefined, undefined, component_ctx);
  let showDouble = track(true, undefined, undefined, component_ctx);

  // Root block - wraps entire component rendering
  return _$_.root(() => {
    // Template creation (HTML string → DOM)
    const __anchor = document.createTextNode('');
    const __template0 = _$_.template('<div><p><!></p><!></div>');
    const __fragment = __template0();
    __anchor.before(__fragment);

    // Count text rendering block
    const __text0 = __fragment.querySelector('p');
    _$_.render(() => {
      // @count becomes _$_.get(count)
      _$_.set_text(__text0.firstChild, 'Count: ' + _$_.get(count));
    });

    // Conditional block for showDouble
    const __anchor1 = __fragment.querySelector('p').nextSibling;
    _$_.if(__anchor1, (set_branch) => {
      // @showDouble becomes _$_.get(showDouble)
      if (_$_.get(showDouble)) {
        set_branch((anchor) => {
          const __template1 = _$_.template('<p><!></p>');
          const __fragment1 = __template1();
          anchor.before(__fragment1);

          _$_.render(() => {
            // @double becomes _$_.get(double)
            _$_.set_text(__fragment1.firstChild, 'Double: ' + _$_.get(double));
          });
        });
      }
    });

    _$_.pop_component();
    return () => { /* teardown function */ };
  }, component_ctx);
}

Key Transformations:

Ripple SyntaxCompiled JavaScriptWhat It Does
@count_$_.get(count)Read tracked value, register dependency
@count++_$_.set(count, _$_.get(count) + 1)Update tracked value, schedule update
track(() => @count * 2)track(() => _$_.get(count) * 2)Create derived value
if (@showDouble)_$_.if(anchor, (set_branch) => { if (_$_.get(showDouble)) ... })Conditional rendering block

Notice how @count becomes _$_.get(count). This function call is where dependency tracking happens. When the compiled code executes, _$_.get(count) will read the value and register count as a dependency of the currently executing block.

Now that we understand how the code is transformed, let's see what happens when this compiled code actually runs.


Visual Overview: The Reactivity Flow

Before we dive deep, here's a high-level view of how reactivity works:

Reactivity Flow Overview

Key Variables to Watch:

VariablePurposeChanges When
count.__vCurrent valueset() called
count.cClock valueset() called (increments)
block.dDependency chainget() called (if tracking)
dependency.cStored clockregister_dependency() called
trackingEnable/disable trackingBlock execution context