Proposal: Native ReScript macros for extensions and derivers

オープン
#8,330 コメント 4 件 リアクション 5 件 担当者 0 名 GitHub で見る

まだ誰も着手していません。

評価

難易度
5/5
見積もり時間
1週間以上
初心者へのやさしさ
25/100
issue の種類
機能追加
明瞭さ
説明が足りない
活発さ
静か
技術スタック
javascript, node.js, ocaml

調査の方向性

PoC 設計ドキュメントから始め、プロジェクトローカルの macros/ レイアウトと rescript.json のフィールドを、コンパイラの現在の PPX および extension インフラストラクチャと比較します。提案されている Node.js のリクエスト/レスポンスフローと構造化 AST を、パースからコンパイルまで追跡します。ここでの完了は小さなパッチによって定義されるものではありません。この issue では、まず合意された初期スコープと実装計画が必要です。

索引モデルが issue の本文から書いたものです。

説明

Update 26-04-10: Added the sequence diagram for extension and deriver.

Summary

I'd like to propose a native macro system for ReScript.

The goal is to provide a ReScript-native replacement path for custom extension and deriver workflows that currently rely on PPX-style rewriting. Instead of writing transforms in OCaml against the legacy PPX surface, macro authors would write handlers in ReScript and work against a structured public AST.

The proposed user-facing forms are:

  • expression extensions: %ns.name(...)
  • structure/signature extensions: %%ns.name(...)
  • native derivers on type declarations: @ns.name and @ns.name(...)

Motivation

ReScript currently has extensibility through existing PPX-compatible infrastructure, but the authoring model is still inherited from the OCaml world.

That has a few drawbacks for ReScript users:

  • custom transforms are not written in ReScript
  • the public surface is not designed around ReScript ergonomics
  • extension/deriver authoring feels disconnected from the rest of the toolchain
  • it is harder to build a ReScript-first ecosystem of compile-time transforms

A native macro system would let us design this around ReScript itself.

Proposal

1. Project-level macro source set

Enable macros from rescript.json:

{
  "name": "@foo/a",
  "sources": {
    "dir": "src",
    "subdirs": true
  },
  "dependencies": ["ppxlib_res"],
  "macros": {
    "dir": "macros",
    "namespace": "foo.a",
    "node": "node"
  }
}

Proposed fields:

  • macros.dir
  • macros.namespace
  • macros.node
    If namespace is omitted, it can default from the package name.
2. Macro registration in ReScript

Macro handlers live under macros/ and are registered with attributes on top-level let bindings:

@macro.extension("graphql")
let expand = (...) => ...

@macro.deriver("spice")
let derive = (...) => ...
3. ReScript-native call-site syntax

Example usage:

let message = %foo.a.graphql("spice")

%%foo.a.graphql("macroValue")

@foo.a.accessors
type user = {name: string}

@foo.a.spice
type drink = {label: string}
4. Structured public AST

Instead of a string-in/string-out protocol, macros should work against a structured public AST API, for example:

  • PpxlibRes.Ast
  • PpxlibRes.Builder
  • PpxlibRes.Payload
  • PpxlibRes.Extension
  • PpxlibRes.Deriver
  • PpxlibRes.Codec

Example extension API:

type ctx = {
  filePath: string,
  moduleName: string,
}

type context =
  | Pexp
  | Pstr
  | Psig

type result =
  | Pexp(Ast.expression)
  | Pstr(array<Ast.structureItem>)
  | Psig(array<Ast.signatureItem>)

Example deriver API:

type ctx = {
  filePath: string,
  moduleName: string,
}

type result = {
  structure: array<Ast.structureItem>,
  signature: array<Ast.signatureItem>,
}

Suggested handler shapes:

@macro.extension("graphql")
let expand = (
  kind: Extension.context,
  payload: Ast.payload,
  ctx: Extension.ctx,
): Extension.result => ...

