@ugo-studio/jspp 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import ts from "typescript";
2
- import { CodeGenerator } from "./";
2
+ import { CodeGenerator } from "./index.js";
3
3
  export function visitVariableDeclarationList(node, context) {
4
4
  return node.declarations
5
5
  .map((d) => this.visit(d, context))
@@ -12,27 +12,50 @@ export function visitVariableDeclaration(node, context) {
12
12
  const scope = this.getScopeForNode(varDecl);
13
13
  const typeInfo = this.typeAnalyzer.scopeManager.lookupFromScope(name, scope);
14
14
  // Mark the symbol as checked
15
- this.markSymbolAsChecked(name, context.topLevelScopeSymbols, context.localScopeSymbols);
15
+ this.markSymbolAsInitialized(name, context.globalScopeSymbols, context.localScopeSymbols);
16
+ let nativeLambdaCode = "";
16
17
  let initializer = "";
18
+ let shouldSkipDeref = false;
17
19
  if (varDecl.initializer) {
18
20
  const initExpr = varDecl.initializer;
19
- const initContext = {
20
- ...context,
21
- lambdaName: ts.isArrowFunction(initExpr) ? name : undefined, // Pass the variable name for arrow functions
22
- };
23
21
  let initText = ts.isNumericLiteral(initExpr)
24
22
  ? initExpr.getText()
25
- : this.visit(initExpr, initContext);
23
+ : this.visit(initExpr, context);
26
24
  if (ts.isIdentifier(initExpr)) {
27
25
  const initScope = this.getScopeForNode(initExpr);
28
26
  const initTypeInfo = this.typeAnalyzer.scopeManager.lookupFromScope(initExpr.text, initScope);
29
27
  const varName = this.getJsVarName(initExpr);
28
+ // Check if both target and initializer are heap allocated
29
+ if (typeInfo.needsHeapAllocation &&
30
+ initTypeInfo?.needsHeapAllocation) {
31
+ shouldSkipDeref = true;
32
+ }
30
33
  if (initTypeInfo &&
31
34
  !initTypeInfo.isParameter &&
32
- !initTypeInfo.isBuiltin) {
33
- initText = this.getDerefCode(initText, varName, initContext, initTypeInfo);
35
+ !initTypeInfo.isBuiltin &&
36
+ !shouldSkipDeref) {
37
+ initText = this.getDerefCode(initText, varName, context, initTypeInfo);
34
38
  }
35
39
  }
40
+ else if (ts.isArrowFunction(initExpr)) {
41
+ const initContext = {
42
+ ...context,
43
+ lambdaName: name, // Use the variable name as function name
44
+ };
45
+ // Generate and update self name
46
+ const nativeName = this.generateUniqueName(`__${name}_native_`, context.localScopeSymbols, context.globalScopeSymbols);
47
+ context.localScopeSymbols.update(name, { func: { nativeName } });
48
+ // Generate lambda
49
+ const lambda = this.generateLambda(initExpr, initContext, {
50
+ isAssignment: true,
51
+ generateOnlyLambda: true,
52
+ nativeName,
53
+ });
54
+ nativeLambdaCode =
55
+ `auto ${nativeName} = ${lambda};\n${this.indent()}`;
56
+ // Generate AnyValue wrapper
57
+ initText = this.generateFullLambdaExpression(initExpr, initContext, nativeName, { isAssignment: true, noTypeSignature: true });
58
+ }
36
59
  initializer = " = " + initText;
37
60
  }
38
61
  const isLetOrConst = (varDecl.parent.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) !== 0;
@@ -40,19 +63,21 @@ export function visitVariableDeclaration(node, context) {
40
63
  (!context.localScopeSymbols.has(name));
41
64
  const assignmentTarget = shouldDeref
42
65
  ? this.getDerefCode(name, name, context, typeInfo)
43
- : (typeInfo.needsHeapAllocation ? `*${name}` : name);
66
+ : (typeInfo.needsHeapAllocation && !shouldSkipDeref
67
+ ? `*${name}`
68
+ : name);
44
69
  if (isLetOrConst) {
45
70
  // If there's no initializer, it should be assigned undefined.
46
71
  if (!initializer) {
47
- return `${assignmentTarget} = jspp::Constants::UNDEFINED`;
72
+ return `${nativeLambdaCode}${assignmentTarget} = jspp::Constants::UNDEFINED`;
48
73
  }
49
- return `${assignmentTarget}${initializer}`;
74
+ return `${nativeLambdaCode}${assignmentTarget}${initializer}`;
50
75
  }
51
76
  // For 'var', it's a bit more complex.
52
77
  if (context.isAssignmentOnly) {
53
78
  if (!initializer)
54
79
  return "";
55
- return `${assignmentTarget}${initializer}`;
80
+ return `${nativeLambdaCode}${assignmentTarget}${initializer}`;
56
81
  }
57
82
  else {
58
83
  // This case should not be hit with the new hoisting logic,
@@ -1,5 +1,5 @@
1
1
  import ts from "typescript";
2
- import { CodeGenerator } from "./";
2
+ import { CodeGenerator } from "./index.js";
3
3
  export function visitObjectPropertyName(node, context) {
4
4
  if (ts.isNumericLiteral(node)) {
5
5
  return context.isBracketNotationPropertyAccess
@@ -28,6 +28,13 @@ export function visitObjectPropertyName(node, context) {
28
28
  export function visitObjectLiteralExpression(node, context) {
29
29
  const obj = node;
30
30
  const objVar = this.generateUniqueName("__obj_", this.getDeclaredSymbols(node));
31
+ if (!obj.properties.some((prop) => ts.isPropertyAssignment(prop) ||
32
+ ts.isShorthandPropertyAssignment(prop) ||
33
+ ts.isMethodDeclaration(prop) || ts.isGetAccessor(prop) ||
34
+ ts.isSetAccessor(prop))) {
35
+ // Empty object
36
+ return `jspp::AnyValue::make_object_with_proto({}, ::Object.get_own_property("prototype"))`;
37
+ }
31
38
  let code = `([&]() {\n`;
32
39
  code +=
33
40
  `${this.indent()} auto ${objVar} = jspp::AnyValue::make_object_with_proto({}, ::Object.get_own_property("prototype"));\n`;
@@ -102,8 +109,6 @@ export function visitObjectLiteralExpression(node, context) {
102
109
  }
103
110
  }
104
111
  this.indentationLevel--;
105
- // code +=
106
- // `${this.indent()} ${returnCmd} ${objVar};\n${this.indent()}} )() ))`;
107
112
  code += `${this.indent()} return ${objVar};\n${this.indent()}})()`;
108
113
  return code;
109
114
  }
@@ -439,6 +444,21 @@ export function visitBinaryExpression(node, context) {
439
444
  const target = context.derefBeforeAssignment
440
445
  ? this.getDerefCode(leftText, leftText, context, typeInfo)
441
446
  : (typeInfo.needsHeapAllocation ? `*${leftText}` : leftText);
447
+ // Update scope symbols on variable re-assignment
448
+ if (ts.isIdentifier(binExpr.left)) {
449
+ if (!ts.isFunctionDeclaration(binExpr.right)) {
450
+ if (context.localScopeSymbols.has(binExpr.left.text)) {
451
+ context.localScopeSymbols.update(binExpr.left.text, {
452
+ func: null,
453
+ });
454
+ }
455
+ else if (context.globalScopeSymbols.has(binExpr.left.text)) {
456
+ context.globalScopeSymbols.update(binExpr.left.text, {
457
+ func: null,
458
+ });
459
+ }
460
+ }
461
+ }
442
462
  return `${target} ${op} ${rightText}`;
443
463
  }
444
464
  const leftText = this.visit(binExpr.left, context);
@@ -535,7 +555,7 @@ export function visitConditionalExpression(node, context) {
535
555
  ...context,
536
556
  isFunctionBody: false,
537
557
  });
538
- return `is_truthy(${condition}) ? ${whenTrueStmt} : ${whenFalseStmt}`;
558
+ return `jspp::is_truthy(${condition}) ? ${whenTrueStmt} : ${whenFalseStmt}`;
539
559
  }
540
560
  export function visitCallExpression(node, context) {
541
561
  const callExpr = node;
@@ -645,6 +665,21 @@ export function visitCallExpression(node, context) {
645
665
  derefCallee = calleeCode;
646
666
  }
647
667
  else if (typeInfo) {
668
+ const name = callee.getText();
669
+ const symbol = context.localScopeSymbols.get(name) ??
670
+ context.globalScopeSymbols.get(name);
671
+ // Optimization: Direct lambda call
672
+ if (symbol && symbol.func?.nativeName) {
673
+ const callExpr = `${symbol.func.nativeName}(jspp::Constants::UNDEFINED, ${argsSpan})`;
674
+ if (symbol.func.isGenerator) {
675
+ if (symbol.func.isAsync) {
676
+ return `jspp::AnyValue::from_async_iterator(${callExpr})`;
677
+ }
678
+ return `jspp::AnyValue::from_iterator(${callExpr})`;
679
+ }
680
+ return callExpr;
681
+ }
682
+ // AnyValue function call
648
683
  derefCallee = this.getDerefCode(calleeCode, this.getJsVarName(callee), context, typeInfo);
649
684
  }
650
685
  }
@@ -1,12 +1,12 @@
1
1
  import ts from "typescript";
2
- import { DeclaredSymbols } from "../../ast/symbols";
3
- import { CodeGenerator } from "./";
4
- import { collectFunctionScopedDeclarations } from "./helpers";
2
+ import { DeclaredSymbols } from "../../ast/symbols.js";
3
+ import { collectFunctionScopedDeclarations } from "./helpers.js";
4
+ import { CodeGenerator } from "./index.js";
5
5
  export function generateLambda(node, context, options) {
6
- const isAssignment = options?.isAssignment || false;
7
6
  const capture = options?.capture || "[=]";
7
+ const nativeName = options?.nativeName;
8
8
  const declaredSymbols = this.getDeclaredSymbols(node);
9
- const argsName = this.generateUniqueName("__args_", declaredSymbols);
9
+ const argsName = this.generateUniqueName("__args_", declaredSymbols, context.globalScopeSymbols, context.localScopeSymbols);
10
10
  const isInsideGeneratorFunction = this.isGeneratorFunction(node);
11
11
  const isInsideAsyncFunction = this.isAsyncFunction(node);
12
12
  const returnCmd = this.getReturnCommand({
@@ -23,26 +23,25 @@ export function generateLambda(node, context, options) {
23
23
  // For generators/async, we manually copy them inside the body.
24
24
  const paramThisType = "const jspp::AnyValue&";
25
25
  const paramArgsType = "std::span<const jspp::AnyValue>";
26
+ const selfParamPart = nativeName ? `this auto&& ${nativeName}, ` : "";
27
+ const mutablePart = nativeName ? "" : "mutable ";
26
28
  const thisArgParam = isArrow
27
29
  ? "const jspp::AnyValue&" // Arrow functions use captured 'this' or are never generators in this parser context
28
30
  : `${paramThisType} ${this.globalThisVar}`;
29
- let lambda = `${capture}(${thisArgParam}, ${paramArgsType} ${argsName}) mutable -> ${funcReturnType} `;
30
- const topLevelScopeSymbols = this.prepareScopeSymbolsForVisit(context.topLevelScopeSymbols, context.localScopeSymbols);
31
+ let lambda = `${capture}(${selfParamPart}${thisArgParam}, ${paramArgsType} ${argsName}) ${mutablePart}-> ${funcReturnType} `;
32
+ const globalScopeSymbols = this.prepareScopeSymbolsForVisit(context.globalScopeSymbols, context.localScopeSymbols);
31
33
  const visitContext = {
34
+ currentScopeNode: context.currentScopeNode,
32
35
  isMainContext: false,
33
36
  isInsideFunction: true,
34
37
  isFunctionBody: false,
35
38
  lambdaName: undefined,
36
- topLevelScopeSymbols,
39
+ globalScopeSymbols,
37
40
  localScopeSymbols: new DeclaredSymbols(),
38
41
  superClassVar: context.superClassVar,
39
42
  isInsideGeneratorFunction: isInsideGeneratorFunction,
40
43
  isInsideAsyncFunction: isInsideAsyncFunction,
41
44
  };
42
- // Name of 'this' and 'args' to be used in the body.
43
- // If generator/async, we use the copied versions.
44
- let bodyThisVar = this.globalThisVar;
45
- let bodyArgsVar = argsName;
46
45
  // Preamble to copy arguments for coroutines
47
46
  let preamble = "";
48
47
  if (isInsideGeneratorFunction || isInsideAsyncFunction) {
@@ -51,40 +50,9 @@ export function generateLambda(node, context, options) {
51
50
  if (!isArrow) {
52
51
  preamble +=
53
52
  `${this.indent()}jspp::AnyValue ${thisCopy} = ${this.globalThisVar};\n`;
54
- bodyThisVar = thisCopy;
55
53
  }
56
54
  preamble +=
57
55
  `${this.indent()}std::vector<jspp::AnyValue> ${argsCopy}(${argsName}.begin(), ${argsName}.end());\n`;
58
- bodyArgsVar = argsCopy;
59
- // Update visitContext to use the new 'this' variable if needed?
60
- // CodeGenerator.globalThisVar is a member of the class, so we can't easily change it for the recursive visit
61
- // without changing the class state or passing it down.
62
- // BUT: visitThisKeyword uses `this.globalThisVar`.
63
- // We need to temporarily swap `this.globalThisVar` or handle it.
64
- // Actually, `this.globalThisVar` is set once in `generate`.
65
- // Wait, `visitThisKeyword` uses `this.globalThisVar`.
66
- // If we change the variable name for 'this', we must ensure `visitThisKeyword` generates the correct name.
67
- // `generateLambda` does NOT create a new CodeGenerator instance, so `this.globalThisVar` is the global one.
68
- //
69
- // We need to support shadowing `globalThisVar` for the duration of the visit.
70
- // The current `CodeGenerator` doesn't seem to support changing `globalThisVar` recursively easily
71
- // because it's a property of the class, not `VisitContext`.
72
- //
73
- // However, `visitFunctionDeclaration` etc don't update `globalThisVar`.
74
- // `visitThisKeyword` just returns `this.globalThisVar`.
75
- //
76
- // If we are inside a function, `this` should refer to the function's `this`.
77
- // `this.globalThisVar` is generated in `generate()`: `__this_val__...`.
78
- // And `generateLambda` uses `this.globalThisVar` as the parameter name.
79
- //
80
- // So, if we copy it to `__this_copy`, `visitThisKeyword` will still print `__this_val__...`.
81
- // This is BAD for generators because `__this_val__...` is a reference that dies.
82
- //
83
- // FIX: We must reuse the SAME name for the local copy, and shadow the parameter.
84
- // But we can't declare a variable with the same name as the parameter in the same scope.
85
- //
86
- // We can rename the PARAMETER to something else, and declare the local variable with `this.globalThisVar`.
87
- //
88
56
  }
89
57
  // Adjust parameter names for generator/async to allow shadowing/copying
90
58
  let finalThisParamName = isArrow ? "" : this.globalThisVar;
@@ -100,7 +68,7 @@ export function generateLambda(node, context, options) {
100
68
  : `${paramThisType} ${finalThisParamName}`;
101
69
  // Re-construct lambda header with potentially new param names
102
70
  lambda =
103
- `${capture}(${thisArgParamFinal}, ${paramArgsType} ${finalArgsParamName}) mutable -> ${funcReturnType} `;
71
+ `${capture}(${selfParamPart}${thisArgParamFinal}, ${paramArgsType} ${finalArgsParamName}) ${mutablePart}-> ${funcReturnType} `;
104
72
  // Regenerate preamble
105
73
  preamble = "";
106
74
  if (isInsideGeneratorFunction || isInsideAsyncFunction) {
@@ -178,7 +146,7 @@ export function generateLambda(node, context, options) {
178
146
  // Hoist var declarations in the function body
179
147
  const varDecls = collectFunctionScopedDeclarations(node.body);
180
148
  varDecls.forEach((decl) => {
181
- preamble += this.hoistDeclaration(decl, visitContext.localScopeSymbols);
149
+ preamble += this.hoistDeclaration(decl, visitContext.localScopeSymbols, node.body);
182
150
  });
183
151
  this.indentationLevel++;
184
152
  const paramExtraction = paramExtractor(node.parameters);
@@ -215,34 +183,49 @@ export function generateLambda(node, context, options) {
215
183
  else {
216
184
  lambda += `{ ${preamble} ${returnCmd} jspp::Constants::UNDEFINED; }\n`;
217
185
  }
218
- let signature = "";
219
- let callable = "";
186
+ // Return only lambda if required
187
+ if (options?.generateOnlyLambda) {
188
+ return lambda.trimEnd();
189
+ }
190
+ return this.generateFullLambdaExpression(node, context, lambda, options);
191
+ }
192
+ export function generateFullLambdaExpression(node, context, lambda, options) {
193
+ const isAssignment = options?.isAssignment || false;
194
+ const noTypeSignature = options?.noTypeSignature || false;
195
+ const isInsideGeneratorFunction = this.isGeneratorFunction(node);
196
+ const isInsideAsyncFunction = this.isAsyncFunction(node);
197
+ const isArrow = ts.isArrowFunction(node);
198
+ let callable = lambda;
220
199
  let method = "";
221
200
  // Handle generator function
222
201
  if (isInsideGeneratorFunction) {
223
202
  if (isInsideAsyncFunction) {
224
- signature =
225
- "jspp::JsAsyncIterator<jspp::AnyValue>(const jspp::AnyValue&, std::span<const jspp::AnyValue>)";
226
- callable = `std::function<${signature}>(${lambda})`;
203
+ if (!noTypeSignature) {
204
+ const signature = "jspp::JsAsyncIterator<jspp::AnyValue>(const jspp::AnyValue&, std::span<const jspp::AnyValue>)";
205
+ callable = `std::function<${signature}>(${lambda})`;
206
+ }
227
207
  method = `jspp::AnyValue::make_async_generator`;
228
208
  }
229
209
  else {
230
- signature =
231
- "jspp::JsIterator<jspp::AnyValue>(const jspp::AnyValue&, std::span<const jspp::AnyValue>)";
232
- callable = `std::function<${signature}>(${lambda})`;
210
+ if (!noTypeSignature) {
211
+ const signature = "jspp::JsIterator<jspp::AnyValue>(const jspp::AnyValue&, std::span<const jspp::AnyValue>)";
212
+ callable = `std::function<${signature}>(${lambda})`;
213
+ }
233
214
  method = `jspp::AnyValue::make_generator`;
234
215
  }
235
216
  } // Handle async function
236
217
  else if (isInsideAsyncFunction) {
237
- signature =
238
- "jspp::JsPromise(const jspp::AnyValue&, std::span<const jspp::AnyValue>)";
239
- callable = `std::function<${signature}>(${lambda})`;
218
+ if (!noTypeSignature) {
219
+ const signature = "jspp::JsPromise(const jspp::AnyValue&, std::span<const jspp::AnyValue>)";
220
+ callable = `std::function<${signature}>(${lambda})`;
221
+ }
240
222
  method = `jspp::AnyValue::make_async_function`;
241
223
  } // Handle normal function
242
224
  else {
243
- signature =
244
- `jspp::AnyValue(const jspp::AnyValue&, std::span<const jspp::AnyValue>)`;
245
- callable = `std::function<${signature}>(${lambda})`;
225
+ if (!noTypeSignature) {
226
+ const signature = `jspp::AnyValue(const jspp::AnyValue&, std::span<const jspp::AnyValue>)`;
227
+ callable = `std::function<${signature}>(${lambda})`;
228
+ }
246
229
  if (options?.isClass) {
247
230
  method = `jspp::AnyValue::make_class`;
248
231
  }
@@ -1,7 +1,7 @@
1
1
  import ts from "typescript";
2
- import { BUILTIN_OBJECTS, Scope } from "../../analysis/scope";
3
- import { DeclaredSymbols, DeclaredSymbolType } from "../../ast/symbols";
4
- import { CodeGenerator } from "./";
2
+ import { BUILTIN_OBJECTS, Scope } from "../../analysis/scope.js";
3
+ import { DeclarationType, DeclaredSymbols } from "../../ast/symbols.js";
4
+ import { CodeGenerator } from "./index.js";
5
5
  export function isBuiltinObject(node) {
6
6
  return BUILTIN_OBJECTS.values().some((obj) => obj.name === node.text);
7
7
  }
@@ -73,7 +73,8 @@ export function escapeString(str) {
73
73
  .replace(/"/g, '\\"')
74
74
  .replace(/\n/g, "\\n")
75
75
  .replace(/\r/g, "\\r")
76
- .replace(/\t/g, "\\t");
76
+ .replace(/\t/g, "\\t")
77
+ .replace(/\?/g, "\\?");
77
78
  }
78
79
  export function getJsVarName(node) {
79
80
  return `"${node.text}"`;
@@ -86,13 +87,13 @@ export function getDerefCode(nodeText, varName, context, typeInfo) {
86
87
  varName = varName + '"';
87
88
  const symbolName = varName.slice(1).slice(0, -1);
88
89
  const symbol = context.localScopeSymbols.get(symbolName) ??
89
- context.topLevelScopeSymbols.get(symbolName);
90
- const checkedIfUninitialized = symbol?.checkedIfUninitialized ||
90
+ context.globalScopeSymbols.get(symbolName);
91
+ const isInitialized = symbol?.checked.initialized ||
91
92
  false;
92
93
  // Mark the symbol as checked
93
- this.markSymbolAsChecked(symbolName, context.topLevelScopeSymbols, context.localScopeSymbols);
94
+ this.markSymbolAsInitialized(symbolName, context.globalScopeSymbols, context.localScopeSymbols);
94
95
  // Apply deref code
95
- if (checkedIfUninitialized) {
96
+ if (isInitialized) {
96
97
  if (typeInfo && typeInfo.needsHeapAllocation) {
97
98
  return `(*${nodeText})`;
98
99
  }
@@ -105,15 +106,15 @@ export function getDerefCode(nodeText, varName, context, typeInfo) {
105
106
  return `jspp::Access::deref_stack(${nodeText}, ${varName})`;
106
107
  }
107
108
  }
108
- export function markSymbolAsChecked(name, topLevel, local) {
109
+ export function markSymbolAsInitialized(name, topLevel, local) {
109
110
  if (topLevel.has(name)) {
110
111
  topLevel.update(name, {
111
- checkedIfUninitialized: true,
112
+ checked: { initialized: true },
112
113
  });
113
114
  }
114
115
  else if (local.has(name)) {
115
116
  local.update(name, {
116
- checkedIfUninitialized: true,
117
+ checked: { initialized: true },
117
118
  });
118
119
  }
119
120
  }
@@ -122,39 +123,58 @@ export function getReturnCommand(context) {
122
123
  ? "co_return"
123
124
  : "return";
124
125
  }
125
- export function hoistDeclaration(decl, hoistedSymbols) {
126
+ export function hoistDeclaration(decl, hoistedSymbols, scopeNode) {
126
127
  const name = decl.name?.getText();
127
128
  if (!name) {
128
129
  return `/* Unknown declaration name: ${ts.SyntaxKind[decl.kind]} */`;
129
130
  }
130
- const isLetOrConst = (decl.parent.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) !==
131
- 0;
132
- // if (name === "letVal") console.log("hoistDeclaration: letVal isLetOrConst=", isLetOrConst, " flags=", decl.parent.flags);
133
- const symbolType = isLetOrConst
134
- ? DeclaredSymbolType.letOrConst
135
- : (ts.isFunctionDeclaration(decl)
136
- ? DeclaredSymbolType.function
137
- : (ts.isClassDeclaration(decl)
138
- ? DeclaredSymbolType.letOrConst
139
- : DeclaredSymbolType.var));
131
+ const isLet = (decl.parent.flags & (ts.NodeFlags.Let)) !== 0;
132
+ const isConst = (decl.parent.flags & (ts.NodeFlags.Const)) !== 0;
133
+ const declType = isLet
134
+ ? DeclarationType.let
135
+ : isConst
136
+ ? DeclarationType.const
137
+ : ts.isFunctionDeclaration(decl)
138
+ ? DeclarationType.function
139
+ : ts.isClassDeclaration(decl)
140
+ ? DeclarationType.class
141
+ : DeclarationType.var;
140
142
  if (hoistedSymbols.has(name)) {
141
- const existingType = hoistedSymbols.get(name)?.type;
142
- // Don't allow multiple declaration of `letOrConst` or `function` or `class` variables
143
- if (existingType === DeclaredSymbolType.letOrConst ||
144
- existingType === DeclaredSymbolType.function ||
145
- existingType !== symbolType) {
146
- throw new SyntaxError(`Identifier '${name}' has already been declared`);
143
+ const existingSymbol = hoistedSymbols.get(name);
144
+ // Don't allow multiple declaration of `let`,`const`,`function` or `class` variables
145
+ if (existingSymbol?.type === DeclarationType.let ||
146
+ existingSymbol?.type === DeclarationType.const ||
147
+ existingSymbol?.type === DeclarationType.function ||
148
+ existingSymbol?.type === DeclarationType.class ||
149
+ existingSymbol?.type !== declType) {
150
+ throw new SyntaxError(`Identifier '${name}' has already been declared.\n\n${" ".repeat(6) + decl.getText()}\n`);
147
151
  }
148
152
  // `var` variables can be declared multiple times
149
153
  return "";
150
154
  }
151
- hoistedSymbols.set(name, {
152
- type: symbolType,
153
- checkedIfUninitialized: false,
154
- });
155
+ else {
156
+ // Add the symbol to the hoisted symbols
157
+ if (declType === DeclarationType.function) {
158
+ const isAsync = this.isAsyncFunction(decl);
159
+ const isGenerator = this.isGeneratorFunction(decl);
160
+ hoistedSymbols.add(name, {
161
+ type: declType,
162
+ func: { isAsync, isGenerator },
163
+ });
164
+ // Don't hoist functions not used as a variable
165
+ // they will be called with their native lambdas
166
+ if (!this.isFunctionUsedAsValue(decl, scopeNode) &&
167
+ !this.isFunctionUsedBeforeDeclaration(name, scopeNode)) {
168
+ return "";
169
+ }
170
+ }
171
+ else {
172
+ hoistedSymbols.add(name, { type: declType });
173
+ }
174
+ }
155
175
  const scope = this.getScopeForNode(decl);
156
176
  const typeInfo = this.typeAnalyzer.scopeManager.lookupFromScope(name, scope);
157
- const initializer = isLetOrConst || ts.isClassDeclaration(decl)
177
+ const initializer = isLet || isConst || ts.isClassDeclaration(decl)
158
178
  ? "jspp::Constants::UNINITIALIZED"
159
179
  : "jspp::Constants::UNDEFINED";
160
180
  if (typeInfo.needsHeapAllocation) {
@@ -246,3 +266,121 @@ export function collectBlockScopedDeclarations(statements) {
246
266
  }
247
267
  return decls;
248
268
  }
269
+ export function isFunctionUsedAsValue(decl, root) {
270
+ const funcName = decl.name?.getText();
271
+ if (!funcName)
272
+ return false;
273
+ let isUsed = false;
274
+ const visitor = (node) => {
275
+ if (isUsed)
276
+ return;
277
+ if (ts.isIdentifier(node) && node.text === funcName) {
278
+ const scope = this.getScopeForNode(node);
279
+ const typeInfo = this.typeAnalyzer.scopeManager.lookupFromScope(node.text, scope);
280
+ if (typeInfo?.declaration === decl) {
281
+ const parent = node.parent;
282
+ // Ignore declarations where the identifier is the name being declared
283
+ if ((ts.isFunctionDeclaration(parent) ||
284
+ ts.isVariableDeclaration(parent) ||
285
+ ts.isClassDeclaration(parent) ||
286
+ ts.isMethodDeclaration(parent) ||
287
+ ts.isParameter(parent) ||
288
+ ts.isImportSpecifier(parent)) &&
289
+ parent.name === node) {
290
+ // Declaration, do nothing
291
+ } // Ignore property names (e.g. obj.funcName)
292
+ else if ((ts.isPropertyAccessExpression(parent) &&
293
+ parent.name === node) ||
294
+ (ts.isPropertyAssignment(parent) && parent.name === node)) {
295
+ // Property name, do nothing
296
+ } // Ignore direct calls (e.g. funcName())
297
+ else if (ts.isCallExpression(parent) && parent.expression === node) {
298
+ // Call, do nothing
299
+ }
300
+ else {
301
+ // Used as a value
302
+ isUsed = true;
303
+ }
304
+ }
305
+ }
306
+ if (!isUsed) {
307
+ ts.forEachChild(node, visitor);
308
+ }
309
+ };
310
+ ts.forEachChild(root, visitor);
311
+ return isUsed;
312
+ }
313
+ export function isFunctionUsedBeforeDeclaration(funcName, root) {
314
+ let declPos = -1;
315
+ let foundDecl = false;
316
+ // Helper to find the function declaration position
317
+ function findDecl(node) {
318
+ if (foundDecl)
319
+ return;
320
+ if (ts.isFunctionDeclaration(node) && node.name?.text === funcName) {
321
+ declPos = node.getStart();
322
+ foundDecl = true;
323
+ }
324
+ else {
325
+ ts.forEachChild(node, findDecl);
326
+ }
327
+ }
328
+ findDecl(root);
329
+ // If not declared in this scope (or at least not found), assume it's not a local forward-ref issue
330
+ if (!foundDecl)
331
+ return false;
332
+ let isUsedBefore = false;
333
+ function visit(node) {
334
+ if (isUsedBefore)
335
+ return;
336
+ if (ts.isIdentifier(node) && node.text === funcName) {
337
+ const parent = node.parent;
338
+ // Ignore declarations where the identifier is the name being declared
339
+ if ((ts.isFunctionDeclaration(parent) ||
340
+ ts.isVariableDeclaration(parent) ||
341
+ ts.isClassDeclaration(parent) ||
342
+ ts.isMethodDeclaration(parent) ||
343
+ ts.isParameter(parent) ||
344
+ ts.isImportSpecifier(parent)) &&
345
+ parent.name === node) {
346
+ // Declaration (or shadowing), do nothing
347
+ } // Ignore property names (e.g. obj.funcName)
348
+ else if ((ts.isPropertyAccessExpression(parent) &&
349
+ parent.name === node) ||
350
+ (ts.isPropertyAssignment(parent) && parent.name === node)) {
351
+ // Property name, do nothing
352
+ }
353
+ else {
354
+ // It is a usage (call or value usage)
355
+ const usagePos = node.getStart();
356
+ // Check 1: Lexically before
357
+ if (usagePos < declPos) {
358
+ isUsedBefore = true;
359
+ return;
360
+ }
361
+ // Check 2: Inside a function declared before
362
+ let current = parent;
363
+ while (current && current !== root) {
364
+ if (ts.isFunctionDeclaration(current) ||
365
+ ts.isMethodDeclaration(current) ||
366
+ ts.isArrowFunction(current) ||
367
+ ts.isFunctionExpression(current) ||
368
+ ts.isGetAccessor(current) ||
369
+ ts.isSetAccessor(current) ||
370
+ ts.isConstructorDeclaration(current)) {
371
+ if (current.getStart() < declPos) {
372
+ isUsedBefore = true;
373
+ return;
374
+ }
375
+ // Once we hit a function boundary, we stop bubbling up for this usage.
376
+ break;
377
+ }
378
+ current = current.parent;
379
+ }
380
+ }
381
+ }
382
+ ts.forEachChild(node, visit);
383
+ }
384
+ visit(root);
385
+ return isUsedBefore;
386
+ }
@@ -1,7 +1,7 @@
1
- import { DeclaredSymbols } from "../../ast/symbols";
2
- import { generateLambda } from "./function-handlers";
3
- import { escapeString, generateUniqueExceptionName, generateUniqueName, getDeclaredSymbols, getDerefCode, getJsVarName, getReturnCommand, getScopeForNode, hoistDeclaration, indent, isAsyncFunction, isBuiltinObject, isGeneratorFunction, markSymbolAsChecked, prepareScopeSymbolsForVisit, } from "./helpers";
4
- import { visit } from "./visitor";
1
+ import { DeclaredSymbols } from "../../ast/symbols.js";
2
+ import { generateFullLambdaExpression, generateLambda, } from "./function-handlers.js";
3
+ import { escapeString, generateUniqueExceptionName, generateUniqueName, getDeclaredSymbols, getDerefCode, getJsVarName, getReturnCommand, getScopeForNode, hoistDeclaration, indent, isAsyncFunction, isBuiltinObject, isFunctionUsedAsValue, isFunctionUsedBeforeDeclaration, isGeneratorFunction, markSymbolAsInitialized, prepareScopeSymbolsForVisit, } from "./helpers.js";
4
+ import { visit } from "./visitor.js";
5
5
  const MODULE_NAME = "__main_function__";
6
6
  export class CodeGenerator {
7
7
  indentationLevel = 0;
@@ -25,9 +25,12 @@ export class CodeGenerator {
25
25
  isGeneratorFunction = isGeneratorFunction;
26
26
  isAsyncFunction = isAsyncFunction;
27
27
  prepareScopeSymbolsForVisit = prepareScopeSymbolsForVisit;
28
- markSymbolAsChecked = markSymbolAsChecked;
28
+ markSymbolAsInitialized = markSymbolAsInitialized;
29
+ isFunctionUsedAsValue = isFunctionUsedAsValue;
30
+ isFunctionUsedBeforeDeclaration = isFunctionUsedBeforeDeclaration;
29
31
  // function handlers
30
32
  generateLambda = generateLambda;
33
+ generateFullLambdaExpression = generateFullLambdaExpression;
31
34
  /**
32
35
  * Main entry point for the code generation process.
33
36
  */
@@ -40,11 +43,12 @@ export class CodeGenerator {
40
43
  containerCode +=
41
44
  `${this.indent()}jspp::AnyValue ${this.globalThisVar} = global;\n`;
42
45
  containerCode += this.visit(ast, {
46
+ currentScopeNode: ast,
43
47
  isMainContext: true,
44
48
  isInsideFunction: true,
45
49
  isFunctionBody: true,
46
50
  isInsideAsyncFunction: true,
47
- topLevelScopeSymbols: new DeclaredSymbols(),
51
+ globalScopeSymbols: new DeclaredSymbols(),
48
52
  localScopeSymbols: new DeclaredSymbols(),
49
53
  });
50
54
  this.indentationLevel--;
@@ -1,5 +1,5 @@
1
1
  import ts from "typescript";
2
- import { CodeGenerator } from "./";
2
+ import { CodeGenerator } from "./index.js";
3
3
  export function visitIdentifier(node) {
4
4
  if (node.text === "NaN") {
5
5
  // return "jspp::AnyValue::make_nan()";