Loading...
Loading...
Use new dot-notation syntax in projects with mo:core dependency
npx skill4agent add research-ag/motoko-skills motoko-dot-notation-migrationModule.func(self, ...)self.func(...)coreselfmotoko-bitcoinbasecorecorecorebaseselfModule.func(self, arg2, arg3)self.func(arg2, arg3)selfArray.tabulateArray.fromVarArrayBlob.fromArrayselfModule.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().>><<&|+-*/%#==!=andor// 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().func()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, corrupteddepth_angle<>depth_paren()>depth_angle > 0selfArray: 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: trapArray.fromVarArray(x)x.toArray()VarArray.toArraysomeBlob.toArray()BlobBlobBlob.toArray().size()Sha256.fromArray(...).toArray()Array.flatten().foldLeft().map().sliceToArray()[T]Nat.toText().size()VarArray.toArray()[var T]Array.fromVarArrayfield X does not exist in type"..."\"42.toText()0xFF.toNat()selfModule.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'),
]selfModule.func(expr, a, b)expr.func(a, b)Array.flatten(xs)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#!/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" | 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
donepublic letpublic let toNat = Prim.nat8ToNat; // No parens visible — check Prim binding
public let encodeUtf8 : Text -> Blob = Prim.encodeUtf8; // self is implicitpublic letT -> UselfselfselfselfZERO_EXTRAMULTI_EXTRAconvert_dot_notation.pypython3 convert_dot_notation.pyZERO_EXTRAMULTI_EXTRAsrc_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()mops.tomlgrep 'core' mops.toml# 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*[:\)]'# 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 -rnpython3 convert_dot_notation.pynpx mops test<>selfconvert_dot_notation.pyModule.func(self, ...)self.func(...)tabulatefromArrayfromVarArrayrepeatemptyrangefromNatfromIntWrapArray.fromVarArray(x)x.toArray()(a >> b).toNat()(x & y).toText()xs.map<Nat64, [Nat8]>(f)Array.tabulate<T>BlobArrayNatVarArrayxs.map<T, U>(f).flatten()xs.map<T.flatten( U>(npx mops test