diff --git a/JSTQL/JSTQL-0.0.1.vsix b/JSTQL/JSTQL-0.0.1.vsix index 965b501..e0b3e4f 100644 Binary files a/JSTQL/JSTQL-0.0.1.vsix and b/JSTQL/JSTQL-0.0.1.vsix differ diff --git a/JSTQL/src/language/jstql-validator.ts b/JSTQL/src/language/jstql-validator.ts index e1b94cc..f5e5bec 100644 --- a/JSTQL/src/language/jstql-validator.ts +++ b/JSTQL/src/language/jstql-validator.ts @@ -1,5 +1,5 @@ import type { ValidationAcceptor, ValidationChecks } from "langium"; -import type { JstqlAstType, Pair } from "./generated/ast.js"; +import type { JstqlAstType, Case } from "./generated/ast.js"; import type { JstqlServices } from "./jstql-module.js"; /** @@ -9,7 +9,7 @@ export function registerValidationChecks(services: JstqlServices) { const registry = services.validation.ValidationRegistry; const validator = services.validation.JstqlValidator; const checks: ValidationChecks = { - Pair: validator.validateWildcards, + Case: validator.validateWildcards, }; registry.register(checks, validator); } @@ -18,7 +18,7 @@ export function registerValidationChecks(services: JstqlServices) { * Implementation of custom validations. */ export class JstqlValidator { - validateWildcards(pair: Pair, accept: ValidationAcceptor): void { + validateWildcards(pair: Case, accept: ValidationAcceptor): void { try { let validationResultAplTo = validateWildcardAplTo( collectWildcard(pair.aplTo.apl_to_code.split("")) diff --git a/JSTQL/src/language/jstql.langium b/JSTQL/src/language/jstql.langium index 67eedf0..75ee7d8 100644 --- a/JSTQL/src/language/jstql.langium +++ b/JSTQL/src/language/jstql.langium @@ -5,11 +5,11 @@ entry Model: Proposal: 'proposal' name=ID "{" - (pair+=Pair)+ + (case+=Case)+ "}"; -Pair: - "pair" name=ID "{" +Case: + "case" name=ID "{" aplTo=ApplicableTo traTo=TraTo "}"; diff --git a/dsl_files/awaitToPromise.jstql b/dsl_files/awaitToPromise.jstql new file mode 100644 index 0000000..cc78a41 --- /dev/null +++ b/dsl_files/awaitToPromise.jstql @@ -0,0 +1,17 @@ +proposal awaitToPomise{ + case single{ + applicable to { + "let <> = await <>; + <> + return <> + " + } + + transform to{ + "return <>.then((<>) => { + <> + <> + });" + } + } +} \ No newline at end of file diff --git a/dsl_files/do.jstql b/dsl_files/do.jstql index 9da176a..e1d1213 100644 --- a/dsl_files/do.jstql +++ b/dsl_files/do.jstql @@ -1,9 +1,9 @@ proposal DoExpression{ - pair arrowFunction{ + case arrowFunction{ applicable to { "let <> = () => { - <> - return <>; + <> + return <>; } " } @@ -15,11 +15,11 @@ proposal DoExpression{ } } - pair immediatelyInvokedUnnamedFunction { + case immediatelyInvokedUnnamedFunction { applicable to { "let <> = function(){ - <> - return <>; + <> + return <>; }();" } diff --git a/dsl_files/multi_stmt_test.jstql b/dsl_files/multi_stmt_test.jstql index dc02419..dd92246 100644 --- a/dsl_files/multi_stmt_test.jstql +++ b/dsl_files/multi_stmt_test.jstql @@ -1,5 +1,5 @@ proposal MultiStmt{ - pair Smthn{ + case Smthn{ applicable to{ "let <> = <>(); let <> = <>; diff --git a/dsl_files/pipeline.jstql b/dsl_files/pipeline.jstql index fc81f04..be65747 100644 --- a/dsl_files/pipeline.jstql +++ b/dsl_files/pipeline.jstql @@ -1,5 +1,6 @@ proposal Pipeline{ - pair SingleArgument { + + case SingleArgument { applicable to { "<>(<>);" } @@ -8,4 +9,13 @@ proposal Pipeline{ "<> |> <>(%);" } } + + case TwoArgument{ + applicable to { + "<>(<>, <>)" + } + transform to { + "<> |> <>(%, <>)" + } + } } \ No newline at end of file diff --git a/dsl_files/star.jstql b/dsl_files/star.jstql new file mode 100644 index 0000000..79f5483 --- /dev/null +++ b/dsl_files/star.jstql @@ -0,0 +1,17 @@ +proposal Star{ + case a { + applicable to { + "let <> = () => { + <> + return <>; + } + " + } + transform to { + "let <> = do { + <> + <> + }" + } + } +} \ No newline at end of file diff --git a/dsl_files/test.jstl b/dsl_files/test.jstl deleted file mode 100644 index b55cb1d..0000000 --- a/dsl_files/test.jstl +++ /dev/null @@ -1,14 +0,0 @@ - - - -proposal async { - applicable to { - let _$_a_$_ = await _$_expr_$_(); - console.log(_$_a_$_); - } - replace with { - _$_expr_$_().then(() => { - console.log(_$_a_$_); - }) - } -} \ No newline at end of file diff --git a/dsl_files/test_hello.jstl b/dsl_files/test_hello.jstl deleted file mode 100644 index a20f1a8..0000000 --- a/dsl_files/test_hello.jstl +++ /dev/null @@ -1,11 +0,0 @@ -proposal p1 { - pair something{ - applicable to { - "let a = 0;" - } - transform to { - "" - } - } -} - diff --git a/output_files/output_await_to_promise.js b/output_files/output_await_to_promise.js new file mode 100644 index 0000000..161d609 --- /dev/null +++ b/output_files/output_await_to_promise.js @@ -0,0 +1,100 @@ +// "fast-glob" and `createTwoFilesPatch` are bundled here since the API uses `micromatch` and `diff` too +import { createTwoFilesPatch } from "diff/lib/patch/create.js"; +import fastGlob from "fast-glob"; +import * as vnopts from "vnopts"; +import * as errors from "./common/errors.js"; +import getFileInfoWithoutPlugins from "./common/get-file-info.js"; +import mockable from "./common/mockable.js"; +import { clearCache as clearConfigCache, resolveConfig, resolveConfigFile } from "./config/resolve-config.js"; +import * as core from "./main/core.js"; +import { formatOptionsHiddenDefaults } from "./main/normalize-format-options.js"; +import normalizeOptions from "./main/normalize-options.js"; +import * as optionCategories from "./main/option-categories.js"; +import { clearCache as clearPluginCache, loadBuiltinPlugins, loadPlugins } from "./main/plugins/index.js"; +import { getSupportInfo as getSupportInfoWithoutPlugins, normalizeOptionSettings } from "./main/support.js"; +import { createIsIgnoredFunction } from "./utils/ignore.js"; +import isNonEmptyArray from "./utils/is-non-empty-array.js"; +import omit from "./utils/object-omit.js"; +import partition from "./utils/partition.js"; + +/** + * @param {*} fn + * @param {number} [optionsArgumentIndex] + * @returns {*} + */ +function withPlugins(fn, optionsArgumentIndex = 1 // Usually `options` is the 2nd argument +) { + return async (...args) => { + const options = args[optionsArgumentIndex] ?? {}; + const { + plugins = [] + } = options; + args[optionsArgumentIndex] = { + ...options, + plugins: (await Promise.all([loadBuiltinPlugins(), + // TODO: standalone version allow `plugins` to be `prettierPlugins` which is an object, should allow that too + loadPlugins(plugins)])).flat() + }; + return fn(...args); + }; +} +const formatWithCursor = withPlugins(core.formatWithCursor); +async function format(text, options) { + const { + formatted + } = await formatWithCursor(text, { + ...options, + cursorOffset: -1 + }); + return formatted; +} +async function check(text, options) { + return (await format(text, options)) === text; +} + +// eslint-disable-next-line require-await +async function clearCache() { + clearConfigCache(); + clearPluginCache(); +} + +/** @type {typeof getFileInfoWithoutPlugins} */ +const getFileInfo = withPlugins(getFileInfoWithoutPlugins); + +/** @type {typeof getSupportInfoWithoutPlugins} */ +const getSupportInfo = withPlugins(getSupportInfoWithoutPlugins, 0); + +// Internal shared with cli +const sharedWithCli = { + errors, + optionCategories, + createIsIgnoredFunction, + formatOptionsHiddenDefaults, + normalizeOptions, + getSupportInfoWithoutPlugins, + normalizeOptionSettings, + vnopts: { + ChoiceSchema: vnopts.ChoiceSchema, + apiDescriptor: vnopts.apiDescriptor + }, + fastGlob, + createTwoFilesPatch, + utils: { + isNonEmptyArray, + partition, + omit + }, + mockable +}; +const debugApis = { + parse: withPlugins(core.parse), + formatAST: withPlugins(core.formatAst), + formatDoc: withPlugins(core.formatDoc), + printToDoc: withPlugins(core.printToDoc), + printDocToString: withPlugins(core.printDocToString), + mockable +}; +export { debugApis as __debug, sharedWithCli as __internal, check, clearCache as clearConfigCache, format, formatWithCursor, getFileInfo, getSupportInfo, resolveConfig, resolveConfigFile }; +export * as doc from "./document/public.js"; +export { default as version } from "./main/version.evaluate.cjs"; +export * as util from "./utils/public.js"; \ No newline at end of file diff --git a/output_files/output_do.js b/output_files/output_do.js new file mode 100644 index 0000000..15e630e --- /dev/null +++ b/output_files/output_do.js @@ -0,0 +1,12 @@ +let aaaa = do { + let g = 100; + let ff = 10; + let ggg = a(b); + 100; +}; +let bbaaa = do { + let lllll = 1 + 1; + 100 + 100; + const aaaaa = aaaa(bb); + lllll; +}; \ No newline at end of file diff --git a/output_files/output_pipeline.js b/output_files/output_pipeline.js new file mode 100644 index 0000000..696dfe6 --- /dev/null +++ b/output_files/output_pipeline.js @@ -0,0 +1,4 @@ +a |> w(%) |> w(%) |> w(%) |> w(%) |> w(%) |> w(%) |> w(%) |> w(%) |> w(%) |> w(%); +a |> b(%, a |> b(%, a |> b(%, a |> b(%, a |> b(%, a |> b(%, a |> b(%, b))))))); +a |> b(%, a) |> b(%, a) |> b(%, a) |> b(%, a) |> b(%, a) |> b(%, a) |> b(%, a) |> b(%, a) |> b(%, a) |> b(%, a) |> b(%, a) |> b(%, a); +b(b(b(b(a, a, a), a, a), a, a), a, a); \ No newline at end of file diff --git a/output_files/testingLOL.js b/output_files/testingLOL.js new file mode 100644 index 0000000..006ee6e --- /dev/null +++ b/output_files/testingLOL.js @@ -0,0 +1,98 @@ +// "fast-glob" and `createTwoFilesPatch` are bundled here since the API uses `micromatch` and `diff` too +import { createTwoFilesPatch } from "diff/lib/patch/create.js"; +import fastGlob from "fast-glob"; +import * as vnopts from "vnopts"; +import * as errors from "./common/errors.js"; +import getFileInfoWithoutPlugins from "./common/get-file-info.js"; +import mockable from "./common/mockable.js"; +import { clearCache as clearConfigCache, resolveConfig, resolveConfigFile } from "./config/resolve-config.js"; +import * as core from "./main/core.js"; +import { formatOptionsHiddenDefaults } from "./main/normalize-format-options.js"; +import normalizeOptions from "./main/normalize-options.js"; +import * as optionCategories from "./main/option-categories.js"; +import { clearCache as clearPluginCache, loadBuiltinPlugins, loadPlugins } from "./main/plugins/index.js"; +import { getSupportInfo as getSupportInfoWithoutPlugins, normalizeOptionSettings } from "./main/support.js"; +import { createIsIgnoredFunction } from "./utils/ignore.js"; +import isNonEmptyArray from "./utils/is-non-empty-array.js"; +import omit from "./utils/object-omit.js"; +import partition from "./utils/partition.js"; + +/** + * @param {*} fn + * @param {number} [optionsArgumentIndex] + * @returns {*} + */ +function withPlugins(fn, optionsArgumentIndex = 1 // Usually `options` is the 2nd argument +) { + return async (...args) => { + const options = args[optionsArgumentIndex] ?? {}; + const { + plugins = [] + } = options; + args[optionsArgumentIndex] = { + ...options, + plugins: (await ([loadBuiltinPlugins(), plugins |> loadPlugins(%)] |> Promise.all(%))).flat() + }; + return fn(...args); + }; +} +const formatWithCursor = core.formatWithCursor |> withPlugins(%); +async function format(text, options) { + const { + formatted + } = await (text |> formatWithCursor(%, { + ...options, + cursorOffset: -1 + })); + return formatted; +} +async function check(text, options) { + return (await (text |> format(%, options))) === text; +} + +// eslint-disable-next-line require-await +async function clearCache() { + clearConfigCache(); + clearPluginCache(); +} + +/** @type {typeof getFileInfoWithoutPlugins} */ +const getFileInfo = getFileInfoWithoutPlugins |> withPlugins(%); + +/** @type {typeof getSupportInfoWithoutPlugins} */ +const getSupportInfo = getSupportInfoWithoutPlugins |> withPlugins(%, 0); + +// Internal shared with cli +const sharedWithCli = { + errors, + optionCategories, + createIsIgnoredFunction, + formatOptionsHiddenDefaults, + normalizeOptions, + getSupportInfoWithoutPlugins, + normalizeOptionSettings, + vnopts: { + ChoiceSchema: vnopts.ChoiceSchema, + apiDescriptor: vnopts.apiDescriptor + }, + fastGlob, + createTwoFilesPatch, + utils: { + isNonEmptyArray, + partition, + omit + }, + mockable +}; +const debugApis = { + parse: core.parse |> withPlugins(%), + formatAST: core.formatAst |> withPlugins(%), + formatDoc: core.formatDoc |> withPlugins(%), + printToDoc: core.printToDoc |> withPlugins(%), + printDocToString: core.printDocToString |> withPlugins(%), + mockable +}; +export { debugApis as __debug, sharedWithCli as __internal, check, clearCache as clearConfigCache, format, formatWithCursor, getFileInfo, getSupportInfo, resolveConfig, resolveConfigFile }; +export * as doc from "./document/public.js"; +export { default as version } from "./main/version.evaluate.cjs"; +export * as util from "./utils/public.js"; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 42c2e64..4139edc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ }, "devDependencies": { "@babel/plugin-proposal-pipeline-operator": "^7.23.3", + "@babel/plugin-syntax-top-level-await": "^7.14.5", "@swc/cli": "^0.1.62", "@types/babel__generator": "^7.6.8", "@types/node": "^20.5.9", @@ -337,6 +338,21 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/template": { "version": "7.22.15", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", diff --git a/package.json b/package.json index 9369df4..8b34036 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ }, "devDependencies": { "@babel/plugin-proposal-pipeline-operator": "^7.23.3", + "@babel/plugin-syntax-top-level-await": "^7.14.5", "@swc/cli": "^0.1.62", "@types/babel__generator": "^7.6.8", "@types/node": "^20.5.9", diff --git a/src/data_structures/tree.ts b/src/data_structures/tree.ts index b830cca..c954dd0 100644 --- a/src/data_structures/tree.ts +++ b/src/data_structures/tree.ts @@ -57,7 +57,7 @@ export const showTreePaired = ( console.log( " ".repeat(idents), tree.element.aplToNode.type, - tree.element.codeNode.type + tree.element.codeNode.map((x) => x.type) ); tree.children.forEach((child) => { showTreePaired(child, idents + 1); diff --git a/src/index.ts b/src/index.ts index 5ec05e3..eeac046 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,22 +9,23 @@ import { } from "./transform/transform"; import { parseJSTQL } from "./langium/langiumRunner"; -const path = "test_files/test.js"; + +const dir = "../prettier/src"; + +const path = "../prettier/src/index.js"; const file = Bun.file(path); const codeFromFile = await file.text(); const main = async () => { //transform(selfHostedTransformExampleMultiStmt, codeFromFile); - + console.log(codeFromFile); const jstql_file = "/home/rolfmg/Coding/Master/didactic-chainsaw/dsl_files/pipeline.jstql"; const test_file = Bun.file(jstql_file); const test_JSTQL = await test_file.text(); let proposals = await parseJSTQL(test_JSTQL); - await Bun.write( - "output_files/output.js", - transform(proposals[0].pairs[0], codeFromFile) - ); + let code = transform(proposals[0].cases, codeFromFile); + await Bun.write("output_files/testingLOL.js", code); }; main(); diff --git a/src/langium/langiumRunner.ts b/src/langium/langiumRunner.ts index 80ab1e5..8921ffe 100644 --- a/src/langium/langiumRunner.ts +++ b/src/langium/langiumRunner.ts @@ -1,19 +1,20 @@ import { TransformRecipe, Proposal as LocalProp } from "../transform/transform"; import { parseDSLtoAST } from "../../JSTQL/src/JSTQL_interface/api"; -import { Model, Proposal } from "../../JSTQL/src/language/generated/ast"; +import { Model, Case } from "../../JSTQL/src/language/generated/ast"; export async function parseJSTQL(jstql: string): Promise { let model: Model = await parseDSLtoAST(jstql); - let proposals: LocalProp[] = []; + let localProposals: LocalProp[] = []; for (let proposal of model.proposals) { - let pairs: TransformRecipe[] = []; - for (let pair of proposal.pair) { - pairs.push({ - applicableTo: pair.aplTo.apl_to_code, - transformTo: pair.traTo.transform_to_code, + let cases: TransformRecipe[] = []; + + for (let singleCase of proposal.case) { + cases.push({ + applicableTo: singleCase.aplTo.apl_to_code, + transformTo: singleCase.traTo.transform_to_code, }); } - proposals.push({ pairs }); + localProposals.push({ cases }); } - return proposals; + return localProposals; } diff --git a/src/matcher/matcher.ts b/src/matcher/matcher.ts index 1a75d94..d69cb57 100644 --- a/src/matcher/matcher.ts +++ b/src/matcher/matcher.ts @@ -14,15 +14,16 @@ export interface MatchedTreeNode { export interface PairedNodes { aplToNode: t.Node; - codeNode: t.Node; + codeNode: t.Node[]; } export interface Match { statements: TreeNode[]; } -enum MatchCurrentResult { +enum MatchResult { MatchedWithWildcard, + MatchedWithStarredWildcard, Matched, NoMatch, } @@ -73,14 +74,17 @@ export class Matcher { singleExprMatcher( code: TreeNode, aplTo: TreeNode - ): TreeNode | undefined { + ): [TreeNode | undefined, MatchResult] { // If we are at start of ApplicableTo, start a new search on each of the child nodes if (aplTo.element === this.aplToFull) { // Perform a new search on all child nodes before trying to verify current node let temp = []; // If any matches bubble up from child nodes, we have to store it for (let code_child of code.children) { - let maybeChildMatch = this.singleExprMatcher(code_child, aplTo); + let [maybeChildMatch, matchResult] = this.singleExprMatcher( + code_child, + aplTo + ); if (maybeChildMatch) { temp.push(maybeChildMatch); } @@ -98,59 +102,102 @@ export class Matcher { let curMatches = this.checkCodeNode(code.element, aplTo.element); let pairedCurrent: TreeNode = new TreeNode(null, { - codeNode: code.element, + codeNode: [code.element], aplToNode: aplTo.element, }); - if (curMatches === MatchCurrentResult.NoMatch) { - return; - } else if (curMatches === MatchCurrentResult.MatchedWithWildcard) { - return pairedCurrent; - } else if (code.children.length !== aplTo.children.length) { - return; + if (curMatches === MatchResult.NoMatch) { + return [undefined, MatchResult.NoMatch]; + } else if ( + curMatches === MatchResult.MatchedWithWildcard || + curMatches === MatchResult.MatchedWithStarredWildcard + ) { + return [pairedCurrent, curMatches]; } // At this point current does match // Perform a search on each of the children of both AplTo and Code. - for (let i = 0; i < aplTo.children.length; i++) { - let childSearch = this.singleExprMatcher( + let i = 0; + let aplToi = 0; + while (aplToi < aplTo.children.length) { + if (i >= code.children.length) { + return [undefined, MatchResult.NoMatch]; + } + let [pairedChild, childResult] = this.singleExprMatcher( code.children[i], - aplTo.children[i] + aplTo.children[aplToi] ); - if (childSearch === undefined) { + if (pairedChild === undefined) { // Failed to get a full match, so early return here - return; + return [undefined, MatchResult.NoMatch]; } - childSearch.parent = pairedCurrent; - pairedCurrent.children.push(childSearch); - } + pairedChild.parent = pairedCurrent; + pairedCurrent.children.push(pairedChild); + if (childResult === MatchResult.MatchedWithStarredWildcard) { + i += 1; + while (i < code.children.length) { + let [maybeChild, starChildResult] = this.singleExprMatcher( + code.children[i], + aplTo.children[aplToi] + ); + if ( + starChildResult != + MatchResult.MatchedWithStarredWildcard || + maybeChild === undefined + ) { + i -= 1; + break; + } + pairedChild.element.codeNode.push( + ...maybeChild.element.codeNode + ); + i += 1; + } + } + + i += 1; + aplToi += 1; + } + if (i < code.children.length) { + return [undefined, MatchResult.NoMatch]; + } // If we are here, a full match has been found - return pairedCurrent; + return [pairedCurrent, curMatches]; } - private checkCodeNode( - codeNode: t.Node, - aplToNode: t.Node - ): MatchCurrentResult { + private checkCodeNode(codeNode: t.Node, aplToNode: t.Node): MatchResult { // First verify the internal DSL variables - + if ( + aplToNode.type === "ExpressionStatement" && + aplToNode.expression.type === "Identifier" + ) { + aplToNode = aplToNode.expression; + } if (aplToNode.type === "Identifier") { for (let wildcard of this.internals) { - if (WildcardEvalVisitor.visit(wildcard.expr, codeNode)) { - return MatchCurrentResult.MatchedWithWildcard; + if (aplToNode.name === wildcard.identifier.name) { + let visitorResult = WildcardEvalVisitor.visit( + wildcard.expr, + codeNode + ); + if (visitorResult && wildcard.star) { + return MatchResult.MatchedWithStarredWildcard; + } else if (visitorResult) { + return MatchResult.MatchedWithWildcard; + } } } } if (codeNode.type != aplToNode.type) { - return MatchCurrentResult.NoMatch; + return MatchResult.NoMatch; } //If not an internal DSL variable, gotta verify that the identifier is the same if (codeNode.type === "Identifier" && aplToNode.type === "Identifier") { if (codeNode.name != aplToNode.name) { - return MatchCurrentResult.NoMatch; + return MatchResult.NoMatch; } } for (let key of Object.keys(aplToNode)) { @@ -159,11 +206,11 @@ export class Matcher { } if (!Object.keys(codeNode).includes(key)) { - return MatchCurrentResult.NoMatch; + return MatchResult.NoMatch; } } - return MatchCurrentResult.Matched; + return MatchResult.Matched; } multiStatementMatcher(code: TreeNode, aplTo: TreeNode) { @@ -184,16 +231,43 @@ export class Matcher { for (let y = 0; y <= code.length - aplTo.length; y++) { let fullMatch = true; let statements: TreeNode[] = []; - for (let i = 0; i < aplTo.length; i++) { - let res = this.exactExprMatcher(code[i + y], aplTo[i]); - if (!res) { + let aplToi = 0; + let codei = 0; + while (aplToi < aplTo.length) { + let [paired, matchResult] = this.exactExprMatcher( + code[codei + y], + aplTo[aplToi] + ); + if (!paired) { fullMatch = false; break; } - statements.push(res); + + if (matchResult === MatchResult.MatchedWithStarredWildcard) { + codei += 1; + while (codei + y < code.length) { + let [next, nextMatchRes] = this.exactExprMatcher( + code[codei + y], + aplTo[aplToi] + ); + if ( + !next || + nextMatchRes !== + MatchResult.MatchedWithStarredWildcard + ) { + codei -= 1; + break; + } + paired.element.codeNode.push(...next.element.codeNode); + codei += 1; + } + } + + statements.push(paired); + aplToi += 1; + codei += 1; } if (fullMatch) { - console.log(statements.length); this.matches.push({ statements }); } } @@ -201,34 +275,68 @@ export class Matcher { exactExprMatcher( code: TreeNode, aplTo: TreeNode - ): TreeNode | undefined { - let curMatches = - this.checkCodeNode(code.element, aplTo.element) && - code.children.length >= aplTo.children.length; + ): [TreeNode | undefined, MatchResult] { + let curMatches = this.checkCodeNode(code.element, aplTo.element); - if (!curMatches) { - return undefined; + if (curMatches === MatchResult.NoMatch) { + return [undefined, MatchResult.NoMatch]; } let paired: TreeNode = new TreeNode(null, { aplToNode: aplTo.element, - codeNode: code.element, + codeNode: [code.element], }); - - for (let i = 0; i < aplTo.children.length; i++) { - let childRes = this.exactExprMatcher( - code.children[i], - aplTo.children[i] - ); - if (!childRes) { - // If child is not match the entire thing is not a match; - return undefined; - } - // This is a match, so we store it - childRes.parent = paired; - paired.children.push(childRes); + if ( + curMatches === MatchResult.MatchedWithStarredWildcard || + curMatches === MatchResult.MatchedWithWildcard + ) { + return [paired, curMatches]; } - return paired; + let i = 0; + let aplToi = 0; + while (i < code.children.length && aplToi < aplTo.children.length) { + let [pairedChild, childResult] = this.exactExprMatcher( + code.children[i], + aplTo.children[aplToi] + ); + if (!pairedChild) { + // If child is not match the entire thing is not a match; + return [undefined, MatchResult.NoMatch]; + } + + // This is a match, so we store it + pairedChild.parent = paired; + paired.children.push(pairedChild); + + if (childResult === MatchResult.MatchedWithStarredWildcard) { + i += 1; + while (i < code.children.length) { + let [maybeChild, starChildResult] = this.singleExprMatcher( + code.children[i], + aplTo.children[aplToi] + ); + if ( + starChildResult != + MatchResult.MatchedWithStarredWildcard || + maybeChild === undefined + ) { + i -= 1; + break; + } + pairedChild.element.codeNode.push( + ...maybeChild.element.codeNode + ); + i += 1; + } + } + + i += 1; + aplToi += 1; + } + if (i < code.children.length) { + return [undefined, MatchResult.NoMatch]; + } + return [paired, curMatches]; } } diff --git a/src/matcher/wildcardEvaluator.ts b/src/matcher/wildcardEvaluator.ts index 30629d2..0202f1c 100644 --- a/src/matcher/wildcardEvaluator.ts +++ b/src/matcher/wildcardEvaluator.ts @@ -7,7 +7,9 @@ import { UnaryExpr, Wildcard, WildcardNode, + WildcardParser, } from "../parser/parse"; +import { WildcardTokenizer } from "../parser/wildcardTokenizer"; export class WildcardEvalVisitor { static visit(node: WildcardNode, toComp: t.Node): boolean { @@ -34,9 +36,26 @@ export class WildcardEvalVisitor { let cur = node as Identifier; if (cur.name === "Expression") { return t.isExpression(toComp); + } else if (cur.name === "Statement") { + return t.isStatement(toComp); } return cur.name === toComp.type; } } } } + +function testWildcardEval() { + console.log( + WildcardEvalVisitor.visit( + new WildcardParser( + new WildcardTokenizer( + "statements:(Statement && !ReturnStatement)*" + ).tokenize() + ).parse().expr, + t.variableDeclaration("let", [ + t.variableDeclarator(t.identifier("Id"), null), + ]) + ) + ); +} diff --git a/src/parser/parse.ts b/src/parser/parse.ts index b26c1b0..6fe7264 100644 --- a/src/parser/parse.ts +++ b/src/parser/parse.ts @@ -275,7 +275,15 @@ export function parse_with_plugins( code: string ): babelparser.ParseResult { return babelparser.parse(code, { - plugins: [["pipelineOperator", { proposal: "hack", topicToken: "%" }]], + plugins: [ + ["pipelineOperator", { proposal: "hack", topicToken: "%" }], + "doExpressions", + "topLevelAwait", + ], + allowAwaitOutsideFunction: true, + allowReturnOutsideFunction: true, + allowUndeclaredExports: true, + sourceType: "unambiguous", }); } diff --git a/src/test/test_outputs/awaitToPromise_output.js b/src/test/test_outputs/awaitToPromise_output.js new file mode 100644 index 0000000..18ef1b8 --- /dev/null +++ b/src/test/test_outputs/awaitToPromise_output.js @@ -0,0 +1,9 @@ +async function something() { + let a = 100; + a *= 100000; + return fetch("https://uib.no").then(uib => { + a += 100000; + a -= 1000; + [a, uib]; + }); +} \ No newline at end of file diff --git a/src/test/test_outputs/do_output.js b/src/test/test_outputs/do_output.js new file mode 100644 index 0000000..15e630e --- /dev/null +++ b/src/test/test_outputs/do_output.js @@ -0,0 +1,12 @@ +let aaaa = do { + let g = 100; + let ff = 10; + let ggg = a(b); + 100; +}; +let bbaaa = do { + let lllll = 1 + 1; + 100 + 100; + const aaaaa = aaaa(bb); + lllll; +}; \ No newline at end of file diff --git a/src/test/test_outputs/pipeline_output.js b/src/test/test_outputs/pipeline_output.js new file mode 100644 index 0000000..696dfe6 --- /dev/null +++ b/src/test/test_outputs/pipeline_output.js @@ -0,0 +1,4 @@ +a |> w(%) |> w(%) |> w(%) |> w(%) |> w(%) |> w(%) |> w(%) |> w(%) |> w(%) |> w(%); +a |> b(%, a |> b(%, a |> b(%, a |> b(%, a |> b(%, a |> b(%, a |> b(%, b))))))); +a |> b(%, a) |> b(%, a) |> b(%, a) |> b(%, a) |> b(%, a) |> b(%, a) |> b(%, a) |> b(%, a) |> b(%, a) |> b(%, a) |> b(%, a) |> b(%, a); +b(b(b(b(a, a, a), a, a), a, a), a, a); \ No newline at end of file diff --git a/src/test/test_transform.test.ts b/src/test/test_transform.test.ts index a55ffb6..905fae6 100644 --- a/src/test/test_transform.test.ts +++ b/src/test/test_transform.test.ts @@ -1,39 +1,44 @@ import { expect, test } from "bun:test"; import { TransformRecipe, transform } from "../transform/transform"; +import { parseJSTQL } from "../langium/langiumRunner"; -const transformExample: TransformRecipe = { - applicableTo: `<>(<>);`, - transformTo: "<> |> <>(%)", -}; -const code = - "a(something);a(1+1);something(some_other_thing + 1 + 10 + 100); console.log(a)"; +async function runTest(inputJS: string, inputJSTQL: string): Promise { + //transform(selfHostedTransformExampleMultiStmt, codeFromFile); + const file = Bun.file(inputJS); + const codeFromFile = await file.text(); -test("Test code: " + code + " on " + transformExample.applicableTo, () => { - expect(transform(transformExample, code).length).toBe( - "something |> a(%);1 + 1 |> a(%);some_other_thing + 1 + 10 + 100 |> something(%);console.log(a);" - ); + const test_file = Bun.file(inputJSTQL); + const test_JSTQL = await test_file.text(); + let proposals = await parseJSTQL(test_JSTQL); + + let code = transform(proposals[0].cases, codeFromFile); + return code; +} +let pipelineRes = await runTest( + "test_files/pipeline_test.js", + "dsl_files/pipeline.jstql" +); +let pipelineResFile = await Bun.file( + "src/test/test_outputs/pipeline_output.js" +).text(); +test("Test code: pipeline", () => { + expect(pipelineRes).toBe(pipelineResFile); }); -// Expected outcome: 3 correct matches -const secondTransformExample: TransformRecipe = { - applicableTo: `<>.<>(<>);`, - transformTo: "c |> a.b(%);", -}; -const code2 = `console.log(a);something.sometingOther(b(c));some.thing(1+1); a(b)`; -test( - "Test code: " + code2 + " on " + secondTransformExample.applicableTo, - () => { - expect(transform(secondTransformExample, code2).length).toBe(3); - } -); -// Expected outcome: 1 correct match -const thirdTransformExample: TransformRecipe = { - applicableTo: `myFunction(<>)`, - transformTo: `a |> myFunction(%)`, -}; -const code3 = `myFunction(a);otherFunction(a); myFunction.otherfunction(a)`; -test( - "Test code: " + code3 + " on " + thirdTransformExample.applicableTo, - () => { - expect(transform(thirdTransformExample, code3)).toBe(``); - } +let doRes = await runTest("test_files/do_test.js", "dsl_files/do.jstql"); + +let doResFile = await Bun.file("src/test/test_outputs/do_output.js").text(); +test("Test code: do", () => { + expect(doRes).toBe(doResFile); +}); + +let awaitToPromise = await runTest( + "test_files/do_test.js", + "dsl_files/do.jstql" ); + +let awaitToPromiseOutput = await Bun.file( + "src/test/test_outputs/do_output.js" +).text(); +test("Test code: do", () => { + expect(awaitToPromise).toBe(awaitToPromiseOutput); +}); diff --git a/src/transform/transform.ts b/src/transform/transform.ts index 5136254..6833ca0 100644 --- a/src/transform/transform.ts +++ b/src/transform/transform.ts @@ -2,8 +2,7 @@ import traverse from "@babel/traverse"; import * as t from "@babel/types"; import generate from "@babel/generator"; import { - InternalDSLVariable, - parseInternal, + Wildcard, parseInternalAplTo, parseInternalTraTo, parse_with_plugins, @@ -19,7 +18,7 @@ import { transformMatch, transformer } from "./transformMatch"; import { preludeBuilder } from "../parser/preludeBuilder"; import * as babelparser from "@babel/parser"; export interface Proposal { - pairs: TransformRecipe[]; + cases: TransformRecipe[]; } export interface TransformRecipe { @@ -29,42 +28,48 @@ export interface TransformRecipe { export interface SelfHostedRecipe extends TransformRecipe { prelude: string; } -export function transform(recipe: TransformRecipe, code: string): string { - if ((recipe).prelude !== undefined) { - // We are using the self hosted version - return transformSelfHosted( - { - applicableTo: recipe.applicableTo, - transformTo: recipe.transformTo, - }, - preludeBuilder((recipe as SelfHostedRecipe).prelude), - code - ); - } else { - // We are using JSTQL - // We have to parse JSTQL to the self hosted version +export function transform(recipes: TransformRecipe[], code: string): string { + let codeAST: t.Node = parse_with_plugins(code); - let { cleanedJS: applicableTo, prelude } = parseInternalAplTo( - recipe.applicableTo - ); - let transformTo = parseInternalTraTo(recipe.transformTo); + for (let recipe of recipes) { + if ((recipe).prelude !== undefined) { + // We are using the self hosted version + codeAST = transformSelfHosted( + { + applicableTo: recipe.applicableTo, + transformTo: recipe.transformTo, + }, + preludeBuilder((recipe as SelfHostedRecipe).prelude), + codeAST + ); + } else { + // We are using JSTQL + // We have to parse JSTQL to the self hosted version - return transformSelfHosted( - { applicableTo, transformTo }, - prelude, - code - ); + let { cleanedJS: applicableTo, prelude } = parseInternalAplTo( + recipe.applicableTo + ); + let transformTo = parseInternalTraTo(recipe.transformTo); + + codeAST = transformSelfHosted( + { applicableTo, transformTo }, + prelude, + codeAST + ); + } } + + let output = generate(codeAST, { topicToken: "%" }).code; + //showTree(transformToTree); + return output; } function transformSelfHosted( recipe: TransformRecipe, - internals: InternalDSLVariable, - code: string -): string { - console.log(recipe); - let codeAST = parse_with_plugins(code); - let codeTree = makeTree(codeAST); + internals: Wildcard[], + codeAST: t.Node +): t.Node { + let codeTree = makeTree(codeAST as babelparser.ParseResult); let applicabelToAST = parse_with_plugins(recipe.applicableTo); let applicableToTree = makeTree(applicabelToAST); @@ -78,15 +83,9 @@ function transformSelfHosted( ) { throw new Error("This no worky LOL"); } - showTree(applicableToTree); - console.log(generate(codeAST)); let matches = runMatch(codeTree, applicableToTree, internals); - console.log(matches.length); - + console.log("We found", matches.length, "matches"); let outputAST = transformer(matches, transformToTree, codeAST, transformTo); - - let output = generate(outputAST, { topicToken: "%" }).code; - //showTree(transformToTree); - return output; + return outputAST; } diff --git a/src/transform/transformMatch.ts b/src/transform/transformMatch.ts index 4efd79c..4658c9a 100644 --- a/src/transform/transformMatch.ts +++ b/src/transform/transformMatch.ts @@ -34,13 +34,15 @@ export function transformer( ) ) { if ( - path.node === match.statements[0].element.codeNode + path.node === + match.statements[0].element.codeNode[0] ) { path.replaceWithMultiple( traToWithWildcards.program.body ); let siblings = path.getAllNextSiblings(); + // For multi line applicable to for ( let i = 0; i < match.statements.length - 1; @@ -48,6 +50,30 @@ export function transformer( ) { siblings[i].remove(); } + + // For when we have matched with * + + for (let matchStmt of match.statements) { + for (let [ + i, + stmtMatchedWithStar, + ] of matchStmt.element.codeNode.entries()) { + let siblingnodes = siblings.map( + (a) => a.node + ); + if ( + siblingnodes.includes( + stmtMatchedWithStar + ) + ) { + let index = + siblingnodes.indexOf( + stmtMatchedWithStar + ); + siblings[index].remove(); + } + } + } } } }, @@ -76,6 +102,19 @@ export function transformMatch( path.replaceWithMultiple(match.element.codeNode); } }, + ExpressionStatement: (path) => { + if (trnTo.element.type === "ExpressionStatement") { + if ( + path.node.expression.type === "Identifier" && + trnTo.element.expression.type === "Identifier" + ) { + let ident = path.node.expression; + if (ident.name === trnTo.element.expression.name) { + path.replaceWithMultiple(match.element.codeNode); + } + } + } + }, }); } else { for (let match_child of match.children) { @@ -90,6 +129,18 @@ export function transformMatch( function matchNode(aplTo: t.Node, trnTo: t.Node): boolean { //console.log(trnTo); + if ( + trnTo.type === "ExpressionStatement" && + aplTo.type == "ExpressionStatement" + ) { + if ( + trnTo.expression.type === "Identifier" && + aplTo.expression.type === "Identifier" + ) { + return aplTo.expression.name === trnTo.expression.name; + } + } + if (trnTo.type == "Identifier" && aplTo.type == "Identifier") { return aplTo.name === trnTo.name; } diff --git a/test_files/awaitToPromise.js b/test_files/awaitToPromise.js new file mode 100644 index 0000000..425584a --- /dev/null +++ b/test_files/awaitToPromise.js @@ -0,0 +1,8 @@ +async function something() { + let a = 100; + a *= 100000; + let uib = await fetch("https://uib.no"); + a += 100000; + a -= 1000; + return [a, uib]; +} diff --git a/test_files/do_test.js b/test_files/do_test.js index e69de29..6358119 100644 --- a/test_files/do_test.js +++ b/test_files/do_test.js @@ -0,0 +1,13 @@ +let aaaa = () => { + let g = 100; + let ff = 10; + let ggg = a(b); + return 100; +}; + +var bbaaa = (function () { + let lllll = 1 + 1; + 100 + 100; + const aaaaa = aaaa(bb); + return lllll; +})(); diff --git a/test_files/pipeline_test.js b/test_files/pipeline_test.js new file mode 100644 index 0000000..543367c --- /dev/null +++ b/test_files/pipeline_test.js @@ -0,0 +1,7 @@ +w(w(w(w(w(w(w(w(w(w(a)))))))))); + +b(a, b(a, b(a, b(a, b(a, b(a, b(a, b))))))); + +b(b(b(b(b(b(b(b(b(b(b(b(a, a), a), a), a), a), a), a), a), a), a), a), a); + +b(b(b(b(a, a, a), a, a), a, a), a, a); diff --git a/test_files/star_test.js b/test_files/star_test.js new file mode 100644 index 0000000..264faaf --- /dev/null +++ b/test_files/star_test.js @@ -0,0 +1,4 @@ +let x = () => { + let b = 0; + return b; +};