This is an attempt to make a modern replacement for bc, while preserving its best features, such
as big-decimal numbers and explicit support for interactivity in the language.
calx’s motto is: “deterministic and predictable output, not forecasting weather on Mars.”
You will need:
- a GNU C-compatible compiler, such as GNU GCC or Clang;
- CMake;
- the GNU readline library.
To build calx, simply clone this repository and run the following in its root:
git submodule update --init && cmake -DCMAKE_BUILD_TYPE=Release . && make
This will build the ./calx binary.
Calx programs operate on values. Each value has a type, which can be one of the following:
- nil;
- flag;
- number;
- string;
- list;
- dict;
- function;
- weak reference.
There are expressions and statements.
Expression is when you compute the value of something; 2+2 and f(x) are expressions.
Statement is when you do something; a:=2;, x+=1;, return 2+2; and if(x!=1){x*=2;} are
statements.
Now, an expression statement is a statement consisting solely of an expression.
In other words, an expression statement is when an expression is followed by a semicolon.
Note that a fake semicolon is inserted in this context after a line break, a }, and the
end-of-file.
In calx, the value of an expression statement is not simply evaluated and then discarded, as, for example, in C-family languages, but also gets printed. Let’s test it:
≈≈> 2+2 # <- a fake semicolon is inserted here by the lexer
4
Calx has lexical, function-wide scope with hoisting (much like pre-ES6 JavaScript). However, unlike JavaScript, it has no support for closures, although functions can be nested.
A global variable is declared or assigned to with the following syntax: NAME = VALUE;.
Let’s test it:
≈≈> var = 5
≈≈> var
5
Attempt to load the value of an undefined global variable throws a runtime error. Let’s test it:
≈≈> var2
Runtime error: undefined global 'var2'
Stack trace (most recent first):
>>> at (input):1:
var2
A local variable is declared with the following syntax: NAME := VALUE;.
A local variable always has function scope. Note here that any chunk (a program with body) is a
function with zero parameters, and each line in the interactive mode is compiled as a separate
chunk. Let’s test it:
≈≈> var2 := 9; var2
9
≈≈> var2
Runtime error: undefined global 'var2'
Stack trace (most recent first):
>>> at (input):1:
var2
Note that the NAME = VALUE; statement just assigns another value to an existing local variable if
such exists in the current scope.
Functions are defined with the following syntax: fun NAME(PARAMS) { BODY }. Let’s test it:
≈≈> fun myfunc(x, y) { return 2*x*y }
≈≈> myfunc(5, 6)
60
Without an explicit return statement, a function returns nil. The nil value does not get
printed in the “expression statement” context, so you can declare “procedures” — functions that do
things as opposed to compute things — using this syntax as well.
Remember functions are just ordinary values; you can, for example, pass them to other functions
just as well as numbers. The fun NAME statement is just an assignment: it is equivalent to
NAME = <newly_created_function>;.
As a test, let's use the built-in Dasm function that prints opcode listing for a function:
≈≈> Dasm(myfunc)
0 | OP_FUNCTION 0, 1
1 | OP_LOAD_CONST 0, 0
2 | OP_LOAD_LOCAL 0, 0
3 | OP_AOP 12, 0
4 | OP_LOAD_LOCAL 0, 1
5 | OP_AOP 12, 0
6 | OP_RETURN 0, 0
7 | OP_LOAD_CONST 0, 1
8 | OP_RETURN 0, 0
Yes:
≈≈> f := nil; fun f(x) { return 2*x }; f(3)
6
≈≈> f(3)
Runtime error: undefined global 'f'
Stack trace (most recent first):
>>> at (input):1:
f(3)
There are binary and unary operators. All operators have priority; binary operators also have associativity (either left-to-right or right-to-left). Expression can be grouped with parentheses.
In the tables below, operations with higher priority are done first.
Associativity defaults to “left-to-right”; see “types” below for the meaning of the “type” column.
| Spelling | Meaning | Type | Priority | Associativity |
|---|---|---|---|---|
+ |
Sum | Rational | 19 | |
- |
Difference | Rational | 19 | |
* |
Product | Rational | 20 | |
/ |
Quotient | Rational | 20 | |
** |
Power | Rational | 21 | Right-to-left |
// |
Quotient (integral) | Integral | 20 | |
% |
Remainder (integral) | Integral | 20 | |
| |
Bitwise “OR” | Bitwise | 13 | |
& |
Bitwise “AND” | Bitwise | 14 | |
^ |
Bitwise “XOR” | Bitwise | 15 | |
<< |
Bitwise left shift | Bitwise | 18 | |
>> |
Bitwise right shift | Bitwise | 18 | |
< |
Less | Comparative | 17 | |
<= |
Less or equals | Comparative | 17 | |
> |
Greater | Comparative | 17 | |
>= |
Greater or equals | Comparative | 17 | |
~ |
Concatenation | Omnivorous | 10 | |
|| |
Logical “OR” | Omnivorous | 11 | |
&& |
Logical “AND” | Omnivorous | 12 | |
== |
Equals | Omnivorous | 16 | |
!= |
Not equals | Omnivorous | 16 |
-
Rational: expect both of their operands to be numbers; throw otherwise. Also throw if one of the operands is not in the domain of operator:
/throws if the right operand is zero;**throws if the right operand is non-integer, or if the right operand is negative (it is assumed that0 ** 0is1). -
Integral: expect both of their operands to be numbers; throw otherwise. Then, assuming the left and right operands were
leftandright, correspondingly, the result isTO_INTEGER(OPERATION(TO_INTEGER(left), TO_INTEGER(right))). These operators throw if one of the operands is not in the domain of the operator://and%throw ifTO_INTEGER(right)is zero. See below for the definition of theTO_INTEGERfunction. -
Bitwise: expect both of their operands to be numbers; throw otherwise. Then, assuming the left and right operands were
leftandright, correspondingly, the result isOPERATION(TO_UINT32(left), TO_UINT32(right)). The left and right shift operations return zero ifTO_UINT32(right) >= 32. See below for the definition of theTO_UINT32function. -
Comparative: expect their operands to be “meaningfully comparable”; currently, this means that they must either be both numbers, or be both strings. Throw otherwise.
-
Omnivorous: work with operands of any types.
The / operator is the only one whose result depends on the scale (see the section on it below);
other always return the exact result.
The concatenation operator, assuming its left and right operands were left and right,
correspondingly, returns CONCATENATE_STRINGS(TO_STRING(left), TO_STRING(right)). See below for the
definition of the TO_STRING function.
The logical “OR” operator, assuming its left and right operands were left and right, returns
left if TO_FLAG(left) is true; otherwise, it returns right.
The logical “AND” operator, assuming its left and right operands were left and right, returns
left if TO_FLAG(left) is false; otherwise, it returns right.
The == and != operators compare their operands in the following way:
- if the values are of different types, they compare as “not equal”;
- number, string, flag and nil values compare “by value”;
- values of other types compare “by identity” (read “by pointer”).
| Spelling | Meaning | Priority |
|---|---|---|
- |
Negation | 50 |
! |
Logical “NOT” | 50 |
@ |
Length | 60 |
The negation operator expects its operand to be number; throws otherwise.
The logical “NOT” operator works with operand of any type. Assuming the operand was x, the result
is BOOL_NOT(TO_FLAG(x)). See below for the definition of the TO_FLAG function.
The length operator returns the length of a list, a dict, or a string; throws if the operand is neither.
For each binary operator with spelling <op>, there is a corresponding compound assignment
statement with the following syntax: LHS <op>= VALUE;. For example, to add something to a
variable, use NAME += VALUE;.
The scale can be thought of as a global variable that sets the precision of the result of /
(division) operator. It also sets the precision of the results of various built-in analytic
functions.
Precision means decimal places, and thus can not be negative.
Its value can be read via the following call: Scale(). It returns the current precision as a number.
Its value can be set via the following call: Scale(n).
Lists are lists of values. [], [12], [12, 34], [12, 34, 56] expressions all create new lists
with the specified elements.
To get an element of the list by index, use list[index] syntax. index must be number;
otherwise, this construct throws.
If index >= 0 and {TO_INTEGER(index) is a valid list index}, then the result is the value behind that index.
Otherwise, the result is nil.
See below for the definition of the TO_INTEGER function.
Let’s test it:
≈≈> xs = [1, 2, 3]
≈≈> xs[0]
1
≈≈> xs[0.9]
1
≈≈> xs[1]
2
≈≈> xs[2]
3
≈≈> xs[3]
≈≈> xs[-1]
≈≈> xs[-0.1]
≈≈> xs["test"]
Runtime error: attempt to index list with string (expected number)
Stack trace (most recent first):
>>> at (input):1:
xs["test"]
To set list element by index, use list[index] = value syntax. index must be number; otherwise,
this construct throws.
If index >= 0 and {TO_INTEGER(index) is a valid list index}, then the value behind that index is altered.
If TO_INTEGER(index) equals to the size of the list, a new element is pushed to the back of the list.
Otherwise, this construct throws.
See below for the definition of the TO_INTEGER function.
To get the size of the list, use the @ operator:
≈≈> @[12, 34]
2
Use the built-in Pop function to pop the last element off the list:
≈≈> xs = [1, 2, 3]
≈≈> Pop(xs)
3
≈≈> Pop(xs)
2
≈≈> Pop(xs)
1
≈≈> Pop(xs)
Runtime error: the list is empty
Stack trace (most recent first):
>>> at (input):1:
Pop(xs)
Dicts are mappings from strings to values. {}, {"x": 1}, {"x": 1, "y": 2} expressions all
create new dicts with the specified entries.
To get the size (the number of entries) of the dict, use the @ operator:
≈≈> @{"a": 1, "b": 2}
2
To get the value behind a key, use dict[key] syntax. key must be string;
otherwise, this construct throws. If there is no such key, the result is nil.
There is a shortcut notation for constant keys that are valid identifiers: dict.ident is
equivalent to dict["ident"].
Let’s test it:
≈≈> d = {"key1": 1, "key2": true, "key3": "str"}
≈≈> d["key1"]
1
≈≈> d.key1
1
≈≈> d.key2
true
≈≈> d.key3
str
≈≈> d.key4
≈≈> d[0]
Runtime error: attempt to index dict with number (expected string)
Stack trace (most recent first):
>>> at (input):1:
d[0]
To alter the value behind a key or insert a new entry, use dict[key] = value syntax. key must be
string; otherwise, this construct throws.
Let’s test it:
≈≈> d = {"key1": 1, "key2": true, "key3": "str"}
≈≈> d.key3 = 3
≈≈> d["key4"] = 4
≈≈> d.key3; d.key4
3
4
To remove an entry from a dict behind a key, use RemoveKey(dict, key).
If there is no entry with the key given, this function does nothing.
Let’s test it:
≈≈> d = {"key1": 1, "key2": 2}
≈≈> RemoveKey(d, "key1")
≈≈> d
{"key2": 2}
≈≈> RemoveKey(d, "z")
≈≈> d
{"key2": 2}
≈≈> d = {"key1": 1, "key2": true, "key3": "str"}
≈≈> for (k := NextKey(d, nil); k; k = NextKey(d, k)) { k ~ " => " ~ d[k] }
key3 => str
key2 => true
key1 => 1
Note that the order of keys is unspecified.
Numbers are just numbers, big-decimal and having both integer and fractional parts of potentially unlimited length:
≈≈> 123456789123456789123456789123456789.0123456789012345678901234567890123456789
123456789123456789123456789123456789.0123456789012345678901234567890123456789
You can also optionally separate their digits with a single quote symbol:
≈≈> 1'000'000
1000000
Strings are just immutable arrays of bytes. String literals must always use double quotes; single quotes are not supported:
≈≈> "test"
test
The following escapes in string literals are supported:
| Spelling | C equivalent |
|---|---|
\\ |
\\ |
\a |
\a |
\b |
\b |
\e |
\033 |
\f |
\f |
\n |
\n |
\r |
\r |
\t |
\t |
\v |
\v |
\" |
\" |
\0 |
\0 |
\x## |
\x## |
where ## means two hexadecimal digits (both lower- and uppercase letters are accepted).
To get the size (the number of bytes) of the string, use the @ operator:
≈≈> @"test"
4
≈≈> @"ш"
2
To get string byte by index, use str[index] syntax. index must be number;
otherwise, this construct throws.
If index >= 0 and {TO_INTEGER(index) is a valid string index}, then the result is a single-byte
string.
Otherwise, the result is nil.
See below for the definition of the TO_INTEGER function.
Let’s test it:
≈≈> "abcde"[3]
d
≈≈> "abcde"[100]
≈≈> "abcde"["x"]
Runtime error: attempt to index string with string (expected number)
Stack trace (most recent first):
>>> at (input):1:
"abcde"["x"]
nil is the literal of nil type.
true and false are literals of flag type.
We say the value x is truthy iff TO_FLAG(x) returns true.
See below for the definition of the TO_FLAG function.
An if statement, in its base form, has the following syntax: if (CONDITION) { BODY }.
It executes BODY if TO_FLAG(CONDITION) is true.
Another form is has the following syntax: if (CONDITION) { BODY_1 } else { BODY_2 }.
It executes BODY_1 if TO_FLAG(CONDITION) is true, and BODY_2 otherwise.
After the if clause (and before the else clause, if any), any number of elif (meaning "else if") clauses may be inserted;
an elif clause has the following syntax: elif (COND) { BODY }. Such a clause means that, if the conditions of all the previous
if/elif clauses were false so that their bodies were not executed, then COND must be evaluated, and, if TO_FLAG(COND) is true,
BODY of this elif clause should be executed, and body of else clause, if any, should not.
Following is an example of an if statement with an elif clause:
≈≈> if (2+2 == 2) {
×⋅⋅⋅> "two"
×⋅⋅⋅> } elif (2+2 == 4) {
×⋅⋅⋅> "four"
×⋅⋅⋅> } else {
×⋅⋅⋅> "neither"
×⋅⋅⋅> }
four
Overall, the semantics of an if statement is the following:
-
Evaluate the condition of the
ifclause; if truthy, execute the body of theifclause and go to step 4. -
If has no more
elifclauses, then go to step 4. Otherwise, evaluate the condition of the nextelifclause; if truthy, execute the body of thatelifclause and go to step 4. Otherwise, repeat step 2. -
If an
elseclause is present, then execute its body. -
The execution of the statement is done.
The while statement has the following syntax: while (CONDITION) { BODY }.
Its semantics is the following:
-
Evaluate the condition. If truthy, then execute the body and repeat step 1.
-
The execution of the statement is done.
The for statement has the following syntax: for (PRE; COND; POST) { BODY }.
In the notation above:
- both
PREandPOSTmust be “maybe-expr-or-assignment-type statements”, where a “maybe-expr-or-assignment-type statement” is either of:- an empty statement;
- an expression statement;
- an assignment statement, including a compound assignment statement;
CONDmust be either empty or an expression.
Its semantics is the following:
-
Execute the
PREstatement. -
If
CONDis present, then evaluate it and, if not truthy, go to step 6. -
Execute
BODY. -
Execute the
POSTstatement. -
Go to step 2.
-
The execution of the statement is done.
The return EXPRESSION; statement evaluates EXPRESSION and returns its value from the innermost
function.
The return; statement is equivalent to return nil;.
The break; statement breaks out of the innermost while or for loop.
For while loop, it means the control flow jumps to the step 2; for for loop, it means the
control flow jumps to the step 6.
The continue; statement forces the next ieration of the innermost while or for loop.
For while loop, it means the control flow jumps to the step 1; for for loop, it means the
control flow jumps to the step 4.
A semicolon inserted anywhere outside of an expression before:
- a line break;
- an end-of-input;
- a
}lexeme.
This function can only be applied to a number. It truncates the fractional part of the number; in other words, it rounds the number towards zero.
This function can only be applied to a number.
TO_INT32(x) returns TO_INTEGER(x) modulo 2 ** 32; the result is always non-negative and less
than 2 ** 32.
The behavior of this function depends on the type of its argument:
- for string, it returns the value unmodified;
- for number, it returns its string representation;
- for flag, it returns either
"true"or"false"; - for nil, it returns
"<nil>"; - for list, it returns
"<list>"; - for dict, it returns
"<dict>"; - for function, it returns
"<function>"; - for weak reference, it returns
"<weakref>".
TO_FLAG(x) returns false if x is either false or nil; otherwise, it returns true.
Dasm(f) prints out the disassembly (bytecode listing) of a bytecode function f.
Returns nil.
Kind(v) returns the name of the type of v:
- for string, it returns
"string"; - for number, it returns
"number"; - for flag, it returns
"flag"; - for nil, it returns
"nil"; - for list, it returns
"list"; - for dict, it returns
"dict"; - for function, it returns
"function". - for weak reference, it returns
"weakref".
Pop(L) pops an element from the back of the list L and returns the element.
Throws if L is empty.
Input() asks the user to enter a line and returns it as a string.
If the user refused, an empty string is returned.
Ord(c), where c is a one-byte string, returns the numeric value of that byte.
Chr(n) returns a character (a single-byte string) by its numeric value n.
Error(s), where s is a string, throws an error with message s.
RawRead(s), where s is a (single-byte) string, behaves in either of the following ways,
depending on the value of s:
-
If
sis"L", then the function reads a line from stdin, and returns the line with the trailing line break, if any; on I/O error, or if the end-of-file is reached, returns an empty string. -
If
sis"s", then the function reads a line from stdin, and returns the line without the trailing line break; on I/O error, or if the end-of-file is reached, returns an empty string. -
If
sis"B", then the function reads a single byte from stdin, and returns a single-byte string with that byte; on I/O error, or if the end-of-file is reached, returns an empty string. -
Otherwise, it throws.
RawWrite(s), where s is a string, prints s to stdout without trailing newline.
Returns nil.
Scale() returns the current scale as a number.
Scale(n), where n is a number, sets the current scale to n.
For what scale is, see the “The scale” section.
Where() prints out the stack trace.
Returns nil.
Random32() returns a random 32-bit unsigned integer.
LoadString(s), where s is a string, compiles s as a code, and returns the compiled function.
Throws if compilation fails.
For example, Eval in calx can be implemented as follows:
fun Eval(s) {
return LoadString(s)()
}
Require(s), where s is a string, tries to load and evaluate contents of file <value of s>.calx
from the directory $CALX_PATH (the value of CALX_PATH environment variable).
If $CALX_PATH was not defined or was empty, throws.
If s contains a prohibited character (including /, ., \0), throws.
NextKey(d,k), where d is a dict and k is either string or nil, returns the "next" key
after k (or, if k is nil, the "first" key) in dict d; or, if there is no such ("next"/"first")
key, returns nil.
The total order on keys is defined on any version of a dict; it may potentially be changed every time a dict is mutated.
RemoveKey(d,k), where d is a dict and k is a string, removes the entry with key k from d,
if any.
ToNumber(s), where s is a string, parses s as a decimal number and returns the result.
Throws if the string cannot be parsed as decimal number.
Encode(x,b) is equivalent to Encode(x,b,0).
Encode(x,b,n), where:
xis a number;bis integer number such that2 <= b <= 36;nis non-negative integer number,
returns the string representation of x in base b with n digits in the fractional part.
Note that not any decimal number has finite representation in any base; see, for example,
Encode(0.1, 3, 100).
Decode(s,b), where:
sis a string;bis integer number such that2 <= b <= 36,
parses s as a number in base b, truncating at Scale() decimal places (see the documentation
for the Scale() function).
Throws if s cannot be parsed as number in base b.
Note that not any number in any base has finite decimal representation; see, for example,
Decode("0.1", 3).
NumDigits(x,s), where x is a number and s is a string, behaves in either of the following
ways, depending on the value of s:
- If
sis"i", it returns the number of significant digits in the integer part ofx. - If
sis"f", it returns the number of significant digits in the fractional part ofx. - If
sis"+", it returnsNumDigits(x, "i") + NumDigits(x, "f"). - Otherwise, it throws.
Wref(x), where x is a weakrefable value (currently, either list or dict value), returns a new
weak reference to x.
Wvalue(w), where w is a weak reference, returns the value behind the reference, or, if the value
has been garbage collected, returns nil.
Clock() returns time, in seconds, since some fixed point in the past (before the start of the
program).
UpScale(x,n), where x is a number and n is non-negative integer number, returns x*(10**n).
DownScale(x,n), where x is a number and n is non-negative integer number, returns x/(10**n).
trunc(x), where x is a number, truncates the fractiotal part of x; in other words, it rounds
it towards zero.
floor(x), where x is a number, rounds x towards negative infinity.
ceil(x), where x is a number, rounds x away from zero (towards infinity whose sign corresponds
to the sign of x).
round(x), where x is a number, rounds x to the nearest, ties away from zero.
frac(x), where x is a number, returns the fractional part of x. Formally, for
non-negative x, it returns x - trunc(x); for negative x, it returns x + trunc(x).
The behavior of ToString(x) is equivalent to that of the special function TO_STRING(x); see the
section on special functions for more information.
Assert(cond) throws if !cond.
abs(x), where x is a number, returns the absolute value of x.
mod(x,y), where x and y are integer numbers, y > 0, returns the
“canonical” representation of x modulo y: the integer in [0; y) congruent to x modulo y.
div_ceil(a,b), where a,b are non-negative integers, b is non-zero, returns
a//b if (a%b)==0, otherwise it returns a//b + 1.
fact(n), where n is non-negative integer number, returns the factorial of n.
choice(n,k), where n and k are non-negative integer numbers, returns C(n,k).
fdiv(x,y), where x and y are numbers, returns trunc(x / y).
fmod(x,y), where x and y are numbers, returns x - y * trunc(x / y).
gcd(u,v), where u and v are integer numbers, returns the greatest common divisor of u and v.
lcm(u,v), where u and v are integer numbers, returns the least common multiple of u and v.
mod_pow(b,e,m), where b,e,m are non-negative integers, m is non-zero, returns (b**e)%m.
random_bits(n), where n is non-negative integer, returns a random number in [0; 2**n-1].
random_mod(n), where n is positive integer, returns a random number in [0; n-1].
random_range(lb,rb), where lb,rb are integers, lb<rb, returns a random number in [lb; rb-1].
probab_prime(x,nrounds), where x is integer, nrounds is non-negative integer, performs
some unspecified probabilistic primality tests. If x is prime, returns true; otherwise,
returns true with probability bounded by 4^(-nrounds), false otherwise.
jacobi(a,n), where a is integer, n is positive odd integer, returns the value of Jacobi
symbol (a/n).
kronecker(a,n), where a,n are integers, returns the value of Kronecker
symbol (a|n).
factorize(n), where n is positive integer, tries to find a non-trivial factor of n;
if it succeeds, it returns that factor; otherwise, it returns 0.
nth_root(x,n), where x is a number, n is integer, n>=2,
returns the n-th root of x, rounded down, with precision
specified by the current scale (see the documentation on the Scale() function).
If n is even and x is negative, it throws.
sqrt(x) is equivalent to nth_root(x,2).
cbrt(x) is equivalent to nth_root(x,3).