From 9968bd508b6e64969a7c398a3ea28e2aeef54ca5 Mon Sep 17 00:00:00 2001 From: Novus Nota <68142933+novusnota@users.noreply.github.com> Date: Thu, 7 Nov 2024 13:07:27 +0100 Subject: [PATCH] feat: backport `.isSubsetOf()` --- .github/workflows/tact.yml | 23 +++++++++ scripts/copy-files.ts | 22 ++++++++ src/types/resolveDescriptors.ts | 9 ++-- src/utils/isSubsetOf.spec.ts | 91 +++++++++++++++++++++++++++++++++ src/utils/isSubsetOf.ts | 38 ++++++++++++++ 5 files changed, 179 insertions(+), 4 deletions(-) create mode 100644 scripts/copy-files.ts create mode 100644 src/utils/isSubsetOf.spec.ts create mode 100644 src/utils/isSubsetOf.ts diff --git a/.github/workflows/tact.yml b/.github/workflows/tact.yml index bddd162b1..940fea8d7 100644 --- a/.github/workflows/tact.yml +++ b/.github/workflows/tact.yml @@ -24,6 +24,29 @@ jobs: - name: Checkout repository uses: actions/checkout@v3 + - name: Setup Node.js 18 for backwards compat tests + uses: actions/setup-node@v3 + with: + node-version: 18 + # without caching + + - name: Backwards compat tests + run: | + # Temporarily ignore engines + yarn config set ignore-engines true + # Install dependencies, gen and build the compiler + yarn install + yarn clean + yarn gen + yarn build + # Test some specific things for backwards compatibility. + # It's important to restrain from using too much of Node.js 22+ features + # until it goes into maintenance LTS state and majority of users catches up + yarn jest -t 'isSubsetOf' + # Clean-up + yarn cleanall + yarn config delete ignore-engines + - name: Setup Node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 with: diff --git a/scripts/copy-files.ts b/scripts/copy-files.ts new file mode 100644 index 000000000..ae69bcd37 --- /dev/null +++ b/scripts/copy-files.ts @@ -0,0 +1,22 @@ +import * as fs from "node:fs/promises"; +import * as path from "node:path"; +import * as glob from "glob"; + +const cp = async (fromGlob: string, toPath: string) => { + const files = glob.sync(fromGlob); + for (const file of files) { + await fs.copyFile(file, path.join(toPath, path.basename(file))); + } +}; + +const main = async () => { + try { + await cp("./src/grammar/grammar.ohm*", "./dist/grammar/"); + await cp("./src/func/funcfiftlib.*", "./dist/func/"); + } catch (e) { + console.error(e); + process.exit(1); + } +}; + +void main(); diff --git a/src/types/resolveDescriptors.ts b/src/types/resolveDescriptors.ts index f806e4477..486319bf5 100644 --- a/src/types/resolveDescriptors.ts +++ b/src/types/resolveDescriptors.ts @@ -43,6 +43,7 @@ import { import { getRawAST } from "../grammar/store"; import { cloneNode } from "../grammar/clone"; import { crc16 } from "../utils/crc16"; +import { isSubsetOf } from "../utils/isSubsetOf"; import { evalConstantExpression } from "../constEval"; import { resolveABIType, intMapFormats } from "./resolveABITypeRef"; import { enabledExternals } from "../config/features"; @@ -897,13 +898,13 @@ export function resolveDescriptors(ctx: CompilerContext) { const paramSet = new Set( a.params.map((typedId) => idText(typedId.name)), ); - if (!paramSet.isSubsetOf(shuffleArgSet)) { + if (!isSubsetOf(paramSet, shuffleArgSet)) { throwCompilationError( "asm argument rearrangement must mention all function parameters", a.loc, ); } - if (!shuffleArgSet.isSubsetOf(paramSet)) { + if (!isSubsetOf(shuffleArgSet, paramSet)) { throwCompilationError( "asm argument rearrangement must mention only function parameters", a.loc, @@ -961,13 +962,13 @@ export function resolveDescriptors(ctx: CompilerContext) { // mutating functions also return `self` arg (implicitly in Tact, but explicitly in FunC) retTupleSize += isMutating ? 1 : 0; const returnValueSet = new Set([...Array(retTupleSize).keys()]); - if (!returnValueSet.isSubsetOf(shuffleRetSet)) { + if (!isSubsetOf(returnValueSet, shuffleRetSet)) { throwCompilationError( `asm return rearrangement must mention all return position numbers: [0..${retTupleSize - 1}]`, a.loc, ); } - if (!shuffleRetSet.isSubsetOf(returnValueSet)) { + if (!isSubsetOf(shuffleRetSet, returnValueSet)) { throwCompilationError( `asm return rearrangement must mention only valid return position numbers: [0..${retTupleSize - 1}]`, a.loc, diff --git a/src/utils/isSubsetOf.spec.ts b/src/utils/isSubsetOf.spec.ts new file mode 100644 index 000000000..cde7c2925 --- /dev/null +++ b/src/utils/isSubsetOf.spec.ts @@ -0,0 +1,91 @@ +import { ReadonlySetLike, isSubsetOf } from "./isSubsetOf"; + +// Tests are adapted from: +// https://github.com/zloirock/core-js/blob/227a758ef96fa585a66cc1e89741e7d0bb696f48/tests/unit-global/es.set.is-subset-of.js + +describe("isSubsetOf", () => { + /* eslint-disable @typescript-eslint/no-explicit-any */ + let s1: Set; + let s2: ReadonlySetLike; + + it("should implement isSubsetOf correctly", () => { + s1 = new Set([1]); + s2 = new Set([1, 2, 3]); + expect(isSubsetOf(s1, s2)).toBe(true); + + s1 = new Set([1]); + s2 = new Set([2, 3, 4]); + expect(isSubsetOf(s1, s2)).toBe(false); + + s1 = new Set([1, 2, 3]); + s2 = new Set([5, 4, 3, 2, 1]); + expect(isSubsetOf(s1, s2)).toBe(true); + + s1 = new Set([1, 2, 3]); + s2 = new Set([5, 4, 3, 2]); + expect(isSubsetOf(s1, s2)).toBe(false); + + s1 = new Set([1]); + s2 = createSetLike([1, 2, 3]); + expect(isSubsetOf(s1, s2)).toBe(true); + + s1 = new Set([1]); + s2 = createSetLike([2, 3, 4]); + expect(isSubsetOf(s1, s2)).toBe(false); + + s1 = new Set([1, 2, 3]); + s2 = createSetLike([5, 4, 3, 2, 1]); + expect(isSubsetOf(s1, s2)).toBe(true); + + s1 = new Set([1, 2, 3]); + s2 = createSetLike([5, 4, 3, 2]); + expect(isSubsetOf(s1, s2)).toBe(false); + + s1 = new Set([1, 2, 3]); + s2 = new Set([1]); + expect(isSubsetOf(s1, s2)).toBe(false); + + s1 = new Set([1, 2, 3]); + s2 = new Set(); + expect(isSubsetOf(s1, s2)).toBe(false); + + s1 = new Set(); + s2 = new Set([1, 2, 3]); + expect(isSubsetOf(s1, s2)).toBe(true); + }); +}); + +// Helper functions are adapted from: +// https://github.com/zloirock/core-js/blob/227a758ef96fa585a66cc1e89741e7d0bb696f48/tests/helpers/helpers.js + +function createSetLike(elements: T[]): ReadonlySetLike { + return { + size: elements.length, + has(value: T): boolean { + return includes(elements, value); + }, + keys(): Iterator { + return createIterator(elements); + }, + }; +} + +function includes(target: T[], wanted: T) { + return target.some((element) => element === wanted); +} + +function createIterator(elements: T[]): Iterator { + let index = 0; + const iterator = { + called: false, + /* eslint-disable @typescript-eslint/no-explicit-any */ + next(): IteratorResult { + iterator.called = true; + return { + value: elements[index++], + done: index > elements.length, + }; + }, + }; + return iterator; +} diff --git a/src/utils/isSubsetOf.ts b/src/utils/isSubsetOf.ts new file mode 100644 index 000000000..c333bb58c --- /dev/null +++ b/src/utils/isSubsetOf.ts @@ -0,0 +1,38 @@ +/** Taken from TypeScript collection lib to perfectly match the .isSubsetOf signature */ +export interface ReadonlySetLike { + /** + * Despite its name, returns an iterator of the values in the set-like. + */ + keys(): Iterator; + /** + * @returns a boolean indicating whether an element with the specified value exists in the set-like or not. + */ + has(value: T): boolean; + /** + * @returns the number of (unique) elements in the set-like. + */ + readonly size: number; +} + +/** + * @returns a boolean indicating whether all the elements in Set `one` are also in the `other`. + */ +export function isSubsetOf( + one: Set, + other: ReadonlySetLike, +): boolean { + // If the builtin method exists, just call it + if ("isSubsetOf" in Set.prototype) { + return one.isSubsetOf(other); + } + // If not, provide the implementation + if (one.size > other.size) { + return false; + } + for (const element of one) { + if (!other.has(element)) { + return false; + } + } + return true; +}