Repository navigation
Expand file tree
/
Copy pathutils.ts
More file actions
132 lines (121 loc) · 4.15 KB
/
Copy pathutils.ts
File metadata and controls
132 lines (121 loc) · 4.15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
/** Helpers shared by the examples. */
/**
* Read a required environment variable, failing with an actionable message.
*
* Examples never fall back to a placeholder key: a placeholder turns a missing
* credential into a confusing 401 from the provider instead of a clear error
* here, and committing anything key-shaped invites leaking a real one.
*/
export function requireEnv(name: string): string {
const value = process.env[name];
if (!value) {
console.error(
`Missing ${name}.\n\n` +
`Set it before running this example:\n` +
` export ${name}="your-key-here"\n`
);
process.exit(1);
}
return value;
}
/**
* Evaluate a basic arithmetic expression.
*
* A recursive-descent parser over `+ - * / % ^ ( )` and numbers. The examples
* previously passed model output straight to `eval()`, which executes arbitrary
* JavaScript — never do that with text a model produced.
*/
export function evaluateArithmetic(expression: string): number {
const tokens = tokenize(expression);
let position = 0;
const peek = () => tokens[position];
const consume = () => tokens[position++];
// expression := term (('+' | '-') term)*
function parseExpression(): number {
let value = parseTerm();
while (peek() === '+' || peek() === '-') {
const operator = consume();
const right = parseTerm();
value = operator === '+' ? value + right : value - right;
}
return value;
}
// term := factor (('*' | '/' | '%') factor)*
function parseTerm(): number {
let value = parseFactor();
while (peek() === '*' || peek() === '/' || peek() === '%') {
const operator = consume();
const right = parseFactor();
if ((operator === '/' || operator === '%') && right === 0) {
throw new Error('Division by zero');
}
if (operator === '*') value *= right;
else if (operator === '/') value /= right;
else value %= right;
}
return value;
}
// factor := unary ('^' factor)? — right-associative
function parseFactor(): number {
const base = parseUnary();
if (peek() === '^') {
consume();
return base ** parseFactor();
}
return base;
}
function parseUnary(): number {
if (peek() === '-') {
consume();
return -parseUnary();
}
if (peek() === '+') {
consume();
return parseUnary();
}
return parsePrimary();
}
function parsePrimary(): number {
const token = consume();
if (token === undefined) {
throw new Error('Unexpected end of expression');
}
if (token === '(') {
const value = parseExpression();
if (consume() !== ')') {
throw new Error('Unbalanced parentheses');
}
return value;
}
const value = Number(token);
if (Number.isNaN(value)) {
throw new Error(`Unexpected token: ${token}`);
}
return value;
}
const result = parseExpression();
if (position < tokens.length) {
throw new Error(`Unexpected trailing input: ${tokens.slice(position).join(' ')}`);
}
if (!Number.isFinite(result)) {
throw new Error('Expression did not evaluate to a finite number');
}
return result;
}
function tokenize(expression: string): string[] {
const tokens = expression.match(/\d+\.?\d*|[+\-*/%^()]/g);
if (!tokens) {
throw new Error(`Not an arithmetic expression: ${expression}`);
}
// Reject anything the tokenizer skipped, e.g. identifiers or function calls.
const consumed = tokens.join('').length;
const stripped = expression.replace(/\s+/g, '').length;
if (consumed !== stripped) {
throw new Error(`Expression contains unsupported characters: ${expression}`);
}
return tokens;
}
/** Print a labelled section header. */
export function section(title: string): void {
console.log(`\n${'='.repeat(60)}\n${title}\n${'='.repeat(60)}`);
}