/// Entry point for all help output.
///
/// - `None`       → brief usage (for `-h` / `--help` flag)
/// - `Some("")`   → one-screen cheatsheet (REPL `help`)
/// - `Some(topic)`→ detailed section (REPL `help <topic>`)
pub fn print(topic: Option<&str>) {
    match topic {
        None => print_usage(),
        Some("" | "help") => print_cheatsheet(),
        Some("syntax") => print_syntax(),
        Some("functions" | "fn" | "func") => print_functions(),
        Some("bases" | "base") => print_bases(),
        Some("vars" | "variables") => print_vars(),
        Some("script" | "pipe" | "printf") => print_script(),
        Some("format" | "fmt") => print_format(),
        Some("matrices" | "matrix" | "mat") => print_matrices(),
        Some("index" | "indexing" | "indexed" | "assignment") => print_index_assign(),
        Some("logic" | "logical" | "comparison") => print_logic(),
        Some("examples" | "ex") => print_examples(),
        Some("vectors" | "vector" | "utils") => print_vectors(),
        Some("complex" | "cplx" | "imag") => print_complex(),
        Some("strings" | "string" | "str" | "char") => print_strings(),
        Some("files" | "file" | "fileio" | "io" | "fopen" | "fclose") => print_fileio(),
        Some(
            "control" | "flow" | "if" | "for" | "while" | "switch" | "do" | "run" | "source"
            | "path" | "addpath" | "rmpath",
        ) => print_control(),
        Some(
            "errors" | "error" | "warning" | "try" | "catch" | "pcall" | "lasterr"
            | "error-handling" | "exceptions",
        ) => print_errors(),
        Some(
            "userfuncs" | "userfunc" | "ufunc" | "lambda" | "lambdas" | "anon" | "user"
            | "function" | "closures",
        ) => print_userfuncs(),
        Some(
            "cells" | "cell" | "cellfun" | "arrayfun" | "varargin" | "varargout" | "cell-arrays",
        ) => print_cells(),
        Some("structs" | "struct" | "fieldnames" | "isfield" | "rmfield" | "isstruct") => {
            print_structs()
        }
        Some(
            "scoping" | "scope" | "global" | "persistent" | "private" | "packages" | "package"
            | "namespace" | "namespaces" | "pkg",
        ) => print_scoping(),
        Some(unknown) => {
            eprintln!("Unknown help topic: '{unknown}'");
            eprintln!(
                "Available topics: syntax  functions  userfuncs  cells  structs  errors  scoping  bases  vars  script  format  matrices  index  logic  vectors  complex  strings  files  io  control  path  examples"
            );
        }
    }
}

// ---------------------------------------------------------------------------
// -h / --help  (CLI flag — modes and options only)
// ---------------------------------------------------------------------------

fn print_usage() {
    println!(
        "\
ccalc v{ver} — terminal calculator with Octave/MATLAB syntax

USAGE
    ccalc                   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 a file

OPTIONS
    -h, --help      show this message
    -v, --version   show version

Start the REPL and type 'help' for the full reference.",
        ver = env!("CARGO_PKG_VERSION")
    );
}

// ---------------------------------------------------------------------------
// help  (REPL — one-screen cheatsheet)
// ---------------------------------------------------------------------------

fn print_cheatsheet() {
    println!(
        "\
ccalc v{ver} — terminal calculator with Octave/MATLAB syntax

Operators   + - * / ^ **     ^ and ** are right-associative
            2(3+1) → 8       implicit multiplication
            +x               unary + (no-op)   ...  line continuation
Comparison  ==  ~= (!=)  <  >  <=  >=     return 1 (true) or 0 (false)
Logical     ~expr (!expr)  &&  ||          NOT, AND, OR (short-circuit)
            &   |                          element-wise AND, OR (matrices)
            xor(a,b)  not(a)               exclusive OR, logical NOT
Constants   pi  e  ans  nan  inf  i  j  (imaginary unit, 4i works)
Partial     [ 100 ]: / 4     starts with operator → uses ans

1-arg   sqrt abs floor ceil round sign exp ln log
        sin cos tan  asin acos atan
2-arg   atan2(y,x)  mod(a,b)  rem(a,b)  max(a,b)  min(a,b)
        hypot(a,b)  log(x,base)
        fn() → fn(ans)

Bases   0xFF  0b1010  0o17    hex dec bin oct base

Matrix  [1 2 3]   [1;2;3]   [1 2;3 4]
        A*B (matmul)  A' (transpose)  A.*B  A./B  A.^n
        A\\b (left-divide / linear solve)  a\\b = b/a (scalar)
        zeros(m,n)  ones(m,n)  eye(n)  size  det  inv  trace
Range   1:5  →  [1 2 3 4 5]     1:2:9  →  [1 3 5 7 9]
        linspace(a,b,n)   [1:3, 10]  →  [1 2 3 10]
Index   v(3)  v(2:4)  v(:)  v(end)  v(end-1:end)   1-based
        A(i,j)  A(:,j)  A(i,:)  A(end,:)  A(1:end-1, 2:end)
        v(i) = x  v(1:3) = 0  v(end+1) = x  A(:,j) = col
        v(v > 0)  v(mask) = 0  (logical mask indexing)
Vector  sum prod mean min max any all norm(v) norm(v,p)
        cumsum cumprod  sort  find  unique
        reshape(A,m,n)  fliplr  flipud
NaN/Inf nan  inf  isnan  isinf  isfinite  nan(m,n)
Complex 3+4i  3+4j  4i  complex(re,im)    (Ni syntax works directly)
        real(z) imag(z) abs(z) angle(z) conj(z) isreal(z)
        z' = conj(z)   z.' = plain transpose (no conjugation)
Strings 'char array'  \"string object\"
        num2str  str2num  str2double  strcat  strcmp  strcmpi
        lower  upper  strtrim  strrep  ischar  isstring
        strsplit(s,delim)  int2str(x)  mat2str(A)
Bitwise bitand(a,b)  bitor(a,b)  bitxor(a,b)
        bitshift(a,n)  bitnot(a)  bitnot(a,bits)

Compound  x += e  x -= e  x *= e  x /= e    desugar to x = x op e
          x++  x--  ++x  --x                statement-level only
Control   if cond / elseif / else / end
          for var = range / end
          while cond / end
          switch expr / case val / otherwise / end
          do / until (cond)                 body runs at least once
          break   continue
Errors    error('msg')  error('fmt', v...)  raise a runtime error
          warning('fmt', v...)              print warning, continue
          lasterr()                         last error message
          try / catch / end                 protected block (anonymous)
          try / catch e / end              named: e.message = error string
          try(expr, default)               inline fallback (lazy default)
          pcall(@f, args...)               [ok, val] = pcall(...)
Scripts   run('file.calc')  run('file')     .calc first, then .m
          source('file')                    Octave alias for run()
Path      addpath('/dir')                   prepend to session search path
          addpath('/dir', '-end')           append to session search path
          rmpath('/dir')                    remove from search path
          path()                            display current search path
          genpath('/dir')                   return dir + all subdirs as path string
Functions function y = f(x) ... end         named, single return
          function [a,b] = f(x) ... end     multiple return values
          [a,b] = f(x)   [~,b] = f(x)      multi-assign, ~ discards
          nargin                            # args actually passed
          return                            early exit
          varargin / varargout              variadic args via cell array
Lambda    f = @(x) expr                    anonymous function
          g = @(x,y) expr                  multi-arg lambda
          h = @funcname                    function handle (wraps builtin/named)
Cells     c = {{1, 'hi', [1 2 3]}}        cell literal (heterogeneous)
          c{{2}}                           brace-index: returns element
          c{{2}} = val                     assign to element (auto-grows)
          iscell(c)  cell(n)  numel(c)     predicates, constructor, size
          cellfun(@f, c)                   apply f to each cell element
          arrayfun(@f, v)                  apply f to each vector element
          case {{2, 3}}                    multi-value switch case
Scoping global x              shared across all functions and the workspace
        persistent x         per-function value that survives between calls
        private/             directory-scoped helpers (visible only to parent dir)
        utils.func(args)     package call — searches +utils/func.calc on path
Vars    x = expr              shows: x = <val>  (ans unchanged)
        x = expr;             silent assignment
        who   clear   clear x
        ws/save (save workspace)   wl/load (load workspace)
        save('f.mat')  save('f.mat','x','y')  load('f.mat')

Output  disp(expr)
        fprintf('fmt', v1, v2, ...)   print formatted  (C printf)
        sprintf('fmt', v1, v2, ...)   return formatted string
        Specifiers: %d %i %f %e %g %s %%   Width/prec: %8.3f %-10s
Files   fd = fopen('f.txt','w')   fclose(fd)   fclose('all')
        fprintf(fd,'fmt',v1,...)  fgetl(fd)  fgets(fd)
        dlmwrite('f.csv',A)  dlmwrite('f.tsv',A,'\t')
        data = dlmread('f.csv')  data = dlmread('f.tsv','\t')
        isfile(p)  isfolder(p)  pwd()  exist('x','var')  exist('f','file')
Format  format short   5 sig digits (default)   format long    15 sig digits
        format shortE  always scientific         format longE
        format shortG  same as short             format bank    2 decimal places
        format rat     rational (p/q)            format hex     IEEE 754 hex
        format +       sign only (+/-/space)
        format compact suppress blank lines      format loose   add blank lines
        format N       N decimal places (e.g. format 4)
Config  config                show config path and active settings
        config reload         re-read config.toml and apply changes
REPL    exit  quit  cls  Ctrl+L (clear screen)
Keys    ↑↓ history  Ctrl+R search  Ctrl+A/E line start/end
        Ctrl+W del word  Ctrl+U del to start  Ctrl+K del to end

  help syntax      operators, precedence, implicit multiplication
  help functions   built-in function reference with examples
  help userfuncs   user-defined functions, multiple return, lambdas
  help cells       cell arrays, varargin/varargout, cellfun, arrayfun
  help structs     scalar structs + struct arrays, field access, fieldnames/isfield/rmfield
  help errors      error/warning, try/catch, try(expr,default), pcall
  help scoping     global/persistent variables, private/ dirs, packages (pkg.func)
  help bases       number bases, display switching
  help format      number display format modes (short/long/bank/rat/hex/+)
  help vars        variables and workspace
  help script      pipe/script mode, semicolons, disp, fprintf
  help matrices    matrix literals, arithmetic, ranges, indexing
  help index       indexed assignment, growing vectors, logical masks
  help vectors     nan/inf, reductions, sort/find/unique, end, reshape
  help logic       comparison and logical operators, masks
  help complex     complex numbers, i/j unit, abs/angle/conj/real/imag
  help strings     char arrays, string objects, strcmp, num2str, ...
  help files       file I/O: fopen/fclose/fgetl/fgets, dlmread/dlmwrite, isfile, pwd
  help control     if/for/while, break/continue, compound assignment, run/source
  help path        addpath/rmpath/path()/genpath() — session search path
  help examples    practical usage examples",
        ver = env!("CARGO_PKG_VERSION")
    );
}

// ---------------------------------------------------------------------------
// help syntax
// ---------------------------------------------------------------------------

fn print_syntax() {
    println!(
        "\
SYNTAX

Operators
    +  -  *  /  ^  **   (** is Octave alias for ^)
    Comparison:   ==  ~=  <  >  <=  >=    (return 1.0 or 0.0)
    Logical:      ~expr   &&   ||          short-circuit scalars
                  &    |                   element-wise (matrices OK)
                  xor(a,b)  not(a)

    Precedence (high to low):
      postfix '  .'  transpose / plain transpose
      ^  **       exponentiation (right-associative)
      unary +  -  ~  no-op, negation, logical NOT
      *  /  \\  .*  ./  .^  multiply, divide, left-divide, element-wise
      +  -        addition, subtraction
      :           range (a:b, a:step:b)
      ==  ~=  <  >  <=  >=   comparison (non-associative)
      &           element-wise AND
      |           element-wise OR
      &&          short-circuit logical AND
      ||          short-circuit logical OR

    2 > 1 && 3 > 2     →  1    AND of two comparisons
    1 + 1 == 2         →  1    arithmetic evaluated first
    ~0                 →  1    logical NOT
    v > 3              →  element-wise mask (0/1 matrix)
    +x                 →  x    unary + is a no-op

Grouping
    (2 + 3) * 4     →  20
    ~(x == 0)       →  negate comparison

Implicit multiplication
    A number or closing parenthesis immediately before ( triggers *:
    2(3 + 1)        →   8    (same as 2 * (3 + 1))
    (2+1)(4-1)      →   9

Partial expressions
    An expression starting with an operator uses ans as the left operand:
    [ 100 ]: / 4      →  25
    [  25 ]: + 5      →  30
    [  30 ]: ^ 2      →  900

Comments
    % this is a comment           (Octave/MATLAB style)
    # this is a comment           (Octave/shell alias)
    10 * 5  % inline comment — expression still evaluates
    10 * 5  # hash-style inline comment

Semicolons and commas
    Trailing ; suppresses output.
    Expressions — ans is still updated:    0.06 / 12;
    Assignments — ans is never updated:    rate = 0.06 / 12;

    Statement separators on one line:
    a = 1; b = 2         ;  → a silent, b shown
    a = 1, b = 2         ,  → both shown (comma is non-silent separator)
    a = 1; b = 2;        both silent

    Inside a matrix literal [ ], ; is always a row separator:
    [1 2; 3 4]           2×2 matrix — the ; is not a statement separator

Line continuation  (...)
    Long lines can continue on the next line using ...:
    result = 1 + ...
             2 + ...
             3;               → result = 6
    A = [1 2 3; ...
         4 5 6];              → 2×3 matrix
    if value > 0 && ...
       value < 100
      disp('ok')
    end

Range operator
    a:b               row vector  [a, a+1, ..., b]   (step = 1)
    a:step:b          row vector with explicit step
    1:5               →  [1 2 3 4 5]
    0:0.5:2           →  [0 0.5 1 1.5 2]
    5:-1:1            →  [5 4 3 2 1]
    Range is lower precedence than arithmetic:
    1+1:2+2           →  2:4  →  [2 3 4]"
    );
}

// ---------------------------------------------------------------------------
// help functions
// ---------------------------------------------------------------------------

fn print_functions() {
    println!(
        "\
FUNCTIONS

One-argument
    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)        natural logarithm (base e)
    log2(x)       base-2 logarithm
    log10(x)      base-10 logarithm
    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
    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)   sqrt(a^2 + b^2), numerically stable
    log(x, base)  logarithm of x to an arbitrary base

mod vs rem
    mod(-1, 3)  →   2    sign of the divisor  (use for angle wrapping)
    rem(-1, 3)  →  -1    sign of the dividend (IEEE 754 truncation)

Empty parentheses — ans is passed as the sole argument
    sqrt()   →   sqrt(ans)
    abs()    →   abs(ans)

Nesting
    sqrt(abs(-16))        →   4
    max(hypot(3, 4), 6)   →   6
    floor(log(1000))      →   3

Bitwise (non-negative integers only; works naturally with 0x/0b/0o literals)
    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); 0 if |n|>=64
    bitnot(a)         NOT in 32-bit window  (Octave uint32 default)
    bitnot(a, bits)   NOT in explicit bit-width window (bits in [1, 53])

    bitand(0xFF, 0x0F)      →  15
    bitor(0b1010, 0b0101)   →  15
    bitxor(0xFF, 0x0F)      →  240
    bitshift(1, 8)          →  256     (1 << 8)
    bitshift(256, -4)       →  16      (256 >> 4)
    bitnot(5, 8)            →  250     (~5 in 8 bits = 0b11111010)
    bitnot(0, 32)           →  4294967295

Constants
    pi     3.14159265358979...
    e      2.71828182845904...
    nan    IEEE 754 Not-a-Number — propagates through all arithmetic
           nan + 5  →  NaN     nan == nan  →  0  (always false)
    inf    positive infinity   -inf for negative
    i, j   imaginary unit: 0 + 1i  (can be reassigned; restart to restore)
    ans    result of last expression

Complex functions  (see also: help complex)
    real(z)          real part  (real(5) = 5)
    imag(z)          imaginary part  (imag(5) = 0)
    abs(z)           modulus sqrt(re²+im²)  (overloads scalar/matrix abs)
    angle(z)         argument atan2(im, re), in radians
    conj(z)          complex conjugate  re - im*i
    complex(re, im)  construct from two real scalars
    isreal(z)        1 if im == 0, else 0

Examples
    hypot(3, 4)                →   5
    atan2(1, 1) * 180 / pi     →  45
    log(8, 2)                  →   3
    mod(370, 360)              →  10
    abs(3 + 4*i)               →   5
    angle(i)                   →   1.5707963...  (π/2)

String functions  (see also: help strings)
    num2str(x)         number → char array ('3.1416' for pi)
    num2str(x, N)      number → char array with N decimal digits
    str2num(s)         char array → number  (error if not parseable)
    str2double(s)      char array → number  (NaN if not parseable)
    strcat(a, b, ...)  concatenate two or more strings
    strcmp(a, b)       1 if equal (case-sensitive), else 0
    strcmpi(a, b)      1 if equal (case-insensitive), else 0
    lower(s)           convert to lowercase
    upper(s)           convert to uppercase
    strtrim(s)         strip leading and trailing whitespace
    strrep(s, old, new)  replace all occurrences of old with new
    sprintf(fmt, ...)  format string (C printf); returns char array
    fprintf(fmt, ...)  format and print to stdout
    ischar(s)          1 if s is a char array, else 0
    isstring(s)        1 if s is a string object, else 0

Higher-order  (see also: help cells)
    cellfun(f, c)   apply f to each element of cell c; returns Matrix if all scalar
    arrayfun(f, v)  apply f to each element of numeric vector v; returns Matrix
    @funcname       function handle — wraps a builtin or named function as a lambda

    cellfun(@sqrt, {{1, 4, 9}})     →  [1 2 3]
    arrayfun(@(x) x^2, [1 2 3])    →  [1 4 9]
    f = @abs; f(-5)                 →  5

See also: help vectors    (sum, min, max, sort, find, norm, cumsum, ...)
          help complex    (full complex number reference)
          help strings    (char arrays, string objects, full reference)
          help script     (fprintf/sprintf reference with format specifiers)
          help cells      (cell arrays, varargin, cellfun, arrayfun)"
    );
}

// ---------------------------------------------------------------------------
// help format
// ---------------------------------------------------------------------------

