mcpField
M
motoko-dot-notation-migration

motoko-dot-notation-migration

github.com/research-ag開発057 回インストール10 週間前に更新
リポジトリ

motoko-dot-notation-migration について

Motoko の `core` 関数呼び出しを `Module.func(self, ...)` から `self.func(...)` に変換するスキルです。`base` からの移行や `core` の更新を行う開発者向けに、関数一覧、変換スクリプト、注意点と検証方法を提供します。

主な機能

  1. 01core 2.2.0 の関数一覧
  2. 02Python 変換スクリプト
  3. 03演算子優先順位の確認
  4. 04型パラメーター内のカンマ処理
  5. 05移行手順と検証チェックリスト

活用シーン

  1. 01プロジェクトを base から core に移行
  2. 02core 更新時に呼び出し構文を変更
  3. 03ドット記法変換の問題を確認

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/research-ag/motoko-skills --skill motoko-dot-notation-migration

インストール後、Claude Code で/motoko-dot-notation-migrationを入力して読み込まれたか確認します。

詳細

Motoko のドット記法への移行

目的

最初のパラメーター名が self である core パッケージ内のすべての関数について、Motoko の関数呼び出しを従来の Module.func(self, ...) 形式から新しい self.func(...) ドット記法に変換します。

このスキルは、motoko-bitcoin プロジェクトの移行成功をもとに作成されました。core 2.2.0 の関数カタログ一式、実績のある Python 変換スクリプト、対処が必要な重要な落とし穴がすべて含まれています。

使用する場面

  • Motoko プロジェクトを base から core に移行するとき、または core のバージョンをアップグレードするとき
  • ユーザーから関数呼び出しをドット記法に変換するよう依頼されたとき
  • ユーザーから Motoko の呼び出し構文を最新化するよう依頼されたとき

背景

Motoko の core パッケージ(base の後継)では、多くのモジュール関数が最初のパラメーターに self を宣言しています。これらの関数はドット記法に対応しており、Module.func(self, arg2, arg3) と書く代わりに、self.func(arg2, arg3) と書けます。

最初のパラメーター名が文字どおり self である関数のみが対応しています。 Array.tabulate、Array.fromVarArray、Blob.fromArray などのファクトリー関数は最初のパラメーターが self ではないため、変換してはいけません。

変換ルール

追加引数がない関数
Module.func(expr)         →  expr.func()
Module.func<T>(expr)      →  expr.func<T>()
複数引数の関数
Module.func(expr, a, b)   →  expr.func(a, b)
Module.func<T>(expr, a)   →  expr.func<T>(a)
ネストされた呼び出しは自然につながる
Array.flatten(Array.map<T, U>(xs, f))  →  xs.map<T, U>(f).flatten()
List.toArray(listExpr)                 →  listExpr.toArray()
Blob.toArray(Text.encodeUtf8(t))       →  t.encodeUtf8().toArray()

重要な落とし穴

1. 演算子の優先順位 — ドットは中置演算子より優先順位が高い

