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(...)。面向升级 core 或从 base 迁移的开发者,提供函数目录、转换脚本及常见错误检查方法。

主要功能

  1. 01core 2.2.0 函数目录
  2. 02Python 转换脚本
  3. 03运算符优先级检查
  4. 04类型参数逗号处理
  5. 05迁移步骤与验证清单

适用场景

  1. 01将项目从 base 迁移到 core
  2. 02升级 core 时更新调用语法
  3. 03检查点号记法转换错误

安装

复制下面的命令,在终端中运行。

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

安装后在 Claude Code 中输入/motoko-dot-notation-migration确认已加载。

详细介绍

Motoko 点表示法迁移

目的

将 core 包中首个参数名为 self 的所有函数调用,从旧的 Module.func(self, ...) 风格转换为新的 self.func(...) 点表示法。

此技能源自 motoko-bitcoin 项目的成功迁移。它包含 core 2.2.0 的完整函数目录、经过实战检验的 Python 转换脚本,以及必须处理的所有关键陷阱。

适用场景

  • 将 Motoko 项目从 base 迁移到 core,或升级 core 版本时
  • 用户要求将函数调用转换为点表示法时
  • 用户要求现代化 Motoko 调用语法时

背景

在 Motoko 的 core 包(base 的后继版本)中,许多模块函数将首个参数声明为 self。这些函数支持点表示法:可以写成 self.func(arg2, arg3),而不是 Module.func(self, 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 表达式包含任何顶层中缀运算符,在末尾添加 .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 的参数。将 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'),
]
多额外参数函数(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 包时,可能会添加新函数或更改函数签名。运行此 shell 代码段,从实际源代码重新生成目录:

#!/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 声明使用内联 lambda 语法:

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(没有其他参数)。放入零额外参数列表。
  2. MULTI_EXTRA:接收 self 以及其他参数。放入多额外参数列表。
  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

评论

支持 Markdown