Hi everyone,
I’m stuck on a Motoko compile error that I’ve not been able to resolve after multiple clean rebuilds, and I’d appreciate guidance from someone more experienced with Motoko syntax and compiler behavior.
Environment
-
OS: Windows (WSL Ubuntu)
-
dfx version: 0.30.2
-
Network: local replica
-
Project type: Motoko backend + JS/HTML frontend
-
Command used:
dfx stop
dfx start --clean
dfx deploy
The Error I Keep Getting
Every deploy fails with variations of this error:
syntax error [M0001], unexpected token ';'
syntax error [M0001], unexpected token 'stable'
Example:
/home/.../main.mo:15.3-15.9: syntax error, unexpected token 'stable'
or
/home/.../main.mo:22.59-22.60: syntax error, unexpected token ';'
What I’ve Tried
-
Removed semicolons from variable declarations
-
Ran
dfx start --clean -
Deleted
.dfxdirectory -
Reduced the file to minimal examples (which do compile)
-
Re-added code gradually, but the error returns once stable variables or upgrade hooks are involved
Current main.mo (backend)
import Debug "mo:base/Debug";
import Time "mo:base/Time";
import Principal "mo:base/Principal";
import HashMap "mo:base/HashMap";
import Iter "mo:base/Iter";
import List "mo:base/List";
import Array "mo:base/Array";
actor DBank {
/* -----------------------------
STABLE STORAGE
-------------------------------- */
stable var balancesStable : [(Principal, Int)] = []
stable var timestampsStable : [(Principal, Int)] = []
stable var platformEarningsStable : Int = 0
stable var transactionLogsStable : [(Principal, Text)] = []
/* -----------------------------
TRANSIENT STORAGE
-------------------------------- */
transient var balances : HashMap.HashMap<Principal, Int>
transient var lastUpdated : HashMap.HashMap<Principal, Int>
transient var platformEarnings : Int
transient var transactionLogs : [(Principal, Text)]
/* -----------------------------
CONFIG
-------------------------------- */
let INTEREST_BP : Int = 500;
let PLATFORM_FEE_BP : Int = 1000;
let BP_DIVISOR : Int = 10_000;
let YEAR_NS : Int = 31_536_000_000_000_000;
/* -----------------------------
INIT
-------------------------------- */
{
balances := HashMap.fromIter(
balancesStable.vals(),
10,
Principal.equal,
Principal.hash
);
lastUpdated := HashMap.fromIter(
timestampsStable.vals(),
10,
Principal.equal,
Principal.hash
);
platformEarnings := platformEarningsStable;
transactionLogs := transactionLogsStable;
}
/* -----------------------------
UPGRADE HOOKS
-------------------------------- */
system func preupgrade() {
balancesStable := Iter.toArray(balances.entries());
timestampsStable := Iter.toArray(lastUpdated.entries());
platformEarningsStable := platformEarnings;
transactionLogsStable := transactionLogs;
};
system func postupgrade() {
balances := HashMap.fromIter(
balancesStable.vals(),
10,
Principal.equal,
Principal.hash
);
lastUpdated := HashMap.fromIter(
timestampsStable.vals(),
10,
Principal.equal,
Principal.hash
);
platformEarnings := platformEarningsStable;
transactionLogs := transactionLogsStable;
};
/* -----------------------------
INTEREST
-------------------------------- */
func applyInterest(user : Principal) {
let now = Time.now();
let lastTime = switch (lastUpdated.get(user)) {
case (?t) t;
case null now;
};
let elapsed = now - lastTime;
if (elapsed <= 0) return;
let balance = switch (balances.get(user)) {
case (?b) b;
case null 0;
};
let rawInterest =
((balance * INTEREST_BP * elapsed) / YEAR_NS) / BP_DIVISOR;
if (rawInterest <= 0) {
lastUpdated.put(user, now);
return;
};
let fee = (rawInterest * PLATFORM_FEE_BP) / BP_DIVISOR;
let userInterest = rawInterest - fee;
balances.put(user, balance + userInterest);
platformEarnings += fee;
lastUpdated.put(user, now);
};
/* -----------------------------
TOP UP
-------------------------------- */
public func topUp(user : Principal, amount : Int)
: async { success : Bool; message : Text; balance : Int } {
if (amount <= 0) {
return {
success = false,
message = "Top-up amount must be greater than 0",
balance = switch (balances.get(user)) {
case (?b) b;
case null 0;
}
};
};
applyInterest(user);
let current = switch (balances.get(user)) {
case (?b) b;
case null 0;
};
let newBalance = current + amount;
balances.put(user, newBalance);
lastUpdated.put(user, Time.now());
let log = "Top-up: $" # debug_show(amount);
transactionLogs := Array.append(transactionLogs, [(user, log)]);
Debug.print(log);
return {
success = true,
message = "You have successfully topped up $" # debug_show(amount),
balance = newBalance
};
};
/* -----------------------------
WITHDRAW
-------------------------------- */
public func withdraw(user : Principal, amount : Int)
: async { success : Bool; message : Text; balance : Int } {
if (amount <= 0) {
return {
success = false,
message = "Withdrawal amount must be greater than 0",
balance = switch (balances.get(user)) {
case (?b) b;
case null 0;
}
};
};
applyInterest(user);
let current = switch (balances.get(user)) {
case (?b) b;
case null 0;
};
if (amount > current) {
let log = "Withdrawal failed: insufficient funds";
transactionLogs := Array.append(transactionLogs, [(user, log)]);
Debug.print(log);
return {
success = false,
message = "Insufficient balance",
balance = current
};
};
let newBalance = current - amount;
balances.put(user, newBalance);
lastUpdated.put(user, Time.now());
let log = "Withdrawal: $" # debug_show(amount);
transactionLogs := Array.append(transactionLogs, [(user, log)]);
Debug.print(log);
return {
success = true,
message = "You have successfully withdrawn $" # debug_show(amount),
balance = newBalance
};
};
/* -----------------------------
READ
-------------------------------- */
public query func getBalance(user : Principal) : async Int {
switch (balances.get(user)) {
case (?b) b;
case null 0;
};
};
public query func getTransactionLogs(user : Principal) : async [Text] {
transactionLogs
|> List.fromArray
|> List.filter(func (e) { e.0 == user })
|> List.map(func (e) { e.1 })
|> List.toArray;
};
}
My Questions
-
Are
stable+transientvariables inside the same actor still valid in Motoko 0.30.x? -
Is the anonymous actor init block
{ ... }still correct syntax? -
Are there breaking syntax changes I’m missing compared to older Motoko examples?
-
Is there a recommended minimal pattern for
stable+HashMap+ upgrade hooks that definitely compiles today?
At this point I’m not trying to add features I just want a correct, compiling Motoko baseline that follows best practices.
Thanks in advance ![]()
Any help or reference example would be massively appreciated.