From 4dd562aa6ea0d0eb53892da0c82ec4591d0ba2da Mon Sep 17 00:00:00 2001 From: EthanThatOneKid <31261035+EthanThatOneKid@users.noreply.github.com> Date: Thu, 23 Dec 2021 02:30:27 -0800 Subject: [PATCH] some progress with 3 failures --- .vscode/settings.json | 2 +- lib/transpile/cartridge/cartridge.ts | 54 +++++++- lib/transpile/text_builder/text_builder.ts | 6 +- lib/transpile/transpile.test.ts | 111 +++++++++++++-- lib/transpile/transpile.ts | 149 ++++++++++++++++++--- lib/transpile/utils.ts | 10 +- 6 files changed, 294 insertions(+), 38 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 930532a..6b66ee3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -13,5 +13,5 @@ "https://cdn.skypack.dev": false, "https://fart.tools": false }, - "cSpell.words": ["typedefs", "typemap", "typemaps"] + "cSpell.words": ["transpiles", "typedefs", "typemap", "typemaps"] } diff --git a/lib/transpile/cartridge/cartridge.ts b/lib/transpile/cartridge/cartridge.ts index cfadb26..8a0116f 100644 --- a/lib/transpile/cartridge/cartridge.ts +++ b/lib/transpile/cartridge/cartridge.ts @@ -11,6 +11,21 @@ export enum CartridgeEvent { FileEnd = "file_end", } +export enum ReservedType { + Omit = "_", + Number = "number", + String = "string", + Boolean = "boolean", + Default = "any", +} + +export enum Modifier { + Array = "array", // Modifies anything. + Async = "async", // Modifies anything. + Dictionary = "dict", // Modifies length-2 tuples. + Function = "fn", // Modifies length-2 tuples. +} + export type CartridgeEventReturnType = ( | void | Promise @@ -73,11 +88,32 @@ export interface CartridgeHandlerMap { } /** - * @todo @ethanthatonekid pass all tests in ./cartridge.test.ts - * @todo @ethanthatonekid refactor + * Returns a type composed into plain text (e.g. `number`, + * `Array`, `(a: number, b: number) => number`, etc.). + */ +export type ModHandler = (...inner: string[]) => string; + +/** + * The TypeMap API is designed to delegate the generation of + * syntax for various programming languages. */ +export interface CartridgeTypeMap { + [ReservedType.Omit]?: string; + [ReservedType.Number]?: string; + [ReservedType.String]?: string; + [ReservedType.Boolean]?: string; + [ReservedType.Default]?: string; + + // Modifiers are not required for all languages. + [Modifier.Array]?: ModHandler; + [Modifier.Async]?: ModHandler; + [Modifier.Dictionary]?: ModHandler; + [Modifier.Function]?: ModHandler; +} + export class Cartridge { constructor( + private typemap: CartridgeTypeMap = {}, private handlers: CartridgeHandlerMap = {}, ) {} @@ -121,9 +157,7 @@ export class Cartridge { this.handlers[name] = handler; } - /** - * `on` is an alias for `addEventListener` - */ + /** `on` is an alias for `addEventListener` */ public on = this.addEventListener.bind(this); public removeEventListener(name: CartridgeEvent) { @@ -138,8 +172,16 @@ export class Cartridge { if (handleEvent === undefined) return null; const executionResult = handleEvent(ctx); if (executionResult instanceof Promise) { - return await executionResult ?? null; + return (await executionResult) ?? null; } return executionResult ?? null; } + + public getType(type: string): string | undefined { + return this.typemap[type as ReservedType]; + } + + public getMod(mod: string): ModHandler | undefined { + return this.typemap[mod as Modifier]; + } } diff --git a/lib/transpile/text_builder/text_builder.ts b/lib/transpile/text_builder/text_builder.ts index 1553344..e822708 100644 --- a/lib/transpile/text_builder/text_builder.ts +++ b/lib/transpile/text_builder/text_builder.ts @@ -81,8 +81,8 @@ export class TextBuilder { ): Promise; public async append( event: CartridgeEvent.FileEnd, - tokens: Token[], - comments: Token[], + tokens?: Token[], + comments?: Token[], ): Promise; public async append( event: CartridgeEvent, @@ -99,6 +99,7 @@ export class TextBuilder { CartridgeEvent.FileStart, makeFileStartEventContext(this.currentBlock, tokens), ); + this.stash(); break; } @@ -172,6 +173,7 @@ export class TextBuilder { } case CartridgeEvent.FileEnd: { + this.stash(); code = await this.cartridge.dispatch( CartridgeEvent.FileEnd, makeFileEndEventContext(this.currentBlock, tokens), diff --git a/lib/transpile/transpile.test.ts b/lib/transpile/transpile.test.ts index 71484cc..58a298f 100644 --- a/lib/transpile/transpile.test.ts +++ b/lib/transpile/transpile.test.ts @@ -2,23 +2,69 @@ import { assertEquals } from "../../deps/std/testing.ts"; import { TranspilationContext, transpile } from "./transpile.ts"; import { Cartridge, CartridgeEvent } from "./cartridge/mod.ts"; import type { CartridgeEventContext } from "./cartridge/mod.ts"; -import { TextBuilder } from "./text_builder/mod.ts"; import { tokenize } from "./tokenize/mod.ts"; Deno.test("create transpilation context without crashing", () => { const iterator = tokenize(""); const cartridge = new Cartridge(); - const builder = new TextBuilder(cartridge); - const ctx = new TranspilationContext(iterator, builder); + const ctx = new TranspilationContext(iterator, cartridge); assertEquals(ctx.started, false); }); -Deno.test("empty ", () => { - const iterator = tokenize(""); - const cartridge = new Cartridge(); - const builder = new TextBuilder(cartridge); - const ctx = new TranspilationContext(iterator, builder); - assertEquals(ctx.started, false); +Deno.test("empty input only fires file_start event and then file_end event", async () => { + const fakeCart = new Cartridge(); + fakeCart.on(CartridgeEvent.FileStart, () => "ABC"); + fakeCart.on(CartridgeEvent.FileEnd, () => "XYZ"); + const result = await transpile("", fakeCart); + assertEquals(result, "ABC\n\nXYZ"); +}); + +Deno.test("transpiles inline_comment event", async () => { + const fakeCart = new Cartridge(); + fakeCart.on( + CartridgeEvent.InlineComment, + (event: CartridgeEventContext) => { + assertEquals(event.data.comments, ["hello world"]); + return "ABC"; + }, + ); + const result = await transpile("; hello world", fakeCart); + assertEquals(result, "ABC"); +}); + +Deno.test("transpiles multiline_comment event", async () => { + const fakeCart = new Cartridge(); + fakeCart.on( + CartridgeEvent.MultilineComment, + (event: CartridgeEventContext) => { + assertEquals(event.data.comments, ["example"]); + return "ABC"; + }, + ); + const result = await transpile( + `/* + example +*/`, + fakeCart, + ); + assertEquals(result, "ABC"); +}); + +Deno.test("transpiles load event", async () => { + const fakeCart = new Cartridge(); + fakeCart.on( + CartridgeEvent.Load, + (event: CartridgeEventContext) => { + assertEquals(event.data.source, "./example.fart"); + assertEquals(event.data.dependencies, ["Example1", "Example2"]); + return "ABC"; + }, + ); + const result = await transpile( + "load './example.fart' ( Example1, Example2 )", + fakeCart, + ); + assertEquals(result, "ABC"); }); Deno.test("transpiles struct_open event", async () => { @@ -34,3 +80,50 @@ Deno.test("transpiles struct_open event", async () => { const result = await transpile(`type Example {`, fakeCart); assertEquals(result, "ABC"); }); + +Deno.test("transpiles set_property event", async () => { + const fakeCart = new Cartridge(); + fakeCart.on( + CartridgeEvent.SetProperty, + (event: CartridgeEventContext) => { + assertEquals(event.data.name, "example"); + assertEquals(event.data.definition.optional, false); + assertEquals(event.data.comments, []); + return "ABC"; + }, + ); + const result = await transpile( + `type Example { example: string }`, + fakeCart, + ); + assertEquals(result, "ABC"); +}); +// StructClose = "struct_close", +// Deno.test("transpiles struct_open event", async () => { +// const fakeCart = new Cartridge(); +// fakeCart.on( +// CartridgeEvent.StructOpen, +// (event: CartridgeEventContext) => { +// assertEquals(event.data.name, "Example"); +// assertEquals(event.data.comments, []); +// return "ABC"; +// }, +// ); +// const result = await transpile(`type Example {`, fakeCart); +// assertEquals(result, "ABC"); +// }); +// FileEnd = "file_end", + +// Deno.test("transpiles struct_open event", async () => { +// const fakeCart = new Cartridge(); +// fakeCart.on( +// CartridgeEvent.StructOpen, +// (event: CartridgeEventContext) => { +// assertEquals(event.data.name, "Example"); +// assertEquals(event.data.comments, []); +// return "ABC"; +// }, +// ); +// const result = await transpile(`type Example {`, fakeCart); +// assertEquals(result, "ABC"); +// }); diff --git a/lib/transpile/transpile.ts b/lib/transpile/transpile.ts index 027cd59..78b0951 100644 --- a/lib/transpile/transpile.ts +++ b/lib/transpile/transpile.ts @@ -1,5 +1,6 @@ import { Lexicon, Token, tokenize } from "./tokenize/mod.ts"; import { Cartridge, CartridgeEvent } from "./cartridge/mod.ts"; +import type { PropertyDefinition } from "./cartridge/mod.ts"; import { TextBuilder } from "./text_builder/mod.ts"; // import { Lang } from "../constants/lang.ts"; import { assertKind } from "./utils.ts"; @@ -22,11 +23,14 @@ export class TranspilationContext { public started = false; public done = false; public prevTokens: Token[] = []; + public builder: TextBuilder; constructor( public tokenizer: FartTokenGenerator, - public builder: TextBuilder, - ) {} + public cartridge: Cartridge, + ) { + this.builder = new TextBuilder(cartridge); + } public nextToken(): Token | undefined { if (this.done) return undefined; @@ -37,15 +41,100 @@ export class TranspilationContext { } /** - * @todo implement + * Consumes the next struct, tuple, or value. */ - public async nextStruct(): Promise { + public async nextLiteral(currentToken?: Token): Promise { + const def: PropertyDefinition = {}; + const wildToken = currentToken ?? this.nextToken(); + + switch (wildToken?.kind) { + case Lexicon.StructOpener: { + def.struct = await this.nextStruct(); + break; + } + + case Lexicon.TupleOpener: { + def.tuple = await this.nextTuple(); + break; + } + + case Lexicon.Identifier: { + // const modifier = await this.nextModifier(wildToken); + // if (modifier !== undefined) { + // if ident is known modifier, await nextModifier(); + // } + def.value = wildToken.value; + break; + } + + case Lexicon.TextLiteral: { + def.value = wildToken.value; + break; + } + + default: { + const errMessage = + `Expected struct opener, tuple opener, or type value, but got '${wildToken}'`; + throw new Error(errMessage); + } + } + + return def; + } + + public async nextStruct(): Promise { + const result: PropertyDefinition["struct"] = {}; + + while (true) { + const ident = assertKind( + this.nextToken(), + Lexicon.Identifier, + Lexicon.StructCloser, + ); + if (ident.kind === Lexicon.StructCloser) break; + + const propertyDefiner = assertKind( + this.nextToken(), + Lexicon.PropertyDefiner, + Lexicon.PropertyOptionalDefiner, + ); + + // 1st token of right-hand expression + const wildToken = await this.nextToken(); + + switch (wildToken?.kind) { + case Lexicon.StructOpener: { + await this.builder.append( + CartridgeEvent.StructOpen, + [ident, propertyDefiner, wildToken], + [], + ); + result[ident.value] = await this.nextLiteral(wildToken); + break; + } + + case Lexicon.Identifier: + case Lexicon.TextLiteral: { + result[ident.value] = await this.nextLiteral(wildToken); + break; + } + + default: { + throw new Error( + `Expected struct opener, tuple opener, or type value, but got ${wildToken}`, + ); + } + } + } + + return result; } /** * @todo implement */ - public async nextTuple(): Promise { + public async nextTuple(): Promise { + return []; } } @@ -61,7 +150,6 @@ export async function transpile( code: string, options: Cartridge | FartOptions, ): Promise { - // return Promise.resolve(""); // const srcLang = (options as FartOptions).sourceLanguage ?? Lang.Fart; // const targetLang = (options as FartOptions).sourceLanguage ?? Lang.TypeScript; // const indentation: number | undefined = (options as FartOptions).indentation; @@ -69,26 +157,50 @@ export async function transpile( const cartridge = options instanceof Cartridge ? options : options.codeCartridge; + const ctx = new TranspilationContext(tokenize(code), cartridge); - const builder = new TextBuilder(cartridge); - const ctx = new TranspilationContext(tokenize(code), builder); + // dispatch the file_start event at the start of the transpilation + await ctx.builder.append(CartridgeEvent.FileStart); - for ( - let token = ctx.nextToken(); - !ctx.done; - token = ctx.nextToken() - ) { + for (let token = ctx.nextToken(); !ctx.done; token = ctx.nextToken()) { switch (token?.kind) { + case Lexicon.InlineComment: { + const comment = assertKind(token, Lexicon.InlineComment); + await ctx.builder.append( + CartridgeEvent.InlineComment, + [comment], + [comment], + ); + break; + } + + case Lexicon.MultilineComment: { + const comment = assertKind(token, Lexicon.MultilineComment); + await ctx.builder.append( + CartridgeEvent.MultilineComment, + [comment], + [comment], + ); + break; + } + case Lexicon.Load: { const loader = assertKind(token, Lexicon.Load); const source = assertKind(ctx.nextToken(), Lexicon.TextLiteral); - const opener = assertKind(ctx.nextToken(), Lexicon.StructOpener); + const opener = assertKind(ctx.nextToken(), Lexicon.TupleOpener); + const tuple = await ctx.nextTuple(); + if (tuple === undefined) throw new Error("Expected tuple"); + const dependencies = tuple + .filter(({ value: def }) => typeof def.value === "string") + .map(({ value: def }) => def.value as string); await ctx.builder.append( - CartridgeEvent.StructOpen, + CartridgeEvent.Load, [loader, source, opener], [], + undefined, + source.value, + ...dependencies, ); - await ctx.nextTuple(); break; } @@ -100,7 +212,7 @@ export async function transpile( CartridgeEvent.StructOpen, [definer, ident, opener], /* comments=*/ [], - { value: ident.value }, + { value: ident.value }, // pass struct name to builder ); await ctx.nextStruct(); break; @@ -108,5 +220,8 @@ export async function transpile( } } + // dispatch the file_end event at the end of the transpilation + await ctx.builder.append(CartridgeEvent.FileEnd); + return ctx.builder.export(); } diff --git a/lib/transpile/utils.ts b/lib/transpile/utils.ts index dc83242..49d0c06 100644 --- a/lib/transpile/utils.ts +++ b/lib/transpile/utils.ts @@ -3,10 +3,14 @@ import { Lexicon, Token } from "./tokenize/mod.ts"; /** * @todo write tests in utils.test.ts */ -export function assertKind(token: Token | undefined, lexeme: Lexicon): Token { - if (token === undefined || token.kind !== lexeme) { +export function assertKind( + token?: Token, + ...validLex: Lexicon[] +): Token { + const isValidLexeme = validLex.includes(token?.kind ?? Lexicon.Unknown); + if (token === undefined || !isValidLexeme) { throw new Error( - `Expected token kind ${Lexicon[lexeme]}, got ${token}`, + `Expected token kind ${validLex.join(" or ")}, but got ${token}`, ); } return token;