@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/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 T.string;
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
- applyBinaryOp(node.operator, l, r),
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
- if (node.init) {
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 T.undefined;
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
+ }