@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
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { T, typeValueEquals, typeValueToString, createEnvironment } from "@nudojs/core";
|
|
3
|
+
import { evaluateProgram, setSampleCount } from "../evaluator.ts";
|
|
4
|
+
import { parse } from "@nudojs/parser";
|
|
5
|
+
import type { TypeValue } from "@nudojs/core";
|
|
6
|
+
|
|
7
|
+
function evalCode(code: string): TypeValue {
|
|
8
|
+
const ast = parse(code);
|
|
9
|
+
const env = createEnvironment();
|
|
10
|
+
return evaluateProgram(ast, env);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function evalWith(code: string, bindings: Record<string, TypeValue>): TypeValue {
|
|
14
|
+
const ast = parse(code);
|
|
15
|
+
const env = createEnvironment();
|
|
16
|
+
for (const [k, v] of Object.entries(bindings)) env.bind(k, v);
|
|
17
|
+
return evaluateProgram(ast, env);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("ForStatement", () => {
|
|
21
|
+
beforeEach(() => setSampleCount(3));
|
|
22
|
+
|
|
23
|
+
it("evaluates simple for loop with literal bounds", () => {
|
|
24
|
+
const result = evalCode(`
|
|
25
|
+
let sum = 0;
|
|
26
|
+
for (let i = 0; i < 3; i++) {
|
|
27
|
+
sum = sum + i;
|
|
28
|
+
}
|
|
29
|
+
sum
|
|
30
|
+
`);
|
|
31
|
+
expect(typeValueEquals(result, T.literal(3))).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("evaluates for loop that returns early", () => {
|
|
35
|
+
const result = evalCode(`
|
|
36
|
+
function f() {
|
|
37
|
+
for (let i = 0; i < 10; i++) {
|
|
38
|
+
if (i === 2) return i;
|
|
39
|
+
}
|
|
40
|
+
return -1;
|
|
41
|
+
}
|
|
42
|
+
`);
|
|
43
|
+
// f is a function; we just check it's defined
|
|
44
|
+
expect(result).toBeDefined();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("for loop with abstract bounds widens variables", () => {
|
|
48
|
+
const result = evalWith(`
|
|
49
|
+
let sum = 0;
|
|
50
|
+
for (let i = 0; i < n; i++) {
|
|
51
|
+
sum = sum + i;
|
|
52
|
+
}
|
|
53
|
+
sum
|
|
54
|
+
`, { n: T.number });
|
|
55
|
+
// After widening, sum should be T.number
|
|
56
|
+
expect(typeValueEquals(result, T.number)).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("for loop variable accessible after loop", () => {
|
|
60
|
+
const result = evalCode(`
|
|
61
|
+
let x = 0;
|
|
62
|
+
for (let i = 0; i < 2; i++) {
|
|
63
|
+
x = x + 1;
|
|
64
|
+
}
|
|
65
|
+
x
|
|
66
|
+
`);
|
|
67
|
+
expect(typeValueEquals(result, T.literal(2))).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("WhileStatement", () => {
|
|
72
|
+
beforeEach(() => setSampleCount(3));
|
|
73
|
+
|
|
74
|
+
it("evaluates while loop with literal false condition", () => {
|
|
75
|
+
const result = evalCode(`
|
|
76
|
+
let x = 0;
|
|
77
|
+
while (false) {
|
|
78
|
+
x = 1;
|
|
79
|
+
}
|
|
80
|
+
x
|
|
81
|
+
`);
|
|
82
|
+
expect(typeValueEquals(result, T.literal(0))).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("evaluates while loop with concrete iterations", () => {
|
|
86
|
+
const result = evalCode(`
|
|
87
|
+
let x = 0;
|
|
88
|
+
let i = 0;
|
|
89
|
+
while (i < 3) {
|
|
90
|
+
x = x + 1;
|
|
91
|
+
i = i + 1;
|
|
92
|
+
}
|
|
93
|
+
x
|
|
94
|
+
`);
|
|
95
|
+
expect(typeValueEquals(result, T.literal(3))).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("while loop with abstract condition", () => {
|
|
99
|
+
const result = evalWith(`
|
|
100
|
+
let count = 0;
|
|
101
|
+
while (cond) {
|
|
102
|
+
count = count + 1;
|
|
103
|
+
}
|
|
104
|
+
count
|
|
105
|
+
`, { cond: T.boolean });
|
|
106
|
+
// After sample iterations, count should be widened to number
|
|
107
|
+
expect(typeValueEquals(result, T.number) || result.kind === "literal").toBe(true);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("DoWhileStatement", () => {
|
|
112
|
+
beforeEach(() => setSampleCount(3));
|
|
113
|
+
|
|
114
|
+
it("executes body at least once", () => {
|
|
115
|
+
const result = evalCode(`
|
|
116
|
+
let x = 0;
|
|
117
|
+
do {
|
|
118
|
+
x = x + 1;
|
|
119
|
+
} while (false);
|
|
120
|
+
x
|
|
121
|
+
`);
|
|
122
|
+
expect(typeValueEquals(result, T.literal(1))).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("evaluates do-while with multiple iterations", () => {
|
|
126
|
+
const result = evalCode(`
|
|
127
|
+
let x = 0;
|
|
128
|
+
let i = 0;
|
|
129
|
+
do {
|
|
130
|
+
x = x + 1;
|
|
131
|
+
i = i + 1;
|
|
132
|
+
} while (i < 3);
|
|
133
|
+
x
|
|
134
|
+
`);
|
|
135
|
+
expect(typeValueEquals(result, T.literal(3))).toBe(true);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe("@nudo:sample integration", () => {
|
|
140
|
+
it("respects custom sample count", () => {
|
|
141
|
+
setSampleCount(5);
|
|
142
|
+
const result = evalCode(`
|
|
143
|
+
let sum = 0;
|
|
144
|
+
for (let i = 0; i < 5; i++) {
|
|
145
|
+
sum = sum + i;
|
|
146
|
+
}
|
|
147
|
+
sum
|
|
148
|
+
`);
|
|
149
|
+
expect(typeValueEquals(result, T.literal(10))).toBe(true);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("default sample count is 3", () => {
|
|
153
|
+
setSampleCount(3);
|
|
154
|
+
const result = evalCode(`
|
|
155
|
+
let sum = 0;
|
|
156
|
+
for (let i = 0; i < 3; i++) {
|
|
157
|
+
sum = sum + i;
|
|
158
|
+
}
|
|
159
|
+
sum
|
|
160
|
+
`);
|
|
161
|
+
expect(typeValueEquals(result, T.literal(3))).toBe(true);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { T, typeValueEquals, typeValueToString, isSubtypeOf } from "@nudojs/core";
|
|
3
|
+
import { isTemplate, getTemplateParts, isRange, getRangeMeta } from "@nudojs/core";
|
|
4
|
+
import { evaluate, evaluateProgram } from "../evaluator.ts";
|
|
5
|
+
import { createEnvironment } from "@nudojs/core";
|
|
6
|
+
import { parse } from "@nudojs/parser";
|
|
7
|
+
|
|
8
|
+
function evalExpr(code: string): import("@nudojs/core").TypeValue {
|
|
9
|
+
const ast = parse(code);
|
|
10
|
+
const env = createEnvironment();
|
|
11
|
+
return evaluateProgram(ast, env);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function evalWithBindings(code: string, bindings: Record<string, import("@nudojs/core").TypeValue>): import("@nudojs/core").TypeValue {
|
|
15
|
+
const ast = parse(code);
|
|
16
|
+
const env = createEnvironment();
|
|
17
|
+
for (const [k, v] of Object.entries(bindings)) {
|
|
18
|
+
env.bind(k, v);
|
|
19
|
+
}
|
|
20
|
+
return evaluateProgram(ast, env);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("Template Literal Evaluation", () => {
|
|
24
|
+
it("all-literal template produces literal", () => {
|
|
25
|
+
const result = evalExpr("`hello world`");
|
|
26
|
+
expect(typeValueEquals(result, T.literal("hello world"))).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("template with abstract expression produces template type", () => {
|
|
30
|
+
const result = evalWithBindings("`xy${x}`", { x: T.string });
|
|
31
|
+
expect(isTemplate(result)).toBe(true);
|
|
32
|
+
expect(typeValueToString(result)).toBe("`xy${string}`");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("template with multiple parts", () => {
|
|
36
|
+
const result = evalWithBindings("`${x}!`", { x: T.string });
|
|
37
|
+
expect(isTemplate(result)).toBe(true);
|
|
38
|
+
expect(typeValueToString(result)).toBe("`${string}!`");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("string concatenation with + produces template", () => {
|
|
42
|
+
const result = evalWithBindings('"xy" + x', { x: T.string });
|
|
43
|
+
expect(isTemplate(result)).toBe(true);
|
|
44
|
+
expect(typeValueToString(result)).toBe("`xy${string}`");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("chained concatenation produces template", () => {
|
|
48
|
+
const result = evalWithBindings('"x" + x + "!"', { x: T.string });
|
|
49
|
+
expect(isTemplate(result)).toBe(true);
|
|
50
|
+
expect(typeValueToString(result)).toBe("`x${string}!`");
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("Template Method Dispatch", () => {
|
|
55
|
+
it("startsWith on template with known prefix returns true", () => {
|
|
56
|
+
const result = evalWithBindings(
|
|
57
|
+
'const s = "xy" + x; s.startsWith("x")',
|
|
58
|
+
{ x: T.string },
|
|
59
|
+
);
|
|
60
|
+
// The evaluateMethodCall should dispatch to template's startsWith
|
|
61
|
+
// and return T.literal(true)
|
|
62
|
+
// Note: this requires string method support in evaluateMethodCall
|
|
63
|
+
// which we add in 6.1. For now, refined dispatch handles it.
|
|
64
|
+
if (result.kind === "literal") {
|
|
65
|
+
expect(result.value).toBe(true);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("Range Narrowing", () => {
|
|
71
|
+
it("x >= 0 narrows to range in true branch", () => {
|
|
72
|
+
const code = `
|
|
73
|
+
function f(x) {
|
|
74
|
+
if (x >= 0) {
|
|
75
|
+
return x;
|
|
76
|
+
}
|
|
77
|
+
return x;
|
|
78
|
+
}
|
|
79
|
+
`;
|
|
80
|
+
const ast = parse(code);
|
|
81
|
+
const env = createEnvironment();
|
|
82
|
+
evaluateProgram(ast, env);
|
|
83
|
+
const fn = env.lookup("f");
|
|
84
|
+
expect(fn.kind).toBe("function");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("range comparison produces correct results", () => {
|
|
88
|
+
const result = evalWithBindings(
|
|
89
|
+
'const r = x >= 0 ? x : 0; r',
|
|
90
|
+
{ x: T.number },
|
|
91
|
+
);
|
|
92
|
+
// In the true branch, x is narrowed to range(min:0)
|
|
93
|
+
// In the false branch, result is T.literal(0)
|
|
94
|
+
// Union of both
|
|
95
|
+
expect(result).toBeDefined();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("User-defined Refined Types", () => {
|
|
100
|
+
it("refined type with custom ops", () => {
|
|
101
|
+
const odd = T.refine(T.number, {
|
|
102
|
+
name: "odd",
|
|
103
|
+
meta: {},
|
|
104
|
+
ops: {
|
|
105
|
+
"%"(self, other) {
|
|
106
|
+
if (other.kind === "literal" && other.value === 2) return T.literal(1);
|
|
107
|
+
return undefined;
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const result = evalWithBindings("x % 2", { x: odd });
|
|
113
|
+
expect(typeValueEquals(result, T.literal(1))).toBe(true);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("refined type falls back to base for undefined ops", () => {
|
|
117
|
+
const odd = T.refine(T.number, { name: "odd", meta: {} });
|
|
118
|
+
const result = evalWithBindings("x + 1", { x: odd });
|
|
119
|
+
expect(typeValueEquals(result, T.number)).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { T, typeValueEquals, createEnvironment } from "@nudojs/core";
|
|
3
|
+
import { evaluateProgram } from "../evaluator.ts";
|
|
4
|
+
import { parse } from "@nudojs/parser";
|
|
5
|
+
import type { TypeValue } from "@nudojs/core";
|
|
6
|
+
|
|
7
|
+
function evalExpr(code: string): TypeValue {
|
|
8
|
+
const ast = parse(code);
|
|
9
|
+
const env = createEnvironment();
|
|
10
|
+
return evaluateProgram(ast, env);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function evalWith(code: string, bindings: Record<string, TypeValue>): TypeValue {
|
|
14
|
+
const ast = parse(code);
|
|
15
|
+
const env = createEnvironment();
|
|
16
|
+
for (const [k, v] of Object.entries(bindings)) env.bind(k, v);
|
|
17
|
+
return evaluateProgram(ast, env);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("String methods - literal receiver", () => {
|
|
21
|
+
it("toUpperCase", () => {
|
|
22
|
+
expect(typeValueEquals(evalExpr('"hello".toUpperCase()'), T.literal("HELLO"))).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("toLowerCase", () => {
|
|
26
|
+
expect(typeValueEquals(evalExpr('"HELLO".toLowerCase()'), T.literal("hello"))).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("trim", () => {
|
|
30
|
+
expect(typeValueEquals(evalExpr('" hi ".trim()'), T.literal("hi"))).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("trimStart", () => {
|
|
34
|
+
expect(typeValueEquals(evalExpr('" hi".trimStart()'), T.literal("hi"))).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("trimEnd", () => {
|
|
38
|
+
expect(typeValueEquals(evalExpr('"hi ".trimEnd()'), T.literal("hi"))).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("charAt", () => {
|
|
42
|
+
expect(typeValueEquals(evalExpr('"abc".charAt(1)'), T.literal("b"))).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("charCodeAt", () => {
|
|
46
|
+
expect(typeValueEquals(evalExpr('"A".charCodeAt(0)'), T.literal(65))).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("at", () => {
|
|
50
|
+
expect(typeValueEquals(evalExpr('"abc".at(-1)'), T.literal("c"))).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("startsWith", () => {
|
|
54
|
+
expect(typeValueEquals(evalExpr('"hello".startsWith("he")'), T.literal(true))).toBe(true);
|
|
55
|
+
expect(typeValueEquals(evalExpr('"hello".startsWith("lo")'), T.literal(false))).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("endsWith", () => {
|
|
59
|
+
expect(typeValueEquals(evalExpr('"hello".endsWith("lo")'), T.literal(true))).toBe(true);
|
|
60
|
+
expect(typeValueEquals(evalExpr('"hello".endsWith("he")'), T.literal(false))).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("includes", () => {
|
|
64
|
+
expect(typeValueEquals(evalExpr('"hello".includes("ell")'), T.literal(true))).toBe(true);
|
|
65
|
+
expect(typeValueEquals(evalExpr('"hello".includes("xyz")'), T.literal(false))).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("indexOf", () => {
|
|
69
|
+
expect(typeValueEquals(evalExpr('"hello".indexOf("l")'), T.literal(2))).toBe(true);
|
|
70
|
+
expect(typeValueEquals(evalExpr('"hello".indexOf("z")'), T.literal(-1))).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("lastIndexOf", () => {
|
|
74
|
+
expect(typeValueEquals(evalExpr('"hello".lastIndexOf("l")'), T.literal(3))).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("slice", () => {
|
|
78
|
+
expect(typeValueEquals(evalExpr('"hello".slice(1, 3)'), T.literal("el"))).toBe(true);
|
|
79
|
+
expect(typeValueEquals(evalExpr('"hello".slice(2)'), T.literal("llo"))).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("substring", () => {
|
|
83
|
+
expect(typeValueEquals(evalExpr('"hello".substring(1, 4)'), T.literal("ell"))).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("split", () => {
|
|
87
|
+
const result = evalExpr('"a,b,c".split(",")');
|
|
88
|
+
expect(result.kind).toBe("tuple");
|
|
89
|
+
if (result.kind === "tuple") {
|
|
90
|
+
expect(result.elements.length).toBe(3);
|
|
91
|
+
expect(typeValueEquals(result.elements[0], T.literal("a"))).toBe(true);
|
|
92
|
+
expect(typeValueEquals(result.elements[1], T.literal("b"))).toBe(true);
|
|
93
|
+
expect(typeValueEquals(result.elements[2], T.literal("c"))).toBe(true);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("replace", () => {
|
|
98
|
+
expect(typeValueEquals(evalExpr('"hello".replace("l", "r")'), T.literal("herlo"))).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("replaceAll", () => {
|
|
102
|
+
expect(typeValueEquals(evalExpr('"hello".replaceAll("l", "r")'), T.literal("herro"))).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("repeat", () => {
|
|
106
|
+
expect(typeValueEquals(evalExpr('"ab".repeat(3)'), T.literal("ababab"))).toBe(true);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("padStart", () => {
|
|
110
|
+
expect(typeValueEquals(evalExpr('"5".padStart(3, "0")'), T.literal("005"))).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("padEnd", () => {
|
|
114
|
+
expect(typeValueEquals(evalExpr('"5".padEnd(3, "0")'), T.literal("500"))).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("String methods - abstract receiver", () => {
|
|
119
|
+
it("toUpperCase returns string", () => {
|
|
120
|
+
const result = evalWith("x.toUpperCase()", { x: T.string });
|
|
121
|
+
expect(typeValueEquals(result, T.string)).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("startsWith returns boolean", () => {
|
|
125
|
+
const result = evalWith('x.startsWith("a")', { x: T.string });
|
|
126
|
+
expect(typeValueEquals(result, T.boolean)).toBe(true);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("indexOf returns number", () => {
|
|
130
|
+
const result = evalWith('x.indexOf("a")', { x: T.string });
|
|
131
|
+
expect(typeValueEquals(result, T.number)).toBe(true);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("split returns string[]", () => {
|
|
135
|
+
const result = evalWith('x.split(",")', { x: T.string });
|
|
136
|
+
expect(result.kind).toBe("array");
|
|
137
|
+
if (result.kind === "array") {
|
|
138
|
+
expect(typeValueEquals(result.element, T.string)).toBe(true);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("charAt returns string", () => {
|
|
143
|
+
const result = evalWith("x.charAt(0)", { x: T.string });
|
|
144
|
+
expect(typeValueEquals(result, T.string)).toBe(true);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("slice returns string", () => {
|
|
148
|
+
const result = evalWith("x.slice(1)", { x: T.string });
|
|
149
|
+
expect(typeValueEquals(result, T.string)).toBe(true);
|
|
150
|
+
});
|
|
151
|
+
});
|