Skip to content

Commit 6a5cc18

Browse files
committed
documents update
1 parent 026bad8 commit 6a5cc18

36 files changed

Lines changed: 2263 additions & 1877 deletions

docs/command_line_interface.md

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# Command Line Interface
2+
3+
Exprify ships with a built-in command-line interface (CLI) that lets you evaluate mathematical and logical expressions directly from your terminal - no need to open a script file or write boilerplate code. It supports one-off evaluations, piped input from other commands, and a full interactive REPL (Read-Eval-Print Loop) for exploratory work.
4+
5+
This makes Exprify useful not just as a library, but as a quick everyday calculator, a scripting utility for shell pipelines, and a sandbox for testing how expressions are parsed and evaluated.
6+
7+
## Installation Check
8+
9+
Before using the CLI, make sure Exprify is installed and accessible on your `PATH`. You can confirm this by checking the version:
10+
11+
```bash
12+
exprify --version
13+
```
14+
15+
If the command is not found, ensure your package manager's global bin directory is included in your shell's `PATH`.
16+
17+
## Usage
18+
19+
Exprify can be invoked in three primary ways, depending on your workflow.
20+
21+
### 1. Direct Expression Evaluation
22+
23+
Pass an expression as a quoted argument and Exprify will evaluate it immediately and print the result:
24+
25+
```bash
26+
exprify "2 + 2"
27+
# => 4
28+
```
29+
30+
This works well for quick calculations, embedding inside shell scripts, or chaining with other commands.
31+
32+
```bash
33+
exprify "sqrt(16) + 2^3"
34+
# => 12
35+
```
36+
37+
### 2. Piped Input
38+
39+
Exprify can read an expression from standard input (stdin), which is useful when the expression is generated dynamically by another program:
40+
41+
```bash
42+
echo "2+2" | exprify
43+
# => 4
44+
```
45+
46+
```bash
47+
cat expression.txt | exprify
48+
```
49+
50+
### 3. Interactive REPL
51+
52+
Running `exprify` with no arguments and no piped input launches an interactive REPL session. This is ideal for exploring expressions, testing functions, and working with variables across multiple evaluations:
53+
54+
```bash
55+
exprify
56+
```
57+
58+
```
59+
exprify> 2 + 2
60+
4
61+
exprify> x = 10
62+
10
63+
exprify> x * 5
64+
50
65+
```
66+
67+
## Options
68+
69+
The following flags can be passed when invoking `exprify` directly:
70+
71+
| Flag | Description |
72+
|---|---|
73+
| `--help` / `-h` | Display the help message, including usage examples and a summary of available flags. |
74+
| `--version` / `-v` | Print the currently installed version of Exprify. |
75+
| `--parse <expr>` | Parse the given expression without evaluating it, and display both the token stream and the resulting Abstract Syntax Tree (AST). Useful for debugging custom grammars or understanding operator precedence. |
76+
| `--tokens <expr>` | Tokenize the given expression and display only the resulting list of tokens, without building an AST or evaluating. |
77+
78+
### Example: Inspecting Parsing Behavior
79+
80+
```bash
81+
exprify --tokens "2 + 3 * 4"
82+
```
83+
84+
```
85+
[NUMBER(2), PLUS, NUMBER(3), STAR, NUMBER(4)]
86+
```
87+
88+
```bash
89+
exprify --parse "2 + 3 * 4"
90+
```
91+
92+
```
93+
Tokens:
94+
[NUMBER(2), PLUS, NUMBER(3), STAR, NUMBER(4)]
95+
96+
AST:
97+
(+ 2 (* 3 4))
98+
```
99+
100+
## REPL Commands
101+
102+
Once inside the REPL, you can type any valid expression to evaluate it, or use one of the following special commands:
103+
104+
| Command | Description |
105+
|---|---|
106+
| `.help` | Show a list of available REPL commands and shortcuts. |
107+
| `.exit`, `exit`, `quit` | Exit the REPL session and return to your shell. |
108+
| `<expr>` | Evaluate any valid expression and print the result. |
109+
| `Ctrl+C` | Cancel the current input line, or exit the REPL if pressed on an empty line. |
110+
111+
### REPL Features
112+
113+
The REPL is designed to make interactive exploration comfortable and efficient:
114+
115+
- **Tab completion** - Press `Tab` while typing to auto-complete the names of common functions (e.g., `sqrt`, `sin`, `cos`) and constants (e.g., `pi`, `e`).
116+
- **Colored output** - Results, errors, and warnings are syntax-highlighted for readability, making it easy to distinguish values from error messages at a glance.
117+
- **Persistent evaluation context** - Variables and assignments persist across expressions within the same session, allowing you to build up calculations step by step.
118+
119+
```
120+
exprify> a = 5
121+
5
122+
exprify> b = a * 2
123+
10
124+
exprify> a + b
125+
15
126+
exprify> exit
127+
```
128+
129+
## Tips
130+
131+
- Wrap expressions containing spaces or special shell characters (like `*`, `(`, `)`, or `|`) in quotes to prevent your shell from interpreting them.
132+
- Use `--tokens` and `--parse` as debugging tools when an expression doesn't evaluate the way you expect.
133+
- The REPL's persistent context makes it well suited for multi-step calculations - define variables once and reuse them throughout your session.

