Skip to content

Eject-grade emit: per-target language-affordance catalog (views→JSX/HEEx, properties, generics re-generalization, native idioms) #62

Description

@rubys

Use case

Eject: transpile once to a target language and leave the Ruby source behind, continuing development natively. Under eject the emitted tree is the product — readability, diff-ability, and use of the target's native affordances become functional requirements, not cosmetics. The argument is strongest where source and target idioms diverge most (Go, Rust, Kotlin, Swift), and the per-target known-unsupported ledger doubles as the new owner's hand-porting punch list.

Precedent: juntos (ruby2js) collapsed views back into EJS and JSX. The mechanism translates directly: roundhouse's pipeline already expands ERB to Ruby string-concat (_buf appends) and ruby2js's filter/erb.rb demonstrates collapsing exactly that compiled form back to template syntax (erb_to_jsx.rb is the direct route). Our lowering is collapse-friendly by construction.

Not a priority now. Demos would be more compelling with this work done, but it doesn't rise to priority yet. This issue is the catalog of what to explore either when eject truly becomes a priority, or as a polish pass after diminishing returns on new Rails features. The tier tags are the point: Tier B items cost almost nothing now if we record the intent fact whenever we next touch the relevant lowering — the polish stays deferred, but the fact isn't destroyed.

Tier A — pure emit-layer collapses

Pattern-match IR shapes we already have. Deferrable indefinitely, safe forever.

  1. Views → native templates (juntos-proven). Collapse _buf-append render bodies to: JSX/TSX (TypeScript), HEEx (Elixir — affinity with the live-updates arc, analysis: write/invalidation graph as a lowering (+ per-target revalidation policies; prereq: async-coloring into client contexts) #44), Razor (C#), templ or html/template (Go), kotlinx.html builders (Kotlin), a result-builder DSL (Swift). Ruby eject: re-emit the original ERB verbatim.
  2. Accessors → native properties. AccessorKind already survives into the IR: Kotlin var title: String with custom get(), Swift computed properties, C# auto-properties, Python @property, Crystal property, Ruby attr_* (gated on spinel attr_reader flattening shadows an explicit subclass override at call sites (field access emitted instead of dispatch) matz/spinel#1702 — attr flattening currently shadows explicit overrides).
  3. Record shapes. Row/Params classes → Kotlin data classes, C# records, Swift structs + Codable, Python dataclasses, Rust derives + serde, Crystal record, TS interfaces with object literals.
  4. Nil idiom. Explicit .nil? check trees → Swift guard let/if let, Kotlin ?./?:, Rust Option combinators, C# nullable patterns, TS optional chaining.
  5. Pattern matching. Case trees and class-id dispatch switches → Rust match, Kotlin when, Swift switch, C# switch expressions, Python match, Elixir multi-head functions.
  6. Naming affordances. Predicate ?is-prefix (isPersisted) on Kotlin/Swift/TS/C#/Go rather than the current Pred/_pred suffix; the affix sweep centralized the rename point, so this is a table change.

Tier B — needs an intent fact recorded in the IR

Additive metadata. Record it when next touching the relevant lowering; spend it later.

  1. Generics re-generalization. Monomorphized specializations (ArticleCommentsProxy) carry their origin (CollectionProxy[Comment], fk article_id) so Rust/Kotlin/Swift/C#/Go/Crystal emitters can re-generalize (HasMany<Comment>) while other targets keep the specialization. The model-registry pattern (metadata in IR, emitters pick resolution) extended to generic origins — the one place idiom needs an IR schema addition rather than emit-layer work.
  2. DSL/closure affordances. Routes, json_builder, broadcasts marked as builder-with-implicit-receiver → Kotlin lambdas-with-receiver, Swift @resultBuilder, C# LINQ/collection expressions, Elixir pipelines.
  3. Resource lifecycle. Db.prepare/finalize marked as an acquire/release pair → Go defer, C# using, Python with, Rust Drop/RAII, Swift defer. Independent correctness value: eliminates the leaked-statement-on-early-return hazard class outright, which may justify pulling this one forward on its own.

Tier C — renovation-scale (only with eject as a driver)

  1. Override seams → protocols/traits. Callbacks-as-overrides rendered as Rust traits with default methods, Swift protocol extensions, Go interfaces — instead of inheritance emulation and class-id switches.
  2. Concurrency coloring. Kotlin suspend, Swift/C#/Rust native async, Go goroutines, Elixir processes. Already has its own thread via async-coloring and live-updates (analysis: write/invalidation graph as a lowering (+ per-target revalidation policies; prereq: async-coloring into client contexts) #44); lands there, not as polish.

Triggers

  • Eject becomes a demo/positioning need — a juntos-style JSX view collapse is the single most demo-visible item here.
  • Diminishing returns on Rails-feature coverage (the polish-pass scenario).
  • Item 9's correctness angle justifying early adoption independent of idiom.

Design rule in the meantime

The catalog doubles as a checklist for new lowerings: before flattening a construct in shared lowering, check whether a Tier A/B item needs the fact being destroyed (self-describing IR). Keeping the _buf render-body shape stable and pattern-matchable is the standing example.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions