X Tutup
Skip to content

Commit d1efec1

Browse files
authored
Generator and iteration improvements (#872)
* Generator and iteration improvements * Pass input vararg count to unpack * Fix .next() parameter not being passed * Methods are missing `FunctionExpressionFlags.Declaration` * Add changelog
1 parent b2988fd commit d1efec1

File tree

13 files changed

+248
-252
lines changed

13 files changed

+248
-252
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
<!-- TODO: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-9.html doesn't seem to work now -->
66

77
- TypeScript has been updated to 3.9. See [release notes](https://devblogs.microsoft.com/typescript/announcing-typescript-3-9/) for details. This update includes some fixes specific to our API usage:
8+
89
- Importing a non-module using `import "./file"` produced a TS2307 error [#35973](https://github.com/microsoft/TypeScript/issues/35973)
910
- TypeScript now tries to find a call signature even in presence of type errors (#36665)(https://github.com/microsoft/TypeScript/pull/36665):
1011
```ts
@@ -18,6 +19,12 @@
1819
foo(1)
1920
```
2021

22+
- Reduced memory consumption and optimized performance of generators and iterators
23+
24+
- Fixed generator syntax being ignored on methods (`*foo() {}`) and function expressions (`function*() {}`)
25+
26+
- Fixed iteration over generators stopping at first yielded `nil` value
27+
2128
## 0.33.0
2229

2330
- Added support for nullish coalescing `A ?? B`.

src/LuaLib.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export enum LuaLibFeature {
3333
FunctionApply = "FunctionApply",
3434
FunctionBind = "FunctionBind",
3535
FunctionCall = "FunctionCall",
36+
Generator = "Generator",
3637
InstanceOf = "InstanceOf",
3738
InstanceOfObject = "InstanceOfObject",
3839
Iterator = "Iterator",
@@ -72,6 +73,7 @@ const luaLibDependencies: Partial<Record<LuaLibFeature, LuaLibFeature[]>> = {
7273
ArrayFlat: [LuaLibFeature.ArrayConcat],
7374
ArrayFlatMap: [LuaLibFeature.ArrayConcat],
7475
Error: [LuaLibFeature.New, LuaLibFeature.Class, LuaLibFeature.FunctionCall],
76+
Generator: [LuaLibFeature.Symbol],
7577
InstanceOf: [LuaLibFeature.Symbol],
7678
Iterator: [LuaLibFeature.Symbol],
7779
ObjectFromEntries: [LuaLibFeature.Iterator, LuaLibFeature.Symbol],

src/lualib/Generator.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
interface GeneratorIterator {
2+
____coroutine: LuaThread;
3+
[Symbol.iterator](): GeneratorIterator;
4+
next: typeof __TS__GeneratorNext;
5+
}
6+
7+
function __TS__GeneratorIterator(this: GeneratorIterator) {
8+
return this;
9+
}
10+
11+
function __TS__GeneratorNext(this: GeneratorIterator, ...args: Vararg<any[]>) {
12+
const co = this.____coroutine;
13+
if (coroutine.status(co) === "dead") return { done: true };
14+
15+
const [status, value] = coroutine.resume(co, ...args);
16+
if (!status) throw value;
17+
18+
return { value, done: coroutine.status(co) === "dead" };
19+
}
20+
21+
function __TS__Generator(this: void, fn: (this: void, ...args: any[]) => any) {
22+
return function(this: void, ...args: Vararg<any[]>): GeneratorIterator {
23+
const argsLength = select("#", ...args);
24+
return {
25+
// Using explicit this there, since we don't pass arguments after the first nil and context is likely to be nil
26+
____coroutine: coroutine.create(() => fn((unpack || table.unpack)(args, 1, argsLength))),
27+
[Symbol.iterator]: __TS__GeneratorIterator,
28+
next: __TS__GeneratorNext,
29+
};
30+
};
31+
}

src/lualib/Iterator.ts

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,32 @@
1-
function __TS__Iterator<T>(this: void, iterable: Iterable<T>): (this: void) => T {
2-
if (iterable[Symbol.iterator]) {
1+
/** @tupleReturn */
2+
function __TS__IteratorGeneratorStep(this: GeneratorIterator): [true, any] | [] {
3+
const co = this.____coroutine;
4+
5+
const [status, value] = coroutine.resume(co);
6+
if (!status) throw value;
7+
8+
if (coroutine.status(co) === "dead") return [];
9+
return [true, value];
10+
}
11+
12+
/** @tupleReturn */
13+
function __TS__IteratorIteratorStep<T>(this: Iterator<T>): [true, T] | [] {
14+
const result = this.next();
15+
if (result.done) return [];
16+
return [true, result.value];
17+
}
18+
19+
/** @tupleReturn */
20+
function __TS__Iterator<T>(
21+
this: void,
22+
iterable: Iterable<T> | GeneratorIterator | readonly T[]
23+
): [(...args: any[]) => [any, T] | [], ...any[]] {
24+
if ("____coroutine" in iterable) {
25+
return [__TS__IteratorGeneratorStep, iterable];
26+
} else if (iterable[Symbol.iterator]) {
327
const iterator = iterable[Symbol.iterator]();
4-
return () => {
5-
const result = iterator.next();
6-
if (!result.done) {
7-
return result.value;
8-
} else {
9-
return undefined;
10-
}
11-
};
28+
return [__TS__IteratorIteratorStep, iterator];
1229
} else {
13-
let i = 0;
14-
return () => {
15-
i += 1;
16-
return iterable[i];
17-
};
30+
return ipairs(iterable as readonly T[]);
1831
}
1932
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/** @noSelfInFile */
2+
3+
interface LuaThread {
4+
readonly __internal__: unique symbol;
5+
}
6+
7+
declare namespace coroutine {
8+
function create(f: (...args: any[]) => any): LuaThread;
9+
10+
/** @tupleReturn */
11+
function resume(co: LuaThread, ...val: any[]): [true, ...any[]] | [false, string];
12+
13+
function status(co: LuaThread): "running" | "suspended" | "normal" | "dead";
14+
15+
function wrap(f: (...args: any[]) => any): /** @tupleReturn */ (...args: any[]) => any[];
16+
17+
/** @tupleReturn */
18+
function yield(...args: any[]): any[];
19+
}

src/lualib/declarations/global.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,9 @@ declare function unpack<T>(list: T[], i?: number, j?: number): T[];
2424

2525
declare function select<T>(index: number, ...args: T[]): T;
2626
declare function select<T>(index: "#", ...args: T[]): number;
27+
28+
/**
29+
* @luaIterator
30+
* @tupleReturn
31+
*/
32+
declare function ipairs<T>(t: Record<number, T>): LuaTupleIterable<[number, T]>;

src/transformation/visitors/class/members/method.ts

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import * as ts from "typescript";
22
import * as lua from "../../../../LuaAST";
33
import { TransformationContext } from "../../../context";
4-
import { ContextType, getFunctionContextType } from "../../../utils/function-context";
5-
import { createSelfIdentifier } from "../../../utils/lua-ast";
6-
import { transformFunctionBody, transformParameters } from "../../function";
4+
import { transformFunctionToExpression } from "../../function";
75
import { transformPropertyName } from "../../literal";
86
import { isStaticNode } from "../utils";
97

@@ -18,29 +16,17 @@ export function transformMethodDeclaration(
1816
return undefined;
1917
}
2018

19+
const methodTable =
20+
isStaticNode(node) || noPrototype
21+
? lua.cloneIdentifier(className)
22+
: lua.createTableIndexExpression(lua.cloneIdentifier(className), lua.createStringLiteral("prototype"));
23+
2124
let methodName = transformPropertyName(context, node.name);
2225
if (lua.isStringLiteral(methodName) && methodName.value === "toString") {
2326
methodName = lua.createStringLiteral("__tostring", node.name);
2427
}
2528

26-
const type = context.checker.getTypeAtLocation(node);
27-
const functionContext =
28-
getFunctionContextType(context, type) !== ContextType.Void ? createSelfIdentifier() : undefined;
29-
const [paramNames, dots, restParamName] = transformParameters(context, node.parameters, functionContext);
30-
31-
const [body] = transformFunctionBody(context, node.parameters, node.body, restParamName);
32-
const functionExpression = lua.createFunctionExpression(
33-
lua.createBlock(body),
34-
paramNames,
35-
dots,
36-
lua.FunctionExpressionFlags.Declaration,
37-
node.body
38-
);
39-
40-
const methodTable =
41-
isStaticNode(node) || noPrototype
42-
? lua.cloneIdentifier(className)
43-
: lua.createTableIndexExpression(lua.cloneIdentifier(className), lua.createStringLiteral("prototype"));
29+
const [functionExpression] = transformFunctionToExpression(context, node);
4430

4531
return lua.createAssignmentStatement(
4632
lua.createTableIndexExpression(methodTable, methodName),

src/transformation/visitors/function.ts

Lines changed: 45 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as ts from "typescript";
22
import * as lua from "../../LuaAST";
3+
import { assert } from "../../utils";
34
import { FunctionVisitor, TransformationContext } from "../context";
45
import { isVarargType } from "../utils/annotations";
56
import { createDefaultExportStringLiteral, hasDefaultExportModifier } from "../utils/export";
@@ -11,8 +12,8 @@ import {
1112
createSelfIdentifier,
1213
wrapInTable,
1314
} from "../utils/lua-ast";
15+
import { LuaLibFeature, transformLuaLibFunction } from "../utils/lualib";
1416
import { peekScope, performHoisting, popScope, pushScope, Scope, ScopeType } from "../utils/scope";
15-
import { transformGeneratorFunctionBody } from "./generator";
1617
import { transformIdentifier } from "./identifier";
1718
import { transformBindingPattern } from "./variable-declaration";
1819

@@ -157,12 +158,13 @@ export function transformParameters(
157158
return [paramNames, dotsLiteral, restParamName];
158159
}
159160

160-
export function transformFunctionLikeDeclaration(
161-
node: ts.FunctionLikeDeclaration,
162-
context: TransformationContext
163-
): lua.Expression {
164-
const type = context.checker.getTypeAtLocation(node);
161+
export function transformFunctionToExpression(
162+
context: TransformationContext,
163+
node: ts.FunctionLikeDeclaration
164+
): [lua.Expression, Scope] {
165+
assert(node.body);
165166

167+
const type = context.checker.getTypeAtLocation(node);
166168
let functionContext: lua.Identifier | undefined;
167169
if (getFunctionContextType(context, type) !== ContextType.Void) {
168170
if (ts.isArrowFunction(node)) {
@@ -176,14 +178,10 @@ export function transformFunctionLikeDeclaration(
176178
}
177179
}
178180

179-
// Build parameter string
180-
const [paramNames, dotsLiteral, spreadIdentifier] = transformParameters(context, node.parameters, functionContext);
181-
182181
let flags = lua.FunctionExpressionFlags.None;
183-
184-
if (node.body === undefined) {
185-
// This code can be reached only from object methods, which is TypeScript error
186-
return lua.createNilLiteral();
182+
if (!ts.isBlock(node.body)) flags |= lua.FunctionExpressionFlags.Inline;
183+
if (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)) {
184+
flags |= lua.FunctionExpressionFlags.Declaration;
187185
}
188186

189187
let body: ts.Block;
@@ -193,14 +191,11 @@ export function transformFunctionLikeDeclaration(
193191
const returnExpression = ts.createReturn(node.body);
194192
body = ts.createBlock([returnExpression]);
195193
returnExpression.parent = body;
196-
if (node.body) {
197-
body.parent = node.body.parent;
198-
}
199-
flags |= lua.FunctionExpressionFlags.Inline;
194+
if (node.body) body.parent = node.body.parent;
200195
}
201196

202-
const [transformedBody, scope] = transformFunctionBody(context, node.parameters, body, spreadIdentifier);
203-
197+
const [paramNames, dotsLiteral, spreadIdentifier] = transformParameters(context, node.parameters, functionContext);
198+
const [transformedBody, functionScope] = transformFunctionBody(context, node.parameters, body, spreadIdentifier);
204199
const functionExpression = lua.createFunctionExpression(
205200
lua.createBlock(transformedBody),
206201
paramNames,
@@ -209,12 +204,31 @@ export function transformFunctionLikeDeclaration(
209204
node
210205
);
211206

207+
return [
208+
node.asteriskToken
209+
? transformLuaLibFunction(context, LuaLibFeature.Generator, undefined, functionExpression)
210+
: functionExpression,
211+
functionScope,
212+
];
213+
}
214+
215+
export function transformFunctionLikeDeclaration(
216+
node: ts.FunctionLikeDeclaration,
217+
context: TransformationContext
218+
): lua.Expression {
219+
if (node.body === undefined) {
220+
// This code can be reached only from object methods, which is TypeScript error
221+
return lua.createNilLiteral();
222+
}
223+
224+
const [functionExpression, functionScope] = transformFunctionToExpression(context, node);
225+
212226
// Handle named function expressions which reference themselves
213-
if (ts.isFunctionExpression(node) && node.name && scope.referencedSymbols) {
227+
if (ts.isFunctionExpression(node) && node.name && functionScope.referencedSymbols) {
214228
const symbol = context.checker.getSymbolAtLocation(node.name);
215229
if (symbol) {
216230
// TODO: Not using symbol ids because of https://github.com/microsoft/TypeScript/issues/37131
217-
const isReferenced = [...scope.referencedSymbols].some(([, nodes]) =>
231+
const isReferenced = [...functionScope.referencedSymbols].some(([, nodes]) =>
218232
nodes.some(n => context.checker.getSymbolAtLocation(n)?.valueDeclaration === symbol.valueDeclaration)
219233
);
220234

@@ -234,34 +248,19 @@ export function transformFunctionLikeDeclaration(
234248

235249
export const transformFunctionDeclaration: FunctionVisitor<ts.FunctionDeclaration> = (node, context) => {
236250
// Don't transform functions without body (overload declarations)
237-
if (!node.body) {
251+
if (node.body === undefined) {
238252
return undefined;
239253
}
240254

241-
const type = context.checker.getTypeAtLocation(node);
242-
const functionContext =
243-
getFunctionContextType(context, type) !== ContextType.Void ? createSelfIdentifier() : undefined;
244-
const [params, dotsLiteral, restParamName] = transformParameters(context, node.parameters, functionContext);
245-
246-
const [body, functionScope] = node.asteriskToken
247-
? transformGeneratorFunctionBody(context, node.parameters, node.body, restParamName)
248-
: transformFunctionBody(context, node.parameters, node.body, restParamName);
249-
250-
const block = lua.createBlock(body);
251-
const functionExpression = lua.createFunctionExpression(
252-
block,
253-
params,
254-
dotsLiteral,
255-
lua.FunctionExpressionFlags.Declaration
256-
);
257-
258255
if (hasDefaultExportModifier(node)) {
259256
return lua.createAssignmentStatement(
260257
lua.createTableIndexExpression(createExportsIdentifier(), createDefaultExportStringLiteral(node)),
261258
transformFunctionLikeDeclaration(node, context)
262259
);
263260
}
264261

262+
const [functionExpression, functionScope] = transformFunctionToExpression(context, node);
263+
265264
// Name being undefined without default export is a TypeScript error
266265
const name = node.name ? transformIdentifier(context, node.name) : lua.createAnonymousIdentifier();
267266

@@ -278,3 +277,10 @@ export const transformFunctionDeclaration: FunctionVisitor<ts.FunctionDeclaratio
278277

279278
return createLocalOrExportedOrGlobalDeclaration(context, name, functionExpression, node);
280279
};
280+
281+
export const transformYieldExpression: FunctionVisitor<ts.YieldExpression> = (expression, context) =>
282+
lua.createCallExpression(
283+
lua.createTableIndexExpression(lua.createIdentifier("coroutine"), lua.createStringLiteral("yield")),
284+
expression.expression ? [context.transformExpression(expression.expression)] : [],
285+
expression
286+
);

0 commit comments

Comments
 (0)
X Tutup