Migrating to the const/let keyword scheme
Harn's variable-binding keywords follow the TypeScript and Swift convention.
Migrate every .harn source file that predates this change.
| Before | After | Meaning |
|---|---|---|
let x = … (immutable) | const x = … | Immutable binding (the default) |
var x = … (mutable) | let x = … | Mutable binding (reassignable) |
const NAME = … (compile-time) | const NAME = … | Unchanged spelling; see below |
var keyword | Removed | Using it is a compile error with a migration hint |
The old scheme used let for immutable bindings and var for mutable ones.
Now const is the immutable default and let is mutable, matching TypeScript
and Swift.
Semantics#
constmeans this binding's value never changes. It is the common case: reach for it by default and useletonly when the value must change.letis a mutable binding. Reassignment and field/index mutation are allowed (let o = {}; o.a = 1).- The keyword spelling follows TypeScript; the rule follows Swift. The
two agree on reassignment but disagree on collection contents, so it is worth
being exact. TypeScript's
const o = {}; o.a = 1is legal, because a TS object is a reference andconstconstrains only the binding. Harn's collections are values, soo.a = 1changeso's whole value and requireslet, the same position Swift takes for the same reason. Methods such asappendingreturn a new value and modify nothing, so they remain fine on aconst. See Binding mutability for the full rule. constnow accepts any initializer. Previouslyconstwas a strict compile-time constant that rejected impure or non-foldable initializers. Becauseconstis now the default immutable binding, that restriction is gone:const user = fetch_user()is fine. When the initializer happens to be in the pure const-eval subset it is still folded at compile time, but this is a transparent optimization. It never changes observable behavior, and an impure or erroring initializer is simply not folded (it is not a compile error).const z = 1 / 0errors at runtime, exactly likelet z = 1 / 0.varis removed. It is retained as a reserved word only so that using it produces a clear migration diagnostic pointing atlet/const.
Before#
let name = "ada" // immutable; old `let` was the immutable keyword
var count = 0 // mutable; old `var` was the mutable keyword
count = count + 1
After#
const name = "ada" // immutable
let count = 0 // mutable
count = count + 1
Automated migration#
The rename is fully mechanical. Use harn codemod with these two rules, run
in order (let→const first, then var→let):
# 01-let-to-const.toml
id = "harn-let-to-const"
language = "harn"
fix = "const"
fixTarget = "kw"
[rule]
query = '(let_binding "let" @kw) @__match'
# 02-var-to-let.toml
id = "harn-var-to-let"
language = "harn"
fix = "let"
fixTarget = "kw"
[rule]
query = '(var_binding "var" @kw) @__match'
harn codemod --apply --allow-unsafe --rule 01-let-to-const.toml .
harn codemod --apply --allow-unsafe --rule 02-var-to-let.toml .
These rules rewrite only the binding keyword. Type annotations, destructuring
patterns, and initializers are preserved byte-for-byte, and text inside strings
and comments is never touched. Run let→const first: running var→let
first would let the first rule re-match the freshly-minted lets and wrongly
turn mutable bindings into const.