simplex-lang 0.3.0 → 1.0.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 +529 -12
- package/build/parser/index.js +1161 -479
- package/build/parser/index.js.map +1 -1
- package/build/src/compiler.d.ts +26 -18
- package/build/src/compiler.js +197 -463
- package/build/src/compiler.js.map +1 -1
- package/build/src/constants.d.ts +25 -0
- package/build/src/constants.js +29 -0
- package/build/src/constants.js.map +1 -0
- package/build/src/error-mapping.d.ts +31 -0
- package/build/src/error-mapping.js +72 -0
- package/build/src/error-mapping.js.map +1 -0
- package/build/src/errors.d.ts +8 -5
- package/build/src/errors.js +8 -10
- package/build/src/errors.js.map +1 -1
- package/build/src/index.d.ts +1 -0
- package/build/src/index.js +1 -0
- package/build/src/index.js.map +1 -1
- package/build/src/simplex-tree.d.ts +25 -3
- package/build/src/simplex.peggy +120 -3
- package/build/src/tools/index.d.ts +17 -7
- package/build/src/tools/index.js +81 -17
- package/build/src/tools/index.js.map +1 -1
- package/build/src/version.js +1 -1
- package/build/src/visitors.d.ts +15 -0
- package/build/src/visitors.js +330 -0
- package/build/src/visitors.js.map +1 -0
- package/package.json +6 -5
- package/parser/index.js +1161 -479
- package/parser/index.js.map +1 -1
- package/src/compiler.ts +308 -610
- package/src/constants.ts +30 -0
- package/src/error-mapping.ts +112 -0
- package/src/errors.ts +8 -12
- package/src/index.ts +1 -0
- package/src/simplex-tree.ts +30 -2
- package/src/simplex.peggy +120 -3
- package/src/tools/index.ts +107 -22
- package/src/visitors.ts +491 -0
- package/build/src/tools/cast.d.ts +0 -2
- package/build/src/tools/cast.js +0 -20
- package/build/src/tools/cast.js.map +0 -1
- package/build/src/tools/ensure.d.ts +0 -3
- package/build/src/tools/ensure.js +0 -30
- package/build/src/tools/ensure.js.map +0 -1
- package/build/src/tools/guards.d.ts +0 -2
- package/build/src/tools/guards.js +0 -24
- package/build/src/tools/guards.js.map +0 -1
- package/src/tools/cast.ts +0 -26
- package/src/tools/ensure.ts +0 -41
- package/src/tools/guards.ts +0 -34
package/src/constants.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Topic reference token used in pipe expressions
|
|
2
|
+
export const TOPIC_TOKEN = '%'
|
|
3
|
+
|
|
4
|
+
// Generated variable names used in bootstrap code and visitors
|
|
5
|
+
export const GEN = {
|
|
6
|
+
bool: 'bool',
|
|
7
|
+
bop: 'bop',
|
|
8
|
+
lop: 'lop',
|
|
9
|
+
uop: 'uop',
|
|
10
|
+
call: 'call',
|
|
11
|
+
getIdentifierValue: 'getIdentifierValue',
|
|
12
|
+
prop: 'prop',
|
|
13
|
+
pipe: 'pipe',
|
|
14
|
+
globals: 'globals',
|
|
15
|
+
get: 'get',
|
|
16
|
+
scope: 'scope',
|
|
17
|
+
_get: '_get',
|
|
18
|
+
_scope: '_scope',
|
|
19
|
+
_varNames: '_varNames',
|
|
20
|
+
_varValues: '_varValues',
|
|
21
|
+
ensObj: 'ensObj',
|
|
22
|
+
ensArr: 'ensArr',
|
|
23
|
+
str: 'str',
|
|
24
|
+
nna: 'nna',
|
|
25
|
+
} as const
|
|
26
|
+
|
|
27
|
+
// Semantic indices into the scope array [names, values, parent]
|
|
28
|
+
export const SCOPE_NAMES = 0
|
|
29
|
+
export const SCOPE_VALUES = 1
|
|
30
|
+
export const SCOPE_PARENT = 2
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { ExpressionError } from './errors.js'
|
|
2
|
+
import type { Location } from './simplex-tree.js'
|
|
3
|
+
import type { SourceLocation } from './visitors.js'
|
|
4
|
+
|
|
5
|
+
export type { SourceLocation }
|
|
6
|
+
|
|
7
|
+
// --- ErrorMapper Interface ---
|
|
8
|
+
|
|
9
|
+
export interface ErrorMapper {
|
|
10
|
+
/**
|
|
11
|
+
* Map a runtime error from generated code back to source expression location.
|
|
12
|
+
* Returns ExpressionError with source location, or null if unable to map.
|
|
13
|
+
*
|
|
14
|
+
* @param err — the caught runtime error
|
|
15
|
+
* @param expression — original SimplEx expression string
|
|
16
|
+
* @param offsets — source location mapping from code generation
|
|
17
|
+
* @param codeOffset — length of bootstrap code prepended before the expression
|
|
18
|
+
*/
|
|
19
|
+
mapError(
|
|
20
|
+
err: unknown,
|
|
21
|
+
expression: string,
|
|
22
|
+
offsets: SourceLocation[],
|
|
23
|
+
codeOffset: number
|
|
24
|
+
): ExpressionError | null
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Test if this mapper is compatible with the current JS engine.
|
|
28
|
+
* Called once during registration.
|
|
29
|
+
*/
|
|
30
|
+
probe(): boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// --- Helper ---
|
|
34
|
+
|
|
35
|
+
/** Map a code column offset back to an AST Location via the offsets array. */
|
|
36
|
+
export function getExpressionErrorLocation(
|
|
37
|
+
colOffset: number,
|
|
38
|
+
locations: SourceLocation[]
|
|
39
|
+
): Location | null {
|
|
40
|
+
var curCol = 0
|
|
41
|
+
for (const loc of locations) {
|
|
42
|
+
curCol += loc.len
|
|
43
|
+
if (curCol >= colOffset) return loc.location
|
|
44
|
+
}
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// --- V8 ErrorMapper ---
|
|
49
|
+
|
|
50
|
+
var V8_STACK_REGEX = /<anonymous>:(?<row>\d+):(?<col>\d+)/g
|
|
51
|
+
|
|
52
|
+
export var v8ErrorMapper: ErrorMapper = {
|
|
53
|
+
probe() {
|
|
54
|
+
try {
|
|
55
|
+
new Function('throw new Error("__simplex_probe__")')()
|
|
56
|
+
} catch (err) {
|
|
57
|
+
return /<anonymous>:\d+:\d+/.test((err as Error).stack ?? '')
|
|
58
|
+
}
|
|
59
|
+
return false
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
mapError(err, expression, offsets, codeOffset) {
|
|
63
|
+
if (!(err instanceof Error)) return null
|
|
64
|
+
|
|
65
|
+
var evalRow = err.stack
|
|
66
|
+
?.split('\n')
|
|
67
|
+
.map(r => r.trim())
|
|
68
|
+
.find(r => r.startsWith('at eval '))
|
|
69
|
+
if (!evalRow) return null
|
|
70
|
+
|
|
71
|
+
V8_STACK_REGEX.lastIndex = 0
|
|
72
|
+
var match = V8_STACK_REGEX.exec(evalRow)
|
|
73
|
+
var rowStr = match?.groups?.['row']
|
|
74
|
+
var colStr = match?.groups?.['col']
|
|
75
|
+
if (!rowStr || !colStr) return null
|
|
76
|
+
|
|
77
|
+
var row = Number.parseInt(rowStr)
|
|
78
|
+
if (row !== 3) return null
|
|
79
|
+
|
|
80
|
+
var col = Number.parseInt(colStr)
|
|
81
|
+
var adjustedCol = col - codeOffset
|
|
82
|
+
if (adjustedCol < 0) return null
|
|
83
|
+
|
|
84
|
+
var location = getExpressionErrorLocation(adjustedCol, offsets)
|
|
85
|
+
return new ExpressionError(err.message, expression, location, {
|
|
86
|
+
cause: err
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// --- Registration ---
|
|
92
|
+
|
|
93
|
+
var activeErrorMapper: ErrorMapper | null = null
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Register an ErrorMapper. If the mapper passes its probe(), it becomes
|
|
97
|
+
* the active mapper. Skips if the mapper is already active.
|
|
98
|
+
*/
|
|
99
|
+
export function registerErrorMapper(mapper: ErrorMapper): void {
|
|
100
|
+
if (activeErrorMapper === mapper) return
|
|
101
|
+
if (mapper.probe()) {
|
|
102
|
+
activeErrorMapper = mapper
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Get the currently active ErrorMapper (auto-detected or last registered). */
|
|
107
|
+
export function getActiveErrorMapper(): ErrorMapper | null {
|
|
108
|
+
return activeErrorMapper
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Auto-register V8 mapper at module load
|
|
112
|
+
registerErrorMapper(v8ErrorMapper)
|
package/src/errors.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Location } from './simplex-tree.js'
|
|
2
2
|
import { typeOf } from './tools/index.js'
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
/** Base error with source expression and location info. */
|
|
5
|
+
export class SimplexError extends Error {
|
|
5
6
|
constructor(
|
|
6
7
|
message: string,
|
|
7
8
|
public expression: string,
|
|
@@ -13,18 +14,13 @@ export class ExpressionError extends Error {
|
|
|
13
14
|
}
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
options?: ErrorOptions
|
|
22
|
-
) {
|
|
23
|
-
super(message, options)
|
|
24
|
-
this.name = this.constructor.name
|
|
25
|
-
}
|
|
26
|
-
}
|
|
17
|
+
/** Runtime error thrown when expression evaluation fails (e.g. unknown identifier). */
|
|
18
|
+
export class ExpressionError extends SimplexError {}
|
|
19
|
+
|
|
20
|
+
/** Error thrown during compilation (e.g. duplicate let bindings, invalid keys). */
|
|
21
|
+
export class CompileError extends SimplexError {}
|
|
27
22
|
|
|
23
|
+
/** TypeError with expected-vs-actual type info (e.g. "Expected number, but got string"). */
|
|
28
24
|
export class UnexpectedTypeError extends TypeError {
|
|
29
25
|
I18N_STRING = 'UNEXPECTED_TYPE'
|
|
30
26
|
|
package/src/index.ts
CHANGED
package/src/simplex-tree.ts
CHANGED
|
@@ -7,9 +7,11 @@ export type Expression =
|
|
|
7
7
|
| LogicalExpression
|
|
8
8
|
| LiteralExpression
|
|
9
9
|
| MemberExpression
|
|
10
|
+
| NonNullAssertExpression
|
|
10
11
|
| ObjectExpression
|
|
11
12
|
| NullishCoalescingExpression
|
|
12
13
|
| PipeSequence
|
|
14
|
+
| TemplateLiteralExpression
|
|
13
15
|
| TopicReference
|
|
14
16
|
| UnaryExpression
|
|
15
17
|
| LambdaExpression
|
|
@@ -54,19 +56,26 @@ export interface PropertyAssignment {
|
|
|
54
56
|
type: 'Property'
|
|
55
57
|
key: Expression
|
|
56
58
|
value: Expression
|
|
59
|
+
computed: boolean
|
|
57
60
|
kind: 'init'
|
|
58
61
|
location: Location
|
|
59
62
|
}
|
|
60
63
|
|
|
64
|
+
export interface SpreadElement {
|
|
65
|
+
type: 'SpreadElement'
|
|
66
|
+
argument: Expression
|
|
67
|
+
location: Location
|
|
68
|
+
}
|
|
69
|
+
|
|
61
70
|
export interface ObjectExpression {
|
|
62
71
|
type: 'ObjectExpression'
|
|
63
|
-
properties: PropertyAssignment[]
|
|
72
|
+
properties: (PropertyAssignment | SpreadElement)[]
|
|
64
73
|
location: Location
|
|
65
74
|
}
|
|
66
75
|
|
|
67
76
|
export interface ArrayExpression {
|
|
68
77
|
type: 'ArrayExpression'
|
|
69
|
-
elements: (Expression | null)[]
|
|
78
|
+
elements: (Expression | SpreadElement | null)[]
|
|
70
79
|
location: Location
|
|
71
80
|
}
|
|
72
81
|
|
|
@@ -188,3 +197,22 @@ export interface LetExpression {
|
|
|
188
197
|
expression: Expression
|
|
189
198
|
location: Location
|
|
190
199
|
}
|
|
200
|
+
|
|
201
|
+
export interface TemplateElement {
|
|
202
|
+
value: string
|
|
203
|
+
location: Location
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export interface NonNullAssertExpression {
|
|
207
|
+
type: 'NonNullAssertExpression'
|
|
208
|
+
expression: Expression
|
|
209
|
+
location: Location
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export interface TemplateLiteralExpression {
|
|
213
|
+
type: 'TemplateLiteral'
|
|
214
|
+
tag: Expression | null
|
|
215
|
+
quasis: TemplateElement[]
|
|
216
|
+
expressions: Expression[]
|
|
217
|
+
location: Location
|
|
218
|
+
}
|
package/src/simplex.peggy
CHANGED
|
@@ -271,6 +271,47 @@ SingleStringCharacter
|
|
|
271
271
|
/ "\\" sequence:EscapeSequence { return sequence; }
|
|
272
272
|
/ LineContinuation
|
|
273
273
|
|
|
274
|
+
TemplateLiteral "template literal"
|
|
275
|
+
= "`" head:TemplateCharacter* parts:TemplatePart* "`" {
|
|
276
|
+
var quasis = [];
|
|
277
|
+
var expressions = [];
|
|
278
|
+
|
|
279
|
+
quasis.push({
|
|
280
|
+
value: head.join(""),
|
|
281
|
+
location: getLocation(location())
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
for (var i = 0; i < parts.length; i++) {
|
|
285
|
+
expressions.push(parts[i].expression);
|
|
286
|
+
quasis.push({
|
|
287
|
+
value: (parts[i].suffix || []).join(""),
|
|
288
|
+
location: getLocation(location())
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
type: "TemplateLiteral",
|
|
294
|
+
tag: null,
|
|
295
|
+
quasis: quasis,
|
|
296
|
+
expressions: expressions,
|
|
297
|
+
location: getLocation(location())
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
TemplatePart
|
|
302
|
+
= "${" __ expression:Expression __ "}" suffix:TemplateCharacter* {
|
|
303
|
+
return { expression: expression, suffix: suffix };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
TemplateCharacter
|
|
307
|
+
= "\\" sequence:TemplateEscapeSequence { return sequence; }
|
|
308
|
+
/ !("`" / "${" / "\\") SourceCharacter { return text(); }
|
|
309
|
+
|
|
310
|
+
TemplateEscapeSequence
|
|
311
|
+
= "`" { return "`"; }
|
|
312
|
+
/ "$" { return "$"; }
|
|
313
|
+
/ EscapeSequence
|
|
314
|
+
|
|
274
315
|
LineContinuation
|
|
275
316
|
= "\\" LineTerminatorSequence { return ""; }
|
|
276
317
|
|
|
@@ -381,6 +422,7 @@ PrimaryExpression
|
|
|
381
422
|
= Identifier
|
|
382
423
|
/ Literal
|
|
383
424
|
/ TopicReference
|
|
425
|
+
/ TemplateLiteral
|
|
384
426
|
/ ArrayLiteral
|
|
385
427
|
/ ObjectLiteral
|
|
386
428
|
/ "(" __ expression:Expression __ ")" { return expression; }
|
|
@@ -410,12 +452,12 @@ ArrayLiteral
|
|
|
410
452
|
|
|
411
453
|
ElementList
|
|
412
454
|
= head:(
|
|
413
|
-
elision:(Elision __)? element:Expression {
|
|
455
|
+
elision:(Elision __)? element:(SpreadElement / Expression) {
|
|
414
456
|
return optionalList(extractOptional(elision, 0)).concat(element);
|
|
415
457
|
}
|
|
416
458
|
)
|
|
417
459
|
tail:(
|
|
418
|
-
__ "," __ elision:(Elision __)? element:Expression {
|
|
460
|
+
__ "," __ elision:(Elision __)? element:(SpreadElement / Expression) {
|
|
419
461
|
return optionalList(extractOptional(elision, 0)).concat(element);
|
|
420
462
|
}
|
|
421
463
|
)*
|
|
@@ -452,12 +494,33 @@ PropertyNameAndValueList
|
|
|
452
494
|
return buildList(head, tail, 3);
|
|
453
495
|
}
|
|
454
496
|
|
|
497
|
+
SpreadElement
|
|
498
|
+
= "..." __ argument:Expression {
|
|
499
|
+
return {
|
|
500
|
+
type: "SpreadElement",
|
|
501
|
+
argument: argument,
|
|
502
|
+
location: getLocation(location())
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
455
506
|
PropertyAssignment
|
|
456
|
-
=
|
|
507
|
+
= SpreadElement
|
|
508
|
+
/ key:PropertyName __ ":" __ value:Expression {
|
|
509
|
+
return {
|
|
510
|
+
type: "Property",
|
|
511
|
+
key: key,
|
|
512
|
+
value: value,
|
|
513
|
+
computed: false,
|
|
514
|
+
kind: "init",
|
|
515
|
+
location: getLocation(location())
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
/ "[" __ key:Expression __ "]" __ ":" __ value:Expression {
|
|
457
519
|
return {
|
|
458
520
|
type: "Property",
|
|
459
521
|
key: key,
|
|
460
522
|
value: value,
|
|
523
|
+
computed: true,
|
|
461
524
|
kind: "init",
|
|
462
525
|
location: getLocation(location())
|
|
463
526
|
};
|
|
@@ -488,9 +551,31 @@ MemberExpression
|
|
|
488
551
|
property: property, computed: false, extension: true, location: getLocation(location())
|
|
489
552
|
};
|
|
490
553
|
}
|
|
554
|
+
/ __ template:TemplateLiteral {
|
|
555
|
+
return {
|
|
556
|
+
tagged: true, template: template, location: getLocation(location())
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
/ "!" !"=" {
|
|
560
|
+
return {
|
|
561
|
+
nonNullAssert: true, location: getLocation(location())
|
|
562
|
+
};
|
|
563
|
+
}
|
|
491
564
|
)*
|
|
492
565
|
{
|
|
493
566
|
return tail.reduce(function(result, element) {
|
|
567
|
+
if (element.tagged) {
|
|
568
|
+
element.template.tag = result;
|
|
569
|
+
element.template.location = getLocation(location());
|
|
570
|
+
return element.template;
|
|
571
|
+
}
|
|
572
|
+
if (element.nonNullAssert) {
|
|
573
|
+
return {
|
|
574
|
+
type: "NonNullAssertExpression",
|
|
575
|
+
expression: result,
|
|
576
|
+
location: element.location
|
|
577
|
+
};
|
|
578
|
+
}
|
|
494
579
|
return {
|
|
495
580
|
type: "MemberExpression",
|
|
496
581
|
object: result,
|
|
@@ -534,12 +619,44 @@ CallExpression
|
|
|
534
619
|
type: "MemberExpression",
|
|
535
620
|
property: property,
|
|
536
621
|
computed: false,
|
|
622
|
+
extension: false,
|
|
623
|
+
location: getLocation(location())
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
/ __ "::" __ property:IdentifierName {
|
|
627
|
+
return {
|
|
628
|
+
type: "MemberExpression",
|
|
629
|
+
property: property,
|
|
630
|
+
computed: false,
|
|
631
|
+
extension: true,
|
|
537
632
|
location: getLocation(location())
|
|
538
633
|
};
|
|
539
634
|
}
|
|
635
|
+
/ __ template:TemplateLiteral {
|
|
636
|
+
return {
|
|
637
|
+
tagged: true, template: template, location: getLocation(location())
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
/ "!" !"=" {
|
|
641
|
+
return {
|
|
642
|
+
nonNullAssert: true, location: getLocation(location())
|
|
643
|
+
};
|
|
644
|
+
}
|
|
540
645
|
)*
|
|
541
646
|
{
|
|
542
647
|
return tail.reduce(function(result, element) {
|
|
648
|
+
if (element.tagged) {
|
|
649
|
+
element.template.tag = result;
|
|
650
|
+
element.template.location = getLocation(location());
|
|
651
|
+
return element.template;
|
|
652
|
+
}
|
|
653
|
+
if (element.nonNullAssert) {
|
|
654
|
+
return {
|
|
655
|
+
type: "NonNullAssertExpression",
|
|
656
|
+
expression: result,
|
|
657
|
+
location: element.location
|
|
658
|
+
};
|
|
659
|
+
}
|
|
543
660
|
element[TYPES_TO_PROPERTY_NAMES[element.type]] = result;
|
|
544
661
|
|
|
545
662
|
return element;
|
package/src/tools/index.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
export * from './ensure.js'
|
|
3
|
-
export * from './guards.js'
|
|
1
|
+
import { UnexpectedTypeError } from '../errors.js'
|
|
4
2
|
|
|
5
3
|
/**
|
|
6
4
|
* Alias for `Object.prototype.toString`
|
|
@@ -8,25 +6,6 @@ export * from './guards.js'
|
|
|
8
6
|
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
9
7
|
export const objToStringAlias = Object.prototype.toString
|
|
10
8
|
|
|
11
|
-
/**
|
|
12
|
-
* Converts instances of Number, String and Boolean to primitives
|
|
13
|
-
*/
|
|
14
|
-
export function unbox(val: unknown) {
|
|
15
|
-
if (typeof val !== 'object' || val === null) return val
|
|
16
|
-
|
|
17
|
-
const objConstructor = val.constructor
|
|
18
|
-
|
|
19
|
-
if (
|
|
20
|
-
objConstructor === Number ||
|
|
21
|
-
objConstructor === String ||
|
|
22
|
-
objConstructor === Boolean
|
|
23
|
-
) {
|
|
24
|
-
return val.valueOf()
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
return val
|
|
28
|
-
}
|
|
29
|
-
|
|
30
9
|
/**
|
|
31
10
|
* The method is needed to obtain the most specific readable data type.
|
|
32
11
|
*
|
|
@@ -51,3 +30,109 @@ export function typeOf(val: unknown) {
|
|
|
51
30
|
|
|
52
31
|
return type
|
|
53
32
|
}
|
|
33
|
+
|
|
34
|
+
// --- Guards ---
|
|
35
|
+
|
|
36
|
+
/** Check if value is a plain object (not Array, Map, etc.). */
|
|
37
|
+
export function isObject(val: unknown): val is object {
|
|
38
|
+
return objToStringAlias.call(val) === '[object Object]'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Boxed primitives (new String, etc.) are intentionally not handled — they
|
|
42
|
+
// cannot originate from SimplEx expressions and are not worth the overhead.
|
|
43
|
+
export function isSimpleValue(
|
|
44
|
+
val: unknown
|
|
45
|
+
): val is number | string | boolean | bigint | null | undefined {
|
|
46
|
+
const type = typeof val
|
|
47
|
+
|
|
48
|
+
if (
|
|
49
|
+
type === 'string' ||
|
|
50
|
+
type === 'number' ||
|
|
51
|
+
type === 'boolean' ||
|
|
52
|
+
type === 'bigint'
|
|
53
|
+
) {
|
|
54
|
+
return true
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (val == null) return true
|
|
58
|
+
|
|
59
|
+
return false
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// --- Cast ---
|
|
63
|
+
|
|
64
|
+
/** Coerce any value to boolean (standard JS truthiness). */
|
|
65
|
+
export function castToBoolean(val: unknown): boolean {
|
|
66
|
+
// Boxed primitives (new String, etc.) are intentionally not handled — see isSimpleValue comment.
|
|
67
|
+
return Boolean(val)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Coerce value to string; objects use Object.prototype.toString. */
|
|
71
|
+
export function castToString(val: unknown): string {
|
|
72
|
+
const type = typeof val
|
|
73
|
+
|
|
74
|
+
// Boxed primitives (new String, etc.) are intentionally not handled — see isSimpleValue comment.
|
|
75
|
+
if (type === 'string') return val as string
|
|
76
|
+
if (
|
|
77
|
+
val == null ||
|
|
78
|
+
type === 'number' ||
|
|
79
|
+
type === 'boolean' ||
|
|
80
|
+
type === 'bigint'
|
|
81
|
+
) {
|
|
82
|
+
return String(val)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return objToStringAlias.call(val)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// --- Ensure ---
|
|
89
|
+
|
|
90
|
+
/** Validate that value is a finite number or bigint; throw UnexpectedTypeError otherwise. */
|
|
91
|
+
export function ensureNumber(val: unknown): number | bigint {
|
|
92
|
+
if (typeof val === 'number' && Number.isFinite(val)) {
|
|
93
|
+
return val
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (typeof val === 'bigint') {
|
|
97
|
+
return val
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Boxed primitives (new Number, etc.) are intentionally not handled — see isSimpleValue comment.
|
|
101
|
+
throw new UnexpectedTypeError(['number', 'bigint'], val)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Validate that value is a function; throw UnexpectedTypeError otherwise. */
|
|
105
|
+
export function ensureFunction(val: unknown): Function {
|
|
106
|
+
if (typeof val === 'function') return val
|
|
107
|
+
throw new UnexpectedTypeError(['function'], val)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Validate that value is a plain object; throw UnexpectedTypeError otherwise. */
|
|
111
|
+
export function ensureObject(val: unknown): object {
|
|
112
|
+
if (isObject(val)) return val
|
|
113
|
+
throw new UnexpectedTypeError(['object'], val)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Validate that value is an array; throw UnexpectedTypeError otherwise. */
|
|
117
|
+
export function ensureArray(val: unknown): unknown[] {
|
|
118
|
+
if (Array.isArray(val)) return val
|
|
119
|
+
throw new UnexpectedTypeError(['Array'], val)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Validate that value is comparable (<, >, <=, >=); must be number, bigint, or string. */
|
|
123
|
+
export function ensureRelationalComparable(
|
|
124
|
+
val: unknown
|
|
125
|
+
): number | string | bigint {
|
|
126
|
+
// Boxed primitives (new String, etc.) are intentionally not handled — see isSimpleValue comment.
|
|
127
|
+
const type = typeof val
|
|
128
|
+
|
|
129
|
+
if (
|
|
130
|
+
(type === 'number' && Number.isNaN(val) === false) ||
|
|
131
|
+
type === 'string' ||
|
|
132
|
+
type === 'bigint'
|
|
133
|
+
) {
|
|
134
|
+
return val as number | string | bigint
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
throw new UnexpectedTypeError(['number', 'bigint', 'string'], val)
|
|
138
|
+
}
|