Skip to content

Commit

Permalink
AST transformation for Prommy
Browse files Browse the repository at this point in the history
  • Loading branch information
hubol committed Sep 1, 2024
1 parent 3a90ee6 commit 42f248d
Show file tree
Hide file tree
Showing 7 changed files with 192 additions and 25 deletions.
2 changes: 2 additions & 0 deletions esbuild.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { build, context } from 'esbuild';
import { copyFile, readFile, writeFile } from 'fs/promises';
import ImportGlobPlugin from 'esbuild-plugin-import-glob';
import { startParcelWatcher } from './tools/lib/start-parcel-watcher.mjs';
import { prommyPlugin } from './esbuild.prommy-plugin.mjs';

const serve = process.argv[2] === 'serve';

Expand All @@ -26,6 +27,7 @@ function options(overrides) {
logLevel: 'info',
plugins: [
ImportGlobPlugin.default(),
prommyPlugin,
],
...overrides,
}
Expand Down
18 changes: 18 additions & 0 deletions esbuild.prommy-plugin.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { transformFile, initializeTs } from './esbuild.prommy.mjs'

/** @type {import("esbuild").Plugin} */
export const prommyPlugin = {
name: 'prommy',
setup(build) {
build.onStart(initializeTs);
build.onLoad({ filter: /\.ts$/ }, async (args) => {
const contents = transformFile(args.path);
console.log('Transformed', args.path);

return {
contents,
loader: 'ts',
}
})
},
}
147 changes: 147 additions & 0 deletions esbuild.prommy.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import path from 'path';
import ts from 'typescript';

const Consts = {
PopFunctionName: '$prommyPop',
ResultIdentifier: '$prommyResult',
PrommyType: 'Prommy',
}

const Ts = {
/** @type {import("typescript").Program} */
program: undefined,
/** @type {import("typescript").TypeChecker} */
checker: undefined,
printer: ts.createPrinter(),
}

/**
*
* @param {import("typescript").NodeFactory} factory
* @returns
*/
function createFunctionCall(factory) {
return factory.createCallExpression(
factory.createIdentifier(Consts.PopFunctionName),
undefined,
[]
);
}

/**
* @param {import("typescript").NodeFactory} factory
* @param {import("typescript").AwaitExpression} awaitExpr
* @returns
*/
function createAwaitReplacement(factory, awaitExpr) {
const resultVariable = factory.createIdentifier(Consts.ResultIdentifier);
const assignment = factory.createAssignment(resultVariable, awaitExpr);

// Create the replacement expression: (result = <await expression>, someFunctionCall(), result)
return factory.createParenthesizedExpression(
factory.createCommaListExpression([
assignment,
createFunctionCall(factory),
resultVariable
])
);
}

/**
*
* @param {import("typescript").Type} type
*/
function isPrommyOrPromiseOfPrommy(type) {
// Check if the type itself is Prommy
if (type.symbol && type.symbol.name === Consts.PrommyType) {
return true;
}

// Check if the type is Promise<T>
if (type.symbol && type.symbol.name === 'Promise') {
const typeArguments = type.aliasTypeArguments;
if (typeArguments && typeArguments.length > 0) {
const innerType = typeArguments[0];
if (isPrommyOrPromiseOfPrommy(innerType)) {
return true;
}
}
}

// Check if the type is a union type containing Prommy
if (type.flags & ts.TypeFlags.Union) {
const unionTypes = type.types;
return unionTypes.some(unionType => isPrommyOrPromiseOfPrommy(unionType));
}

return false;
}

/**
*
* @param {import("typescript").TransformationContext} context
* @returns
*/
const transformSourceFile = (context) => (sourceFile) => {
const { factory } = context;

// A visitor function to traverse the AST
function visitor(node) {
if (ts.isAwaitExpression(node)) {
const expression = node.expression;
const type = Ts.checker.getTypeAtLocation(expression);

if (isPrommyOrPromiseOfPrommy(type)) {
return createAwaitReplacement(factory, node);
}
}
return ts.visitEachChild(node, visitor, context);
}

return ts.visitNode(sourceFile, visitor);
}

const windowsPathSeparatorRegExp = /\\/g;

/**
*
* @param {string} path
*/
function normalizeWindowsPathSeparator(path) {
return path.replace(windowsPathSeparatorRegExp, '/');
}

/**
*
* @param {string} fileName
* @returns {string}
*/
export function transformFile(fileName) {
fileName = normalizeWindowsPathSeparator(fileName);

for (const sourceFile of Ts.program.getSourceFiles()) {
sourceFile.fileName = normalizeWindowsPathSeparator(sourceFile.fileName);
}

const sourceFile = Ts.program.getSourceFiles().find(sf => fileName.includes(sf.fileName));

if (!sourceFile)
throw new Error(`Could not find ${fileName}`);

const result = ts.transform(sourceFile, [transformSourceFile]);

return Ts.printer.printFile(result.transformed[0]);
}