fn print_format() {
    println!(
        "\
NUMBER DISPLAY FORMAT  (help format)

Change how numbers are displayed in the REPL and pipe/script mode.
The format command affects disp(), variable assignment output, and
the REPL prompt — but not fprintf/sprintf (which use their own specifiers).

SYNTAX
    format               reset to 'short' (5 significant digits)
    format <mode>        switch to named mode
    format <N>           N decimal places (e.g. format 4)

MODES
    short     5 significant digits, auto fixed/scientific  (default)
    long      15 significant digits, auto fixed/scientific
    shortE    always scientific notation, 4 decimal places
    longE     always scientific notation, 14 decimal places
    shortG    same as short  (MATLAB shortG alias)
    longG     same as long   (MATLAB longG alias)
    bank      fixed 2 decimal places  (currency)
    rat       rational approximation  p/q  (e.g. 1/3, 22/7)
    hex       IEEE 754 double-precision bit pattern (16 hex digits)
    +         sign character only: + for positive, - for negative, space for 0
    compact   suppress blank lines between outputs
    loose     add blank line after every output
    N         N decimal places (fixed or scientific as needed)

EXAMPLES
    >> format short
    >> pi
    3.1416

    >> format long
    >> pi
    3.14159265358979

    >> format shortE
    >> pi
    3.1416e+00

    >> format bank
    >> 1/3
    0.33

    >> format rat
    >> pi
    355/113

    >> format hex
    >> 1.0
    3FF0000000000000

    >> format +
    >> [1 -2 0 3]
    +- +

    >> format 4
    >> 1/3
    0.3333

    >> format compact
    (blank lines between matrix outputs are suppressed)

    >> format loose
    (blank line added after every output)

Note: 'format hex' (IEEE 754 bits) and 'hex' (integer display base) are
different commands. 'hex' changes the base for integer display; 'format hex'
shows the raw double-precision bit pattern of any number."
    );
}

// ---------------------------------------------------------------------------
// help bases
// ---------------------------------------------------------------------------

