Migrating to the const/let keyword scheme
Harn's variable-binding keywords now follow the TypeScript / Swift
convention. This is a breaking change to every .harn source file.
| 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 inverted the universal JS/TS meaning: let was immutable
and var was the mutable form. Now const is the immutable default and let
is the mutable one, matching what every editor, linter, and code model
expects.
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.