Migrating old scripts¶
Phonometrica’s scripting engine was rewritten from the ground up. The new
language keeps the overall look and feel of the old one — newline-terminated
statements, end-delimited blocks, 1-based indexing — but a number of
constructs changed, some silently. This page lists everything you need to know
to update scripts written for the old engine. Each entry shows the old form and
its replacement.
The single most important change is that names are now resolved when a script is compiled, not when it runs: calling an unknown function or reading an undeclared variable is an error before the first statement executes. As a result, most broken scripts fail immediately and loudly rather than midway through an analysis.
Declarations and scope¶
Use var instead of let, and declare before you assign.
Assignment never declares a variable in a script file: assigning to an unknown
name is a compile error. (In the console, bare assignment still creates a
session variable, as before.)
# Old # New
let x = 10 var x = 10
y = 20 # created y var y = 20 # required in a script
const declares an immutable binding. At the top level of a script, var
creates a module binding (visible to code that imports the module), local
var a module-private one, and global var an isolate-global. local
also applies to top-level function and class declarations.
Multiple assignment and destructuring are gone. Declare one name at a time.
The only multi-binding form is for k, v in table.
Control flow¶
Use for ... in instead of foreach, and step -1
instead of downto:
# Old # New
foreach x in xs do for x in xs do
print x print(x)
end end
for i = 10 downto 1 do for i = 10 to 1 step -1 do
... ...
end end
Counted loops (for i = a to b [step s]) have inclusive bounds; a loop whose
direction contradicts its step runs zero times. for k, v in table iterates
key/value pairs.
List comprehensions follow the same rule — they are written with for,
not foreach, so they read like the loop statement they replace:
# Old # New
[y foreach x in xs] [y for x in xs]
[y foreach x in xs if c] [y for x in xs if c]
[k & v foreach k, v in t] [k & v for k, v in t]
An if cond clause filters, so the yield expression is not evaluated on a
rejected iteration. if cond else other yields on every iteration instead,
so the result keeps the length of the collection. The two-variable form gives
key/value pairs over a table and index/value pairs over a list.
One further difference: the collection accepts any expression without
parentheses. The old engine required [y foreach x in (xs if c else zs)]
because its conditional was a postfix if, which would otherwise have
swallowed the comprehension’s own filter; this engine’s conditional is the
prefix if c then a else b end, so the ambiguity does not arise.
Truthiness: null is the only non-boolean value that counts as false.
0, "" and [] are all true. The common idiom if sound then for
functions that return an object or null keeps working.
A condition may declare the value it tests, which is new in this engine — the old one had no equivalent:
if var m = match(re, line) then # instead of naming it on the line above
print(group(m, 0))
end
while var task = next_task() do
handle(task)
end
Available in if, elsif and while, and only with var. The name is
scoped to the branch it guards: it is not visible after the statement, in the
else branch, or in a later elsif condition. In a while the
initializer is re-evaluated on every iteration, continue included. Because
the rule above makes null the only falsy non-boolean, this composes with any
function that returns a result or null — but not with one that signals
exhaustion with "" or 0, which are true.
Strings¶
Interpolation is {expr} instead of ${expr}, and only
double-quoted strings interpolate. Single-quoted strings are raw: no
interpolation and no escape processing, which makes them ideal for regular
expressions and Windows paths. Escape a literal brace in a double-quoted string
as \{; unknown escape sequences are errors.
# Old # New
print "value: ${x}" print("value: {x}")
let pat = "\\d+" var pat = '\d+'
String mutators work in place and return nothing. Functions such as
trim, rtrim, ltrim and append modify their first argument
directly (it is a ref parameter). Old code that used their return value
must be restructured:
# Old # New
let s = trim(line) var s = line
trim(s)
Printing¶
print is a regular function, not a statement. Its arguments are
separated by a single space by default; use string interpolation or
concatenation (&) to control the output precisely.
# Old # New
print "F0: ", f0, " Hz" print("F0: {f0} Hz")
Functions¶
Function declaration syntax is unchanged, but the semantics are richer: every named function is a generic function, and declaring two functions with the same name and different parameter types adds overloads selected by the argument types at each call.
Optional parameters with defaults are keyword-only: a parameter declared
floor as Float = 70can only be filled aspitch(snd, floor = 50), never positionally.Anonymous functions:
function (x) ... end, or the lambda arrow for a single expression:x -> x * 2.Named functions are first-class: they can be stored in variables and passed to functions such as
connect.By-reference parameters are declared with
refin the function signature (function normalize(ref x as Array)); arguments are passed normally at the call site (normalize(samples)).
Classes¶
Field declarations now use the field keyword, and the constructor is named
init (it was initialize):
# Old # New
class Point class Point
x = 0 field x = 0
y = 0 field y = 0
function initialize(x, y) method init(x as Number, y as Number)
this.x = x this.x = x
this.y = y this.y = y
end end
end end
Class bodies contain only fields and a closed set of method hooks
(init, to_string, get_item, set_item, iterate, next);
all other behaviour is written as ordinary functions taking the instance as
their first parameter. class declares a value class (copy-on-write value
semantics); ref class declares a reference class with identity semantics.
Use class Sub is Base for single inheritance (it was inherits).
Equality between class instances is identity-based: two independently
constructed instances with equal field values compare unequal, for both value
and reference classes. (The old engine compared value classes structurally.)
Test dynamic types with the is operator: x is List replaces
type(x) == type([]).
Errors¶
Only Error values can be thrown, and a caught error is an object,
not a string:
# Old # New
throw "bad input" throw Error("bad input")
catch e do catch e
print e print(e.message)
end end
An Error carries message (the text), trace (a formatted backtrace
string) and frames (a list of {function, line, file} tables). There is
no rethrow: throw e inside a catch block preserves the original
backtrace.
Two arithmetic changes in the same spirit: float division by zero yields
inf instead of raising an error, while integer division by zero (1 div
0) raises a math error. The % operator no longer exists; use mod.
Modules and imports¶
import is a compile-time statement, not a function:
# Old # New
let M = import("../lib/mytools") import mytools
Imports are resolved by module name on the module search path, not by relative path expression.
A missing module is a compile error; you cannot wrap
importintry/catch.The top-level code of an imported module runs before the importing script’s own statements.
First-class module values are gone (
Module("name")no longer exists). A module’s publicvar/constbindings are accessed qualified (mytools.x); its public functions become globally visible generic functions.Top-level helpers that should not be visible to other modules must be declared
local function(this is what replaces private module state).
Renamed and changed functions¶
Old |
New |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Behavioural changes to keep in mind:
Regular expressions use a stateless API:
Regex(pattern[, flags])builds a pattern,match(re, subject)returns aMatchobject ornull, andgroup(m, i),group_start,group_endandgroup_countinspect it.group_countincludes group 0 (the whole match), unlike the oldcount().lenapplies to lists, strings, tables, sets and arrays (element count); usenrow/ncol/ndimfor array shapes.TableandSetare unordered: iteration and key order are unspecified. Sort keys explicitly when order matters.sorted_findreturns 0 when the value is absent (it used to return the insertion slot).intersect,uniteandsubtracton lists no longer require sorted inputs.min/maxon an empty array raise an error.Numbers distinguish
IntegerandFloat(1vs1.0);_digit separators and scientific notation are supported. Float literals need a digit after the decimal point (write2.0, not2.).Compound assignment (
+=and friends) works on variables and subscripts (xs[i] += 1), but not on fields:obj.f += 1(includingtable.key += 1) is a compile error — writeobj.f = obj.f + 1.
Getting help¶
If a script fails after migration, run it from a terminal:
phonometrica -r my_script.phon
Compile-time errors point at the offending line; runtime errors print the full call-stack trace. The same trace appears in the console inside Phonometrica, and the script editor highlights the failing line.