@nudojs/cli 0.1.0 → 0.2.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/CHANGELOG.md +16 -0
- package/LICENSE +21 -0
- package/dist/{chunk-JW3RMCNY.js → chunk-LGWVGEMB.js} +363 -13
- package/dist/evaluator-api.d.ts +3 -1
- package/dist/evaluator-api.js +7 -3
- package/dist/index.js +12 -2
- package/package.json +13 -3
- package/src/__tests__/evaluator-loops.test.ts +163 -0
- package/src/__tests__/evaluator-refined.test.ts +121 -0
- package/src/__tests__/evaluator-string-methods.test.ts +151 -0
- package/src/evaluator-api.ts +2 -0
- package/src/evaluator.ts +380 -9
- package/src/index.ts +12 -0
- package/src/narrowing.ts +56 -0
- package/dist/chunk-RBHWWZII.js +0 -1467
- package/dist/chunk-VN5HMBGB.js +0 -1467
package/src/evaluator.ts
CHANGED
|
@@ -4,6 +4,9 @@ import {
|
|
|
4
4
|
T,
|
|
5
5
|
simplifyUnion,
|
|
6
6
|
applyBinaryOp,
|
|
7
|
+
dispatchBinaryOp,
|
|
8
|
+
dispatchMethod,
|
|
9
|
+
dispatchProperty,
|
|
7
10
|
Ops,
|
|
8
11
|
type Environment,
|
|
9
12
|
createEnvironment,
|
|
@@ -12,6 +15,8 @@ import {
|
|
|
12
15
|
typeValueEquals,
|
|
13
16
|
typeValueToString,
|
|
14
17
|
isSubtypeOf,
|
|
18
|
+
widenLiteral,
|
|
19
|
+
createTemplate,
|
|
15
20
|
} from "@nudojs/core";
|
|
16
21
|
import { narrow } from "./narrowing.ts";
|
|
17
22
|
|
|
@@ -89,6 +94,16 @@ let currentModuleResolver: ((source: string, fromDir: string) => { ast: Node; fi
|
|
|
89
94
|
let currentFileDir = "";
|
|
90
95
|
|
|
91
96
|
let _nodeTypeCollector: ((node: Node, tv: TypeValue) => void) | null = null;
|
|
97
|
+
let _sampleCount = 3;
|
|
98
|
+
let _maxConcreteIter = 1000;
|
|
99
|
+
|
|
100
|
+
export function setSampleCount(count: number): void {
|
|
101
|
+
_sampleCount = count;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function setMaxConcreteIter(count: number): void {
|
|
105
|
+
_maxConcreteIter = count;
|
|
106
|
+
}
|
|
92
107
|
|
|
93
108
|
export function setNodeTypeCollector(collector: ((node: Node, tv: TypeValue) => void) | null): void {
|
|
94
109
|
_nodeTypeCollector = collector;
|
|
@@ -264,7 +279,7 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
|
|
|
264
279
|
parts.map((p) => (p.kind === "literal" ? String(p.value) : "")).join(""),
|
|
265
280
|
);
|
|
266
281
|
}
|
|
267
|
-
return
|
|
282
|
+
return createTemplate(parts);
|
|
268
283
|
}
|
|
269
284
|
|
|
270
285
|
case "BinaryExpression": {
|
|
@@ -277,7 +292,7 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
|
|
|
277
292
|
return evaluateInstanceof(leftVal, rightVal, node.right, env);
|
|
278
293
|
}
|
|
279
294
|
return distributeBinaryOverUnion(leftVal, rightVal, (l, r) =>
|
|
280
|
-
|
|
295
|
+
dispatchBinaryOp(node.operator, l, r),
|
|
281
296
|
);
|
|
282
297
|
}
|
|
283
298
|
|
|
@@ -539,15 +554,16 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
|
|
|
539
554
|
}
|
|
540
555
|
|
|
541
556
|
case "ForStatement": {
|
|
542
|
-
|
|
543
|
-
const initResult = evaluate(node.init, env);
|
|
544
|
-
if (isReturn(initResult) || isBranch(initResult) || isThrow(initResult)) return initResult;
|
|
545
|
-
}
|
|
546
|
-
return T.undefined;
|
|
557
|
+
return evaluateForStatement(node, env);
|
|
547
558
|
}
|
|
548
559
|
|
|
549
|
-
case "WhileStatement":
|
|
550
|
-
return
|
|
560
|
+
case "WhileStatement": {
|
|
561
|
+
return evaluateWhileStatement(node, env);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
case "DoWhileStatement": {
|
|
565
|
+
return evaluateDoWhileStatement(node, env);
|
|
566
|
+
}
|
|
551
567
|
|
|
552
568
|
case "FunctionDeclaration": {
|
|
553
569
|
if (!node.id) return T.undefined;
|
|
@@ -691,6 +707,16 @@ function evaluateNode(node: Node, env: Environment): EvalResult {
|
|
|
691
707
|
if (propName === "length" && (obj.kind === "array" || obj.kind === "tuple")) {
|
|
692
708
|
return obj.kind === "tuple" ? T.literal(obj.elements.length) : T.number;
|
|
693
709
|
}
|
|
710
|
+
if (propName === "length" && obj.kind === "literal" && typeof obj.value === "string") {
|
|
711
|
+
return T.literal(obj.value.length);
|
|
712
|
+
}
|
|
713
|
+
if (propName === "length" && obj.kind === "primitive" && obj.type === "string") {
|
|
714
|
+
return T.number;
|
|
715
|
+
}
|
|
716
|
+
if (obj.kind === "refined") {
|
|
717
|
+
const result = dispatchProperty(obj, propName);
|
|
718
|
+
if (result !== undefined) return result;
|
|
719
|
+
}
|
|
694
720
|
return T.unknown;
|
|
695
721
|
});
|
|
696
722
|
}
|
|
@@ -936,9 +962,200 @@ function evaluateMethodCall(
|
|
|
936
962
|
return evaluateArrayMethod(objVal, methodName, args, env);
|
|
937
963
|
}
|
|
938
964
|
|
|
965
|
+
if (isStringLike(objVal)) {
|
|
966
|
+
const argVals = evaluateArgs(args, env);
|
|
967
|
+
if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
|
|
968
|
+
|
|
969
|
+
if (objVal.kind === "refined") {
|
|
970
|
+
const refined = dispatchMethod(objVal, methodName, argVals as TypeValue[]);
|
|
971
|
+
if (refined !== undefined) return refined;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
return evaluateStringMethod(objVal, methodName, argVals as TypeValue[]);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
if (objVal.kind === "refined") {
|
|
978
|
+
const argVals = evaluateArgs(args, env);
|
|
979
|
+
if (isReturn(argVals) || isBranch(argVals) || isThrow(argVals)) return argVals;
|
|
980
|
+
const result = dispatchMethod(objVal, methodName, argVals as TypeValue[]);
|
|
981
|
+
if (result !== undefined) return result;
|
|
982
|
+
}
|
|
983
|
+
|
|
939
984
|
return null;
|
|
940
985
|
}
|
|
941
986
|
|
|
987
|
+
function isStringLike(tv: TypeValue): boolean {
|
|
988
|
+
if (tv.kind === "literal" && typeof tv.value === "string") return true;
|
|
989
|
+
if (tv.kind === "primitive" && tv.type === "string") return true;
|
|
990
|
+
if (tv.kind === "refined") return isStringLike(tv.base);
|
|
991
|
+
return false;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function evaluateStringMethod(
|
|
995
|
+
receiver: TypeValue,
|
|
996
|
+
method: string,
|
|
997
|
+
args: TypeValue[],
|
|
998
|
+
): TypeValue | null {
|
|
999
|
+
if (receiver.kind === "literal" && typeof receiver.value === "string") {
|
|
1000
|
+
return evaluateStringMethodLiteral(receiver.value, method, args);
|
|
1001
|
+
}
|
|
1002
|
+
return evaluateStringMethodAbstract(method, args);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
function evaluateStringMethodLiteral(
|
|
1006
|
+
str: string,
|
|
1007
|
+
method: string,
|
|
1008
|
+
args: TypeValue[],
|
|
1009
|
+
): TypeValue | null {
|
|
1010
|
+
const litArg = (i: number): string | number | undefined => {
|
|
1011
|
+
const a = args[i];
|
|
1012
|
+
if (a?.kind === "literal" && (typeof a.value === "string" || typeof a.value === "number")) return a.value;
|
|
1013
|
+
return undefined;
|
|
1014
|
+
};
|
|
1015
|
+
|
|
1016
|
+
switch (method) {
|
|
1017
|
+
case "toUpperCase": return T.literal(str.toUpperCase());
|
|
1018
|
+
case "toLowerCase": return T.literal(str.toLowerCase());
|
|
1019
|
+
case "trim": return T.literal(str.trim());
|
|
1020
|
+
case "trimStart": return T.literal(str.trimStart());
|
|
1021
|
+
case "trimEnd": return T.literal(str.trimEnd());
|
|
1022
|
+
case "charAt": {
|
|
1023
|
+
const idx = litArg(0);
|
|
1024
|
+
return typeof idx === "number" ? T.literal(str.charAt(idx)) : T.string;
|
|
1025
|
+
}
|
|
1026
|
+
case "charCodeAt": {
|
|
1027
|
+
const idx = litArg(0);
|
|
1028
|
+
return typeof idx === "number" ? T.literal(str.charCodeAt(idx)) : T.number;
|
|
1029
|
+
}
|
|
1030
|
+
case "at": {
|
|
1031
|
+
const idx = litArg(0);
|
|
1032
|
+
if (typeof idx === "number") {
|
|
1033
|
+
const ch = str.at(idx);
|
|
1034
|
+
return ch !== undefined ? T.literal(ch) : T.undefined;
|
|
1035
|
+
}
|
|
1036
|
+
return T.union(T.string, T.undefined);
|
|
1037
|
+
}
|
|
1038
|
+
case "startsWith": {
|
|
1039
|
+
const search = litArg(0);
|
|
1040
|
+
return typeof search === "string" ? T.literal(str.startsWith(search as string)) : T.boolean;
|
|
1041
|
+
}
|
|
1042
|
+
case "endsWith": {
|
|
1043
|
+
const search = litArg(0);
|
|
1044
|
+
return typeof search === "string" ? T.literal(str.endsWith(search as string)) : T.boolean;
|
|
1045
|
+
}
|
|
1046
|
+
case "includes": {
|
|
1047
|
+
const search = litArg(0);
|
|
1048
|
+
return typeof search === "string" ? T.literal(str.includes(search as string)) : T.boolean;
|
|
1049
|
+
}
|
|
1050
|
+
case "indexOf": {
|
|
1051
|
+
const search = litArg(0);
|
|
1052
|
+
return typeof search === "string" ? T.literal(str.indexOf(search as string)) : T.number;
|
|
1053
|
+
}
|
|
1054
|
+
case "lastIndexOf": {
|
|
1055
|
+
const search = litArg(0);
|
|
1056
|
+
return typeof search === "string" ? T.literal(str.lastIndexOf(search as string)) : T.number;
|
|
1057
|
+
}
|
|
1058
|
+
case "slice": {
|
|
1059
|
+
const start = litArg(0);
|
|
1060
|
+
const end = litArg(1);
|
|
1061
|
+
if (typeof start === "number") {
|
|
1062
|
+
return T.literal(str.slice(start, typeof end === "number" ? end : undefined));
|
|
1063
|
+
}
|
|
1064
|
+
return T.string;
|
|
1065
|
+
}
|
|
1066
|
+
case "substring": {
|
|
1067
|
+
const start = litArg(0);
|
|
1068
|
+
const end = litArg(1);
|
|
1069
|
+
if (typeof start === "number") {
|
|
1070
|
+
return T.literal(str.substring(start, typeof end === "number" ? end : undefined));
|
|
1071
|
+
}
|
|
1072
|
+
return T.string;
|
|
1073
|
+
}
|
|
1074
|
+
case "split": {
|
|
1075
|
+
const sep = litArg(0);
|
|
1076
|
+
if (typeof sep === "string") {
|
|
1077
|
+
const parts = str.split(sep);
|
|
1078
|
+
return T.tuple(parts.map((p) => T.literal(p)));
|
|
1079
|
+
}
|
|
1080
|
+
return T.array(T.string);
|
|
1081
|
+
}
|
|
1082
|
+
case "replace": {
|
|
1083
|
+
const search = litArg(0);
|
|
1084
|
+
const replacement = litArg(1);
|
|
1085
|
+
if (typeof search === "string" && typeof replacement === "string") {
|
|
1086
|
+
return T.literal(str.replace(search, replacement));
|
|
1087
|
+
}
|
|
1088
|
+
return T.string;
|
|
1089
|
+
}
|
|
1090
|
+
case "replaceAll": {
|
|
1091
|
+
const search = litArg(0);
|
|
1092
|
+
const replacement = litArg(1);
|
|
1093
|
+
if (typeof search === "string" && typeof replacement === "string") {
|
|
1094
|
+
return T.literal(str.replaceAll(search, replacement));
|
|
1095
|
+
}
|
|
1096
|
+
return T.string;
|
|
1097
|
+
}
|
|
1098
|
+
case "repeat": {
|
|
1099
|
+
const count = litArg(0);
|
|
1100
|
+
return typeof count === "number" ? T.literal(str.repeat(count)) : T.string;
|
|
1101
|
+
}
|
|
1102
|
+
case "padStart": {
|
|
1103
|
+
const len = litArg(0);
|
|
1104
|
+
const fill = litArg(1);
|
|
1105
|
+
if (typeof len === "number") {
|
|
1106
|
+
return T.literal(str.padStart(len, typeof fill === "string" ? fill : undefined));
|
|
1107
|
+
}
|
|
1108
|
+
return T.string;
|
|
1109
|
+
}
|
|
1110
|
+
case "padEnd": {
|
|
1111
|
+
const len = litArg(0);
|
|
1112
|
+
const fill = litArg(1);
|
|
1113
|
+
if (typeof len === "number") {
|
|
1114
|
+
return T.literal(str.padEnd(len, typeof fill === "string" ? fill : undefined));
|
|
1115
|
+
}
|
|
1116
|
+
return T.string;
|
|
1117
|
+
}
|
|
1118
|
+
default:
|
|
1119
|
+
return null;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
function evaluateStringMethodAbstract(
|
|
1124
|
+
method: string,
|
|
1125
|
+
_args: TypeValue[],
|
|
1126
|
+
): TypeValue | null {
|
|
1127
|
+
switch (method) {
|
|
1128
|
+
case "toUpperCase":
|
|
1129
|
+
case "toLowerCase":
|
|
1130
|
+
case "trim":
|
|
1131
|
+
case "trimStart":
|
|
1132
|
+
case "trimEnd":
|
|
1133
|
+
case "charAt":
|
|
1134
|
+
case "slice":
|
|
1135
|
+
case "substring":
|
|
1136
|
+
case "replace":
|
|
1137
|
+
case "replaceAll":
|
|
1138
|
+
case "repeat":
|
|
1139
|
+
case "padStart":
|
|
1140
|
+
case "padEnd":
|
|
1141
|
+
return T.string;
|
|
1142
|
+
case "charCodeAt":
|
|
1143
|
+
case "indexOf":
|
|
1144
|
+
case "lastIndexOf":
|
|
1145
|
+
return T.number;
|
|
1146
|
+
case "at":
|
|
1147
|
+
return T.union(T.string, T.undefined);
|
|
1148
|
+
case "startsWith":
|
|
1149
|
+
case "endsWith":
|
|
1150
|
+
case "includes":
|
|
1151
|
+
return T.boolean;
|
|
1152
|
+
case "split":
|
|
1153
|
+
return T.array(T.string);
|
|
1154
|
+
default:
|
|
1155
|
+
return null;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
|
|
942
1159
|
function evaluateObjectStaticMethod(
|
|
943
1160
|
method: string,
|
|
944
1161
|
obj: TypeValue,
|
|
@@ -1215,6 +1432,160 @@ function bindForLoopVar(left: Node, value: TypeValue, env: Environment): void {
|
|
|
1215
1432
|
}
|
|
1216
1433
|
}
|
|
1217
1434
|
|
|
1435
|
+
function getLoopVarNames(node: Node): string[] {
|
|
1436
|
+
if (node.type === "VariableDeclaration") {
|
|
1437
|
+
return node.declarations
|
|
1438
|
+
.map((d: any) => d.id?.type === "Identifier" ? d.id.name : null)
|
|
1439
|
+
.filter((n: string | null): n is string => n !== null);
|
|
1440
|
+
}
|
|
1441
|
+
return [];
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
function snapshotVars(names: string[], env: Environment): Map<string, TypeValue> {
|
|
1445
|
+
const snap = new Map<string, TypeValue>();
|
|
1446
|
+
for (const name of names) {
|
|
1447
|
+
snap.set(name, env.lookup(name));
|
|
1448
|
+
}
|
|
1449
|
+
return snap;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
function varsStabilized(prev: Map<string, TypeValue>, curr: Map<string, TypeValue>): boolean {
|
|
1453
|
+
for (const [name, prevVal] of prev) {
|
|
1454
|
+
const currVal = curr.get(name);
|
|
1455
|
+
if (!currVal || !typeValueEquals(prevVal, currVal)) return false;
|
|
1456
|
+
}
|
|
1457
|
+
return true;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
function widenVars(names: string[], env: Environment): void {
|
|
1461
|
+
for (const name of names) {
|
|
1462
|
+
const val = env.lookup(name);
|
|
1463
|
+
const widened = widenLiteral(val);
|
|
1464
|
+
if (!typeValueEquals(val, widened)) {
|
|
1465
|
+
if (!env.update(name, widened)) env.bind(name, widened);
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
function evaluateForStatement(
|
|
1471
|
+
node: Node & { type: "ForStatement" },
|
|
1472
|
+
env: Environment,
|
|
1473
|
+
): EvalResult {
|
|
1474
|
+
const loopEnv = env.extend({});
|
|
1475
|
+
|
|
1476
|
+
if (node.init) {
|
|
1477
|
+
const initResult = evaluate(node.init, loopEnv);
|
|
1478
|
+
if (isReturn(initResult) || isBranch(initResult) || isThrow(initResult)) return initResult;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
const varNames = node.init ? getLoopVarNames(node.init) : [];
|
|
1482
|
+
const returnValues: TypeValue[] = [];
|
|
1483
|
+
let concreteCompleted = false;
|
|
1484
|
+
|
|
1485
|
+
for (let i = 0; i < _maxConcreteIter; i++) {
|
|
1486
|
+
if (node.test) {
|
|
1487
|
+
const testVal = evaluate(node.test, loopEnv);
|
|
1488
|
+
if (isReturn(testVal) || isBranch(testVal) || isThrow(testVal)) return testVal;
|
|
1489
|
+
if (testVal.kind === "literal" && testVal.value === false) { concreteCompleted = true; break; }
|
|
1490
|
+
if (testVal.kind !== "literal") break;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
const bodyResult = evaluate(node.body, loopEnv);
|
|
1494
|
+
if (isReturn(bodyResult)) {
|
|
1495
|
+
returnValues.push(bodyResult.value);
|
|
1496
|
+
concreteCompleted = true;
|
|
1497
|
+
break;
|
|
1498
|
+
}
|
|
1499
|
+
if (isBranch(bodyResult)) {
|
|
1500
|
+
returnValues.push(bodyResult.returnedValue);
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
if (node.update) {
|
|
1504
|
+
const updateResult = evaluate(node.update, loopEnv);
|
|
1505
|
+
if (isReturn(updateResult) || isBranch(updateResult) || isThrow(updateResult)) return updateResult;
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
if (!concreteCompleted) {
|
|
1510
|
+
widenVars(varNames, loopEnv);
|
|
1511
|
+
const prevSnap = snapshotVars(varNames, loopEnv);
|
|
1512
|
+
|
|
1513
|
+
for (let i = 0; i < 10; i++) {
|
|
1514
|
+
evaluate(node.body, loopEnv);
|
|
1515
|
+
if (node.update) evaluate(node.update, loopEnv);
|
|
1516
|
+
widenVars(varNames, loopEnv);
|
|
1517
|
+
const currSnap = snapshotVars(varNames, loopEnv);
|
|
1518
|
+
if (varsStabilized(prevSnap, currSnap)) break;
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
for (const name of varNames) {
|
|
1523
|
+
const val = loopEnv.lookup(name);
|
|
1524
|
+
if (!env.update(name, val)) env.bind(name, val);
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
if (returnValues.length > 0) {
|
|
1528
|
+
return makeBranch(simplifyUnion(returnValues), env);
|
|
1529
|
+
}
|
|
1530
|
+
return T.undefined;
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
function evaluateWhileStatement(
|
|
1534
|
+
node: Node & { type: "WhileStatement" },
|
|
1535
|
+
env: Environment,
|
|
1536
|
+
): EvalResult {
|
|
1537
|
+
const returnValues: TypeValue[] = [];
|
|
1538
|
+
|
|
1539
|
+
for (let i = 0; i < _maxConcreteIter; i++) {
|
|
1540
|
+
const tv = evaluate(node.test, env);
|
|
1541
|
+
if (isReturn(tv) || isBranch(tv) || isThrow(tv)) return tv;
|
|
1542
|
+
if (tv.kind === "literal" && tv.value === false) break;
|
|
1543
|
+
if (tv.kind !== "literal") break;
|
|
1544
|
+
|
|
1545
|
+
const bodyResult = evaluate(node.body, env);
|
|
1546
|
+
if (isReturn(bodyResult)) {
|
|
1547
|
+
returnValues.push(bodyResult.value);
|
|
1548
|
+
break;
|
|
1549
|
+
}
|
|
1550
|
+
if (isBranch(bodyResult)) {
|
|
1551
|
+
returnValues.push(bodyResult.returnedValue);
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
if (returnValues.length > 0) {
|
|
1556
|
+
return makeBranch(simplifyUnion(returnValues), env);
|
|
1557
|
+
}
|
|
1558
|
+
return T.undefined;
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
function evaluateDoWhileStatement(
|
|
1562
|
+
node: Node & { type: "DoWhileStatement" },
|
|
1563
|
+
env: Environment,
|
|
1564
|
+
): EvalResult {
|
|
1565
|
+
const returnValues: TypeValue[] = [];
|
|
1566
|
+
|
|
1567
|
+
for (let i = 0; i < _maxConcreteIter; i++) {
|
|
1568
|
+
const bodyResult = evaluate(node.body, env);
|
|
1569
|
+
if (isReturn(bodyResult)) {
|
|
1570
|
+
returnValues.push(bodyResult.value);
|
|
1571
|
+
break;
|
|
1572
|
+
}
|
|
1573
|
+
if (isBranch(bodyResult)) {
|
|
1574
|
+
returnValues.push(bodyResult.returnedValue);
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
const tv = evaluate(node.test, env);
|
|
1578
|
+
if (isReturn(tv) || isBranch(tv) || isThrow(tv)) return tv;
|
|
1579
|
+
if (tv.kind === "literal" && tv.value === false) break;
|
|
1580
|
+
if (tv.kind !== "literal") break;
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
if (returnValues.length > 0) {
|
|
1584
|
+
return makeBranch(simplifyUnion(returnValues), env);
|
|
1585
|
+
}
|
|
1586
|
+
return T.undefined;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1218
1589
|
function evaluateImportDeclaration(node: Node & { type: "ImportDeclaration" }, env: Environment): EvalResult {
|
|
1219
1590
|
const source = node.source.value;
|
|
1220
1591
|
if (!currentModuleResolver) return T.undefined;
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
simplifyUnion,
|
|
8
8
|
createEnvironment,
|
|
9
9
|
isSubtypeOf,
|
|
10
|
+
isTemplate,
|
|
11
|
+
getTemplateParts,
|
|
10
12
|
} from "@nudojs/core";
|
|
11
13
|
import type { TypeValue } from "@nudojs/core";
|
|
12
14
|
import { parse, extractDirectives, parseTypeValueExpr } from "@nudojs/parser";
|
|
@@ -65,6 +67,16 @@ function typeValueToTSType(tv: TypeValue): string {
|
|
|
65
67
|
}
|
|
66
68
|
case "primitive":
|
|
67
69
|
return tv.type;
|
|
70
|
+
case "refined": {
|
|
71
|
+
if (isTemplate(tv)) {
|
|
72
|
+
const parts = getTemplateParts(tv)!;
|
|
73
|
+
const inner = parts
|
|
74
|
+
.map((p) => (p.kind === "literal" && typeof p.value === "string" ? p.value : `\${${typeValueToTSType(p)}}`))
|
|
75
|
+
.join("");
|
|
76
|
+
return `\`${inner}\``;
|
|
77
|
+
}
|
|
78
|
+
return typeValueToTSType(tv.base);
|
|
79
|
+
}
|
|
68
80
|
case "object": {
|
|
69
81
|
const entries = Object.entries(tv.properties);
|
|
70
82
|
if (entries.length === 0) return "{}";
|
package/src/narrowing.ts
CHANGED
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
subtractType,
|
|
8
8
|
getPrimitiveTypeOf,
|
|
9
9
|
typeValueEquals,
|
|
10
|
+
isSubtypeOf,
|
|
11
|
+
createRange,
|
|
10
12
|
} from "@nudojs/core";
|
|
11
13
|
|
|
12
14
|
/**
|
|
@@ -123,6 +125,33 @@ export function narrow(
|
|
|
123
125
|
return narrowByInstanceof(test.left.name, test.right.name, env);
|
|
124
126
|
}
|
|
125
127
|
|
|
128
|
+
// x >= literal / x > literal / x <= literal / x < literal
|
|
129
|
+
if (
|
|
130
|
+
test.type === "BinaryExpression" &&
|
|
131
|
+
(test.operator === ">=" || test.operator === ">" || test.operator === "<=" || test.operator === "<") &&
|
|
132
|
+
test.left.type === "Identifier" &&
|
|
133
|
+
isLiteralNode(test.right)
|
|
134
|
+
) {
|
|
135
|
+
const litVal = getLiteralValue(test.right);
|
|
136
|
+
if (litVal.kind === "literal" && typeof litVal.value === "number") {
|
|
137
|
+
return narrowByComparison(test.left.name, test.operator, litVal.value, env);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// literal >= x / literal > x / literal <= x / literal < x
|
|
142
|
+
if (
|
|
143
|
+
test.type === "BinaryExpression" &&
|
|
144
|
+
(test.operator === ">=" || test.operator === ">" || test.operator === "<=" || test.operator === "<") &&
|
|
145
|
+
isLiteralNode(test.left) &&
|
|
146
|
+
test.right.type === "Identifier"
|
|
147
|
+
) {
|
|
148
|
+
const litVal = getLiteralValue(test.left);
|
|
149
|
+
if (litVal.kind === "literal" && typeof litVal.value === "number") {
|
|
150
|
+
const flipped = { ">=": "<=", ">": "<", "<=": ">=", "<": ">" } as const;
|
|
151
|
+
return narrowByComparison(test.right.name, flipped[test.operator as keyof typeof flipped], litVal.value, env);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
126
155
|
// !expr (negate)
|
|
127
156
|
if (test.type === "UnaryExpression" && test.operator === "!") {
|
|
128
157
|
const [trueEnv, falseEnv] = narrow(test.argument, env);
|
|
@@ -264,3 +293,30 @@ function narrowByStrictEqual(
|
|
|
264
293
|
falseEnv.bind(varName, excluded.kind === "never" ? current : excluded);
|
|
265
294
|
return [trueEnv, falseEnv];
|
|
266
295
|
}
|
|
296
|
+
|
|
297
|
+
function narrowByComparison(
|
|
298
|
+
varName: string,
|
|
299
|
+
op: ">=" | ">" | "<=" | "<",
|
|
300
|
+
value: number,
|
|
301
|
+
env: Environment,
|
|
302
|
+
): [Environment, Environment] {
|
|
303
|
+
const current = env.lookup(varName);
|
|
304
|
+
if (!isSubtypeOf(current, T.number) && current.kind !== "union") return [env, env];
|
|
305
|
+
|
|
306
|
+
const trueEnv = env.extend({});
|
|
307
|
+
const falseEnv = env.extend({});
|
|
308
|
+
|
|
309
|
+
const trueRange = op === ">=" ? createRange({ min: value })
|
|
310
|
+
: op === ">" ? createRange({ min: value + 1 })
|
|
311
|
+
: op === "<=" ? createRange({ max: value })
|
|
312
|
+
: createRange({ max: value - 1 });
|
|
313
|
+
|
|
314
|
+
const falseRange = op === ">=" ? createRange({ max: value - 1 })
|
|
315
|
+
: op === ">" ? createRange({ max: value })
|
|
316
|
+
: op === "<=" ? createRange({ min: value + 1 })
|
|
317
|
+
: createRange({ min: value });
|
|
318
|
+
|
|
319
|
+
trueEnv.bind(varName, trueRange);
|
|
320
|
+
falseEnv.bind(varName, falseRange);
|
|
321
|
+
return [trueEnv, falseEnv];
|
|
322
|
+
}
|