ccalc 0.25.0

Command-line calculator with accumulator, memory cells, multi-base arithmetic, and script file support
ccalc-0.25.0 is not a library.

ccalc

A fast terminal calculator with Octave/MATLAB syntax and script support β€” one binary, no runtime.

Current version: 0.25.0 β€” see CHANGELOG for history.

πŸ“– Documentation


Why ccalc?

Octave is hundreds of megabytes. Python requires a runtime. ccalc is a single self-contained binary that starts instantly and works anywhere: interactive sessions, shell scripts, CI pipelines, Docker containers.

It speaks Octave/MATLAB syntax β€” familiar to engineers and scientists β€” without requiring a full language installation.

Who Typical use
Embedded / systems engineer Arithmetic, hex/bin conversions, bit masks
DevOps / SRE Quick calculations in scripts and pipelines
Scientist / student Interactive session with variables and math functions
MATLAB / Octave user Familiar syntax, no heavy installation

Installation

git clone https://github.com/holgertkey/ccalc
cd ccalc
cargo build --release

The binary is placed at target/release/ccalc. Copy it anywhere on your PATH.


Usage

ccalc [OPTIONS]           start interactive REPL
ccalc "EXPR"              evaluate expression and print result
ccalc script.m            run a script file
echo "EXPR" | ccalc       pipe mode β€” silent, result only
ccalc < formulas.txt      read expressions from file
Option Description
-h, --help Show help
-v, --version Show version

Modes

Interactive REPL

Run without arguments:

$ ccalc
[ 0 ]:

Single expression

Pass an expression as a command-line argument:

$ ccalc "2 ^ 32"
4294967296

$ ccalc "sqrt(144)"
12

Script file

Pass a script file as an argument β€” any file that exists on disk:

$ ccalc script.m
$ ccalc script.calc
$ ccalc examples/mortgage.calc

Pipe / non-interactive mode

When stdin is not a terminal, ccalc runs silently β€” no prompt, one result per line. ans carries over across lines, so you can chain calculations:

$ echo "sin(pi / 6)" | ccalc
0.5

$ printf "10\n+ 5\n* 2" | ccalc
10
15
30

$ ccalc < formulas.txt

All commands work in script/pipe mode: exit/quit stop processing, who/clear/ws/wl/save/load manage variables, format controls number display, hex/dec/bin/oct/base control number base. cls is ignored.


How it works

The prompt shows ans β€” the result of the last expression. Every new expression updates it. Expressions that start with an operator automatically use ans as the left-hand operand (partial expressions):

[ 0 ]: 100
[ 100 ]: / 4
[ 25 ]: + 5
[ 30 ]: ^ 2
[ 900 ]:

Arithmetic

Operators

Operator Description Precedence
^ Power (right-associative) highest
* / Multiply, divide medium
+ - Add, subtract lowest
[ 0 ]: 2 + 3 * 4
[ 14 ]:

[ 0 ]: 2 ^ 3 ^ 2
[ 512 ]:               (right-associative: 2^(3^2) = 2^9)

Grouping

[ 0 ]: (2 + 3) * 4
[ 20 ]:

Unary minus

[ 0 ]: -5
[ -5 ]:

[ 0 ]: -(3 + 2)
[ -5 ]:

Ergonomics

Implicit multiplication

