Skip to content

PythonCZX/do-transformer

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

rbxts-do-transformer

A TypeScript transformer for use with roblox-ts that implements a do-notation by transforming generator functions with yield* expressions into continuation-passing style.

Introduction

In languages that lack native support for capturing and storing continuations, multi-shot algebraic effects may be simulated by writing programs in continuation-passing style. The ability to write imperative-style code can then be recovered by building chains of continuations using native language constructs. In the case of JavaScript, generator functions provide a straightforward way to implement this hiding:

// Example using Effect-ts
const program = pipe(
  Effect.succeed(2),
  Effect.map(n => n * 3),
  Effect.flatMap(n => Effect.succeed(n + 1)),
  Effect.map(n => `Result: ${n}`)
)

const program = Effect.gen(function* () {
  const n = yield* Effect.succeed(2)
  const doubled = n * 3
  const incremented = yield* Effect.succeed(doubled + 1)
  return `Result: ${incremented}`
})

The purpose of this transformer is to provide more control over how sugar for effect primitives is interpreted. By sitting between source code written using multi-shot algebraic effects and the TypeScript AST upon on by roblox-ts, it aims to make ahead-of-time structural changes that would otherwise be burdensome.

Usage

With roblox-ts

Once installed, rbxts-do-transformer, along with any configuration options, must be added to your tsconfig.json.

"compiler options": {
  ...
  "plugins": [
    {
      "transform": "rbxts-do-transformer",
      // "debug": bool,
      // "verbose": bool,
      // "adoAlgorithm": "heuristic" | "optimal"
    }
  ],
}

Embedded

rbxts-do-transformer is easily accessed through the TypeScript compiler API:

import ts from "typescript";
import { doTransformer } from "@rbxts/do-transformer";

const result = ts.transpileModule(sourceCode, {
  compilerOptions: { /* ... */ },
  transformers: {
    before: [doTransformer]
  }
});

Syntax

The transformer recognizes calls to do_() or ado()_ with a generator function:

do_(function* () {
  // Program
})

ado_(function* () {
  // Program
})

Examples

Basic assignment

const program = do_(function* () {
  const { x, y } = yield* getCoordinates();
  const [ a, b ] = yield* getTuple();
  return x + a;
});

// Transformed
const program = seq(getCoordinates(), (_t0) => {
    return (() => {
        const { x, y } = _t0;
        return seq(getTuple(), (_t1) => {
            return (() => {
                const [a, b] = _t1;
                return ret(x + a);
            })();
        });
    })();
});

Transforming using the apply higher-order effect

const program = ado_(function* () {
    const x = yield* getX();
    const y = yield* getY();
    const z = yield* getZ(x, y);
    const w = yield* getW();
    return z + w;
});

// Transformed
const program = op("apply", (z, w) => z + w, [
	seq(
		op("apply", (x, y) => getZ(x, y), [getX(), getY()]),
		(_v) => _v,
	),
	getW(),
]);

Mixed control flow

const program = do_(function* () {
  const a = yield* get();
  for (let i = 0; i < a.upper; i++) {
    do {
      yield* log(`${i}`);
    } while (yield* fetch("resource"))
  }

  switch (a.type) {
    case "ok":
      yield* set("No errors");
    case "error":
      yield* set("Errors");
      break;
    case "fuzzy":
      yield* set("Fuzzy");
    default:
      yield* set("Unknown");
  }
  return "Done";
});

// Transformed
const program = seq(get(), (a) => {
	return (() => {
		let i = 0;
		const _k0 = () =>
			(() => {
				const _k2 = () => ret("Done");
				const _t11 = a.type;
				const _t3 = () =>
					seq(set("No errors"), () => {
						return _t4();
					});
				const _t4 = () =>
					seq(set("Errors"), () => {
						return _k2();
					});
				const _t5 = () =>
					seq(set("Fuzzy"), () => {
						return _t6();
					});
				const _t6 = () =>
					seq(set("Unknown"), () => {
						return _k2();
					});
				return _t11 === "ok" ? _t3() : _t11 === "error" ? _t4() : _t11 === "fuzzy" ? _t5() : _t6();
			})();
		const _loop0 = () =>
			i < a.upper
				? (() => {
						const _k1 = () =>
							(() => {
								i++;
								return _loop0();
							})();
						const _loop1 = () =>
							seq(log(`${i}`), () => {
								return seq(fetch("resource"), (_t2) => {
									return _t2 ? _loop1() : _k1();
								});
							});
						return _loop1();
					})()
				: _k0();
		return _loop0();
	})();
});

Features

  • Transforms yield* expressions into CPS combinators
  • Supports complex control flow (if/else, loops, break/continue)
  • Automatic insertion of applicative operations
  • Debug tracing

How It Works

The transformer performs a small number of transformations:

  1. Yield Extraction: Recursively extracts yield* expressions from nested positions within statements
  2. Statement Sequencing: Converts imperative statements into seq chains
  3. Control Flow: Transforms if/else and loops into functional equivalents
  4. Variable Binding: Lifts out variable declarations and threads them through continuations

Current Limitations

  • Does not support aliases or namespaces for do_ and ado_.
  • Requires seq, ret, and apply to be in scope at runtime
  • Does not support yield without asterisk (though this may become a warning rather than an error in the future)

License

MIT

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages