@jarenjs/calc 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -0
- package/dist/types/ast.d.ts +65 -0
- package/dist/types/compile.d.ts +52 -0
- package/dist/types/component/index.d.ts +259 -0
- package/dist/types/component/rates/binance.d.ts +34 -0
- package/dist/types/component/rates/coingecko.d.ts +47 -0
- package/dist/types/component/rates/index.d.ts +84 -0
- package/dist/types/component/rules.d.ts +180 -0
- package/dist/types/component/schema.d.ts +38 -0
- package/dist/types/env.d.ts +24 -0
- package/dist/types/errors.d.ts +18 -0
- package/dist/types/index.d.ts +42 -0
- package/dist/types/modes/converter.d.ts +46 -0
- package/dist/types/modes/financial.d.ts +59 -0
- package/dist/types/modes/index.d.ts +38 -0
- package/dist/types/modes/programmer.d.ts +50 -0
- package/dist/types/modes/scientific.d.ts +28 -0
- package/dist/types/modes/standard.d.ts +32 -0
- package/dist/types/parser/index.d.ts +32 -0
- package/dist/types/plot/plot2d.d.ts +70 -0
- package/dist/types/plot/plot3d.d.ts +68 -0
- package/dist/types/render/error.d.ts +19 -0
- package/dist/types/theme.d.ts +35 -0
- package/dist/types/to-expr.d.ts +13 -0
- package/dist/types/utils.d.ts +9 -0
- package/docs/CALC-FORMAT.md +79 -0
- package/package.json +71 -0
- package/schemas/financial-inputs.schema.json +15 -0
- package/schemas/jaren-calc-ast.schema.json +91 -0
- package/schemas/jaren-calc-state.schema.json +52 -0
- package/src/ast.js +96 -0
- package/src/compile.js +119 -0
- package/src/component/index.js +352 -0
- package/src/component/rates/binance.js +44 -0
- package/src/component/rates/coingecko.js +63 -0
- package/src/component/rates/index.js +116 -0
- package/src/component/rules.js +118 -0
- package/src/component/schema.js +24 -0
- package/src/env.js +163 -0
- package/src/errors.js +23 -0
- package/src/index.js +85 -0
- package/src/modes/converter.js +73 -0
- package/src/modes/financial.js +89 -0
- package/src/modes/index.js +24 -0
- package/src/modes/programmer.js +69 -0
- package/src/modes/scientific.js +31 -0
- package/src/modes/standard.js +39 -0
- package/src/parser/index.js +220 -0
- package/src/plot/plot2d.js +221 -0
- package/src/plot/plot3d.js +177 -0
- package/src/render/error.js +25 -0
- package/src/theme.js +76 -0
- package/src/to-expr.js +84 -0
- package/src/utils.js +11 -0
- package/styles/calc.css +128 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The x·y·z surface plotter. Samples
|
|
3
|
+
* `z = f(x, y)` on an N×N grid into `Vec3f64` model points, rotates them
|
|
4
|
+
* (yaw/pitch) and projects them with the `core` `mat4`/`project.js`
|
|
5
|
+
* kernel, builds quad faces, **depth-sorts back-to-front (painter's
|
|
6
|
+
* algorithm)**, shades each quad by height and emits `<polygon>`s with an
|
|
7
|
+
* optional wireframe. Deterministic and headless → golden-geometry tests.
|
|
8
|
+
*/
|
|
9
|
+
export type Plot3dConfig = {
|
|
10
|
+
/**
|
|
11
|
+
* the z = f(x,y) source
|
|
12
|
+
*/
|
|
13
|
+
expr?: string;
|
|
14
|
+
domainX?: [number, number];
|
|
15
|
+
domainY?: [number, number];
|
|
16
|
+
/**
|
|
17
|
+
* samples per axis
|
|
18
|
+
*/
|
|
19
|
+
grid?: number;
|
|
20
|
+
yaw?: number;
|
|
21
|
+
/**
|
|
22
|
+
* rotation, radians
|
|
23
|
+
*/
|
|
24
|
+
pitch?: number;
|
|
25
|
+
width?: number;
|
|
26
|
+
height?: number;
|
|
27
|
+
/**
|
|
28
|
+
* independent variable names
|
|
29
|
+
*/
|
|
30
|
+
variables?: [string, string];
|
|
31
|
+
scope?: any;
|
|
32
|
+
env?: any;
|
|
33
|
+
theme?: any;
|
|
34
|
+
wireframe?: boolean;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* @typedef {object} Plot3dConfig
|
|
38
|
+
* @property {string} [expr] the z = f(x,y) source
|
|
39
|
+
* @property {[number,number]} [domainX] @property {[number,number]} [domainY]
|
|
40
|
+
* @property {number} [grid] samples per axis
|
|
41
|
+
* @property {number} [yaw] @property {number} [pitch] rotation, radians
|
|
42
|
+
* @property {number} [width] @property {number} [height]
|
|
43
|
+
* @property {[string,string]} [variables] independent variable names
|
|
44
|
+
* @property {any} [scope] @property {any} [env]
|
|
45
|
+
* @property {any} [theme] @property {boolean} [wireframe]
|
|
46
|
+
*/
|
|
47
|
+
/**
|
|
48
|
+
* Build the projected, depth-sorted scene (geometry as plain JSON).
|
|
49
|
+
* @param {string|Plot3dConfig} exprOrConfig
|
|
50
|
+
* @param {Plot3dConfig} [options]
|
|
51
|
+
* @returns {any}
|
|
52
|
+
*/
|
|
53
|
+
export declare function buildScene3d(exprOrConfig: string | Plot3dConfig, options?: Plot3dConfig): any;
|
|
54
|
+
/**
|
|
55
|
+
* Render a 3D scene into pure-vnode SVG.
|
|
56
|
+
* @param {any} scene @param {{ theme?: any }} [options]
|
|
57
|
+
* @returns {any}
|
|
58
|
+
*/
|
|
59
|
+
export declare function scene3dToVnode(scene: any, options?: {
|
|
60
|
+
theme?: any;
|
|
61
|
+
}): any;
|
|
62
|
+
/**
|
|
63
|
+
* Compile → sample → project → SVG vnode in one call.
|
|
64
|
+
* @param {string|Plot3dConfig} exprOrConfig
|
|
65
|
+
* @param {Plot3dConfig} [options]
|
|
66
|
+
* @returns {any}
|
|
67
|
+
*/
|
|
68
|
+
export declare function plot3d(exprOrConfig: string | Plot3dConfig, options?: Plot3dConfig): any;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The error vnode. The render path is total — it emits this
|
|
3
|
+
* instead of throwing. A small standalone `<svg>` that
|
|
4
|
+
* states the parse/eval error with its line/column when known.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* @param {{ message: string, line?: number, column?: number }} error
|
|
8
|
+
* @param {{ theme?: any, width?: number, height?: number }} [options]
|
|
9
|
+
* @returns {any}
|
|
10
|
+
*/
|
|
11
|
+
export declare function errorToVnode(error: {
|
|
12
|
+
message: string;
|
|
13
|
+
line?: number;
|
|
14
|
+
column?: number;
|
|
15
|
+
}, options?: {
|
|
16
|
+
theme?: any;
|
|
17
|
+
width?: number;
|
|
18
|
+
height?: number;
|
|
19
|
+
}): any;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Plot theme tokens (mirrors `@jarenjs/mermaid`'s theme). Resolves a
|
|
3
|
+
* name (or overrides) into concrete colors (written as SVG presentation
|
|
4
|
+
* attributes so `toSvgString()` is a valid standalone image) plus a
|
|
5
|
+
* matching `--calc-*` CSS variable set stamped inline on the root `<svg>`.
|
|
6
|
+
* The inline stamp beats every stylesheet rule, so the stamp itself is the
|
|
7
|
+
* re-theming hook: the `'host'` theme stamps linked variables as
|
|
8
|
+
* `var(--<host-token>, <concrete>)` (see `HOST_VARS`), making plots follow
|
|
9
|
+
* a host's light/dark tokens live without a re-render. Only the token
|
|
10
|
+
* tables live here; the resolution mechanics are shared
|
|
11
|
+
* (`@jarenjs/view/helpers` `resolveTheme`).
|
|
12
|
+
*/
|
|
13
|
+
/** @type {Record<string, Record<string, string>>} */
|
|
14
|
+
declare const THEMES: Record<string, Record<string, string>>;
|
|
15
|
+
/**
|
|
16
|
+
* Host custom-property links for the `'host'` theme: token key → the host
|
|
17
|
+
* token it should follow (the site token vocabulary, docs/DESIGN.md §2). The
|
|
18
|
+
* default theme's concrete colors remain as `var()` fallbacks, so the
|
|
19
|
+
* same SVG is standalone-valid outside any host. Tokens with no host
|
|
20
|
+
* equivalent (series2, the 3-D surface shades, wire) stay concrete.
|
|
21
|
+
* @type {Record<string, string>}
|
|
22
|
+
*/
|
|
23
|
+
export declare const HOST_VARS: Record<string, string>;
|
|
24
|
+
/**
|
|
25
|
+
* Resolve a theme name or override object. The name `'host'` resolves the
|
|
26
|
+
* default tokens linked to the host token vocabulary via {@link HOST_VARS}.
|
|
27
|
+
* @param {string | Record<string, any>} [nameOrOverrides]
|
|
28
|
+
* @returns {{ name: string, tokens: Record<string, string>, cssVars: Record<string, string> }}
|
|
29
|
+
*/
|
|
30
|
+
export declare function createTheme(nameOrOverrides?: string | Record<string, any>): {
|
|
31
|
+
name: string;
|
|
32
|
+
tokens: Record<string, string>;
|
|
33
|
+
cssVars: Record<string, string>;
|
|
34
|
+
};
|
|
35
|
+
export { THEMES };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file `toExpression(ast)` — the canonical printer. It is a **round-trip
|
|
3
|
+
* fixed point**: `parseExpression(toExpression(ast))` deep-equals
|
|
4
|
+
* `ast` for every AST the parser can produce. Parentheses are emitted
|
|
5
|
+
* from operator precedence/associativity only where removing them would
|
|
6
|
+
* change the parse; numbers print in canonical decimal (the AST never
|
|
7
|
+
* carried their original radix).
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* @param {any} node
|
|
11
|
+
* @returns {string}
|
|
12
|
+
*/
|
|
13
|
+
export declare function toExpression(node: any): string;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Small shared helpers for the calc engine.
|
|
3
|
+
*
|
|
4
|
+
* The content hash is the suite's single fingerprint primitive, so calc
|
|
5
|
+
* re-exports the one implementation from `@jarenjs/core` rather than
|
|
6
|
+
* carrying its own copy: equal content hits the same O(1) fast path
|
|
7
|
+
* (vnode `key`, memo key) everywhere downstream.
|
|
8
|
+
*/
|
|
9
|
+
export { hashContent } from '@jarenjs/core/string';
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# CALC-FORMAT — the `@jarenjs/calc` expression AST & mode contract
|
|
2
|
+
|
|
3
|
+
`@jarenjs/calc` is a two-layer package: a pure
|
|
4
|
+
**engine** (`src/*`, knows only `@jarenjs/core` + `@jarenjs/view`) and a
|
|
5
|
+
**component** (`src/component/`, adds `@jarenjs/app` + `@jarenjs/forms`).
|
|
6
|
+
This document is the engine contract.
|
|
7
|
+
|
|
8
|
+
## Expression AST
|
|
9
|
+
|
|
10
|
+
`parseExpression(text) → ExprAST` is a char-offset recursive-descent,
|
|
11
|
+
precedence-climbing parser (no `eval`, no `new Function`). Nodes are
|
|
12
|
+
monomorphic plain objects tagged by `type`; the AST is **geometry-free**
|
|
13
|
+
and does not preserve a literal's radix or source spacing. Node kinds:
|
|
14
|
+
|
|
15
|
+
| kind | shape |
|
|
16
|
+
|------|-------|
|
|
17
|
+
| `num` | `{ type:'num', value }` |
|
|
18
|
+
| `const` | `{ type:'const', name }` — `pi`,`e`,`phi`,`tau`,`inf`,`nan` (+ `π φ τ`) |
|
|
19
|
+
| `var` | `{ type:'var', name }` — free identifiers (`x`,`y`,`ans`,…) |
|
|
20
|
+
| `unary` | `{ type:'unary', op, arg }` — `- + ~` (prefix) |
|
|
21
|
+
| `postfix` | `{ type:'postfix', op, arg }` — `!` (factorial), `%` (percent) |
|
|
22
|
+
| `binary` | `{ type:'binary', op, left, right }` — `+ - * / ^ & \| << >>` |
|
|
23
|
+
| `call` | `{ type:'call', name, args }` |
|
|
24
|
+
|
|
25
|
+
The schema `schemas/jaren-calc-ast.schema.json` (draft-neutral) validates it.
|
|
26
|
+
|
|
27
|
+
### Grammar / precedence (low → high)
|
|
28
|
+
|
|
29
|
+
`|` → `&` → `<< >>` → `+ -` → `* /` → prefix `- + ~` → `^` (right-assoc) →
|
|
30
|
+
postfix `! %` → primary. Power binds tighter than unary minus
|
|
31
|
+
(`-2^2 = -(2^2)`); `^` is exponentiation in **every** mode.
|
|
32
|
+
|
|
33
|
+
### Round-trip fixed point
|
|
34
|
+
|
|
35
|
+
`toExpression(ast)` is a canonical printer for which
|
|
36
|
+
`parseExpression(toExpression(ast))` deep-equals `ast` across the fixture
|
|
37
|
+
corpus, and `toExpression` is idempotent on its own output. Parentheses
|
|
38
|
+
are emitted from precedence/associativity only where they change the parse.
|
|
39
|
+
|
|
40
|
+
## Compilation & evaluation
|
|
41
|
+
|
|
42
|
+
`compileExpr(ast, { env }) → (scope) => number` bakes every dispatch
|
|
43
|
+
decision into a nested closure (second compiler stage). `evaluate(source,
|
|
44
|
+
scope?, { env }?)` is the error-safe front door, returning
|
|
45
|
+
`{ ok:true, value } | { ok:false, error:{ message, line?, column? } }` —
|
|
46
|
+
it never throws into the app path. `scope` supplies variables (`x`, `y`,
|
|
47
|
+
`ans`, …) and runtime flags (`angleMode`, `wordBits`, `signed`).
|
|
48
|
+
|
|
49
|
+
**Environments** (the binding sets a mode resolves against): `defaultEnv()`
|
|
50
|
+
is float/scientific (angle-aware trig, `^` = power); `programmerEnv()`
|
|
51
|
+
overrides `& | << >> ~` with word-masked BigInt math from
|
|
52
|
+
`@jarenjs/core/math/word.js` and adds `and/or/xor/not/shl/shr/rol/ror/mod`.
|
|
53
|
+
|
|
54
|
+
> Bitwise **XOR** is the `xor(a, b)` function (not an infix operator), so
|
|
55
|
+
> `^` stays exponentiation everywhere. Programmer results are surfaced as
|
|
56
|
+
> `Number`; values beyond 2^53 lose precision when read back as a float —
|
|
57
|
+
> the four-base display uses `word.js` on BigInt directly and stays exact.
|
|
58
|
+
|
|
59
|
+
## Modes
|
|
60
|
+
|
|
61
|
+
Each mode (`standard`, `scientific`, `programmer`, `financial`,
|
|
62
|
+
`converter`) is a data-driven descriptor: a keypad/panel (`{ label, k,
|
|
63
|
+
tone?, span? }` rows, where `k` is the token appended to the expression
|
|
64
|
+
entry or a command `clear`/`back`/`equals`), a function-binding env, and a
|
|
65
|
+
formatter. `financial` holds **no formulas** (it orchestrates
|
|
66
|
+
`@jarenjs/core/finance` via `solveTvm`/`buildAmortization`/`npvOf`/`irrOf`);
|
|
67
|
+
`converter` holds **no factors** (it orchestrates `@jarenjs/core/convert`
|
|
68
|
+
via `convertValue`, currency included).
|
|
69
|
+
|
|
70
|
+
## Plotting
|
|
71
|
+
|
|
72
|
+
`plot2d`/`plot3d` (and `calcToVnode`) produce pure-vnode SVG rooted at
|
|
73
|
+
`['svg', …]`. They build a deterministic **scene** (geometry as plain JSON
|
|
74
|
+
— golden-testable) then render it. 2D compiles `f(x)` once, samples into a
|
|
75
|
+
`Float64Array`, maps to the viewport and breaks the path on NaN/±Inf and
|
|
76
|
+
asymptote jumps. 3D samples `z = f(x,y)` on a grid, rotates/projects with
|
|
77
|
+
the core `mat4`/`project.js` kernel, builds quads, **depth-sorts
|
|
78
|
+
back-to-front (painter's algorithm)** and shades by height.
|
|
79
|
+
`toSvgString(vnode)` gives a standalone SSR string.
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jarenjs/calc",
|
|
3
|
+
"private": false,
|
|
4
|
+
"version": "0.34.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"types": "./dist/types/index.d.ts",
|
|
8
|
+
"sideEffects": false,
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/types/index.d.ts",
|
|
12
|
+
"default": "./src/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./component": {
|
|
15
|
+
"types": "./dist/types/component/index.d.ts",
|
|
16
|
+
"default": "./src/component/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./theme": {
|
|
19
|
+
"types": "./dist/types/theme.d.ts",
|
|
20
|
+
"default": "./src/theme.js"
|
|
21
|
+
},
|
|
22
|
+
"./styles/calc.css": "./styles/calc.css",
|
|
23
|
+
"./schemas/*": "./schemas/*",
|
|
24
|
+
"./package.json": "./package.json"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist/types/",
|
|
28
|
+
"src/",
|
|
29
|
+
"docs/",
|
|
30
|
+
"schemas/",
|
|
31
|
+
"styles/"
|
|
32
|
+
],
|
|
33
|
+
"description": "A native, headless multi-mode calculator (standard / programmer / scientific / financial / converter) with x·y and x·y·z plotting rendered as pure-vnode SVG through @jarenjs/view — a two-stage expression compiler (parseExpression ⇄ toExpression), built on @jarenjs/core (math/finance/convert) and driven as an @jarenjs/app document.",
|
|
34
|
+
"author": "joham",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/jklarenbeek/jarenjs.git",
|
|
38
|
+
"directory": "components/calc"
|
|
39
|
+
},
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=24"
|
|
43
|
+
},
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public",
|
|
46
|
+
"registry": "https://registry.npmjs.org/"
|
|
47
|
+
},
|
|
48
|
+
"keywords": [
|
|
49
|
+
"jaren",
|
|
50
|
+
"json",
|
|
51
|
+
"calculator",
|
|
52
|
+
"expression",
|
|
53
|
+
"parser",
|
|
54
|
+
"plot",
|
|
55
|
+
"svg",
|
|
56
|
+
"headless",
|
|
57
|
+
"finance",
|
|
58
|
+
"units"
|
|
59
|
+
],
|
|
60
|
+
"scripts": {
|
|
61
|
+
"build": "npm run build:types",
|
|
62
|
+
"build:types": "tsc -p tsconfig.json",
|
|
63
|
+
"prepack": "npm run build:types"
|
|
64
|
+
},
|
|
65
|
+
"dependencies": {
|
|
66
|
+
"@jarenjs/core": "^0.34.0",
|
|
67
|
+
"@jarenjs/view": "^0.34.0",
|
|
68
|
+
"@jarenjs/forms": "^0.34.0",
|
|
69
|
+
"@jarenjs/app": "^0.34.0"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://jarenjs.dev/schemas/financial-inputs.schema.json",
|
|
4
|
+
"title": "Time Value of Money",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"nper": { "type": "number", "title": "N — number of periods", "minimum": 0 },
|
|
8
|
+
"rate": { "type": "number", "title": "I/Y — interest % per period" },
|
|
9
|
+
"pv": { "type": "number", "title": "PV — present value" },
|
|
10
|
+
"pmt": { "type": "number", "title": "PMT — payment" },
|
|
11
|
+
"fv": { "type": "number", "title": "FV — future value" },
|
|
12
|
+
"solveFor": { "title": "Solve for", "enum": ["pmt", "pv", "fv", "nper", "rate"] }
|
|
13
|
+
},
|
|
14
|
+
"required": ["solveFor"]
|
|
15
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://jarenjs.dev/schemas/jaren-calc-ast.schema.json",
|
|
4
|
+
"title": "Jaren calc expression AST",
|
|
5
|
+
"description": "The geometry-free expression AST produced by parseExpression and consumed by compileExpr / toExpression. Draft-neutral (no $ref siblings, no unevaluated*, no $dynamic*).",
|
|
6
|
+
"$ref": "#/$defs/node",
|
|
7
|
+
"$defs": {
|
|
8
|
+
"node": {
|
|
9
|
+
"oneOf": [
|
|
10
|
+
{ "$ref": "#/$defs/num" },
|
|
11
|
+
{ "$ref": "#/$defs/const" },
|
|
12
|
+
{ "$ref": "#/$defs/var" },
|
|
13
|
+
{ "$ref": "#/$defs/unary" },
|
|
14
|
+
{ "$ref": "#/$defs/postfix" },
|
|
15
|
+
{ "$ref": "#/$defs/binary" },
|
|
16
|
+
{ "$ref": "#/$defs/call" }
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
"num": {
|
|
20
|
+
"type": "object",
|
|
21
|
+
"required": ["type", "value"],
|
|
22
|
+
"properties": {
|
|
23
|
+
"type": { "const": "num" },
|
|
24
|
+
"value": { "type": "number" }
|
|
25
|
+
},
|
|
26
|
+
"additionalProperties": false
|
|
27
|
+
},
|
|
28
|
+
"const": {
|
|
29
|
+
"type": "object",
|
|
30
|
+
"required": ["type", "name"],
|
|
31
|
+
"properties": {
|
|
32
|
+
"type": { "const": "const" },
|
|
33
|
+
"name": { "type": "string" }
|
|
34
|
+
},
|
|
35
|
+
"additionalProperties": false
|
|
36
|
+
},
|
|
37
|
+
"var": {
|
|
38
|
+
"type": "object",
|
|
39
|
+
"required": ["type", "name"],
|
|
40
|
+
"properties": {
|
|
41
|
+
"type": { "const": "var" },
|
|
42
|
+
"name": { "type": "string" }
|
|
43
|
+
},
|
|
44
|
+
"additionalProperties": false
|
|
45
|
+
},
|
|
46
|
+
"unary": {
|
|
47
|
+
"type": "object",
|
|
48
|
+
"required": ["type", "op", "arg"],
|
|
49
|
+
"properties": {
|
|
50
|
+
"type": { "const": "unary" },
|
|
51
|
+
"op": { "enum": ["-", "+", "~"] },
|
|
52
|
+
"arg": { "$ref": "#/$defs/node" }
|
|
53
|
+
},
|
|
54
|
+
"additionalProperties": false
|
|
55
|
+
},
|
|
56
|
+
"postfix": {
|
|
57
|
+
"type": "object",
|
|
58
|
+
"required": ["type", "op", "arg"],
|
|
59
|
+
"properties": {
|
|
60
|
+
"type": { "const": "postfix" },
|
|
61
|
+
"op": { "enum": ["!", "%"] },
|
|
62
|
+
"arg": { "$ref": "#/$defs/node" }
|
|
63
|
+
},
|
|
64
|
+
"additionalProperties": false
|
|
65
|
+
},
|
|
66
|
+
"binary": {
|
|
67
|
+
"type": "object",
|
|
68
|
+
"required": ["type", "op", "left", "right"],
|
|
69
|
+
"properties": {
|
|
70
|
+
"type": { "const": "binary" },
|
|
71
|
+
"op": { "enum": ["+", "-", "*", "/", "^", "&", "|", "<<", ">>"] },
|
|
72
|
+
"left": { "$ref": "#/$defs/node" },
|
|
73
|
+
"right": { "$ref": "#/$defs/node" }
|
|
74
|
+
},
|
|
75
|
+
"additionalProperties": false
|
|
76
|
+
},
|
|
77
|
+
"call": {
|
|
78
|
+
"type": "object",
|
|
79
|
+
"required": ["type", "name", "args"],
|
|
80
|
+
"properties": {
|
|
81
|
+
"type": { "const": "call" },
|
|
82
|
+
"name": { "type": "string" },
|
|
83
|
+
"args": {
|
|
84
|
+
"type": "array",
|
|
85
|
+
"items": { "$ref": "#/$defs/node" }
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
"additionalProperties": false
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://jarenjs.dev/schemas/jaren-calc-state.schema.json",
|
|
4
|
+
"title": "Jaren calc state slice",
|
|
5
|
+
"description": "The plain-JSON $.calc state slice. Draft-neutral.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"mode": { "enum": ["standard", "scientific", "programmer", "financial", "converter"] },
|
|
9
|
+
"entry": { "type": "string" },
|
|
10
|
+
"ans": { "type": "number" },
|
|
11
|
+
"memory": { "type": "number" },
|
|
12
|
+
"angleMode": { "enum": ["rad", "deg", "grad"] },
|
|
13
|
+
"base": { "enum": ["HEX", "DEC", "OCT", "BIN"] },
|
|
14
|
+
"wordBits": { "enum": [8, 16, 32, 64] },
|
|
15
|
+
"signed": { "type": "boolean" },
|
|
16
|
+
"tape": {
|
|
17
|
+
"type": "array",
|
|
18
|
+
"items": {
|
|
19
|
+
"type": "object",
|
|
20
|
+
"properties": { "expr": { "type": "string" }, "result": { "type": "string" } }
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"plot": {
|
|
24
|
+
"type": "object",
|
|
25
|
+
"properties": {
|
|
26
|
+
"expr": { "type": "string" },
|
|
27
|
+
"kind": { "enum": ["2d", "3d"] }
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"fin": { "$ref": "financial-inputs.schema.json" },
|
|
31
|
+
"conv": {
|
|
32
|
+
"type": "object",
|
|
33
|
+
"properties": {
|
|
34
|
+
"dimension": { "type": "string" },
|
|
35
|
+
"from": { "type": "string" },
|
|
36
|
+
"to": { "type": "string" },
|
|
37
|
+
"value": { "type": "number" }
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"rates": {
|
|
41
|
+
"type": "object",
|
|
42
|
+
"properties": {
|
|
43
|
+
"base": { "type": "string" },
|
|
44
|
+
"rates": { "type": "object" },
|
|
45
|
+
"at": { "type": "number" },
|
|
46
|
+
"stale": { "type": "boolean" },
|
|
47
|
+
"status": { "type": "string" }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"required": ["mode", "entry"]
|
|
52
|
+
}
|
package/src/ast.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file The expression AST. Monomorphic node
|
|
4
|
+
* constructors — one plain-object shape per kind — so the compiler and
|
|
5
|
+
* printer branch on a single `type` tag and the reconciler/`deepEqual`
|
|
6
|
+
* round-trip stays cheap. The AST is **geometry-free**: it carries no
|
|
7
|
+
* layout, no source formatting (not even a literal's original radix), so
|
|
8
|
+
* that `parseExpression(toExpression(ast))` deep-equals `ast`. Meaning is
|
|
9
|
+
* imposed later — by `compileExpr` (evaluation) or the plotter
|
|
10
|
+
* (projection), never by the parser.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const CALC_AST_VERSION = '0.13.0';
|
|
14
|
+
|
|
15
|
+
/** Numeric literal (value only — the radix/format is not preserved). */
|
|
16
|
+
export function num(value) {
|
|
17
|
+
return { type: 'num', value: +value };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** A named constant (`pi`, `e`, `phi`, `tau`, `inf`, `nan`). */
|
|
21
|
+
export function constant(name) {
|
|
22
|
+
return { type: 'const', name };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** A variable / free identifier (`x`, `y`, `ans`, `mem`). */
|
|
26
|
+
export function variable(name) {
|
|
27
|
+
return { type: 'var', name };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A prefix unary node (`-`, `+`, `~`). */
|
|
31
|
+
export function unary(op, arg) {
|
|
32
|
+
return { type: 'unary', op, arg };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** A postfix node (`!` factorial, `%` percent). */
|
|
36
|
+
export function postfix(op, arg) {
|
|
37
|
+
return { type: 'postfix', op, arg };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A binary node (`+ - * / ^ & | << >>`). */
|
|
41
|
+
export function binary(op, left, right) {
|
|
42
|
+
return { type: 'binary', op, left, right };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A function call (`sin(x)`, `log(2, 8)`, `xor(a, b)`). */
|
|
46
|
+
export function call(name, args) {
|
|
47
|
+
return { type: 'call', name, args };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Free identifiers that always denote a constant rather than a variable.
|
|
52
|
+
* @type {Record<string, number>}
|
|
53
|
+
*/
|
|
54
|
+
export const CONSTANTS = {
|
|
55
|
+
pi: Math.PI,
|
|
56
|
+
π: Math.PI,
|
|
57
|
+
tau: Math.PI * 2,
|
|
58
|
+
τ: Math.PI * 2,
|
|
59
|
+
e: Math.E,
|
|
60
|
+
phi: (1 + Math.sqrt(5)) / 2,
|
|
61
|
+
φ: (1 + Math.sqrt(5)) / 2,
|
|
62
|
+
inf: Infinity,
|
|
63
|
+
nan: NaN,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** Is `name` a known constant identifier? */
|
|
67
|
+
export function isConstant(name) {
|
|
68
|
+
return Object.prototype.hasOwnProperty.call(CONSTANTS, name);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Structural equality for ASTs (used by the round-trip fixed-point
|
|
73
|
+
* tests). NaN compares equal to NaN so `nan` literals round-trip.
|
|
74
|
+
* @param {any} a @param {any} b
|
|
75
|
+
* @returns {boolean}
|
|
76
|
+
*/
|
|
77
|
+
export function astEqual(a, b) {
|
|
78
|
+
if (a === b) return true;
|
|
79
|
+
if (typeof a === 'number' && typeof b === 'number') {
|
|
80
|
+
return a === b || (Number.isNaN(a) && Number.isNaN(b));
|
|
81
|
+
}
|
|
82
|
+
if (a === null || b === null || typeof a !== 'object' || typeof b !== 'object') return false;
|
|
83
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
84
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
|
|
85
|
+
for (let i = 0; i < a.length; i++) if (!astEqual(a[i], b[i])) return false;
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
const ka = Object.keys(a);
|
|
89
|
+
const kb = Object.keys(b);
|
|
90
|
+
if (ka.length !== kb.length) return false;
|
|
91
|
+
for (const k of ka) {
|
|
92
|
+
if (!Object.prototype.hasOwnProperty.call(b, k)) return false;
|
|
93
|
+
if (!astEqual(a[k], b[k])) return false;
|
|
94
|
+
}
|
|
95
|
+
return true;
|
|
96
|
+
}
|
package/src/compile.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file `compileExpr(ast, opts) → (scope) => number`. The second stage of
|
|
4
|
+
* the two-stage compiler: every dispatch decision — which operator
|
|
5
|
+
* closure, which function, whether an identifier is a constant — is made
|
|
6
|
+
* once, here, and baked into a nested closure. Evaluation then does no
|
|
7
|
+
* lookups on the hot path. No `eval`, no `new Function` (CSP-safe).
|
|
8
|
+
*
|
|
9
|
+
* `evaluate(source, scope?, opts?)` is the error-safe front door: it
|
|
10
|
+
* parses, compiles and runs, returning a tagged `{ ok, value } | { ok:
|
|
11
|
+
* false, error }` result so the app/render path never throws.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { parseExpression } from './parser/index.js';
|
|
15
|
+
import { defaultEnv } from './env.js';
|
|
16
|
+
import { CalcParseError } from './errors.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Compile an AST against an environment.
|
|
20
|
+
* @param {any} ast
|
|
21
|
+
* @param {{ env?: any }} [opts]
|
|
22
|
+
* @returns {(scope?: any) => number}
|
|
23
|
+
*/
|
|
24
|
+
export function compileExpr(ast, opts = {}) {
|
|
25
|
+
const env = opts.env ?? defaultEnv();
|
|
26
|
+
return build(ast, env);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {any} node
|
|
31
|
+
* @param {any} env
|
|
32
|
+
* @returns {(scope?: any) => number}
|
|
33
|
+
*/
|
|
34
|
+
function build(node, env) {
|
|
35
|
+
switch (node.type) {
|
|
36
|
+
case 'num': {
|
|
37
|
+
const v = node.value;
|
|
38
|
+
return () => v;
|
|
39
|
+
}
|
|
40
|
+
case 'const': {
|
|
41
|
+
const name = node.name;
|
|
42
|
+
const v = env.constants[name];
|
|
43
|
+
if (v === undefined) throw new CalcParseError(`unknown constant '${name}'`);
|
|
44
|
+
return () => v;
|
|
45
|
+
}
|
|
46
|
+
case 'var': {
|
|
47
|
+
const name = node.name;
|
|
48
|
+
return (scope) => {
|
|
49
|
+
const v = scope ? scope[name] : undefined;
|
|
50
|
+
return v === undefined ? NaN : +v;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
case 'unary': {
|
|
54
|
+
const op = env.unops[node.op];
|
|
55
|
+
if (op === undefined) throw new CalcParseError(`unknown unary operator '${node.op}'`);
|
|
56
|
+
const arg = build(node.arg, env);
|
|
57
|
+
return (scope) => op(arg(scope), scope);
|
|
58
|
+
}
|
|
59
|
+
case 'postfix': {
|
|
60
|
+
const op = env.postops[node.op];
|
|
61
|
+
if (op === undefined) throw new CalcParseError(`unknown postfix operator '${node.op}'`);
|
|
62
|
+
const arg = build(node.arg, env);
|
|
63
|
+
return (scope) => op(arg(scope), scope);
|
|
64
|
+
}
|
|
65
|
+
case 'binary': {
|
|
66
|
+
const op = env.binops[node.op];
|
|
67
|
+
if (op === undefined) throw new CalcParseError(`unknown operator '${node.op}'`);
|
|
68
|
+
const l = build(node.left, env);
|
|
69
|
+
const r = build(node.right, env);
|
|
70
|
+
return (scope) => op(l(scope), r(scope), scope);
|
|
71
|
+
}
|
|
72
|
+
case 'call': {
|
|
73
|
+
const fn = env.funcs[node.name];
|
|
74
|
+
if (fn === undefined) throw new CalcParseError(`unknown function '${node.name}'`);
|
|
75
|
+
const args = node.args.map((a) => build(a, env));
|
|
76
|
+
return (scope) => fn(args.map((a) => a(scope)), scope);
|
|
77
|
+
}
|
|
78
|
+
default:
|
|
79
|
+
throw new CalcParseError(`cannot compile node '${node.type}'`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @typedef {object} EvalOk
|
|
85
|
+
* @property {true} ok
|
|
86
|
+
* @property {number} value
|
|
87
|
+
*/
|
|
88
|
+
/**
|
|
89
|
+
* @typedef {object} EvalErr
|
|
90
|
+
* @property {false} ok
|
|
91
|
+
* @property {{ message: string, line?: number, column?: number }} error
|
|
92
|
+
*/
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Parse + compile + evaluate a source expression, error-safe.
|
|
96
|
+
* @param {string} source
|
|
97
|
+
* @param {any} [scope]
|
|
98
|
+
* @param {{ env?: any }} [opts]
|
|
99
|
+
* @returns {EvalOk | EvalErr}
|
|
100
|
+
*/
|
|
101
|
+
export function evaluate(source, scope = {}, opts = {}) {
|
|
102
|
+
try {
|
|
103
|
+
const ast = parseExpression(source, opts);
|
|
104
|
+
const fn = compileExpr(ast, opts);
|
|
105
|
+
const value = fn(scope);
|
|
106
|
+
return { ok: true, value };
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
const e = /** @type {any} */ (err);
|
|
110
|
+
return {
|
|
111
|
+
ok: false,
|
|
112
|
+
error: {
|
|
113
|
+
message: e && e.message ? e.message : String(err),
|
|
114
|
+
line: e instanceof CalcParseError ? e.line : undefined,
|
|
115
|
+
column: e instanceof CalcParseError ? e.column : undefined,
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|