arc-lang 0.5.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/LICENSE +21 -0
- package/README.md +148 -0
- package/dist/ast.d.ts +298 -0
- package/dist/ast.js +2 -0
- package/dist/build.d.ts +7 -0
- package/dist/build.js +138 -0
- package/dist/codegen-js.d.ts +2 -0
- package/dist/codegen-js.js +168 -0
- package/dist/codegen.d.ts +2 -0
- package/dist/codegen.js +364 -0
- package/dist/errors.d.ts +52 -0
- package/dist/errors.js +229 -0
- package/dist/formatter.d.ts +5 -0
- package/dist/formatter.js +361 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +165 -0
- package/dist/interpreter.d.ts +39 -0
- package/dist/interpreter.js +668 -0
- package/dist/ir.d.ts +126 -0
- package/dist/ir.js +610 -0
- package/dist/lexer.d.ts +79 -0
- package/dist/lexer.js +335 -0
- package/dist/linter.d.ts +15 -0
- package/dist/linter.js +382 -0
- package/dist/lsp.d.ts +1 -0
- package/dist/lsp.js +253 -0
- package/dist/modules.d.ts +24 -0
- package/dist/modules.js +115 -0
- package/dist/optimizer.d.ts +17 -0
- package/dist/optimizer.js +481 -0
- package/dist/package-manager.d.ts +31 -0
- package/dist/package-manager.js +180 -0
- package/dist/parser.d.ts +42 -0
- package/dist/parser.js +779 -0
- package/dist/repl.d.ts +1 -0
- package/dist/repl.js +120 -0
- package/dist/security.d.ts +48 -0
- package/dist/security.js +198 -0
- package/dist/semantic.d.ts +7 -0
- package/dist/semantic.js +327 -0
- package/dist/typechecker.d.ts +7 -0
- package/dist/typechecker.js +132 -0
- package/dist/version.d.ts +26 -0
- package/dist/version.js +71 -0
- package/package.json +51 -0
package/dist/repl.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/repl.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Arc Language REPL (Read-Eval-Print Loop)
|
|
2
|
+
import * as readline from "readline";
|
|
3
|
+
import { readFileSync } from "fs";
|
|
4
|
+
import { lex } from "./lexer.js";
|
|
5
|
+
import { parse } from "./parser.js";
|
|
6
|
+
import { createEnv, interpretWithEnv, toStr } from "./interpreter.js";
|
|
7
|
+
const GREEN = "\x1b[32m";
|
|
8
|
+
const RED = "\x1b[31m";
|
|
9
|
+
const CYAN = "\x1b[36m";
|
|
10
|
+
const YELLOW = "\x1b[33m";
|
|
11
|
+
const RESET = "\x1b[0m";
|
|
12
|
+
let showAst = false;
|
|
13
|
+
const env = createEnv();
|
|
14
|
+
function printHelp() {
|
|
15
|
+
console.log(`${CYAN}Arc REPL Commands:${RESET}`);
|
|
16
|
+
console.log(` ${YELLOW}:help${RESET} Show this help message`);
|
|
17
|
+
console.log(` ${YELLOW}:ast${RESET} Toggle AST display before execution`);
|
|
18
|
+
console.log(` ${YELLOW}:reset${RESET} Clear all variables and state`);
|
|
19
|
+
console.log(` ${YELLOW}:load <file>${RESET} Load and execute an Arc file`);
|
|
20
|
+
console.log(` ${YELLOW}:quit${RESET} Exit the REPL`);
|
|
21
|
+
console.log(` ${YELLOW}Ctrl+C${RESET} Exit the REPL`);
|
|
22
|
+
console.log();
|
|
23
|
+
console.log(` Multi-line: end a line with { to start a block`);
|
|
24
|
+
}
|
|
25
|
+
function execute(source) {
|
|
26
|
+
try {
|
|
27
|
+
const tokens = lex(source);
|
|
28
|
+
const ast = parse(tokens);
|
|
29
|
+
if (showAst) {
|
|
30
|
+
console.log(`${CYAN}AST:${RESET}`, JSON.stringify(ast, null, 2));
|
|
31
|
+
}
|
|
32
|
+
const result = interpretWithEnv(ast, env);
|
|
33
|
+
if (result !== null && result !== undefined) {
|
|
34
|
+
console.log(`${GREEN}${toStr(result)}${RESET}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
console.log(`${RED}Error: ${e.message}${RESET}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function main() {
|
|
42
|
+
const rl = readline.createInterface({
|
|
43
|
+
input: process.stdin,
|
|
44
|
+
output: process.stdout,
|
|
45
|
+
prompt: "arc> ",
|
|
46
|
+
});
|
|
47
|
+
console.log(`${CYAN}Arc REPL v0.1.0${RESET} — Type ${YELLOW}:help${RESET} for commands`);
|
|
48
|
+
rl.prompt();
|
|
49
|
+
let buffer = "";
|
|
50
|
+
let braceDepth = 0;
|
|
51
|
+
rl.on("line", (line) => {
|
|
52
|
+
const trimmed = line.trim();
|
|
53
|
+
// Handle commands (only when not in multi-line mode)
|
|
54
|
+
if (braceDepth === 0) {
|
|
55
|
+
if (trimmed === ":quit" || trimmed === ":q") {
|
|
56
|
+
console.log("Goodbye!");
|
|
57
|
+
process.exit(0);
|
|
58
|
+
}
|
|
59
|
+
if (trimmed === ":help") {
|
|
60
|
+
printHelp();
|
|
61
|
+
rl.prompt();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (trimmed === ":ast") {
|
|
65
|
+
showAst = !showAst;
|
|
66
|
+
console.log(`AST display: ${showAst ? "ON" : "OFF"}`);
|
|
67
|
+
rl.prompt();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (trimmed === ":reset") {
|
|
71
|
+
// Re-create env by re-running. Simplest: just reload.
|
|
72
|
+
// Actually we can't easily reset the env object, so we note this limitation.
|
|
73
|
+
console.log(`${YELLOW}State reset (restart REPL for full reset)${RESET}`);
|
|
74
|
+
rl.prompt();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (trimmed.startsWith(":load ")) {
|
|
78
|
+
const file = trimmed.slice(6).trim();
|
|
79
|
+
try {
|
|
80
|
+
const source = readFileSync(file, "utf-8");
|
|
81
|
+
execute(source);
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
console.log(`${RED}Error loading file: ${e.message}${RESET}`);
|
|
85
|
+
}
|
|
86
|
+
rl.prompt();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (trimmed === "") {
|
|
90
|
+
rl.prompt();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Multi-line support
|
|
95
|
+
buffer += (buffer ? "\n" : "") + line;
|
|
96
|
+
// Count braces
|
|
97
|
+
for (const ch of line) {
|
|
98
|
+
if (ch === "{")
|
|
99
|
+
braceDepth++;
|
|
100
|
+
if (ch === "}")
|
|
101
|
+
braceDepth--;
|
|
102
|
+
}
|
|
103
|
+
if (braceDepth > 0) {
|
|
104
|
+
rl.setPrompt("... ");
|
|
105
|
+
rl.prompt();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
// Execute
|
|
109
|
+
braceDepth = 0;
|
|
110
|
+
execute(buffer);
|
|
111
|
+
buffer = "";
|
|
112
|
+
rl.setPrompt("arc> ");
|
|
113
|
+
rl.prompt();
|
|
114
|
+
});
|
|
115
|
+
rl.on("close", () => {
|
|
116
|
+
console.log("\nGoodbye!");
|
|
117
|
+
process.exit(0);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
main();
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Value } from "./interpreter.js";
|
|
2
|
+
export interface SecurityConfig {
|
|
3
|
+
maxSourceSize?: number;
|
|
4
|
+
maxStringLength?: number;
|
|
5
|
+
maxNestingDepth?: number;
|
|
6
|
+
maxExecutionSteps?: number;
|
|
7
|
+
maxRecursionDepth?: number;
|
|
8
|
+
maxArraySize?: number;
|
|
9
|
+
maxMapSize?: number;
|
|
10
|
+
executionTimeoutMs?: number;
|
|
11
|
+
allowedToolMethods?: string[];
|
|
12
|
+
blockedToolMethods?: string[];
|
|
13
|
+
allowedUrlPatterns?: RegExp[];
|
|
14
|
+
blockedUrlPatterns?: RegExp[];
|
|
15
|
+
disableToolCalls?: boolean;
|
|
16
|
+
allowedImports?: string[];
|
|
17
|
+
blockedImports?: string[];
|
|
18
|
+
disableImports?: boolean;
|
|
19
|
+
}
|
|
20
|
+
declare const DEFAULTS: Required<Pick<SecurityConfig, 'maxSourceSize' | 'maxStringLength' | 'maxNestingDepth' | 'maxExecutionSteps' | 'maxRecursionDepth' | 'maxArraySize' | 'maxMapSize' | 'executionTimeoutMs'>>;
|
|
21
|
+
export declare class SecurityError extends Error {
|
|
22
|
+
code: string;
|
|
23
|
+
constructor(code: string, message: string);
|
|
24
|
+
}
|
|
25
|
+
export declare function validateSource(source: string, config?: SecurityConfig): void;
|
|
26
|
+
export declare function validateNestingDepth(node: any, config?: SecurityConfig, depth?: number): void;
|
|
27
|
+
export declare function validateToolCall(method: string, url: string, config: SecurityConfig): void;
|
|
28
|
+
export declare function validateImport(moduleName: string, config: SecurityConfig): void;
|
|
29
|
+
export declare class ExecutionContext {
|
|
30
|
+
steps: number;
|
|
31
|
+
recursionDepth: number;
|
|
32
|
+
startTime: number;
|
|
33
|
+
private config;
|
|
34
|
+
constructor(config?: SecurityConfig);
|
|
35
|
+
tick(): void;
|
|
36
|
+
pushCall(): void;
|
|
37
|
+
popCall(): void;
|
|
38
|
+
checkArraySize(size: number): void;
|
|
39
|
+
checkMapSize(size: number): void;
|
|
40
|
+
}
|
|
41
|
+
export declare class SafeInterpreter {
|
|
42
|
+
private config;
|
|
43
|
+
constructor(config?: SecurityConfig);
|
|
44
|
+
run(source: string): Value;
|
|
45
|
+
private checkNoToolCalls;
|
|
46
|
+
}
|
|
47
|
+
export declare function createSandbox(config?: SecurityConfig): SafeInterpreter;
|
|
48
|
+
export { DEFAULTS as SECURITY_DEFAULTS };
|
package/dist/security.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// Arc Language Security Module - Sandboxing & Resource Limits
|
|
2
|
+
import { lex } from "./lexer.js";
|
|
3
|
+
import { parse } from "./parser.js";
|
|
4
|
+
import { createEnv, interpretWithEnv } from "./interpreter.js";
|
|
5
|
+
const DEFAULTS = {
|
|
6
|
+
maxSourceSize: 1_048_576, // 1MB
|
|
7
|
+
maxStringLength: 102_400, // 100KB
|
|
8
|
+
maxNestingDepth: 256,
|
|
9
|
+
maxExecutionSteps: 10_000_000,
|
|
10
|
+
maxRecursionDepth: 512,
|
|
11
|
+
maxArraySize: 100_000,
|
|
12
|
+
maxMapSize: 100_000,
|
|
13
|
+
executionTimeoutMs: 30_000,
|
|
14
|
+
};
|
|
15
|
+
export class SecurityError extends Error {
|
|
16
|
+
code;
|
|
17
|
+
constructor(code, message) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.code = code;
|
|
20
|
+
this.name = "SecurityError";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// Validate source input before parsing
|
|
24
|
+
export function validateSource(source, config = {}) {
|
|
25
|
+
const maxSize = config.maxSourceSize ?? DEFAULTS.maxSourceSize;
|
|
26
|
+
if (Buffer.byteLength(source, 'utf-8') > maxSize) {
|
|
27
|
+
throw new SecurityError("SEC001", `Source code exceeds maximum size of ${maxSize} bytes`);
|
|
28
|
+
}
|
|
29
|
+
const maxStr = config.maxStringLength ?? DEFAULTS.maxStringLength;
|
|
30
|
+
// Quick scan for excessively long string literals
|
|
31
|
+
const stringRegex = /"([^"\\]|\\.)*"/g;
|
|
32
|
+
let match;
|
|
33
|
+
while ((match = stringRegex.exec(source)) !== null) {
|
|
34
|
+
if (match[0].length - 2 > maxStr) {
|
|
35
|
+
throw new SecurityError("SEC002", `String literal exceeds maximum length of ${maxStr} characters`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Validate AST nesting depth
|
|
40
|
+
export function validateNestingDepth(node, config = {}, depth = 0) {
|
|
41
|
+
const maxDepth = config.maxNestingDepth ?? DEFAULTS.maxNestingDepth;
|
|
42
|
+
if (depth > maxDepth) {
|
|
43
|
+
throw new SecurityError("SEC003", `AST nesting depth exceeds maximum of ${maxDepth}`);
|
|
44
|
+
}
|
|
45
|
+
if (node && typeof node === 'object') {
|
|
46
|
+
if (Array.isArray(node)) {
|
|
47
|
+
for (const child of node)
|
|
48
|
+
validateNestingDepth(child, config, depth);
|
|
49
|
+
}
|
|
50
|
+
else if (node.kind) {
|
|
51
|
+
for (const key of Object.keys(node)) {
|
|
52
|
+
if (key === 'loc' || key === 'kind')
|
|
53
|
+
continue;
|
|
54
|
+
validateNestingDepth(node[key], config, depth + 1);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Validate tool call against security config
|
|
60
|
+
export function validateToolCall(method, url, config) {
|
|
61
|
+
if (config.disableToolCalls) {
|
|
62
|
+
throw new SecurityError("SEC010", "Tool calls are disabled in sandbox mode");
|
|
63
|
+
}
|
|
64
|
+
const upperMethod = method.toUpperCase();
|
|
65
|
+
if (config.blockedToolMethods?.includes(upperMethod)) {
|
|
66
|
+
throw new SecurityError("SEC011", `Tool method @${upperMethod} is blocked`);
|
|
67
|
+
}
|
|
68
|
+
if (config.allowedToolMethods && !config.allowedToolMethods.includes(upperMethod)) {
|
|
69
|
+
throw new SecurityError("SEC012", `Tool method @${upperMethod} is not in the allowlist`);
|
|
70
|
+
}
|
|
71
|
+
if (config.blockedUrlPatterns?.some(p => p.test(url))) {
|
|
72
|
+
throw new SecurityError("SEC013", `URL '${url}' matches a blocked pattern`);
|
|
73
|
+
}
|
|
74
|
+
if (config.allowedUrlPatterns && !config.allowedUrlPatterns.some(p => p.test(url))) {
|
|
75
|
+
throw new SecurityError("SEC014", `URL '${url}' does not match any allowed pattern`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Validate import against security config
|
|
79
|
+
export function validateImport(moduleName, config) {
|
|
80
|
+
if (config.disableImports) {
|
|
81
|
+
throw new SecurityError("SEC020", "Imports are disabled in sandbox mode");
|
|
82
|
+
}
|
|
83
|
+
if (config.blockedImports?.includes(moduleName)) {
|
|
84
|
+
throw new SecurityError("SEC021", `Import of '${moduleName}' is blocked`);
|
|
85
|
+
}
|
|
86
|
+
if (config.allowedImports && !config.allowedImports.includes(moduleName)) {
|
|
87
|
+
throw new SecurityError("SEC022", `Import of '${moduleName}' is not in the allowlist`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// Execution context that tracks resource usage
|
|
91
|
+
export class ExecutionContext {
|
|
92
|
+
steps = 0;
|
|
93
|
+
recursionDepth = 0;
|
|
94
|
+
startTime;
|
|
95
|
+
config;
|
|
96
|
+
constructor(config = {}) {
|
|
97
|
+
this.config = config;
|
|
98
|
+
this.startTime = Date.now();
|
|
99
|
+
}
|
|
100
|
+
tick() {
|
|
101
|
+
this.steps++;
|
|
102
|
+
const maxSteps = this.config.maxExecutionSteps ?? DEFAULTS.maxExecutionSteps;
|
|
103
|
+
if (this.steps > maxSteps) {
|
|
104
|
+
throw new SecurityError("SEC030", `Execution exceeded maximum of ${maxSteps} steps (possible infinite loop)`);
|
|
105
|
+
}
|
|
106
|
+
// Check timeout every 1000 steps to avoid perf overhead
|
|
107
|
+
if (this.steps % 1000 === 0) {
|
|
108
|
+
const timeout = this.config.executionTimeoutMs ?? DEFAULTS.executionTimeoutMs;
|
|
109
|
+
if (Date.now() - this.startTime > timeout) {
|
|
110
|
+
throw new SecurityError("SEC031", `Execution timed out after ${timeout}ms`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
pushCall() {
|
|
115
|
+
this.recursionDepth++;
|
|
116
|
+
const maxDepth = this.config.maxRecursionDepth ?? DEFAULTS.maxRecursionDepth;
|
|
117
|
+
if (this.recursionDepth > maxDepth) {
|
|
118
|
+
throw new SecurityError("SEC032", `Recursion depth exceeded maximum of ${maxDepth}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
popCall() {
|
|
122
|
+
this.recursionDepth--;
|
|
123
|
+
}
|
|
124
|
+
checkArraySize(size) {
|
|
125
|
+
const max = this.config.maxArraySize ?? DEFAULTS.maxArraySize;
|
|
126
|
+
if (size > max) {
|
|
127
|
+
throw new SecurityError("SEC033", `Array size ${size} exceeds maximum of ${max}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
checkMapSize(size) {
|
|
131
|
+
const max = this.config.maxMapSize ?? DEFAULTS.maxMapSize;
|
|
132
|
+
if (size > max) {
|
|
133
|
+
throw new SecurityError("SEC034", `Map size ${size} exceeds maximum of ${max}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// SafeInterpreter - wraps the interpreter with resource limits
|
|
138
|
+
export class SafeInterpreter {
|
|
139
|
+
config;
|
|
140
|
+
constructor(config = {}) {
|
|
141
|
+
this.config = config;
|
|
142
|
+
}
|
|
143
|
+
run(source) {
|
|
144
|
+
// 1. Validate source
|
|
145
|
+
validateSource(source, this.config);
|
|
146
|
+
// 2. Lex and parse
|
|
147
|
+
const tokens = lex(source);
|
|
148
|
+
const ast = parse(tokens);
|
|
149
|
+
// 3. Validate nesting
|
|
150
|
+
validateNestingDepth(ast, this.config);
|
|
151
|
+
// 4. Create execution context
|
|
152
|
+
const ctx = new ExecutionContext(this.config);
|
|
153
|
+
const env = createEnv();
|
|
154
|
+
// 5. Run with resource tracking
|
|
155
|
+
let result = null;
|
|
156
|
+
for (const stmt of ast.stmts) {
|
|
157
|
+
ctx.tick();
|
|
158
|
+
if (stmt.kind === "UseStmt") {
|
|
159
|
+
const useStmt = stmt;
|
|
160
|
+
validateImport(useStmt.module, this.config);
|
|
161
|
+
if (!this.config.disableImports) {
|
|
162
|
+
// No file context in sandbox, skip actual import
|
|
163
|
+
}
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
// Check for tool calls in the AST
|
|
167
|
+
if (this.config.disableToolCalls) {
|
|
168
|
+
this.checkNoToolCalls(stmt);
|
|
169
|
+
}
|
|
170
|
+
result = interpretWithEnv({ kind: "Program", stmts: [stmt] }, env);
|
|
171
|
+
}
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
|
+
checkNoToolCalls(node) {
|
|
175
|
+
if (!node || typeof node !== 'object')
|
|
176
|
+
return;
|
|
177
|
+
if (node.kind === "ToolCallExpr") {
|
|
178
|
+
throw new SecurityError("SEC010", "Tool calls are disabled in sandbox mode");
|
|
179
|
+
}
|
|
180
|
+
for (const key of Object.keys(node)) {
|
|
181
|
+
if (key === 'loc')
|
|
182
|
+
continue;
|
|
183
|
+
const val = node[key];
|
|
184
|
+
if (Array.isArray(val)) {
|
|
185
|
+
for (const item of val)
|
|
186
|
+
this.checkNoToolCalls(item);
|
|
187
|
+
}
|
|
188
|
+
else if (val && typeof val === 'object') {
|
|
189
|
+
this.checkNoToolCalls(val);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// Main factory function
|
|
195
|
+
export function createSandbox(config = {}) {
|
|
196
|
+
return new SafeInterpreter(config);
|
|
197
|
+
}
|
|
198
|
+
export { DEFAULTS as SECURITY_DEFAULTS };
|