@vibe-lang/runtime 0.2.5
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/package.json +46 -0
- package/src/ast/index.ts +375 -0
- package/src/ast.ts +2 -0
- package/src/debug/advanced-features.ts +482 -0
- package/src/debug/bun-inspector.ts +424 -0
- package/src/debug/handoff-manager.ts +283 -0
- package/src/debug/index.ts +150 -0
- package/src/debug/runner.ts +365 -0
- package/src/debug/server.ts +565 -0
- package/src/debug/stack-merger.ts +267 -0
- package/src/debug/state.ts +581 -0
- package/src/debug/test/advanced-features.test.ts +300 -0
- package/src/debug/test/e2e.test.ts +218 -0
- package/src/debug/test/handoff-manager.test.ts +256 -0
- package/src/debug/test/runner.test.ts +256 -0
- package/src/debug/test/stack-merger.test.ts +163 -0
- package/src/debug/test/state.test.ts +400 -0
- package/src/debug/test/ts-debug-integration.test.ts +374 -0
- package/src/debug/test/ts-import-tracker.test.ts +125 -0
- package/src/debug/test/ts-source-map.test.ts +169 -0
- package/src/debug/ts-import-tracker.ts +151 -0
- package/src/debug/ts-source-map.ts +171 -0
- package/src/errors/index.ts +124 -0
- package/src/index.ts +358 -0
- package/src/lexer/index.ts +348 -0
- package/src/lexer.ts +2 -0
- package/src/parser/index.ts +792 -0
- package/src/parser/parse.ts +45 -0
- package/src/parser/test/async.test.ts +248 -0
- package/src/parser/test/destructuring.test.ts +167 -0
- package/src/parser/test/do-expression.test.ts +486 -0
- package/src/parser/test/errors/do-expression.test.ts +95 -0
- package/src/parser/test/errors/error-locations.test.ts +230 -0
- package/src/parser/test/errors/invalid-expressions.test.ts +144 -0
- package/src/parser/test/errors/missing-tokens.test.ts +126 -0
- package/src/parser/test/errors/model-declaration.test.ts +185 -0
- package/src/parser/test/errors/nested-blocks.test.ts +226 -0
- package/src/parser/test/errors/unclosed-delimiters.test.ts +122 -0
- package/src/parser/test/errors/unexpected-tokens.test.ts +120 -0
- package/src/parser/test/import-export.test.ts +143 -0
- package/src/parser/test/literals.test.ts +404 -0
- package/src/parser/test/model-declaration.test.ts +161 -0
- package/src/parser/test/nested-blocks.test.ts +402 -0
- package/src/parser/test/parser.test.ts +743 -0
- package/src/parser/test/private.test.ts +136 -0
- package/src/parser/test/template-literal.test.ts +127 -0
- package/src/parser/test/tool-declaration.test.ts +302 -0
- package/src/parser/test/ts-block.test.ts +252 -0
- package/src/parser/test/type-annotations.test.ts +254 -0
- package/src/parser/visitor/helpers.ts +330 -0
- package/src/parser/visitor.ts +794 -0
- package/src/parser.ts +2 -0
- package/src/runtime/ai/cache-chunking.test.ts +69 -0
- package/src/runtime/ai/cache-chunking.ts +73 -0
- package/src/runtime/ai/client.ts +109 -0
- package/src/runtime/ai/context.ts +168 -0
- package/src/runtime/ai/formatters.ts +316 -0
- package/src/runtime/ai/index.ts +38 -0
- package/src/runtime/ai/language-ref.ts +38 -0
- package/src/runtime/ai/providers/anthropic.ts +253 -0
- package/src/runtime/ai/providers/google.ts +201 -0
- package/src/runtime/ai/providers/openai.ts +156 -0
- package/src/runtime/ai/retry.ts +100 -0
- package/src/runtime/ai/return-tools.ts +301 -0
- package/src/runtime/ai/test/client.test.ts +83 -0
- package/src/runtime/ai/test/formatters.test.ts +485 -0
- package/src/runtime/ai/test/retry.test.ts +137 -0
- package/src/runtime/ai/test/return-tools.test.ts +450 -0
- package/src/runtime/ai/test/tool-loop.test.ts +319 -0
- package/src/runtime/ai/test/tool-schema.test.ts +241 -0
- package/src/runtime/ai/tool-loop.ts +203 -0
- package/src/runtime/ai/tool-schema.ts +151 -0
- package/src/runtime/ai/types.ts +113 -0
- package/src/runtime/ai-logger.ts +255 -0
- package/src/runtime/ai-provider.ts +347 -0
- package/src/runtime/async/dependencies.ts +276 -0
- package/src/runtime/async/executor.ts +293 -0
- package/src/runtime/async/index.ts +43 -0
- package/src/runtime/async/scheduling.ts +163 -0
- package/src/runtime/async/test/dependencies.test.ts +284 -0
- package/src/runtime/async/test/executor.test.ts +388 -0
- package/src/runtime/context.ts +357 -0
- package/src/runtime/exec/ai.ts +139 -0
- package/src/runtime/exec/expressions.ts +475 -0
- package/src/runtime/exec/frames.ts +26 -0
- package/src/runtime/exec/functions.ts +305 -0
- package/src/runtime/exec/interpolation.ts +312 -0
- package/src/runtime/exec/statements.ts +604 -0
- package/src/runtime/exec/tools.ts +129 -0
- package/src/runtime/exec/typescript.ts +215 -0
- package/src/runtime/exec/variables.ts +279 -0
- package/src/runtime/index.ts +975 -0
- package/src/runtime/modules.ts +452 -0
- package/src/runtime/serialize.ts +103 -0
- package/src/runtime/state.ts +489 -0
- package/src/runtime/stdlib/core.ts +45 -0
- package/src/runtime/stdlib/directory.test.ts +156 -0
- package/src/runtime/stdlib/edit.test.ts +154 -0
- package/src/runtime/stdlib/fastEdit.test.ts +201 -0
- package/src/runtime/stdlib/glob.test.ts +106 -0
- package/src/runtime/stdlib/grep.test.ts +144 -0
- package/src/runtime/stdlib/index.ts +16 -0
- package/src/runtime/stdlib/readFile.test.ts +123 -0
- package/src/runtime/stdlib/tools/index.ts +707 -0
- package/src/runtime/stdlib/writeFile.test.ts +157 -0
- package/src/runtime/step.ts +969 -0
- package/src/runtime/test/ai-context.test.ts +1086 -0
- package/src/runtime/test/ai-result-object.test.ts +419 -0
- package/src/runtime/test/ai-tool-flow.test.ts +859 -0
- package/src/runtime/test/async-execution-order.test.ts +618 -0
- package/src/runtime/test/async-execution.test.ts +344 -0
- package/src/runtime/test/async-nested.test.ts +660 -0
- package/src/runtime/test/async-parallel-timing.test.ts +546 -0
- package/src/runtime/test/basic1.test.ts +154 -0
- package/src/runtime/test/binary-operators.test.ts +431 -0
- package/src/runtime/test/break-statement.test.ts +257 -0
- package/src/runtime/test/context-modes.test.ts +650 -0
- package/src/runtime/test/context.test.ts +466 -0
- package/src/runtime/test/core-functions.test.ts +228 -0
- package/src/runtime/test/e2e.test.ts +88 -0
- package/src/runtime/test/error-locations/error-locations.test.ts +80 -0
- package/src/runtime/test/error-locations/main-error.vibe +4 -0
- package/src/runtime/test/error-locations/main-import-error.vibe +3 -0
- package/src/runtime/test/error-locations/utils/helper.vibe +5 -0
- package/src/runtime/test/for-in.test.ts +312 -0
- package/src/runtime/test/helpers.ts +69 -0
- package/src/runtime/test/imports.test.ts +334 -0
- package/src/runtime/test/json-expressions.test.ts +232 -0
- package/src/runtime/test/literals.test.ts +372 -0
- package/src/runtime/test/logical-indexing.test.ts +478 -0
- package/src/runtime/test/member-methods.test.ts +324 -0
- package/src/runtime/test/model-config.test.ts +338 -0
- package/src/runtime/test/null-handling.test.ts +342 -0
- package/src/runtime/test/private-visibility.test.ts +332 -0
- package/src/runtime/test/runtime-state.test.ts +514 -0
- package/src/runtime/test/scoping.test.ts +370 -0
- package/src/runtime/test/string-interpolation.test.ts +354 -0
- package/src/runtime/test/template-literal.test.ts +181 -0
- package/src/runtime/test/tool-execution.test.ts +467 -0
- package/src/runtime/test/tool-schema-generation.test.ts +477 -0
- package/src/runtime/test/tostring.test.ts +210 -0
- package/src/runtime/test/ts-block.test.ts +594 -0
- package/src/runtime/test/ts-error-location.test.ts +231 -0
- package/src/runtime/test/types.test.ts +732 -0
- package/src/runtime/test/verbose-logger.test.ts +710 -0
- package/src/runtime/test/vibe-expression.test.ts +54 -0
- package/src/runtime/test/vibe-value-errors.test.ts +541 -0
- package/src/runtime/test/while.test.ts +232 -0
- package/src/runtime/tools/builtin.ts +30 -0
- package/src/runtime/tools/directory-tools.ts +70 -0
- package/src/runtime/tools/file-tools.ts +228 -0
- package/src/runtime/tools/index.ts +5 -0
- package/src/runtime/tools/registry.ts +48 -0
- package/src/runtime/tools/search-tools.ts +134 -0
- package/src/runtime/tools/security.ts +36 -0
- package/src/runtime/tools/system-tools.ts +312 -0
- package/src/runtime/tools/test/fixtures/base-types.ts +40 -0
- package/src/runtime/tools/test/fixtures/test-types.ts +132 -0
- package/src/runtime/tools/test/registry.test.ts +713 -0
- package/src/runtime/tools/test/security.test.ts +86 -0
- package/src/runtime/tools/test/system-tools.test.ts +679 -0
- package/src/runtime/tools/test/ts-schema.test.ts +357 -0
- package/src/runtime/tools/ts-schema.ts +341 -0
- package/src/runtime/tools/types.ts +89 -0
- package/src/runtime/tools/utility-tools.ts +198 -0
- package/src/runtime/ts-eval.ts +126 -0
- package/src/runtime/types.ts +797 -0
- package/src/runtime/validation.ts +160 -0
- package/src/runtime/verbose-logger.ts +459 -0
- package/src/runtime.ts +2 -0
- package/src/semantic/analyzer-context.ts +62 -0
- package/src/semantic/analyzer-validators.ts +575 -0
- package/src/semantic/analyzer-visitors.ts +534 -0
- package/src/semantic/analyzer.ts +83 -0
- package/src/semantic/index.ts +11 -0
- package/src/semantic/symbol-table.ts +58 -0
- package/src/semantic/test/async-validation.test.ts +301 -0
- package/src/semantic/test/compress-validation.test.ts +179 -0
- package/src/semantic/test/const-reassignment.test.ts +111 -0
- package/src/semantic/test/control-flow.test.ts +346 -0
- package/src/semantic/test/destructuring.test.ts +185 -0
- package/src/semantic/test/duplicate-declarations.test.ts +168 -0
- package/src/semantic/test/export-validation.test.ts +111 -0
- package/src/semantic/test/fixtures/math.ts +31 -0
- package/src/semantic/test/imports.test.ts +148 -0
- package/src/semantic/test/json-type.test.ts +68 -0
- package/src/semantic/test/literals.test.ts +127 -0
- package/src/semantic/test/model-validation.test.ts +179 -0
- package/src/semantic/test/prompt-validation.test.ts +343 -0
- package/src/semantic/test/scoping.test.ts +312 -0
- package/src/semantic/test/tool-validation.test.ts +306 -0
- package/src/semantic/test/ts-type-checking.test.ts +563 -0
- package/src/semantic/test/type-constraints.test.ts +111 -0
- package/src/semantic/test/type-inference.test.ts +87 -0
- package/src/semantic/test/type-validation.test.ts +552 -0
- package/src/semantic/test/undefined-variables.test.ts +163 -0
- package/src/semantic/ts-block-checker.ts +204 -0
- package/src/semantic/ts-signatures.ts +194 -0
- package/src/semantic/ts-types.ts +170 -0
- package/src/semantic/types.ts +58 -0
- package/tests/fixtures/conditional-logic.vibe +14 -0
- package/tests/fixtures/function-call.vibe +16 -0
- package/tests/fixtures/imports/cycle-detection/a.vibe +6 -0
- package/tests/fixtures/imports/cycle-detection/b.vibe +5 -0
- package/tests/fixtures/imports/cycle-detection/main.vibe +3 -0
- package/tests/fixtures/imports/module-isolation/main-b.vibe +8 -0
- package/tests/fixtures/imports/module-isolation/main.vibe +9 -0
- package/tests/fixtures/imports/module-isolation/moduleA.vibe +6 -0
- package/tests/fixtures/imports/module-isolation/moduleB.vibe +6 -0
- package/tests/fixtures/imports/nested-import/helper.vibe +6 -0
- package/tests/fixtures/imports/nested-import/main.vibe +3 -0
- package/tests/fixtures/imports/nested-import/utils.ts +3 -0
- package/tests/fixtures/imports/nested-isolation/file2.vibe +15 -0
- package/tests/fixtures/imports/nested-isolation/file3.vibe +10 -0
- package/tests/fixtures/imports/nested-isolation/main.vibe +21 -0
- package/tests/fixtures/imports/pure-cycle/a.vibe +5 -0
- package/tests/fixtures/imports/pure-cycle/b.vibe +5 -0
- package/tests/fixtures/imports/pure-cycle/main.vibe +3 -0
- package/tests/fixtures/imports/ts-boolean/checks.ts +14 -0
- package/tests/fixtures/imports/ts-boolean/main.vibe +10 -0
- package/tests/fixtures/imports/ts-boolean/type-mismatch.vibe +5 -0
- package/tests/fixtures/imports/ts-boolean/use-constant.vibe +18 -0
- package/tests/fixtures/imports/ts-error-handling/helpers.ts +42 -0
- package/tests/fixtures/imports/ts-error-handling/main.vibe +5 -0
- package/tests/fixtures/imports/ts-import/main.vibe +4 -0
- package/tests/fixtures/imports/ts-import/math.ts +9 -0
- package/tests/fixtures/imports/ts-variables/call-non-function.vibe +5 -0
- package/tests/fixtures/imports/ts-variables/data.ts +10 -0
- package/tests/fixtures/imports/ts-variables/import-json.vibe +5 -0
- package/tests/fixtures/imports/ts-variables/import-type-mismatch.vibe +5 -0
- package/tests/fixtures/imports/ts-variables/import-variable.vibe +5 -0
- package/tests/fixtures/imports/vibe-import/greet.vibe +5 -0
- package/tests/fixtures/imports/vibe-import/main.vibe +3 -0
- package/tests/fixtures/multiple-ai-calls.vibe +10 -0
- package/tests/fixtures/simple-greeting.vibe +6 -0
- package/tests/fixtures/template-literals.vibe +11 -0
- package/tests/integration/basic-ai/basic-ai.integration.test.ts +166 -0
- package/tests/integration/basic-ai/basic-ai.vibe +12 -0
- package/tests/integration/bug-fix/bug-fix.integration.test.ts +201 -0
- package/tests/integration/bug-fix/buggy-code.ts +22 -0
- package/tests/integration/bug-fix/fix-bug.vibe +21 -0
- package/tests/integration/compress/compress.integration.test.ts +206 -0
- package/tests/integration/destructuring/destructuring.integration.test.ts +92 -0
- package/tests/integration/hello-world-translator/hello-world-translator.integration.test.ts +61 -0
- package/tests/integration/line-annotator/context-modes.integration.test.ts +261 -0
- package/tests/integration/line-annotator/line-annotator.integration.test.ts +148 -0
- package/tests/integration/multi-feature/cumulative-sum.integration.test.ts +75 -0
- package/tests/integration/multi-feature/number-analyzer.integration.test.ts +191 -0
- package/tests/integration/multi-feature/number-analyzer.vibe +59 -0
- package/tests/integration/tool-calls/tool-calls.integration.test.ts +93 -0
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { parse } from '../../parser/parse';
|
|
3
|
+
import { Runtime, type AIProvider, type AIExecutionResult } from '../index';
|
|
4
|
+
|
|
5
|
+
// Event types for tracking execution order
|
|
6
|
+
type EventType =
|
|
7
|
+
| 'ai_call_start'
|
|
8
|
+
| 'ai_call_end'
|
|
9
|
+
| 'sync_op';
|
|
10
|
+
|
|
11
|
+
interface ExecutionEvent {
|
|
12
|
+
type: EventType;
|
|
13
|
+
id: string;
|
|
14
|
+
timestamp: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Create AI provider that logs precise execution events
|
|
18
|
+
function createOrderTrackingAI(
|
|
19
|
+
delayMs: number,
|
|
20
|
+
responses: Record<string, unknown>,
|
|
21
|
+
events: ExecutionEvent[]
|
|
22
|
+
): AIProvider {
|
|
23
|
+
return {
|
|
24
|
+
async execute(prompt: string): Promise<AIExecutionResult> {
|
|
25
|
+
// Extract ID from prompt for tracking
|
|
26
|
+
const id = prompt.replace(/[^a-zA-Z0-9_]/g, '_').substring(0, 20);
|
|
27
|
+
|
|
28
|
+
events.push({ type: 'ai_call_start', id, timestamp: Date.now() });
|
|
29
|
+
await Bun.sleep(delayMs);
|
|
30
|
+
events.push({ type: 'ai_call_end', id, timestamp: Date.now() });
|
|
31
|
+
|
|
32
|
+
// Find matching response
|
|
33
|
+
for (const [key, value] of Object.entries(responses)) {
|
|
34
|
+
if (prompt.includes(key)) {
|
|
35
|
+
return { value };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return { value: `response_${id}` };
|
|
39
|
+
},
|
|
40
|
+
async generateCode(): Promise<AIExecutionResult> {
|
|
41
|
+
return { value: '' };
|
|
42
|
+
},
|
|
43
|
+
async askUser(): Promise<string> {
|
|
44
|
+
return '';
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Helper to check if events overlap in time (parallel execution)
|
|
50
|
+
function eventsOverlap(events: ExecutionEvent[], id1: string, id2: string): boolean {
|
|
51
|
+
const start1 = events.find(e => e.type === 'ai_call_start' && e.id.includes(id1));
|
|
52
|
+
const end1 = events.find(e => e.type === 'ai_call_end' && e.id.includes(id1));
|
|
53
|
+
const start2 = events.find(e => e.type === 'ai_call_start' && e.id.includes(id2));
|
|
54
|
+
const end2 = events.find(e => e.type === 'ai_call_end' && e.id.includes(id2));
|
|
55
|
+
|
|
56
|
+
if (!start1 || !end1 || !start2 || !end2) return false;
|
|
57
|
+
|
|
58
|
+
// Overlap if one starts before the other ends
|
|
59
|
+
return (start1.timestamp < end2.timestamp && start2.timestamp < end1.timestamp);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Helper to check if event A started before event B ended
|
|
63
|
+
function startedBefore(events: ExecutionEvent[], idA: string, idB: string): boolean {
|
|
64
|
+
const startA = events.find(e => e.type === 'ai_call_start' && e.id.includes(idA));
|
|
65
|
+
const endB = events.find(e => e.type === 'ai_call_end' && e.id.includes(idB));
|
|
66
|
+
|
|
67
|
+
if (!startA || !endB) return false;
|
|
68
|
+
return startA.timestamp < endB.timestamp;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Helper to get event order
|
|
72
|
+
function getEventOrder(events: ExecutionEvent[]): string[] {
|
|
73
|
+
return events
|
|
74
|
+
.sort((a, b) => a.timestamp - b.timestamp)
|
|
75
|
+
.map(e => `${e.type}:${e.id}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
describe('Async Execution Order Verification', () => {
|
|
79
|
+
describe('parallel async operations start before any awaits', () => {
|
|
80
|
+
test('three async lets all START before any END (true parallelism)', async () => {
|
|
81
|
+
const events: ExecutionEvent[] = [];
|
|
82
|
+
|
|
83
|
+
const ast = parse(`
|
|
84
|
+
model m = { name: "test", apiKey: "key", url: "http://test" }
|
|
85
|
+
async let a = do "op_A" m default
|
|
86
|
+
async let b = do "op_B" m default
|
|
87
|
+
async let c = do "op_C" m default
|
|
88
|
+
let result = a + b + c
|
|
89
|
+
`);
|
|
90
|
+
|
|
91
|
+
const aiProvider = createOrderTrackingAI(100, {
|
|
92
|
+
'op_A': 'A',
|
|
93
|
+
'op_B': 'B',
|
|
94
|
+
'op_C': 'C',
|
|
95
|
+
}, events);
|
|
96
|
+
|
|
97
|
+
const runtime = new Runtime(ast, aiProvider);
|
|
98
|
+
await runtime.run();
|
|
99
|
+
|
|
100
|
+
// Get all start events
|
|
101
|
+
const starts = events.filter(e => e.type === 'ai_call_start');
|
|
102
|
+
const ends = events.filter(e => e.type === 'ai_call_end');
|
|
103
|
+
|
|
104
|
+
// All 3 operations should have started
|
|
105
|
+
expect(starts.length).toBe(3);
|
|
106
|
+
expect(ends.length).toBe(3);
|
|
107
|
+
|
|
108
|
+
// Key test: ALL operations should START before ANY operation ENDS
|
|
109
|
+
// This proves true parallel execution
|
|
110
|
+
const lastStartTime = Math.max(...starts.map(e => e.timestamp));
|
|
111
|
+
const firstEndTime = Math.min(...ends.map(e => e.timestamp));
|
|
112
|
+
|
|
113
|
+
expect(lastStartTime).toBeLessThan(firstEndTime);
|
|
114
|
+
|
|
115
|
+
// Verify overlapping execution
|
|
116
|
+
expect(eventsOverlap(events, 'op_A', 'op_B')).toBe(true);
|
|
117
|
+
expect(eventsOverlap(events, 'op_B', 'op_C')).toBe(true);
|
|
118
|
+
expect(eventsOverlap(events, 'op_A', 'op_C')).toBe(true);
|
|
119
|
+
|
|
120
|
+
expect(runtime.getValue('result')).toBe('ABC');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('async operations start immediately, not lazily on use', async () => {
|
|
124
|
+
const events: ExecutionEvent[] = [];
|
|
125
|
+
|
|
126
|
+
const ast = parse(`
|
|
127
|
+
model m = { name: "test", apiKey: "key", url: "http://test" }
|
|
128
|
+
async let a = do "start_A" m default
|
|
129
|
+
async let b = do "start_B" m default
|
|
130
|
+
`);
|
|
131
|
+
|
|
132
|
+
// Use longer delay to make timing clearer
|
|
133
|
+
const aiProvider = createOrderTrackingAI(150, {
|
|
134
|
+
'start_A': 'A',
|
|
135
|
+
'start_B': 'B',
|
|
136
|
+
}, events);
|
|
137
|
+
|
|
138
|
+
const runtime = new Runtime(ast, aiProvider);
|
|
139
|
+
await runtime.run();
|
|
140
|
+
|
|
141
|
+
const starts = events.filter(e => e.type === 'ai_call_start');
|
|
142
|
+
|
|
143
|
+
// Both should start almost immediately (within 50ms of each other)
|
|
144
|
+
// not lazily when the variable is used
|
|
145
|
+
const timeDiff = Math.abs(starts[0].timestamp - starts[1].timestamp);
|
|
146
|
+
expect(timeDiff).toBeLessThan(50);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('implicit await happens at correct point', () => {
|
|
151
|
+
test('await triggers when async variable is used in expression', async () => {
|
|
152
|
+
const events: ExecutionEvent[] = [];
|
|
153
|
+
let syncOpTime = 0;
|
|
154
|
+
|
|
155
|
+
const ast = parse(`
|
|
156
|
+
model m = { name: "test", apiKey: "key", url: "http://test" }
|
|
157
|
+
async let a = do "async_op" m default
|
|
158
|
+
let sync = "before_use"
|
|
159
|
+
let result = a + "_used"
|
|
160
|
+
`);
|
|
161
|
+
|
|
162
|
+
const aiProvider = createOrderTrackingAI(100, {
|
|
163
|
+
'async_op': 'ASYNC',
|
|
164
|
+
}, events);
|
|
165
|
+
|
|
166
|
+
const runtime = new Runtime(ast, aiProvider);
|
|
167
|
+
await runtime.run();
|
|
168
|
+
|
|
169
|
+
// The sync assignment should happen before async completes
|
|
170
|
+
// (async doesn't block sync operations)
|
|
171
|
+
const asyncStart = events.find(e => e.type === 'ai_call_start');
|
|
172
|
+
const asyncEnd = events.find(e => e.type === 'ai_call_end');
|
|
173
|
+
|
|
174
|
+
expect(asyncStart).toBeDefined();
|
|
175
|
+
expect(asyncEnd).toBeDefined();
|
|
176
|
+
|
|
177
|
+
// Result should be correct (await happened before concatenation)
|
|
178
|
+
expect(runtime.getValue('result')).toBe('ASYNC_used');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('multiple async variables await only when used', async () => {
|
|
182
|
+
const events: ExecutionEvent[] = [];
|
|
183
|
+
|
|
184
|
+
const ast = parse(`
|
|
185
|
+
model m = { name: "test", apiKey: "key", url: "http://test" }
|
|
186
|
+
async let a = do "first" m default
|
|
187
|
+
async let b = do "second" m default
|
|
188
|
+
let useA = a + "!"
|
|
189
|
+
let useB = b + "!"
|
|
190
|
+
`);
|
|
191
|
+
|
|
192
|
+
const aiProvider = createOrderTrackingAI(75, {
|
|
193
|
+
'first': 'FIRST',
|
|
194
|
+
'second': 'SECOND',
|
|
195
|
+
}, events);
|
|
196
|
+
|
|
197
|
+
const runtime = new Runtime(ast, aiProvider);
|
|
198
|
+
await runtime.run();
|
|
199
|
+
|
|
200
|
+
// Both should have completed
|
|
201
|
+
const ends = events.filter(e => e.type === 'ai_call_end');
|
|
202
|
+
expect(ends.length).toBe(2);
|
|
203
|
+
|
|
204
|
+
expect(runtime.getValue('useA')).toBe('FIRST!');
|
|
205
|
+
expect(runtime.getValue('useB')).toBe('SECOND!');
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe('sequential vs parallel execution patterns', () => {
|
|
210
|
+
test('sync do blocks sequentially, async do runs in parallel', async () => {
|
|
211
|
+
// First: sync pattern (should be sequential)
|
|
212
|
+
const syncEvents: ExecutionEvent[] = [];
|
|
213
|
+
const syncAst = parse(`
|
|
214
|
+
model m = { name: "test", apiKey: "key", url: "http://test" }
|
|
215
|
+
let a = do "sync_1" m default
|
|
216
|
+
let b = do "sync_2" m default
|
|
217
|
+
`);
|
|
218
|
+
|
|
219
|
+
const syncProvider = createOrderTrackingAI(50, {
|
|
220
|
+
'sync_1': 'S1',
|
|
221
|
+
'sync_2': 'S2',
|
|
222
|
+
}, syncEvents);
|
|
223
|
+
|
|
224
|
+
const syncRuntime = new Runtime(syncAst, syncProvider);
|
|
225
|
+
const syncStart = Date.now();
|
|
226
|
+
await syncRuntime.run();
|
|
227
|
+
const syncElapsed = Date.now() - syncStart;
|
|
228
|
+
|
|
229
|
+
// Sync: second should start AFTER first ends
|
|
230
|
+
const sync1End = syncEvents.find(e => e.type === 'ai_call_end' && e.id.includes('sync_1'));
|
|
231
|
+
const sync2Start = syncEvents.find(e => e.type === 'ai_call_start' && e.id.includes('sync_2'));
|
|
232
|
+
expect(sync2Start!.timestamp).toBeGreaterThanOrEqual(sync1End!.timestamp);
|
|
233
|
+
|
|
234
|
+
// Second: async pattern (should be parallel)
|
|
235
|
+
const asyncEvents: ExecutionEvent[] = [];
|
|
236
|
+
const asyncAst = parse(`
|
|
237
|
+
model m = { name: "test", apiKey: "key", url: "http://test" }
|
|
238
|
+
async let a = do "async_1" m default
|
|
239
|
+
async let b = do "async_2" m default
|
|
240
|
+
let result = a + b
|
|
241
|
+
`);
|
|
242
|
+
|
|
243
|
+
const asyncProvider = createOrderTrackingAI(50, {
|
|
244
|
+
'async_1': 'A1',
|
|
245
|
+
'async_2': 'A2',
|
|
246
|
+
}, asyncEvents);
|
|
247
|
+
|
|
248
|
+
const asyncRuntime = new Runtime(asyncAst, asyncProvider);
|
|
249
|
+
const asyncStart = Date.now();
|
|
250
|
+
await asyncRuntime.run();
|
|
251
|
+
const asyncElapsed = Date.now() - asyncStart;
|
|
252
|
+
|
|
253
|
+
// Async: should overlap
|
|
254
|
+
expect(eventsOverlap(asyncEvents, 'async_1', 'async_2')).toBe(true);
|
|
255
|
+
|
|
256
|
+
// Async should be significantly faster than sync
|
|
257
|
+
expect(asyncElapsed).toBeLessThan(syncElapsed);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test('dependent async operations with !{} expansion execute in correct order', async () => {
|
|
261
|
+
const events: ExecutionEvent[] = [];
|
|
262
|
+
|
|
263
|
+
// Both are async, but b depends on a via !{a} expansion
|
|
264
|
+
// !{a} expands the value into the prompt, creating a true dependency
|
|
265
|
+
const ast = parse(`
|
|
266
|
+
model m = { name: "test", apiKey: "key", url: "http://test" }
|
|
267
|
+
async let a = do "get_value" m default
|
|
268
|
+
async let b = do "use_!{a}" m default
|
|
269
|
+
`);
|
|
270
|
+
|
|
271
|
+
const aiProvider = createOrderTrackingAI(50, {
|
|
272
|
+
'get_value': 'VALUE',
|
|
273
|
+
'use_VALUE': 'RESULT',
|
|
274
|
+
}, events);
|
|
275
|
+
|
|
276
|
+
const runtime = new Runtime(ast, aiProvider);
|
|
277
|
+
await runtime.run();
|
|
278
|
+
|
|
279
|
+
// a should complete before b starts (b depends on a via !{a} expansion)
|
|
280
|
+
const aEnd = events.find(e => e.type === 'ai_call_end' && e.id.includes('get_value'));
|
|
281
|
+
const bStart = events.find(e => e.type === 'ai_call_start' && e.id.includes('use_'));
|
|
282
|
+
|
|
283
|
+
expect(aEnd).toBeDefined();
|
|
284
|
+
expect(bStart).toBeDefined();
|
|
285
|
+
expect(aEnd!.timestamp).toBeLessThanOrEqual(bStart!.timestamp);
|
|
286
|
+
|
|
287
|
+
expect(runtime.getValue('b')).toBe('RESULT');
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test('dependent async operations with {} reference execute in correct order', async () => {
|
|
291
|
+
const events: ExecutionEvent[] = [];
|
|
292
|
+
|
|
293
|
+
// Both are async, b references a via {a} (reference syntax)
|
|
294
|
+
// Even though {a} is left as literal in prompt, the context system needs
|
|
295
|
+
// the resolved value, so b should wait for a to complete
|
|
296
|
+
const ast = parse(`
|
|
297
|
+
model m = { name: "test", apiKey: "key", url: "http://test" }
|
|
298
|
+
async let a = do "get_value" m default
|
|
299
|
+
async let b = do "use_{a}" m default
|
|
300
|
+
`);
|
|
301
|
+
|
|
302
|
+
const aiProvider = createOrderTrackingAI(50, {
|
|
303
|
+
'get_value': 'VALUE',
|
|
304
|
+
// Note: prompt will be "use_{a}" (literal), not "use_VALUE"
|
|
305
|
+
'use_{a}': 'RESULT',
|
|
306
|
+
}, events);
|
|
307
|
+
|
|
308
|
+
const runtime = new Runtime(ast, aiProvider);
|
|
309
|
+
await runtime.run();
|
|
310
|
+
|
|
311
|
+
// a should complete before b starts (b references a, context needs resolved value)
|
|
312
|
+
const aEnd = events.find(e => e.type === 'ai_call_end' && e.id.includes('get_value'));
|
|
313
|
+
const bStart = events.find(e => e.type === 'ai_call_start' && e.id.includes('use_'));
|
|
314
|
+
|
|
315
|
+
expect(aEnd).toBeDefined();
|
|
316
|
+
expect(bStart).toBeDefined();
|
|
317
|
+
expect(aEnd!.timestamp).toBeLessThanOrEqual(bStart!.timestamp);
|
|
318
|
+
|
|
319
|
+
expect(runtime.getValue('b')).toBe('RESULT');
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
describe('execution order in functions', () => {
|
|
324
|
+
test('async operations in function body run in parallel', async () => {
|
|
325
|
+
const events: ExecutionEvent[] = [];
|
|
326
|
+
|
|
327
|
+
const ast = parse(`
|
|
328
|
+
model m = { name: "test", apiKey: "key", url: "http://test" }
|
|
329
|
+
|
|
330
|
+
function fetchBoth() {
|
|
331
|
+
async let x = do "func_X" m default
|
|
332
|
+
async let y = do "func_Y" m default
|
|
333
|
+
return x + y
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
let result = fetchBoth()
|
|
337
|
+
`);
|
|
338
|
+
|
|
339
|
+
const aiProvider = createOrderTrackingAI(75, {
|
|
340
|
+
'func_X': 'X',
|
|
341
|
+
'func_Y': 'Y',
|
|
342
|
+
}, events);
|
|
343
|
+
|
|
344
|
+
const runtime = new Runtime(ast, aiProvider);
|
|
345
|
+
await runtime.run();
|
|
346
|
+
|
|
347
|
+
// Both should overlap (parallel inside function)
|
|
348
|
+
expect(eventsOverlap(events, 'func_X', 'func_Y')).toBe(true);
|
|
349
|
+
expect(runtime.getValue('result')).toBe('XY');
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test('nested function calls maintain correct order', async () => {
|
|
353
|
+
const events: ExecutionEvent[] = [];
|
|
354
|
+
|
|
355
|
+
const ast = parse(`
|
|
356
|
+
model m = { name: "test", apiKey: "key", url: "http://test" }
|
|
357
|
+
|
|
358
|
+
function inner() {
|
|
359
|
+
async let i = do "inner_op" m default
|
|
360
|
+
return i
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function outer() {
|
|
364
|
+
let fromInner = inner()
|
|
365
|
+
async let o = do "outer_op" m default
|
|
366
|
+
return fromInner + o
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
let result = outer()
|
|
370
|
+
`);
|
|
371
|
+
|
|
372
|
+
const aiProvider = createOrderTrackingAI(50, {
|
|
373
|
+
'inner_op': 'I',
|
|
374
|
+
'outer_op': 'O',
|
|
375
|
+
}, events);
|
|
376
|
+
|
|
377
|
+
const runtime = new Runtime(ast, aiProvider);
|
|
378
|
+
await runtime.run();
|
|
379
|
+
|
|
380
|
+
// inner_op should complete before outer_op starts
|
|
381
|
+
// (because fromInner = inner() is sync assignment that needs inner to complete)
|
|
382
|
+
const innerEnd = events.find(e => e.type === 'ai_call_end' && e.id.includes('inner'));
|
|
383
|
+
const outerStart = events.find(e => e.type === 'ai_call_start' && e.id.includes('outer'));
|
|
384
|
+
|
|
385
|
+
expect(innerEnd).toBeDefined();
|
|
386
|
+
expect(outerStart).toBeDefined();
|
|
387
|
+
expect(innerEnd!.timestamp).toBeLessThanOrEqual(outerStart!.timestamp);
|
|
388
|
+
|
|
389
|
+
expect(runtime.getValue('result')).toBe('IO');
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
describe('execution order with loops', () => {
|
|
394
|
+
test('async in loop iterations - each iteration awaits before next', async () => {
|
|
395
|
+
const events: ExecutionEvent[] = [];
|
|
396
|
+
|
|
397
|
+
const ast = parse(`
|
|
398
|
+
model m = { name: "test", apiKey: "key", url: "http://test" }
|
|
399
|
+
let results = []
|
|
400
|
+
for i in [1, 2, 3] {
|
|
401
|
+
async let r = do "iter_!{i}" m default
|
|
402
|
+
results.push(r)
|
|
403
|
+
}
|
|
404
|
+
`);
|
|
405
|
+
|
|
406
|
+
const aiProvider = createOrderTrackingAI(30, {
|
|
407
|
+
'iter_1': 'R1',
|
|
408
|
+
'iter_2': 'R2',
|
|
409
|
+
'iter_3': 'R3',
|
|
410
|
+
}, events);
|
|
411
|
+
|
|
412
|
+
const runtime = new Runtime(ast, aiProvider);
|
|
413
|
+
await runtime.run();
|
|
414
|
+
|
|
415
|
+
// Each iteration should complete before the next starts
|
|
416
|
+
// (because results.push(r) uses r, triggering await)
|
|
417
|
+
const iter1End = events.find(e => e.type === 'ai_call_end' && e.id.includes('iter_1'));
|
|
418
|
+
const iter2Start = events.find(e => e.type === 'ai_call_start' && e.id.includes('iter_2'));
|
|
419
|
+
const iter2End = events.find(e => e.type === 'ai_call_end' && e.id.includes('iter_2'));
|
|
420
|
+
const iter3Start = events.find(e => e.type === 'ai_call_start' && e.id.includes('iter_3'));
|
|
421
|
+
|
|
422
|
+
expect(iter1End!.timestamp).toBeLessThanOrEqual(iter2Start!.timestamp);
|
|
423
|
+
expect(iter2End!.timestamp).toBeLessThanOrEqual(iter3Start!.timestamp);
|
|
424
|
+
|
|
425
|
+
expect(runtime.getValue('results')).toEqual(['R1', 'R2', 'R3']);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
test('multiple async in same iteration can run in parallel', async () => {
|
|
429
|
+
const events: ExecutionEvent[] = [];
|
|
430
|
+
|
|
431
|
+
const ast = parse(`
|
|
432
|
+
model m = { name: "test", apiKey: "key", url: "http://test" }
|
|
433
|
+
let results = []
|
|
434
|
+
for i in [1] {
|
|
435
|
+
async let a = do "loop_A" m default
|
|
436
|
+
async let b = do "loop_B" m default
|
|
437
|
+
results.push(a + b)
|
|
438
|
+
}
|
|
439
|
+
`);
|
|
440
|
+
|
|
441
|
+
const aiProvider = createOrderTrackingAI(50, {
|
|
442
|
+
'loop_A': 'A',
|
|
443
|
+
'loop_B': 'B',
|
|
444
|
+
}, events);
|
|
445
|
+
|
|
446
|
+
const runtime = new Runtime(ast, aiProvider);
|
|
447
|
+
await runtime.run();
|
|
448
|
+
|
|
449
|
+
// A and B should overlap within the same iteration
|
|
450
|
+
expect(eventsOverlap(events, 'loop_A', 'loop_B')).toBe(true);
|
|
451
|
+
expect(runtime.getValue('results')).toEqual(['AB']);
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
describe('block boundary awaits', () => {
|
|
456
|
+
test('if block awaits pending async before exiting', async () => {
|
|
457
|
+
const events: ExecutionEvent[] = [];
|
|
458
|
+
|
|
459
|
+
const ast = parse(`
|
|
460
|
+
model m = { name: "test", apiKey: "key", url: "http://test" }
|
|
461
|
+
let captured = ""
|
|
462
|
+
if true {
|
|
463
|
+
async let x = do "in_if_block" m default
|
|
464
|
+
captured = x
|
|
465
|
+
}
|
|
466
|
+
let after = "after_if"
|
|
467
|
+
`);
|
|
468
|
+
|
|
469
|
+
const aiProvider = createOrderTrackingAI(75, {
|
|
470
|
+
'in_if_block': 'IF_VALUE',
|
|
471
|
+
}, events);
|
|
472
|
+
|
|
473
|
+
const runtime = new Runtime(ast, aiProvider);
|
|
474
|
+
await runtime.run();
|
|
475
|
+
|
|
476
|
+
// x should be awaited before exiting if block
|
|
477
|
+
expect(runtime.getValue('captured')).toBe('IF_VALUE');
|
|
478
|
+
expect(runtime.getValue('after')).toBe('after_if');
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
test('for loop awaits pending async at end of each iteration', async () => {
|
|
482
|
+
const events: ExecutionEvent[] = [];
|
|
483
|
+
|
|
484
|
+
const ast = parse(`
|
|
485
|
+
model m = { name: "test", apiKey: "key", url: "http://test" }
|
|
486
|
+
let results = []
|
|
487
|
+
for i in [1, 2] {
|
|
488
|
+
async let x = do "iter_!{i}" m default
|
|
489
|
+
results.push(x)
|
|
490
|
+
}
|
|
491
|
+
`);
|
|
492
|
+
|
|
493
|
+
const aiProvider = createOrderTrackingAI(50, {
|
|
494
|
+
'iter_1': 'R1',
|
|
495
|
+
'iter_2': 'R2',
|
|
496
|
+
}, events);
|
|
497
|
+
|
|
498
|
+
const runtime = new Runtime(ast, aiProvider);
|
|
499
|
+
await runtime.run();
|
|
500
|
+
|
|
501
|
+
// Each iteration should await before next iteration
|
|
502
|
+
// So results should be in order
|
|
503
|
+
expect(runtime.getValue('results')).toEqual(['R1', 'R2']);
|
|
504
|
+
|
|
505
|
+
// Verify sequential execution (iter_1 ends before iter_2 starts)
|
|
506
|
+
const iter1End = events.find(e => e.type === 'ai_call_end' && e.id.includes('iter_1'));
|
|
507
|
+
const iter2Start = events.find(e => e.type === 'ai_call_start' && e.id.includes('iter_2'));
|
|
508
|
+
expect(iter1End).toBeDefined();
|
|
509
|
+
expect(iter2Start).toBeDefined();
|
|
510
|
+
expect(iter1End!.timestamp).toBeLessThanOrEqual(iter2Start!.timestamp);
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
test('while loop awaits pending async at end of each iteration', async () => {
|
|
514
|
+
const events: ExecutionEvent[] = [];
|
|
515
|
+
|
|
516
|
+
const ast = parse(`
|
|
517
|
+
model m = { name: "test", apiKey: "key", url: "http://test" }
|
|
518
|
+
let results = []
|
|
519
|
+
let i = 0
|
|
520
|
+
while i < 2 {
|
|
521
|
+
async let x = do "while_!{i}" m default
|
|
522
|
+
results.push(x)
|
|
523
|
+
i = i + 1
|
|
524
|
+
}
|
|
525
|
+
`);
|
|
526
|
+
|
|
527
|
+
const aiProvider = createOrderTrackingAI(50, {
|
|
528
|
+
'while_0': 'W0',
|
|
529
|
+
'while_1': 'W1',
|
|
530
|
+
}, events);
|
|
531
|
+
|
|
532
|
+
const runtime = new Runtime(ast, aiProvider);
|
|
533
|
+
await runtime.run();
|
|
534
|
+
|
|
535
|
+
expect(runtime.getValue('results')).toEqual(['W0', 'W1']);
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
test('nested blocks await at each level', async () => {
|
|
539
|
+
const events: ExecutionEvent[] = [];
|
|
540
|
+
|
|
541
|
+
const ast = parse(`
|
|
542
|
+
model m = { name: "test", apiKey: "key", url: "http://test" }
|
|
543
|
+
let outer_val = ""
|
|
544
|
+
let inner_val = ""
|
|
545
|
+
if true {
|
|
546
|
+
async let o = do "outer_async" m default
|
|
547
|
+
outer_val = o
|
|
548
|
+
if true {
|
|
549
|
+
async let i = do "inner_async" m default
|
|
550
|
+
inner_val = i
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
`);
|
|
554
|
+
|
|
555
|
+
const aiProvider = createOrderTrackingAI(50, {
|
|
556
|
+
'outer_async': 'OUTER',
|
|
557
|
+
'inner_async': 'INNER',
|
|
558
|
+
}, events);
|
|
559
|
+
|
|
560
|
+
const runtime = new Runtime(ast, aiProvider);
|
|
561
|
+
await runtime.run();
|
|
562
|
+
|
|
563
|
+
expect(runtime.getValue('outer_val')).toBe('OUTER');
|
|
564
|
+
expect(runtime.getValue('inner_val')).toBe('INNER');
|
|
565
|
+
});
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
describe('program completion awaits all pending', () => {
|
|
569
|
+
test('pending async operations complete before program ends', async () => {
|
|
570
|
+
const events: ExecutionEvent[] = [];
|
|
571
|
+
|
|
572
|
+
const ast = parse(`
|
|
573
|
+
model m = { name: "test", apiKey: "key", url: "http://test" }
|
|
574
|
+
async let a = do "fire_forget_A" m default
|
|
575
|
+
async let b = do "fire_forget_B" m default
|
|
576
|
+
`);
|
|
577
|
+
|
|
578
|
+
const aiProvider = createOrderTrackingAI(75, {
|
|
579
|
+
'fire_forget_A': 'A',
|
|
580
|
+
'fire_forget_B': 'B',
|
|
581
|
+
}, events);
|
|
582
|
+
|
|
583
|
+
const runtime = new Runtime(ast, aiProvider);
|
|
584
|
+
await runtime.run();
|
|
585
|
+
|
|
586
|
+
// Both should have completed (not left pending)
|
|
587
|
+
const ends = events.filter(e => e.type === 'ai_call_end');
|
|
588
|
+
expect(ends.length).toBe(2);
|
|
589
|
+
|
|
590
|
+
// Values should be set
|
|
591
|
+
expect(runtime.getValue('a')).toBe('A');
|
|
592
|
+
expect(runtime.getValue('b')).toBe('B');
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
test('standalone async (fire-and-forget) completes before program ends', async () => {
|
|
596
|
+
const events: ExecutionEvent[] = [];
|
|
597
|
+
|
|
598
|
+
const ast = parse(`
|
|
599
|
+
model m = { name: "test", apiKey: "key", url: "http://test" }
|
|
600
|
+
async do "standalone_op" m default
|
|
601
|
+
let x = "done"
|
|
602
|
+
`);
|
|
603
|
+
|
|
604
|
+
const aiProvider = createOrderTrackingAI(50, {
|
|
605
|
+
'standalone_op': 'ignored',
|
|
606
|
+
}, events);
|
|
607
|
+
|
|
608
|
+
const runtime = new Runtime(ast, aiProvider);
|
|
609
|
+
await runtime.run();
|
|
610
|
+
|
|
611
|
+
// The standalone async should have completed
|
|
612
|
+
const ends = events.filter(e => e.type === 'ai_call_end');
|
|
613
|
+
expect(ends.length).toBe(1);
|
|
614
|
+
|
|
615
|
+
expect(runtime.getValue('x')).toBe('done');
|
|
616
|
+
});
|
|
617
|
+
});
|
|
618
|
+
});
|