Motoko では、. はすべての中置演算子(>>、<<、&、|、+、-、*、/、%、#、==、!=、and、or)より優先順位が高くなります。

// WRONG: dot binds to 24, not the whole expression
(value >> 24).toNat()   // ← need parens
value >> 24.toNat()     // ← BROKEN: parses as value >> (24.toNat())

// WRONG: dot binds to b, not the whole expression  
(a & b).toNat()         // ← need parens
a & b.toNat()           // ← BROKEN

// OK: no operators at top level
myArray.flatten()
foo.bar.toNat()
someFunc(x, y).toText()

ルール: self 式にトップレベルの中置演算子が1つでも含まれる場合は、.func() を付ける前に式を括弧で囲んでください。

2. 型パラメーターにはカンマが含まれる — 山括弧の深さを追跡する

Module.func(self, rest...) を最初のカンマで分割するときは、<> の山括弧の深さを追跡する必要があります。<Nat64, [Nat8]> のような型パラメーターには、引数の区切りではないカンマが含まれています。

// The comma between Nat64 and [Nat8] is inside <>, not an argument separator
Array.map<Nat64, [Nat8]>(amounts, func(x) { ... })
//                       ^ THIS is the first real comma (after amounts)

// If you split at the first comma ignoring <>, you get:
//   self = "Array.map<Nat64"    ← WRONG, corrupted
//   rest = "[Nat8]>(amounts..." ← WRONG, corrupted

ルール: カンマを検索する際は、() の depth_paren と同様に、<> の depth_angle を追跡してください。比較演算子と閉じ山括弧を混同しないよう、depth_angle > 0 の場合にのみ > で深さを減らしてください。

3. 変換してはいけない関数

これらの関数は最初のパラメーターが self ではありません。変換しないでください。

Array: tabulate, fromVarArray, fromIter, empty, repeat
Blob: fromArray, fromVarArray, empty
Text: fromArray, fromIter, fromVarArray
List: empty, repeat, tabulate, fromIter, fromArray, fromVarArray
VarArray: empty, repeat, fromArray, fromIter, tabulate
Nat: range, min, max, rangeByInclusive
Nat8/16/32/64: fromNat, fromIntWrap, fromNat16, fromNat32, fromNat64 (constructors)
Int: abs, fromNat, range, min, max
Char: fromNat32, fromText
Runtime: trap

ただし、Array.fromVarArray(x) にはドット記法の同等表現として、x.toArray()(VarArray.toArray 経由)があります。これはドット記法への変換スクリプトでは処理されません。別途リファクタリングが必要です(motoko-core-code-improvements スキルのセクション H を参照)。

4. ドット記法を使うには、その型のモジュールをインポートする必要がある

値に対してドット記法でメソッドを呼び出す場合(例:someBlob.toArray())、呼び出し側のコードにモジュール名 Blob が明示されていなくても、その値の型のモジュール(Blob)をインポートする必要があります。

インポートが未使用に見えても必要となる一般的な例:

  • Blob — Blob の戻り値に対する .toArray()、.size()(例:Sha256.fromArray(...).toArray())
  • Array — [T] の値に対する .flatten()、.foldLeft()、.map()、.sliceToArray()
  • Nat — .size() の呼び出しから得られる Nat の値に対する .toText()
  • VarArray — [var T] の値に対する .toArray()(Array.fromVarArray の変換後)

これらのインポートを削除すると、field X does not exist in type というエラーが発生し、モジュールをインポートするよう案内されてコンパイルに失敗します。

5. 文字列リテラルはスキップする必要がある

演算子やカンマを検索するときは、エスケープされた引用符(\")を含め、文字列リテラル("...")をスキップしてください。これにより、文字列内の演算子による誤検出を防げます。

6. 数値リテラルは括弧なしで安全に使える

42.toText() と 0xFF.toNat() は有効な Motoko 構文です。数値リテラルには括弧なしで直接ドット記法を使えます。

関数カタログ(core 2.2.0)

追加引数がない関数(self のみ)

これらの関数は self という名前の引数を1つだけ受け取ります。Module.func(expr) を expr.func() に変換してください。

ZERO_EXTRA = [
    # Nat8
    ('Nat8', 'toNat'), ('Nat8', 'toText'), ('Nat8', 'toNat16'),
    ('Nat8', 'toNat32'), ('Nat8', 'toNat64'),
    # Nat16
    ('Nat16', 'toNat'), ('Nat16', 'toText'), ('Nat16', 'toNat8'),
    ('Nat16', 'toNat32'), ('Nat16', 'toNat64'),
    # Nat32
    ('Nat32', 'toNat'), ('Nat32', 'toText'), ('Nat32', 'toNat8'),
    ('Nat32', 'toNat16'), ('Nat32', 'toNat64'), ('Nat32', 'toChar'),
    # Nat64
    ('Nat64', 'toNat'), ('Nat64', 'toText'), ('Nat64', 'toNat8'),
    ('Nat64', 'toNat16'), ('Nat64', 'toNat32'),
    # Nat
    ('Nat', 'toText'), ('Nat', 'toInt'), ('Nat', 'toFloat'),
    ('Nat', 'toNat8'), ('Nat', 'toNat16'), ('Nat', 'toNat32'), ('Nat', 'toNat64'),
    # Int
    ('Int', 'toText'), ('Int', 'toNat'), ('Int', 'toFloat'),
    ('Int', 'toInt8'), ('Int', 'toInt16'), ('Int', 'toInt32'), ('Int', 'toInt64'),
    # Char
    ('Char', 'toNat32'), ('Char', 'toText'), ('Char', 'isDigit'),
    ('Char', 'isAlphabetic'), ('Char', 'isWhitespace'),
    ('Char', 'isLowercase'), ('Char', 'isUppercase'),
    # Blob
    ('Blob', 'toArray'), ('Blob', 'toVarArray'), ('Blob', 'isEmpty'),
    ('Blob', 'size'), ('Blob', 'hash'),
    # Text (zero-arg self functions)
    ('Text', 'toArray'), ('Text', 'toIter'), ('Text', 'toVarArray'),
    ('Text', 'encodeUtf8'), ('Text', 'decodeUtf8'), ('Text', 'size'),
    ('Text', 'isEmpty'), ('Text', 'reverse'), ('Text', 'toText'),
    # Array (zero-arg self functions)
    ('Array', 'toVarArray'), ('Array', 'size'), ('Array', 'isEmpty'),
    ('Array', 'keys'), ('Array', 'values'), ('Array', 'reverse'),
    ('Array', 'enumerate'),
    # VarArray (zero-arg self functions)
    ('VarArray', 'toArray'), ('VarArray', 'clone'), ('VarArray', 'size'),
    ('VarArray', 'isEmpty'), ('VarArray', 'keys'), ('VarArray', 'values'),
    ('VarArray', 'reverse'), ('VarArray', 'enumerate'),
    # List (zero-arg self functions)
    ('List', 'size'), ('List', 'toArray'), ('List', 'toVarArray'),
    ('List', 'isEmpty'), ('List', 'first'), ('List', 'last'),
    ('List', 'clone'), ('List', 'clear'), ('List', 'values'),
    ('List', 'keys'), ('List', 'enumerate'), ('List', 'reverse'),
    ('List', 'removeLast'),
]
Multi-Extra-Arg 関数(self + 追加引数)

self と追加の引数を受け取ります。Module.func(expr, a, b) を expr.func(a, b) に変換します。

注:これらの一部は、self だけで呼び出すこともできます(例:Array.flatten(xs) は追加引数を取りません)。複数引数用のハンドラーは両方のケースに対応する必要があります。self の後にカンマが見つからない場合は self.func() を出力し、カンマが見つかった場合は self.func(rest...) を出力します。

MULTI_EXTRA = [
    # Array
    ('Array', 'flatten'), ('Array', 'map'), ('Array', 'filter'),
    ('Array', 'filterMap'), ('Array', 'flatMap'),
    ('Array', 'foldLeft'), ('Array', 'foldRight'),
    ('Array', 'concat'), ('Array', 'sliceToArray'),
    ('Array', 'forEach'), ('Array', 'mapEntries'),
    ('Array', 'sort'), ('Array', 'find'), ('Array', 'findIndex'),
    ('Array', 'equal'), ('Array', 'compare'),
    ('Array', 'all'), ('Array', 'any'),
    ('Array', 'indexOf'), ('Array', 'contains'),
    ('Array', 'toText'), ('Array', 'binarySearch'),
    ('Array', 'isSorted'), ('Array', 'mapResult'), ('Array', 'range'),
    # VarArray
    ('VarArray', 'sortInPlace'), ('VarArray', 'map'), ('VarArray', 'filter'),
    ('VarArray', 'filterMap'), ('VarArray', 'flatMap'),
    ('VarArray', 'foldLeft'), ('VarArray', 'foldRight'),
    ('VarArray', 'concat'), ('VarArray', 'forEach'),
    ('VarArray', 'sort'), ('VarArray', 'find'), ('VarArray', 'findIndex'),
    ('VarArray', 'equal'), ('VarArray', 'compare'),
    ('VarArray', 'all'), ('VarArray', 'any'),
    ('VarArray', 'indexOf'), ('VarArray', 'contains'),
    ('VarArray', 'toText'), ('VarArray', 'mapResult'),
    # Text
    ('Text', 'replace'), ('Text', 'tokens'), ('Text', 'contains'),
    ('Text', 'split'), ('Text', 'startsWith'), ('Text', 'endsWith'),
    ('Text', 'concat'), ('Text', 'map'), ('Text', 'flatMap'),
    ('Text', 'foldLeft'), ('Text', 'join'),
    ('Text', 'trimStart'), ('Text', 'trimEnd'), ('Text', 'trim'),
    ('Text', 'stripStart'), ('Text', 'stripEnd'),
    # List
    ('List', 'add'), ('List', 'at'), ('List', 'get'), ('List', 'put'),
    ('List', 'map'), ('List', 'filter'), ('List', 'forEach'),
    ('List', 'sort'), ('List', 'find'), ('List', 'findIndex'),
    # Blob
    ('Blob', 'compare'), ('Blob', 'equal'),
]

新しい Core バージョン向けにカタログを更新する方法

core パッケージが更新されると、新しい関数が追加されたり、シグネチャが変更されたりする場合があります。実際のソースからカタログを再生成するには、次のシェルスニペットを実行します。

#!/bin/bash
# Usage: ./scan_self_functions.sh /path/to/.mops/[email protected]/src
CORE_SRC="${1:-.mops/[email protected]/src}"

echo "=== Functions WITH self as first parameter (convert to dot notation) ==="
for f in "$CORE_SRC"/*.mo; do
    module=$(basename "$f" .mo)
    # Match: public func name(self  OR  public let name = func(self
    grep -nE '^\s*public\s+(func|let)\s+\w+' "$f" | while read -r line; do
        # Extract function name
        fname=$(echo "$line" | sed -E 's/.*public\s+(func|let)\s+([a-zA-Z0-9_]+).*/\2/')
        # Check if first param is named 'self'
        if echo "$line" | grep -qE '\(\s*self\s*[:\)]'; then
            echo "  ($module, $fname)  -- has self"
        elif echo "$line" | grep -qE '=\s*func\s*\(\s*self\s*[:\)]'; then
            echo "  ($module, $fname)  -- has self (let)"
        fi
    done
done

echo ""
echo "=== Functions WITHOUT self (do NOT convert) ==="
for f in "$CORE_SRC"/*.mo; do
    module=$(basename "$f" .mo)
    grep -nE '^\s*public\s+(func|let)\s+\w+' "$f" | while read -r line; do
        fname=$(echo "$line" | sed -E 's/.*public\s+(func|let)\s+([a-zA-Z0-9_]+).*/\2/')
        if ! echo "$line" | grep -qE '\(\s*self\s*[:\)]' && \
           ! echo "$line" | grep -qE '=\s*func\s*\(\s*self\s*[:\)]'; then
            echo "  $module.$fname"
        fi
    done
done

重要: 一部の public let 宣言では、インラインのラムダ構文が使われています。

public let toNat = Prim.nat8ToNat;  // No parens visible — check Prim binding
public let encodeUtf8 : Text -> Blob = Prim.encodeUtf8;  // self is implicit

単一の値を受け取る Prim 関数を参照する public let バインディングについては、型シグネチャを確認してください。型が T -> U の場合、単一の引数が self です。これらはドット記法に対応しています。

スキャン後、各関数を次のように分類します。

  1. ZERO_EXTRA:self のみを受け取ります(ほかのパラメーターはありません)。ZERO_EXTRA リストに追加します。
  2. MULTI_EXTRA:self とほかのパラメーターを受け取ります。MULTI_EXTRA リストに追加します。
  3. NO_SELF:最初のパラメーター名が self ではありません。変換しないでください。

次に、変換スクリプト内の ZERO_EXTRA リストと MULTI_EXTRA リストを更新します。名前が同じローカル関数との誤一致を防ぐため、プロジェクトのソースファイルで実際に使用されている関数だけを記載してください。

変換スクリプト

これを convert_dot_notation.py として保存し、python3 convert_dot_notation.py で実行します。必要に応じて、ZERO_EXTRA、MULTI_EXTRA リストと src_dir のパスを編集してください。

#!/usr/bin/env python3
"""
Convert Module.func(self, ...) calls to self.func(...) dot notation.

Usage:
  1. Update ZERO_EXTRA and MULTI_EXTRA lists for your core version and project.
  2. Set src_dir to your project's source directory.
  3. Run: python3 convert_dot_notation.py
  4. Run tests: npx mops test
  5. If all pass, delete this script.
"""

import re
import os

# ──────────────────────────────────────────────────────────────────────
# CONFIGURATION: Update these lists per your core version and project.
# Only include (Module, func) pairs that are actually used in your code.
# ──────────────────────────────────────────────────────────────────────

# Functions that take ONLY self (zero extra args):
#   Module.func(self) -> self.func()
ZERO_EXTRA = [
    ('Nat8', 'toNat'), ('Nat8', 'toText'), ('Nat8', 'toNat32'), ('Nat8', 'toNat64'),
    ('Nat16', 'toNat'), ('Nat16', 'toText'), ('Nat16', 'toNat64'),
    ('Nat32', 'toNat'), ('Nat32', 'toText'), ('Nat32', 'toNat8'), ('Nat32', 'toNat64'),
    ('Nat64', 'toNat'), ('Nat64', 'toText'), ('Nat64', 'toNat8'), ('Nat64', 'toNat16'),
    ('Nat', 'toText'),
    ('Int', 'toText'), ('Int', 'toNat'),
    ('Char', 'toNat32'),
    ('Blob', 'toArray'),
    ('Text', 'toArray'), ('Text', 'encodeUtf8'), ('Text', 'decodeUtf8'),
]

# Functions that take self + possibly more args:
#   Module.func(self, rest...) -> self.func(rest...)
#   Module.func(self) -> self.func()  (when no extra args)
MULTI_EXTRA = [
    ('List', 'size'),
    ('List', 'toArray'),
    ('Array', 'flatten'),
    ('Array', 'map'),
    ('Array', 'filter'),
    ('Array', 'foldLeft'),
    ('Array', 'concat'),
    ('Array', 'sliceToArray'),
    ('Text', 'replace'),
    ('Text', 'tokens'),
    ('Text', 'contains'),
]

# ──────────────────────────────────────────────────────────────────────
# ENGINE: Do not modify below unless fixing bugs.
# ──────────────────────────────────────────────────────────────────────

def needs_parens(s):
    """Check if an expression needs wrapping in parens for safe dot notation.

    Returns True if the expression contains top-level infix operators that
    would cause `.func()` to bind incorrectly (dot binds tighter).
    """
    s = s.strip()
    if not s:
        return False

    # Simple identifier: no parens
    if re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', s):
        return False

    # Entire expression already wrapped in matching parens
    if s[0] == '(' and s[-1] == ')':
        depth = 0
        for i, c in enumerate(s):
            if c == '(':
                depth += 1
            elif c == ')':
                depth -= 1
            if depth == 0 and i < len(s) - 1:
                break
        else:
            return False

    # Entire expression is an array literal
    if s[0] == '[' and s[-1] == ']':
        depth = 0
        for i, c in enumerate(s):
            if c == '[':
                depth += 1
            elif c == ']':
                depth -= 1
            if depth == 0 and i < len(s) - 1:
                break
        else:
            return False

    # Numeric literal
    if re.match(r'^[0-9][0-9a-fA-FxX_]*$', s):
        return False

    # Scan for top-level infix operators
    depth_paren = 0
    depth_bracket = 0
    depth_brace = 0
    i = 0
    while i < len(s):
        c = s[i]
        if c == '(':
            depth_paren += 1
        elif c == ')':
            depth_paren -= 1
        elif c == '[':
            depth_bracket += 1
        elif c == ']':
            depth_bracket -= 1
        elif c == '{':
            depth_brace += 1
        elif c == '}':
            depth_brace -= 1
        elif c == '"':
            # Skip string literals
            i += 1
            while i < len(s) and s[i] != '"':
                if s[i] == '\\':
                    i += 1
                i += 1
        elif depth_paren == 0 and depth_bracket == 0 and depth_brace == 0:
            rest = s[i:]
            # Multi-char operators
            if (rest.startswith('>>') or rest.startswith('<<') or
                rest.startswith('==') or rest.startswith('!=') or
                rest.startswith('and ') or rest.startswith('or ') or
                rest.startswith('#')):
                return True
            # Single-char binary operators in infix position
            if c in '&|^%' and i > 0:
                return True
            if c in '+-' and i > 0 and s[i-1] not in '(,;:=[<>':
                return True
            if c in '*/' and i > 0:
                return True
        i += 1

    return False


def wrap_if_needed(s):
    """Wrap expression in parens if needed for dot notation."""
    if needs_parens(s):
        return '(' + s + ')'
    return s


def find_matching_paren(s, start):
    """Find matching closing paren for opening paren at position start.
    Returns the index of the closing paren, or -1 if not found.
    """
    depth = 1
    i = start + 1
    while i < len(s) and depth > 0:
        if s[i] == '(':
            depth += 1
        elif s[i] == ')':
            depth -= 1
        elif s[i] == '"':
            # Skip string literal
            i += 1
            while i < len(s) and s[i] != '"':
                if s[i] == '\\':
                    i += 1
                i += 1
        i += 1
    return i - 1 if depth == 0 else -1


def find_first_comma_at_depth0(s):
    """Find first comma at depth 0 (outside parens, brackets, braces, AND angle brackets).

    CRITICAL: Must track <> depth so commas inside type parameters like
    <Nat64, [Nat8]> are not treated as argument separators.
    """
    depth_paren = 0
    depth_bracket = 0
    depth_brace = 0
    depth_angle = 0
    i = 0
    while i < len(s):
        c = s[i]
        if c == '(':
            depth_paren += 1
        elif c == ')':
            depth_paren -= 1
        elif c == '[':
            depth_bracket += 1
        elif c == ']':
            depth_bracket -= 1
        elif c == '{':
            depth_brace += 1
        elif c == '}':
            depth_brace -= 1
        elif c == '<':
            depth_angle += 1
        elif c == '>' and depth_angle > 0:
            # Only decrement if we're inside angle brackets.
            # Bare > (comparison) should not affect depth.
            depth_angle -= 1
        elif c == '"':
            i += 1
            while i < len(s) and s[i] != '"':
                if s[i] == '\\':
                    i += 1
                i += 1
        elif (c == ',' and depth_paren == 0 and depth_bracket == 0
              and depth_brace == 0 and depth_angle == 0):
            return i
        i += 1
    return -1


def find_all_args(s):
    """Split content inside parens into (self_arg, rest_text).
    rest_text includes everything after the first depth-0 comma (preserving whitespace).
    """
    comma_pos = find_first_comma_at_depth0(s)
    if comma_pos == -1:
        return s.strip(), ""
    else:
        return s[:comma_pos].strip(), s[comma_pos + 1:]


def process_zero_extra(text, module, func):
    """Convert Module.func(arg) -> arg.func() for zero-extra-arg functions."""
    pattern = re.compile(
        r'\b' + re.escape(module) + r'\.' + re.escape(func) + r'(?=[\s<(])')

    result = text
    offset = 0
    changes = 0

    while True:
        m = pattern.search(result, offset)
        if not m:
            break

        pos = m.end()
        while pos < len(result) and result[pos] in ' \t\n':
            pos += 1

        # Optional type params <...>
        type_params = ""
        if pos < len(result) and result[pos] == '<':
            depth = 1
            tp_start = pos
            pos += 1
            while pos < len(result) and depth > 0:
                if result[pos] == '<':
                    depth += 1
                elif result[pos] == '>':
                    depth -= 1
                pos += 1
            type_params = result[tp_start:pos]
            while pos < len(result) and result[pos] in ' \t\n':
                pos += 1

        if pos >= len(result) or result[pos] != '(':
            offset = m.end()
            continue

        close = find_matching_paren(result, pos)
        if close == -1:
            offset = m.end()
            continue

        inner = result[pos + 1:close]
        self_arg = inner.strip()
        if not self_arg:
            offset = m.end()
            continue

        wrapped = wrap_if_needed(self_arg)
        replacement = f"{wrapped}.{func}{type_params}()"
        result = result[:m.start()] + replacement + result[close + 1:]
        offset = m.start() + len(replacement)
        changes += 1

    return result, changes


def process_multi_extra(text, module, func):
    """Convert Module.func(self, rest...) -> self.func(rest...) for multi-arg functions."""
    pattern = re.compile(
        r'\b' + re.escape(module) + r'\.' + re.escape(func) + r'(?=[\s<(])')

    result = text
    offset = 0
    changes = 0

    while True:
        m = pattern.search(result, offset)
        if not m:
            break

        pos = m.end()
        while pos < len(result) and result[pos] in ' \t\n':
            pos += 1

        type_params = ""
        if pos < len(result) and result[pos] == '<':
            depth = 1
            tp_start = pos
            pos += 1
            while pos < len(result) and depth > 0:
                if result[pos] == '<':
                    depth += 1
                elif result[pos] == '>':
                    depth -= 1
                pos += 1
            type_params = result[tp_start:pos]
            while pos < len(result) and result[pos] in ' \t\n':
                pos += 1

        if pos >= len(result) or result[pos] != '(':
            offset = m.end()
            continue

        close = find_matching_paren(result, pos)
        if close == -1:
            offset = m.end()
            continue

        inner = result[pos + 1:close]
        self_arg, rest = find_all_args(inner)
        if not self_arg:
            offset = m.end()
            continue

        wrapped = wrap_if_needed(self_arg)
        if rest == "":
            replacement = f"{wrapped}.{func}{type_params}()"
        else:
            replacement = f"{wrapped}.{func}{type_params}({rest})"

        result = result[:m.start()] + replacement + result[close + 1:]
        offset = m.start() + len(replacement)
        changes += 1

    return result, changes


def process_file(filepath):
    with open(filepath, 'r') as f:
        text = f.read()

    total_changes = 0

    for module, func in ZERO_EXTRA:
        text, changes = process_zero_extra(text, module, func)
        total_changes += changes

    for module, func in MULTI_EXTRA:
        text, changes = process_multi_extra(text, module, func)
        total_changes += changes

    if total_changes > 0:
        with open(filepath, 'w') as f:
            f.write(text)
        print(f"  {filepath}: {total_changes} changes")

    return total_changes


def main():
    # ── Change this to your project's source directory ──
    src_dir = os.path.join(os.path.dirname(__file__), 'src')
    total = 0
    for root, dirs, files in os.walk(src_dir):
        for f in sorted(files):
            if f.endswith('.mo'):
                filepath = os.path.join(root, f)
                total += process_file(filepath)
    print(f"\nTotal changes: {total}")


if __name__ == '__main__':
    main()

手順

  1. mops.toml から core のバージョンを特定します。

    grep 'core' mops.toml
    
  2. core のソースをスキャンして、関数カタログを作成または検証します。

    # List all public functions with self as first param
    grep -rnE 'public\s+(func|let)\s+\w+' .mops/core@*/src/*.mo | grep -E '\(\s*self\s*[:\)]'
    
  3. プロジェクトで実際に使用されている関数を特定します。

    # Find all Module.func( patterns in your source
    grep -rnoE '\b(Array|Blob|Text|List|VarArray|Nat8?|Nat16|Nat32|Nat64|Int|Char)\.[a-zA-Z]+\(' src/ | \
      sed 's/.*://' | sort | uniq -c | sort -rn
    

    誤一致を避けるため、使用されている関数だけをスクリプトに記載してください。

  4. core のソースでシグネチャを確認し、使用されている各関数を ZERO_EXTRA または MULTI_EXTRA に 分類します。

  5. 変換を実行します。

    python3 convert_dot_notation.py
    
  6. テストを実行します。

    npx mops test
    
  7. テストに失敗した場合は、次の項目を確認してください。

    • 演算子の優先順位に関する問題(中置式を囲むかっこが不足していないか)
    • 型パラメーターのカンマ分割(<> の深さ追跡に関するバグ)
    • 変換すべきでない関数が変換されていないか(self リストに含まれていない関数)
  8. 後片付け:すべてのテストに合格したら、convert_dot_notation.py を削除します。

検証チェックリスト

  • すべての Module.func(self, ...) 呼び出しが self.func(...) に変換されている
  • ファクトリー関数(tabulate、fromArray、fromVarArray、repeat、empty、range、fromNat、fromIntWrap など)が変換されていない
  • Array.fromVarArray(x) は x.toArray() に個別に変換されている(ドット記法スクリプトの対象外)
  • 中置式がかっこで囲まれている:(a >> b).toNat()、(x & y).toText()
  • 型パラメーターが保持されている:xs.map<Nat64, [Nat8]>(f) が破損していない
  • Array.tabulate<T> の型注釈が保持されている(コンパイラーでは推論できません)
  • コード内でモジュール名が明示的に使われていない場合でも、ドット記法の型に必要なインポート(Blob、Array、Nat、VarArray など)が残っている
  • ネストされたチェーンが正しい:xs.map<T, U>(f).flatten() が xs.map<T.flatten( U>( になっていない
  • npx mops test ですべてのテストに合格する

コメント