You bind a name to a value with let or
const. A let can be reassigned; a
const cannot, and the value it holds is frozen, so it can't
be changed either. Both are block-scoped, and both can be declared at
the top level of a file where functions in the same file can see
them.
let score = 10
score = score + 5
print(score)
const MAX_HP = 100
print(MAX_HP)
Run it:
$ scua basics.scua
15
100
#let is reassignable
A let binding can be assigned a new value as many times
as you like. Assignment uses the name on its own, without
let:
let score = 10
score = score + 5
Use const for a value that should never change after
it's set. By convention SCUA programs name constants in upper case, but
that's a habit, not a rule.
#const is immutable
Reassigning a const is a compile error, caught before
the program runs:
const MAX_HP = 100
MAX_HP = 120
Run it:
$ scua const-error.scua
scua: const-error.scua:2: cannot assign to const 'MAX_HP'
The error names the line and the binding.
#const freezes the value
A const fixes the value as well as the name. A table,
array or record held in a const is frozen: its fields and
elements can't be changed, added or removed, and the same goes for every
table and array inside it. A write the compiler can see is a compile
error:
const CONFIG = { speed = 4, tags = ["fast"] }
CONFIG.speed = 5
Run it:
$ scua const-frozen.scua
scua: const-frozen.scua:2: `CONFIG` is a frozen constant, so it can't be changed: `const` in SCUA freezes the value, not just the name. Use `let` for a value you change
A write the compiler can't see, such as one inside a function that was handed the constant, stops the program when it runs. The message still names the constant:
const CONFIG = { speed = 4 }
fn tweak(c)
c.speed = 5
end
tweak(CONFIG)
Run it:
$ scua const-fault.scua
scua: const-fault.scua:4: `CONFIG` is a frozen constant, so it can't be changed: `const` in SCUA freezes the value, not just the name. Use `let` for a value you change
at tweak (const-fault.scua:4)
at main (const-fault.scua:7)
If you come from JavaScript, this is the difference to remember:
const xs = [] followed by xs.push(1) is an
error in SCUA. Use let for a list you add to.
A const holds a frozen copy. The value you gave
it is left alone, so another name that refers to the same table can
still change it, and the constant doesn't see the change:
let settings = { volume = 3 }
const DEFAULTS = settings
settings.volume = 9
print(DEFAULTS.volume, settings.volume)
Run it:
$ scua const-copy.scua
3 9
This is true wherever the const is written, at the top
of a file or inside a function.
#Build a table, then freeze it
To make a constant you fill step by step, build it in a
let, then freeze it with freeze.
freeze(value) returns a frozen copy:
let by_name = {}
for item in [{ name = "sword", damage = 5 }, { name = "bow", damage = 3 }] do
by_name[item.name] = item
end
const BY_NAME = freeze(by_name)
print(BY_NAME.sword.damage)
Run it:
$ scua build-then-freeze.scua
5
Filling the const directly is refused, and the error
shows that fix:
const BY_NAME = {}
for item in [{ name = "sword", damage = 5 }] do
BY_NAME[item.name] = item
end
Run it:
$ scua fill-const.scua
scua: fill-const.scua:3: `BY_NAME` is frozen, so it can't be filled after it's declared. Build it as a `let`, then freeze it: `let by_name = {}` … `const BY_NAME = freeze(by_name)`
A module's top level can't hold a let, so a module
builds the table in a function and keeps what the function returns. This
works in the main script too. A const holds a frozen copy
of whatever it's given, so the function needs no
freeze:
fn build_by_name()
let by_name = {}
for item in [{ name = "sword", damage = 5 }, { name = "bow", damage = 3 }] do
by_name[item.name] = item
end
return by_name
end
const BY_NAME = build_by_name()
print(BY_NAME.bow.damage)
Run it:
$ scua build-in-a-function.scua
3
Inside a module, the error for a filled const names this
fix instead of the let. See State in a module.
#A constant holds data
A constant holds data: numbers, strings, bytes, booleans,
nil, enums and flags, arrays, tables and records of these,
frames and typed arrays, and functions that don't capture variables.
Anything else is refused, and the message says what it is and where it
sits. A reference to a partition is the one you're most likely to meet.
Pass it to whoever needs it instead:
partition Logger
on Line(text) print(text) end
end
const LOG = Logger()
Run it:
$ scua const-partition.scua
scua: const-partition.scua:5: a constant holds data only, but `LOG` is a partition reference — pass it in instead, as a constructor argument or in a message
freeze takes the same values a const does.
A function in a constant is still the same function: after
const F = f, F == f is true, and so is a test
against a function kept inside a constant table. See Comparing functions.
#Block scope
A binding is visible from where it's declared to the end of the block
that contains it. A do ... end block is the simplest way to
introduce a scope:
let x = 1
do
let x = 2
print(`inside: {x}`)
end
print(`outside: {x}`)
Run it:
$ scua scope.scua
inside: 2
outside: 1
The inner let x is a separate binding that lives only
inside the block. When the block ends, the outer x is
what's in scope again. The bodies of if,
while, for, and functions are blocks too, so a
binding made inside one of them is not visible outside it.
#Top-level bindings are visible inside functions
A let or const written at the top level of
your script is in scope for every function defined in that same file. A
top-level let is the main script's mutable state: a
function can read it and update it, and the change persists across
calls. A top-level const is read-only everywhere. (A
module's top level holds no let: it uses
const, or state for a value each partition
keeps its own copy of. See Modules.)
const STEP = 5
let score = 0
fn add_points()
score = score + STEP
end
print(`start: {score}`)
add_points()
add_points()
print(`after two: {score}`)
Run it:
$ scua shared-score.scua
start: 0
after two: 10
add_points reads the constant STEP and
updates the shared score. Each call adds to the same
binding, so after two calls score is 10. This
is handy for a counter or a small piece of configuration that several
functions share.
A partition can't reach it. A top-level
let belongs to the main script. Code that runs in a
partition (its handlers, its state lines, and every
function they call) may not read or change one, and the compiler says
so, naming the variable and the path to it:
let score = 0
fn add_points()
score = score + 5
end
partition Player
on Score()
add_points()
end
end
tell Player().Score()
Run it:
$ scua actor-copy.scua
scua: actor-copy.scua:9: the `Score` handler of `Player` calls `add_points`, which changes `score`, a variable of the main script (`let score`, line 1). A partition can't reach the main script's variables: keep the value in the partition's `state` block, or `tell` whoever owns it
Keep data that belongs to a partition in its state, and
ask it when you need it:
partition Player
state score = 0
on Score() score = score + 5 end
ask Total() return score end
end
partition Game
on Play(p)
tell p.Score()
tell p.Score()
print(`score: {ask p.Total() timeout 1s}`)
end
end
tell Game().Play(Player())
Run it:
$ scua actor-state.scua
score: 10
A top-level const is different: it is frozen, so no code
can change it, and every partition reads the same value. A partition
reads a value that never changes from a const, or gets it
as a constructor argument. See Partitions and actors.
#Using a name before its declaration
A top-level fn can be called from anywhere in its file,
above its declaration included: every function at the top level exists
before the first line runs.
print(greet("Ada"))
fn greet(name) return `hello, {name}` end
$ scua hoist.scua
hello, Ada
A value is different. A top-level let or
const exists from the line that declares it, so reading one
above that line is a compile error that names the line to move:
print(LIMIT)
const LIMIT = 3
$ scua early.scua
scua: early.scua:1: `LIMIT` is used before its declaration (line 2). Move the declaration above this line
When the read happens inside a function, the compiler can't tell when
the function will run, so the check happens then: reading the value
before its declaration has run is a fault naming the line, never a
silent nil.
fn limit() return LIMIT end
print(limit())
const LIMIT = 3
$ scua late.scua
scua: late.scua:1: `LIMIT` is used before its declaration ran (line 3)
at limit (late.scua:1)
at main (late.scua:2)
A partition sees exactly the
top-level declarations that had run when it was created. One created
above const LIMIT gets this fault every time it reads
LIMIT, on one core or many, however long it runs: declare
what partitions need before you create them.
#Shadowing
You can declare a new binding with the same name as one already in scope. The new binding shadows the old one from that point on:
let value = "first"
let value = "second"
print(value)
Run it:
$ scua shadow.scua
second
This is a new binding, not an assignment. It's most useful when you want to refine a value through a few steps and don't need the earlier versions, for example reassigning a name to a trimmed or parsed form of itself.
One exception: at the top level of a file, a name declared with
const is declared once. const LIMIT = 10
followed by let LIMIT = 20 is an error, because functions
and actors would read one and the rest of the file the other. Inside a
block or a function you can still shadow a const with a new
binding.
For how functions capture the variables around them, see Functions and closures.
#Your names and the built-ins
Every file can use the built-ins (print,
len, round, tostring and the
rest) without importing anything. They come from the standard library,
which sits around your file like an outer scope.
You can still declare your own function or variable with a built-in's
name. Your declaration hides the built-in in the whole file, above the
line you wrote it on as well as below, and SCUA says so once, with a
warning. To reach the built-in anyway, write std. in front
of its name:
fn round(x)
return x // 1
end
print(round(2.7))
print(std.round(2.7))
Run it:
$ scua rounding.scua
2.0
3
scua: rounding.scua:1: warning: `round` hides the built-in `round` in this file; the built-in is still `std.round`
std.<name> always means the built-in, whatever
your file declares. std is a reserved word, so nothing in
your program can be called std.
This warning never stops a program. --deny-warnings
doesn't turn it into an error, and neither does a --!strict
file. That's on purpose: a later SCUA release may add a built-in whose
name your program already uses, and your program must keep running
exactly as it did. For the same reason it's printed when the run ends,
after everything the program wrote, so the first line of the error
output is always about your program: its error, if it stopped with one.
If you'd rather not see the warning, rename your declaration.
A top-level fn exists before the first line runs (see Using a name before its
declaration), so a call above it is already your function. A
let or const exists only from its line, and
because the declaration covers the whole file, using its name in the
file's top-level code before that line has run is an error. It would
otherwise quietly use the built-in above the line and your value below
it:
print(round(2.7))
let round = 3
$ scua early.scua
scua: early.scua:1: `round` is used here before its declaration on line 2 has run. That declaration hides the built-in `round` in this whole file, so `round` here means it too; to use the built-in, write `std.round`
scua: early.scua:2: warning: `round` hides the built-in `round` in this file; the built-in is still `std.round`
When a program is refused, the error that stopped it comes first and any warnings follow it.
A parameter or a local variable may have a built-in's name too. It
hides the built-in only where it's in scope, the way any local hides an
outer name, and there's no warning. A method call like
xs.len() always means the built-in unless your file
declares its own len, so a local called len
never changes it:
fn largest(xs)
let max = xs[0]
for x in xs do
if x > max then max = x end
end
return max
end
print(largest([3, 9, 4]), max(3, 9))
$ scua local.scua
9 9
Built-ins can't be assigned to.
tostring = fn(x) return "?" end is an error in your program
and in any module, because it would change tostring for
code that never asked for it. Declare your own instead, which affects
only your file:
tostring = fn(x) return "?" end
$ scua assign.scua
scua: assign.scua:1: `tostring` is a built-in, and built-ins can't be assigned to. To have your own `tostring` in this file, declare it — `fn tostring(…) … end` or `let tostring = …` — which hides the built-in here and nowhere else; the built-in stays reachable as `std.tostring`
Modules follow the same rules: a module that declares its own
round hides the built-in in that module only. One
difference: a name the module exports gives no warning where it's
declared. A geometry module that declares fn distance and
returns { distance = distance } has picked that name for
the files that call geom.distance, so the warning would
only be noise; inside the module, distance still means the
module's own. A name the module keeps to itself warns as usual. See Modules.
If the module also calls that name without a prefix, the warning
moves to the call, because that's where the mix-up happens. A database
module that exports its own fn error(message) and then
writes error(fault) to re-raise a fault calls its own
function, not the built-in that raises. The warning says where the call
goes:
scua: db.scua:12: warning: `error(…)` here calls this file's own `error` (line 3), not the built-in `error`; write `std.error(…)` if you meant the built-in
If you meant your own function, nothing needs to change. A module that calls the name in several places gets one warning that lists the lines.