@ugo-studio/jspp 0.2.5 → 0.2.6
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 +51 -36
- package/dist/analysis/scope.js +7 -0
- package/dist/analysis/typeAnalyzer.js +40 -19
- package/dist/ast/symbols.js +9 -8
- package/dist/cli/args.js +59 -0
- package/dist/cli/colors.js +9 -0
- package/dist/cli/file-utils.js +20 -0
- package/dist/cli/index.js +160 -0
- package/dist/cli/spinner.js +55 -0
- package/dist/core/codegen/control-flow-handlers.js +2 -2
- package/dist/core/codegen/declaration-handlers.js +9 -1
- package/dist/core/codegen/expression-handlers.js +101 -45
- package/dist/core/codegen/function-handlers.js +14 -3
- package/dist/core/codegen/helpers.js +33 -7
- package/dist/core/codegen/index.js +7 -5
- package/dist/core/codegen/statement-handlers.js +109 -37
- package/dist/core/codegen/visitor.js +22 -2
- package/dist/core/constants.js +16 -0
- package/dist/core/error.js +58 -0
- package/dist/index.js +6 -3
- package/package.json +3 -3
- package/src/prelude/scheduler.hpp +144 -144
- package/src/prelude/utils/log_any_value/object.hpp +12 -10
package/README.md
CHANGED
|
@@ -3,37 +3,52 @@
|
|
|
3
3
|
[](https://github.com/ugo-studio/jspp/actions/workflows/ci.yml)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
|
|
6
|
-
**JSPP is a modern, experimental transpiler that converts JavaScript code into high-performance, standard C++23.**
|
|
6
|
+
**JSPP is a modern, experimental transpiler that converts JavaScript and TypeScript code into high-performance, standard C++23.**
|
|
7
7
|
|
|
8
8
|
The primary goal of this project is to achieve a near-perfect translation of JavaScript's dynamic nature and core features into the statically-typed, compiled world of C++, exploring modern C++ capabilities to bridge the gap between these two powerful languages.
|
|
9
9
|
|
|
10
10
|
## About The Project
|
|
11
11
|
|
|
12
|
-
JavaScript is flexible and dynamic; C++ is performant and type-safe. JSPP aims to offer the best of both worlds. By transpiling JS to C++, we can potentially run JavaScript logic in environments where C++ is native, with significant performance gains and opportunities for low-level interoperability.
|
|
12
|
+
JavaScript is flexible and dynamic; C++ is performant and type-safe. JSPP aims to offer the best of both worlds. By transpiling JS/TS to C++, we can potentially run JavaScript logic in environments where C++ is native, with significant performance gains and opportunities for low-level interoperability.
|
|
13
13
|
|
|
14
|
-
This project serves as a deep dive into compiler design, language semantics, and the expressive power of modern C++. The current implementation translates an entire JavaScript file into a single C++ `main` function, cleverly using `std::shared_ptr<std::any>` to replicate JavaScript's dynamic typing, garbage-collection-like memory management, and complex features like closures.
|
|
14
|
+
This project serves as a deep dive into compiler design, language semantics, and the expressive power of modern C++. The current implementation translates an entire JavaScript/TypeScript file into a single C++ `main` function, cleverly using `std::shared_ptr<std::any>` (and custom wrappers) to replicate JavaScript's dynamic typing, garbage-collection-like memory management, and complex features like closures.
|
|
15
15
|
|
|
16
16
|
## Features
|
|
17
17
|
|
|
18
|
-
JSPP currently supports a
|
|
18
|
+
JSPP currently supports a comprehensive set of JavaScript features:
|
|
19
19
|
|
|
20
|
-
- **
|
|
21
|
-
- **
|
|
20
|
+
- **Languages:** JavaScript (`.js`) and TypeScript (`.ts`) support.
|
|
21
|
+
- **Dynamic Variables:** Declaration (`let`, `const`, `var`), assignment, and type changes at runtime.
|
|
22
|
+
- **Primitive Types:** `undefined`, `null`, `boolean`, `number`, `string`, `symbol`.
|
|
22
23
|
- **Functions:**
|
|
23
|
-
- Function declarations and
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
- **
|
|
28
|
-
- **
|
|
24
|
+
- Function declarations, arrow functions, and function expressions.
|
|
25
|
+
- **Generators:** `function*` and `yield` support.
|
|
26
|
+
- **Async/Await:** `async function` and `await` support (built on C++20 coroutines).
|
|
27
|
+
- Closures with proper lexical scoping.
|
|
28
|
+
- **Object Oriented:**
|
|
29
|
+
- **Classes:** Class declarations, constructors, methods, getters/setters, and inheritance (`extends`).
|
|
30
|
+
- **Prototypes:** Prototype chain traversal and manipulation.
|
|
31
|
+
- **Control Flow:**
|
|
32
|
+
- `if`, `else if`, `else`.
|
|
33
|
+
- Loops: `for`, `for-of`, `for-in`, `while`, `do-while`.
|
|
34
|
+
- `switch` statements.
|
|
35
|
+
- `try`, `catch`, `finally` blocks.
|
|
36
|
+
- **Operators:** Full suite of arithmetic, assignment, comparison, logical, bitwise, and unary operators (including `typeof`, `delete`, `void`, `instanceof`, `in`).
|
|
37
|
+
- **Standard Library:**
|
|
38
|
+
- **Console:** `log`, `warn`, `error`, `time`, `timeEnd`.
|
|
39
|
+
- **Math:** Comprehensive `Math` object implementation.
|
|
40
|
+
- **Timers:** `setTimeout`, `clearTimeout`, `setInterval`, `clearInterval`.
|
|
41
|
+
- **Promise:** Full `Promise` implementation with chaining.
|
|
42
|
+
- **Error:** Standard `Error` class and stack traces.
|
|
43
|
+
- **Arrays & Objects:** Extensive methods support (`map`, `filter`, `reduce`, `push`, `pop`, `Object.keys`, etc.).
|
|
29
44
|
|
|
30
45
|
## Reserved Keywords
|
|
31
46
|
|
|
32
47
|
JSPP reserves certain keywords to avoid conflicts with the generated C++ code and internal mechanisms. The following keywords are reserved and cannot be used as variable names:
|
|
33
48
|
|
|
34
|
-
- `
|
|
35
|
-
- `
|
|
36
|
-
- `co_yield
|
|
49
|
+
- `jspp`: Reserved for internal use by the transpiler.
|
|
50
|
+
- `std`: Reserved to prevent conflicts with the C++ standard library.
|
|
51
|
+
- `co_yield`, `co_return`, `co_await`: Reserved for C++ coroutine mechanics.
|
|
37
52
|
|
|
38
53
|
Using these keywords as variable names will result in a `SyntaxError`.
|
|
39
54
|
|
|
@@ -51,8 +66,7 @@ To contribute to JSPP or run its test suite, follow these steps:
|
|
|
51
66
|
|
|
52
67
|
### Prerequisites
|
|
53
68
|
|
|
54
|
-
- **
|
|
55
|
-
- [Install Node.js >= v20.0.0](https://nodejs.org/)
|
|
69
|
+
- **Bun:** This project uses [Bun](https://bun.sh/) for package management, script execution, and testing.
|
|
56
70
|
- **C++ Compiler:** A compiler with support for C++23 is required. This project is tested with `g++`.
|
|
57
71
|
- `g++` (MinGW on Windows, or available via build-essentials on Linux)
|
|
58
72
|
|
|
@@ -64,31 +78,31 @@ To contribute to JSPP or run its test suite, follow these steps:
|
|
|
64
78
|
```
|
|
65
79
|
2. Install dependencies:
|
|
66
80
|
```sh
|
|
67
|
-
|
|
81
|
+
bun install
|
|
68
82
|
```
|
|
69
83
|
|
|
70
84
|
## Usage
|
|
71
85
|
|
|
72
|
-
The primary way to use JSPP is via its command-line interface. This will transpile your
|
|
86
|
+
The primary way to use JSPP is via its command-line interface. This will transpile your file to C++, compile it, and execute the resulting binary.
|
|
73
87
|
|
|
74
88
|
```sh
|
|
75
|
-
jspp <path-to-your-
|
|
89
|
+
jspp <path-to-your-file>
|
|
76
90
|
```
|
|
77
91
|
|
|
78
92
|
**Example:**
|
|
79
93
|
|
|
80
|
-
To run a sample
|
|
94
|
+
To run a sample TypeScript file located at `my-code/test.ts`:
|
|
81
95
|
|
|
82
96
|
```sh
|
|
83
|
-
jspp my-code/
|
|
97
|
+
jspp my-code/test.ts
|
|
84
98
|
```
|
|
85
99
|
|
|
86
|
-
The transpiled C++ file and executable will be generated in the same directory as the input file and cleaned up after execution.
|
|
100
|
+
The transpiled C++ file and executable will be generated in the same directory as the input file and cleaned up after execution (unless `--keep-cpp` is used).
|
|
87
101
|
|
|
88
|
-
You can also run the test suite
|
|
102
|
+
You can also run the test suite:
|
|
89
103
|
|
|
90
104
|
```sh
|
|
91
|
-
|
|
105
|
+
bun test
|
|
92
106
|
```
|
|
93
107
|
|
|
94
108
|
## Roadmap
|
|
@@ -101,38 +115,39 @@ This project is ambitious, and there is a long and exciting road ahead. Here is
|
|
|
101
115
|
|
|
102
116
|
This phase focuses on building a solid foundation that correctly models JavaScript's core runtime behavior.
|
|
103
117
|
|
|
104
|
-
- [x] Dynamic Variables & Primitives
|
|
118
|
+
- [x] Dynamic Variables & Primitives
|
|
105
119
|
- [x] Function Declarations & Arrow Functions
|
|
106
120
|
- [x] Correct Hoisting for Variables and Functions
|
|
107
121
|
- [x] Closures & Lexical Scoping
|
|
108
|
-
- [x] `if
|
|
122
|
+
- [x] Basic Control Flow (`if`, `loops`)
|
|
109
123
|
- [x] Basic `console` API
|
|
110
124
|
|
|
111
125
|
---
|
|
112
126
|
|
|
113
127
|
### **Phase 2: Expanded Language Support**
|
|
114
128
|
|
|
115
|
-
This phase
|
|
129
|
+
This phase broadens the range of supported JavaScript syntax and features.
|
|
116
130
|
|
|
117
131
|
- [x] **Error Handling**: `try`/`catch`/`finally` blocks and `throw`.
|
|
118
|
-
- [x] **Objects:**
|
|
119
|
-
- [x] **Arrays:** Literals, indexing, and core methods
|
|
132
|
+
- [x] **Objects & Classes:** Classes, inheritance, literals, property access.
|
|
133
|
+
- [x] **Arrays:** Literals, indexing, and core methods.
|
|
120
134
|
- [x] **Operators:** Full suite of arithmetic, logical, and comparison operators.
|
|
121
|
-
- [x] **Control Flow:**
|
|
135
|
+
- [x] **Advanced Control Flow:** `switch`, `for-of`, `for-in`, generators.
|
|
136
|
+
- [x] **TypeScript Support:** Compilation of `.ts` files.
|
|
122
137
|
|
|
123
|
-
###
|
|
138
|
+
### Phase 3: Interoperability & Standard Library
|
|
124
139
|
|
|
125
|
-
This phase
|
|
140
|
+
This phase focuses on building out the standard library and enabling modular code.
|
|
126
141
|
|
|
127
|
-
- [ ] **JS Standard Library:** Implementation of common built-in objects
|
|
128
|
-
- [x] **Asynchronous Operations:**
|
|
142
|
+
- [ ] **JS Standard Library:** Implementation of common built-in objects. (Currently supports: `Math`, `Symbol`, `Error`, `String`, `Array`, `Object`, `Timer`). **Pending:** `Date`, `Temporal`, `Map`, `Set`, `JSON`, etc.
|
|
143
|
+
- [x] **Asynchronous Operations:** Event loop, `Promise`, `async/await`, and timers (`setTimeout`).
|
|
129
144
|
- [ ] **Module System:** Support for `import` and `export` to transpile multi-file projects.
|
|
130
145
|
|
|
131
146
|
### **Phase 4: Optimization & Advanced Features**
|
|
132
147
|
|
|
133
148
|
With a feature-complete transpiler, the focus will shift to performance and advanced capabilities.
|
|
134
149
|
|
|
135
|
-
- [ ] **Performance Benchmarking:** Create a suite to compare transpiled C++ performance against V8
|
|
150
|
+
- [ ] **Performance Benchmarking:** Create a suite to compare transpiled C++ performance against V8.
|
|
136
151
|
- [ ] **C++ Interoperability:** Define a clear API for calling C++ functions from the transpiled JavaScript and vice-versa.
|
|
137
152
|
|
|
138
153
|
## Contributing
|
package/dist/analysis/scope.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as ts from "typescript";
|
|
2
|
+
import { CompilerError } from "../core/error.js";
|
|
2
3
|
export const RESERVED_KEYWORDS = new Set([
|
|
3
4
|
"jspp",
|
|
4
5
|
"std",
|
|
@@ -86,6 +87,9 @@ export class ScopeManager {
|
|
|
86
87
|
define(name, type) {
|
|
87
88
|
// if (name === "named" || name === "letVal") console.log("Defining", name, "in scope. isBuiltin:", type.isBuiltin, " type:", type.type);
|
|
88
89
|
if (this.reservedKeywords.has(name) && !type.isBuiltin) {
|
|
90
|
+
if (type.declaration) {
|
|
91
|
+
throw new CompilerError(`Unexpected reserved word "${name}"`, type.declaration, "SyntaxError");
|
|
92
|
+
}
|
|
89
93
|
throw new Error(`SyntaxError: Unexpected reserved word "${name}"`);
|
|
90
94
|
}
|
|
91
95
|
this.currentScope.define(name, type);
|
|
@@ -93,6 +97,9 @@ export class ScopeManager {
|
|
|
93
97
|
// Defines a `var` variable (hoisted to function or global scope).
|
|
94
98
|
defineVar(name, type) {
|
|
95
99
|
if (this.reservedKeywords.has(name) && !type.isBuiltin) {
|
|
100
|
+
if (type.declaration) {
|
|
101
|
+
throw new CompilerError(`Unexpected reserved word "${name}"`, type.declaration, "SyntaxError");
|
|
102
|
+
}
|
|
96
103
|
throw new Error(`SyntaxError: Unexpected reserved word "${name}"`);
|
|
97
104
|
}
|
|
98
105
|
let scope = this.currentScope;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as ts from "typescript";
|
|
2
|
-
import { isBuiltinObject } from "../core/codegen/helpers.js";
|
|
2
|
+
import { isBuiltinObject, shouldIgnoreStatement, } from "../core/codegen/helpers.js";
|
|
3
|
+
import { CompilerError } from "../core/error.js";
|
|
3
4
|
import { Traverser } from "../core/traverser.js";
|
|
4
5
|
import { Scope, ScopeManager } from "./scope.js";
|
|
5
6
|
export class TypeAnalyzer {
|
|
@@ -167,12 +168,12 @@ export class TypeAnalyzer {
|
|
|
167
168
|
const breakNode = node;
|
|
168
169
|
if (breakNode.label) {
|
|
169
170
|
if (!this.labelStack.includes(breakNode.label.text)) {
|
|
170
|
-
throw new
|
|
171
|
+
throw new CompilerError(`Undefined label '${breakNode.label.text}'`, breakNode.label, "SyntaxError");
|
|
171
172
|
}
|
|
172
173
|
}
|
|
173
174
|
else {
|
|
174
175
|
if (this.loopDepth === 0 && this.switchDepth === 0) {
|
|
175
|
-
throw new
|
|
176
|
+
throw new CompilerError("Unlabeled break must be inside an iteration or switch statement", node, "SyntaxError");
|
|
176
177
|
}
|
|
177
178
|
}
|
|
178
179
|
},
|
|
@@ -182,14 +183,14 @@ export class TypeAnalyzer {
|
|
|
182
183
|
const continueNode = node;
|
|
183
184
|
if (continueNode.label) {
|
|
184
185
|
if (!this.labelStack.includes(continueNode.label.text)) {
|
|
185
|
-
throw new
|
|
186
|
+
throw new CompilerError(`Undefined label '${continueNode.label.text}'`, continueNode.label, "SyntaxError");
|
|
186
187
|
}
|
|
187
188
|
// Also need to check if the label belongs to a loop, but that's harder here.
|
|
188
189
|
// The TS checker should handle this. We'll assume for now it does.
|
|
189
190
|
}
|
|
190
191
|
else {
|
|
191
192
|
if (this.loopDepth === 0) {
|
|
192
|
-
throw new
|
|
193
|
+
throw new CompilerError("Unlabeled continue must be inside an iteration statement", node, "SyntaxError");
|
|
193
194
|
}
|
|
194
195
|
}
|
|
195
196
|
},
|
|
@@ -208,10 +209,7 @@ export class TypeAnalyzer {
|
|
|
208
209
|
// Catch invalid parameters
|
|
209
210
|
node.parameters.forEach((p) => {
|
|
210
211
|
if (p.getText() == "this") {
|
|
211
|
-
|
|
212
|
-
const { line, character } = sourceFile
|
|
213
|
-
.getLineAndCharacterOfPosition(p.getStart());
|
|
214
|
-
throw new SyntaxError(`Cannot use 'this' as a parameter name.\n\n${" ".repeat(6)}at ${sourceFile.fileName}:${line + 1}:${character + 1}\n`);
|
|
212
|
+
throw new CompilerError("Cannot use 'this' as a parameter name.", p, "SyntaxError");
|
|
215
213
|
}
|
|
216
214
|
});
|
|
217
215
|
// Define parameters in the new scope
|
|
@@ -250,10 +248,7 @@ export class TypeAnalyzer {
|
|
|
250
248
|
// Catch invalid parameters
|
|
251
249
|
node.parameters.forEach((p) => {
|
|
252
250
|
if (p.getText() == "this") {
|
|
253
|
-
|
|
254
|
-
const { line, character } = sourceFile
|
|
255
|
-
.getLineAndCharacterOfPosition(p.getStart());
|
|
256
|
-
throw new SyntaxError(`Cannot use 'this' as a parameter name.\n\n${" ".repeat(6)}at ${sourceFile.fileName}:${line + 1}:${character + 1}\n`);
|
|
251
|
+
throw new CompilerError("Cannot use 'this' as a parameter name.", p, "SyntaxError");
|
|
257
252
|
}
|
|
258
253
|
});
|
|
259
254
|
// Define parameters in the new scope
|
|
@@ -288,17 +283,14 @@ export class TypeAnalyzer {
|
|
|
288
283
|
this.scopeManager.define(funcName, funcType);
|
|
289
284
|
this.functionTypeInfo.set(node, funcType);
|
|
290
285
|
}
|
|
286
|
+
this.scopeManager.enterScope(node);
|
|
287
|
+
this.nodeToScope.set(node, this.scopeManager.currentScope);
|
|
291
288
|
// Catch invalid parameters
|
|
292
289
|
node.parameters.forEach((p) => {
|
|
293
290
|
if (p.getText() == "this") {
|
|
294
|
-
|
|
295
|
-
const { line, character } = sourceFile
|
|
296
|
-
.getLineAndCharacterOfPosition(p.getStart());
|
|
297
|
-
throw new SyntaxError(`Cannot use 'this' as a parameter name.\n\n${" ".repeat(6)}at ${sourceFile.fileName}:${line + 1}:${character + 1}\n`);
|
|
291
|
+
throw new CompilerError("Cannot use 'this' as a parameter name.", p, "SyntaxError");
|
|
298
292
|
}
|
|
299
293
|
});
|
|
300
|
-
this.scopeManager.enterScope(node);
|
|
301
|
-
this.nodeToScope.set(node, this.scopeManager.currentScope);
|
|
302
294
|
// Define parameters in the new scope
|
|
303
295
|
node.parameters.forEach((p) => this.scopeManager.define(p.name.getText(), {
|
|
304
296
|
type: "auto",
|
|
@@ -336,6 +328,25 @@ export class TypeAnalyzer {
|
|
|
336
328
|
this.scopeManager.exitScope();
|
|
337
329
|
},
|
|
338
330
|
},
|
|
331
|
+
EnumDeclaration: {
|
|
332
|
+
enter: (node) => {
|
|
333
|
+
const enumNode = node;
|
|
334
|
+
if (enumNode.name) {
|
|
335
|
+
const name = enumNode.name.getText();
|
|
336
|
+
const typeInfo = {
|
|
337
|
+
type: "object", // Enums are objects
|
|
338
|
+
declaration: enumNode,
|
|
339
|
+
needsHeapAllocation: true,
|
|
340
|
+
};
|
|
341
|
+
this.scopeManager.define(name, typeInfo);
|
|
342
|
+
}
|
|
343
|
+
// Enums don't create a new scope for variables, but they are objects.
|
|
344
|
+
// We don't strictly need to enterScope unless we want to track something inside.
|
|
345
|
+
// But standard JS/TS behavior doesn't really have block scope inside enum definition that leaks out or captures differently.
|
|
346
|
+
// However, we map nodes to scopes.
|
|
347
|
+
this.nodeToScope.set(node, this.scopeManager.currentScope);
|
|
348
|
+
},
|
|
349
|
+
},
|
|
339
350
|
MethodDeclaration: {
|
|
340
351
|
enter: (node) => {
|
|
341
352
|
if (ts.isMethodDeclaration(node)) {
|
|
@@ -397,6 +408,16 @@ export class TypeAnalyzer {
|
|
|
397
408
|
VariableDeclaration: {
|
|
398
409
|
enter: (node) => {
|
|
399
410
|
if (ts.isVariableDeclaration(node)) {
|
|
411
|
+
// Check if it is an ambient declaration (declare var/let/const ...)
|
|
412
|
+
let isAmbient = false;
|
|
413
|
+
if (ts.isVariableDeclarationList(node.parent) &&
|
|
414
|
+
ts.isVariableStatement(node.parent.parent)) {
|
|
415
|
+
if (shouldIgnoreStatement(node.parent.parent)) {
|
|
416
|
+
isAmbient = true;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (isAmbient)
|
|
420
|
+
return;
|
|
400
421
|
const name = node.name.getText();
|
|
401
422
|
const isBlockScoped = (node.parent.flags &
|
|
402
423
|
(ts.NodeFlags.Let | ts.NodeFlags.Const)) !== 0;
|
package/dist/ast/symbols.js
CHANGED
|
@@ -5,14 +5,15 @@ export var DeclarationType;
|
|
|
5
5
|
DeclarationType["const"] = "const";
|
|
6
6
|
DeclarationType["function"] = "function";
|
|
7
7
|
DeclarationType["class"] = "class";
|
|
8
|
+
DeclarationType["enum"] = "enum";
|
|
8
9
|
})(DeclarationType || (DeclarationType = {}));
|
|
9
10
|
export class DeclaredSymbol {
|
|
10
11
|
type;
|
|
11
|
-
|
|
12
|
+
checks;
|
|
12
13
|
func;
|
|
13
14
|
constructor(type) {
|
|
14
15
|
this.type = type;
|
|
15
|
-
this.
|
|
16
|
+
this.checks = {
|
|
16
17
|
initialized: false,
|
|
17
18
|
};
|
|
18
19
|
this.func = null;
|
|
@@ -22,8 +23,8 @@ export class DeclaredSymbol {
|
|
|
22
23
|
this.type === DeclarationType.var;
|
|
23
24
|
}
|
|
24
25
|
updateChecked(update) {
|
|
25
|
-
this.
|
|
26
|
-
...this.
|
|
26
|
+
this.checks = {
|
|
27
|
+
...this.checks,
|
|
27
28
|
...update,
|
|
28
29
|
};
|
|
29
30
|
}
|
|
@@ -53,8 +54,8 @@ export class DeclaredSymbols {
|
|
|
53
54
|
}
|
|
54
55
|
add(name, value) {
|
|
55
56
|
const sym = new DeclaredSymbol(value.type);
|
|
56
|
-
if (value.
|
|
57
|
-
sym.updateChecked(value.
|
|
57
|
+
if (value.checks !== undefined)
|
|
58
|
+
sym.updateChecked(value.checks);
|
|
58
59
|
if (value.func !== undefined)
|
|
59
60
|
sym.updateFunc(value.func);
|
|
60
61
|
return this.symbols.set(name, sym);
|
|
@@ -64,8 +65,8 @@ export class DeclaredSymbols {
|
|
|
64
65
|
if (sym) {
|
|
65
66
|
if (update.type !== undefined)
|
|
66
67
|
sym.type = update.type;
|
|
67
|
-
if (update.
|
|
68
|
-
sym.updateChecked(update.
|
|
68
|
+
if (update.checks !== undefined)
|
|
69
|
+
sym.updateChecked(update.checks);
|
|
69
70
|
if (update.func !== undefined)
|
|
70
71
|
sym.updateFunc(update.func);
|
|
71
72
|
return this.symbols.set(name, sym);
|
package/dist/cli/args.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import pkg from "../../package.json" with { type: "json" };
|
|
3
|
+
import { COLORS } from "./colors.js";
|
|
4
|
+
export function parseArgs(rawArgs) {
|
|
5
|
+
let jsFilePathArg = null;
|
|
6
|
+
let isRelease = false;
|
|
7
|
+
let keepCpp = false;
|
|
8
|
+
let outputExePath = null;
|
|
9
|
+
let scriptArgs = [];
|
|
10
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
11
|
+
const arg = rawArgs[i];
|
|
12
|
+
if (!arg)
|
|
13
|
+
continue;
|
|
14
|
+
if (arg === "--") {
|
|
15
|
+
scriptArgs = rawArgs.slice(i + 1);
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
if (arg === "--release") {
|
|
19
|
+
isRelease = true;
|
|
20
|
+
}
|
|
21
|
+
else if (arg === "--keep-cpp") {
|
|
22
|
+
keepCpp = true;
|
|
23
|
+
}
|
|
24
|
+
else if (arg === "-o" || arg === "--output") {
|
|
25
|
+
if (i + 1 < rawArgs.length) {
|
|
26
|
+
outputExePath = rawArgs[i + 1] ?? null;
|
|
27
|
+
i++;
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
console.error(`${COLORS.red}Error: --output requires a file path argument.${COLORS.reset}`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
else if (arg.startsWith("-")) {
|
|
35
|
+
console.warn(`${COLORS.yellow}Warning: Unknown argument '${arg}'${COLORS.reset}`);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
if (jsFilePathArg) {
|
|
39
|
+
console.error(`${COLORS.red}Error: Multiple input files specified.${COLORS.reset}`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
jsFilePathArg = arg;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (!jsFilePathArg) {
|
|
46
|
+
console.log(`${COLORS.bold}JSPP Compiler${COLORS.reset} ${COLORS.dim}v${pkg.version}${COLORS.reset}`);
|
|
47
|
+
console.log(`${COLORS.bold}Usage:${COLORS.reset} jspp <path-to-js-file> [--release] [--keep-cpp] [-o <output-path>] [-- <args...>]`);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
jsFilePath: path.resolve(process.cwd(), jsFilePathArg),
|
|
52
|
+
isRelease,
|
|
53
|
+
keepCpp,
|
|
54
|
+
outputExePath: outputExePath
|
|
55
|
+
? path.resolve(process.cwd(), outputExePath)
|
|
56
|
+
: null,
|
|
57
|
+
scriptArgs,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
export async function getLatestMtime(dirPath) {
|
|
4
|
+
let maxMtime = 0;
|
|
5
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
6
|
+
for (const entry of entries) {
|
|
7
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
8
|
+
if (entry.isDirectory()) {
|
|
9
|
+
const nestedMtime = await getLatestMtime(fullPath);
|
|
10
|
+
if (nestedMtime > maxMtime)
|
|
11
|
+
maxMtime = nestedMtime;
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
const stats = await fs.stat(fullPath);
|
|
15
|
+
if (stats.mtimeMs > maxMtime)
|
|
16
|
+
maxMtime = stats.mtimeMs;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return maxMtime;
|
|
20
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
|
+
import fs from "fs/promises";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import pkg from "../../package.json" with { type: "json" };
|
|
6
|
+
import { Interpreter } from "../index.js";
|
|
7
|
+
import { CompilerError } from "../core/error.js";
|
|
8
|
+
import { parseArgs } from "./args.js";
|
|
9
|
+
import { COLORS } from "./colors.js";
|
|
10
|
+
import { getLatestMtime } from "./file-utils.js";
|
|
11
|
+
import { Spinner } from "./spinner.js";
|
|
12
|
+
const pkgDir = path.dirname(path.dirname(import.meta.dirname));
|
|
13
|
+
async function main() {
|
|
14
|
+
const { jsFilePath, isRelease, keepCpp, outputExePath, scriptArgs } = parseArgs(process.argv.slice(2));
|
|
15
|
+
const ext = path.extname(jsFilePath);
|
|
16
|
+
const jsFileName = path.basename(jsFilePath, ext);
|
|
17
|
+
const sourceDir = path.dirname(jsFilePath);
|
|
18
|
+
// Intermediate C++ file goes alongside the source JS file
|
|
19
|
+
const cppFilePath = path.join(sourceDir, `${jsFileName}.cpp`);
|
|
20
|
+
// Determine output executable path
|
|
21
|
+
let exeFilePath;
|
|
22
|
+
if (outputExePath) {
|
|
23
|
+
exeFilePath = outputExePath;
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
const ext = process.platform === "win32" ? ".exe" : "";
|
|
27
|
+
exeFilePath = path.join(sourceDir, `${jsFileName}${ext}`);
|
|
28
|
+
}
|
|
29
|
+
// Mode Configuration
|
|
30
|
+
const mode = isRelease ? "release" : "debug";
|
|
31
|
+
console.log(`${COLORS.bold}JSPP Compiler${COLORS.reset} ${COLORS.dim}v${pkg.version}${COLORS.reset}`);
|
|
32
|
+
console.log(`Mode: ${isRelease ? COLORS.green : COLORS.yellow}${mode.toUpperCase()}${COLORS.reset}\n`);
|
|
33
|
+
const flags = isRelease ? ["-O3", "-DNDEBUG"] : ["-O0"];
|
|
34
|
+
if (process.platform === "win32") {
|
|
35
|
+
flags.push("-Wa,-mbig-obj");
|
|
36
|
+
}
|
|
37
|
+
const pchDir = path.resolve(pkgDir, "prelude-build", mode);
|
|
38
|
+
const spinner = new Spinner("Initializing...");
|
|
39
|
+
try {
|
|
40
|
+
spinner.start();
|
|
41
|
+
// 1. Interpreter Phase
|
|
42
|
+
spinner.update(`Reading ${path.basename(jsFilePath)}...`);
|
|
43
|
+
const jsCode = await fs.readFile(jsFilePath, "utf-8");
|
|
44
|
+
spinner.update("Transpiling to C++...");
|
|
45
|
+
const interpreter = new Interpreter();
|
|
46
|
+
const { cppCode, preludePath } = interpreter.interpret(jsCode, jsFilePath);
|
|
47
|
+
// Ensure directory for cpp file exists (should exist as it's source dir, but for safety if we change logic)
|
|
48
|
+
await fs.mkdir(path.dirname(cppFilePath), { recursive: true });
|
|
49
|
+
await fs.writeFile(cppFilePath, cppCode);
|
|
50
|
+
spinner.succeed(`Generated cpp`);
|
|
51
|
+
// 2. Precompiled Header Check
|
|
52
|
+
spinner.text = "Checking precompiled headers...";
|
|
53
|
+
spinner.start();
|
|
54
|
+
const pchFile = path.join(pchDir, "index.hpp.gch");
|
|
55
|
+
let shouldRebuild = false;
|
|
56
|
+
try {
|
|
57
|
+
const pchStats = await fs.stat(pchFile);
|
|
58
|
+
const sourceMtime = await getLatestMtime(preludePath);
|
|
59
|
+
if (sourceMtime > pchStats.mtimeMs) {
|
|
60
|
+
shouldRebuild = true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
shouldRebuild = true;
|
|
65
|
+
}
|
|
66
|
+
if (shouldRebuild) {
|
|
67
|
+
spinner.update("Rebuilding precompiled headers (this may take a while)...");
|
|
68
|
+
// Use spawn (async) instead of spawnSync to keep spinner alive
|
|
69
|
+
const rebuild = spawn("bun", [
|
|
70
|
+
"run",
|
|
71
|
+
"scripts/precompile-headers.ts",
|
|
72
|
+
], {
|
|
73
|
+
cwd: pkgDir,
|
|
74
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
75
|
+
});
|
|
76
|
+
const stderrChunks = [];
|
|
77
|
+
if (rebuild.stderr) {
|
|
78
|
+
rebuild.stderr.on("data", (chunk) => stderrChunks.push(chunk));
|
|
79
|
+
}
|
|
80
|
+
const exitCode = await new Promise((resolve) => {
|
|
81
|
+
rebuild.on("close", (code) => resolve(code ?? 1));
|
|
82
|
+
});
|
|
83
|
+
if (exitCode !== 0) {
|
|
84
|
+
const stderr = Buffer.concat(stderrChunks).toString();
|
|
85
|
+
spinner.fail("Failed to rebuild precompiled headers");
|
|
86
|
+
console.error(stderr);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
spinner.succeed("Precompiled headers updated");
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
spinner.succeed("Precompiled headers");
|
|
93
|
+
}
|
|
94
|
+
// 3. Compilation Phase
|
|
95
|
+
spinner.text = `Compiling binary...`;
|
|
96
|
+
spinner.start();
|
|
97
|
+
// Ensure output directory exists
|
|
98
|
+
await fs.mkdir(path.dirname(exeFilePath), { recursive: true });
|
|
99
|
+
const compile = spawn("g++", [
|
|
100
|
+
"-std=c++23",
|
|
101
|
+
...flags,
|
|
102
|
+
cppFilePath,
|
|
103
|
+
"-o",
|
|
104
|
+
exeFilePath,
|
|
105
|
+
"-I",
|
|
106
|
+
pchDir,
|
|
107
|
+
"-I",
|
|
108
|
+
preludePath,
|
|
109
|
+
], {
|
|
110
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
111
|
+
});
|
|
112
|
+
const compileStderrChunks = [];
|
|
113
|
+
if (compile.stderr) {
|
|
114
|
+
compile.stderr.on("data", (chunk) => compileStderrChunks.push(chunk));
|
|
115
|
+
}
|
|
116
|
+
const compileExitCode = await new Promise((resolve) => {
|
|
117
|
+
compile.on("close", (code) => resolve(code ?? 1));
|
|
118
|
+
});
|
|
119
|
+
if (compileExitCode !== 0) {
|
|
120
|
+
const stderr = Buffer.concat(compileStderrChunks).toString();
|
|
121
|
+
spinner.fail(`Compilation failed`);
|
|
122
|
+
console.error(stderr);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
spinner.succeed(`Compiled to ${COLORS.green}${COLORS.bold}${path.basename(exeFilePath)}${COLORS.reset}`);
|
|
126
|
+
// Clean up C++ file if not requested to keep
|
|
127
|
+
if (!keepCpp) {
|
|
128
|
+
try {
|
|
129
|
+
await fs.unlink(cppFilePath);
|
|
130
|
+
}
|
|
131
|
+
catch (e) {
|
|
132
|
+
// Ignore error if file cannot be deleted
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// 4. Execution Phase
|
|
136
|
+
console.log(`\n${COLORS.cyan}--- Running Output ---${COLORS.reset}`);
|
|
137
|
+
const run = spawn(exeFilePath, scriptArgs, {
|
|
138
|
+
stdio: "inherit",
|
|
139
|
+
});
|
|
140
|
+
const runExitCode = await new Promise((resolve) => {
|
|
141
|
+
run.on("close", (code) => resolve(code ?? 1));
|
|
142
|
+
});
|
|
143
|
+
console.log(`${COLORS.cyan}----------------------${COLORS.reset}\n`);
|
|
144
|
+
if (runExitCode !== 0) {
|
|
145
|
+
console.error(`${COLORS.red}Execution failed with exit code ${runExitCode}${COLORS.reset}`);
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
if (error instanceof CompilerError) {
|
|
151
|
+
spinner.fail("Compilation failed");
|
|
152
|
+
console.error(error.getFormattedError());
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
spinner.fail("An unexpected error occurred");
|
|
156
|
+
console.error(error);
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
main();
|