docs/core/chaining.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Chaining
2+
3+
The `.chain()` method returns a fluent `Chain` object designed for sequential evaluations where each step builds on the result of the previous one. This is especially useful for multi-step calculations, pipelines, or interactive REPL-style sessions where you don't want to manually track intermediate results.
4+
5+
Each step automatically stores its result in the special variable `ans`, which becomes available to the next expression in the chain. This mirrors the behavior of calculator "ANS" buttons, letting you write expressions like `ans * 10` without explicitly naming the previous result.
6+
7+
## Basic Usage
8+
9+
```js
10+
const c = expr.chain();
11+
c.evaluate('2 + 2'); // ans = 4
12+
c.evaluate('ans * 10'); // ans = 40
13+
c.evaluate('ans / 2'); // ans = 20
14+
c.done(); // 20
15+
```
16+
17+
In this example, each call to `evaluate()` updates `ans` internally. The chain doesn't return the result of each step directly - instead, results accumulate silently until `done()` is called, which retrieves and formats the final value.
18+
19+
## Method Reference
20+
21+
All methods on the `Chain` object return the `Chain` instance itself (with the exception of `done()`), which enables fluent, method-chained syntax. This means you can compose an entire sequence of operations in a single expression without intermediate variables.
22+
23+
```js
24+
const result = expr
25+
.chain()
26+
.setVariable('x', 5)
27+
.evaluate('x + 2')
28+
.done(); // 7
29+
```
30+
31+
| Method | Returns | Description |
32+
|---|---|---|
33+
| `evaluate(expr, scope?)` | `Chain` | Evaluates the given expression and stores the result as `ans`. An optional `scope` object can be passed to provide additional variables for this evaluation only. |
34+
| `setVariable(name, value)` | `Chain` | Sets a named variable that persists across subsequent evaluations in the chain. Useful for defining constants or intermediate values to reference later. |
35+
| `compile(expr)` | `Function` | Compiles an expression into a reusable function, delegating to the parent instance. The compiled function is not itself part of the chain's state. |
36+
| `done()` | `any` | Terminates the chain and returns the final formatted result (i.e., the current value of `ans`). After calling `done()`, the chain should be considered complete. |
37+
38+
## Using Scope Overrides
39+
40+
The optional `scope` parameter on `evaluate()` allows you to inject variables for a single step without permanently adding them to the chain's persistent state (unlike `setVariable()`).
41+
42+
```js
43+
const c = expr.chain();
44+
c.evaluate('a + b', { a: 3, b: 4 }); // 7
45+
c.evaluate('ans + 1'); // 8
46+
c.done(); // 8
47+
```
48+
49+
Here, `a` and `b` are only available during the first `evaluate()` call. The second call only has access to `ans` (and any variables previously set via `setVariable()`), demonstrating that scope overrides are scoped strictly to the call in which they're provided.
50+
51+
## Notes
52+
53+
- Since every method except `done()` returns the chain instance, you can freely interleave `evaluate()` and `setVariable()` calls in any order before finalizing with `done()`.
54+
- The `ans` variable is automatically managed; manually overwriting it via `setVariable('ans', ...)` is possible but generally discouraged, as it may produce confusing results in later steps.