fn print_bases() {
    println!(
        "\
NUMBER BASES

Input literals — mix freely in any expression
    0xFF        hexadecimal    (0x or 0X prefix)
    0b1010      binary         (0b or 0B prefix)
    0o17        octal          (0o or 0O prefix)

    0xFF + 0b1010   →  265

Display commands
    hex     switch display to hexadecimal
    dec     switch display to decimal  (default)
    bin     switch display to binary
    oct     switch display to octal

Inline base suffix — evaluate and switch display in one step
    0xFF + 0b1010 hex   →   0x109

base — show ans in all four bases at once
    [ 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 shown first:
    [ 0x6 ]: 0b11 + 0b11
    0x3 + 0x3
    [ 0x6 ]:"
    );
}

// ---------------------------------------------------------------------------
// help vars
// ---------------------------------------------------------------------------

fn print_vars() {
    println!(
        "\
VARIABLES

Assignment  (never updates ans; ; suppresses display)
    x = expr       shows: x = <val>
    x = expr;      silent

Using variables
    [ 0 ]: rate = 0.06 / 12
    [ 0 ]: 1 + rate
    [ 1.005 ]:

Built-in variables
    ans    result of last expression  (initialized to 0 on startup)
    pi     3.14159265358979...
    e      2.71828182845904...
    nan    IEEE 754 Not-a-Number  (propagates through arithmetic)
    inf    positive infinity  (use -inf for negative)
    i, j   imaginary unit: 0 + 1i  (can be reassigned; restart to restore)

View and clear
    who            list all defined variables and their values
    clear          clear all variables  (ans reset to 0)
    clear name     clear a single variable by name

Workspace persistence
    ws / save               save all variables to ~/.config/ccalc/workspace.toml
    wl / load               load variables from file  (replaces the current workspace)
    save('path.mat')        save all variables to named file
    save('path.mat','x','y')  save specific variables only
    load('path.mat')        load from named file

    Only scalars and strings are persisted; matrices and complex values are skipped.
    The workspace file is plain text: one 'name = value' per line."
    );
}

// ---------------------------------------------------------------------------
// help script
// ---------------------------------------------------------------------------

fn print_script() {
    println!(
        "\
PIPE / SCRIPT MODE

Running non-interactively: no prompt, one result printed per line.
    echo \"2 ^ 10\" | ccalc
    ccalc script.m
    ccalc < formulas.txt

Semicolon — suppress output
    rate = 0.06 / 12;    silent assignment  (ans unchanged)
    n = 360;             silent assignment  (ans unchanged)
    0.06 / 12;           silent expression  (ans = 0.005)

    Assignments never update ans regardless of ;.
    Expressions always update ans; ; only hides the output.

Comments
    % full-line comment
    10 * 5  % inline comment — expression still evaluates

disp(expr) — print value without updating ans
    disp(ans)
    disp(rate * 12)

fprintf(fmt, v1, v2, ...) — print formatted output (C printf)
    fprintf('x = %d\\n', 42)
    fprintf('%.4f\\n', pi)
    fprintf('%s = %.2f\\n', 'rate', 0.065)
    fprintf('%8.3f  %-10s\\n', 3.14159, 'pi')

sprintf(fmt, v1, v2, ...) — format and return as string
    s = sprintf('R = %.1f Ohm', 47.5)
    disp(s)

Format specifiers
    %d  %i    integer (truncated)
    %f        fixed decimal  (default 6 places)
    %.Nf      fixed with N decimal places
    %e        scientific  1.23e+04
    %g        shorter of %%f and %%e  (trailing zeros trimmed)
    %s        string
    %%        literal percent sign
    Width/flags:  %8.3f   %-10s   %+.4e   %05d

Escape sequences inside strings
    \\n   newline
    \\t   horizontal tab
    \\\\   literal backslash

Octave behaviour: if more arguments than specifiers, the format
string repeats for the remaining arguments.

Commands that work in pipe/script mode
    exit / quit              stop processing
    who / clear              manage variables
    ws / wl / save / load    workspace save and load
    save('f.mat')            save to explicit path
    save('f.mat','x','y')    save specific variables
    load('f.mat')            load from explicit path
    hex/dec/bin/oct          display base
    Precision is set in config.toml (default 10)

Example script
    % Monthly mortgage payment
    rate = 0.06 / 12;
    n = 360;
    factor = (1 + rate) ^ n;
    payment = 200000 * rate * factor / (factor - 1);
    fprintf('Monthly payment: $%.2f\\n', payment)"
    );
}

// ---------------------------------------------------------------------------
// help matrices
// ---------------------------------------------------------------------------

fn print_matrices() {
    println!(
        "\
MATRICES

Literals
    [1 2 3]           row vector  (1×3)
    [1; 2; 3]         column vector  (3×1)
    [1 2; 3 4]        2×2 matrix
    [1, 2; 3, 4]      commas also separate elements

    Elements can be expressions:
    [sqrt(4), 2^3]    →  [2, 8]

Arithmetic  (scalar operations are element-wise)
    A + B   A - B     element-wise  (shapes must match)
    2 * A             scale all elements
    A / 10   A ^ 2    element-wise divide / power

Matrix multiplication and transpose
    A * B             matrix multiplication  (inner dims must agree)
    A'                transpose  (postfix, highest precedence)
    v' * v            dot product  (row × column → scalar-like 1×1)
    v * v'            outer product  (column × row → matrix)

Element-wise operators  (.* ./ .^ — shapes must match)
    A .* B            element-wise product  (Hadamard product)
    A ./ B            element-wise division
    A .^ 2            element-wise power  (same as A .* A)

Left division (backslash)
    A \\ b            solve A*x = b  (Gaussian elim with partial pivoting)
                      more stable than inv(A) * b
    a \\ b            scalar: equivalent to b / a
    A \\ B            multiple RHS: solve for each column of B independently

    A = [2 1; 5 7];  b = [11; 13];
    x = A \\ b              →  [3; 5]   (solves exactly)
    4 \\ 20                 →  5        (scalar: 20 / 4)

Built-in functions
    zeros(m,n)        m×n matrix of zeros
    zeros(n)          n×n matrix of zeros
    ones(m,n)         m×n matrix of ones
    ones(n)           n×n matrix of ones
    eye(n)            n×n identity matrix
    size(A)           [rows cols] as a 1×2 row vector
    size(A, dim)      rows (dim=1) or cols (dim=2) as scalar
    length(A)         max(rows, cols)
    numel(A)          total element count
    trace(A)          sum of diagonal elements
    det(A)            determinant  (square matrices only)
    inv(A)            inverse  (square, non-singular)

Range operator
    a:b               row vector from a to b with step 1
    a:step:b          row vector with explicit step (may be negative)
    1:5               →  [1 2 3 4 5]
    1:2:9             →  [1 3 5 7 9]
    5:-1:1            →  [5 4 3 2 1]
    0:0.5:2           →  [0 0.5 1 1.5 2]
    Ranges work inside [ ]:
    [1:3, 10]         →  [1 2 3 10]
    [1:2:7]           →  [1 3 5 7]

linspace
    linspace(a,b,n)   n evenly spaced values from a to b (inclusive)
    linspace(0,1,5)   →  [0 0.25 0.5 0.75 1]

Display
    A =
       1   2
       3   4
    Prompt shows size when ans is a matrix:  [ [2×2] ]:

Indexing  (1-based — Octave convention)
    v(3)              scalar element (3rd)
    v(2:4)            sub-vector  (elements 2, 3, 4)
    v(:)              all elements as a column vector
    A(i,j)            scalar element at row i, column j
    A(:,j)            entire column j  (result: Nx1)
    A(i,:)            entire row i     (result: 1xM)
    A(1:2, 2:3)       submatrix via range indices
    Variables in env shadow function names (same as Octave):
    v = [10 20 30]; v(2)  →  20

end keyword — resolves to the size of the indexed dimension
    v(end)            last element
    v(end-2:end)      last three elements
    A(end, :)         last row
    A(1:end-1, 2:end) all rows except last, columns 2 to end

Display
    A =
       1   2
       3   4
    Prompt shows size when ans is a matrix:  [ [2×2] ]:

Indexed assignment  (write path — mirrors the read forms above)
    v(3) = 42             set single element
    v(1:3) = [10 20 30]   slice: RHS same length as selection
    v(4:6) = 0            scalar broadcast to all selected positions
    v(:) = 0              reset all elements at once
    A(2,3) = 7            2-D element
    A(:,1) = [1;2;3;4]   entire column
    A(1,:) = [1 2 3 4]   entire row
    A(2:3,2:3) = eye(2)   submatrix

Growing vectors — assigning beyond current length extends with zeros
    v = [];  v(end+1) = x   append: end resolves to current length
    v(7) = 99               pads to length 7, fills gap with zeros
    v(i) = x                auto-creates row vector if v is undefined

Logical (boolean mask) indexing
    v(v > 0)              read: select elements where mask is true
    v(v < 0) = 0          write: modify elements where mask is true
    m = v > 3;  v(m) = 0  using a pre-computed mask variable
    M(M > 5)              2-D matrix: elements in column-major order
    M(M > 5) = 0          2-D masked write

Workspace
    ws  saves only scalar variables — matrices are not persisted.
    who shows dimensions:  A = [2×2 double]

See also: help index   (full indexed-assignment reference)"
    );
}

// ---------------------------------------------------------------------------
// help index
// ---------------------------------------------------------------------------

fn print_index_assign() {
    println!(
        "\
INDEXED ASSIGNMENT  (help index)

All read-index forms work as write targets. The left-hand side must be a
variable name — arbitrary expressions are not allowed as assignment targets.

─── Scalar and slice assignment ────────────────────────────────────────────────

  v = zeros(1, 6);
  v(3) = 42;               % set element 3
  v(1:2) = [10, 20];       % slice: RHS length must equal selection
  v(4:6) = 99;             % scalar broadcast to 3 positions
  v(:) = 0;                % reset all elements at once

─── 2-D matrix assignment ──────────────────────────────────────────────────────

  A = zeros(4);
  A(2, 3) = 7;               % scalar at row 2, col 3
  A(:, 1) = [1; 2; 3; 4];   % full column (RHS must be column vector)
  A(1, :) = [10 20 30 40];   % full row
  A(2:3, 2:3) = eye(2);      % 2×2 submatrix

─── Broadcasting rule ──────────────────────────────────────────────────────────

  When RHS is a scalar and the LHS index selects multiple elements,
  the scalar is broadcast to every selected position:

  v(1:5) = 0                 zero five elements
  A(:, 2) = 1                fill column 2 with ones

─── Growing vectors ────────────────────────────────────────────────────────────

  v = [];
  for k = 1:5
    v(end+1) = k^2;          % end = current length, so this appends
  end
  % v = [1 4 9 16 25]

  v = [1 2 3];
  v(7) = 99;                 % → [1 2 3 0 0 0 99]  (gap padded with zeros)

  If the variable does not exist, the first indexed assignment creates it
  as a 1×N row vector.

─── Logical (boolean mask) indexing ────────────────────────────────────────────

  A 0/1 vector whose element count equals the dimension size is a boolean
  mask rather than a list of indices.  This enables compact read/write idioms:

  Read — select elements where mask is 1:
    v = [3, -1, 8, 0, 5, -2, 7];
    pos = v(v > 0);            % → [3  8  5  7]

  Write — modify elements where mask is 1:
    v(v < 0) = 0;              % zero out negatives (half-wave rectifier)

  Using a separate mask variable:
    signal = [0.5, -1.2, 0.8, -0.3, 1.5, -2.0, 0.1];
    noise  = signal < 0;
    signal(noise) = 0;         % zero out noise samples

  2-D matrix logical mask (elements in column-major order):
    M = [1 2 3; 4 5 6; 7 8 9];
    M(M > 5)                   % → [7 8 6 9]
    M(M > 5) = 0;              % zero those elements in place

─── end in index expressions ───────────────────────────────────────────────────

  end resolves to the current length of the dimension being indexed,
  enabling relative addressing from the tail:

    v(end)       last element (read)
    v(end) = x   overwrite last element (write)
    v(end+1) = x append (write — extends the vector)
    v(end-1:end) = [a b]   overwrite last two elements

─── Practical patterns ─────────────────────────────────────────────────────────

  % Build a Fibonacci sequence element by element
  fib = [];
  fib(1) = 1;  fib(2) = 1;
  for k = 3:12
    fib(end+1) = fib(end) + fib(end-1);
  end

  % Collect even numbers, then cap at 10
  evens = [];
  for k = 1:20
    if mod(k, 2) == 0
      evens(end+1) = k;
    end
  end
  evens(evens > 10) = 10;

See also: help matrices  help vectors  help logic
Example:  ccalc examples/indexed_assignment.calc"
    );
}

// ---------------------------------------------------------------------------
// help logic
// ---------------------------------------------------------------------------

fn print_logic() {
    println!(
        "\
COMPARISON AND LOGICAL OPERATORS

Comparison  (return 1.0 = true, 0.0 = false)
    a == b           equal
    a ~= b  (a != b) not equal        ~= and != are equivalent
    a <  b           less than
    a >  b           greater than
    a <= b           less than or equal
    a >= b           greater than or equal

Logical
    ~expr   (!expr)  NOT: 1 if expr == 0, else 0
    a && b           AND: short-circuit scalar (scalars only)
    a || b           OR:  short-circuit scalar (scalars only)
    a & b            element-wise AND (works on matrices, always evaluates both)
    a | b            element-wise OR  (works on matrices, always evaluates both)
    xor(a, b)        element-wise XOR
    not(a)           element-wise NOT (alias for ~)

  ! and != are C/shell-style aliases for ~ and ~= (Octave extension).
  Use & and | for matrix logical masks; && and || for scalar conditions.

Precedence (low to high inside an expression)
    ||  →  &&  →  |  →  &  →  comparisons  →  :  →  +/-  →  *//  →  ^  →  unary

Scalar examples
    3 > 2             →  1
    3 == 4            →  0
    5 ~= 5            →  0
    ~0                →  1
    ~1                →  0
    2 > 1 && 3 > 2    →  1
    0 || 1            →  1
    xor(1, 0)         →  1
    not(5)            →  0

Arithmetic + comparison
    1 + 1 == 2        →  1    (arithmetic first, then ==)
    2 * 3 > 5         →  1
    2 > 3 || 1 < 2    →  1

Element-wise on matrices
    v = [1 2 3 4 5]
    v > 3                      →  [0 0 0 1 1]
    v == 3                     →  [0 0 1 0 0]
    v ~= 3                     →  [1 1 0 1 1]

    % & and | work on boolean matrices:
    a = [1 0 1 0];  b = [1 1 0 0];
    a & b                      →  [1 0 0 0]
    a | b                      →  [1 1 1 0]
    xor(a, b)                  →  [0 1 1 0]

Logical mask pattern
    v = [3, -1, 8, 0, 5, -2, 7];
    mask = v > 0 & v < 6       →  [1 0 0 0 1 0 0]

Soft masking — zero out elements that fail a condition
    v .* (v > 3)               →  [0 0 0 4 5]  keep elements > 3 only

See also: help matrices, help syntax
Example:  ccalc examples/logic.calc   ccalc examples/language_polish.calc"
    );
}

// ---------------------------------------------------------------------------
// help vectors
// ---------------------------------------------------------------------------

fn print_vectors() {
    println!(
        "\
VECTOR UTILITIES & SPECIAL CONSTANTS

Special constants
    nan    IEEE 754 Not-a-Number — propagates through all arithmetic
           nan + 5  →  NaN     nan == nan  →  0  (always false)
    inf    Positive infinity.  Use -inf for negative infinity.
    nan(n)          n×n matrix of NaN
    nan(m, n)       m×n matrix of NaN

Predicates  (element-wise — work on scalars and matrices)
    isnan(x)        1.0 if NaN, else 0.0
    isinf(x)        1.0 if ±Inf, else 0.0
    isfinite(x)     1.0 if finite, else 0.0

Reductions
    For vectors (1×N or N×1) these collapse to a scalar.
    For M×N matrices (M>1, N>1) they operate column-wise, returning 1×N.

    sum(v)          sum of elements
    prod(v)         product of elements
    mean(v)         arithmetic mean
    min(v)          minimum  (1-arg form; 2-arg min(a,b) still works)
    max(v)          maximum  (1-arg form)
    any(v)          1 if any element is non-zero, else 0
    all(v)          1 if all elements are non-zero, else 0
    norm(v)         Euclidean (L2) norm
    norm(v, p)      Lp norm  (norm(v, inf) = max of absolute values)

    sum([1 2 3 4])       →  10
    sum([1 2; 3 4])      →  [4  6]     (column sums)
    any([0 1 0])         →  1
    all([1 2 3] > 0)     →  1
    norm([3 4])          →  5

Cumulative operations  (return same shape as input)
    cumsum(v)       cumulative sum
    cumprod(v)      cumulative product

    cumsum([1 2 3 4])    →  [1  3  6  10]
    cumprod([1 2 3 4])   →  [1  2  6  24]

Sorting and searching
    sort(v)             sort ascending  (vectors only)
    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

    sort([3 1 4 1 5])          →  [1  1  3  4  5]
    find([0 3 0 5])            →  [2  4]
    find([1 0 2 0 3], 2)       →  [1  3]
    unique([3 1 4 1 5 9 2 6])  →  [1  2  3  4  5  6  9]

Reshape and flip
    reshape(A, m, n)    reshape to m×n  (column-major element order)
    fliplr(v)           reverse column order  (mirror left↔right)
    flipud(v)           reverse row order    (mirror up↔down)

    reshape(1:6, 2, 3)  →  [1 3 5; 2 4 6]
    fliplr([1 2 3])     →  [3 2 1]
    flipud([1;2;3])     →  [3;2;1]

end in index expressions
    Inside index parentheses, end resolves to the size of that dimension.
    Arithmetic on end is fully supported.

    v(end)              last element
    v(end-1)            second to last
    v(end-2:end)        last three elements
    v(1:2:end)          every other element (first to last)
    A(end, :)           last row
    A(:, end)           last column
    A(1:end-1, 2:end)   submatrix: all rows except last, col 2 to end

See also: help matrices  help functions
Example:  ccalc examples/vector_utils.calc"
    );
}

// ---------------------------------------------------------------------------
// help complex
// ---------------------------------------------------------------------------

fn print_complex() {
    println!(
        "\
COMPLEX NUMBERS

Creating complex numbers
    3 + 4i           →  3 + 4i    (Ni suffix: 4i = 4*i, tokenizer handles this)
    3 + 4*i          →  3 + 4i    (explicit multiply — also works)
    3 + 4*j          →  3 + 4i    (j is also the imaginary unit)
    complex(3, 4)    →  3 + 4i    (construct from real and imaginary parts)
    5i               →  5i         (pure imaginary; 5*i also works)
    2 - 3i           →  2 - 3i

    Ni suffix: any decimal number immediately followed by i or j (no space,
    no further alphanumeric chars) is treated as a complex literal.
    When im is exactly 0, the result collapses to a real scalar.

Arithmetic
    z1 = 3 + 4*i;  z2 = 1 - 2*i
    z1 + z2    →  4 + 2i
    z1 - z2    →  2 + 6i
    z1 * z2    →  11 - 2i     (ac-bd) + (ad+bc)i
    z1 / z2    →  -1 + 2i
    z1 ^ 2     →  -7 + 24i
    2 * z1     →  6 + 8i

Powers
    i^2        →  -1          (exact integer exponentiation)
    i^3        →  -i
    i^4        →   1
    (1+i)^-1   →  0.5 - 0.5i
    i^0.5      →  0.7071... + 0.7071...i   (polar form for non-integers)

Conjugate and plain transpose
    z = 3 + 4i
    z'         →  3 - 4i      (conjugate transpose — flips sign of imaginary part)
    z.'        →  3 + 4i      (plain transpose — no conjugation)
    conj(z)    →  3 - 4i      (same as z')

Polar form
    abs(z)     →  5           modulus  sqrt(re² + im²)
    angle(z)   →  0.9272...   argument atan2(im, re), in radians

Built-in functions
    real(z)          real part                real(3+4i)  →  3
    imag(z)          imaginary part           imag(3+4i)  →  4
    abs(z)           modulus                  abs(3+4i)   →  5
    angle(z)         argument in radians      angle(i)    →  π/2
    conj(z)          complex conjugate        conj(3+4i)  →  3-4i
    complex(re, im)  construct               complex(3,4) →  3+4i
    isreal(z)        1 if im==0, else 0      isreal(5)   →  1

    real(5) = 5  (real of a scalar is itself)
    imag(5) = 0  (imaginary part of a real scalar is 0)

Comparison
    ==  and  ~=  compare both real and imaginary parts
    (3+4i) == (3+4i)   →  1
    (3+4i) == (3-4i)   →  0
    <  >  <=  >=  on complex numbers → error (ordering not defined)

Variables i and j
    i and j are initialized to 0+1i at startup.
    You can reassign them:  i = 5   (shadows the imaginary unit)
    Restart ccalc or use complex(0,1) to restore the imaginary unit.

Limitations
    Complex matrices [1+2i, 3] are not yet supported (returns an error).
    ws/wl do not persist complex variables (same policy as matrices).

Example: ccalc examples/complex_numbers.calc"
    );
}

// ---------------------------------------------------------------------------
// help strings
// ---------------------------------------------------------------------------

fn print_strings() {
    println!(
        "\
STRINGS

Two types — both display as plain text (no surrounding quotes).

Char arrays — single quotes  (MATLAB classic, numeric-compatible)
    'hello'            →  Str(\"hello\")   1×5 char array
    'it''s ok'         →  it's ok         '' inside '' = escaped quote
    length('hello')    →  5
    size('hello')      →  [1  5]
    numel('hello')     →  5

    Arithmetic converts chars to their ASCII codes:
    'a' + 0            →  97
    'abc' + 1          →  [98  99  100]
    'abc' == 'aXc'     →  [1  0  1]      element-wise comparison

String objects — double quotes  (modern style, scalar element)
    \"hello\"            →  StringObj(\"hello\")
    \"it\"\"s ok\"         →  it\"s ok          \"\" inside \"\" = escaped quote
    \"a\\n\" + \"b\"         →  a<newline>b      escape sequences in double-quoted strings
    length(\"hello\")    →  1            scalar element — not a char array
    \"abc\" + \"def\"       →  \"abcdef\"        + concatenates string objects

Escape sequences inside \"...\"  (also work in fprintf/sprintf)
    \\n    newline
    \\t    horizontal tab
    \\\\    literal backslash
    \\\"    literal double-quote

String built-in functions
    num2str(x)          number → char array ('3.1416' for pi)
    num2str(x, N)       number → char array with N decimal digits
    int2str(x)          round to integer, then → char array ('4' for 3.7)
    mat2str(A)          matrix → MATLAB literal string ('[1 2;3 4]')
    str2num(s)          char array → number  (error if not parseable)
    str2double(s)       char array → number  (NaN if not parseable)
    strsplit(s)         split on whitespace → cell array of char arrays
    strsplit(s, delim)  split on delimiter  → cell array of char arrays
    strcat(a, b, ...)   concatenate two or more strings
    strcmp(a, b)        1 if equal (case-sensitive), else 0
    strcmpi(a, b)       1 if equal (case-insensitive), else 0
    lower(s)            convert to lowercase
    upper(s)            convert to uppercase
    strtrim(s)          strip leading and trailing whitespace
    strrep(s, old, new) replace all occurrences of old with new
    sprintf(fmt, ...)   format string using C printf specifiers; returns char array
    ischar(s)           1 if s is a char array, else 0
    isstring(s)         1 if s is a string object, else 0

    strsplit examples
    parts = strsplit('a,b,c', ',')   → {{'a', 'b', 'c'}}  (cell array)
    parts{{1}}                       → 'a'
    words = strsplit('hello world')  → {{'hello', 'world'}}

Type checking
    ischar('hello')     →  1
    isstring(\"hello\")  →  1
    ischar(\"hello\")    →  0    string object is not a char array
    ischar(42)          →  0

Practical — building labeled output
    num2str(4700) + ' Ohm'
    strcat('R = ', num2str(R), ' kOhm')

Comparison
    strcmp('abc', 'abc')    →  1
    strcmpi('ABC', 'abc')   →  1
    \"hello\" == \"hello\"      →  1
    \"hello\" == \"world\"      →  0

Workspace
    ws/wl do not persist string variables (same policy as matrices).
    who shows: name [1×N char]  or  name [string]

Example: ccalc examples/strings.calc"
    );
}

// ---------------------------------------------------------------------------
// help examples
// ---------------------------------------------------------------------------

fn print_examples() {
    println!(
        "\
EXAMPLES

Single expression
    $ ccalc \"2 ^ 32\"                →  4294967296
    $ ccalc \"hypot(3, 4)\"           →  5
    $ ccalc \"0xFF + 0b1010\"         →  265
    $ ccalc \"atan2(1,1) * 180 / pi\" →  45

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

    $ printf \"100\\n/ 4\\n+ 5\" | ccalc
    100
    25
    30

REPL — chained calculations with ans
    [ 0 ]: 2 ^ 10
    [ 1024 ]: / 4
    [ 256 ]: sqrt()
    [ 16 ]:

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

REPL — two-argument functions
    [ 0 ]: hypot(3, 4)              →  5
    [ 0 ]: atan2(1, 1) * 180 / pi  →  45
    [ 0 ]: log(8, 2)                →  3
    [ 0 ]: mod(-1, 3)               →  2
    [ 0 ]: max(hypot(3,4), 6)       →  6

REPL — number bases
    [ 0 ]: 0xFF + 0b1010            →  265
    [ 265 ]: hex
    [ 0x109 ]: + 0b10               →  0x10B
    [ 0x10B ]: base
    2  - 0b100001011
    8  - 0o413
    10 - 267
    16 - 0x10B

REPL — matrices
    [ 0 ]: A = [1 2; 3 4]
    A =
       1   2
       3   4
    [ [2×2] ]: A'
    ans =
       1   3
       2   4
    [ [2×2] ]: det(A)
    [ -2 ]: inv(A)
    ans =
       -2    1
       1.5  -0.5

REPL — ranges and indexing
    [ 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:3; 4:6; 7:9]
    A =
       1   2   3
       4   5   6
       7   8   9
    [ [3×3] ]: A(:,2)
    ans =
       2
       5
       8
    [ [3×1] ]: A(1:2, 2:3)
    ans =
       2   3
       5   6

REPL — bitwise operations (combine with hex/bin literals)
    [ 0 ]: bitand(0xFF, 0x0F)
    [ 15 ]: bitor(0b1010, 0b0101)
    [ 15 ]: bitxor(0xFF, 0x0F)
    [ 240 ]: bitshift(1, 8)
    [ 256 ]: bitshift(256, -4)
    [ 16 ]: bitnot(5, 8)
    [ 250 ]:

REPL — comparison and logical operators
    [ 0 ]: 3 > 2
    [ 1 ]:
    [ 0 ]: 5 ~= 5
    [ 0 ]:
    [ 0 ]: ~0
    [ 1 ]:
    [ 0 ]: 2 > 1 && 3 > 2
    [ 1 ]:
    [ 0 ]: v = [1 2 3 4 5];
    [ 0 ]: v > 3
    ans =
       0   0   0   1   1
    [ [1×5] ]: v .* (v > 3)
    ans =
       0   0   0   4   5

Script files  (see examples/ directory)
    ccalc examples/mortgage.calc
    ccalc examples/resistors.calc
    ccalc examples/matrix_ops.calc
    ccalc examples/sequences.calc
    ccalc examples/logic.calc
    ccalc examples/bitwise.calc
    ccalc examples/vector_utils.calc
    ccalc examples/complex_numbers.calc
    ccalc examples/strings.calc
    ccalc examples/file_io.calc
    ccalc examples/control_flow.calc
    ccalc examples/extended_control_flow/extended_control_flow.calc
    ccalc examples/user_functions.calc
    ccalc examples/cell_arrays.calc
    ccalc examples/structs.calc
    ccalc examples/struct_arrays.calc
    ccalc examples/matrix_ops.calc
    ccalc examples/path_system.calc"
    );
}

// ---------------------------------------------------------------------------
// help files
// ---------------------------------------------------------------------------

fn print_fileio() {
    println!(
        "\
FILE I/O

File handles
    fd = fopen(path, mode)    open file; returns fd (≥3) or -1 on failure
    fclose(fd)                close by fd; returns 0 on success, -1 on failure
    fclose('all')             close all open handles

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

Read / write
    fprintf(fd, fmt, v1, ...)  write formatted output to fd
    fprintf(fmt, v1, ...)      write to stdout  (fd 1)
    line = fgetl(fd)           read one line; newline stripped; returns -1 at EOF
    raw  = fgets(fd)           read one line; newline kept; returns -1 at EOF

  Example — write then read back:
    fd = fopen('log.txt', 'w');
    fprintf(fd, 'x = %.4f\\n', 3.14159);
    fclose(fd);
    fd = fopen('log.txt', 'r');
    line = fgetl(fd);          % 'x = 3.1416'
    fclose(fd);

Delimiter-separated data
    dlmwrite(path, A)          write matrix with ',' separator
    dlmwrite(path, A, delim)   explicit delimiter  (',' or '\\t')
    A = dlmread(path)          read; auto-detect ',' / '\\t' / whitespace
    A = dlmread(path, delim)   explicit delimiter

  Example:
    data = [1, 3.3; 2, 4.7];
    dlmwrite('meas.csv', data);
    loaded = dlmread('meas.csv');

Filesystem queries
    isfile(path)            1 if path is an existing file, else 0
    isfolder(path)          1 if path is an existing directory, else 0
    pwd()                   current working directory as a char array
    exist(name)             1 if variable in workspace, 2 if file on disk
    exist(name, 'var')      check workspace only; 1 or 0
    exist(name, 'file')     check filesystem only; 2 if found, 0 otherwise

Workspace with explicit path
    save                         save to ~/.config/ccalc/workspace.toml
    save('path.mat')             save all variables to named file
    save('path.mat', 'x', 'y')  save only named variables
    load('path.mat')             load from named file

  The path argument may be a variable reference:
    p = 'session.mat';
    save(p);
    load(p);

  Persisted types: Scalar, char array, string object.
  Skipped always:  Matrix, Complex.

See also: help script  help vars"
    );
}

// ---------------------------------------------------------------------------
// help control
// ---------------------------------------------------------------------------

fn print_control() {
    println!(
        "\
CONTROL FLOW

All block constructs use the keyword `end` to close. Blocks may be nested
to any depth. Multi-line blocks work in both REPL and script/pipe mode.

─── if / elseif / else ────────────────────────────────────────────────────────

  if cond
    ...
  elseif cond2
    ...
  else
    ...
  end

  Condition is truthy when:
    Scalar:       non-zero and not NaN
    Matrix:       all elements non-zero and not NaN
    Str/StringObj: non-empty

  Example:
    score = 73;
    if score >= 90
      grade = 'A';
    elseif score >= 70
      grade = 'C';
    else
      grade = 'F';
    end

─── for ───────────────────────────────────────────────────────────────────────

  for var = range_expr
    ...
  end

  Range is evaluated once before the loop. Iteration over matrix columns:
    Row vector  →  each element as a scalar
    M×N matrix  →  each column as an M×1 column vector

  Examples:
    for k = 1:5
      fprintf('%d\\n', k)
    end

    for k = 1:2:9           % step = 2  →  1 3 5 7 9
      fprintf('%d ', k)
    end

─── while ─────────────────────────────────────────────────────────────────────

  while cond
    ...
  end

  Example:
    x = 1.0;
    while abs(x ^ 2 - 2) > 1e-12
      x = (x + 2 / x) / 2;
    end

─── break / continue ──────────────────────────────────────────────────────────

  break      exit the innermost loop immediately
  continue   skip to the next iteration of the innermost loop

  Example:
    for n = 1:20
      if mod(n, 2) == 0
        continue
      end
      if n > 9
        break
      end
      fprintf('%d ', n)      % prints: 1 3 5 7 9
    end

─── Compound assignment operators ─────────────────────────────────────────────

  All forms desugar at parse time to a plain assignment — no new AST nodes.

    x += e     →  x = x + e
    x -= e     →  x = x - e
    x *= e     →  x = x * e
    x /= e     →  x = x / e
    x++        →  x = x + 1   (suffix)
    x--        →  x = x - 1   (suffix)
    ++x        →  x = x + 1   (prefix)
    --x        →  x = x - 1   (prefix)

  RHS is a full expression:
    x *= 2 + 3    →  x = x * (2 + 3)   (not x * 2 + 3)

  Limitation: ++ and -- are statement-level only.
    b = a - b--   is NOT supported (use two statements instead).

─── switch / case / otherwise ─────────────────────────────────────────────────

  switch expr
    case val1
      ...
    case val2
      ...
    otherwise       % optional default branch
      ...
  end

  No fall-through: only the first matching case runs.
  Scalars: exact == comparison.
  Strings: Str and StringObj are interchangeable.
  break/continue inside switch propagate to the nearest enclosing loop.

  Example:
    switch code
      case 200
        msg = 'OK';
      case 404
        msg = 'Not Found';
      otherwise
        msg = 'Unknown';
    end

─── do...until ────────────────────────────────────────────────────────────────

  do
    ...
  until (cond)

  Octave post-test loop: body always executes at least once, then cond is
  checked. Parentheses around cond are optional.
  break and continue work as in while.
  until closes the block (no separate end needed).

  Example:
    x = 1;
    do
      x *= 2;
    until (x > 100)
    % x == 128

─── run() / source() ──────────────────────────────────────────────────────────

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

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

  Extension resolution for bare names (no extension):
    1. <name>.calc   native ccalc format (checked first)
    2. <name>.m      Octave/MATLAB compatibility

  With explicit extension: used verbatim.
  Maximum nesting depth: 64 levels (catches accidental recursion).

  Example:
    a = 252; b = 105;
    run('euclid_helper')       % defines g = gcd(a, b) in workspace
    fprintf('gcd = %d\\n', g)   % 21

─── Search path (addpath / rmpath / path / genpath) ───────────────────────────

  addpath('/dir')             prepend directory to the session search path
  addpath('/dir', '-end')     append directory to the session search path
  rmpath('/dir')              remove directory from the session search path
  path()                      display all search path entries
  genpath('/dir')             return '/dir' and all subdirectories as a
                              path-separator-delimited string (';' on Windows,
                              ':' on Unix); combine with addpath to add the
                              whole tree at once

  Search order for run() and script lookup:
    1. Current working directory
    2. Session path entries in order (first entry wins)

  Duplicate entries are silently deduplicated (last addpath wins position).
  path changes are session-only; they are NOT written back to config.toml.
  ~ is expanded to the user's home directory on all platforms.

  To make paths persistent, add them to ~/.config/ccalc/config.toml:
    path = [\"~/.config/ccalc/lib\", \"/home/user/scripts\"]

  A trailing slash on a config entry enables genpath semantics — the directory
  and all its subdirectories are added at startup:
    path = [\"~/.config/ccalc/lib/\", \"/home/user/scripts\"]

  Example:
    addpath('/my/scripts')
    addpath(genpath('/my/libs'))   % add /my/libs and every subdir
    addpath('/my/utils', '-end')
    path()                         % list the current path
    rmpath('/my/utils')            % remove an entry

─── REPL multi-line input ─────────────────────────────────────────────────────

  The REPL detects unclosed blocks by tracking depth changes:
    Keywords that open a block (+1): if  for  while  switch  do  try
    Keywords that close a block (-1): end  until
  Lines accumulate with a continuation prompt until the block is complete.
  Press Ctrl+C to cancel an in-progress block.

See also: help syntax  help logic  help userfuncs  help path  help errors
Examples: ccalc examples/control_flow.calc
          ccalc examples/extended_control_flow.calc
          ccalc examples/error_handling.calc
          ccalc examples/matrix_ops.calc   (backslash linear solve)
          ccalc examples/path_system.calc  (addpath/rmpath/path demo)"
    );
}

// ---------------------------------------------------------------------------
// help userfuncs
// ---------------------------------------------------------------------------

fn print_userfuncs() {
    println!(
        "\
USER-DEFINED FUNCTIONS AND LAMBDAS  (help userfuncs)

─── Named functions ───────────────────────────────────────────────────────────

  function result = name(p1, p2)
    ...
    result = expr;
  end

  Defined at the top level or in a script file. Stored in the workspace
  like any variable. Functions persist until cleared.

  Single return value:
    function y = square(x)
      y = x ^ 2;
    end
    square(5)    →  25

  Multiple return values:
    function [mn, mx] = bounds(v)
      mn = min(v);
      mx = max(v);
    end
    [lo, hi] = bounds([3 1 4 1 5])   →  lo = 1, hi = 5

  Discard outputs with ~:
    [~, hi] = bounds([3 1 4 1 5])    →  hi = 5

─── nargin — optional arguments ───────────────────────────────────────────────

  nargin holds the number of arguments actually passed by the caller.
  Use it to implement optional parameters with defaults:

    function y = power_fn(base, exp)
      if nargin < 2
        exp = 2;
      end
      y = base ^ exp;
    end
    power_fn(5)     →  25   (uses default exp = 2)
    power_fn(2, 8)  →  256

─── return — early exit ───────────────────────────────────────────────────────

  return immediately exits the current function. Output variables must
  be assigned before return is reached:

    function g = gcd_fn(a, b)
      while b ~= 0
        r = mod(a, b);
        a = b;
        b = r;
      end
      g = a;
    end

─── Scope ─────────────────────────────────────────────────────────────────────

  Each call creates a fresh local scope. The caller's data variables
  (scalars, matrices, strings) are NOT visible inside the function.
  Parameters are bound to the local scope.

  However, all Function and Lambda values from the caller's workspace
  are forwarded, enabling:
    - self-recursion: a function can call itself by name
    - mutual recursion: two functions can call each other

─── Function files and autoload ───────────────────────────────────────────────

  A file starting with 'function' is a function file:
    - Only the PRIMARY function is added to the workspace on source().
    - Helper functions after the primary are LOCAL — invisible outside the
      file but available to the primary (MATLAB-style local scoping).

  AUTOLOAD: calling an unknown function name triggers an automatic search
  for <name>.calc / <name>.m on the current directory and session path.
  No explicit source() needed:

    [c, k] = bisect(@(x) x^2-2, 1, 2, 1e-8)   % bisect.calc auto-loaded

  source() still works for explicit loading.

─── Anonymous functions (lambdas) ─────────────────────────────────────────────

  Syntax:  @(param1, param2, ...) expr

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

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

  Zero-argument lambda:
    const_pi = @() pi;
    const_pi()   →  3.14159...

  Lambdas are stored in variables and passed like any value.

─── Lexical capture ───────────────────────────────────────────────────────────

  A lambda captures the enclosing environment at DEFINITION time.
  Changing a captured variable later has no effect:

    rate = 0.05;
    interest = @(p, n) p * (1 + rate) ^ n;
    rate = 0.99;              % too late — the lambda captured 0.05
    interest(1000, 10)   →  1628.89

─── Lambdas as arguments ──────────────────────────────────────────────────────

  Pass a lambda to a function using @:

    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 returning functions ─────────────────────────────────────────────

  A named function can return a lambda (higher-order programming):

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

    add5  = make_adder(5);
    add10 = make_adder(10);
    add5(3)         →   8
    add10(7)        →  17
    add5(add10(1))  →  16

─── varargin — variadic input ─────────────────────────────────────────────────

  When the last parameter is named varargin, all extra call arguments are
  collected into a cell array bound to that name:

    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)     →  60
    sum_all()               →  0   (empty varargin cell)

  Fixed and variadic parameters may be mixed:

    function show(label, varargin)
      fprintf('[%s]', label)
      for k = 1:numel(varargin)
        fprintf(' %g', varargin{{k}})
      end
      fprintf('\\n')
    end

─── varargout — variadic output ───────────────────────────────────────────────

  When the sole output variable is varargout, fill it as a cell array and
  the caller receives one output per cell element:

    function varargout = first_n(v, n)
      for k = 1:n
        varargout{{k}} = v(k);
      end
    end

    [a, b, c] = first_n([10 20 30 40], 3)   →  a=10  b=20  c=30

─── global and persistent — cross-call state ──────────────────────────────────

  global x     Declares x as shared storage accessible from any function that
               also declares  global x.

  persistent x  Declares x as a per-function variable that retains its value
               between calls.  On the first call x is []; use isempty(x) to
               initialize it.

  See  help scoping  for full documentation with examples.

See also: help control  help functions  help cells  help scoping
Example:  ccalc examples/user_functions.calc
          ccalc examples/cell_arrays.calc
          ccalc examples/scoping/scoping.calc"
    );
}

// ---------------------------------------------------------------------------
// help cells
// ---------------------------------------------------------------------------

fn print_cells() {
    println!(
        "\
CELL ARRAYS  (help cells)

A cell array is a heterogeneous 1-D container: each element can be any value
(scalar, matrix, string, complex, another cell, or a function handle).

─── Creating cell arrays ──────────────────────────────────────────────────────

  {{e1, e2, e3}}            cell literal — one expression per element
  cell(n)                   1×n cell pre-filled with zeros
  cell(m, n)                1×(m*n) cell pre-filled with zeros

  c = {{1, 'hello', [1 2 3]}}
  c{{1}}                    →  1         (scalar)
  c{{2}}                    →  hello     (char array)
  c{{3}}                    →  [1 2 3]   (matrix)

─── Brace indexing ────────────────────────────────────────────────────────────

  c{{i}}                    access element i (1-based); returns its VALUE
  c{{i}} = v               assign to element i; auto-grows if i > numel(c)

  iscell(c)                 1 if c is a cell array, else 0
  numel(c)                  number of elements
  length(c)                 number of elements (same as numel for 1-D)
  size(c)                   [1  numel(c)] as a 1×2 matrix

  Note: c(i) with round parentheses returns an error — use c{{i}} for content.

─── varargin / varargout ──────────────────────────────────────────────────────

  varargin   — last parameter that collects all extra call arguments into a cell:
    function s = sum_all(varargin)
      s = 0;
      for k = 1:numel(varargin)
        s += varargin{{k}};
      end
    end
    sum_all(1, 2, 3)    →  6

  varargout  — sole output that is expanded into multiple return values:
    function varargout = swap(a, b)
      varargout{{1}} = b;
      varargout{{2}} = a;
    end
    [x, y] = swap(10, 20)   →  x=20  y=10

─── case with cell array ──────────────────────────────────────────────────────

  Inside a switch block, case {{v1, v2}} matches if the switch expression
  equals any element of the cell array:

    switch x
      case {{1, 2, 3}}
        disp('small')
      case {{4, 5, 6}}
        disp('medium')
      otherwise
        disp('large')
    end

─── cellfun ───────────────────────────────────────────────────────────────────

  cellfun(f, c)    apply f to each element of cell c
  Returns Value::Matrix when all results are scalar; Value::Cell otherwise.

    c = {{1, 4, 9}};
    cellfun(@sqrt, c)          →  [1  2  3]
    cellfun(@(x) x*2, c)       →  [2  8  18]

─── arrayfun ──────────────────────────────────────────────────────────────────

  arrayfun(f, v)   apply f to each element of numeric vector v
  Returns a same-shape matrix (function must return a scalar per element).

    arrayfun(@(x) x^2, [1 2 3])       →  [1  4  9]
    arrayfun(@(x) x > 2, [1 2 3 4])   →  [0  0  1  1]

─── @funcname — function handles ──────────────────────────────────────────────

  @funcname creates a lambda that forwards its arguments to funcname.
  Works with builtins and user-defined functions.

    f = @sqrt;       f(16)       →  4
    g = @abs;        g(-7.5)     →  7.5
    h = @clamp01;    h(-0.5)     →  0   (user function)

  Compose handles via a lambda that calls them sequentially:
    compose = @(f, g) @(x) f(g(x));
    sqrt_abs = compose(@sqrt, @abs);
    sqrt_abs(-9)    →  3

─── Workspace ─────────────────────────────────────────────────────────────────

  Cell arrays are NOT persisted by ws/save — same policy as matrices.
  who shows: c = {{1×N cell}}

See also: help userfuncs  help functions  help control  help structs
Example:  ccalc examples/cell_arrays.calc"
    );
}

// ---------------------------------------------------------------------------
// help structs
// ---------------------------------------------------------------------------

fn print_structs() {
    println!(
        "\
STRUCTS AND STRUCT ARRAYS  (help structs)

A scalar struct groups named fields, each holding any value (scalar, matrix,
string, complex, cell, or another struct).  Fields are ordered by insertion.

─── Scalar struct ─────────────────────────────────────────────────────────────

  s.x = 1               field assignment; creates struct if s doesn't exist yet
  s.y = [1 2 3]         field can hold any Value
  s.a.b = 42            nested field — creates s.a as an empty struct if needed

  s = struct()                  empty struct
  s = struct('x', 1, 'y', 2)   constructor; pairs: string key + value

  s.x                   read field value
  s.a.b                 chained: read nested field (any depth)

─── Built-in utilities ────────────────────────────────────────────────────────

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

─── Struct arrays ─────────────────────────────────────────────────────────────

  s(i).field = val      indexed assignment; creates/grows struct array
  s(i).field            read field from element i  (1-based)
  s.field               collect field across ALL elements:
                            all scalars → 1×N matrix
                            mixed types → 1×N cell 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)
  ys = pts.y     →  [0 4 5]

  String fields collect into a cell array:
  roster(1).name = 'Alice';  roster(2).name = 'Bob';
  names = roster.name        →  {{'Alice', 'Bob'}}

─── Display ───────────────────────────────────────────────────────────────────

  Scalar struct:
    s =
      struct with fields:
        x: 1
        y: [1×3 double]

  Struct array (N > 1):
    pts =
      1×3 struct array with fields:
        x
        y

─── Workspace ─────────────────────────────────────────────────────────────────

  Structs are NOT persisted by ws/save — same policy as matrices and cells.
  who shows: s = [1×1 struct]  or  pts = [1×3 struct]

See also: help cells  help userfuncs  help control
Examples: ccalc examples/structs.calc
          ccalc examples/struct_arrays.calc"
    );
}

// ---------------------------------------------------------------------------
// help errors
// ---------------------------------------------------------------------------

fn print_errors() {
    println!(
        "\
ERROR HANDLING  (help errors)

─── error() and warning() ─────────────────────────────────────────────────────

  error(msg)               raise a runtime error; stops execution in current
                           block (caught by try/catch or propagates to REPL)
  error(fmt, v1, v2, ...)  printf-formatted message (same specifiers as fprintf)
  warning(msg)             print warning to stderr; execution continues
  warning(fmt, v1, ...)    printf-formatted warning

  Examples:
    error('value must be positive')
    error('expected %d arguments, got %d', 2, nargin)
    warning('result may be inaccurate: condition number = %.1e', cond(A))

─── lasterr ───────────────────────────────────────────────────────────────────

  lasterr()       return the message from the most recent runtime error
  lasterr(msg)    set the last-error string; returns the previous value
  lasterr('')     clear the last-error string (returns previous)

  lasterr is set automatically whenever the REPL or a try/catch block
  catches a runtime error.

  Examples:
    inv([1 0; 0 0]);          % triggers an error
    msg = lasterr()           % 'singular matrix'
    lasterr('');              % clear it

─── try / catch / end ─────────────────────────────────────────────────────────

  MATLAB-compatible protected block.  Two forms:

  Anonymous catch — no error variable:
    try
      risky_code()
    catch
      fallback_code()
    end

  Named catch — e is bound to a struct with field 'message':
    try
      result = risky_function(data)
    catch e
      fprintf('caught: %s\\n', e.message)
      result = default_value
    end

  try with no catch — silently swallows the error:
    try
      might_fail()
    end

  Behaviour:
    If the try body completes without error, the catch body is skipped.
    If any statement in the try body raises an error, execution jumps
    immediately to the catch body (remaining try statements are skipped).
    lasterr is set on entry to the catch body.
    break/continue/return inside a try or catch work as normal.

  Example:
    for k = 1:10
      try
        results(k) = compute(data(k))
      catch e
        fprintf('step %d failed: %s\\n', k, e.message)
        results(k) = 0
      end
    end

─── try(expr, default) — inline fallback ──────────────────────────────────────

  x = try(expr, default)

  Evaluates expr; returns its value on success. If expr raises an error,
  evaluates and returns default instead (lazy — default is only evaluated
  on failure).

  Examples:
    x = try(inv(A), eye(n))          % fallback to identity if singular
    n = try(str2num(s), 0)           % fallback to 0 if not a number
    v = try(risky(data), NaN)        % NaN sentinel on error

  Note: try(expr, default) is a special form, not a regular function call.
  The default expression is NOT evaluated unless expr fails.

─── pcall — protected call ────────────────────────────────────────────────────

  [ok, val] = pcall(@func, arg1, arg2, ...)

  Calls @func with the given arguments in a protected context.
  Returns a two-element tuple:
    ok = 1   val = function return value   (on success)
    ok = 0   val = error message string    (on failure)

  Compatible with anonymous functions and named function handles.
  lasterr is set to the error message on failure.

  Examples:
    [ok, x] = pcall(@inv, A)
    if ~ok
      fprintf('inv failed: %s\\n', x)
      x = eye(n)
    end

    [ok, y] = pcall(@(x) sqrt(x), -1)   % ok=0, y='sqrt of negative'

    for k = 1:numel(data)
      [ok, v] = pcall(@process, data(k))
      results(k) = ok * v             % 0 on failure
    end

─── 'e' as a catch variable ───────────────────────────────────────────────────

  The constant 'e' (Euler's number, 2.718...) and the catch variable 'e'
  do not conflict. Variable assignments always shadow built-in constants:

    try
      error('oops')
    catch e
      fprintf('message: %s\\n', e.message)   % e is a struct here
    end
    e                                         % back to 2.718... after block

See also: help control  help userfuncs  help structs
Example:  ccalc examples/error_handling.calc"
    );
}

// ---------------------------------------------------------------------------
// help scoping
// ---------------------------------------------------------------------------

fn print_scoping() {
    println!(
        "\
VARIABLE SCOPING  (help scoping)

Four mechanisms control visibility and lifetime of variables across functions.

─── global — shared workspace storage ─────────────────────────────────────────

  Declare the SAME name in every function that needs to share the value.
  Changes in one function are immediately visible in all others.

    function reset_counter()
      global g_count
      g_count = 0;
    end

    function increment(step)
      global g_count
      g_count = g_count + step;
    end

    reset_counter()
    increment(3)
    increment(7)
    % g_count is now 10 in the base workspace and in any function that
    % also declares  global g_count

  Use case: configuration, counters, shared state across a call graph.
  Anti-pattern: overusing globals creates hidden coupling; prefer passing
  values as arguments when the call chain is shallow.

─── persistent — per-function long-lived storage ──────────────────────────────

  A persistent variable keeps its value between calls to the SAME function.
  On the first call the variable is [], so isempty() is the standard guard.

    function n = how_many_calls()
      persistent call_count
      if isempty(call_count)
        call_count = 0;
      end
      call_count += 1;
      n = call_count;
    end

    how_many_calls()   % 1
    how_many_calls()   % 2
    how_many_calls()   % 3

  Use cases: call counters, memoization caches, lazy initialization.

  Memoized Fibonacci (persistent write-through ensures recursive calls see
  each other's updates immediately):

    function f = fib_memo(n)
      persistent cache
      if isempty(cache)
        cache = zeros(1, 100);
        cache(1) = 1;  cache(2) = 1;
      end
      if cache(n) ~= 0; f = cache(n); return; end
      cache(n) = fib_memo(n-1) + fib_memo(n-2);
      f = cache(n);
    end

─── private/ — directory-scoped helpers ───────────────────────────────────────

  Functions in a private/ sub-directory are visible ONLY to scripts and
  functions in the PARENT directory.  Any other caller sees 'Unknown function'.

  Directory layout:
    mylib/
      main.calc        <- can call clamp() and lerp()
      private/
        clamp.calc     <- invisible outside mylib/
        lerp.calc      <- invisible outside mylib/

  This is the file-system equivalent of making helpers package-private.
  private/ directories are skipped when ccalc builds the autoload path —
  even if mylib/ is on the session path, its private/ folder stays hidden.

    function y = normalize(data, lo, hi)
      % clamp() and lerp() come from private/ — callers cannot use them directly
      span = hi - lo;
      for k = 1:numel(data)
        y(k) = lerp(0, 1, (clamp(data(k), lo, hi) - lo) / span);
      end
    end

─── Packages (+pkg/) — named namespaces ───────────────────────────────────────

  A directory whose name starts with '+' is a PACKAGE.  Functions inside are
  invisible at the top level; call them with the package prefix:

    pkg.function(args)

  Example layout:
    +utils/
      clamp.calc          <- utils.clamp(x, lo, hi)
      lerp.calc           <- utils.lerp(a, b, t)
    +geom/
      circle_area.calc    <- geom.circle_area(r)

  Usage:
    utils.clamp(-3, 0, 10)         % 0
    utils.lerp(0, 100, 0.25)       % 25
    geom.circle_area(1)            % 3.14159...

  Nested packages map subdirectories:
    +geom/+solid/sphere_vol.calc   <- geom.solid.sphere_vol(r)

  Package functions are autoloaded on first call from SCRIPT_DIR_STACK → CWD
  → SESSION_PATH. No explicit source() required.

  Package directories are transparent to addpath and genpath — the search
  path does not include +pkg/ dirs directly; they are only found via the
  qualified call syntax.

─── Interaction summary ───────────────────────────────────────────────────────

  global     — cross-function shared state; requires declaration in each function
  persistent — per-function state; survives between calls; one slot per function
  private/   — file-system visibility guard; MATLAB-compatible
  +pkg/      — named namespace; avoids function-name collisions across libraries

See also: help userfuncs  help control  help path
Example:  ccalc examples/scoping/scoping.calc"
    );
}