@macro.deriver("spice")
let derive = (
  ctx: Deriver.ctx,
  args: option<Ast.payload>,
  typeDecls: array<Ast.typeDeclaration>,
): Deriver.result => ...

Intended Direction

This is meant to be the preferred model for new ReScript-authored transforms.

In other words:

  • macro authors write ReScript, not OCaml
  • the public API is designed for ReScript
  • extension points and derivers use ReScript-native syntax
  • this reduces the need to introduce new PPX-style rewriting layers for ReScript-specific use cases

Execution Model

A reasonable implementation model is:

  1. compile macro sources to JavaScript
  2. run them in Node.js during compilation
  3. serialize payloads/type declarations across the process boundary
  4. expose a structured AST API to macro authors
  5. decode the result back into compiler AST and continue compilation
  6. The transport format can be JSON, while the public API remains structured AST.

Initial Scope

A first version does not need the full compiler AST.

A useful subset would already cover many cases:

  • payloads

    • PStr
    • PSig
    • PTyp
    • PPat
  • expressions

    • Pexp_ident
    • Pexp_constant
    • Pexp_apply
    • Pexp_fun
    • Pexp_construct
    • Pexp_let
    • Pexp_sequence
    • Pexp_record
    • Pexp_field
  • patterns

    • Ppat_any
    • Ppat_var
    • Ppat_record
  • structure/signature items

    • Pstr_eval
    • Pstr_value
    • Pstr_type
    • Psig_value
    • Psig_type
  • core types

    • Ptyp_constr
    • Ptyp_arrow
    • Ptyp_tuple

Unsupported nodes can fail explicitly at the macro call site.

Sequence Diagrams

Extension
sequenceDiagram
  participant Src as User source
  participant Parser as Parser
  participant NM as Native_macro
  participant Node as Node runner
  participant Macro as expand

  Src->>Parser: Parse source module
  Parser->>NM: Encounter expression extension
  NM->>NM: Resolve macro registration
  NM->>Node: request JSON
  Node->>Macro: Call expand handler
  Macro-->>Node: Return public AST result
  Node-->>NM: response JSON
  NM->>NM: Decode to compiler AST
  NM-->>Parser: Replace original expression
Deriver
sequenceDiagram
  participant Src as User source
  participant Parser as Parser
  participant NM as Native_macro
  participant Node as Node runner
  participant Macro as registered deriver

  Src->>Parser: Parse source module
  Parser->>NM: Encounter type declaration with deriver
  NM->>NM: Resolve deriver registration
  NM->>NM: Preserve original type and strip macro attrs
  NM->>Node: request JSON
  Node->>Macro: Call registered deriver handler
  Macro-->>Node: Return generated structure and signature items
  Node-->>NM: response JSON
  NM->>NM: Decode to compiler AST
  NM-->>Parser: Append generated items after original type group

Example Outcomes

A simple extension could turn:

let message = %foo.a.graphql("spice")

into something like:

let message = "query:" ++ "spice"

A simple deriver could turn:

@foo.a.spice
type drink = {label: string}

into generated bindings such as:

let encodeDrink: drink => JSON.t
let decodeDrink: JSON.t => drink

while preserving the original type declaration.

PoC

There is already a working reference implementation and design write-up in my fork:

Follow-up Design Questions

The current PoC only covers project-local macros under macros/.

A future design step should define how macros can be published by npm packages and consumed through normal package dependencies. In practice, that means designing the interface for:

  • exposing macro entry points from a package
  • resolving macro namespaces from dependencies
  • using dependency-provided macros from an application project

I think this is an important missing piece for a complete macro design, even though it is intentionally out of scope for the current prototype.

主要言語
OCaml
スター
7.5k
フォーク
485
平均マージ
1日 2時間
マージ済み PR(30日)
55

コントリビューションガイド

コントリビューションガイドを開く

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

rescript-lang/rescript のほかの issue

rescript-lang/rescript の issue をすべて見る

似ている issue

Build System の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。