docs/core/configuration.md

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
# Configuration
2+
3+
Exprify follows a minimal-configuration philosophy: rather than exposing a large global settings object, almost all behavior is controlled per-instance through the API methods documented below. This keeps configuration explicit, predictable, and easy to reason.
4+
5+
## BigNumber Precision
6+
7+
The `ExprDecimal` class wraps an arbitrary-precision decimal library and is used internally whenever `bignumber()`-mode arithmetic is requested. By default, Exprify uses 20 decimal places of precision, which is sufficient for most financial and scientific calculations. You can raise this (up to a hard cap of 100) when you need finer-grained results, such as for cryptographic computations, high-precision scientific simulations, or chained division operations where rounding error could otherwise accumulate.
8+
9+
```js
10+
const { Exprify } = require('exprify');
11+
const { ExprDecimal } = require('exprify/src/utils/decimal');
12+
13+
// Default precision is 20 decimal places
14+
console.log(ExprDecimal.DP); // 20
15+
16+
// Increase precision to 50 decimal places
17+
ExprDecimal.DP = 50;
18+
19+
const expr = new Exprify();
20+
21+
// Division now returns up to 50 significant decimal digits
22+
const result = expr.evaluate('bignumber(1) / bignumber(3)');
23+
console.log(result.toString());
24+
// "0.33333333333333333333333333333333333333333333333333"
25+
26+
// Reset back to a lower precision if needed
27+
ExprDecimal.DP = 20;
28+
```
29+
30+
**Important considerations:**
31+
32+
- `ExprDecimal.DP` is a **static, global** setting - changing it affects *all* `ExprDecimal` instances and all Exprify instances in the running process, not just the one you're configuring it from. If your application uses Exprify in multiple places with different precision needs, set `DP` immediately before the relevant evaluation and consider resetting it afterward.
33+
- Values above 100 are rejected; attempting to set `ExprDecimal.DP = 150` will either clamp to 100 or throw, depending on your version - always validate against the documented maximum.
34+
- Higher precision comes with a performance cost. Arbitrary-precision arithmetic at DP=100 is meaningfully slower than DP=20, so only raise it when correctness genuinely requires it.
35+
- This setting only affects results produced via `bignumber()` - ordinary numeric expressions (e.g. `1 / 3`) continue to use standard double-precision floating point and are unaffected by `ExprDecimal.DP`.
36+
37+
## Evaluation Scope
38+
39+
A *scope* is a plain JavaScript object mapping variable names to values. It lets you supply or override variables for a single call to `evaluate()` or `compile()` without permanently mutating the engine's internal variable table. This is the recommended way to run the same expression repeatedly with different inputs - for example, evaluating a formula once per row of a dataset.
40+
41+
```js
42+
const expr = new Exprify();
43+
44+
// One-off evaluation with scope
45+
console.log(expr.evaluate('x^2 + y', { x: 3, y: 4 })); // 13
46+
47+
// Scope variables do not persist after evaluation
48+
console.log(expr.evaluate('x')); // throws: x is undefined (unless set globally)
49+
50+
// Compiling once, evaluating many times with different scopes
51+
const compiled = expr.compile('a * b + c');
52+
53+
const rows = [
54+
{ a: 2, b: 3, c: 1 },
55+
{ a: 5, b: 5, c: 0 },
56+
{ a: 10, b: 0.5, c: 2 },
57+
];
58+
59+
const results = rows.map(row => compiled.evaluate(row));
60+
console.log(results); // [7, 25, 7]
61+
```
62+
63+
**Scope precedence:** if a variable exists both in the scope object *and* as a globally-set variable on the engine instance (via `expr.setVariable()` or similar), the scope value takes precedence for that evaluation only. This makes scope ideal for "default value with override" patterns:
64+
65+
```js
66+
expr.setVariable('taxRate', 0.07); // global default
67+
68+
expr.evaluate('price * (1 + taxRate)', { price: 100 }); // 107 (uses global taxRate)
69+
expr.evaluate('price * (1 + taxRate)', { price: 100, taxRate: 0 }); // 100 (override)
70+
```
71+
72+
## Custom Functions
73+
74+
`addFunction()` registers a JavaScript function under a given name so it can be called from within expressions, just like a built-in function such as `sin()` or `sqrt()`. This is the primary extension point for domain-specific logic - unit conversions, lookups, string formatting, business rules, and so on.
75+
76+
```js
77+
const expr = new Exprify();
78+
79+
// Simple single-argument function
80+
expr.addFunction('double', (x) => x * 2);
81+
console.log(expr.evaluate('double(21)')); // 42
82+
83+
// Multi-argument function
84+
expr.addFunction('clamp', (value, min, max) => Math.max(min, Math.min(max, value)));
85+
console.log(expr.evaluate('clamp(150, 0, 100)')); // 100
86+
87+
// Functions can call other built-ins or do arbitrary computation
88+
expr.addFunction('hypotenuse', (a, b) => Math.sqrt(a * a + b * b));
89+
console.log(expr.evaluate('hypotenuse(3, 4)')); // 5
90+
91+
// Functions can be used inside larger expressions, including nested calls
92+
console.log(expr.evaluate('double(clamp(75, 0, 50))')); // 100
93+
94+
// Variadic-style functions using rest parameters
95+
expr.addFunction('sumAll', (...args) => args.reduce((acc, v) => acc + v, 0));
96+
console.log(expr.evaluate('sumAll(1, 2, 3, 4, 5)')); // 15
97+
```
98+
99+
**Notes on custom functions:**
100+
101+
- Function names follow the same identifier rules as variables: they should start with a letter or underscore and contain only letters, digits, and underscores.
102+
- Registering a function with a name that already exists (including built-in function names) overwrites the existing definition for that engine instance - use this carefully, as it can change the meaning of expressions that worked before.
103+
- Custom functions are included when you call `exportState()`, provided they're serializable (see below) - functions defined as closures over external state will not survive serialization and must be re-registered manually after `importState()`.
104+
105+
## State Serialization
106+
107+
`exportState()` and `importState()` allow you to capture the full configuration of an engine instance - its variables, registered functions, and any custom units - and restore it later, either in the same process or after persisting it to disk, a database, or a network transfer.
108+
109+
```js
110+
const expr = new Exprify();
111+
112+
expr.setVariable('x', 10);
113+
expr.setVariable('y', 20);
114+
expr.addFunction('double', (x) => x * 2);
115+
expr.addUnit('smoot', { definition: '1.7018 m' });
116+
117+
// Export the full state
118+
const state = expr.exportState();
119+
console.log(state);
120+
/*
121+
{
122+
variables: { x: 10, y: 20 },
123+
functions: { double: '(x) => x * 2' }, // serialized as source where possible
124+
units: { smoot: { definition: '1.7018 m' } }
125+
}
126+
*/
127+
128+
// Persist to disk (example)
129+
const fs = require('fs');
130+
fs.writeFileSync('exprify-state.json', JSON.stringify(state));
131+
132+
// ... later, in a new process ...
133+
const restoredState = JSON.parse(fs.readFileSync('exprify-state.json', 'utf8'));
134+
135+
const expr2 = new Exprify();
136+
expr2.importState(restoredState);
137+
138+
console.log(expr2.evaluate('double(x) + y')); // 40
139+
console.log(expr2.evaluate('5 smoot to m')); // ~8.509 m
140+
```
141+
142+
**Caveats:**
143+
144+
- Functions registered via closures over non-serializable values (e.g. database connections, file handles) will not round-trip correctly through `exportState()`/`importState()`. For these, export only the simple state and re-register such functions manually after import.
145+
- `importState()` merges into the target instance by default in most implementations - if you need a completely clean slate, create a fresh `Exprify` instance before importing.
146+
- State serialization does not include `ExprDecimal.DP`, since that setting is global rather than per-instance; you must set it separately on the receiving side.
147+
148+
## Constants
149+
150+
Exprify ships with a set of built-in mathematical constants, available by name in any expression without prior declaration:
151+
152+
| Name | Value | Description |
153+
|---|---|---|
154+
| `pi` | 3.141592653589793 | Ratio of a circle's circumference to its diameter |
155+
| `e` | 2.718281828459045 | Euler's number, base of the natural logarithm |
156+
| `PHI` | 1.618033988749895 | The golden ratio, (1 + √5) / 2 |
157+
| `TAU` | 6.283185307179586 | The full circle constant, 2π |
158+
| `INFINITY` | Infinity | Positive infinity |
159+
| `NaN` | NaN | "Not a Number" - result of undefined numeric operations |
160+
161+
```js
162+
const expr = new Exprify();
163+
164+
console.log(expr.evaluate('2 * pi * 5')); // ~31.4159265358979 (circumference, r=5)
165+
console.log(expr.evaluate('e^1')); // 2.718281828459045
166+
console.log(expr.evaluate('PHI - 1')); // 0.618033988749895 (== 1/PHI)
167+
console.log(expr.evaluate('TAU / 2 == pi')); // true
168+
console.log(expr.evaluate('1 / 0 == INFINITY')); // true
169+
console.log(expr.evaluate('isNaN(0 / 0)')); // true
170+
```
171+
172+
**Overriding constants:** because constants are just pre-populated entries in the variable table, they can be shadowed within a given scope or instance if your application has a specific need (for example, using a higher-precision value of `pi`). However, this is discouraged for general use, since it can make expressions confusing to read and debug - prefer introducing a new, clearly-named variable instead (e.g. `piHighPrecision`) rather than redefining a well-known constant.

0 commit comments

Comments
 (0)