A number or closing parenthesis immediately before ( multiplies without an explicit *:

[ 0 ]: 2(3 + 1)
[ 8 ]:

[ 0 ]: (2 + 1)(4 - 1)
[ 9 ]:

Constants

Name Value
pi 3.14159265358979...
e 2.71828182845904...
nan Not-a-Number (IEEE 754 NaN)
inf Positive infinity
i, j Imaginary unit 0 + 1i (can be reassigned)
ans Result of the last expression

ans is the implicit accumulator β€” it is updated after every expression and can be used anywhere in an expression:

[ 9 ]: ans * 2 + 1
[ 19 ]:

[ 25 ]: sqrt(ans)
[ 5 ]:

Math functions

If called with empty parentheses, ans is used as the argument.

One-argument

Function Description
sqrt(x) Square root
abs(x) Absolute value
floor(x) Round down to integer
ceil(x) Round up to integer
round(x) Round to nearest integer
sign(x) Sign: βˆ’1, 0, or 1
log(x) Base-10 logarithm
ln(x) Natural logarithm (base e)
exp(x) e raised to the power x
sin(x) Sine (radians)
cos(x) Cosine (radians)
tan(x) Tangent (radians)
asin(x) Inverse sine (radians)
acos(x) Inverse cosine (radians)
atan(x) Inverse tangent (radians)

Two-argument

Function Description
atan2(y, x) Four-quadrant inverse tangent (radians)
mod(a, b) Remainder, sign follows divisor (Octave convention)
rem(a, b) Remainder, sign follows dividend
max(a, b) Larger of two values
min(a, b) Smaller of two values
hypot(a, b) √(a²+b²), numerically stable
log(x, base) Logarithm of x to an arbitrary base
[ 0 ]: sqrt(144)
[ 12 ]:

[ 0 ]: sin(pi / 6)
[ 0.5 ]:

[ 0 ]: hypot(3, 4)
[ 5 ]:

[ 0 ]: atan2(1, 1) * 180 / pi
[ 45 ]:

[ 0 ]: mod(-1, 3)
[ 2 ]:

[ 16 ]: sqrt()          same as sqrt(16)
[ 4 ]:

Functions can be nested and combined:

[ 0 ]: sqrt(abs(-25))
[ 5 ]:

[ 0 ]: max(hypot(3, 4), 6)
[ 6 ]:

Variables

Any identifier can be used as a variable. ans is the implicit variable updated after every standalone expression.

Assignment

name = expr shows the assigned value and does not update ans. Append ; to suppress output.

[ 0 ]: rate = 0.06 / 12
rate = 0.005
[ 0 ]: n = 360
n = 360
[ 0 ]: 200000 * 0.005
[ 1000 ]:

Using variables

[ 0 ]: rate = 0.07
rate = 0.07
[ 0 ]: 1000 * (1 + rate) ^ 10
[ 1967.1513573 ]:

View and clear

Command Action
who Show all defined variables and their values
clear Clear all variables
clear name Clear a single variable
ws / save Save workspace to ~/.config/ccalc/workspace.toml
wl / load Load workspace from file
save('path.mat') Save to explicit path
save('path.mat', 'x', 'y') Save specific variables only
load('path.mat') Load from explicit path
[ 0 ]: rate = 0.05
[ 0 ]: n = 12
[ 0 ]: rate + n
[ 12.05 ]: who
ans = 12.05
n = 12
rate = 0.05
[ 12.05 ]: clear rate
[ 12.05 ]: clear

Matrices

Create matrices using bracket syntax. Separate elements with spaces or commas; separate rows with ;:

[ 0 ]: A = [1 2; 3 4]
A =
   1   2
   3   4

[ [2Γ—2] ]: B = [5 6; 7 8]
B =
   5   6
   7   8

[ [2Γ—2] ]: A + B
ans =
    6    8
   10   12

Scalar operations are element-wise:

[ [2Γ—2] ]: 2 * A
ans =
   2   4
   6   8

Matrix multiplication and transpose

[ [2Γ—2] ]: A * B          matrix multiplication
[ [2Γ—2] ]: A'             transpose
[ [2Γ—2] ]: v' * v         dot product (row Γ— column)

Element-wise operators

[ [2Γ—2] ]: A .* B         element-wise multiply
[ [2Γ—2] ]: A ./ B         element-wise divide
[ [2Γ—2] ]: A .^ 2         element-wise power

Matrix built-ins

Function Description
zeros(m, n) All-zeros matrix
ones(m, n) All-ones matrix
eye(n) nΓ—n identity matrix
size(A) [rows cols] as a 1Γ—2 matrix
size(A, dim) Number of rows (dim=1) or cols (dim=2)
length(A) max(rows, cols)
numel(A) Total number of elements
trace(A) Sum of diagonal elements
det(A) Determinant
inv(A) Inverse matrix
A \ b Solve linear system A*x = b
qr(A) QR decomposition
lu(A) LU decomposition with partial pivoting
chol(A) Cholesky factor (SPD matrices)
svd(A) Singular value decomposition
eig(A) Eigenvalue decomposition
rank(A) Numerical rank
null(A) Null-space basis
orth(A) Column-space orthonormal basis
cond(A) Condition number
pinv(A) Moore-Penrose pseudoinverse
norm(A) Matrix 2-norm (largest singular value)

The REPL prompt shows the matrix dimensions when ans is a matrix. who displays dimensions: A = [2Γ—2 double]. ws saves only scalar variables; matrices are not persisted.

Range operator

Generate row vectors with the : operator:

1:5              % [1 2 3 4 5]
1:2:9            % [1 3 5 7 9]   (start:step:stop)
0:0.5:2          % [0 0.5 1 1.5 2]
5:-1:1           % [5 4 3 2 1]

Ranges work inside matrix literals and can be mixed with scalars:

[ 0 ]: v = 1:4
v =
   1   2   3   4

[ [1Γ—4] ]: [0, 1:3, 10]
ans =
    0   1   2   3   10

linspace(a, b, n) generates n evenly spaced values between a and b:

linspace(0, 1, 5)    % [0  0.25  0.5  0.75  1]
linspace(1, 5, 5)    % [1  2  3  4  5]

Indexing

All indices are 1-based (Octave convention). A name that exists as a variable always indexes β€” variables shadow built-in function names.

[ 0 ]: v = 1:5
v =
   1   2   3   4   5

[ [1Γ—5] ]: v(3)
[ 3 ]: v(2:4)
ans =
   2   3   4

[ [1Γ—3] ]: A = [1 2 3; 4 5 6; 7 8 9]
[ [3Γ—3] ]: A(2,3)
[ 6 ]: A(:,2)
ans =
   2
   5
   8

[ [3Γ—1] ]: A(1:2, 2:3)
ans =
   2   3
   5   6

Indexed assignment

All read forms work as write targets. A scalar RHS is broadcast to all selected positions:

v = zeros(1, 6);
v(3) = 42;            % set element 3
v(1:2) = [10, 20];    % slice assignment
v(4:6) = 99;          % broadcast scalar to three positions
v(:) = 0;             % reset all elements

A = zeros(4);
A(2, 3) = 7;               % 2-D element
A(:, 1) = [1; 2; 3; 4];   % full column
A(2:3, 2:3) = eye(2);      % submatrix

Growing vectors β€” assigning beyond the current length pads with zeros. end+1 is the idiomatic append:

v = [];
for k = 1:8
  v(end+1) = k^2;     % append k-squared
end
% v = [1 4 9 16 25 36 49 64]

Logical (boolean mask) indexing β€” a 0/1 mask selects positions for reading or writing:

temps = [18, 22, 35, 12, 29, 41, 8, 33];
hot   = temps(temps >= 30);   % read: [35 41 33]
temps(temps >= 30) = 30;      % write: cap all hot values at 30

Vector & Data Utilities

Special constants and predicates

Function / Constant Description
nan IEEE 754 Not-a-Number (propagates through arithmetic)
inf Positive infinity (-inf for negative)
nan(n) nΓ—n matrix filled with NaN
nan(m, n) mΓ—n matrix filled with NaN
isnan(x) 1 if NaN, else 0 (element-wise)
isinf(x) 1 if Β±Inf, else 0 (element-wise)
isfinite(x) 1 if finite, else 0 (element-wise)

Reductions

For vectors (1Γ—N or NΓ—1) these return a scalar. For MΓ—N matrices (M>1, N>1) they operate column-wise and return a 1Γ—N row vector.

Function Description
sum(v) Sum of elements
prod(v) Product of elements
mean(v) Arithmetic mean
min(v) Minimum element
max(v) Maximum element
any(v) 1 if any element is non-zero
all(v) 1 if all elements are non-zero
norm(v) Euclidean (L2) norm
norm(v, p) General Lp norm (p = inf supported)
cumsum(v) Cumulative sum (same shape as input)
cumprod(v) Cumulative product (same shape)

Data manipulation

Function Description
sort(v) Sort ascending (vectors only)
reshape(A, m, n) Reshape to mΓ—n, column-major (MATLAB element order)
fliplr(v) Reverse column order
flipud(v) Reverse row order
find(v) 1-based column-major indices of non-zero elements
find(v, k) First k non-zero indices
unique(v) Sorted unique elements as a 1Γ—N row vector

end in index expressions

Inside (...) index positions, end resolves to the length of that dimension. Arithmetic on end is fully supported:

[ 0 ]: v = [10 20 30 40 50];
[ [1Γ—5] ]: v(end)
[ 50 ]: v(end-1)
[ 40 ]: v(3:end)
ans =
   30   40   50

[ [1Γ—3] ]: A = [1:4; 5:8; 9:12];
[ [3Γ—4] ]: A(end, :)
ans =
    9   10   11   12

[ [1Γ—4] ]: A(1:end-1, 2:end)
ans =
   2   3   4
   6   7   8
[ 0 ]: data = [3 1 4 1 5 9 2 6];
[ [1Γ—8] ]: sort(data)
ans =
   1   1   2   3   4   5   6   9

[ [1Γ—8] ]: find(data > 4)
ans =
   5   6   8

[ [1Γ—3] ]: cumsum(data)
ans =
    3   4   8   9   14   23   25   31

Statistics & Random Numbers

Random number generation

Function Description
rand() Scalar uniform in [0, 1)
rand(n) nΓ—n uniform matrix
rand(m, n) mΓ—n uniform matrix
randn() / randn(n) / randn(m, n) Standard-normal scalar or matrix
randi(max) Random integer in [1, max]
randi(max, n) / randi(max, m, n) Matrix of random integers
randi([lo hi], ...) Integers from [lo, hi]
rng(seed) Seed RNG β€” same seed β†’ same sequence
rng('shuffle') Reseed from OS entropy
rng(42)
x = randn(1, 5)         % reproducible 5-element sequence
d = randi(6, 1, 10)     % ten dice rolls

Descriptive statistics

All functions operate column-wise on MΓ—N matrices and collapse to a scalar for vectors.

Function Description
std(v) Sample standard deviation (nβˆ’1 denominator)
std(v, 1) Population standard deviation (n denominator)
var(v) / var(v, 1) Sample / population variance
median(v) Median (linear interpolation for even length)
mode(v) Most frequent value; smallest wins on ties
cov(v) Variance of a vector
cov(A) NΓ—N covariance matrix of an mΓ—N data matrix
skewness(v) Population skewness: m3 / m2^(3/2) β€” 0 = symmetric
kurtosis(v) Population kurtosis: m4 / m2^2 β€” β‰ˆ 3 for normal
prctile(v, p) p-th percentile; p can be a vector
iqr(v) Interquartile range: prctile(75) βˆ’ prctile(25)
zscore(v) Standardise: (v βˆ’ mean) / std, same shape
hist(data) ASCII bar chart to stdout (10 bins default)
hist(data, n) ASCII bar chart with n bins
histc(data, edges) Count vector for user-supplied bin edges

Normal distribution

Function Description
normcdf(x) P(Z ≀ x), Z ~ N(0, 1)
normcdf(x, mu, s) P(X ≀ x), X ~ N(mu, sΒ²)
normpdf(x) Standard normal PDF
normpdf(x, mu, s) General normal PDF
erf(x) Gauss error function
erfc(x) 1 βˆ’ erf(x)
normcdf(1) - normcdf(-1)   % 0.6827  (68% rule)
normcdf(2) - normcdf(-2)   % 0.9545  (95% rule)

See examples/statistics.calc for a full worked example.


Bitwise Functions

All require non-negative integer arguments β€” combine naturally with 0xFF, 0b1010, 0o17 literals.

Function Description
bitand(a, b) Bitwise AND
bitor(a, b) Bitwise OR
bitxor(a, b) Bitwise XOR
bitshift(a, n) Left shift (n > 0) / logical right shift (n < 0)
bitnot(a) NOT within 32-bit window
bitnot(a, bits) NOT within bits-wide window (1–53)
[ 0 ]: bitand(0xFF, 0x0F)
[ 15 ]: bitxor(0xFF, 0x0F)
[ 240 ]: bitshift(1, 8)
[ 256 ]: bitnot(5, 8)
[ 250 ]:

Comparison & Logical Operators

Comparison operators return 1 (true) or 0 (false):

Operator Meaning
== Equal
~= or != Not equal
< Less than
> Greater than
<= Less or equal
>= Greater or equal

Logical operators:

Operator Meaning
~expr or !expr Logical NOT (unary)
&& Logical AND
|| Logical OR

! and != are C/shell-style aliases for ~ and ~=.

Precedence (low β†’ high): || β†’ && β†’ comparisons β†’ : β†’ +/- β†’ *// β†’ ^ β†’ unary (-, ~) β†’ primary

[ 0 ]: 3 > 2
[ 1 ]:

[ 0 ]: 3 == 3
[ 1 ]:

[ 0 ]: 5 ~= 5
[ 0 ]:

[ 0 ]: ~0
[ 1 ]:

[ 0 ]: 2 > 1 && 3 > 2
[ 1 ]:

Element-wise on matrices β€” comparisons produce a 0/1 mask of the same shape:

[ 0 ]: v = [1 2 3 4 5];
[ 0 ]: v > 3
ans =
   0   0   0   1   1

[ [1Γ—5] ]: v .* (v > 3)    % zero out elements <= 3
ans =
   0   0   0   4   5

Strings

ccalc supports two string types, matching MATLAB/Octave:

Char arrays β€” single quotes

[ 0 ]: greeting = 'Hello!'
greeting = Hello!
[ 'Hello!' ]: length(greeting)
[ 6 ]:

Char arrays are numeric-compatible β€” arithmetic converts each character to its ASCII code:

[ 0 ]: 'a' + 0        % ASCII of 'a'
[ 97 ]:
[ 0 ]: 'abc' + 1      % shift each code by 1
ans =
   98   99   100

String objects β€” double quotes

[ 0 ]: s = "Hello"
s = Hello
[ '"Hello"' ]: s + ", World!"
[ '"Hello, World!"' ]:

String objects use + for concatenation.

String built-ins

Function Description
num2str(x) / num2str(x, N) Number β†’ char array (N digits)
str2num(s) Char array β†’ number (error on failure)
str2double(s) Char array β†’ number (NaN on failure)
strcat(a, b, ...) Concatenate strings
strcmp(a, b) Case-sensitive equality β†’ 0/1
strcmpi(a, b) Case-insensitive equality β†’ 0/1
lower(s) / upper(s) Case conversion
strtrim(s) Strip leading/trailing whitespace
strrep(s, old, new) Find-and-replace
sprintf(fmt, v1, ...) Format string (C printf), returns char array
ischar(s) 1 if char array, else 0
isstring(s) 1 if string object, else 0

length(s), numel(s), and size(s) all work on strings.


Complex Numbers

i and j are pre-set to the imaginary unit 0 + 1i. Complex numbers work with the same operators and functions as real numbers:

[ 0 ]: 3 + 4*i
[ 3 + 4i ]: abs(ans)
[ 5 ]: angle(3 + 4*i) * 180/pi
[ 53.1301023542 ]:

Create, decompose, and manipulate:

z = complex(3, 4)    % 3 + 4i
real(z)              % 3
imag(z)              % 4
conj(z)              % 3 - 4i
z'                   % 3 - 4i  (conjugate transpose)
isreal(z)            % 0

Arithmetic works for all combinations of complex and real scalars:

(3 + 4*i) * (1 - 2*i)   % 11 - 2i
i^2                       % -1    (exact integer exponentiation)
i^4                       %  1

Complex built-ins

Function Description
real(z) Real part (real(5) β†’ 5)
imag(z) Imaginary part (imag(5) β†’ 0)
abs(z) Modulus sqrt(reΒ²+imΒ²)
angle(z) Argument atan2(im, re), radians
conj(z) Complex conjugate re - im*i
complex(re, im) Construct from two real scalars
isreal(z) 1 if im == 0, else 0

Note: Complex matrices ([1+2i, 3]) are not yet supported.


Formatted Output

fprintf β€” print to stdout

fprintf(fmt, v1, v2, ...) prints formatted output using C-style conversion specifiers:

Specifier Meaning
%d, %i Integer (truncated to whole number)
%f Fixed-point decimal
%e Scientific notation (1.23e+04)
%g Shorter of %f and %e
%s String (char array or string object)
%% Literal %

Width, precision, and alignment flags follow standard C printf conventions:

fprintf('%8.3f\n', pi)      %     3.142
fprintf('%-10s|\n', 'hi')   % hi        |
fprintf('%+.4e\n', 1000)    % +1.0000e+03
fprintf('%05d\n', 42)       % 00042

Escape sequences: \n (newline), \t (tab), \\ (backslash).

When there are more arguments than conversion specifiers, the format string repeats (Octave behaviour):

fprintf('%d ', 1, 2, 3)     % 1 2 3

sprintf β€” format to string

Same as fprintf but returns the result as a char array instead of printing:

label = sprintf('R = %.1f Ohm', R);
disp(label)

File I/O

File handles

fd = fopen('log.txt', 'w');         % open for writing; returns fd (β‰₯3) or -1 on failure
fprintf(fd, 'x = %.4f\n', x);      % write formatted text to file
fclose(fd);                         % close; returns 0 or -1

fd = fopen('data.txt', 'r');        % open for reading
line = fgetl(fd);                   % read one line, newline stripped; -1 at EOF
raw  = fgets(fd);                   % read one line, newline kept
fclose(fd);

fclose('all');                      % close all open file handles

Modes: 'r' read, 'w' write (create/truncate), 'a' append, 'r+' read+write.
File descriptor 1 = stdout, 2 = stderr.

Delimiter-separated data

dlmwrite('results.csv', A);         % write matrix, comma-separated
dlmwrite('results.tsv', A, '\t');   % explicit delimiter

data = dlmread('results.csv');      % read; auto-detect ',' / '\t' / whitespace
data = dlmread('results.tsv', '\t');

CSV tables β€” readmatrix / readtable / writetable

Higher-level CSV functions with header row support and type inference.

% readmatrix β€” numeric data, auto-skip non-numeric header
A = readmatrix('sensor.csv')                   % header skipped automatically
A = readmatrix('data.tsv', 'Delimiter', '\t')  % explicit delimiter

% readtable β€” first row always the header; returns Struct of columns
T = readtable('grades.csv')
scores = T.score      % Matrix NΓ—1  (numeric column)
names  = T.name       % Cell         (string column)

% writetable β€” write Struct to CSV with header row
T.name  = {'Alice', 'Bob', 'Carol'};
T.score = [91; 85; 78];
writetable(T, 'out.csv')
writetable(T, 'out.tsv', 'Delimiter', '\t')

Column type inference in readtable: if every cell in a column is parseable as a number (empty cells β†’ NaN), the column becomes a Matrix NΓ—1; otherwise a Cell of Str. writetable accepts the same column types and applies RFC 4180 quoting automatically for values containing the delimiter or quotes.

Filesystem queries

isfile('data.csv')          % 1 if the path is an existing file, else 0
isfolder('output/')         % 1 if the path is an existing directory, else 0
pwd()                       % current working directory as a char array

exist('x', 'var')           % 1 if variable x is in the workspace
exist('x', 'file')          % 2 if file exists on disk (MATLAB numeric code)
exist('x')                  % checks workspace first, then filesystem

Workspace with explicit path

save                            % save all variables to default path
save('session.mat')             % save all variables to named file
save('session.mat', 'x', 'y')  % save specific variables only
load('session.mat')             % load variables from named file

path = 'session.mat';
save(path)                      % variable reference also accepted

Scalars, char arrays, and string objects are persisted. Matrices, complex values, and functions are always skipped.


JSON

Requires the json feature flag:

cargo build --release --features json

Without this flag, calling either built-in returns an informative error. Both names always appear in tab completion.

% Decode β€” JSON string β†’ ccalc Value
s = jsondecode('{"name":"Alice","scores":[88,92,75]}')
s.name       % β†’ 'Alice'  (Str)
s.scores     % β†’ [88  92  75]  (Matrix 1Γ—3)

jsondecode('[1, 2, 3]')          % β†’ [1  2  3]  (all-numeric β†’ Matrix)
jsondecode('[1, "two", true]')   % β†’ {1, 'two', 1}  (mixed β†’ Cell)
jsondecode('null')               % β†’ NaN
jsondecode('true')               % β†’ 1

% Encode β€” ccalc Value β†’ compact JSON string
jsonencode(42)                   % β†’ '42.0'
jsonencode('hello')              % β†’ '"hello"'
jsonencode([1 2 3])              % β†’ '[1.0,2.0,3.0]'
jsonencode(s)                    % β†’ '{"name":"Alice","scores":[88.0,92.0,75.0]}'
jsonencode(nan)                  % β†’ 'null'

Type mapping:

JSON β†’ ccalc (jsondecode) ccalc β†’ JSON (jsonencode)
object {…} β†’ Struct Struct β†’ object
all-numeric array β†’ Matrix 1Γ—N Matrix 1Γ—N β†’ flat array
mixed array β†’ Cell Matrix MΓ—N β†’ array of row arrays
string β†’ Str Cell β†’ array
number β†’ Scalar Scalar(NaN) β†’ null
true/false β†’ Scalar (1/0) Str/StringObj β†’ string
null β†’ Scalar(NaN) Inf/Complex/Function β†’ error

Reading a JSON file (combine with fgetl for single-line JSON):

fid = fopen('data.json', 'r');
raw = fgetl(fid);
fclose(fid);
data = jsondecode(raw);

MAT Files

Requires the mat feature flag:

cargo build --release --features mat

Without this flag, calling load('*.mat') returns an informative error. load always appears in tab completion.

% Assignment form β€” returns a Struct of all variables in the file
data = load('results.mat');
data.score        % Scalar
data.readings     % Matrix
data.label        % Str (char array)
data.sensor.gain  % nested Struct field

% Bare form β€” injects all variables directly into the workspace
load('results.mat')
score             % now a workspace variable

Type mapping:

MAT type ccalc value
double (1Γ—1) Scalar
double (MΓ—N) Matrix
char array Str
struct Struct
struct array StructArray
cell array Cell
null / empty Scalar(NaN)

Backed by matrw = "=0.1.4". Complex and sparse matrices are not yet supported.


Control Flow

Multi-line control flow blocks are supported in both REPL and script mode.

REPL multi-line input

The REPL detects unclosed blocks and buffers lines with a continuation prompt until end is seen. Press Ctrl+C to cancel an incomplete block.

[ 0 ]:   for k = 1:3
  >>   fprintf('%d\n', k)
  >> end
1
2
3

if / elseif / else

score = 73;
if score >= 90
  grade = 'A';
elseif score >= 70
  grade = 'C';
else
  grade = 'F';
end
fprintf('grade: %s\n', grade)

for

Iterates over a range (or matrix columns):

total = 0;
for k = 1:10
  total += k ^ 2;
end
fprintf('sum of squares: %d\n', total)   % 385

while

x = 1.0;
while abs(x ^ 2 - 2) > 1e-12
  x = (x + 2 / x) / 2;
end
fprintf('sqrt(2) β‰ˆ %.15f\n', x)

break and continue

for n = 1:20
  if mod(n, 2) == 0
    continue       % skip even numbers
  end
  if n > 9
    break          % stop after 9
  end
  fprintf('%d ', n)
end

switch / case / otherwise

switch code
  case 200
    msg = 'OK';
  case 404
    msg = 'Not Found';
  otherwise
    msg = 'Unknown';
end
fprintf('%d: %s\n', code, msg)

No fall-through β€” only the first matching case runs. Works with scalars and strings. otherwise is optional.

do ... until

Octave post-test loop β€” the body always runs at least once, then the condition is checked:

x = 1;
do
  x *= 2;
until (x > 100)
fprintf('%d\n', x)   % 128

break and continue work inside do...until.

User-defined functions

Named functions use the function ... end block syntax:

function result = factorial_r(n)
  if n <= 1
    result = 1;
    return
  end
  result = n * factorial_r(n - 1);
end

factorial_r(7)   % 5040

Multiple return values are separated with [...]:

function [mn, mx, avg] = stats(v)
  mn  = min(v);
  mx  = max(v);
  avg = mean(v);
end

data = [4 7 2 9 1 5 8];
[lo, hi, mu] = stats(data);   % lo=1  hi=9  mu=5.14...

Use ~ to discard individual outputs:

[~, top, ~] = stats([10 30 20]);   % top = 30

nargin β€” number of arguments actually passed; useful for optional parameters:

function y = power_fn(base, exp)
  if nargin < 2
    exp = 2;
  end
  y = base ^ exp;
end

power_fn(5)     % 25   (default exponent)
power_fn(2, 8)  % 256

Functions are recursive. They see all other Function/Lambda values defined in the workspace at the time of the call.

Anonymous functions (lambdas)

sq   = @(x) x ^ 2;
hyp  = @(a, b) sqrt(a^2 + b^2);

sq(7)        % 49
hyp(3, 4)    % 5

Lambdas capture the enclosing environment at definition time (lexical closure):

rate = 0.05;
interest = @(p, n) p * (1 + rate) ^ n;
rate = 0.99;                        % does not affect the lambda
interest(1000, 10)                  % 1628.89  (uses captured 5%)

Pass lambdas to named functions (higher-order functions):

function s = midpoint(f, a, b, n)
  h = (b - a) / n;
  s = 0;
  for k = 1:n
    xm = a + (k - 0.5) * h;
    s += f(xm);
  end
  s *= h;
end

midpoint(@(x) x^2, 0, 1, 1000)         % 0.333333
midpoint(@(x) sin(x), 0, pi, 1000)     % 2.000001

Functions can return functions:

function f = make_adder(c)
  f = @(x) x + c;
end

add5 = make_adder(5);
add5(3)            % 8
add5(make_adder(10)(1))  % 16

Structs and Struct Arrays

Scalar structs group named fields of any type. Fields are accessed with . notation; intermediate structs are created automatically on write.

pt.x = 3;
pt.y = 4;
dist = sqrt(pt.x^2 + pt.y^2)    % 5

car.engine.hp = 190;             % nested struct created automatically
car.engine.hp                    % 190

p = struct('name', 'Alice', 'score', 98.5);
p.name                           % Alice
p.score                          % 98.5

Struct arrays store collections of records. Use indexed assignment to create or grow the array:

pts(1).x = 1;  pts(1).y = 0;
pts(2).x = 3;  pts(2).y = 4;
pts(3).x = 0;  pts(3).y = 5;

numel(pts)   % 3
pts(2).x     % 3

xs = pts.x   % [1 3 0]  β€” field collection across all elements
ys = pts.y   % [0 4 5]

Struct utilities:

fieldnames(s)         % cell array of field names (insertion order)
isfield(s, 'x')       % 1 if field exists, else 0
rmfield(s, 'x')       % copy of s without field 'x'
isstruct(v)           % 1 if v is a struct or struct array, else 0

Structs are displayed MATLAB-style:

s =

  struct with fields:

    x: 3
    y: 4

pts =

  1Γ—3 struct array with fields:
    x
    y

Nested or complex fields show compact inline: [1Γ—1 struct], [1Γ—3 double].


Cell Arrays

Cell arrays are heterogeneous 1-D containers where each element can be any value β€” scalar, matrix, string, or function handle.

c = {42, 'hello', [1 2 3]};   % cell literal
c{1}                           % 42   (brace indexing, 1-based)
c{2}                           % hello
c{3}                           % [1Γ—3 double]
c{4} = 'new';                  % auto-grows beyond current size

iscell(c)                      % 1
numel(c)                       % 4
cell(5)                        % pre-allocated 1Γ—5 cell of zeros

varargin / varargout β€” variadic functions:

function s = sum_all(varargin)
  s = 0;
  for k = 1:numel(varargin)
    s += varargin{k};
  end
end

sum_all(1, 2, 3)    % 6
sum_all(10, 20)     % 30

cellfun / arrayfun β€” apply a function to each element:

cellfun(@sqrt, {1, 4, 9})          % [1  2  3]
cellfun(@(x) x*2, {1, 4, 9})       % [2  8  18]
arrayfun(@(x) x^2, [1 2 3 4])      % [1  4  9  16]

@funcname handles β€” wrap any builtin or named function as a callable:

f = @sqrt;   f(16)            % 4
g = @abs;    g(-7.5)          % 7.5

case {v1, v2} in switch β€” matches if the switch expression equals any element:

switch x
  case {1, 2, 3}
    disp('small')
  otherwise
    disp('not small')
end

run() / source()

Execute a script file in the current workspace. Variables defined in the script persist in the caller's scope (MATLAB run semantics):

a = 252; b = 105;
run('euclid_helper')      % looks for euclid_helper.calc, then .m
fprintf('gcd = %d\n', g)  % g was set by the helper

source('utils')           % Octave alias for run()

Extension resolution for bare names: .calc is tried first (native), then .m (compatibility).

Search path (addpath / rmpath / path / genpath)

A session search path controls where run() looks for scripts. Entries are checked after the current working directory:

addpath('/my/scripts')            % prepend β€” highest priority
addpath('/my/utils', '-end')      % append  β€” lowest priority
rmpath('/my/scripts')             % remove entry
path()                            % display current path
addpath(genpath('/my/libs'))      % add /my/libs and all its subdirectories

genpath(dir) returns dir and all its subdirectories as a path-separator-delimited string (; on Windows, : on Unix).

Persistent paths can be configured in ~/.config/ccalc/config.toml:

path = [
  "~/.config/ccalc/lib",    # exact directory only
  "/home/user/scripts/",    # trailing slash β†’ dir + all subdirectories
]

Error Handling

Scripts can raise, catch, and recover from runtime errors without crashing the session.

% Raise an error with a formatted message
error('value must be positive, got %g', x)

% Print a warning and continue
warning('result may be inaccurate')

% try/catch β€” anonymous (swallow error)
try
  x = risky_compute(data)
catch
  x = 0
end

% try/catch β€” named: e.message holds the error string
try
  result = inv(A) * b
catch e
  fprintf('caught: %s\n', e.message)
  result = zeros(size(b))
end

% Inline fallback β€” default evaluated only on error
n = try(str2num(s), 0)          % 0 if s is not a valid number

% Protected call β€” returns [ok, value_or_message]
[ok, x] = pcall(@inv, A)
if ~ok
  fprintf('inv failed: %s\n', x)
end

% Last error message
lasterr()                        % message from most recent error
lasterr('')                      % clear

Compound assignment operators

Operator Meaning
x += e x = x + e
x -= e x = x - e
x *= e x = x * e
x /= e x = x / e
x++ x = x + 1
x-- x = x - 1
++x x = x + 1
--x x = x - 1

All forms desugar at parse time β€” no performance penalty.


REPL commands

Command Action
exit, quit Quit
cls Clear the screen
help, ? Show cheatsheet
help <topic> Detailed help (see below)
who List all defined variables
clear Clear all variables
clear <name> Clear a single variable
format Reset to short (5 significant digits)
format <mode> Switch display mode (see below)
format <N> N decimal places (e.g. format 4)
hex / dec / bin / oct Switch display base
base Show ans in all four bases
ws / save Save workspace to disk
wl / load Load workspace from disk
save('path') Save to explicit file
save('path', 'x', 'y') Save specific variables
load('path') Load from explicit file
Ctrl+C / Ctrl+D Quit

Help topics: syntax functions userfuncs cells structs errors bases vars script format matrices files control examples

Keyboard shortcuts

Key Action
↑ / ↓ Browse input history
Ctrl+R Reverse history search
← / β†’ / Home / End Cursor movement
Ctrl+A Go to beginning of line
Ctrl+E Go to end of line
Ctrl+W Delete word before cursor
Ctrl+U Delete from cursor to beginning of line
Ctrl+K Delete from cursor to end of line
Ctrl+L Clear screen
Ctrl+C / Ctrl+D Quit

Number formatting and bases

Display format

The format command controls how numbers are displayed (MATLAB-compatible):

Command Mode Example for pi
format short 3.1416 (5 sig digits, default)
format short short 3.1416
format long long 3.14159265358979
format shortE shortE 3.1416e+00
format longE longE 3.14159265358979e+00
format bank bank 3.14 (2 decimal places)
format rat rat 355/113 (rational approx)
format hex hex 400921FB54442D18 (IEEE 754 bits)
format + + + (sign only)
format compact β€” suppress blank lines
format loose β€” add blank line after outputs
format N custom format 4 β†’ 0.3333

format affects disp(), assignment output, and the REPL prompt β€” not fprintf/sprintf (which use their own specifiers).

See help format for full documentation.

Very large (|n| >= 1e15) and very small numbers switch to scientific notation automatically in short/long modes.

Number bases

Input literals β€” mix bases freely in any expression:

Prefix Base Example
0x hex 0xFF β†’ 255
0b binary 0b1010 β†’ 10
0o octal 0o17 β†’ 15

Display base β€” controls how the prompt and results are shown:

Command Effect
hex Switch display to hexadecimal
dec Switch display to decimal (default)
bin Switch display to binary
oct Switch display to octal
base Show ans in all four bases
[ 0 ]: 0xFF + 0b1010
[ 265 ]: hex
[ 0x109 ]: + 0b10
[ 0x10B ]: dec
[ 267 ]:

Inline base suffix β€” evaluate an expression and switch display base in one step:

[ 0 ]: 0xFF + 0b1010 hex
[ 0x109 ]:

base command:

[ 10 ]: base
2  - 0b1010
8  - 0o12
10 - 10
16 - 0xA

Expression conversion β€” when the display base is non-decimal and the expression contains literals in other bases, the converted expression is printed before the result:

[ 0x6 ]: 0b11 + 0b11
0x3 + 0x3
[ 0x6 ]:

[ 0b110 ]: 2 + 0b110 + 0xa
0b10 + 0b110 + 0b1010
[ 0b10010 ]:

Examples

Implicit multiplication:

[ 0 ]: 2(3 + 1)
[ 8 ]:

[ 0 ]: (2 + 1)(4 - 1)
[ 9 ]:

Compound interest β€” 1000 at 7% for 10 years:

[ 0 ]: 1000 * 1.07 ^ 10
[ 1967.15135729 ]:

Pythagorean hypotenuse β€” sides 3 and 4:

[ 0 ]: sqrt(3^2 + 4^2)
[ 5 ]:

Variables β€” monthly mortgage:

[ 0 ]: rate = 0.06 / 12
rate = 0.005
[ 0 ]: n = 360
n = 360
[ 0 ]: factor = (1 + rate) ^ n
factor = 10.9357...
[ 0 ]: 200000 * rate * factor / (factor - 1)
[ 1199.1010503 ]:

Angle conversion β€” degrees to radians, then sine:

[ 0 ]: 30 * pi / 180
[ 0.5235987756 ]: sin()
[ 0.5 ]:

Script files

When reading from a file (ccalc < formula.txt) you have three tools to control output:

Comments

% starts a comment (Octave/MATLAB convention). # is a supported alias (Octave and shell style). Both can appear as the first character on the line (full-line comment) or inline after an expression.

% Cylinder volume: V = pi * r^2 * h
pi * 5^2      % pi * r^2, r = 5

# same as above β€” hash-style comment
pi * 5^2      # inline hash comment

Semicolon β€” suppress output

A trailing ; evaluates the expression and updates ans, but prints nothing. Use it to silence intermediate steps.

rate = 0.06 / 12;     % monthly rate β€” silent
n = 360;              % 30-year term β€” silent
factor = (1 + rate) ^ n;
200000 * rate * factor / (factor - 1)
fprintf('Monthly payment ($): ')
disp(ans)

disp(expr) β€” print value

disp(expr) evaluates the expression and prints the result. It does not update ans.

disp(ans)             % print current ans
disp(rate * 12)       % print expression result

fprintf β€” print formatted text

fprintf(fmt, v1, ...) prints formatted output using C-style specifiers. No newline is added automatically β€” include \n explicitly.

fprintf('=== Resistors in series ===\n')

R_total = 100 + 220 + 470;
fprintf('Total resistance: %d Ohm\n', R_total)

fprintf('=== Parallel combination ===\n')

R_par = 1 / (1/100 + 1/220);
fprintf('Parallel resistance: %.2f Ohm\n', R_par)

Output:

=== Resistors in series ===
Total resistance: 790 Ohm
=== Parallel combination ===
Parallel resistance: 68.75 Ohm

Example files

The examples/ directory contains annotated formula files ready to run:

File Description
cylinder.calc Volume and surface area of a cylinder
mortgage.calc Monthly mortgage payment
data_storage.calc Real GiB capacity of a "500 GB" drive
resistors.calc Series, parallel resistance, voltage divider, power
ac_impedance.calc AC impedance, phase angle, dB level, bit width
matrix_ops.calc Rotation, linear system solve, element-wise ops
sequences.calc Ranges, linspace, indexing, slicing, finite differences
logic.calc Comparison, logical NOT, &&/||, masks, soft clipping
bitwise.calc Bitmask construction, register bit fields, RGB colour packing
vector_utils.calc nan/inf, reductions, sort/find/unique, end indexing, reshape/flip
complex_numbers.calc Complex arithmetic, polar form, built-ins, AC circuit
strings.calc Char arrays, string objects, arithmetic, built-ins, unit labels
formatted_output.calc fprintf/sprintf specifiers, flags, escape sequences, data table
format_modes.calc All format display modes: short/long/shortE/bank/rat/hex/+/compact
file_io.calc File I/O: fopen/fclose/fgetl/fgets, dlmread/dlmwrite, isfile/isfolder/exist/pwd, save/load with path
csv/csv.calc CSV tables: readmatrix (header auto-skip, NaN for empty), readtable (Struct of columns), writetable (RFC 4180 quoting), tab-separated variant
json/json.calc JSON encode/decode: primitives, arrays, objects, nested structs, roundtrip, file I/O, dataset analysis β€” requires --features json
control_flow.calc Core control flow: if/elseif/else, for, while, break/continue, compound operators; grade classifier, prime sieve, Newton-Raphson, Collatz
extended_control_flow.calc Extended control flow: switch/case, do...until, run()/source(); exit-code classifier, unit converter, digit sum, Euclidean GCD
user_functions.calc User-defined functions and lambdas: recursion, multiple return values, nargin, anonymous functions, lexical capture, midpoint integration, higher-order functions
cell_arrays.calc Cell arrays: literals, brace-indexing, auto-grow, @funcname handles, cellfun/arrayfun, varargin/varargout, case {…}, function pipelines
structs.calc Scalar structs: field assignment/read, nested structs, struct() constructor, fieldnames/isfield/rmfield/isstruct, 3-D vector example
struct_arrays.calc Struct arrays: indexed creation, element access, field collection β†’ matrix/cell, loop building, fieldnames/isfield, nested fields, inventory ledger
error_handling.calc Error handling: error/warning, lasterr, try/catch, try(expr,default), pcall, nested and loop-safe error recovery
indexed_assignment.calc Indexed assignment: element/slice/submatrix write, growing vectors with end+1, cell array growth, logical mask read/write
ccalc < examples/mortgage.calc

Building and testing

cargo build            # debug build
cargo build --release  # optimized build
cargo test             # run all tests
cargo bench            # run Criterion benchmarks (release)
cargo bench --bench engine -- loop_10k   # run one benchmark

Optional features:

cargo build --release --features json   # enable jsondecode / jsonencode (serde_json)
cargo build --release --features blas   # accelerate matrix multiply via system OpenBLAS
cargo build --release --features json,blas  # both

Project structure

crates/
  ccalc/src/
    main.rs      β€” entry point, mode detection (arg / pipe / REPL), CLI flags
    repl.rs      β€” REPL loop, run_pipe(), run_expr(), shared evaluate() core
    help.rs      β€” help text
  ccalc-engine/src/
    lib.rs       β€” crate root, public module exports
    env.rs       β€” Value enum (Scalar/Matrix/Complex/Str/StringObj/Void/Lambda/Function/Tuple/Cell/Struct), Env type (HashMap<String, Value>), workspace save/load
    eval.rs      β€” AST types (Expr, Op) + evaluator returning Value + number formatters + Base/FormatMode enums + FnCallHook
    exec.rs      β€” block statement executor: exec_stmts(), Signal enum (Break/Continue/Return), call_user_function()
    io.rs        β€” IoContext (file descriptor table), fopen/fclose/fgetl/fgets/write_to_fd
    parser.rs    β€” lexer (tokenizer) + recursive descent parser, Stmt enum (incl. If/For/While/FunctionDef/Return/MultiAssign)
  ccalc-engine/benches/
    engine.rs    β€” Criterion benchmark suite (scalar ops, fib, loop, matmul, inv, fn calls)
Cargo.toml       β€” workspace manifest (single source of truth for version)
CHANGELOG.md     β€” version history

License

MIT