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 ** 0
is1
). -
Integral: expect both of their operands to be numbers; throw otherwise. Then, assuming the left and right operands were
left
andright
, 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_INTEGER
function. -
Bitwise: expect both of their operands to be numbers; throw otherwise. Then, assuming the left and right operands were
left
andright
, 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_UINT32
function. -
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
if
clause; if truthy, execute the body of theif
clause and go to step 4. -
If has no more
elif
clauses, then go to step 4. Otherwise, evaluate the condition of the nextelif
clause; if truthy, execute the body of thatelif
clause and go to step 4. Otherwise, repeat step 2. -
If an
else
clause 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
PRE
andPOST
must 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;
COND
must be either empty or an expression.
Its semantics is the following:
-
Execute the
PRE
statement. -
If
COND
is present, then evaluate it and, if not truthy, go to step 6. -
Execute
BODY
. -
Execute the
POST
statement. -
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
s
is"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
s
is"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
s
is"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:
x
is a number;b
is integer number such that2 <= b <= 36
;n
is 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:
s
is a string;b
is 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
s
is"i"
, it returns the number of significant digits in the integer part ofx
. - If
s
is"f"
, it returns the number of significant digits in the fractional part ofx
. - If
s
is"+"
, 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)
.