Creating Variables
Variables are pretty much aliases that the programmers make for using memory to allocate some data. Obviously, we do not want, for our sanity's sake, to use memory addresses directly for accessing our data. That is really complicated, hard to remember, and an absolute nightmare to maintain. So instead, we assign these memory locations, containing our data, names which we can use in our programs as needed.
As such, variables are a crucial component of a programming language.
There are two ways to create variables in Scribe.
Definition
To define a variable in Scribe, the let <identifier> = <expression>;
pattern is used.
The identifier will be the variable name, followed by the value expression after the assignment (=
) symbol, terminated by a semicolon (;
).
Since Scribe is a statically typed language, the type of each variable is necessary. However, in this (definition) format, the datatype is inferred. In other words, the compiler can deduce the type of the variable from the expression.
This is the syntax used to import the std/io
module in the Hello World example.
For example:
let an_int = 20; // type: i32
let a_string = "this is a string"; // type: *const i8 (just like C)
let a_flt = 2.5; // type: f32 (float)
Declaration
To declare a variable, the let <identifer>: <type>;
pattern is used.
As before, the identifier will be the variable name, followed by the datatype of the variable after a colon (:
), terminated by a semicolon (;
).
Here, we explicitly mention the datatype of the variable since there is no expression to deduce it from.
For example:
let an_int: u64; // type: u64 (unsigned 64 bit integer)
let a_flt: f64; // type: f64 (64 bit float)
Notes
Both the styles can be merged as well, as long as the type
and expression
's type are same. The only exception is automatic type casting between primitive datatypes.
For example, if you want to create a variable with value 5
(which has type: i32
), but of type: u64
, you can do:
let u64int: u64 = 5;
Variable (Re)assignment
A variable won't quite be a "variable" if its value cannot be changed. Therefore, to change (or set) the value of a variable, you can use the variable name, followed by the assignment (=
) symbol, followed by the value.
Do note that the datatype of the variable can never be changed after a variable is created. Hence, for (re)assignment, the datatype of the new value must be the same as the one used/inferred when creating the variable.
For example:
let a = 5;
a = 10; // valid
a = "a string"; // invalid
Similarly,
let a: u64;
a = 10; // valid
a = "a string"; // invalid
a = 5.0; // valid
Type Coercion
You may be wondering how is a = 5.0;
valid. The datatype of a is u64
, while that of 5.0
is f32
.
The reason is that the compiler can automatically "coerce" between primitive (i1, i8, i16, i32, i64, u8, u16, u32, u64, f32, f64
) types.
In other words, the data from these types can be used with a value/variable of another of these types.
This does come at a loss of data itself (about which more can be found on the Internet), therefore one must be careful when writing code that can be automatically type coerced.
Variable Scope
The scope of a variable defines its lifetime
- when is it created to when it is destroyed. Scribe, like many languages, uses braces
({
and }
) to define blocks of code.
Any variable has its lifetime bound to this block - it cannot be used outside this block.
For example:
let a = 5;
This is globally scoped - not contained within any block. Therefore, this variable can be used anywhere after its creation.
Another example:
let main = fn(): i32 {
let a = 5;
};
Here, the variable a
can be used anywhere within the braces of the main
function (and of course, after the creation).
Scribe also supports variable shadowing
. That is, if there is a variable in a parent block, a new variable can be created of the same name, within a new block, inside the parent block.
For example:
let main = fn(): i32 {
let a = 5;
{
let a = "a string";
}
};
The above code is valid. Within the main
function's block, the integer variable a
is usable. And within the block inside the main
function, the string variable a
is usable.
Variable Names
One last important thing about variables is their name. Scribe defines specific rules based on which you can name variables, quite similar to most other languages. These rules are that variable names:
- Must begin with an alphabet (irrelevant of the case) or underscore
- Can contain numbers anywhere except the first character
- Cannot contain any symbol other than alphabets, numbers, and underscores.
Variable Type Modifiers
Type modifiers alter the working of a variable - what they store, how they work, etc.
There are 4 type modifiers in Scribe:
- Static
- Reference
- Constant
- Comptime
Static
The static modifier allows the variable to be stored in global storage - the variable is created only once. If such a variable is present inside a function, the variable will be initialized only once and it will maintain its value across function calls.
For example,
let io = @import("std/io");
let f = fn(): i32 {
let static x = 5;
return x++;
};
let main = fn(): i32 {
io.println(f()); // prints 5
io.println(f()); // prints 6
io.println(f()); // prints 7
return 0;
};
Reference
Reference variables are the same as in C++ - they are references to other variables. In other words, reference variables can be considered as aliases to other variables.
These variables are created by using &
with the type.
For example,
let f = fn(data: &i32) {
data = 10;
};
let main = fn(): i32 {
let a = 5;
f(a);
// now a is 10
return 0;
};
Constant
Variables, in general, can be modified. However, there are times when the programmer does not want a variable to be modifiable - it must stay constant. That's what the constant modifier allows.
Constant variables, once created, can never be modified again. Attempting to modify them is a compilation error.
For example,
let main = fn(): i32 {
let const i = 10;
i = 20; // error: i is a constant
return 0;
};
Comptime
Scribe contains a pretty nifty and useful feature - comptime variables. A comptime variable is special in the sense that the value of such a variable is evaluated during the code compilation. Hence, some conditions apply for a variable to be allowed to be comptime:
- There must be a value expression for the variable
- The value expression must be able to be evaluated during compile time itself - it cannot contain data that cannot be computed during compilation
For example,
let comptime a = 50; // valid - 50 is a value which is known by the compiler
let f = fn(x: i32): i32 {
return x * 5 + 25;
};
let comptime b = f(10); // valid - function call is provided a value known by the compiler, and everything inside the function body is computable by the compiler
let io = @import("std/io");
let f2 = fn(x: i32): i32 {
io.println(x);
return x * 5 + 25;
};
let comptime c = f2(10); // invalid - io.println() does not work at compile time
let arr = @array(i32, 10);
let comptime x = arr[1]; // valid - @array() creates an array and initializes it to zero at compile time
Conclusion
Well, that is basically how variables, their reassignment, and their scopes, work. Not much to learn or understand and pretty easy - which is the goal!
Next, we'll understand the concept of datatypes and see some of the fundamental (primitive) datatypes in Scribe.