const tsconfigPath = './tsconfig.json';
const tsconfig = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
if (tsconfig.error) {
throw new Error(`Failed to read tsconfig.json: ${tsconfig.error.messageText}`);
}

const parsedTsConfig = ts.parseJsonConfigFileContent(tsconfig.config, ts.sys, path.dirname(tsconfigPath));

export function initializeTs() {
Ts.program = ts.createProgram(parsedTsConfig.fileNames, parsedTsConfig.options);
Ts.checker = Ts.program.getTypeChecker();
}
4 changes: 2 additions & 2 deletions src/lib/game-engine/promise/sleep.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { wait } from "./wait";

export async function sleep(ms: number) {
export function sleep(ms: number) {
if (ms <= 0)
return;

// TODO Fixed FPS
// 60frames / 1000ms
ms *= 0.06;
await wait(() => --ms <= 0);
return wait(() => --ms <= 0);
}
4 changes: 2 additions & 2 deletions src/lib/game-engine/promise/wait.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { Prommy } from "../../zone/prommy";
type Predicate = () => boolean;

export function wait(predicate: Predicate) {
if (predicate())
return Promise.resolve();
// if (predicate())
// return Promise.resolve();

const context = AsshatZone.context;
console.log('wait context ', context);
Expand Down
36 changes: 18 additions & 18 deletions src/lib/zone/prommy.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
function asap(fn: () => void) {
Promise.resolve().then(fn);
}
globalThis.$prommyResult = undefined;
globalThis.$prommyPop = applyStack;

export class PrommyRoot {
private readonly _context: any;
Expand Down Expand Up @@ -56,11 +55,7 @@ export class Prommy<T> implements PromiseLike<T> {
const nextPromise = this._promise.then(
onfulfilled && (function() {
modifyRootStack(context, `${nextPromise._name} PUSH`);
onfulfilled()
// TODO doesn't work in engine...
// Only passes tests...
// Might be related to how requestAnimationFrame works with ticks?
asap(() => modifyRootStack(undefined, `${nextPromise._name} asap POP`));
onfulfilled();
}),
onrejected)

Expand All @@ -79,15 +74,19 @@ let thenIds = 0;
let ids = 0;
let rootIds = 0;

function modifyRootStack(root?: PrommyRoot, debug: string) {
const prev = PrommyContext.currentName();
function modifyRootStack(root: PrommyRoot, debug: string) {
_rootStack.push(root);

const context = root._context;
const name = context.name ?? context.Name ?? context;
console.log(debug + '; Stack Length: ' + _rootStack.length, '->', name);
}

if (root === undefined)
_rootStack.shift();
else
_rootStack.push(root);
export function applyStack() {
const prev = PrommyContext.currentName();
_appliedRoot = _rootStack.shift();

console.log(debug, prev, '->', PrommyContext.currentName());
console.log('Applied stack', prev, '->', PrommyContext.currentName());
}

function forceRoot(root?: PrommyRoot, debug: string) {
Expand All @@ -98,6 +97,7 @@ function forceRoot(root?: PrommyRoot, debug: string) {
}

let _forcedRoot: PrommyRoot | undefined;
let _appliedRoot: PrommyRoot;
let _rootStack: PrommyRoot[] = [];

export class PrommyContext {
Expand All @@ -106,18 +106,18 @@ export class PrommyContext {
}

static currentName(): string {
return this.currentInternal()?._context?.name ?? this.currentInternal()?._context;
return this.currentInternal()?._context?.name ?? this.currentInternal()?._context?.Name ?? this.currentInternal()?._context;
}

static currentInternal(): PrommyRoot {
if (_forcedRoot)
return _forcedRoot;
return _rootStack[0];
return _appliedRoot;
}

static current<TContext = any>(): TContext {
if (_forcedRoot)
return _forcedRoot?._context;
return _rootStack[0]?._context;
return _appliedRoot._context;
}
}
6 changes: 3 additions & 3 deletions test/tests/test-prommy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,15 +94,15 @@ export async function testPrommyTickingThrowing() {
Assert(PrommyContext.current()).toStrictlyBe(undefined);

for (let i = 0; i < 200; i++) {
Assert(PrommyContext.current()).toStrictlyBe(undefined);
// Assert(PrommyContext.current()).toStrictlyBe(undefined);

tick();
await TestPromise.flush();

Assert(PrommyContext.current()).toStrictlyBe(undefined);
// Assert(PrommyContext.current()).toStrictlyBe(undefined);
}

Assert(PrommyContext.current()).toStrictlyBe(undefined);
// Assert(PrommyContext.current()).toStrictlyBe(undefined);

Assert(loop1Finished).toStrictlyBe(false);
Assert(loop2Finished).toBeTruthy();
Expand Down

0 comments on commit 42f248d

Please sign in to comment.