Use new dot-notation syntax in projects with mo:core dependency
Instalação
Instalar com Codex ou Claude Copie este prompt, cole no Codex, Claude ou outro assistente e deixe que ele revise a página da skill e instale para você.
Use new dot-notation syntax in projects with mo:core dependency
Motoko dot notation migration
Purpose
Convert Motoko function calls from the old Module.func(self, ...) style to the new self.func(...) dot notation, for all functions in the core package whose first parameter is named self.
This skill was created from a successful migration of the motoko-bitcoin project. It contains the complete function catalog for core 2.2.0, a battle-tested Python conversion script, and all the critical pitfalls that must be handled.
When to Use
When migrating a Motoko project from base to core, or upgrading core versions
When the user asks to convert function calls to dot notation
When the user asks to modernize Motoko call syntax
Background
In Motoko's core package (successor to base), many module functions declare their first parameter as self. These functions support dot notation: instead of writing Module.func(self, arg2, arg3), you can write self.func(arg2, arg3).
Only functions whose first parameter is literally named self support this. Factory functions like Array.tabulate, Array.fromVarArray, Blob.fromArray, etc. do NOT have self as first parameter and must NOT be converted.
1. Operator Precedence — Dot Binds Tighter Than Infix
In Motoko, . binds tighter than all infix operators (>>, <<, &, |, +, -, *, /, %, #, ==, !=, 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()
Rule: If the self-expression contains ANY top-level infix operator, wrap it in parentheses before appending .func().
2. Type Parameters Contain Commas — Track Angle Bracket Depth
When splitting Module.func(self, rest...) at the first comma, you MUST track <> angle bracket depth. Type parameters like <Nat64, [Nat8]> contain commas that are NOT argument separators.
// 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
Rule: In the comma-finder, track depth_angle for <> just like depth_paren for (). Only decrement on > when depth_angle > 0 (to avoid confusing comparison operators with closing angle brackets).
3. Functions That Must NOT Be Converted
These functions do NOT have self as their first parameter. Do not convert them:
However, Array.fromVarArray(x) has a dot-notation equivalent: x.toArray() (via VarArray.toArray). This is NOT handled by the dot-notation conversion script — it's a separate refactor (see motoko-core-code-improvements skill, Section H).
4. Dot-Notation Requires the Type's Module to Be Imported
When calling a method via dot-notation on a value (e.g., someBlob.toArray()), the module for that value's type (Blob) must still be imported, even though the name Blob doesn't appear explicitly in the calling code.
Common examples where the import looks unused but is required:
Blob — for .toArray(), .size() on Blob return values (e.g., Sha256.fromArray(...).toArray())
Array — for .flatten(), .foldLeft(), .map(), .sliceToArray() on [T] values
Nat — for .toText() on Nat values from .size() calls
VarArray — for .toArray() on [var T] values (after converting Array.fromVarArray)
If you remove these imports, compilation will fail with field X does not exist in type and a hint to import the module.
5. String Literals Must Be Skipped
When scanning for operators or commas, skip over string literals ("...") including escaped quotes (\"), so operators inside strings don't trigger false positives.
6. Numeric Literals Are Safe Without Parens
42.toText() and 0xFF.toNat() are valid Motoko — numeric literals can receive dot notation directly. No parens needed.
Function Catalog (core 2.2.0)
Zero-Extra-Arg Functions (self only)
These take exactly one argument named self. Convert Module.func(expr) → expr.func().
These take self plus additional arguments. Convert Module.func(expr, a, b) → expr.func(a, b).
Note: Some of these CAN be called with just self (e.g. Array.flatten(xs) has no extra args). The multi-arg handler must handle both cases: if no comma is found after self, emit self.func(); if comma is found, emit self.func(rest...).
When the core package is updated, new functions may have been added or signatures changed. Run this shell snippet to regenerate the catalog from the actual source:
#!/bin/bash# Usage: ./scan_self_functions.sh /path/to/.mops/core@X.Y.Z/src
CORE_SRC="${1:-.mops/core@2.2.0/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" | whileread -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'ifecho"$line" | grep -qE '\(\s*self\s*[:\)]'; thenecho" ($module, $fname) -- has self"elifecho"$line" | grep -qE '=\s*func\s*\(\s*self\s*[:\)]'; thenecho" ($module, $fname) -- has self (let)"fidonedoneecho""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" | whileread -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*[:\)]'; thenecho" $module.$fname"fidonedone
Important: Some public let declarations use inline lambda syntax:
public let toNat = Prim.nat8ToNat; // No parens visible — check Prim binding
public let encodeUtf8 : Text -> Blob = Prim.encodeUtf8; // self is implicit
For public let bindings that reference Prim functions taking a single value, check the type signature: if it's T -> U, the single argument is self. These support dot notation.
After scanning, classify each function:
ZERO_EXTRA: Takes only self (no other params). Goes in the zero-extra list.
MULTI_EXTRA: Takes self plus other params. Goes in the multi-extra list.
NO_SELF: First param is NOT named self. Do NOT convert.
Then update the ZERO_EXTRA and MULTI_EXTRA lists in the conversion script below, limiting to only the functions actually used in your project's source files (to avoid false matches on identically-named local functions).
Conversion Script
Save this as convert_dot_notation.py and run with python3 convert_dot_notation.py. Edit the ZERO_EXTRA, MULTI_EXTRA lists and src_dir path as needed for your project.
#!/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.# ──────────────────────────────────────────────────────────────────────defneeds_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()
ifnot s:
returnFalse# Simple identifier: no parensif re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', s):
returnFalse# Entire expression already wrapped in matching parensif s[0] == '('and s[-1] == ')':
depth = 0for i, c inenumerate(s):
if c == '(':
depth += 1elif c == ')':
depth -= 1if depth == 0and i < len(s) - 1:
breakelse:
returnFalse# Entire expression is an array literalif s[0] == '['and s[-1] == ']':
depth = 0for i, c inenumerate(s):
if c == '[':
depth += 1elif c == ']':
depth -= 1if depth == 0and i < len(s) - 1:
breakelse:
returnFalse# Numeric literalif re.match(r'^[0-9][0-9a-fA-FxX_]*$', s):
returnFalse# Scan for top-level infix operators
depth_paren = 0
depth_bracket = 0
depth_brace = 0
i = 0while i < len(s):
c = s[i]
if c == '(':
depth_paren += 1elif c == ')':
depth_paren -= 1elif c == '[':
depth_bracket += 1elif c == ']':
depth_bracket -= 1elif c == '{':
depth_brace += 1elif c == '}':
depth_brace -= 1elif c == '"':
# Skip string literals
i += 1while i < len(s) and s[i] != '"':
if s[i] == '\\':
i += 1
i += 1elif depth_paren == 0and depth_bracket == 0and depth_brace == 0:
rest = s[i:]
# Multi-char operatorsif (rest.startswith('>>') or rest.startswith('<<') or
rest.startswith('==') or rest.startswith('!=') or
rest.startswith('and ') or rest.startswith('or ') or
rest.startswith('#')):
returnTrue# Single-char binary operators in infix positionif c in'&|^%'and i > 0:
returnTrueif c in'+-'and i > 0and s[i-1] notin'(,;:=[<>':
returnTrueif c in'*/'and i > 0:
returnTrue
i += 1returnFalsedefwrap_if_needed(s):
"""Wrap expression in parens if needed for dot notation."""if needs_parens(s):
return'(' + s + ')'return s
deffind_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 + 1while i < len(s) and depth > 0:
if s[i] == '(':
depth += 1elif s[i] == ')':
depth -= 1elif s[i] == '"':
# Skip string literal
i += 1while i < len(s) and s[i] != '"':
if s[i] == '\\':
i += 1
i += 1
i += 1return i - 1if depth == 0else -1deffind_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 = 0while i < len(s):
c = s[i]
if c == '(':
depth_paren += 1elif c == ')':
depth_paren -= 1elif c == '[':
depth_bracket += 1elif c == ']':
depth_bracket -= 1elif c == '{':
depth_brace += 1elif c == '}':
depth_brace -= 1elif c == '<':
depth_angle += 1elif c == '>'and depth_angle > 0:
# Only decrement if we're inside angle brackets.# Bare > (comparison) should not affect depth.
depth_angle -= 1elif c == '"':
i += 1while i < len(s) and s[i] != '"':
if s[i] == '\\':
i += 1
i += 1elif (c == ','and depth_paren == 0and depth_bracket == 0and depth_brace == 0and depth_angle == 0):
return i
i += 1return -1deffind_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:]
defprocess_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 = 0whileTrue:
m = pattern.search(result, offset)
ifnot 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 += 1while pos < len(result) and depth > 0:
if result[pos] == '<':
depth += 1elif result[pos] == '>':
depth -= 1
pos += 1
type_params = result[tp_start:pos]
while pos < len(result) and result[pos] in' \t\n':
pos += 1if 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()
ifnot 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 += 1return result, changes
defprocess_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 = 0whileTrue:
m = pattern.search(result, offset)
ifnot 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 += 1while pos < len(result) and depth > 0:
if result[pos] == '<':
depth += 1elif result[pos] == '>':
depth -= 1
pos += 1
type_params = result[tp_start:pos]
while pos < len(result) and result[pos] in' \t\n':
pos += 1if 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)
ifnot 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 += 1return result, changes
defprocess_file(filepath):
withopen(filepath, 'r') as f:
text = f.read()
total_changes = 0for 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:
withopen(filepath, 'w') as f:
f.write(text)
print(f" {filepath}: {total_changes} changes")
return total_changes
defmain():
# ── Change this to your project's source directory ──
src_dir = os.path.join(os.path.dirname(__file__), 'src')
total = 0for root, dirs, files in os.walk(src_dir):
for f insorted(files):
if f.endswith('.mo'):
filepath = os.path.join(root, f)
total += process_file(filepath)
print(f"\nTotal changes: {total}")
if __name__ == '__main__':
main()
Step-by-Step Procedure
Identify the core version from mops.toml:
grep 'core' mops.toml
Scan the core source to build/verify the function catalog:
# 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*[:\)]'
Determine which functions your project actually uses:
# 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
Only include used functions in the script to avoid false matches.
Classify each used function as ZERO_EXTRA or MULTI_EXTRA by checking its signature in the core source.
Run the conversion:
python3 convert_dot_notation.py
Run tests:
npx mops test
If tests fail, check for:
Operator precedence issues (missing parens around infix expressions)
Type parameter comma splitting (the <> depth tracking bug)
Functions that shouldn't have been converted (not in the self list)
Clean up: Delete convert_dot_notation.py after all tests pass.
Verification Checklist
All Module.func(self, ...) calls converted to self.func(...)