freelang-v4 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +548 -0
- package/dist/ast.d.ts +367 -0
- package/dist/ast.js +4 -0
- package/dist/ast.js.map +1 -0
- package/dist/async-basic.test.d.ts +1 -0
- package/dist/async-basic.test.js +88 -0
- package/dist/async-basic.test.js.map +1 -0
- package/dist/async-jest.test.d.ts +1 -0
- package/dist/async-jest.test.js +99 -0
- package/dist/async-jest.test.js.map +1 -0
- package/dist/channel-jest.test.d.ts +1 -0
- package/dist/channel-jest.test.js +148 -0
- package/dist/channel-jest.test.js.map +1 -0
- package/dist/checker-jest.test.d.ts +1 -0
- package/dist/checker-jest.test.js +160 -0
- package/dist/checker-jest.test.js.map +1 -0
- package/dist/checker.d.ts +149 -0
- package/dist/checker.js +1565 -0
- package/dist/checker.js.map +1 -0
- package/dist/checker.test.d.ts +1 -0
- package/dist/checker.test.js +217 -0
- package/dist/checker.test.js.map +1 -0
- package/dist/compiler-jest.test.d.ts +1 -0
- package/dist/compiler-jest.test.js +233 -0
- package/dist/compiler-jest.test.js.map +1 -0
- package/dist/compiler.d.ts +127 -0
- package/dist/compiler.js +1588 -0
- package/dist/compiler.js.map +1 -0
- package/dist/compiler.test.d.ts +1 -0
- package/dist/compiler.test.js +313 -0
- package/dist/compiler.test.js.map +1 -0
- package/dist/db-100m-full.d.ts +5 -0
- package/dist/db-100m-full.js +78 -0
- package/dist/db-100m-full.js.map +1 -0
- package/dist/db-100m-no-index.d.ts +12 -0
- package/dist/db-100m-no-index.js +119 -0
- package/dist/db-100m-no-index.js.map +1 -0
- package/dist/db-100m-real.d.ts +5 -0
- package/dist/db-100m-real.js +131 -0
- package/dist/db-100m-real.js.map +1 -0
- package/dist/db-100m-streaming.d.ts +15 -0
- package/dist/db-100m-streaming.js +164 -0
- package/dist/db-100m-streaming.js.map +1 -0
- package/dist/db-100m-test.d.ts +5 -0
- package/dist/db-100m-test.js +111 -0
- package/dist/db-100m-test.js.map +1 -0
- package/dist/db-jest.test.d.ts +1 -0
- package/dist/db-jest.test.js +182 -0
- package/dist/db-jest.test.js.map +1 -0
- package/dist/db-runtime.d.ts +24 -0
- package/dist/db-runtime.js +204 -0
- package/dist/db-runtime.js.map +1 -0
- package/dist/db.d.ts +249 -0
- package/dist/db.js +593 -0
- package/dist/db.js.map +1 -0
- package/dist/file-io-jest.test.d.ts +1 -0
- package/dist/file-io-jest.test.js +225 -0
- package/dist/file-io-jest.test.js.map +1 -0
- package/dist/for-of-jest.test.d.ts +1 -0
- package/dist/for-of-jest.test.js +230 -0
- package/dist/for-of-jest.test.js.map +1 -0
- package/dist/for-of.test.d.ts +1 -0
- package/dist/for-of.test.js +305 -0
- package/dist/for-of.test.js.map +1 -0
- package/dist/function-literal-jest.test.d.ts +1 -0
- package/dist/function-literal-jest.test.js +180 -0
- package/dist/function-literal-jest.test.js.map +1 -0
- package/dist/function-literal.test.d.ts +1 -0
- package/dist/function-literal.test.js +245 -0
- package/dist/function-literal.test.js.map +1 -0
- package/dist/generics-jest.test.d.ts +1 -0
- package/dist/generics-jest.test.js +93 -0
- package/dist/generics-jest.test.js.map +1 -0
- package/dist/ir-gen.d.ts +15 -0
- package/dist/ir-gen.js +400 -0
- package/dist/ir-gen.js.map +1 -0
- package/dist/ir.d.ts +114 -0
- package/dist/ir.js +5 -0
- package/dist/ir.js.map +1 -0
- package/dist/lexer.d.ts +110 -0
- package/dist/lexer.js +467 -0
- package/dist/lexer.js.map +1 -0
- package/dist/lexer.test.d.ts +1 -0
- package/dist/lexer.test.js +426 -0
- package/dist/lexer.test.js.map +1 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +241 -0
- package/dist/main.js.map +1 -0
- package/dist/module-jest.test.d.ts +1 -0
- package/dist/module-jest.test.js +123 -0
- package/dist/module-jest.test.js.map +1 -0
- package/dist/parser.d.ts +56 -0
- package/dist/parser.js +1060 -0
- package/dist/parser.js.map +1 -0
- package/dist/parser.test.d.ts +1 -0
- package/dist/parser.test.js +461 -0
- package/dist/parser.test.js.map +1 -0
- package/dist/pattern-matching-jest.test.d.ts +1 -0
- package/dist/pattern-matching-jest.test.js +158 -0
- package/dist/pattern-matching-jest.test.js.map +1 -0
- package/dist/pkg/init.d.ts +1 -0
- package/dist/pkg/init.js +118 -0
- package/dist/pkg/init.js.map +1 -0
- package/dist/pkg/install.d.ts +1 -0
- package/dist/pkg/install.js +77 -0
- package/dist/pkg/install.js.map +1 -0
- package/dist/pkg/registry.d.ts +23 -0
- package/dist/pkg/registry.js +106 -0
- package/dist/pkg/registry.js.map +1 -0
- package/dist/pkg/run.d.ts +1 -0
- package/dist/pkg/run.js +76 -0
- package/dist/pkg/run.js.map +1 -0
- package/dist/pkg/toml.d.ts +5 -0
- package/dist/pkg/toml.js +117 -0
- package/dist/pkg/toml.js.map +1 -0
- package/dist/repl.d.ts +15 -0
- package/dist/repl.js +197 -0
- package/dist/repl.js.map +1 -0
- package/dist/runtime/bytecode.d.ts +92 -0
- package/dist/runtime/bytecode.js +253 -0
- package/dist/runtime/bytecode.js.map +1 -0
- package/dist/runtime/value.d.ts +102 -0
- package/dist/runtime/value.js +302 -0
- package/dist/runtime/value.js.map +1 -0
- package/dist/runtime/vm.d.ts +65 -0
- package/dist/runtime/vm.js +293 -0
- package/dist/runtime/vm.js.map +1 -0
- package/dist/struct-instance-jest.test.d.ts +1 -0
- package/dist/struct-instance-jest.test.js +209 -0
- package/dist/struct-instance-jest.test.js.map +1 -0
- package/dist/struct-instance.test.d.ts +1 -0
- package/dist/struct-instance.test.js +291 -0
- package/dist/struct-instance.test.js.map +1 -0
- package/dist/struct-jest.test.d.ts +1 -0
- package/dist/struct-jest.test.js +176 -0
- package/dist/struct-jest.test.js.map +1 -0
- package/dist/struct.test.d.ts +1 -0
- package/dist/struct.test.js +231 -0
- package/dist/struct.test.js.map +1 -0
- package/dist/trait-jest.test.d.ts +1 -0
- package/dist/trait-jest.test.js +120 -0
- package/dist/trait-jest.test.js.map +1 -0
- package/dist/vm-jest.test.d.ts +1 -0
- package/dist/vm-jest.test.js +569 -0
- package/dist/vm-jest.test.js.map +1 -0
- package/dist/vm.d.ts +81 -0
- package/dist/vm.js +1956 -0
- package/dist/vm.js.map +1 -0
- package/dist/vm.test.d.ts +1 -0
- package/dist/vm.test.js +337 -0
- package/dist/vm.test.js.map +1 -0
- package/dist/web-repl/sandbox.d.ts +11 -0
- package/dist/web-repl/sandbox.js +76 -0
- package/dist/web-repl/sandbox.js.map +1 -0
- package/dist/web-repl/server.d.ts +1 -0
- package/dist/web-repl/server.js +111 -0
- package/dist/web-repl/server.js.map +1 -0
- package/dist/while-loop-jest.test.d.ts +1 -0
- package/dist/while-loop-jest.test.js +201 -0
- package/dist/while-loop-jest.test.js.map +1 -0
- package/dist/while-loop.test.d.ts +1 -0
- package/dist/while-loop.test.js +262 -0
- package/dist/while-loop.test.js.map +1 -0
- package/docs/EXPERIENCE.md +787 -0
- package/docs/README.md +175 -0
- package/docs/V1_V2_V3_ANALYSIS.md +107 -0
- package/docs/_config.yml +36 -0
- package/docs/api-reference.md +459 -0
- package/docs/architecture.md +470 -0
- package/docs/benchmarks.md +295 -0
- package/docs/comparison.md +454 -0
- package/docs/index.md +335 -0
- package/docs/language-completeness.md +228 -0
- package/docs/learning-guide.md +651 -0
- package/package.json +65 -0
- package/src/api/deploy_key.fl +294 -0
- package/src/api/issue.fl +302 -0
- package/src/api/org.fl +356 -0
- package/src/api/repo.fl +394 -0
- package/src/api/team.fl +299 -0
- package/src/api/user.fl +385 -0
- package/src/api/webhook.fl +273 -0
- package/src/ast.ts +158 -0
- package/src/async-basic.test.ts +94 -0
- package/src/async-jest.test.ts +107 -0
- package/src/channel-jest.test.ts +158 -0
- package/src/checker-jest.test.ts +189 -0
- package/src/checker.test.ts +279 -0
- package/src/checker.ts +1861 -0
- package/src/commands/analyze.fl +227 -0
- package/src/commands/auth.fl +315 -0
- package/src/commands/batch.fl +349 -0
- package/src/commands/config.fl +199 -0
- package/src/commands/deploy_key.fl +352 -0
- package/src/commands/issue.fl +275 -0
- package/src/commands/main.fl +492 -0
- package/src/commands/org.fl +425 -0
- package/src/commands/repo.fl +581 -0
- package/src/commands/team.fl +244 -0
- package/src/commands/user.fl +423 -0
- package/src/commands/webhook.fl +400 -0
- package/src/compiler-jest.test.ts +275 -0
- package/src/compiler.test.ts +375 -0
- package/src/compiler.ts +1770 -0
- package/src/config.fl +175 -0
- package/src/core/batch.fl +355 -0
- package/src/core/cache.fl +284 -0
- package/src/core/ensure.fl +324 -0
- package/src/db-100m-full.ts +96 -0
- package/src/db-100m-no-index.ts +133 -0
- package/src/db-100m-real.ts +152 -0
- package/src/db-100m-streaming.ts +154 -0
- package/src/db-100m-test.ts +136 -0
- package/src/db-jest.test.ts +161 -0
- package/src/db-runtime.ts +242 -0
- package/src/db.ts +676 -0
- package/src/errors.fl +134 -0
- package/src/for-of-jest.test.ts +246 -0
- package/src/for-of.test.ts +308 -0
- package/src/function-literal-jest.test.ts +193 -0
- package/src/function-literal.test.ts +248 -0
- package/src/generics-jest.test.ts +104 -0
- package/src/http/client.fl +327 -0
- package/src/ir-gen.ts +459 -0
- package/src/ir.ts +80 -0
- package/src/lexer.test.ts +499 -0
- package/src/lexer.ts +522 -0
- package/src/main.ts +223 -0
- package/src/models.fl +162 -0
- package/src/module-jest.test.ts +145 -0
- package/src/parser.test.ts +542 -0
- package/src/parser.ts +1211 -0
- package/src/pattern-matching-jest.test.ts +170 -0
- package/src/pkg/init.ts +91 -0
- package/src/pkg/install.ts +56 -0
- package/src/pkg/registry.ts +103 -0
- package/src/pkg/run.ts +49 -0
- package/src/pkg/toml.ts +129 -0
- package/src/repl.ts +190 -0
- package/src/runtime/bytecode.ts +291 -0
- package/src/runtime/value.ts +322 -0
- package/src/runtime/vm.ts +354 -0
- package/src/self-host/bootstrap.fl +68 -0
- package/src/self-host/interpreter.fl +361 -0
- package/src/self-host/lexer-simple.fl +22 -0
- package/src/self-host/lexer.fl +305 -0
- package/src/self-host/parser.fl +580 -0
- package/src/struct-instance-jest.test.ts +221 -0
- package/src/struct-instance.test.ts +293 -0
- package/src/struct-jest.test.ts +187 -0
- package/src/struct.test.ts +234 -0
- package/src/trait-jest.test.ts +136 -0
- package/src/vm-jest.test.ts +754 -0
- package/src/vm.ts +1976 -0
- package/src/web-repl/public/index.html +50 -0
- package/src/web-repl/public/main.js +105 -0
- package/src/web-repl/public/style.css +225 -0
- package/src/web-repl/sandbox.ts +88 -0
- package/src/web-repl/server.ts +97 -0
- package/src/while-loop-jest.test.ts +218 -0
- package/src/while-loop.test.ts +267 -0
package/src/vm.ts
ADDED
|
@@ -0,0 +1,1976 @@
|
|
|
1
|
+
// FreeLang v4 — Stack VM (SPEC_02 구현)
|
|
2
|
+
// fetch-decode-execute + Actor cooperative scheduling
|
|
3
|
+
|
|
4
|
+
import { Op, Chunk, FuncInfo } from "./compiler";
|
|
5
|
+
import * as crypto from "crypto";
|
|
6
|
+
import * as fs from "fs";
|
|
7
|
+
import { SQLiteDB, DBAdapter } from "./db";
|
|
8
|
+
|
|
9
|
+
// ============================================================
|
|
10
|
+
// Value (SPEC_02 Q3)
|
|
11
|
+
// ============================================================
|
|
12
|
+
|
|
13
|
+
export type Value =
|
|
14
|
+
| { tag: "i32"; val: number }
|
|
15
|
+
| { tag: "f64"; val: number }
|
|
16
|
+
| { tag: "str"; val: string }
|
|
17
|
+
| { tag: "bool"; val: boolean }
|
|
18
|
+
| { tag: "arr"; val: Value[] }
|
|
19
|
+
| { tag: "struct"; fields: Map<string, Value> }
|
|
20
|
+
| { tag: "ok"; val: Value }
|
|
21
|
+
| { tag: "err"; val: Value }
|
|
22
|
+
| { tag: "some"; val: Value }
|
|
23
|
+
| { tag: "none" }
|
|
24
|
+
| { tag: "chan"; id: number }
|
|
25
|
+
| { tag: "db"; id: number }
|
|
26
|
+
| { tag: "void" };
|
|
27
|
+
|
|
28
|
+
// ============================================================
|
|
29
|
+
// CallFrame (SPEC_02 Q5)
|
|
30
|
+
// ============================================================
|
|
31
|
+
|
|
32
|
+
type CallFrame = {
|
|
33
|
+
returnAddr: number;
|
|
34
|
+
baseSlot: number;
|
|
35
|
+
locals: Value[];
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// ============================================================
|
|
39
|
+
// Channel
|
|
40
|
+
// ============================================================
|
|
41
|
+
|
|
42
|
+
type Channel = {
|
|
43
|
+
id: number;
|
|
44
|
+
buffer: Value[];
|
|
45
|
+
waitingRecv: number[]; // actor ids waiting to recv
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// ============================================================
|
|
49
|
+
// Actor (SPEC_02 Q7)
|
|
50
|
+
// ============================================================
|
|
51
|
+
|
|
52
|
+
type Actor = {
|
|
53
|
+
id: number;
|
|
54
|
+
ip: number;
|
|
55
|
+
stack: Value[];
|
|
56
|
+
frames: CallFrame[];
|
|
57
|
+
state: "running" | "waiting" | "done";
|
|
58
|
+
waitingChan: number | null;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// ============================================================
|
|
62
|
+
// VM
|
|
63
|
+
// ============================================================
|
|
64
|
+
|
|
65
|
+
export class VM {
|
|
66
|
+
private chunk!: Chunk;
|
|
67
|
+
private actors: Actor[] = [];
|
|
68
|
+
private channels: Map<number, Channel> = new Map();
|
|
69
|
+
private nextChannelId: number = 0;
|
|
70
|
+
private databases: Map<number, DBAdapter> = new Map();
|
|
71
|
+
private nextDbId: number = 0;
|
|
72
|
+
private httpServers: Map<number, any> = new Map(); // {app, server, requestQueue, nextReqId}
|
|
73
|
+
private nextServerId: number = 0;
|
|
74
|
+
private nextReqId: number = 0;
|
|
75
|
+
private globals: Map<string, Value> = new Map();
|
|
76
|
+
private output: string[] = [];
|
|
77
|
+
private instructionCount: number = 0;
|
|
78
|
+
private maxInstructions: number = 1_000_000;
|
|
79
|
+
private runningCount: number = 0;
|
|
80
|
+
|
|
81
|
+
async run(chunk: Chunk): Promise<{ output: string[]; error: string | null }> {
|
|
82
|
+
this.chunk = chunk;
|
|
83
|
+
this.output = [];
|
|
84
|
+
this.instructionCount = 0;
|
|
85
|
+
|
|
86
|
+
// main actor
|
|
87
|
+
this.actors = [{
|
|
88
|
+
id: 0,
|
|
89
|
+
ip: 0,
|
|
90
|
+
stack: [],
|
|
91
|
+
frames: [{ returnAddr: -1, baseSlot: 0, locals: [] }],
|
|
92
|
+
state: "running",
|
|
93
|
+
waitingChan: null,
|
|
94
|
+
}];
|
|
95
|
+
this.runningCount = 1;
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
await this.schedule();
|
|
99
|
+
return { output: this.output, error: null };
|
|
100
|
+
} catch (e: any) {
|
|
101
|
+
return { output: this.output, error: e.message || String(e) };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ============================================================
|
|
106
|
+
// Scheduler — round-robin (SPEC_02 Q7)
|
|
107
|
+
// ============================================================
|
|
108
|
+
|
|
109
|
+
private async schedule(): Promise<void> {
|
|
110
|
+
const SLICE = 1000;
|
|
111
|
+
let current = 0;
|
|
112
|
+
|
|
113
|
+
while (this.runningCount > 0) {
|
|
114
|
+
const actor = this.actors[current];
|
|
115
|
+
|
|
116
|
+
if (actor.state === "running") {
|
|
117
|
+
await this.runSlice(actor, SLICE);
|
|
118
|
+
} else if (actor.state === "waiting" && actor.waitingChan !== null) {
|
|
119
|
+
const chan = this.channels.get(actor.waitingChan);
|
|
120
|
+
if (chan && chan.buffer.length > 0) {
|
|
121
|
+
const val = chan.buffer.shift()!;
|
|
122
|
+
actor.stack.push({ tag: "ok", val });
|
|
123
|
+
actor.state = "running";
|
|
124
|
+
actor.waitingChan = null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
current = (current + 1) % this.actors.length;
|
|
129
|
+
|
|
130
|
+
if (this.instructionCount > this.maxInstructions) {
|
|
131
|
+
throw new Error("execution limit exceeded (infinite loop?)");
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ============================================================
|
|
137
|
+
// Execute slice
|
|
138
|
+
// ============================================================
|
|
139
|
+
|
|
140
|
+
private async runSlice(actor: Actor, maxOps: number): Promise<void> {
|
|
141
|
+
let ops = 0;
|
|
142
|
+
|
|
143
|
+
while (ops < maxOps && actor.state === "running") {
|
|
144
|
+
if (actor.ip >= this.chunk.code.length) {
|
|
145
|
+
actor.state = "done";
|
|
146
|
+
this.runningCount--;
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const op = this.chunk.code[actor.ip++];
|
|
151
|
+
this.instructionCount++;
|
|
152
|
+
ops++;
|
|
153
|
+
|
|
154
|
+
// currentFrame 캐시 (CALL/RETURN에서 업데이트)
|
|
155
|
+
let currentFrame = actor.frames[actor.frames.length - 1];
|
|
156
|
+
|
|
157
|
+
switch (op) {
|
|
158
|
+
// ---- 상수 로드 ----
|
|
159
|
+
case Op.PUSH_I32: {
|
|
160
|
+
const val = this.readI32(actor);
|
|
161
|
+
actor.stack.push({ tag: "i32", val });
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
case Op.PUSH_F64: {
|
|
165
|
+
const val = this.readF64(actor);
|
|
166
|
+
actor.stack.push({ tag: "f64", val });
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
case Op.PUSH_STR: {
|
|
170
|
+
const idx = this.readI32(actor);
|
|
171
|
+
actor.stack.push({ tag: "str", val: this.chunk.constants[idx] });
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
case Op.PUSH_TRUE:
|
|
175
|
+
actor.stack.push({ tag: "bool", val: true });
|
|
176
|
+
break;
|
|
177
|
+
case Op.PUSH_FALSE:
|
|
178
|
+
actor.stack.push({ tag: "bool", val: false });
|
|
179
|
+
break;
|
|
180
|
+
case Op.PUSH_VOID:
|
|
181
|
+
actor.stack.push({ tag: "void" });
|
|
182
|
+
break;
|
|
183
|
+
case Op.PUSH_NONE:
|
|
184
|
+
actor.stack.push({ tag: "none" });
|
|
185
|
+
break;
|
|
186
|
+
case Op.POP:
|
|
187
|
+
actor.stack.pop();
|
|
188
|
+
break;
|
|
189
|
+
case Op.DUP:
|
|
190
|
+
actor.stack.push(actor.stack[actor.stack.length - 1]);
|
|
191
|
+
break;
|
|
192
|
+
|
|
193
|
+
// ---- 산술 (i32) ----
|
|
194
|
+
case Op.ADD_I32: {
|
|
195
|
+
const b = actor.stack.pop()!;
|
|
196
|
+
const a = actor.stack.pop()!;
|
|
197
|
+
if (a.tag === "str" && b.tag === "str") {
|
|
198
|
+
actor.stack.push({ tag: "str", val: a.val + b.val });
|
|
199
|
+
} else {
|
|
200
|
+
actor.stack.push({ tag: a.tag as any, val: (a as any).val + (b as any).val });
|
|
201
|
+
}
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
case Op.SUB_I32: {
|
|
205
|
+
const b = actor.stack.pop()!;
|
|
206
|
+
const a = actor.stack.pop()!;
|
|
207
|
+
actor.stack.push({ tag: a.tag as any, val: (a as any).val - (b as any).val });
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
case Op.MUL_I32: {
|
|
211
|
+
const b = actor.stack.pop()!;
|
|
212
|
+
const a = actor.stack.pop()!;
|
|
213
|
+
actor.stack.push({ tag: a.tag as any, val: (a as any).val * (b as any).val });
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
case Op.DIV_I32: {
|
|
217
|
+
const b = actor.stack.pop()!;
|
|
218
|
+
const a = actor.stack.pop()!;
|
|
219
|
+
if ((b as any).val === 0) throw new Error("panic: division by zero");
|
|
220
|
+
const result = a.tag === "i32"
|
|
221
|
+
? Math.trunc((a as any).val / (b as any).val)
|
|
222
|
+
: (a as any).val / (b as any).val;
|
|
223
|
+
actor.stack.push({ tag: a.tag as any, val: result });
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
case Op.MOD_I32: {
|
|
227
|
+
const b = actor.stack.pop()!;
|
|
228
|
+
const a = actor.stack.pop()!;
|
|
229
|
+
if ((b as any).val === 0) throw new Error("panic: division by zero");
|
|
230
|
+
actor.stack.push({ tag: a.tag as any, val: (a as any).val % (b as any).val });
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
case Op.NEG_I32: {
|
|
234
|
+
const a = actor.stack.pop()!;
|
|
235
|
+
actor.stack.push({ tag: a.tag as any, val: -(a as any).val });
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ---- f64 산술 ----
|
|
240
|
+
case Op.ADD_F64: {
|
|
241
|
+
const b = actor.stack.pop()!;
|
|
242
|
+
const a = actor.stack.pop()!;
|
|
243
|
+
if (a.tag === "str" && b.tag === "str") {
|
|
244
|
+
actor.stack.push({ tag: "str", val: a.val + b.val });
|
|
245
|
+
} else {
|
|
246
|
+
actor.stack.push({ tag: "f64", val: (a as any).val + (b as any).val });
|
|
247
|
+
}
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
case Op.SUB_F64: {
|
|
251
|
+
const b = actor.stack.pop()!;
|
|
252
|
+
const a = actor.stack.pop()!;
|
|
253
|
+
actor.stack.push({ tag: "f64", val: (a as any).val - (b as any).val });
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
case Op.MUL_F64: {
|
|
257
|
+
const b = actor.stack.pop()!;
|
|
258
|
+
const a = actor.stack.pop()!;
|
|
259
|
+
actor.stack.push({ tag: "f64", val: (a as any).val * (b as any).val });
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
case Op.DIV_F64: {
|
|
263
|
+
const b = actor.stack.pop()!;
|
|
264
|
+
const a = actor.stack.pop()!;
|
|
265
|
+
if ((b as any).val === 0) throw new Error("panic: division by zero");
|
|
266
|
+
actor.stack.push({ tag: "f64", val: (a as any).val / (b as any).val });
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
case Op.MOD_F64: {
|
|
270
|
+
const b = actor.stack.pop()!;
|
|
271
|
+
const a = actor.stack.pop()!;
|
|
272
|
+
if ((b as any).val === 0) throw new Error("panic: division by zero");
|
|
273
|
+
actor.stack.push({ tag: "f64", val: (a as any).val % (b as any).val });
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
case Op.NEG_F64: {
|
|
277
|
+
const a = actor.stack.pop()!;
|
|
278
|
+
actor.stack.push({ tag: "f64", val: -(a as any).val });
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ---- 비교 ----
|
|
283
|
+
case Op.EQ: {
|
|
284
|
+
const b = actor.stack.pop()!;
|
|
285
|
+
const a = actor.stack.pop()!;
|
|
286
|
+
actor.stack.push({ tag: "bool", val: this.valuesEqual(a, b) });
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
case Op.NEQ: {
|
|
290
|
+
const b = actor.stack.pop()!;
|
|
291
|
+
const a = actor.stack.pop()!;
|
|
292
|
+
actor.stack.push({ tag: "bool", val: !this.valuesEqual(a, b) });
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
case Op.LT: {
|
|
296
|
+
const b = actor.stack.pop()!;
|
|
297
|
+
const a = actor.stack.pop()!;
|
|
298
|
+
actor.stack.push({ tag: "bool", val: (a as any).val < (b as any).val });
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
case Op.GT: {
|
|
302
|
+
const b = actor.stack.pop()!;
|
|
303
|
+
const a = actor.stack.pop()!;
|
|
304
|
+
actor.stack.push({ tag: "bool", val: (a as any).val > (b as any).val });
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
case Op.LTEQ: {
|
|
308
|
+
const b = actor.stack.pop()!;
|
|
309
|
+
const a = actor.stack.pop()!;
|
|
310
|
+
actor.stack.push({ tag: "bool", val: (a as any).val <= (b as any).val });
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
case Op.GTEQ: {
|
|
314
|
+
const b = actor.stack.pop()!;
|
|
315
|
+
const a = actor.stack.pop()!;
|
|
316
|
+
actor.stack.push({ tag: "bool", val: (a as any).val >= (b as any).val });
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ---- 논리 ----
|
|
321
|
+
case Op.AND: {
|
|
322
|
+
const b = actor.stack.pop()!;
|
|
323
|
+
const a = actor.stack.pop()!;
|
|
324
|
+
actor.stack.push({ tag: "bool", val: (a as any).val && (b as any).val });
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
case Op.OR: {
|
|
328
|
+
const b = actor.stack.pop()!;
|
|
329
|
+
const a = actor.stack.pop()!;
|
|
330
|
+
actor.stack.push({ tag: "bool", val: (a as any).val || (b as any).val });
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
case Op.NOT: {
|
|
334
|
+
const a = actor.stack.pop()!;
|
|
335
|
+
actor.stack.push({ tag: "bool", val: !(a as any).val });
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ---- 변수 ----
|
|
340
|
+
case Op.LOAD_LOCAL: {
|
|
341
|
+
const slot = this.readI32(actor);
|
|
342
|
+
const frame = actor.frames[actor.frames.length - 1];
|
|
343
|
+
actor.stack.push(frame.locals[slot] ?? { tag: "void" });
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
case Op.STORE_LOCAL: {
|
|
347
|
+
const slot = this.readI32(actor);
|
|
348
|
+
const val = actor.stack.pop()!;
|
|
349
|
+
const frame = actor.frames[actor.frames.length - 1];
|
|
350
|
+
while (frame.locals.length <= slot) frame.locals.push({ tag: "void" });
|
|
351
|
+
frame.locals[slot] = val;
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
case Op.LOAD_GLOBAL: {
|
|
355
|
+
const idx = this.readI32(actor);
|
|
356
|
+
const name = this.chunk.constants[idx];
|
|
357
|
+
actor.stack.push(this.globals.get(name) ?? { tag: "void" });
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
case Op.STORE_GLOBAL: {
|
|
361
|
+
const idx = this.readI32(actor);
|
|
362
|
+
const name = this.chunk.constants[idx];
|
|
363
|
+
const val = actor.stack.pop()!;
|
|
364
|
+
this.globals.set(name, val);
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ---- 제어 ----
|
|
369
|
+
case Op.JUMP: {
|
|
370
|
+
const target = this.readI32(actor);
|
|
371
|
+
actor.ip = target;
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
case Op.JUMP_IF_FALSE: {
|
|
375
|
+
const target = this.readI32(actor);
|
|
376
|
+
const cond = actor.stack.pop()!;
|
|
377
|
+
if (cond.tag === "bool" && !cond.val) {
|
|
378
|
+
actor.ip = target;
|
|
379
|
+
}
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
case Op.RETURN: {
|
|
383
|
+
const retVal = actor.stack.pop() ?? { tag: "void" as const };
|
|
384
|
+
const frame = actor.frames.pop()!;
|
|
385
|
+
|
|
386
|
+
if (actor.frames.length === 0) {
|
|
387
|
+
actor.state = "done";
|
|
388
|
+
this.runningCount--;
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
actor.ip = frame.returnAddr;
|
|
393
|
+
// 스택 정리
|
|
394
|
+
actor.stack.length = frame.baseSlot;
|
|
395
|
+
actor.stack.push(retVal);
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
case Op.HALT:
|
|
399
|
+
actor.state = "done";
|
|
400
|
+
this.runningCount--;
|
|
401
|
+
return;
|
|
402
|
+
|
|
403
|
+
// ---- 함수 호출 ----
|
|
404
|
+
case Op.CALL: {
|
|
405
|
+
const fnIdx = this.readI32(actor);
|
|
406
|
+
const argCount = this.chunk.code[actor.ip++];
|
|
407
|
+
|
|
408
|
+
const fn = this.chunk.functions[fnIdx];
|
|
409
|
+
if (!fn) throw new Error(`panic: undefined function index ${fnIdx}`);
|
|
410
|
+
|
|
411
|
+
const args: Value[] = new Array(argCount);
|
|
412
|
+
for (let i = argCount - 1; i >= 0; i--) {
|
|
413
|
+
args[i] = actor.stack.pop()!;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
actor.frames.push({
|
|
417
|
+
returnAddr: actor.ip,
|
|
418
|
+
baseSlot: actor.stack.length,
|
|
419
|
+
locals: args,
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
actor.ip = fn.offset;
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
case Op.CALL_BUILTIN: {
|
|
426
|
+
const nameIdx = this.readI32(actor);
|
|
427
|
+
const argCount = this.chunk.code[actor.ip++];
|
|
428
|
+
const name = this.chunk.constants[nameIdx];
|
|
429
|
+
|
|
430
|
+
const args: Value[] = new Array(argCount);
|
|
431
|
+
for (let i = argCount - 1; i >= 0; i--) {
|
|
432
|
+
args[i] = actor.stack.pop()!;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const result = await this.callBuiltin(name, args);
|
|
436
|
+
actor.stack.push(result);
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ---- 배열 ----
|
|
441
|
+
case Op.ARRAY_NEW: {
|
|
442
|
+
const count = this.readI32(actor);
|
|
443
|
+
const elements: Value[] = new Array(count);
|
|
444
|
+
for (let i = count - 1; i >= 0; i--) {
|
|
445
|
+
elements[i] = actor.stack.pop()!;
|
|
446
|
+
}
|
|
447
|
+
actor.stack.push({ tag: "arr", val: elements });
|
|
448
|
+
break;
|
|
449
|
+
}
|
|
450
|
+
case Op.ARRAY_GET: {
|
|
451
|
+
const idx = actor.stack.pop()!;
|
|
452
|
+
const arr = actor.stack.pop()!;
|
|
453
|
+
if (arr.tag !== "arr") throw new Error("panic: not an array");
|
|
454
|
+
const i = (idx as any).val;
|
|
455
|
+
if (i < 0 || i >= arr.val.length) throw new Error(`panic: index out of bounds: ${i}`);
|
|
456
|
+
actor.stack.push(arr.val[i]);
|
|
457
|
+
break;
|
|
458
|
+
}
|
|
459
|
+
case Op.ARRAY_SET: {
|
|
460
|
+
const val = actor.stack.pop()!;
|
|
461
|
+
const idx = actor.stack.pop()!;
|
|
462
|
+
const arr = actor.stack.pop()!;
|
|
463
|
+
if (arr.tag !== "arr") throw new Error("panic: not an array");
|
|
464
|
+
arr.val[(idx as any).val] = val;
|
|
465
|
+
break;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// ---- 구조체 ----
|
|
469
|
+
case Op.STRUCT_NEW: {
|
|
470
|
+
const count = this.readI32(actor);
|
|
471
|
+
const fields = new Map<string, Value>();
|
|
472
|
+
for (let i = 0; i < count; i++) {
|
|
473
|
+
const val = actor.stack.pop()!;
|
|
474
|
+
const key = actor.stack.pop()!;
|
|
475
|
+
fields.set((key as any).val, val);
|
|
476
|
+
}
|
|
477
|
+
actor.stack.push({ tag: "struct", fields });
|
|
478
|
+
break;
|
|
479
|
+
}
|
|
480
|
+
case Op.STRUCT_GET: {
|
|
481
|
+
const nameIdx = this.readI32(actor);
|
|
482
|
+
const fieldName = this.chunk.constants[nameIdx];
|
|
483
|
+
const obj = actor.stack.pop()!;
|
|
484
|
+
if (obj.tag !== "struct") throw new Error("panic: not a struct");
|
|
485
|
+
actor.stack.push(obj.fields.get(fieldName) ?? { tag: "void" });
|
|
486
|
+
break;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// ---- Option/Result ----
|
|
490
|
+
case Op.WRAP_OK: {
|
|
491
|
+
const val = actor.stack.pop()!;
|
|
492
|
+
actor.stack.push({ tag: "ok", val });
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
495
|
+
case Op.WRAP_ERR: {
|
|
496
|
+
const val = actor.stack.pop()!;
|
|
497
|
+
actor.stack.push({ tag: "err", val });
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
case Op.WRAP_SOME: {
|
|
501
|
+
const val = actor.stack.pop()!;
|
|
502
|
+
actor.stack.push({ tag: "some", val });
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
case Op.UNWRAP: {
|
|
506
|
+
const val = actor.stack.pop()!;
|
|
507
|
+
if (val.tag === "ok" || val.tag === "some") {
|
|
508
|
+
actor.stack.push(val.val);
|
|
509
|
+
} else {
|
|
510
|
+
throw new Error(`panic: unwrap on ${val.tag}`);
|
|
511
|
+
}
|
|
512
|
+
break;
|
|
513
|
+
}
|
|
514
|
+
case Op.IS_OK: {
|
|
515
|
+
const val = actor.stack.pop()!;
|
|
516
|
+
actor.stack.push({ tag: "bool", val: val.tag === "ok" });
|
|
517
|
+
break;
|
|
518
|
+
}
|
|
519
|
+
case Op.IS_ERR: {
|
|
520
|
+
const val = actor.stack.pop()!;
|
|
521
|
+
actor.stack.push({ tag: "bool", val: val.tag === "err" });
|
|
522
|
+
break;
|
|
523
|
+
}
|
|
524
|
+
case Op.IS_SOME: {
|
|
525
|
+
const val = actor.stack.pop()!;
|
|
526
|
+
actor.stack.push({ tag: "bool", val: val.tag === "some" });
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
case Op.IS_NONE: {
|
|
530
|
+
const val = actor.stack.pop()!;
|
|
531
|
+
actor.stack.push({ tag: "bool", val: val.tag === "none" });
|
|
532
|
+
break;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// ---- Actor/Channel ----
|
|
536
|
+
case Op.SPAWN: {
|
|
537
|
+
const bodyOffset = this.readI32(actor);
|
|
538
|
+
const newActor: Actor = {
|
|
539
|
+
id: this.actors.length,
|
|
540
|
+
ip: bodyOffset,
|
|
541
|
+
stack: [],
|
|
542
|
+
frames: [{ returnAddr: -1, baseSlot: 0, locals: [] }],
|
|
543
|
+
state: "running",
|
|
544
|
+
waitingChan: null,
|
|
545
|
+
};
|
|
546
|
+
this.actors.push(newActor);
|
|
547
|
+
this.runningCount++;
|
|
548
|
+
break;
|
|
549
|
+
}
|
|
550
|
+
case Op.CHAN_NEW: {
|
|
551
|
+
const id = this.nextChannelId++;
|
|
552
|
+
const chan: Channel = {
|
|
553
|
+
id,
|
|
554
|
+
buffer: [],
|
|
555
|
+
waitingRecv: [],
|
|
556
|
+
};
|
|
557
|
+
this.channels.set(id, chan);
|
|
558
|
+
actor.stack.push({ tag: "chan", id });
|
|
559
|
+
break;
|
|
560
|
+
}
|
|
561
|
+
case Op.CHAN_SEND: {
|
|
562
|
+
const val = actor.stack.pop()!;
|
|
563
|
+
const chanVal = actor.stack.pop()!;
|
|
564
|
+
if (chanVal.tag !== "chan") throw new Error("panic: send on non-channel");
|
|
565
|
+
const chan = this.channels.get(chanVal.id)!;
|
|
566
|
+
chan.buffer.push(val);
|
|
567
|
+
// 대기 중인 actor 깨우기
|
|
568
|
+
if (chan.waitingRecv.length > 0) {
|
|
569
|
+
const waitId = chan.waitingRecv.shift()!;
|
|
570
|
+
const waitActor = this.actors[waitId];
|
|
571
|
+
if (waitActor) {
|
|
572
|
+
waitActor.state = "running";
|
|
573
|
+
waitActor.waitingChan = null;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
break;
|
|
577
|
+
}
|
|
578
|
+
case Op.CHAN_RECV: {
|
|
579
|
+
const chanVal = actor.stack.pop()!;
|
|
580
|
+
if (chanVal.tag !== "chan") throw new Error("panic: recv on non-channel");
|
|
581
|
+
const chan = this.channels.get(chanVal.id)!;
|
|
582
|
+
if (chan.buffer.length > 0) {
|
|
583
|
+
const val = chan.buffer.shift()!;
|
|
584
|
+
actor.stack.push({ tag: "ok", val });
|
|
585
|
+
} else {
|
|
586
|
+
// 대기 상태로 전환
|
|
587
|
+
actor.state = "waiting";
|
|
588
|
+
actor.waitingChan = chanVal.id;
|
|
589
|
+
chan.waitingRecv.push(actor.id);
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
break;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
default:
|
|
596
|
+
throw new Error(`panic: unknown opcode 0x${op.toString(16)}`);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// ============================================================
|
|
602
|
+
// 내장 함수 (SPEC_10)
|
|
603
|
+
// ============================================================
|
|
604
|
+
|
|
605
|
+
private async callBuiltin(name: string, args: Value[]): Promise<Value> {
|
|
606
|
+
// ============================================================
|
|
607
|
+
// DB 헬퍼 함수들
|
|
608
|
+
// ============================================================
|
|
609
|
+
const getDB = (arg: Value): DBAdapter | null => {
|
|
610
|
+
if (arg.tag !== "db") return null;
|
|
611
|
+
return this.databases.get(arg.id) ?? null;
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
const dbErr = (msg: string): Value => ({
|
|
615
|
+
tag: "err",
|
|
616
|
+
val: { tag: "str", val: msg },
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
const rowToValue = (row: any): Value => {
|
|
620
|
+
const fields = new Map<string, Value>();
|
|
621
|
+
for (const [key, val] of Object.entries(row)) {
|
|
622
|
+
fields.set(key, this.jsonToValue(val));
|
|
623
|
+
}
|
|
624
|
+
return { tag: "struct", fields };
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
switch (name) {
|
|
628
|
+
case "println": {
|
|
629
|
+
const text = args.map((a) => this.valueToString(a)).join(" ");
|
|
630
|
+
this.output.push(text);
|
|
631
|
+
return { tag: "void" };
|
|
632
|
+
}
|
|
633
|
+
case "print": {
|
|
634
|
+
const text = args.map((a) => this.valueToString(a)).join(" ");
|
|
635
|
+
// print는 줄바꿈 없이 마지막 출력에 이어붙임
|
|
636
|
+
if (this.output.length > 0) {
|
|
637
|
+
this.output[this.output.length - 1] += text;
|
|
638
|
+
} else {
|
|
639
|
+
this.output.push(text);
|
|
640
|
+
}
|
|
641
|
+
return { tag: "void" };
|
|
642
|
+
}
|
|
643
|
+
case "str":
|
|
644
|
+
return { tag: "str", val: this.valueToString(args[0]) };
|
|
645
|
+
case "length":
|
|
646
|
+
if (args[0].tag === "arr") return { tag: "i32", val: args[0].val.length };
|
|
647
|
+
if (args[0].tag === "str") return { tag: "i32", val: args[0].val.length };
|
|
648
|
+
return { tag: "i32", val: 0 };
|
|
649
|
+
case "range": {
|
|
650
|
+
const start = (args[0] as any).val;
|
|
651
|
+
const end = (args[1] as any).val;
|
|
652
|
+
const arr: Value[] = [];
|
|
653
|
+
for (let i = start; i < end; i++) arr.push({ tag: "i32", val: i });
|
|
654
|
+
return { tag: "arr", val: arr };
|
|
655
|
+
}
|
|
656
|
+
case "push":
|
|
657
|
+
if (args[0].tag === "arr") args[0].val.push(args[1]);
|
|
658
|
+
return { tag: "void" };
|
|
659
|
+
case "pop":
|
|
660
|
+
if (args[0].tag === "arr") return args[0].val.pop() ?? { tag: "void" };
|
|
661
|
+
return { tag: "void" };
|
|
662
|
+
case "abs":
|
|
663
|
+
return { tag: (args[0] as any).tag, val: Math.abs((args[0] as any).val) };
|
|
664
|
+
case "min":
|
|
665
|
+
return { tag: (args[0] as any).tag, val: Math.min((args[0] as any).val, (args[1] as any).val) };
|
|
666
|
+
case "max":
|
|
667
|
+
return { tag: (args[0] as any).tag, val: Math.max((args[0] as any).val, (args[1] as any).val) };
|
|
668
|
+
case "pow":
|
|
669
|
+
return { tag: "f64", val: Math.pow((args[0] as any).val, (args[1] as any).val) };
|
|
670
|
+
case "sqrt":
|
|
671
|
+
return { tag: "f64", val: Math.sqrt((args[0] as any).val) };
|
|
672
|
+
case "typeof":
|
|
673
|
+
return { tag: "str", val: args[0].tag };
|
|
674
|
+
case "assert":
|
|
675
|
+
if (args[0].tag === "bool" && !args[0].val) {
|
|
676
|
+
const msg = args.length > 1 ? this.valueToString(args[1]) : "assertion failed";
|
|
677
|
+
throw new Error(`panic: ${msg}`);
|
|
678
|
+
}
|
|
679
|
+
return { tag: "void" };
|
|
680
|
+
case "panic":
|
|
681
|
+
throw new Error(`panic: ${this.valueToString(args[0])}`);
|
|
682
|
+
case "bitand":
|
|
683
|
+
return { tag: "i32", val: (args[0] as any).val & (args[1] as any).val };
|
|
684
|
+
case "bitor":
|
|
685
|
+
return { tag: "i32", val: (args[0] as any).val | (args[1] as any).val };
|
|
686
|
+
case "bitxor":
|
|
687
|
+
return { tag: "i32", val: (args[0] as any).val ^ (args[1] as any).val };
|
|
688
|
+
case "shl":
|
|
689
|
+
return { tag: "i32", val: (args[0] as any).val << (args[1] as any).val };
|
|
690
|
+
case "shr":
|
|
691
|
+
return { tag: "i32", val: (args[0] as any).val >> (args[1] as any).val };
|
|
692
|
+
case "contains":
|
|
693
|
+
if (args[0].tag === "str") {
|
|
694
|
+
return { tag: "bool", val: args[0].val.includes((args[1] as any).val) };
|
|
695
|
+
}
|
|
696
|
+
return { tag: "bool", val: false };
|
|
697
|
+
case "split":
|
|
698
|
+
if (args[0].tag === "str") {
|
|
699
|
+
const parts = args[0].val.split((args[1] as any).val);
|
|
700
|
+
return { tag: "arr", val: parts.map((s) => ({ tag: "str" as const, val: s })) };
|
|
701
|
+
}
|
|
702
|
+
return { tag: "arr", val: [] };
|
|
703
|
+
case "trim":
|
|
704
|
+
if (args[0].tag === "str") return { tag: "str", val: args[0].val.trim() };
|
|
705
|
+
return args[0];
|
|
706
|
+
case "to_upper":
|
|
707
|
+
if (args[0].tag === "str") return { tag: "str", val: args[0].val.toUpperCase() };
|
|
708
|
+
return args[0];
|
|
709
|
+
case "to_lower":
|
|
710
|
+
if (args[0].tag === "str") return { tag: "str", val: args[0].val.toLowerCase() };
|
|
711
|
+
return args[0];
|
|
712
|
+
case "char_at":
|
|
713
|
+
if (args[0].tag === "str") {
|
|
714
|
+
const i = (args[1] as any).val;
|
|
715
|
+
return { tag: "str", val: args[0].val[i] ?? "" };
|
|
716
|
+
}
|
|
717
|
+
return { tag: "str", val: "" };
|
|
718
|
+
case "slice":
|
|
719
|
+
if (args[0].tag === "arr") {
|
|
720
|
+
const s = (args[1] as any).val;
|
|
721
|
+
const e = (args[2] as any).val;
|
|
722
|
+
return { tag: "arr", val: args[0].val.slice(s, e) };
|
|
723
|
+
}
|
|
724
|
+
if (args[0].tag === "str") {
|
|
725
|
+
const s = (args[1] as any).val;
|
|
726
|
+
const e = (args[2] as any).val;
|
|
727
|
+
return { tag: "str", val: args[0].val.slice(s, e) };
|
|
728
|
+
}
|
|
729
|
+
return args[0];
|
|
730
|
+
case "clone":
|
|
731
|
+
return this.deepClone(args[0]);
|
|
732
|
+
case "channel": {
|
|
733
|
+
const id = this.nextChannelId++;
|
|
734
|
+
const chan: Channel = { id, buffer: [], waitingRecv: [] };
|
|
735
|
+
this.channels.set(id, chan);
|
|
736
|
+
return { tag: "chan", id };
|
|
737
|
+
}
|
|
738
|
+
case "i32": {
|
|
739
|
+
const parsed = parseInt(this.valueToString(args[0]), 10);
|
|
740
|
+
if (isNaN(parsed)) return { tag: "err", val: { tag: "str", val: "Invalid number for i32" } };
|
|
741
|
+
return { tag: "ok", val: { tag: "i32", val: parsed } };
|
|
742
|
+
}
|
|
743
|
+
case "i64":
|
|
744
|
+
return { tag: "ok", val: { tag: "i32", val: parseInt(this.valueToString(args[0]), 10) || 0 } };
|
|
745
|
+
case "f64":
|
|
746
|
+
return { tag: "ok", val: { tag: "f64", val: parseFloat(this.valueToString(args[0])) || 0 } };
|
|
747
|
+
case "read_line": {
|
|
748
|
+
// synchronous readline from stdin (simplified - returns empty for now)
|
|
749
|
+
// In real implementation, would need async or proper stdin handling
|
|
750
|
+
return { tag: "str", val: "" };
|
|
751
|
+
}
|
|
752
|
+
case "read_file": {
|
|
753
|
+
const filepath = this.valueToString(args[0]);
|
|
754
|
+
try {
|
|
755
|
+
const content = fs.readFileSync(filepath, "utf-8");
|
|
756
|
+
return { tag: "ok", val: { tag: "str", val: content } };
|
|
757
|
+
} catch (err) {
|
|
758
|
+
const errMsg = err instanceof Error ? err.message : "unknown error";
|
|
759
|
+
return { tag: "err", val: { tag: "str", val: errMsg } };
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
case "write_file": {
|
|
763
|
+
const filepath = this.valueToString(args[0]);
|
|
764
|
+
const content = this.valueToString(args[1]);
|
|
765
|
+
try {
|
|
766
|
+
fs.writeFileSync(filepath, content, "utf-8");
|
|
767
|
+
return { tag: "ok", val: { tag: "void" } };
|
|
768
|
+
} catch (err) {
|
|
769
|
+
const errMsg = err instanceof Error ? err.message : "unknown error";
|
|
770
|
+
return { tag: "err", val: { tag: "str", val: errMsg } };
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
case "recv":
|
|
774
|
+
// method-style: obj.recv()
|
|
775
|
+
if (args[0] && args[0].tag === "chan") {
|
|
776
|
+
const chan = this.channels.get(args[0].id);
|
|
777
|
+
if (chan && chan.buffer.length > 0) {
|
|
778
|
+
return { tag: "ok", val: chan.buffer.shift()! };
|
|
779
|
+
}
|
|
780
|
+
return { tag: "err", val: { tag: "str", val: "channel empty" } };
|
|
781
|
+
}
|
|
782
|
+
return { tag: "err", val: { tag: "str", val: "not a channel" } };
|
|
783
|
+
case "send":
|
|
784
|
+
if (args[0] && args[0].tag === "chan") {
|
|
785
|
+
const chan = this.channels.get(args[0].id);
|
|
786
|
+
if (chan) chan.buffer.push(args[1]);
|
|
787
|
+
return { tag: "void" };
|
|
788
|
+
}
|
|
789
|
+
return { tag: "void" };
|
|
790
|
+
|
|
791
|
+
// ============================================================
|
|
792
|
+
// Phase 7: 20 Core Libraries
|
|
793
|
+
// ============================================================
|
|
794
|
+
|
|
795
|
+
// Cryptography & Encoding (6)
|
|
796
|
+
case "md5": {
|
|
797
|
+
const input = this.valueToString(args[0]);
|
|
798
|
+
const hash = crypto.createHash("md5").update(input).digest("hex");
|
|
799
|
+
return { tag: "str", val: hash };
|
|
800
|
+
}
|
|
801
|
+
case "sha256": {
|
|
802
|
+
const input = this.valueToString(args[0]);
|
|
803
|
+
const hash = crypto.createHash("sha256").update(input).digest("hex");
|
|
804
|
+
return { tag: "str", val: hash };
|
|
805
|
+
}
|
|
806
|
+
case "sha512": {
|
|
807
|
+
const input = this.valueToString(args[0]);
|
|
808
|
+
const hash = crypto.createHash("sha512").update(input).digest("hex");
|
|
809
|
+
return { tag: "str", val: hash };
|
|
810
|
+
}
|
|
811
|
+
case "base64_encode": {
|
|
812
|
+
const input = this.valueToString(args[0]);
|
|
813
|
+
const encoded = Buffer.from(input, "utf8").toString("base64");
|
|
814
|
+
return { tag: "str", val: encoded };
|
|
815
|
+
}
|
|
816
|
+
case "base64_decode": {
|
|
817
|
+
try {
|
|
818
|
+
const input = this.valueToString(args[0]);
|
|
819
|
+
const decoded = Buffer.from(input, "base64").toString("utf8");
|
|
820
|
+
return { tag: "ok", val: { tag: "str", val: decoded } };
|
|
821
|
+
} catch (e) {
|
|
822
|
+
return { tag: "err", val: { tag: "str", val: "invalid base64" } };
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
case "hmac": {
|
|
826
|
+
const message = this.valueToString(args[0]);
|
|
827
|
+
const secret = this.valueToString(args[1]);
|
|
828
|
+
const hmac = crypto.createHmac("sha256", secret).update(message).digest("hex");
|
|
829
|
+
return { tag: "str", val: hmac };
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// JSON (4)
|
|
833
|
+
case "json_parse": {
|
|
834
|
+
try {
|
|
835
|
+
const jsonStr = this.valueToString(args[0]);
|
|
836
|
+
const obj = JSON.parse(jsonStr);
|
|
837
|
+
const value = this.jsonToValue(obj);
|
|
838
|
+
return { tag: "ok", val: value };
|
|
839
|
+
} catch (e) {
|
|
840
|
+
return { tag: "err", val: { tag: "str", val: `JSON parse error: ${String(e)}` } };
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
case "json_stringify": {
|
|
844
|
+
try {
|
|
845
|
+
const jsonStr = JSON.stringify(this.valueToJSON(args[0]), null, 0);
|
|
846
|
+
return { tag: "str", val: jsonStr };
|
|
847
|
+
} catch (e) {
|
|
848
|
+
return { tag: "err", val: { tag: "str", val: `JSON stringify error: ${String(e)}` } };
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
case "json_validate": {
|
|
852
|
+
try {
|
|
853
|
+
const jsonStr = this.valueToString(args[0]);
|
|
854
|
+
JSON.parse(jsonStr);
|
|
855
|
+
return { tag: "bool", val: true };
|
|
856
|
+
} catch {
|
|
857
|
+
return { tag: "bool", val: false };
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
case "json_pretty": {
|
|
861
|
+
try {
|
|
862
|
+
const jsonStr = this.valueToString(args[0]);
|
|
863
|
+
const obj = JSON.parse(jsonStr);
|
|
864
|
+
const pretty = JSON.stringify(obj, null, 2);
|
|
865
|
+
return { tag: "str", val: pretty };
|
|
866
|
+
} catch (e) {
|
|
867
|
+
return { tag: "err", val: { tag: "str", val: `JSON pretty error: ${String(e)}` } };
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// Advanced Strings (3)
|
|
872
|
+
case "starts_with": {
|
|
873
|
+
if (args[0].tag === "str" && args[1].tag === "str") {
|
|
874
|
+
const result = args[0].val.startsWith(args[1].val);
|
|
875
|
+
return { tag: "bool", val: result };
|
|
876
|
+
}
|
|
877
|
+
return { tag: "bool", val: false };
|
|
878
|
+
}
|
|
879
|
+
case "ends_with": {
|
|
880
|
+
if (args[0].tag === "str" && args[1].tag === "str") {
|
|
881
|
+
const result = args[0].val.endsWith(args[1].val);
|
|
882
|
+
return { tag: "bool", val: result };
|
|
883
|
+
}
|
|
884
|
+
return { tag: "bool", val: false };
|
|
885
|
+
}
|
|
886
|
+
case "replace": {
|
|
887
|
+
if (args[0].tag === "str" && args[1].tag === "str" && args[2].tag === "str") {
|
|
888
|
+
const result = args[0].val.replaceAll(args[1].val, args[2].val);
|
|
889
|
+
return { tag: "str", val: result };
|
|
890
|
+
}
|
|
891
|
+
return args[0];
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// Advanced Arrays (3)
|
|
895
|
+
case "reverse": {
|
|
896
|
+
if (args[0].tag === "arr") {
|
|
897
|
+
const reversed = [...args[0].val].reverse();
|
|
898
|
+
return { tag: "arr", val: reversed };
|
|
899
|
+
}
|
|
900
|
+
return args[0];
|
|
901
|
+
}
|
|
902
|
+
case "sort": {
|
|
903
|
+
if (args[0].tag === "arr") {
|
|
904
|
+
const sorted = [...args[0].val].sort((a, b) => {
|
|
905
|
+
const aVal = (a as any).val ?? 0;
|
|
906
|
+
const bVal = (b as any).val ?? 0;
|
|
907
|
+
return aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
|
|
908
|
+
});
|
|
909
|
+
return { tag: "arr", val: sorted };
|
|
910
|
+
}
|
|
911
|
+
return args[0];
|
|
912
|
+
}
|
|
913
|
+
case "unique": {
|
|
914
|
+
if (args[0].tag === "arr") {
|
|
915
|
+
const seen = new Set<string>();
|
|
916
|
+
const unique: Value[] = [];
|
|
917
|
+
for (const item of args[0].val) {
|
|
918
|
+
const key = JSON.stringify(this.valueToJSON(item));
|
|
919
|
+
if (!seen.has(key)) {
|
|
920
|
+
seen.add(key);
|
|
921
|
+
unique.push(item);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
return { tag: "arr", val: unique };
|
|
925
|
+
}
|
|
926
|
+
return args[0];
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// Math (2)
|
|
930
|
+
case "gcd": {
|
|
931
|
+
const a = Math.abs((args[0] as any).val);
|
|
932
|
+
const b = Math.abs((args[1] as any).val);
|
|
933
|
+
const gcd = (x: number, y: number): number => (y === 0 ? x : gcd(y, x % y));
|
|
934
|
+
return { tag: "i32", val: gcd(a, b) };
|
|
935
|
+
}
|
|
936
|
+
case "lcm": {
|
|
937
|
+
const a = Math.abs((args[0] as any).val);
|
|
938
|
+
const b = Math.abs((args[1] as any).val);
|
|
939
|
+
const gcd = (x: number, y: number): number => (y === 0 ? x : gcd(y, x % y));
|
|
940
|
+
return { tag: "i32", val: (a * b) / gcd(a, b) };
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// Utils (2)
|
|
944
|
+
case "uuid": {
|
|
945
|
+
const uuid = crypto.randomUUID();
|
|
946
|
+
return { tag: "str", val: uuid };
|
|
947
|
+
}
|
|
948
|
+
case "timestamp": {
|
|
949
|
+
const now = Date.now();
|
|
950
|
+
return { tag: "f64", val: now };
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// Environment (1)
|
|
954
|
+
case "env": {
|
|
955
|
+
const key = this.valueToString(args[0]);
|
|
956
|
+
const value = process.env[key] ?? "";
|
|
957
|
+
return { tag: "str", val: value };
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// HTTP Client (5) — Phase 2
|
|
961
|
+
case "http_get": {
|
|
962
|
+
const url = this.valueToString(args[0]);
|
|
963
|
+
try {
|
|
964
|
+
const result = await this.httpGetAsync(url);
|
|
965
|
+
return result;
|
|
966
|
+
} catch (e) {
|
|
967
|
+
return { tag: "err", val: { tag: "str", val: `HTTP error: ${String(e)}` } };
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
case "http_post": {
|
|
971
|
+
const url = this.valueToString(args[0]);
|
|
972
|
+
const body = this.valueToString(args[1]);
|
|
973
|
+
try {
|
|
974
|
+
const result = await this.httpPostAsync(url, body);
|
|
975
|
+
return result;
|
|
976
|
+
} catch (e) {
|
|
977
|
+
return { tag: "err", val: { tag: "str", val: `HTTP error: ${String(e)}` } };
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
case "http_post_json": {
|
|
981
|
+
const url = this.valueToString(args[0]);
|
|
982
|
+
const jsonBody = this.valueToString(args[1]);
|
|
983
|
+
try {
|
|
984
|
+
const result = await this.httpPostJsonAsync(url, jsonBody);
|
|
985
|
+
return result;
|
|
986
|
+
} catch (e) {
|
|
987
|
+
return { tag: "err", val: { tag: "str", val: `HTTP error: ${String(e)}` } };
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
case "fetch": {
|
|
991
|
+
const url = this.valueToString(args[0]);
|
|
992
|
+
const method = args.length > 1 ? this.valueToString(args[1]) : "GET";
|
|
993
|
+
const headers = args.length > 2 ? args[2] : null;
|
|
994
|
+
const body = args.length > 3 ? this.valueToString(args[3]) : null;
|
|
995
|
+
try {
|
|
996
|
+
const result = await this.fetchAsync(url, method, headers, body);
|
|
997
|
+
return result;
|
|
998
|
+
} catch (e) {
|
|
999
|
+
return { tag: "err", val: { tag: "str", val: `HTTP error: ${String(e)}` } };
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// Database (5)
|
|
1004
|
+
case "sqlite_open": {
|
|
1005
|
+
const path = this.valueToString(args[0]);
|
|
1006
|
+
try {
|
|
1007
|
+
const db = new SQLiteDB(path);
|
|
1008
|
+
await db.init();
|
|
1009
|
+
const dbId = this.nextDbId++;
|
|
1010
|
+
this.databases.set(dbId, db);
|
|
1011
|
+
return { tag: "db", id: dbId };
|
|
1012
|
+
} catch (e: any) {
|
|
1013
|
+
return { tag: "err", val: { tag: "str", val: `Database error: ${e.message}` } };
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
case "sqlite_query": {
|
|
1017
|
+
const db = getDB(args[0]);
|
|
1018
|
+
if (!db) return dbErr("first argument must be a database");
|
|
1019
|
+
const sql = this.valueToString(args[1]);
|
|
1020
|
+
const params = args.length > 2 && args[2].tag === "arr" ? args[2].val.map(v => (v as any).val) : [];
|
|
1021
|
+
try {
|
|
1022
|
+
const rows = await db.query(sql, params);
|
|
1023
|
+
const result = rows.map(rowToValue);
|
|
1024
|
+
return { tag: "ok", val: { tag: "arr", val: result } };
|
|
1025
|
+
} catch (e: any) {
|
|
1026
|
+
return dbErr(e.message);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
case "sqlite_execute": {
|
|
1030
|
+
const db = getDB(args[0]);
|
|
1031
|
+
if (!db) return dbErr("first argument must be a database");
|
|
1032
|
+
const sql = this.valueToString(args[1]);
|
|
1033
|
+
const params = args.length > 2 && args[2].tag === "arr" ? args[2].val.map(v => (v as any).val) : [];
|
|
1034
|
+
try {
|
|
1035
|
+
const result = await db.execute(sql, params);
|
|
1036
|
+
return { tag: "ok", val: { tag: "struct", fields: new Map([["changes", { tag: "i32", val: result.changes }]]) } };
|
|
1037
|
+
} catch (e: any) {
|
|
1038
|
+
return dbErr(e.message);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
case "sqlite_close": {
|
|
1042
|
+
const db = getDB(args[0]);
|
|
1043
|
+
if (!db) return dbErr("argument must be a database");
|
|
1044
|
+
try {
|
|
1045
|
+
await db.close();
|
|
1046
|
+
const dbId = (args[0] as { tag: "db"; id: number }).id;
|
|
1047
|
+
this.databases.delete(dbId);
|
|
1048
|
+
return { tag: "void" };
|
|
1049
|
+
} catch (e: any) {
|
|
1050
|
+
return dbErr(e.message);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// Transaction builtins
|
|
1055
|
+
case "sqlite_begin": {
|
|
1056
|
+
const db = getDB(args[0]);
|
|
1057
|
+
if (!db) return dbErr("argument must be a database");
|
|
1058
|
+
const isolation = args.length > 1 ? this.valueToString(args[1]) : "deferred";
|
|
1059
|
+
try {
|
|
1060
|
+
await (db as any).begin(isolation);
|
|
1061
|
+
return { tag: "void" };
|
|
1062
|
+
} catch (e: any) {
|
|
1063
|
+
return dbErr(e.message);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
case "sqlite_commit": {
|
|
1068
|
+
const db = getDB(args[0]);
|
|
1069
|
+
if (!db) return dbErr("argument must be a database");
|
|
1070
|
+
try {
|
|
1071
|
+
await (db as any).commit();
|
|
1072
|
+
return { tag: "void" };
|
|
1073
|
+
} catch (e: any) {
|
|
1074
|
+
return dbErr(e.message);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
case "sqlite_rollback": {
|
|
1079
|
+
const db = getDB(args[0]);
|
|
1080
|
+
if (!db) return dbErr("argument must be a database");
|
|
1081
|
+
try {
|
|
1082
|
+
await (db as any).rollback();
|
|
1083
|
+
return { tag: "void" };
|
|
1084
|
+
} catch (e: any) {
|
|
1085
|
+
return dbErr(e.message);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// PostgreSQL builtins
|
|
1090
|
+
case "pg_connect": {
|
|
1091
|
+
if (args.length < 5) return dbErr("pg_connect requires 5 arguments");
|
|
1092
|
+
const [host, port, user, password, database] = args.map(a => this.valueToString(a));
|
|
1093
|
+
try {
|
|
1094
|
+
const { PostgreSQLDB } = await import("./db");
|
|
1095
|
+
const db = new PostgreSQLDB({
|
|
1096
|
+
host,
|
|
1097
|
+
port: parseInt(port),
|
|
1098
|
+
user,
|
|
1099
|
+
password,
|
|
1100
|
+
database,
|
|
1101
|
+
});
|
|
1102
|
+
await db.connect();
|
|
1103
|
+
const dbId = this.nextDbId++;
|
|
1104
|
+
this.databases.set(dbId, db);
|
|
1105
|
+
return { tag: "db", id: dbId };
|
|
1106
|
+
} catch (e: any) {
|
|
1107
|
+
return dbErr(`pg_connect error: ${e.message}`);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
case "pg_query": {
|
|
1112
|
+
const db = getDB(args[0]);
|
|
1113
|
+
if (!db) return dbErr("first argument must be a database");
|
|
1114
|
+
const sql = this.valueToString(args[1]);
|
|
1115
|
+
const params = args.length > 2 && args[2].tag === "arr" ? args[2].val.map(v => (v as any).val) : [];
|
|
1116
|
+
try {
|
|
1117
|
+
const rows = await db.query(sql, params);
|
|
1118
|
+
const result = rows.map(rowToValue);
|
|
1119
|
+
return { tag: "ok", val: { tag: "arr", val: result } };
|
|
1120
|
+
} catch (e: any) {
|
|
1121
|
+
return dbErr(e.message);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
case "pg_execute": {
|
|
1126
|
+
const db = getDB(args[0]);
|
|
1127
|
+
if (!db) return dbErr("first argument must be a database");
|
|
1128
|
+
const sql = this.valueToString(args[1]);
|
|
1129
|
+
const params = args.length > 2 && args[2].tag === "arr" ? args[2].val.map(v => (v as any).val) : [];
|
|
1130
|
+
try {
|
|
1131
|
+
const result = await db.execute(sql, params);
|
|
1132
|
+
return { tag: "ok", val: { tag: "struct", fields: new Map([["changes", { tag: "i32", val: result.changes }]]) } };
|
|
1133
|
+
} catch (e: any) {
|
|
1134
|
+
return dbErr(e.message);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
case "pg_close": {
|
|
1139
|
+
const db = getDB(args[0]);
|
|
1140
|
+
if (!db) return dbErr("argument must be a database");
|
|
1141
|
+
try {
|
|
1142
|
+
await db.close();
|
|
1143
|
+
const dbId = (args[0] as { tag: "db"; id: number }).id;
|
|
1144
|
+
this.databases.delete(dbId);
|
|
1145
|
+
return { tag: "void" };
|
|
1146
|
+
} catch (e: any) {
|
|
1147
|
+
return dbErr(e.message);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
case "pg_begin": {
|
|
1152
|
+
const db = getDB(args[0]);
|
|
1153
|
+
if (!db) return dbErr("argument must be a database");
|
|
1154
|
+
const isolation = args.length > 1 ? this.valueToString(args[1]) : "deferred";
|
|
1155
|
+
try {
|
|
1156
|
+
await (db as any).begin(isolation);
|
|
1157
|
+
return { tag: "void" };
|
|
1158
|
+
} catch (e: any) {
|
|
1159
|
+
return dbErr(e.message);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
case "pg_commit": {
|
|
1164
|
+
const db = getDB(args[0]);
|
|
1165
|
+
if (!db) return dbErr("argument must be a database");
|
|
1166
|
+
try {
|
|
1167
|
+
await (db as any).commit();
|
|
1168
|
+
return { tag: "void" };
|
|
1169
|
+
} catch (e: any) {
|
|
1170
|
+
return dbErr(e.message);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
case "pg_rollback": {
|
|
1175
|
+
const db = getDB(args[0]);
|
|
1176
|
+
if (!db) return dbErr("argument must be a database");
|
|
1177
|
+
try {
|
|
1178
|
+
await (db as any).rollback();
|
|
1179
|
+
return { tag: "void" };
|
|
1180
|
+
} catch (e: any) {
|
|
1181
|
+
return dbErr(e.message);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
case "mysql_connect": {
|
|
1186
|
+
if (args.length < 5) return dbErr("mysql_connect requires 5 arguments");
|
|
1187
|
+
const [host, port, user, password, database] = args.map(a => this.valueToString(a));
|
|
1188
|
+
try {
|
|
1189
|
+
const { MySQLDB } = await import("./db");
|
|
1190
|
+
const db = new MySQLDB({
|
|
1191
|
+
host,
|
|
1192
|
+
port: parseInt(port),
|
|
1193
|
+
user,
|
|
1194
|
+
password,
|
|
1195
|
+
database,
|
|
1196
|
+
});
|
|
1197
|
+
await db.connect();
|
|
1198
|
+
const dbId = this.nextDbId++;
|
|
1199
|
+
this.databases.set(dbId, db);
|
|
1200
|
+
return { tag: "db", id: dbId };
|
|
1201
|
+
} catch (e: any) {
|
|
1202
|
+
return dbErr(`mysql_connect error: ${e.message}`);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
case "mysql_query": {
|
|
1207
|
+
const db = getDB(args[0]);
|
|
1208
|
+
if (!db) return dbErr("first argument must be a database");
|
|
1209
|
+
const sql = this.valueToString(args[1]);
|
|
1210
|
+
const params = args.length > 2 && args[2].tag === "arr" ? args[2].val.map(v => (v as any).val) : [];
|
|
1211
|
+
try {
|
|
1212
|
+
const rows = await db.query(sql, params);
|
|
1213
|
+
const result = rows.map(rowToValue);
|
|
1214
|
+
return { tag: "ok", val: { tag: "arr", val: result } };
|
|
1215
|
+
} catch (e: any) {
|
|
1216
|
+
return dbErr(e.message);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
case "mysql_execute": {
|
|
1221
|
+
const db = getDB(args[0]);
|
|
1222
|
+
if (!db) return dbErr("first argument must be a database");
|
|
1223
|
+
const sql = this.valueToString(args[1]);
|
|
1224
|
+
const params = args.length > 2 && args[2].tag === "arr" ? args[2].val.map(v => (v as any).val) : [];
|
|
1225
|
+
try {
|
|
1226
|
+
const result = await db.execute(sql, params);
|
|
1227
|
+
return { tag: "ok", val: { tag: "struct", fields: new Map([["changes", { tag: "i32", val: result.changes }]]) } };
|
|
1228
|
+
} catch (e: any) {
|
|
1229
|
+
return dbErr(e.message);
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
case "mysql_close": {
|
|
1234
|
+
const db = getDB(args[0]);
|
|
1235
|
+
if (!db) return dbErr("argument must be a database");
|
|
1236
|
+
try {
|
|
1237
|
+
await db.close();
|
|
1238
|
+
const dbId = (args[0] as { tag: "db"; id: number }).id;
|
|
1239
|
+
this.databases.delete(dbId);
|
|
1240
|
+
return { tag: "void" };
|
|
1241
|
+
} catch (e: any) {
|
|
1242
|
+
return dbErr(e.message);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
case "mysql_begin": {
|
|
1247
|
+
const db = getDB(args[0]);
|
|
1248
|
+
if (!db) return dbErr("argument must be a database");
|
|
1249
|
+
const isolation = args.length > 1 ? this.valueToString(args[1]) : "deferred";
|
|
1250
|
+
try {
|
|
1251
|
+
await (db as any).begin(isolation);
|
|
1252
|
+
return { tag: "void" };
|
|
1253
|
+
} catch (e: any) {
|
|
1254
|
+
return dbErr(e.message);
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
case "mysql_commit": {
|
|
1259
|
+
const db = getDB(args[0]);
|
|
1260
|
+
if (!db) return dbErr("argument must be a database");
|
|
1261
|
+
try {
|
|
1262
|
+
await (db as any).commit();
|
|
1263
|
+
return { tag: "void" };
|
|
1264
|
+
} catch (e: any) {
|
|
1265
|
+
return dbErr(e.message);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
case "mysql_rollback": {
|
|
1270
|
+
const db = getDB(args[0]);
|
|
1271
|
+
if (!db) return dbErr("argument must be a database");
|
|
1272
|
+
try {
|
|
1273
|
+
await (db as any).rollback();
|
|
1274
|
+
return { tag: "void" };
|
|
1275
|
+
} catch (e: any) {
|
|
1276
|
+
return dbErr(e.message);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// Math Functions (7) — B-1
|
|
1281
|
+
case "floor": {
|
|
1282
|
+
const num = (args[0] as any).val ?? 0;
|
|
1283
|
+
return { tag: "i32", val: Math.floor(num) };
|
|
1284
|
+
}
|
|
1285
|
+
case "ceil": {
|
|
1286
|
+
const num = (args[0] as any).val ?? 0;
|
|
1287
|
+
return { tag: "i32", val: Math.ceil(num) };
|
|
1288
|
+
}
|
|
1289
|
+
case "round": {
|
|
1290
|
+
const num = (args[0] as any).val ?? 0;
|
|
1291
|
+
return { tag: "i32", val: Math.round(num) };
|
|
1292
|
+
}
|
|
1293
|
+
case "random": {
|
|
1294
|
+
return { tag: "f64", val: Math.random() };
|
|
1295
|
+
}
|
|
1296
|
+
case "sin": {
|
|
1297
|
+
const num = (args[0] as any).val ?? 0;
|
|
1298
|
+
return { tag: "f64", val: Math.sin(num) };
|
|
1299
|
+
}
|
|
1300
|
+
case "cos": {
|
|
1301
|
+
const num = (args[0] as any).val ?? 0;
|
|
1302
|
+
return { tag: "f64", val: Math.cos(num) };
|
|
1303
|
+
}
|
|
1304
|
+
case "log": {
|
|
1305
|
+
const num = (args[0] as any).val ?? 1;
|
|
1306
|
+
if (num <= 0) return { tag: "err", val: { tag: "str", val: "log: invalid argument" } };
|
|
1307
|
+
return { tag: "f64", val: Math.log(num) };
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
// String Functions (3) — B-2
|
|
1311
|
+
case "index_of": {
|
|
1312
|
+
if (args[0].tag === "str" && args[1].tag === "str") {
|
|
1313
|
+
const idx = args[0].val.indexOf(args[1].val);
|
|
1314
|
+
if (idx >= 0) {
|
|
1315
|
+
return { tag: "some", val: { tag: "i32", val: idx } };
|
|
1316
|
+
}
|
|
1317
|
+
return { tag: "none" };
|
|
1318
|
+
}
|
|
1319
|
+
return { tag: "none" };
|
|
1320
|
+
}
|
|
1321
|
+
case "pad_left": {
|
|
1322
|
+
if (args[0].tag === "str" && args[1].tag === "i32" && args[2].tag === "str") {
|
|
1323
|
+
const char = args[2].val.charAt(0) || " ";
|
|
1324
|
+
const padded = args[0].val.padStart(args[1].val, char);
|
|
1325
|
+
return { tag: "str", val: padded };
|
|
1326
|
+
}
|
|
1327
|
+
return args[0];
|
|
1328
|
+
}
|
|
1329
|
+
case "pad_right": {
|
|
1330
|
+
if (args[0].tag === "str" && args[1].tag === "i32" && args[2].tag === "str") {
|
|
1331
|
+
const char = args[2].val.charAt(0) || " ";
|
|
1332
|
+
const padded = args[0].val.padEnd(args[1].val, char);
|
|
1333
|
+
return { tag: "str", val: padded };
|
|
1334
|
+
}
|
|
1335
|
+
return args[0];
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
// Regex Functions (3) — B-3
|
|
1339
|
+
case "regex_match": {
|
|
1340
|
+
if (args[0].tag === "str" && args[1].tag === "str") {
|
|
1341
|
+
try {
|
|
1342
|
+
const regex = new RegExp(args[1].val);
|
|
1343
|
+
const match = args[0].val.match(regex);
|
|
1344
|
+
if (match) {
|
|
1345
|
+
return { tag: "some", val: { tag: "str", val: match[0] } };
|
|
1346
|
+
}
|
|
1347
|
+
return { tag: "none" };
|
|
1348
|
+
} catch (e) {
|
|
1349
|
+
return { tag: "err", val: { tag: "str", val: `regex error: ${String(e)}` } };
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
return { tag: "none" };
|
|
1353
|
+
}
|
|
1354
|
+
case "regex_find_all": {
|
|
1355
|
+
if (args[0].tag === "str" && args[1].tag === "str") {
|
|
1356
|
+
try {
|
|
1357
|
+
const regex = new RegExp(args[1].val, "g");
|
|
1358
|
+
const matches = args[0].val.match(regex) || [];
|
|
1359
|
+
const arr: Value[] = matches.map(m => ({ tag: "str", val: m }));
|
|
1360
|
+
return { tag: "arr", val: arr };
|
|
1361
|
+
} catch (e) {
|
|
1362
|
+
return { tag: "err", val: { tag: "str", val: `regex error: ${String(e)}` } };
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
return { tag: "arr", val: [] };
|
|
1366
|
+
}
|
|
1367
|
+
case "regex_replace": {
|
|
1368
|
+
if (args[0].tag === "str" && args[1].tag === "str" && args[2].tag === "str") {
|
|
1369
|
+
try {
|
|
1370
|
+
const regex = new RegExp(args[1].val, "g");
|
|
1371
|
+
const replaced = args[0].val.replace(regex, args[2].val);
|
|
1372
|
+
return { tag: "str", val: replaced };
|
|
1373
|
+
} catch (e) {
|
|
1374
|
+
return { tag: "err", val: { tag: "str", val: `regex error: ${String(e)}` } };
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
return args[0];
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
// CSV Functions (2) — B-4
|
|
1381
|
+
case "csv_parse": {
|
|
1382
|
+
if (args[0].tag === "str") {
|
|
1383
|
+
try {
|
|
1384
|
+
const lines = args[0].val.split("\n").filter(l => l.trim());
|
|
1385
|
+
const result: Value[] = [];
|
|
1386
|
+
for (const line of lines) {
|
|
1387
|
+
const cells = this.parseCsvRow(line);
|
|
1388
|
+
const row: Value[] = cells.map(c => ({ tag: "str", val: c }));
|
|
1389
|
+
result.push({ tag: "arr", val: row });
|
|
1390
|
+
}
|
|
1391
|
+
return { tag: "arr", val: result };
|
|
1392
|
+
} catch (e) {
|
|
1393
|
+
return { tag: "err", val: { tag: "str", val: `CSV parse error: ${String(e)}` } };
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
return { tag: "arr", val: [] };
|
|
1397
|
+
}
|
|
1398
|
+
case "csv_stringify": {
|
|
1399
|
+
if (args[0].tag === "arr") {
|
|
1400
|
+
try {
|
|
1401
|
+
const rows: string[] = [];
|
|
1402
|
+
for (const row of args[0].val) {
|
|
1403
|
+
if (row.tag === "arr") {
|
|
1404
|
+
const cells = row.val.map(v => this.valueToString(v));
|
|
1405
|
+
const escaped = cells.map(c => {
|
|
1406
|
+
if (c.includes(",") || c.includes('"') || c.includes("\n")) {
|
|
1407
|
+
return `"${c.replace(/"/g, '""')}"`;
|
|
1408
|
+
}
|
|
1409
|
+
return c;
|
|
1410
|
+
});
|
|
1411
|
+
rows.push(escaped.join(","));
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
return { tag: "str", val: rows.join("\n") };
|
|
1415
|
+
} catch (e) {
|
|
1416
|
+
return { tag: "err", val: { tag: "str", val: `CSV stringify error: ${String(e)}` } };
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
return { tag: "str", val: "" };
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
// DateTime Functions (3) — B-5
|
|
1423
|
+
case "now": {
|
|
1424
|
+
return { tag: "f64", val: Date.now() };
|
|
1425
|
+
}
|
|
1426
|
+
case "format_date": {
|
|
1427
|
+
if (args[0].tag === "f64" && args[1].tag === "str") {
|
|
1428
|
+
const timestamp = args[0].val;
|
|
1429
|
+
const format = args[1].val;
|
|
1430
|
+
const date = new Date(timestamp);
|
|
1431
|
+
let result = format;
|
|
1432
|
+
result = result.replace(/YYYY/g, date.getFullYear().toString());
|
|
1433
|
+
result = result.replace(/MM/g, String(date.getMonth() + 1).padStart(2, "0"));
|
|
1434
|
+
result = result.replace(/DD/g, String(date.getDate()).padStart(2, "0"));
|
|
1435
|
+
result = result.replace(/HH/g, String(date.getHours()).padStart(2, "0"));
|
|
1436
|
+
result = result.replace(/mm/g, String(date.getMinutes()).padStart(2, "0"));
|
|
1437
|
+
result = result.replace(/ss/g, String(date.getSeconds()).padStart(2, "0"));
|
|
1438
|
+
return { tag: "str", val: result };
|
|
1439
|
+
}
|
|
1440
|
+
return { tag: "str", val: "" };
|
|
1441
|
+
}
|
|
1442
|
+
case "parse_date": {
|
|
1443
|
+
if (args[0].tag === "str" && args[1].tag === "str") {
|
|
1444
|
+
const dateStr = args[0].val;
|
|
1445
|
+
const format = args[1].val;
|
|
1446
|
+
try {
|
|
1447
|
+
// Simple date parsing - support YYYY-MM-DD HH:mm:ss
|
|
1448
|
+
const timestamp = new Date(dateStr).getTime();
|
|
1449
|
+
if (isNaN(timestamp)) {
|
|
1450
|
+
return { tag: "err", val: { tag: "str", val: "Invalid date format" } };
|
|
1451
|
+
}
|
|
1452
|
+
return { tag: "ok", val: { tag: "f64", val: timestamp } };
|
|
1453
|
+
} catch (e) {
|
|
1454
|
+
return { tag: "err", val: { tag: "str", val: `Date parse error: ${String(e)}` } };
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
return { tag: "err", val: { tag: "str", val: "Invalid arguments" } };
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
// YAML Functions (2) — v4.3 Extension
|
|
1461
|
+
case "yaml_parse": {
|
|
1462
|
+
if (args[0].tag === "str") {
|
|
1463
|
+
try {
|
|
1464
|
+
const yaml = args[0].val;
|
|
1465
|
+
const obj = this.parseYAML(yaml);
|
|
1466
|
+
return { tag: "ok", val: this.jsonToValue(obj) };
|
|
1467
|
+
} catch (e) {
|
|
1468
|
+
return { tag: "err", val: { tag: "str", val: `YAML parse error: ${String(e)}` } };
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
return { tag: "err", val: { tag: "str", val: "Invalid arguments" } };
|
|
1472
|
+
}
|
|
1473
|
+
case "yaml_stringify": {
|
|
1474
|
+
try {
|
|
1475
|
+
const yamlStr = this.valueToYAML(args[0], 0);
|
|
1476
|
+
return { tag: "str", val: yamlStr };
|
|
1477
|
+
} catch (e) {
|
|
1478
|
+
return { tag: "err", val: { tag: "str", val: `YAML stringify error: ${String(e)}` } };
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
case "http_server_create": {
|
|
1483
|
+
const port = (args[0] as any).val;
|
|
1484
|
+
try {
|
|
1485
|
+
const result = await this.httpServerCreateAsync(port);
|
|
1486
|
+
return result;
|
|
1487
|
+
} catch (e) {
|
|
1488
|
+
return { tag: "err", val: { tag: "str", val: `HTTP server error: ${String(e)}` } };
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
case "http_server_accept": {
|
|
1493
|
+
const serverId = (args[0] as any).val;
|
|
1494
|
+
try {
|
|
1495
|
+
const result = await this.httpServerAcceptAsync(serverId);
|
|
1496
|
+
return result;
|
|
1497
|
+
} catch (e) {
|
|
1498
|
+
return { tag: "err", val: { tag: "str", val: `HTTP accept error: ${String(e)}` } };
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
case "http_server_respond": {
|
|
1503
|
+
const serverId = (args[0] as any).val;
|
|
1504
|
+
const reqId = (args[1] as any).val;
|
|
1505
|
+
const status = (args[2] as any).val;
|
|
1506
|
+
const headersJson = (args[3] as any).val;
|
|
1507
|
+
const body = this.valueToString(args[4]);
|
|
1508
|
+
try {
|
|
1509
|
+
await this.httpServerRespondAsync(serverId, reqId, status, headersJson, body);
|
|
1510
|
+
return { tag: "void" };
|
|
1511
|
+
} catch (e) {
|
|
1512
|
+
return { tag: "err", val: { tag: "str", val: `HTTP respond error: ${String(e)}` } };
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
case "exec_command": {
|
|
1517
|
+
const cmd = (args[0] as any).val;
|
|
1518
|
+
let cmdArgs: string[] = [];
|
|
1519
|
+
if (args[1].tag === "arr") {
|
|
1520
|
+
cmdArgs = (args[1] as any).val.map((v: Value) => this.valueToString(v));
|
|
1521
|
+
}
|
|
1522
|
+
try {
|
|
1523
|
+
const result = await this.execCommandAsync(cmd, cmdArgs);
|
|
1524
|
+
return result;
|
|
1525
|
+
} catch (e) {
|
|
1526
|
+
return { tag: "err", val: { tag: "str", val: `Command execution error: ${String(e)}` } };
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
case "char_code": {
|
|
1531
|
+
// char_code("A") → 65 (문자의 ASCII 코드)
|
|
1532
|
+
const s = (args[0] as any).val as string;
|
|
1533
|
+
if (s.length === 0) {
|
|
1534
|
+
return { tag: "i32", val: 0 };
|
|
1535
|
+
}
|
|
1536
|
+
return { tag: "i32", val: s.charCodeAt(0) };
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
case "chr": {
|
|
1540
|
+
// chr(65) → "A" (ASCII 코드를 문자로)
|
|
1541
|
+
const n = (args[0] as any).val as number;
|
|
1542
|
+
return { tag: "str", val: String.fromCharCode(n) };
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
default:
|
|
1546
|
+
throw new Error(`panic: unknown builtin '${name}'`);
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
// ============================================================
|
|
1551
|
+
// 유틸리티
|
|
1552
|
+
// ============================================================
|
|
1553
|
+
|
|
1554
|
+
private readI32(actor: Actor): number {
|
|
1555
|
+
const b0 = this.chunk.code[actor.ip++];
|
|
1556
|
+
const b1 = this.chunk.code[actor.ip++];
|
|
1557
|
+
const b2 = this.chunk.code[actor.ip++];
|
|
1558
|
+
const b3 = this.chunk.code[actor.ip++];
|
|
1559
|
+
return b0 | (b1 << 8) | (b2 << 16) | (b3 << 24);
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
private readF64(actor: Actor): number {
|
|
1563
|
+
const buf = new ArrayBuffer(8);
|
|
1564
|
+
const bytes = new Uint8Array(buf);
|
|
1565
|
+
for (let i = 0; i < 8; i++) bytes[i] = this.chunk.code[actor.ip++];
|
|
1566
|
+
return new Float64Array(buf)[0];
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
private valueToString(v: Value): string {
|
|
1570
|
+
switch (v.tag) {
|
|
1571
|
+
case "i32": case "f64": return String(v.val);
|
|
1572
|
+
case "str": return v.val;
|
|
1573
|
+
case "bool": return v.val ? "true" : "false";
|
|
1574
|
+
case "void": return "void";
|
|
1575
|
+
case "none": return "None";
|
|
1576
|
+
case "arr": return `[${v.val.map((e) => this.valueToString(e)).join(", ")}]`;
|
|
1577
|
+
case "struct": {
|
|
1578
|
+
const entries = [...v.fields.entries()].map(([k, val]) => `${k}: ${this.valueToString(val)}`);
|
|
1579
|
+
return `{ ${entries.join(", ")} }`;
|
|
1580
|
+
}
|
|
1581
|
+
case "ok": return `Ok(${this.valueToString(v.val)})`;
|
|
1582
|
+
case "err": return `Err(${this.valueToString(v.val)})`;
|
|
1583
|
+
case "some": return `Some(${this.valueToString(v.val)})`;
|
|
1584
|
+
case "chan": return `channel(${v.id})`;
|
|
1585
|
+
case "db": return `database(${v.id})`;
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
private valuesEqual(a: Value, b: Value): boolean {
|
|
1590
|
+
if (a.tag !== b.tag) return false;
|
|
1591
|
+
if (a.tag === "void" && b.tag === "void") return true;
|
|
1592
|
+
if (a.tag === "none" && b.tag === "none") return true;
|
|
1593
|
+
if ("val" in a && "val" in b) return (a as any).val === (b as any).val;
|
|
1594
|
+
return false;
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
private deepClone(v: Value): Value {
|
|
1598
|
+
switch (v.tag) {
|
|
1599
|
+
case "arr": return { tag: "arr", val: v.val.map((e) => this.deepClone(e)) };
|
|
1600
|
+
case "struct": {
|
|
1601
|
+
const fields = new Map<string, Value>();
|
|
1602
|
+
for (const [k, val] of v.fields) fields.set(k, this.deepClone(val));
|
|
1603
|
+
return { tag: "struct", fields };
|
|
1604
|
+
}
|
|
1605
|
+
case "ok": return { tag: "ok", val: this.deepClone(v.val) };
|
|
1606
|
+
case "err": return { tag: "err", val: this.deepClone(v.val) };
|
|
1607
|
+
case "some": return { tag: "some", val: this.deepClone(v.val) };
|
|
1608
|
+
default: return v; // Copy 타입은 그대로
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
// ============================================================
|
|
1613
|
+
// JSON Conversion (Phase 7)
|
|
1614
|
+
// ============================================================
|
|
1615
|
+
|
|
1616
|
+
private jsonToValue(obj: any): Value {
|
|
1617
|
+
if (obj === null) return { tag: "none" };
|
|
1618
|
+
if (typeof obj === "boolean") return { tag: "bool", val: obj };
|
|
1619
|
+
if (typeof obj === "number") return { tag: "i32", val: Math.floor(obj) };
|
|
1620
|
+
if (typeof obj === "string") return { tag: "str", val: obj };
|
|
1621
|
+
if (Array.isArray(obj)) {
|
|
1622
|
+
return { tag: "arr", val: obj.map((v) => this.jsonToValue(v)) };
|
|
1623
|
+
}
|
|
1624
|
+
if (typeof obj === "object") {
|
|
1625
|
+
const fields = new Map<string, Value>();
|
|
1626
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
1627
|
+
fields.set(k, this.jsonToValue(v));
|
|
1628
|
+
}
|
|
1629
|
+
return { tag: "struct", fields };
|
|
1630
|
+
}
|
|
1631
|
+
return { tag: "void" };
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
private valueToJSON(v: Value): any {
|
|
1635
|
+
switch (v.tag) {
|
|
1636
|
+
case "i32":
|
|
1637
|
+
case "f64":
|
|
1638
|
+
return v.val;
|
|
1639
|
+
case "bool":
|
|
1640
|
+
return v.val;
|
|
1641
|
+
case "str":
|
|
1642
|
+
return v.val;
|
|
1643
|
+
case "arr":
|
|
1644
|
+
return v.val.map((item) => this.valueToJSON(item));
|
|
1645
|
+
case "struct":
|
|
1646
|
+
const obj: any = {};
|
|
1647
|
+
for (const [k, val] of v.fields.entries()) {
|
|
1648
|
+
obj[k] = this.valueToJSON(val);
|
|
1649
|
+
}
|
|
1650
|
+
return obj;
|
|
1651
|
+
case "ok":
|
|
1652
|
+
return this.valueToJSON(v.val);
|
|
1653
|
+
case "err":
|
|
1654
|
+
return { error: this.valueToJSON(v.val) };
|
|
1655
|
+
case "some":
|
|
1656
|
+
return this.valueToJSON(v.val);
|
|
1657
|
+
case "none":
|
|
1658
|
+
return null;
|
|
1659
|
+
default:
|
|
1660
|
+
return null;
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
private parseYAML(yaml: string): any {
|
|
1665
|
+
const lines = yaml.split("\n").map((l) => l);
|
|
1666
|
+
const result: any = {};
|
|
1667
|
+
let currentObj: any = result;
|
|
1668
|
+
const stack: any[] = [result];
|
|
1669
|
+
let lastIndent = -1;
|
|
1670
|
+
|
|
1671
|
+
for (const line of lines) {
|
|
1672
|
+
if (!line.trim() || line.trim().startsWith("#")) continue;
|
|
1673
|
+
|
|
1674
|
+
const indent = line.length - line.trimLeft().length;
|
|
1675
|
+
const trimmed = line.trim();
|
|
1676
|
+
|
|
1677
|
+
// Handle indent changes
|
|
1678
|
+
if (indent > lastIndent) {
|
|
1679
|
+
// Push to stack
|
|
1680
|
+
if (lastIndent >= 0 && typeof currentObj === "object") {
|
|
1681
|
+
stack.push(currentObj);
|
|
1682
|
+
}
|
|
1683
|
+
} else if (indent < lastIndent) {
|
|
1684
|
+
// Pop from stack
|
|
1685
|
+
while (stack.length > 1 && indent < lastIndent) {
|
|
1686
|
+
stack.pop();
|
|
1687
|
+
currentObj = stack[stack.length - 1];
|
|
1688
|
+
lastIndent -= 2;
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
lastIndent = indent;
|
|
1693
|
+
|
|
1694
|
+
// Parse key: value
|
|
1695
|
+
if (trimmed.includes(":")) {
|
|
1696
|
+
const colonIdx = trimmed.indexOf(":");
|
|
1697
|
+
const key = trimmed.substring(0, colonIdx).trim();
|
|
1698
|
+
const valueStr = trimmed.substring(colonIdx + 1).trim();
|
|
1699
|
+
|
|
1700
|
+
if (valueStr === "") {
|
|
1701
|
+
// Nested object
|
|
1702
|
+
currentObj[key] = {};
|
|
1703
|
+
currentObj = currentObj[key];
|
|
1704
|
+
} else {
|
|
1705
|
+
// Scalar value
|
|
1706
|
+
currentObj[key] = this.parseYAMLValue(valueStr);
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
return result;
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
private parseYAMLValue(valueStr: string): any {
|
|
1715
|
+
if (valueStr === "true") return true;
|
|
1716
|
+
if (valueStr === "false") return false;
|
|
1717
|
+
if (valueStr === "null") return null;
|
|
1718
|
+
if (!isNaN(Number(valueStr))) return Number(valueStr);
|
|
1719
|
+
if (valueStr.startsWith('"') && valueStr.endsWith('"')) {
|
|
1720
|
+
return valueStr.slice(1, -1).replace(/\\"/g, '"');
|
|
1721
|
+
}
|
|
1722
|
+
if (valueStr.startsWith("'") && valueStr.endsWith("'")) {
|
|
1723
|
+
return valueStr.slice(1, -1);
|
|
1724
|
+
}
|
|
1725
|
+
return valueStr;
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
private valueToYAML(v: Value, indent: number = 0): string {
|
|
1729
|
+
const indentStr = " ".repeat(indent);
|
|
1730
|
+
const nextIndentStr = " ".repeat(indent + 1);
|
|
1731
|
+
|
|
1732
|
+
switch (v.tag) {
|
|
1733
|
+
case "i32":
|
|
1734
|
+
case "f64":
|
|
1735
|
+
case "bool":
|
|
1736
|
+
return String((v as any).val);
|
|
1737
|
+
case "str":
|
|
1738
|
+
return `"${(v as any).val.replace(/"/g, '\\"')}"`;
|
|
1739
|
+
case "arr": {
|
|
1740
|
+
const items = (v as any).val as Value[];
|
|
1741
|
+
if (items.length === 0) return "[]";
|
|
1742
|
+
return "[\n" + items.map((item) => nextIndentStr + this.valueToYAML(item, indent + 1)).join(",\n") + "\n" + indentStr + "]";
|
|
1743
|
+
}
|
|
1744
|
+
case "struct": {
|
|
1745
|
+
const fields = (v as any).fields as Map<string, Value>;
|
|
1746
|
+
const lines: string[] = [];
|
|
1747
|
+
for (const [key, val] of fields) {
|
|
1748
|
+
lines.push(`${key}: ${this.valueToYAML(val, indent + 1)}`);
|
|
1749
|
+
}
|
|
1750
|
+
return lines.join("\n" + nextIndentStr);
|
|
1751
|
+
}
|
|
1752
|
+
case "ok":
|
|
1753
|
+
return this.valueToYAML((v as any).val, indent);
|
|
1754
|
+
case "err":
|
|
1755
|
+
return `error: ${this.valueToYAML((v as any).val, indent)}`;
|
|
1756
|
+
case "some":
|
|
1757
|
+
return this.valueToYAML((v as any).val, indent);
|
|
1758
|
+
case "none":
|
|
1759
|
+
return "null";
|
|
1760
|
+
default:
|
|
1761
|
+
return "null";
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
private parseCsvRow(line: string): string[] {
|
|
1766
|
+
const cells: string[] = [];
|
|
1767
|
+
let current = "";
|
|
1768
|
+
let inQuotes = false;
|
|
1769
|
+
for (let i = 0; i < line.length; i++) {
|
|
1770
|
+
const char = line[i];
|
|
1771
|
+
if (char === '"' && (i === 0 || line[i - 1] !== "\\")) {
|
|
1772
|
+
inQuotes = !inQuotes;
|
|
1773
|
+
} else if (char === "," && !inQuotes) {
|
|
1774
|
+
cells.push(current);
|
|
1775
|
+
current = "";
|
|
1776
|
+
} else {
|
|
1777
|
+
current += char;
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
cells.push(current);
|
|
1781
|
+
return cells;
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
// ============================================================
|
|
1785
|
+
// HTTP Client Implementation (Phase 2) — fetch based (async)
|
|
1786
|
+
// ============================================================
|
|
1787
|
+
|
|
1788
|
+
private async httpGetAsync(url: string): Promise<Value> {
|
|
1789
|
+
try {
|
|
1790
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
|
|
1791
|
+
const body = await res.text();
|
|
1792
|
+
return { tag: "ok", val: { tag: "str", val: body } };
|
|
1793
|
+
} catch (e: any) {
|
|
1794
|
+
return { tag: "err", val: { tag: "str", val: e.message } };
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
private async httpPostAsync(url: string, body: string): Promise<Value> {
|
|
1799
|
+
try {
|
|
1800
|
+
const res = await fetch(url, {
|
|
1801
|
+
method: "POST",
|
|
1802
|
+
body,
|
|
1803
|
+
signal: AbortSignal.timeout(5000),
|
|
1804
|
+
});
|
|
1805
|
+
const responseBody = await res.text();
|
|
1806
|
+
return { tag: "ok", val: { tag: "str", val: responseBody } };
|
|
1807
|
+
} catch (e: any) {
|
|
1808
|
+
return { tag: "err", val: { tag: "str", val: e.message } };
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
private async httpPostJsonAsync(url: string, jsonBody: string): Promise<Value> {
|
|
1813
|
+
try {
|
|
1814
|
+
const res = await fetch(url, {
|
|
1815
|
+
method: "POST",
|
|
1816
|
+
headers: { "Content-Type": "application/json" },
|
|
1817
|
+
body: jsonBody,
|
|
1818
|
+
signal: AbortSignal.timeout(5000),
|
|
1819
|
+
});
|
|
1820
|
+
const responseBody = await res.text();
|
|
1821
|
+
return { tag: "ok", val: { tag: "str", val: responseBody } };
|
|
1822
|
+
} catch (e: any) {
|
|
1823
|
+
return { tag: "err", val: { tag: "str", val: e.message } };
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
private async fetchAsync(url: string, method: string, headers: Value | null, body: string | null): Promise<Value> {
|
|
1828
|
+
try {
|
|
1829
|
+
const options: any = { method, signal: AbortSignal.timeout(5000) };
|
|
1830
|
+
|
|
1831
|
+
if (headers && headers.tag === "struct") {
|
|
1832
|
+
options.headers = {};
|
|
1833
|
+
for (const [k, v] of headers.fields) {
|
|
1834
|
+
options.headers[k] = this.valueToString(v);
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
if (body) {
|
|
1839
|
+
options.body = body;
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
const res = await fetch(url, options);
|
|
1843
|
+
const responseBody = await res.text();
|
|
1844
|
+
return { tag: "ok", val: { tag: "str", val: responseBody } };
|
|
1845
|
+
} catch (e: any) {
|
|
1846
|
+
return { tag: "err", val: { tag: "str", val: e.message } };
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
// ============================================================
|
|
1851
|
+
// HTTP Server Builtins
|
|
1852
|
+
// ============================================================
|
|
1853
|
+
|
|
1854
|
+
private async httpServerCreateAsync(port: number): Promise<Value> {
|
|
1855
|
+
try {
|
|
1856
|
+
const express = (await import("express")).default;
|
|
1857
|
+
const app = express();
|
|
1858
|
+
app.use(express.json());
|
|
1859
|
+
app.use(express.urlencoded({ extended: true }));
|
|
1860
|
+
|
|
1861
|
+
const requestQueue: Array<{ reqId: string; method: string; path: string; headers: any; body: any; query: any }> = [];
|
|
1862
|
+
const responseMap: Map<string, any> = new Map(); // reqId -> res
|
|
1863
|
+
let nextReqId = 0;
|
|
1864
|
+
|
|
1865
|
+
app.all("*", (req: any, res: any) => {
|
|
1866
|
+
const reqId = String(nextReqId++);
|
|
1867
|
+
const reqObj = {
|
|
1868
|
+
reqId,
|
|
1869
|
+
method: req.method,
|
|
1870
|
+
path: req.path,
|
|
1871
|
+
headers: req.headers,
|
|
1872
|
+
body: req.body || {},
|
|
1873
|
+
query: req.query || {},
|
|
1874
|
+
};
|
|
1875
|
+
requestQueue.push(reqObj);
|
|
1876
|
+
responseMap.set(reqId, res);
|
|
1877
|
+
});
|
|
1878
|
+
|
|
1879
|
+
const serverId = this.nextServerId++;
|
|
1880
|
+
this.httpServers.set(serverId, {
|
|
1881
|
+
app,
|
|
1882
|
+
server: null,
|
|
1883
|
+
requestQueue,
|
|
1884
|
+
responseMap,
|
|
1885
|
+
port,
|
|
1886
|
+
});
|
|
1887
|
+
|
|
1888
|
+
// Start server
|
|
1889
|
+
const server = app.listen(port);
|
|
1890
|
+
const serverData = this.httpServers.get(serverId)!;
|
|
1891
|
+
serverData.server = server;
|
|
1892
|
+
|
|
1893
|
+
return { tag: "ok", val: { tag: "i32", val: serverId } };
|
|
1894
|
+
} catch (e: any) {
|
|
1895
|
+
return { tag: "err", val: { tag: "str", val: e.message } };
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
private async httpServerAcceptAsync(serverId: number): Promise<Value> {
|
|
1900
|
+
const serverData = this.httpServers.get(serverId);
|
|
1901
|
+
if (!serverData) {
|
|
1902
|
+
return { tag: "err", val: { tag: "str", val: "Server not found" } };
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
// Poll for requests
|
|
1906
|
+
while (serverData.requestQueue.length === 0) {
|
|
1907
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
const req = serverData.requestQueue.shift()!;
|
|
1911
|
+
|
|
1912
|
+
// Return as JSON string that can be parsed by FreeLang
|
|
1913
|
+
const reqJson = JSON.stringify({
|
|
1914
|
+
id: req.reqId,
|
|
1915
|
+
method: req.method,
|
|
1916
|
+
path: req.path,
|
|
1917
|
+
query: req.query,
|
|
1918
|
+
body: typeof req.body === "string" ? req.body : JSON.stringify(req.body),
|
|
1919
|
+
});
|
|
1920
|
+
|
|
1921
|
+
return { tag: "ok", val: { tag: "str", val: reqJson } };
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
private async httpServerRespondAsync(serverId: number, reqId: string, status: number, headersJson: string, body: string): Promise<void> {
|
|
1925
|
+
const serverData = this.httpServers.get(serverId);
|
|
1926
|
+
if (!serverData) throw new Error("Server not found");
|
|
1927
|
+
|
|
1928
|
+
const res = serverData.responseMap.get(reqId);
|
|
1929
|
+
if (!res) throw new Error(`Request ${reqId} not found`);
|
|
1930
|
+
|
|
1931
|
+
try {
|
|
1932
|
+
const headers = JSON.parse(headersJson);
|
|
1933
|
+
res.status(status);
|
|
1934
|
+
Object.entries(headers).forEach(([k, v]: [string, any]) => {
|
|
1935
|
+
res.set(k, v);
|
|
1936
|
+
});
|
|
1937
|
+
res.send(body);
|
|
1938
|
+
} finally {
|
|
1939
|
+
serverData.responseMap.delete(reqId);
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
private async execCommandAsync(cmd: string, args: string[]): Promise<Value> {
|
|
1944
|
+
try {
|
|
1945
|
+
const { spawn } = await import("child_process");
|
|
1946
|
+
const proc = spawn(cmd, args);
|
|
1947
|
+
|
|
1948
|
+
let stdout = "";
|
|
1949
|
+
let stderr = "";
|
|
1950
|
+
|
|
1951
|
+
return new Promise((resolve) => {
|
|
1952
|
+
proc.stdout?.on("data", (data) => {
|
|
1953
|
+
stdout += data.toString();
|
|
1954
|
+
});
|
|
1955
|
+
|
|
1956
|
+
proc.stderr?.on("data", (data) => {
|
|
1957
|
+
stderr += data.toString();
|
|
1958
|
+
});
|
|
1959
|
+
|
|
1960
|
+
proc.on("close", (code) => {
|
|
1961
|
+
if (code === 0) {
|
|
1962
|
+
resolve({ tag: "ok", val: { tag: "str", val: stdout } });
|
|
1963
|
+
} else {
|
|
1964
|
+
resolve({ tag: "err", val: { tag: "str", val: stderr || `Exit code ${code}` } });
|
|
1965
|
+
}
|
|
1966
|
+
});
|
|
1967
|
+
|
|
1968
|
+
proc.on("error", (err) => {
|
|
1969
|
+
resolve({ tag: "err", val: { tag: "str", val: err.message } });
|
|
1970
|
+
});
|
|
1971
|
+
});
|
|
1972
|
+
} catch (e: any) {
|
|
1973
|
+
return { tag: "err", val: { tag: "str", val: e.message } };
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
}
|