diff --git a/cspell.json b/cspell.json index 4acf371999..ce76e68d9a 100644 --- a/cspell.json +++ b/cspell.json @@ -53,6 +53,7 @@ "src/test/exit-codes/contracts/compute-phase-errors.tact", "src/test/e2e-slow/map-property-tests/map-properties-key-value-types.ts", "src/test/e2e-slow/map-property-tests/build", + "src/test/fuzzer/src/minimal-fc-stdlib/stdlib.fc", "/docs", "src/benchmarks/contracts/func/notcoin/stdlib-custom.fc", "src/benchmarks/contracts/func/notcoin/gas.fc", diff --git a/package.json b/package.json index 33ee33ee41..5cf49313f3 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "gen:contracts:test:map": "ts-node ./src/test/e2e-slow/map-property-tests/generate.ts", "gen:contracts:all": "yarn gen:contracts:fast && yarn gen:contracts:test:map", "gen": "yarn gen:grammar && yarn gen:stdlib && yarn gen:func-js && yarn gen:contracts:all", + "fuzz:expressions:stats": "ts-node src/test/fuzzer/test/expression-stats.ts", "clean": "rm -fr dist", "cleanall": "rm -fr dist node_modules", "copy:stdlib": "ts-node src/stdlib/copy.build.ts", diff --git a/spell/cspell-list.txt b/spell/cspell-list.txt index efb4f61f75..85fad4c8a4 100644 --- a/spell/cspell-list.txt +++ b/spell/cspell-list.txt @@ -119,6 +119,7 @@ mintable misparse misparsed mktemp +mult multiformats nanoton nanotons @@ -143,6 +144,7 @@ pinst POSIX postpack prando +preorder quadtree quadtrees RANDU diff --git a/src/func/funcCompile.ts b/src/func/funcCompile.ts index 56801437bd..bd357e25d5 100644 --- a/src/func/funcCompile.ts +++ b/src/func/funcCompile.ts @@ -1,4 +1,5 @@ import type { ILogger } from "@/context/logger"; +import { execSync } from "child_process"; // Wasm Imports // eslint-disable-next-line @typescript-eslint/no-require-imports @@ -60,6 +61,71 @@ export async function funcCompile(args: { entries: string[]; sources: { path: string; content: string }[]; logger: ILogger; +}): Promise<FuncCompilationResult> { + const USE_NATIVE = process.env.USE_NATIVE === "true"; + if (USE_NATIVE) { + return funcCompileNative(args); + } + return funcCompileWasm(args); +} + +export function funcCompileNative(args: { + entries: string[]; + sources: { path: string; content: string }[]; + logger: ILogger; +}): FuncCompilationResult { + const FC_STDLIB_PATH = process.env.FC_STDLIB_PATH; + if (typeof FC_STDLIB_PATH === "undefined") { + throw new Error("FC_STDLIB_PATH is not set"); + } + const FIFT_LIBS_PATH = process.env.FIFT_LIBS_PATH; + if (typeof FIFT_LIBS_PATH === "undefined") { + throw new Error("FIFT_LIBS_PATH is not set"); + } + const FUNC_FIFT_COMPILER_PATH = process.env.FUNC_FIFT_COMPILER_PATH; + if (typeof FUNC_FIFT_COMPILER_PATH === "undefined") { + throw new Error("FUNC_FIFT_COMPILER_PATH is not set"); + } + + const files: string[] = args.entries; + const configStr = JSON.stringify({ + sources: files.map((f) => f.replace("@stdlib/", FC_STDLIB_PATH)), + optLevel: 2, + fiftPath: FIFT_LIBS_PATH, + }); + + const retJson = execSync( + [FUNC_FIFT_COMPILER_PATH, `'${configStr}'`].join(" "), + ); + + const result = JSON.parse(retJson.toString()) as CompileResult; + + switch (result.status) { + case "error": { + return { + ok: false, + log: "", + fift: null, + output: null, + }; + } + case "ok": { + return { + ok: true, + log: "", + fift: cutFirstLine(result.fiftCode.replaceAll("\\n", "\n")), + output: Buffer.from(result.codeBoc, "base64"), + }; + } + } + + throw Error("Unexpected compiler response"); +} + +export async function funcCompileWasm(args: { + entries: string[]; + sources: { path: string; content: string }[]; + logger: ILogger; }): Promise<FuncCompilationResult> { // Parameters const files: string[] = args.entries; diff --git a/src/test/fuzzer/src/config.ts b/src/test/fuzzer/src/config.ts new file mode 100644 index 0000000000..a62e5c6f77 --- /dev/null +++ b/src/test/fuzzer/src/config.ts @@ -0,0 +1,346 @@ +import * as os from "os"; +import { existsSync, mkdirSync } from "fs"; +import { + NonTerminal, + Terminal, +} from "@/test/fuzzer/src/generators/uniform-expr-gen"; +import type { + NonTerminalEnum, + TerminalEnum, +} from "@/test/fuzzer/src/generators/uniform-expr-gen"; + +/** + * The default number of executions per test. Corresponds to fast-check defaults. + */ +const DEFAULT_NUM_RUNS: number = 100; + +/** + * Configuration handler for fuzz testing settings. + */ +export class FuzzConfig { + /** + * The number of samples to dump during fuzz testing. + * If `samplesNum` is not set, the fuzzer won't dump samples. + */ + public samplesNum: number | undefined; + + /** + * A format used to dump samples. + */ + public samplesFormat: "ast" | "json" = "ast"; + + /** + * Explicitly specified fast-check seed. + */ + public seed: number | undefined; + + /** + * Number of AST generation cycles. POSITIVE_INFINITY means running in the continuous fuzzing mode. + */ + public numRuns: number; + + /** + * Directory to save contracts compiled during the compilation test. + */ + public compileDir: string; + + /** + * Maximum AST generation depth. + */ + public maxDepth: number = 5; + + /** + * Default generation parameters. Used by entity constructors. + */ + + /** + * ------------------------------------- + * Parameters for contract generation + * ------------------------------------- + */ + + /** + * Minimum number of receivers generated within a contract. + * @default 1 + */ + public static receiveMinNum: number = 1; + + /** + * Maximum number of receivers generated within a contract. + * @default 5 + */ + public static receiveMaxNum: number = 5; + + /** + * Minimum number of constants generated within a contract. + * @default 1 + */ + public static contractConstantMinNum: number = 1; + + /** + * Maximum number of constants generated within a contract. + * @default 5 + */ + public static contractConstantMaxNum: number = 5; + + /** + * Minimum number of fields generated within a contract. + * @default 1 + */ + public static contractFieldMinNum: number = 1; + + /** + * Maximum number of fields generated within a contract. + * @default 5 + */ + public static contractFieldMaxNum: number = 5; + + /** + * ------------------------------------- + * Parameters for trait generation + * ------------------------------------- + * TODO: make this parameters into min and max + */ + + /** + * Number of fields generated within a trait. + * @default 1 + */ + public static traitFieldNum: number = 1; + + /** + * Number of method declarations generated within a trait. + * @default 1 + */ + public static traitMethodDeclarationsNum: number = 1; + + /** + * Number of constant declarations generated within a trait. + * @default 1 + */ + public static traitConstantNum: number = 1; + + /** + * ------------------------------------- + * Parameters for expression generation + * ------------------------------------- + */ + + /** + * Indicates whether generated expressions could use identifiers declared in the scope. + * @default true + */ + public static useIdentifiersInExpressions: boolean = true; + + /** + * The minimum expression size. + * @default 1 + */ + public static minExpressionSize: number = 1; + + /** + * The maximum expression size. + * @default 5 + */ + public static maxExpressionSize: number = 5; + + /** + * Non-terminals that the expression generator is allowed to use. + * @default Object.values(NonTerminal) + */ + public static allowedNonTerminalsInExpressions: NonTerminalEnum[] = + Object.values(NonTerminal); + + /** + * Terminals that the the expression generator is allowed to use. + * @default Object.values(Terminal); + */ + public static allowedTerminalsInExpressions: TerminalEnum[] = + Object.values(Terminal); + + /** + * ------------------------------------- + * Parameters for function generation + * ------------------------------------- + */ + + /** + * Minimum number of let statements at the start of function body. + * @default 1 + */ + public static letStatementsMinNum: number = 1; + + /** + * Maximum number of let statements at the start of function body. + * @default 5 + */ + public static letStatementsMaxNum: number = 5; + + /** + * Minimum number of sequential statements in the function body (not counting initial let statements and final return) + * @default 1 + */ + public static statementsMinLength: number = 1; + + /** + * Maximum number of sequential statements in the function body (not counting initial let statements and final return) + * @default 5 + */ + public static statementsMaxLength: number = 5; + + /** + * ------------------------------------- + * Parameters for module generation + * ------------------------------------- + */ + + /** + * Add definitions that mock stdlib ones to the generated program. + * @default false + */ + public static addStdlib: boolean = false; + + /** + * Minimum number of structures. + * @default 1 + */ + public static structsMinNum: number = 1; + + /** + * Maximum number of structures. + * @default 4 + */ + public static structsMaxNum: number = 4; + + /** + * Minimum number of messages. + * @default 1 + */ + public static messagesMinNum: number = 1; + + /** + * Maximum number of messages. + * @default 4 + */ + public static messagesMaxNum: number = 4; + + /** + * Minimum number of the generated traits. Some of them might be used by the generated contracts. + * @default 1 + */ + public static traitsMinNum: number = 1; + + /** + * Maximum number of the generated traits. Some of them might be used by the generated contracts. + * @default 4 + */ + public static traitsMaxNum: number = 4; + + /** + * Minimum number of generated contracts + * @default 1 + */ + public static contractsMinNum: number = 1; + + /** + * Maximum number of generated contracts + * @default 4 + */ + public static contractsMaxNum: number = 4; + + /** + * Minimum number of generated global functions + * @default 1 + */ + public static functionsMinNum: number = 1; + + /** + * Maximum number of generated global functions + * @default 4 + */ + public static functionsMaxNum: number = 4; + + /** + * Minimum number of global function arguments + * @default 0 + */ + public static functionArgsMinNum: number = 0; + + /** + * Maximum number of global function arguments + * @default 6 + */ + public static functionArgsMaxNum: number = 6; + + /** + * Minimum number of generated global constants + * @default 1 + */ + public static constantsMinNum: number = 1; + + /** + * Maximum number of generated global constants + * @default 4 + */ + public static constantsMaxNum: number = 4; + + /** + * ------------------------------------- + * Parameters for general statement generation + * ------------------------------------- + * TODO: Make these arguments into min and max parameters + */ + + /** + * Determines the maximum depth of nested statement blocks. + * @default 2 + */ + public static nestedBlocksNum: number = 2; + + /** + * Number of statements in each block. + * @default 3 + */ + public static stmtsInBlock: number = 3; + + constructor() { + this.samplesNum = process.env.SAMPLES_NUM + ? parseInt(process.env.SAMPLES_NUM) + : undefined; + if (process.env.SAMPLES_FORMAT) { + this.validateAndSetFormat(process.env.SAMPLES_FORMAT); + } + this.compileDir = process.env.COMPILE_DIR ?? os.tmpdir(); + if (process.env.COMPILE_DIR && !existsSync(process.env.COMPILE_DIR)) { + mkdirSync(process.env.COMPILE_DIR, { recursive: true }); + } + this.seed = process.env.SEED ? parseInt(process.env.SEED) : undefined; + this.numRuns = + process.env.FUZZ === "1" + ? Number.POSITIVE_INFINITY + : process.env.NUM_RUNS + ? parseInt(process.env.NUM_RUNS) + : DEFAULT_NUM_RUNS; + if (this.samplesNum && this.numRuns < this.samplesNum) { + console.warn( + `the requested number of SAMPLES_NUM=${this.samplesNum} is less than NUM_RUNS=${this.numRuns}`, + ); + } + } + + /** + * Validates and sets the sample format based on the provided format string. + * Throws an error if the format is not supported. + * @param fmt The format string to validate and set. + */ + private validateAndSetFormat(fmt: string): void { + const supportedFormats: ("ast" | "json")[] = ["ast", "json"]; + if (supportedFormats.includes(fmt as "ast" | "json")) { + this.samplesFormat = fmt as "ast" | "json"; + } else { + throw new Error( + `unsupported SAMPLES_FMT: ${fmt} (supported options: "ast" and "json")`, + ); + } + } +} diff --git a/src/test/fuzzer/src/context.ts b/src/test/fuzzer/src/context.ts new file mode 100644 index 0000000000..622e46b77b --- /dev/null +++ b/src/test/fuzzer/src/context.ts @@ -0,0 +1,102 @@ +import { prettyPrint } from "@/ast/ast-printer"; +import { FuzzConfig } from "@/test/fuzzer/src/config"; +import type { AstNode } from "@/ast/ast"; +import { stringify } from "@/test/fuzzer/src/util"; +import { getAstFactory } from "@/ast/ast-helpers"; +import type { FactoryAst } from "@/ast/ast-helpers"; +import { getMakeAst } from "@/ast/generated/make-factory"; +import type { MakeAstFactory } from "@/ast/generated/make-factory"; + +/** + * FuzzContext represents a stateful context that handles AST generation. + * It keeps the global options used to control and configure the AST generation. + */ +export class FuzzContext { + /** + * Tracks the number of samples that have been printed. + */ + private printedSamplesCount: number = 0; + + /** + * Configuration of the fuzzer. + */ + public config: FuzzConfig; + + /** + * Current depth of AST expression generation, which limits recursive generation. + */ + private currentDepth: number = 0; + + /** + * The generic AST Factory + */ + public astF: FactoryAst = getAstFactory(); + + /** + * The factory with the makeX methods + */ + public makeF: MakeAstFactory = getMakeAst(this.astF); + + constructor() { + this.config = new FuzzConfig(); + } + + public getDepth(): number { + return this.currentDepth; + } + + public incDepth(): void { + this.currentDepth++; + } + + public decDepth(): void { + this.currentDepth--; + if (this.currentDepth < 0) { + throw new Error("Reached negative recursion depth"); + } + } + + public resetDepth(): void { + this.currentDepth = 0; + } + + /** + * Formats the given AST construction according to the current formatter configuration. + */ + public format(value: AstNode, fmt = this.config.samplesFormat): string { + switch (fmt) { + case "json": + return stringify(value, 2); + case "ast": + return prettyPrint(value); + default: + throw new Error( + `Unsupported sample format: ${this.config.samplesFormat}`, + ); + } + } + + /** + * Prints the given sample if the number of already printed samples is less than the configured limit. + * @param sample The sample to print. + */ + public printSample(sample: AstNode): void { + if ( + this.config.samplesNum === undefined || + this.printedSamplesCount >= this.config.samplesNum + ) { + return; + } + console.log( + `Sample #${this.printedSamplesCount}:\n${this.format(sample)}`, + ); + this.printedSamplesCount++; + } +} + +/** + * A global context accessible and mutable throughout the generation process to + * reflect changes across the AST. + */ +// eslint-disable-next-line prefer-const +export let GlobalContext: FuzzContext = new FuzzContext(); diff --git a/src/test/fuzzer/src/generators/constant.ts b/src/test/fuzzer/src/generators/constant.ts new file mode 100644 index 0000000000..850afdcdd0 --- /dev/null +++ b/src/test/fuzzer/src/generators/constant.ts @@ -0,0 +1,122 @@ +import type * as Ast from "@/ast/ast"; +import { + generateAstIdFromName, + createSample, + generateAstId, + generateName, + dummySrcInfoPrintable, +} from "@/test/fuzzer/src/util"; +import { tyToAstType } from "@/test/fuzzer/src/types"; +import type { Type } from "@/test/fuzzer/src/types"; +import type { Scope } from "@/test/fuzzer/src/scope"; +import { NamedGenerativeEntity } from "@/test/fuzzer/src/generators/generator"; + +import fc from "fast-check"; +import { GlobalContext } from "@/test/fuzzer/src/context"; + +/** + * An object that encapsulates a generated Ast.ConstantDecl. + */ +export class ConstantDecl extends NamedGenerativeEntity<Ast.ConstantDecl> { + constructor(scope: Scope, type: Type) { + super(type, createSample(generateAstId(scope))); + } + + private getAttributes( + extraAttrs: Ast.ConstantAttribute[], + ): Ast.ConstantAttribute[] { + const attrs: Ast.ConstantAttribute[] = extraAttrs; + attrs.push({ type: "abstract", loc: dummySrcInfoPrintable }); + return attrs; + } + + private generateImpl( + extraAttrs: Ast.ConstantAttribute[], + ): fc.Arbitrary<Ast.ConstantDecl> { + return fc.constant( + GlobalContext.makeF.makeDummyConstantDecl( + this.getAttributes(extraAttrs), + this.name, + tyToAstType(this.type), + ), + ); + } + + /** + * Generates a constant declaration without extra attributes. + */ + public generate(): fc.Arbitrary<Ast.ConstantDecl> { + return this.generateImpl([]); + } + + /** + * Create definition for this constant destination. + * @param init An initializer evaluable in compile-time. // cspell:disable-line + */ + public createDefinition(init: fc.Arbitrary<Ast.Expression>): ConstantDef { + return new ConstantDef(this.name.text, this.type, init); + } +} + +/** + * An object that encapsulates a generated Ast.ConstantDef. + * @parentScope Scope this constant belongs to. + */ +export class ConstantDef extends NamedGenerativeEntity<Ast.ConstantDef> { + /** + * Create new constant definition from its name and type. Used to create definition from an existing declaration. + * @param init An initializer evaluable in compile-time. // cspell:disable-line + */ + constructor( + name: string, + type: Type, + private init: fc.Arbitrary<Ast.Expression>, + ) { + super(type, generateAstIdFromName(name)); + } + /** + * Create a new constant definition generation name from scope. + * @param scope Scope to generate constant name from. + * @param type Constant type. + * @param init An initializer evaluable in compile-time. // cspell:disable-line + */ + public static fromScope( + scope: Scope, + type: Type, + init: fc.Arbitrary<Ast.Expression>, + ): ConstantDef { + return new ConstantDef(createSample(generateName(scope)), type, init); + } + + private generateImpl( + extraAttrs: Ast.ConstantAttribute[], + init?: fc.Arbitrary<Ast.Expression>, + ): fc.Arbitrary<Ast.ConstantDef> { + const chosenInit = init ?? this.init; + return chosenInit.map((init) => + GlobalContext.makeF.makeDummyConstantDef( + extraAttrs, + this.name, + tyToAstType(this.type), + init, + ), + ); + } + + /** + * Generates a constant definition without extra attributes. + */ + public generate(): fc.Arbitrary<Ast.ConstantDef> { + return this.generateImpl([]); + } + + /** + * Generates a constant definition with extra attributes and overridden init. + */ + public generateWithAttrs( + extraAttrs: Ast.ConstantAttribute[] = [], + init?: fc.Arbitrary<Ast.Expression>, + ): fc.Arbitrary<Ast.ConstantDef> { + return this.generateImpl(extraAttrs, init); + } +} diff --git a/src/test/fuzzer/src/generators/contract.ts b/src/test/fuzzer/src/generators/contract.ts new file mode 100644 index 0000000000..470cd70f49 --- /dev/null +++ b/src/test/fuzzer/src/generators/contract.ts @@ -0,0 +1,240 @@ +import type * as Ast from "@/ast/ast"; +import { + createSample, + dummySrcInfoPrintable, + generateAstId, + packArbitraries, +} from "@/test/fuzzer/src/util"; +import { FunctionDef } from "@/test/fuzzer/src/generators/function"; +import type { Trait } from "@/test/fuzzer/src/generators/trait"; +import { Expression } from "@/test/fuzzer/src/generators/expression"; +import { Receive } from "@/test/fuzzer/src/generators/receiver"; +import { SUPPORTED_STDLIB_TYPES, UtilType } from "@/test/fuzzer/src/types"; +import type { FunctionType } from "@/test/fuzzer/src/types"; +import { Scope } from "@/test/fuzzer/src/scope"; +import { NamedGenerativeEntity } from "@/test/fuzzer/src/generators/generator"; + +import fc from "fast-check"; +import { Field } from "@/test/fuzzer/src/generators/field"; +import { ConstantDef } from "@/test/fuzzer/src/generators/constant"; +import { FuzzConfig } from "@/test/fuzzer/src/config"; +import { GlobalContext } from "@/test/fuzzer/src/context"; + +export interface ContractParameters { + /** + * Minimum number of receivers generated within a contract. + * @default FuzzConfig.receiveMinNum + */ + receiveMinNum: number; + + /** + * Maximum number of receivers generated within a contract. + * @default FuzzConfig.receiveMaxNum + */ + receiveMaxNum: number; + + /** + * Minimum number of constants generated within a contract. + * @default FuzzConfig.contractConstantMinNum + */ + contractConstantMinNum: number; + + /** + * Maximum number of constants generated within a contract. + * @default FuzzConfig.contractConstantMaxNum + */ + contractConstantMaxNum: number; + + /** + * Minimum number of fields generated within a contract. + * @default FuzzConfig.contractFieldMinNum + */ + contractFieldMinNum: number; + + /** + * Maximum number of fields generated within a contract. + * @default FuzzConfig.contractFieldMaxNum + */ + contractFieldMaxNum: number; +} + +/** + * An object that encapsulates a randomly generated Ast.Contract including extra information + * about its entries and their scopes. + */ +export class Contract extends NamedGenerativeEntity<Ast.Contract> { + /** Scope used within the generated contract. */ + private scope: Scope; + + private receiveMinNum: number; + private receiveMaxNum: number; + private contractConstantMinNum: number; + private contractConstantMaxNum: number; + private contractFieldMinNum: number; + private contractFieldMaxNum: number; + + /** + * @param methodSignatures Signatures of methods to be generated in the contract. + * @param trait An optional trait that the generated contract must implement. + */ + constructor( + parentScope: Scope, + private methodSignatures: FunctionType[], + private trait?: Trait, + params: Partial<ContractParameters> = {}, + ) { + const scope = new Scope("contract", parentScope); + super( + { kind: "util", type: UtilType.Contract }, + createSample(generateAstId(scope)), + ); + this.scope = scope; + + const { + receiveMinNum = FuzzConfig.receiveMinNum, + receiveMaxNum = FuzzConfig.receiveMaxNum, + contractConstantMinNum = FuzzConfig.contractConstantMinNum, + contractConstantMaxNum = FuzzConfig.contractConstantMaxNum, + contractFieldMinNum = FuzzConfig.contractFieldMinNum, + contractFieldMaxNum = FuzzConfig.contractFieldMaxNum, + } = params; + this.receiveMinNum = receiveMinNum; + this.receiveMaxNum = receiveMaxNum; + this.contractConstantMinNum = contractConstantMinNum; + this.contractConstantMaxNum = contractConstantMaxNum; + this.contractFieldMinNum = contractFieldMinNum; + this.contractFieldMaxNum = contractFieldMaxNum; + } + + public generate(): fc.Arbitrary<Ast.Contract> { + // Implemented declarations from the trait + let traitFields: fc.Arbitrary<Ast.FieldDecl>[] = []; + let traitConstants: fc.Arbitrary<Ast.ConstantDef>[] = []; + let traitMethods: fc.Arbitrary<Ast.FunctionDef>[] = []; + if (this.trait !== undefined) { + traitFields = this.trait.fieldDeclarations.map(({ type, name }) => { + const init = new Expression(this.scope, type, { + useIdentifiersInExpressions: false, + }).generate(); + return new Field(this.scope, type, init, name).generate(); + }); + traitConstants = this.trait.constantDeclarations + .map((decl) => { + const init = new Expression(this.scope, decl.type, { + useIdentifiersInExpressions: false, + }).generate(); + return decl + .createDefinition(init) + .generateWithAttrs([ + { type: "override", loc: dummySrcInfoPrintable }, + ]); + }) + .concat( + this.trait.constantDefinitions.map((def) => + def.generateWithAttrs([ + { type: "override", loc: dummySrcInfoPrintable }, + ]), + ), + ); + traitMethods = this.trait.methodDeclarations.map((m) => { + return m.generateDefinition("method", [ + { + kind: "function_attribute", + type: "override", + loc: dummySrcInfoPrintable, + }, + { + kind: "function_attribute", + type: "get", + loc: dummySrcInfoPrintable, + methodId: undefined, + }, + ]); + }); + } + + const requestedMethods = this.methodSignatures.map((signature) => + new FunctionDef(this.scope, "method", signature).generate(), + ); + //const generatedMethods = Array.from( + // this.scope.getAllNamed("methodDef"), + //).map((m) => m.generate()); + + const requestedReceives = fc.array(new Receive(this.scope).generate(), { + minLength: this.receiveMinNum, + maxLength: this.receiveMaxNum, + }); + + // TODO: Augment the SUPPORTED_STDLIB_TYPES + const genConstantDefEntities = fc + .constantFrom(...SUPPORTED_STDLIB_TYPES) + .map((ty) => + ConstantDef.fromScope( + this.scope, + { kind: "stdlib", type: ty }, + new Expression( + this.scope, + { kind: "stdlib", type: ty }, + { useIdentifiersInExpressions: false }, + ).generate(), + ), + ) + .chain((d) => d.generate()); + + const generatedConstants = fc.array(genConstantDefEntities, { + minLength: this.contractConstantMinNum, + maxLength: this.contractConstantMaxNum, + }); + + //const generatedConstants = Array.from( + // this.scope.getAllNamed("constantDef"), + //).map((c) => c.generate()); + + //const generatedFields = Array.from(this.scope.getAllNamed("field")).map( + // (f) => f.generate(), + //); + + // TODO: Augment the SUPPORTED_STDLIB_TYPES + const genFieldEntities = fc + .constantFrom(...SUPPORTED_STDLIB_TYPES) + .map( + (ty) => + new Field( + this.scope, + { kind: "stdlib", type: ty }, + new Expression( + this.scope, + { kind: "stdlib", type: ty }, + { useIdentifiersInExpressions: false }, + ).generate(), + ), + ) + .chain((f) => f.generate()); + + const generatedFields = fc.array(genFieldEntities, { + minLength: this.contractFieldMinNum, + maxLength: this.contractFieldMaxNum, + }); + + return fc + .tuple( + packArbitraries(traitConstants), + generatedConstants, + packArbitraries(traitFields), + generatedFields, + requestedReceives, + packArbitraries(traitMethods), + //...generatedMethods, + packArbitraries(requestedMethods), + ) + .map((decls) => + GlobalContext.makeF.makeDummyContract( + this.name, + this.trait === undefined ? [] : [this.trait.name], + [], + undefined, + decls.flat(), + ), + ); + } +} diff --git a/src/test/fuzzer/src/generators/expression.ts b/src/test/fuzzer/src/generators/expression.ts new file mode 100644 index 0000000000..af015d124b --- /dev/null +++ b/src/test/fuzzer/src/generators/expression.ts @@ -0,0 +1,265 @@ +import type * as Ast from "@/ast/ast"; +import type fc from "fast-check"; +import { + generateAstIdFromName, + packArbitraries, + stringify, +} from "@/test/fuzzer/src/util"; +import { + GenerativeEntity, + NamedGenerativeEntity, +} from "@/test/fuzzer/src/generators/generator"; +import { StdlibType } from "@/test/fuzzer/src/types"; +import type { Type } from "@/test/fuzzer/src/types"; +import type { Scope } from "@/test/fuzzer/src/scope"; +import { + initializeGenerator, + NonTerminal, +} from "@/test/fuzzer/src/generators/uniform-expr-gen"; +import type { + GenInitConfig, + NonTerminalEnum, + TerminalEnum, +} from "@/test/fuzzer/src/generators/uniform-expr-gen"; +import { GlobalContext } from "@/test/fuzzer/src/context"; +import { FuzzConfig } from "@/test/fuzzer/src/config"; + +export type ExpressionParameters = { + /** + * Indicates whether the generated expression could use identifiers declared in the scope. + * @default FuzzConfig.useIdentifiersInExpressions + */ + useIdentifiersInExpressions: boolean; + + /** + * The minimum expression size. + * @default FuzzConfig.minExpressionSize + */ + minExpressionSize: number; + + /** + * The maximum expression size. + * @default FuzzConfig.maxExpressionSize + */ + maxExpressionSize: number; + + /** + * Lists the non-terminals that the generator is allowed to use. + * @default FuzzConfig.allowedNonTerminals + */ + allowedNonTerminals: NonTerminalEnum[]; + + /** + * Lists the terminals that the generator is allowed to use. + * @default FuzzConfig.allowedTerminals + */ + allowedTerminals: TerminalEnum[]; +}; + +export function makeSelfID(): Ast.Id { + return GlobalContext.makeF.makeDummyId("self"); +} + +/** + * Generates expressions used in actual function call arguments. + * @param funTy Signature of the function. + * @param funScope Scope of the function. + */ +export function generateFunctionCallArgs( + funTy: Type, + funScope: Scope, +): fc.Arbitrary<Ast.Expression>[] { + if (funTy.kind !== "function") { + throw new Error(`Incorrect type for function: ${stringify(funTy, 0)}`); + } + if (funTy.signature.length === 1) { + return []; + } + return funTy.signature + .slice(0, -1) + .map((argTy) => new Expression(funScope, argTy).generate()); +} + +/** + * Generates expressions used in actual method call arguments. + * @param methodTy Signature of the method. + * @param methodScope Scope of the method. + */ +export function generateMethodCallArgs( + methodTy: Type, + methodScope: Scope, +): fc.Arbitrary<Ast.Expression>[] { + if (methodTy.kind !== "function") { + throw new Error(`Incorrect type for method: ${stringify(methodTy, 0)}`); + } + if (methodTy.signature.length === 2) { + return []; + } + return methodTy.signature + .slice(1, -1) + .map((argTy) => new Expression(methodScope, argTy).generate()); +} + +/** + * Generates method calls. + */ +export class MethodCall extends NamedGenerativeEntity<Ast.MethodCall> { + constructor( + type: Type, + name: string, + private src: Ast.Expression, + private args?: fc.Arbitrary<Ast.Expression>[], + ) { + super(type, generateAstIdFromName(name)); + } + generate(): fc.Arbitrary<Ast.MethodCall> { + return packArbitraries(this.args).map((args) => + GlobalContext.makeF.makeDummyMethodCall(this.src, this.name, args), + ); + } +} + +/** + * Generates free function calls. + */ +export class StaticCall extends NamedGenerativeEntity<Ast.StaticCall> { + constructor( + type: Type, + name: string, + private args?: fc.Arbitrary<Ast.Expression>[], + ) { + super(type, generateAstIdFromName(name)); + } + generate(): fc.Arbitrary<Ast.StaticCall> { + return packArbitraries(this.args).map((args) => + GlobalContext.makeF.makeDummyStaticCall(this.name, args), + ); + } +} + +/** + * Contains the logic to generate expressions based on their types. + */ +export class Expression extends GenerativeEntity<Ast.Expression> { + private static initializedGens: Map< + string, + (scope: Scope, type: NonTerminalEnum) => fc.Arbitrary<Ast.Expression> + > = new Map(); + private parentScope: Scope; + private exprGen: ( + scope: Scope, + type: NonTerminalEnum, + ) => fc.Arbitrary<Ast.Expression>; + + /** + * @param parentScope Scope to extract declarations from. + * @param type Type of the generated expression. + * @param params Optional parameters for expression generation. + */ + constructor( + parentScope: Scope, + type: Type, + params: Partial<ExpressionParameters> = {}, + ) { + super(type); + this.parentScope = parentScope; + + const { + useIdentifiersInExpressions = FuzzConfig.useIdentifiersInExpressions, + minExpressionSize = FuzzConfig.minExpressionSize, + maxExpressionSize = FuzzConfig.maxExpressionSize, + allowedNonTerminals = FuzzConfig.allowedNonTerminalsInExpressions, + allowedTerminals = FuzzConfig.allowedTerminalsInExpressions, + } = params; + const config: GenInitConfig = { + minSize: minExpressionSize, + maxSize: maxExpressionSize, + allowedNonTerminals, + allowedTerminals, + useIdentifiers: useIdentifiersInExpressions, + }; + const configKey = JSON.stringify(config); + const initGen = Expression.initializedGens.get(configKey); + if (typeof initGen === "undefined") { + this.exprGen = initializeGenerator(config); + Expression.initializedGens.set(configKey, this.exprGen); + } else { + this.exprGen = initGen; + } + } + + private getNonTerminalForType( + type: StdlibType, + optional: boolean, + ): NonTerminalEnum { + switch (type) { + case StdlibType.Int: { + return optional ? NonTerminal.OptInt : NonTerminal.Int; + } + case StdlibType.Bool: { + return optional ? NonTerminal.OptBool : NonTerminal.Bool; + } + case StdlibType.Cell: { + return optional ? NonTerminal.OptCell : NonTerminal.Cell; + } + case StdlibType.Address: { + return optional ? NonTerminal.OptAddress : NonTerminal.Address; + } + case StdlibType.Slice: { + return optional ? NonTerminal.OptSlice : NonTerminal.Slice; + } + case StdlibType.String: { + return optional ? NonTerminal.OptString : NonTerminal.String; + } + case StdlibType.Builder: + case StdlibType.StringBuilder: + throw new Error( + `Generation of expressions of type ${stringify(type, 0)} is currently not supported.`, + ); + } + } + + /** + * Generates an AST expression of the specified type. + */ + generate(): fc.Arbitrary<Ast.Expression> { + switch (this.type.kind) { + case "stdlib": { + const nonTerminal = this.getNonTerminalForType( + this.type.type, + false, + ); + return this.exprGen(this.parentScope, nonTerminal); + } + case "optional": { + switch (this.type.type.kind) { + case "stdlib": { + const nonTerminal = this.getNonTerminalForType( + this.type.type.type, + true, + ); + return this.exprGen(this.parentScope, nonTerminal); + } + case "optional": + case "map": + case "struct": + case "message": + case "util": + case "function": + throw new Error( + `Generation of expressions of type ${stringify(this.type.type, 0)} is currently not supported.`, + ); + } + break; + } + case "map": + case "struct": + case "message": + case "util": + case "function": + throw new Error( + `Generation of expressions of type ${stringify(this.type, 0)} is currently not supported.`, + ); + } + } +} diff --git a/src/test/fuzzer/src/generators/field.ts b/src/test/fuzzer/src/generators/field.ts new file mode 100644 index 0000000000..d90c5613b8 --- /dev/null +++ b/src/test/fuzzer/src/generators/field.ts @@ -0,0 +1,50 @@ +import type * as Ast from "@/ast/ast"; +import { createSample, generateAstId } from "@/test/fuzzer/src/util"; +import { tyToAstType } from "@/test/fuzzer/src/types"; +import type { Type } from "@/test/fuzzer/src/types"; +import type { Scope } from "@/test/fuzzer/src/scope"; +import { NamedGenerativeEntity } from "@/test/fuzzer/src/generators/generator"; + +import fc from "fast-check"; +import { GlobalContext } from "@/test/fuzzer/src/context"; + +/** + * An object that encapsulates a generated Ast.FieldDecl. + */ +export class Field extends NamedGenerativeEntity<Ast.FieldDecl> { + /** + * @param init An optional initializer evaluable in compile-time. // cspell:disable-line + * @param parentScope Scope this field belongs to. Could be a contract or program for struct fields. + */ + constructor( + parentScope: Scope, + type: Type, + private init?: fc.Arbitrary<Ast.Expression>, + name?: Ast.Id, + ) { + if ( + !parentScope.definedIn( + "contract", + "method", + "program" /* struct field */, + "trait", + ) + ) { + throw new Error( + `Cannot define a field in a ${parentScope.kind} scope`, + ); + } + super(type, name ?? createSample(generateAstId(parentScope))); + } + + generate(): fc.Arbitrary<Ast.FieldDecl> { + return (this.init ?? fc.constant(undefined)).map((i) => + GlobalContext.makeF.makeDummyFieldDecl( + this.name, + tyToAstType(this.type), + i, + undefined, + ), + ); + } +} diff --git a/src/test/fuzzer/src/generators/function.ts b/src/test/fuzzer/src/generators/function.ts new file mode 100644 index 0000000000..4508a8b1d9 --- /dev/null +++ b/src/test/fuzzer/src/generators/function.ts @@ -0,0 +1,311 @@ +import type * as Ast from "@/ast/ast"; +import { + tyToAstType, + StdlibType, + isThis, + getReturnType, + isUnit, + UtilType, + SUPPORTED_STDLIB_TYPES, +} from "@/test/fuzzer/src/types"; +import type { FunctionType, Type } from "@/test/fuzzer/src/types"; +import { Let, Return, Statement } from "@/test/fuzzer/src/generators/statement"; +import { Parameter } from "@/test/fuzzer/src/generators/parameter"; +import { Scope } from "@/test/fuzzer/src/scope"; +import { + createSample, + dummySrcInfoPrintable, + generateAstId, + generateAstIdFromName, +} from "@/test/fuzzer/src/util"; +import { NamedGenerativeEntity } from "@/test/fuzzer/src/generators/generator"; + +import fc from "fast-check"; +import { GlobalContext } from "@/test/fuzzer/src/context"; +import { Expression } from "@/test/fuzzer/src/generators/expression"; +import { FuzzConfig } from "@/test/fuzzer/src/config"; + +/** + * Utility type, used inside function definition and declaration classes and in shared functions. + */ +type FunctionKind = "function" | "method"; + +export const SUPPORTED_RETURN_TYS = [ + StdlibType.Int, + StdlibType.Bool, + StdlibType.String, +]; + +function notHaveArguments(kind: FunctionKind, type: FunctionType): boolean { + if (kind === "function") { + return type.signature.length === 1; + } else { + const firstArg = type.signature[0]; + if (typeof firstArg === "undefined") { + throw new Error(`unexpected 'undefined'`); + } + return isThis(firstArg) && type.signature.length === 2; + } +} + +/** + * Creates parameters entries saving them in the scope of the function or method as variables. + */ +function generateParameters( + kind: FunctionKind, + type: FunctionType, + scope: Scope, +): Ast.TypedParameter[] { + if (notHaveArguments(kind, type)) { + return []; + } + const slice = + kind === "method" + ? type.signature.slice(1, -1) + : type.signature.slice(0, -1); + return slice.map((argType) => { + const param = new Parameter(scope, argType); + scope.addNamed("parameter", param); + return createSample(param.generate()); + }); +} + +/** + * Prepares the final list of attributes based on function kind and the current implementation details. + */ +function getAttributes( + extraAttrs: Ast.FunctionAttribute[], + kind: FunctionKind, + onlyDeclaration: boolean, +): Ast.FunctionAttribute[] { + const attrs: Ast.FunctionAttribute[] = extraAttrs; + + // We are marking all the methods with the `get` attribute to ensure they + // will be compiled to func and tested by compilation tests. + // TODO: However, we cannot use `get` for abstract and overridden methods: + // https://github.com/tact-lang/tact/issues/490 + if (kind === "method" && !extraAttrs.find((a) => a.type === "override")) { + attrs.push({ + kind: "function_attribute", + type: "get", + loc: dummySrcInfoPrintable, + methodId: undefined, + }); + } + + if (onlyDeclaration) { + attrs.push({ + kind: "function_attribute", + type: "abstract", + loc: dummySrcInfoPrintable, + }); + } + + return attrs; +} + +export interface FunctionParameters { + /** + * Minimum number of let statements at the start of function body. + * @default FuzzConfig.letStatementsMinNum + */ + letStatementsMinNum: number; + + /** + * Maximum number of let statements at the start of function body. + * @default FuzzConfig.letStatementsMaxNum + */ + letStatementsMaxNum: number; + + /** + * Minimum number of statements in the function body (not counting initial let statements and final return) + * @default FuzzConfig.statementsMinLength + */ + statementsMinLength: number; + + /** + * Maximum number of statements in the function body (not counting initial let statements and final return) + * @default FuzzConfig.statementsMaxLength + */ + statementsMaxLength: number; +} + +/** + * An object that encapsulates the generated free function or contract method definition including + * its scope and nested elements. + */ +export class FunctionDef extends NamedGenerativeEntity<Ast.FunctionDef> { + /** Generated body items. */ + private body: fc.Arbitrary<Ast.Statement>[] = []; + + private letStatementsMinNum: number; + private letStatementsMaxNum: number; + private statementsMinLength: number; + private statementsMaxLength: number; + + /** Scope used within the generated function. */ + private scope: Scope; + + private kind: FunctionKind; + + constructor( + parentScope: Scope, + kind: FunctionKind, + type: FunctionType, + name?: string, + params: Partial<FunctionParameters> = {}, + ) { + const scope = new Scope(kind, parentScope); + super( + type, + name + ? generateAstIdFromName(name) + : createSample(generateAstId(scope)), + ); + this.scope = scope; + this.kind = kind; + const { + letStatementsMinNum = FuzzConfig.letStatementsMinNum, + letStatementsMaxNum = FuzzConfig.letStatementsMaxNum, + statementsMinLength = FuzzConfig.statementsMinLength, + statementsMaxLength = FuzzConfig.statementsMaxLength, + } = params; + this.letStatementsMinNum = letStatementsMinNum; + this.letStatementsMaxNum = letStatementsMaxNum; + this.statementsMinLength = statementsMinLength; + this.statementsMaxLength = statementsMaxLength; + } + + /** + * Generates body of the function emitting return statement and statements generated from the bottom-up. + */ + private generateBody(): fc.Arbitrary<Ast.Statement[]> { + const type = this.type as FunctionType; + const returnTy: Type = + type.signature.length > 0 + ? getReturnType(type) + : { kind: "util", type: UtilType.Unit }; + const returnStmt = new Return(this.scope, returnTy).generate(); + //const generatedLetBindings = Array.from( + // this.scope.getAllNamed("let"), + //).map((c) => c.generate()); + + // TODO: Augment the SUPPORTED_STDLIB_TYPES + const generatedLetEntities = fc + .constantFrom(...SUPPORTED_STDLIB_TYPES) + .map( + (ty) => + new Let( + this.scope, + { kind: "stdlib", type: ty }, + new Expression(this.scope, { + kind: "stdlib", + type: ty, + }).generate(), + ), + ) + .chain((l) => l.generate()); + + const generatedLetBindings = fc.array(generatedLetEntities, { + minLength: this.letStatementsMinNum, + maxLength: this.letStatementsMaxNum, + }); + + const generatedStmts = fc.array(new Statement(this.scope).generate(), { + minLength: this.statementsMinLength, + maxLength: this.statementsMaxLength, + }); + + //const generatedStmts = Array.from( + // this.scope.getAllUnnamed("statement"), + //).map((c) => c.generate()); + + return fc + .tuple(generatedLetBindings, generatedStmts, returnStmt) + .map((tup) => tup.flat()); + } + + public generateImpl( + extraAttrs: Ast.FunctionAttribute[], + ): fc.Arbitrary<Ast.FunctionDef> { + const returnTy = getReturnType(this.type as FunctionType); + return this.generateBody().map((stmt) => + GlobalContext.makeF.makeDummyFunctionDef( + getAttributes(extraAttrs, this.kind, false), + this.name, + isUnit(returnTy) ? undefined : tyToAstType(returnTy), + generateParameters( + this.kind, + this.type as FunctionType, + this.scope, + ), + stmt, + ), + ); + } + + /** + * Generates a function definition without extra attributes. + */ + public generate(): fc.Arbitrary<Ast.FunctionDef> { + return this.generateImpl([]); + } +} + +/** + * An object that encapsulates the generated free function or trait method declaration including + * its scope and nested elements. + */ +export class FunctionDecl extends NamedGenerativeEntity<Ast.FunctionDecl> { + /** Scope used within the generated function. */ + private scope: Scope; + + private kind: FunctionKind; + + constructor(parentScope: Scope, kind: FunctionKind, type: FunctionType) { + const scope = new Scope(kind, parentScope); + super(type, createSample(generateAstId(scope))); + this.kind = "method"; + this.scope = scope; + } + + private generateImpl( + extraAttrs: Ast.FunctionAttribute[], + ): fc.Arbitrary<Ast.FunctionDecl> { + const returnTy = getReturnType(this.type as FunctionType); + return fc.constant( + GlobalContext.makeF.makeDummyFunctionDecl( + getAttributes(extraAttrs, this.kind, true), + this.name, + isUnit(returnTy) ? undefined : tyToAstType(returnTy), + generateParameters( + this.kind, + this.type as FunctionType, + this.scope, + ), + ), + ); + } + + /** + * Generates a function definition without extra attributes. + */ + public generate(): fc.Arbitrary<Ast.FunctionDecl> { + return this.generateImpl([]); + } + + /** + * Generates a new function definition for this declaration. + */ + public generateDefinition( + kind: FunctionKind, + attrs: Ast.FunctionAttribute[] = [], + ): fc.Arbitrary<Ast.FunctionDef> { + return new FunctionDef( + this.scope.parentScope!, + kind, + this.type as FunctionType, + this.name.text, + ).generateImpl(attrs); + } +} diff --git a/src/test/fuzzer/src/generators/generator.ts b/src/test/fuzzer/src/generators/generator.ts new file mode 100644 index 0000000000..1f7e190693 --- /dev/null +++ b/src/test/fuzzer/src/generators/generator.ts @@ -0,0 +1,36 @@ +import type * as Ast from "@/ast/ast"; +import type { Type } from "@/test/fuzzer/src/types"; + +import type fc from "fast-check"; + +abstract class GenerativeEntityBase { + /** The type of the entity. */ + public type: Type; + + constructor(type: Type) { + this.type = type; + } +} + +/** + * Abstract base class for entities that generate AST structures. + */ +export abstract class GenerativeEntity<T> extends GenerativeEntityBase { + abstract generate(): fc.Arbitrary<T>; +} + +export abstract class NamedGenerativeEntity<T> extends GenerativeEntity<T> { + public name: Ast.Id; + + constructor(type: Type, name: Ast.Id) { + super(type); + this.name = name; + } +} + +/** + * A specialized version of GenerativeEntity that cannot generate AST entities in some cases. + */ +export abstract class GenerativeEntityOpt<T> extends GenerativeEntityBase { + abstract generate(): fc.Arbitrary<T> | undefined; +} diff --git a/src/test/fuzzer/src/generators/index.ts b/src/test/fuzzer/src/generators/index.ts new file mode 100644 index 0000000000..8d3571a556 --- /dev/null +++ b/src/test/fuzzer/src/generators/index.ts @@ -0,0 +1,14 @@ +export { + ConstantDef, + ConstantDecl, +} from "@/test/fuzzer/src/generators/constant"; +export { Field } from "@/test/fuzzer/src/generators/field"; +export { Contract } from "@/test/fuzzer/src/generators/contract"; +export { + FunctionDef, + FunctionDecl, +} from "@/test/fuzzer/src/generators/function"; +export { Program } from "@/test/fuzzer/src/generators/program"; +export { Expression } from "@/test/fuzzer/src/generators/expression"; +export { Statement } from "@/test/fuzzer/src/generators/statement"; +export { GenerativeEntity } from "@/test/fuzzer/src/generators/generator"; diff --git a/src/test/fuzzer/src/generators/parameter.ts b/src/test/fuzzer/src/generators/parameter.ts new file mode 100644 index 0000000000..1835183b57 --- /dev/null +++ b/src/test/fuzzer/src/generators/parameter.ts @@ -0,0 +1,40 @@ +import type * as Ast from "@/ast/ast"; +import { createSample, generateAstId } from "@/test/fuzzer/src/util"; +import { tyToAstType } from "@/test/fuzzer/src/types"; +import type { Type } from "@/test/fuzzer/src/types"; +import type { Scope } from "@/test/fuzzer/src/scope"; +import { NamedGenerativeEntity } from "@/test/fuzzer/src/generators/generator"; + +import fc from "fast-check"; +import { GlobalContext } from "@/test/fuzzer/src/context"; + +/** + * An object that encapsulates generated Ast.TypedParameter. + */ +export class Parameter extends NamedGenerativeEntity<Ast.TypedParameter> { + /** + * @param parentScope Scope of the function this argument belongs to. + * @param isBounced If the type of the argument should be wrapped in `bounced<>` + */ + constructor( + parentScope: Scope, + type: Type, + private isBounced: boolean = false, + ) { + if (!parentScope.definedIn("receive", "method", "function")) { + throw new Error( + `Cannot define a function argument in the ${parentScope.kind} scope`, + ); + } + super(type, createSample(generateAstId(parentScope))); + } + + generate(): fc.Arbitrary<Ast.TypedParameter> { + return fc.constant( + GlobalContext.makeF.makeDummyTypedParameter( + this.name, + tyToAstType(this.type, this.isBounced), + ), + ); + } +} diff --git a/src/test/fuzzer/src/generators/program.ts b/src/test/fuzzer/src/generators/program.ts new file mode 100644 index 0000000000..dc390513c3 --- /dev/null +++ b/src/test/fuzzer/src/generators/program.ts @@ -0,0 +1,308 @@ +import type * as Ast from "@/ast/ast"; +import { + createSamplesArray, + createSample, + randomInt, + randomBool, + randomElement, + generateAstIdFromName, +} from "@/test/fuzzer/src/util"; +import { TypeGen, UtilType, getStdlibTypes } from "@/test/fuzzer/src/types"; +import { Contract } from "@/test/fuzzer/src/generators/contract"; +import { Message, Struct } from "@/test/fuzzer/src/generators/struct"; +import { Trait } from "@/test/fuzzer/src/generators/trait"; +import { Scope } from "@/test/fuzzer/src/scope"; +import { NamedGenerativeEntity } from "@/test/fuzzer/src/generators/generator"; +import { getStdlibTraits } from "@/test/fuzzer/src/stdlib"; +import fc from "fast-check"; +import { GlobalContext } from "@/test/fuzzer/src/context"; +import { ConstantDef } from "@/test/fuzzer/src/generators/constant"; +import { Expression } from "@/test/fuzzer/src/generators/expression"; +import { FunctionDef } from "@/test/fuzzer/src/generators/function"; +import { FuzzConfig } from "@/test/fuzzer/src/config"; + +export interface ProgramParameters { + /** Add definitions that mock stdlib ones to the generated program. */ + addStdlib: boolean; + + /** + * Minimum number of structures generated on the program level. + * @default FuzzConfig.structsMinNum + */ + structsMinNum: number; + + /** + * Maximum number of structures generated on the program level. + * @default FuzzConfig.structsMaxNum + */ + structsMaxNum: number; + + /** + * Minimum number of messages generated on the program level. + * @default FuzzConfig.messagesMinNum + */ + messagesMinNum: number; + + /** + * Maximum number of messages generated on the program level. + * @default FuzzConfig.messagesMaxNum + */ + messagesMaxNum: number; + + /** + * Minimum number of the generated traits. Some of them might be used by the generated contracts. + * @default FuzzConfig.traitsMinNum + */ + traitsMinNum: number; + + /** + * Maximum number of the generated traits. Some of them might be used by the generated contracts. + * @default FuzzConfig.traitsMaxNum + */ + traitsMaxNum: number; + + /** + * Minimum number of generated contracts + * @default FuzzConfig.contractsMinNum + */ + contractsMinNum: number; + + /** + * Maximum number of generated contracts + * @default FuzzConfig.contractsMaxNum + */ + contractsMaxNum: number; + + /** + * Minimum number of generated functions + * @default FuzzConfig.functionsMinNum + */ + functionsMinNum: number; + + /** + * Maximum number of generated functions + * @default FuzzConfig.functionsMaxNum + */ + functionsMaxNum: number; + + /** + * Minimum number of function arguments + * @default FuzzConfig.functionArgsMinNum + */ + functionArgsMinNum: number; + + /** + * Maximum number of function arguments + * @default FuzzConfig.functionArgsMaxNum + */ + functionArgsMaxNum: number; + + /** + * Minimum number of generated constants + * @default FuzzConfig.constantsMinNum + */ + constantsMinNum: number; + + /** + * Maximum number of generated constants + * @default FuzzConfig.constantsMaxNum + */ + constantsMaxNum: number; +} + +/** + * An object that encapsulates a randomly generated Ast.Module including extra information + * about its entries and their scopes. + */ +export class Program extends NamedGenerativeEntity<Ast.Module> { + /** Top-level global scope. */ + private scope: Scope; + + private addStdlib: boolean; + + constructor(params: Partial<ProgramParameters> = {}) { + super( + { kind: "util", type: UtilType.Program }, + generateAstIdFromName("program"), + ); + + const { + addStdlib = FuzzConfig.addStdlib, + structsMinNum = FuzzConfig.structsMinNum, + structsMaxNum = FuzzConfig.structsMaxNum, + messagesMinNum = FuzzConfig.messagesMinNum, + messagesMaxNum = FuzzConfig.messagesMaxNum, + traitsMinNum = FuzzConfig.traitsMinNum, + traitsMaxNum = FuzzConfig.traitsMaxNum, + contractsMinNum = FuzzConfig.contractsMinNum, + contractsMaxNum = FuzzConfig.constantsMaxNum, + functionsMinNum = FuzzConfig.functionsMinNum, + functionsMaxNum = FuzzConfig.functionsMaxNum, + functionArgsMinNum = FuzzConfig.functionArgsMinNum, + functionArgsMaxNum = FuzzConfig.functionArgsMaxNum, + constantsMinNum = FuzzConfig.constantsMinNum, + constantsMaxNum = FuzzConfig.constantsMaxNum, + } = params; + this.addStdlib = addStdlib; + + this.scope = new Scope("program", undefined); + + // NOTE: Structures and messages must be generated prior to contracts in order + // to add their entries to scopes for further reuse. + Array.from({ length: randomInt(structsMinNum, structsMaxNum) }).forEach( + (_) => { + this.scope.addNamed("struct", this.makeStruct()); + }, + ); + + Array.from({ + length: randomInt(messagesMinNum, messagesMaxNum), + }).forEach((_) => { + this.scope.addNamed("message", this.makeMessage()); + }); + + // NOTE: Traits must be generated prior to contracts to enable them implement them. + const traits: Trait[] = []; + Array.from({ length: randomInt(traitsMinNum, traitsMaxNum) }).forEach( + (_) => { + const trait = this.makeTrait(); + traits.push(trait); + this.scope.addNamed("trait", trait); + }, + ); + + // One of the traits could be implemented by the generated contracts. + Array.from({ + length: randomInt(contractsMinNum, contractsMaxNum), + }).forEach((_) => { + const traitToImplement = + traits.length > 0 && randomBool() + ? randomElement(traits) + : undefined; + this.scope.addNamed( + "contract", + this.makeContract(traitToImplement), + ); + }); + + Array.from({ + length: randomInt(functionsMinNum, functionsMaxNum), + }).forEach((_) => { + this.scope.addNamed( + "functionDef", + this.makeFunction(functionArgsMinNum, functionArgsMaxNum), + ); + }); + + Array.from({ + length: randomInt(constantsMinNum, constantsMaxNum), + }).forEach((_) => { + this.scope.addNamed("constantDef", this.makeConstant()); + }); + } + + /** + * Generates a Tact program. + * + * It always follows a structure that includes a single contract with a few methods + * which are considered as entry points of the random program generation. This means, the generation + * starts bottom-up from the return types of these methods and adds different AST entries, including + * constants, functions and contract fields. AST nodes inside the contract implementation may vary, + * as determined by fast-check. + */ + public generate(): fc.Arbitrary<Ast.Module> { + const stdlibEntries = this.addStdlib + ? getStdlibTraits() + .concat(getStdlibTypes()) + .map((entry) => fc.constant(entry)) + : []; + + const traits = Array.from(this.scope.getAllNamed("trait")).map((t) => + t.generate(), + ); + const contracts = Array.from(this.scope.getAllNamed("contract")).map( + (c) => c.generate(), + ); + const structs = Array.from(this.scope.getAllNamed("struct")).map((s) => + s.generate(), + ); + const messages = Array.from(this.scope.getAllNamed("message")).map( + (m) => m.generate(), + ); + const constants = Array.from(this.scope.getAllNamed("constantDef")).map( + (c) => c.generate(), + ); + const functions = Array.from(this.scope.getAllNamed("functionDef")).map( + (f) => f.generate(), + ); + return fc + .tuple( + ...stdlibEntries, + ...structs, + ...messages, + ...constants, + ...functions, + ...traits, + ...contracts, + ) + .map((decls) => GlobalContext.makeF.makeModule([], decls)); + } + + /** + * Creates a contract object with the predefined structure which is an entry point of the generation. + * @param trait Trait the generated contract should implement + */ + private makeContract(trait?: Trait): Contract { + const methodSignatures = createSamplesArray( + () => TypeGen.fromScope(this.scope).generateMethod(), + 1, + 3, + ).map((arb) => createSample(arb)); + return new Contract(this.scope, methodSignatures, trait); + } + + /** + * Creates a structure in the program scope. + */ + private makeStruct(): Struct { + return new Struct( + this.scope, + createSample(TypeGen.fromScope(this.scope).generateStruct(false)), + ); + } + + /** + * Creates a message in the program scope. + */ + private makeMessage(): Message { + return new Message( + this.scope, + createSample(TypeGen.fromScope(this.scope).generateStruct(true)), + ); + } + + /** + * Creates a trait in the program scope. + */ + private makeTrait(): Trait { + return new Trait(this.scope); + } + + private makeConstant(): ConstantDef { + const ty = createSample(TypeGen.fromScope(this.scope).generate()); + return ConstantDef.fromScope( + this.scope, + ty, + new Expression(this.scope, ty, { + useIdentifiersInExpressions: false, + }).generate(), + ); + } + + private makeFunction(minArgsNum: number, maxArgsNum: number): FunctionDef { + const ty = createSample( + TypeGen.fromScope(this.scope).generateFun(minArgsNum, maxArgsNum), + ); + return new FunctionDef(this.scope, "function", ty); + } +} diff --git a/src/test/fuzzer/src/generators/receiver.ts b/src/test/fuzzer/src/generators/receiver.ts new file mode 100644 index 0000000000..d832c386bf --- /dev/null +++ b/src/test/fuzzer/src/generators/receiver.ts @@ -0,0 +1,215 @@ +import type * as Ast from "@/ast/ast"; +import { + UtilType, + StdlibType, + isBouncedMessage, +} from "@/test/fuzzer/src/types"; +import type { Type } from "@/test/fuzzer/src/types"; +import { Scope } from "@/test/fuzzer/src/scope"; +import { GenerativeEntity } from "@/test/fuzzer/src/generators/generator"; +import { + createSample, + randomBool, + randomElement, +} from "@/test/fuzzer/src/util"; +import { Expression } from "@/test/fuzzer/src/generators/expression"; +import { Parameter } from "@/test/fuzzer/src/generators/parameter"; +import { Let, Statement } from "@/test/fuzzer/src/generators/statement"; + +import fc from "fast-check"; +import { GlobalContext } from "@/test/fuzzer/src/context"; +import { generateString } from "@/test/fuzzer/src/generators/uniform-expr-gen"; +import { FuzzConfig } from "@/test/fuzzer/src/config"; + +const RECEIVE_RETURN_TY: Type = { kind: "util", type: UtilType.Unit }; + +function generateReceiverSimpleSubKind( + param: Parameter, +): fc.Arbitrary<Ast.ReceiverSimple> { + return param + .generate() + .map((p) => GlobalContext.makeF.makeReceiverSimple(p)); +} + +function generateReceiverFallbackSubKind(): fc.Arbitrary<Ast.ReceiverFallback> { + return fc.constant(GlobalContext.makeF.makeReceiverFallback()); +} + +function generateReceiverCommentSubKind(): fc.Arbitrary<Ast.ReceiverComment> { + return generateString(/*nonEmpty=*/ true).map((s) => + GlobalContext.makeF.makeReceiverComment(s), + ); +} + +function generateInternalReceiverKind( + subKind: fc.Arbitrary<Ast.ReceiverSubKind>, +): fc.Arbitrary<Ast.ReceiverInternal> { + return subKind.map((k) => GlobalContext.makeF.makeDummyReceiverInternal(k)); +} + +function generateExternalReceiverKind( + subKind: fc.Arbitrary<Ast.ReceiverSubKind>, +): fc.Arbitrary<Ast.ReceiverExternal> { + return subKind.map((k) => GlobalContext.makeF.makeDummyReceiverExternal(k)); +} + +export interface ReceiveParameters { + /** + * Minimum number of let statements at the start of function body. + * @default FuzzConfig.letStatementsMinNum + */ + letStatementsMinNum: number; + + /** + * Maximum number of let statements at the start of function body. + * @default FuzzConfig.letStatementsMaxNum + */ + letStatementsMaxNum: number; + + /** + * Minimum number of statements in the function body (not counting initial let statements and final return) + * @default FuzzConfig.statementsMinLength + */ + statementsMinLength: number; + + /** + * Maximum number of statements in the function body (not counting initial let statements and final return) + * @default FuzzConfig.statementsMaxLength + */ + statementsMaxLength: number; +} + +/** + * An object that encapsulates an Ast.Receiver. + */ +export class Receive extends GenerativeEntity<Ast.Receiver> { + /** Generated body items. */ + private body: fc.Arbitrary<Ast.Statement>[] = []; + + /** Scope used within the generated receive method. */ + private scope: Scope; + + private letStatementsMinNum: number; + private letStatementsMaxNum: number; + private statementsMinLength: number; + private statementsMaxLength: number; + + constructor(parentScope: Scope, params: Partial<ReceiveParameters> = {}) { + super(RECEIVE_RETURN_TY); + this.scope = new Scope("receive", parentScope); + const { + letStatementsMinNum = FuzzConfig.letStatementsMinNum, + letStatementsMaxNum = FuzzConfig.letStatementsMaxNum, + statementsMinLength = FuzzConfig.statementsMinLength, + statementsMaxLength = FuzzConfig.statementsMaxLength, + } = params; + this.letStatementsMinNum = letStatementsMinNum; + this.letStatementsMaxNum = letStatementsMaxNum; + this.statementsMinLength = statementsMinLength; + this.statementsMaxLength = statementsMaxLength; + } + + private generateSelector(): fc.Arbitrary<Ast.ReceiverKind> { + if (randomBool()) { + const ty = createSample( + fc.record<Type>({ + kind: fc.constant("stdlib"), + type: fc.constantFrom( + // TODO: Support Slice + StdlibType.String, + ), + }), + ) as Type; + const param = new Parameter(this.scope, ty); + this.scope.addNamed("parameter", param); + const internalSimple = generateInternalReceiverKind( + generateReceiverSimpleSubKind(param), + ); + const externalSimple = generateExternalReceiverKind( + generateReceiverSimpleSubKind(param), + ); + return fc.oneof(internalSimple, externalSimple); + } + + // Choose a random message and create a bounced receiver using it. + const messages = this.scope.getProgramScope().getAllNamed("message"); + if (messages.length > 0 && randomBool()) { + const msg = randomElement(messages); + const param = new Parameter( + this.scope, + msg.type, + isBouncedMessage(msg.type), + ); + this.scope.addNamed("parameter", param); + return param + .generate() + .map((p) => GlobalContext.makeF.makeDummyReceiverBounce(p)); + } + + const internalFallback = generateInternalReceiverKind( + generateReceiverFallbackSubKind(), + ); + const externalFallback = generateExternalReceiverKind( + generateReceiverFallbackSubKind(), + ); + const internalComment = generateInternalReceiverKind( + generateReceiverCommentSubKind(), + ); + const externalComment = generateExternalReceiverKind( + generateReceiverCommentSubKind(), + ); + + return fc.oneof( + internalFallback, + externalFallback, + internalComment, + externalComment, + ); // TODO: add bounce receiver generation + } + + private generateBody(): fc.Arbitrary<Ast.Statement[]> { + // Create a dummy expression to execute the bottom-up AST generation. + //const expr = new Expression(this.scope, this.type).generate(); + //const stmt = new StatementExpression(expr).generate(); + + //const generatedLetBindings = Array.from( + // this.scope.getAllNamed("let"), + //).map((c) => c.generate()); + //const generatedStmts = Array.from( + // this.scope.getAllUnnamed("statement"), + //).map((c) => c.generate()); + + // TODO: Make it generate arbitrary types + const generatedLetBindings = fc.array( + new Let( + this.scope, + { kind: "stdlib", type: StdlibType.Int }, + new Expression(this.scope, { + kind: "stdlib", + type: StdlibType.Int, + }).generate(), + ).generate(), + { + minLength: this.letStatementsMinNum, + maxLength: this.letStatementsMaxNum, + }, + ); + + const generatedStmts = fc.array(new Statement(this.scope).generate(), { + minLength: this.statementsMinLength, + maxLength: this.statementsMaxLength, + }); + + return fc + .tuple(generatedLetBindings, generatedStmts) + .map((tup) => tup.flat()); + } + + public generate(): fc.Arbitrary<Ast.Receiver> { + return fc + .tuple(this.generateSelector(), this.generateBody()) + .map(([sel, stmt]) => + GlobalContext.makeF.makeDummyReceiver(sel, stmt), + ); + } +} diff --git a/src/test/fuzzer/src/generators/statement.ts b/src/test/fuzzer/src/generators/statement.ts new file mode 100644 index 0000000000..145641bae1 --- /dev/null +++ b/src/test/fuzzer/src/generators/statement.ts @@ -0,0 +1,622 @@ +import type * as Ast from "@/ast/ast"; +import fc from "fast-check"; + +import { + Expression, + generateFunctionCallArgs, + generateMethodCallArgs, + makeSelfID, + MethodCall, + StaticCall, +} from "@/test/fuzzer/src/generators/expression"; +import { + randomBool, + createSample, + generateName, + packArbitraries, + randomElement, + generateAstId, + generateAstIdFromName, +} from "@/test/fuzzer/src/util"; +import { + GenerativeEntity, + NamedGenerativeEntity, +} from "@/test/fuzzer/src/generators/generator"; +import { StdlibType, UtilType, tyToAstType } from "@/test/fuzzer/src/types"; +import type { Type } from "@/test/fuzzer/src/types"; +import { Scope } from "@/test/fuzzer/src/scope"; +import type { NamedScopeItemKind } from "@/test/fuzzer/src/scope"; +import { GlobalContext } from "@/test/fuzzer/src/context"; +import { FuzzConfig } from "@/test/fuzzer/src/config"; + +/** Type all the imperative constructions have. */ +const STMT_TY: Type = { kind: "util", type: UtilType.Unit }; + +/** + * Generates `return` statements. + */ +export class Return extends GenerativeEntity<Ast.Statement> { + /** + * @param parentScope Scope this statement belongs to. + */ + constructor( + private parentScope: Scope, + type: Type, + ) { + super(type); + } + generate(): fc.Arbitrary<Ast.Statement> { + const gen = + this.type.kind === "util" && this.type.type === UtilType.Unit + ? fc.constant(undefined) + : new Expression(this.parentScope, this.type).generate(); + + return gen.map((expr) => + GlobalContext.makeF.makeDummyStatementReturn(expr), + ); + } +} + +/** + * Let generator is the entry point of the bottom-up statement generation. + * It creates a variable binding and then adds additional statements that mutate the created binding and the global state. + */ +export class Let extends NamedGenerativeEntity<Ast.Statement> { + /** + * @param parentScope Scope this statement belongs to. + * @param type Type of the generated binding. + * @param expr Expression generator to initialize that binding. + */ + constructor( + parentScope: Scope, + type: Type, + private expr: fc.Arbitrary<Ast.Expression>, + ) { + super(type, createSample(generateAstId(parentScope))); + } + + generate(): fc.Arbitrary<Ast.Statement> { + return this.expr.map((expr) => + GlobalContext.makeF.makeDummyStatementLet( + this.name, + tyToAstType(this.type), + expr, + ), + ); + } +} + +/** + * Creates assignments and augmented assignments to modify global or local variables. + */ +export class AssignStatement extends GenerativeEntity<Ast.Statement> { + /** + * @param path A qualified name of the lhs. + * @param rhs Expression to assign to. + * @param rhsTy Type of the rhs of the assignment. + * @param ty Type of the statement. + */ + constructor( + private path: Ast.Expression, + private rhs: fc.Arbitrary<Ast.Expression>, + private rhsTy: Type, + ty = STMT_TY, + ) { + super(ty); + } + + generate(): fc.Arbitrary<Ast.Statement> { + const assigns: fc.Arbitrary<Ast.Statement>[] = [ + this.rhs.map((expr) => + GlobalContext.makeF.makeDummyStatementAssign(this.path, expr), + ), + ]; + // Only integer types in augmented assignments are supported. + // See: https://github.com/tact-lang/tact/issues/353. + if ( + this.rhsTy.kind === "stdlib" && + this.rhsTy.type === StdlibType.Int + ) { + assigns.push( + this.rhs.chain((expr) => { + return fc + .constantFrom<Ast.AugmentedAssignOperation>( + "+=", + "-=", + "*=", + "/=", + "%=", + ) + .map((op) => + GlobalContext.makeF.makeDummyStatementAugmentedAssign( + op, + this.path, + expr, + ), + ); + }), + ); + } + return fc.oneof(...assigns); + } +} + +/** + * Generates `while` and `until` loops. + */ +export class WhileUntilStatement extends GenerativeEntity<Ast.Statement> { + constructor( + private condition: fc.Arbitrary<Ast.Expression>, + private body: fc.Arbitrary<Ast.Statement>[], + private kind: "until" | "while", + type: Type = STMT_TY, + ) { + super(type); + } + generate(): fc.Arbitrary<Ast.Statement> { + return packArbitraries(this.body).chain((stmts) => + this.condition.map((expr) => { + if (this.kind === "until") { + return GlobalContext.makeF.makeDummyStatementUntil( + expr, + stmts, + ); + } else { + return GlobalContext.makeF.makeDummyStatementWhile( + expr, + stmts, + ); + } + }), + ); + } +} + +/** + * Generates `repeat` loops. + */ +export class RepeatStatement extends GenerativeEntity<Ast.Statement> { + constructor( + private parentScope: Scope, + private body: fc.Arbitrary<Ast.Statement>[], + type: Type = STMT_TY, + ) { + super(type); + } + generate(): fc.Arbitrary<Ast.Statement> { + const iterations = new Expression(this.parentScope, { + kind: "stdlib", + type: StdlibType.Int, + }).generate(); + return iterations.chain((iter) => + packArbitraries(this.body).map((stmts) => + GlobalContext.makeF.makeDummyStatementRepeat(iter, stmts), + ), + ); + } +} + +/** + * Generates `foreach` loops. + */ +export class ForeachStatement extends GenerativeEntity<Ast.Statement> { + constructor( + private map: fc.Arbitrary<Ast.Expression>, + private keyName: string, + private valueName: string, + private body: fc.Arbitrary<Ast.Statement>[], + type: Type = STMT_TY, + ) { + super(type); + } + generate(): fc.Arbitrary<Ast.Statement> { + return this.map.chain((map) => + packArbitraries(this.body).map((stmts) => + GlobalContext.makeF.makeDummyStatementForEach( + generateAstIdFromName(this.keyName), + generateAstIdFromName(this.valueName), + map, + stmts, + ), + ), + ); + } +} + +/** + * Generates conditional statements. + */ +export class ConditionStatement extends GenerativeEntity<Ast.StatementCondition> { + constructor( + private parentScope: Scope, + private trueStmts: fc.Arbitrary<Ast.Statement>[], + private falseStmts?: fc.Arbitrary<Ast.Statement>[], + type: Type = STMT_TY, + ) { + super(type); + } + generate(): fc.Arbitrary<Ast.StatementCondition> { + const condition = new Expression(this.parentScope, { + kind: "stdlib", + type: StdlibType.Bool, + }).generate(); + const falseArb = this.falseStmts + ? packArbitraries(this.falseStmts) + : fc.constant(undefined); + + return condition.chain((cond) => + packArbitraries(this.trueStmts).chain((trueStmts) => + falseArb.map((falseStmts) => + GlobalContext.makeF.makeDummyStatementCondition( + cond, + trueStmts, + falseStmts, + ), + ), + ), + ); + } +} + +/** + * Generates try-catch statements. + */ +export class TryCatch extends GenerativeEntity<Ast.Statement> { + constructor( + private tryStmts: fc.Arbitrary<Ast.Statement>[], + private catchBlock?: Ast.CatchBlock, + type: Type = STMT_TY, + ) { + super(type); + } + generate(): fc.Arbitrary<Ast.Statement> { + return packArbitraries(this.tryStmts).map((stmts) => + GlobalContext.makeF.makeDummyStatementTry(stmts, this.catchBlock), + ); + } +} + +/** + * Generates expression statements. + * The return value of the function/method calls generated by this is never used. + */ +export class StatementExpression extends GenerativeEntity<Ast.Statement> { + constructor( + private expr: fc.Arbitrary<Ast.Expression>, + type: Type = STMT_TY, + ) { + super(type); + } + generate(): fc.Arbitrary<Ast.Statement> { + return this.expr.map((expr) => + GlobalContext.makeF.makeDummyStatementExpression(expr), + ); + } +} + +export interface StatementParameters { + /** + * Determines the maximum depth of nested statement blocks. + * @default FuzzConfig.nestedBlocksNum + */ + nestedBlocksNum: number; + + /** + * Number of statements in each block. + * @default FuzzConfig.stmtsInBlock + */ + stmtsInBlock: number; +} + +/** + * The generator that creates statements in the given block which mutate global or local state. + */ +export class Statement extends GenerativeEntity<Ast.Statement> { + private nestedBlocksNum: number; + private stmtsInBlock: number; + private params: Partial<StatementParameters>; + + /** + * @param parentScope Scope the generated statements belongs to. + * @param recursionLevel Used internally within Statement. + * @param params Optional parameters for statement generation. + * @param type Type of the generated statement. + */ + constructor( + private parentScope: Scope, + private recursionLevel = 0, + params: Partial<StatementParameters> = {}, + type: Type = STMT_TY, + ) { + if (parentScope.definedIn("program", "contract")) { + throw new Error( + `Cannot generate statements in the ${parentScope.kind} scope`, + ); + } + super(type); + + const { + nestedBlocksNum = FuzzConfig.nestedBlocksNum, + stmtsInBlock = FuzzConfig.stmtsInBlock, + } = params; + this.nestedBlocksNum = nestedBlocksNum; + this.stmtsInBlock = stmtsInBlock; + + this.params = params; + } + + generate(): fc.Arbitrary<Ast.Statement> { + const varAssign = this.makeVarAssign(); + const fieldAssign = this.makeFieldAssign(); + const loopStmt = randomBool() + ? this.makeWhileUntil() + : this.makeRepeat(); + const foreachStmt = this.makeForEach(); + const condStmt = this.makeCondition(); + const tryCatch = this.makeTryCatch(); + const callStmt = this.makeCall(); + const generated = [ + ...(varAssign ? [varAssign] : []), + ...(fieldAssign ? [fieldAssign] : []), + ...(loopStmt ? [loopStmt] : []), + ...(foreachStmt ? [foreachStmt] : []), + ...(condStmt ? [condStmt] : []), + ...(tryCatch ? [tryCatch] : []), + ...(callStmt ? [callStmt] : []), + ]; + if (generated.length === 0) { + // No variables in local/global scopes are available: generate dummy statements. + generated.push(this.makeDummyStmt()); + } + return fc.oneof(...generated); + } + + /** + * Creates statements that mutate local variables, including assignments and augmented assignments. + */ + private makeVarAssign(): fc.Arbitrary<Ast.Statement> | undefined { + const varEntries: [string, Type][] = + this.parentScope.getNamedEntriesRecursive("let"); + if (varEntries.length === 0) { + return undefined; + } + const arbs = varEntries.map(([name, ty]) => { + const expr = new Expression(this.parentScope, ty).generate(); + return new AssignStatement( + generateAstIdFromName(name), + expr, + ty, + ).generate(); + }); + return arbs.length > 0 ? fc.oneof(...arbs) : undefined; + } + + /** + * Creates statements that mutate contract fields, including assignments and augmented assignments. + */ + private makeFieldAssign(): fc.Arbitrary<Ast.Statement> | undefined { + if (!this.parentScope.definedIn("method")) { + return undefined; + } + const fieldEntries: [string, Type][] = + this.parentScope.getNamedEntriesRecursive("field"); + if (fieldEntries.length === 0) { + return undefined; + } + const arbs = fieldEntries.map(([name, ty]) => { + const expr = new Expression(this.parentScope, ty).generate(); + return new AssignStatement( + GlobalContext.makeF.makeDummyFieldAccess( + makeSelfID(), + GlobalContext.makeF.makeDummyId(name), + ), + expr, + ty, + ).generate(); + }); + return arbs.length > 0 ? fc.oneof(...arbs) : undefined; + } + + /** + * Creates either while or until loops. + */ + private makeWhileUntil(): fc.Arbitrary<Ast.Statement> | undefined { + if (this.recursionLevel >= this.nestedBlocksNum) { + return undefined; + } + const condition = new Expression(this.parentScope, { + kind: "stdlib", + type: StdlibType.Bool, + }).generate(); + const body = this.makeStmtsBlock(); + return new WhileUntilStatement( + condition, + body, + randomBool() ? "while" : "until", + ).generate(); + } + + /** + * Generates repeat loops. + */ + private makeRepeat(): fc.Arbitrary<Ast.Statement> | undefined { + if (this.recursionLevel >= this.nestedBlocksNum) { + return undefined; + } + const body = this.makeStmtsBlock(); + return new RepeatStatement(this.parentScope, body).generate(); + } + + /** + * Collects all local map Ast.Ids in parent scope. + */ + private collectLocalMapIds(entryKinds: NamedScopeItemKind[]): Ast.Id[] { + return this.parentScope + .getNamedEntriesRecursive(...entryKinds) + .filter(([_, mapTy]: [string, Type]) => mapTy.kind === "map") + .map(([mapName, _]: [string, Type]) => + generateAstIdFromName(mapName), + ); + } + + /** + * Collects all field map Ast.Ids in parent scope. + */ + private collectFieldMapIds( + entryKinds: NamedScopeItemKind[], + ): Ast.FieldAccess[] { + return this.parentScope + .getNamedEntriesRecursive(...entryKinds) + .filter(([_, mapTy]: [string, Type]) => mapTy.kind === "map") + .map(([mapName, _]: [string, Type]) => + GlobalContext.makeF.makeDummyFieldAccess( + makeSelfID(), + GlobalContext.makeF.makeDummyId(mapName), + ), + ); + } + + /** + * Generates foreach loops. + */ + private makeForEach(): fc.Arbitrary<Ast.Statement> | undefined { + if (this.recursionLevel >= this.nestedBlocksNum) { + return undefined; + } + const scope = new Scope("block", this.parentScope); + const simpleMapIds = this.collectLocalMapIds([ + "let", + "constantDecl", + "constantDef", + ]); + const fieldMapPaths = this.collectFieldMapIds(["field"]); + const mapIds = [ + ...simpleMapIds.map(fc.constant), + ...fieldMapPaths.map(fc.constant), + ]; + if (mapIds.length === 0) { + return undefined; + } + const map: fc.Arbitrary<Ast.Expression> = fc.oneof(...mapIds); + const keyVarName = createSample(generateName(scope)); + const valueVarName = createSample(generateName(scope)); + const body = this.makeStmtsBlock(scope); + return new ForeachStatement( + map, + keyVarName, + valueVarName, + body, + ).generate(); + } + + /** + * Generates conditional statements. + */ + private makeCondition(): fc.Arbitrary<Ast.StatementCondition> | undefined { + if (this.recursionLevel >= this.nestedBlocksNum) { + return undefined; + } + const trueStatements = this.makeStmtsBlock(); + const falseStatements = randomBool() + ? undefined + : this.makeStmtsBlock(); + return new ConditionStatement( + this.parentScope, + trueStatements, + falseStatements, + ).generate(); + } + + /** + * Generates try and try-catch statements + */ + private makeTryCatch(): fc.Arbitrary<Ast.Statement> | undefined { + if (this.recursionLevel >= this.nestedBlocksNum) { + return undefined; + } + const tryStmts = this.makeStmtsBlock(); + if (randomBool()) { + const catchScope = new Scope("block", this.parentScope); + const catchName = createSample(generateName(catchScope)); + const catchStmts = this.makeStmtsBlock(catchScope).map((stmt) => + createSample(stmt), + ); + return new TryCatch(tryStmts, { + catchName: generateAstIdFromName(catchName), + catchStatements: catchStmts, + }).generate(); + } else { + return new TryCatch(tryStmts).generate(); + } + } + + /** + * Generates function or method calls without using the return value. + */ + private makeCall(): fc.Arbitrary<Ast.Statement> | undefined { + if (this.recursionLevel >= this.nestedBlocksNum) { + return undefined; + } + if (this.parentScope.definedIn("method", "contract") && randomBool()) { + // Call a method + const methodEntries = + this.parentScope.getNamedEntriesRecursive("methodDef"); + if (methodEntries.length === 0) { + return undefined; + } + const [funName, funTy] = randomElement(methodEntries); + return new StatementExpression( + new MethodCall( + funTy, + funName, + makeSelfID(), + generateMethodCallArgs(funTy, this.parentScope), + ).generate(), + ).generate(); + } else { + // Call a function + const funEntries = + this.parentScope.getNamedEntriesRecursive("functionDef"); + if (funEntries.length === 0) { + return undefined; + } + const [funName, funTy] = randomElement(funEntries); + return new StatementExpression( + new StaticCall( + funTy, + funName, + generateFunctionCallArgs(funTy, this.parentScope), + ).generate(), + ).generate(); + } + } + + /** + * Creates a block of statements nested in curly braces in concrete syntax. + */ + private makeStmtsBlock(blockScope?: Scope): fc.Arbitrary<Ast.Statement>[] { + const scope = blockScope ?? new Scope("block", this.parentScope); + const block: fc.Arbitrary<Ast.Statement>[] = []; + Array.from({ length: this.stmtsInBlock }).forEach(() => { + const stmt = new Statement( + scope, + this.recursionLevel + 1, + this.params, + ).generate(); + block.push(stmt); + }); + return block; + } + + /** + * Generates a dummy statement which doesn't have affect on control-flow nor state: + * `while (false) { }` + */ + private makeDummyStmt(): fc.Arbitrary<Ast.Statement> { + return new WhileUntilStatement( + fc.constant(GlobalContext.makeF.makeDummyBoolean(false)), + [], + "while", + ).generate(); + } +} diff --git a/src/test/fuzzer/src/generators/struct.ts b/src/test/fuzzer/src/generators/struct.ts new file mode 100644 index 0000000000..c934595b2c --- /dev/null +++ b/src/test/fuzzer/src/generators/struct.ts @@ -0,0 +1,94 @@ +import type * as Ast from "@/ast/ast"; +import { tyToString, throwTyError } from "@/test/fuzzer/src/types"; +import type { Type, StructField } from "@/test/fuzzer/src/types"; +import type { Scope } from "@/test/fuzzer/src/scope"; +import { Field } from "@/test/fuzzer/src/generators/field"; +import { generateAstIdFromName, packArbitraries } from "@/test/fuzzer/src/util"; +import { NamedGenerativeEntity } from "@/test/fuzzer/src/generators/generator"; + +import type fc from "fast-check"; +import { GlobalContext } from "@/test/fuzzer/src/context"; + +/** + * An object that generates Ast.StructDecl object. + */ +export class Struct extends NamedGenerativeEntity<Ast.StructDecl> { + /** + * @param programScope A program scope the structure defined in. + */ + constructor( + private programScope: Scope, + type: Type, + ) { + if (type.kind !== "struct") { + throw new Error( + `Cannot create a structure with the ${tyToString(type)} type`, + ); + } + if (!programScope.definedIn("program")) { + throw new Error( + `Cannot define a struct out of the program scope (got ${programScope.kind})`, + ); + } + super(type, generateAstIdFromName(type.name)); + } + + generate(): fc.Arbitrary<Ast.StructDecl> { + if (this.type.kind !== "struct") { + throwTyError(this.type); + } + const fields = this.type.fields.map((fieldTy: StructField) => { + return new Field( + this.programScope, + fieldTy.type, + fieldTy.default, + generateAstIdFromName(fieldTy.name), + ).generate(); + }); + return packArbitraries(fields).map((f) => + GlobalContext.makeF.makeDummyStructDecl(this.name, f), + ); + } +} + +/** + * An object that generates Ast.MessageDecl object messages. + */ +export class Message extends NamedGenerativeEntity<Ast.MessageDecl> { + /** + * @param programScope A program scope the structure defined in. + */ + constructor( + private programScope: Scope, + type: Type, + ) { + if (type.kind !== "message") { + throw new Error( + `Cannot create a message with the ${tyToString(type)} type`, + ); + } + if (!programScope.definedIn("program")) { + throw new Error( + `Cannot define a struct out of the program scope (got ${programScope.kind})`, + ); + } + super(type, generateAstIdFromName(type.name)); + } + + generate(): fc.Arbitrary<Ast.MessageDecl> { + if (this.type.kind !== "message") { + throwTyError(this.type); + } + const fields = this.type.fields.map((fieldTy: StructField) => { + return new Field( + this.programScope, + fieldTy.type, + fieldTy.default, + generateAstIdFromName(fieldTy.name), + ).generate(); + }); + return packArbitraries(fields).map((f) => + GlobalContext.makeF.makeDummyMessageDecl(this.name, undefined, f), + ); + } +} diff --git a/src/test/fuzzer/src/generators/trait.ts b/src/test/fuzzer/src/generators/trait.ts new file mode 100644 index 0000000000..8cf7fb26b9 --- /dev/null +++ b/src/test/fuzzer/src/generators/trait.ts @@ -0,0 +1,143 @@ +import type * as Ast from "@/ast/ast"; +import { + createSample, + generateAstId, + randomBool, +} from "@/test/fuzzer/src/util"; +import { FunctionDecl } from "@/test/fuzzer/src/generators/function"; +import { Field } from "@/test/fuzzer/src/generators/field"; +import { + ConstantDecl, + ConstantDef, +} from "@/test/fuzzer/src/generators/constant"; +import { Expression } from "@/test/fuzzer/src/generators/expression"; +import { TypeGen, makeFunctionTy, UtilType } from "@/test/fuzzer/src/types"; +import type { Type } from "@/test/fuzzer/src/types"; +import { Scope } from "@/test/fuzzer/src/scope"; +import { NamedGenerativeEntity } from "@/test/fuzzer/src/generators/generator"; + +import fc from "fast-check"; +import { GlobalContext } from "@/test/fuzzer/src/context"; +import { FuzzConfig } from "@/test/fuzzer/src/config"; + +export interface TraitParameters { + /** + * Number of fields generated within a trait. + * @default FuzzConfig.traitFieldNum + */ + traitFieldNum: number; + + /** + * Number of method declarations generated within a trait. + * @default FuzzConfig.traitMethodDeclarationsNum + */ + traitMethodDeclarationsNum: number; + + /** + * Number of constant declarations generated within a trait. + * @default FuzzConfig.traitConstantNum + */ + traitConstantNum: number; +} + +/** + * An object that encapsulates a randomly generated Ast.Trait. + */ +export class Trait extends NamedGenerativeEntity<Ast.Trait> { + /** Trait scope. */ + private scope: Scope; + + // Configuration options + private fieldNum: number; + private methodDeclarationsNum: number; + private constantNum: number; + + // Declarations to be defined within contracts/traits that implement this trait. + public fieldDeclarations: Field[] = []; + public constantDeclarations: ConstantDecl[] = []; + public constantDefinitions: ConstantDef[] = []; + public methodDeclarations: FunctionDecl[] = []; + + constructor(programScope: Scope, params: Partial<TraitParameters> = {}) { + const scope = new Scope("trait", programScope); + super( + { kind: "util", type: UtilType.Trait }, + createSample(generateAstId(scope)), + ); + this.scope = scope; + + const { + traitFieldNum = FuzzConfig.traitFieldNum, + traitMethodDeclarationsNum = FuzzConfig.traitMethodDeclarationsNum, + traitConstantNum = FuzzConfig.traitConstantNum, + } = params; + this.fieldNum = traitFieldNum; + this.methodDeclarationsNum = traitMethodDeclarationsNum; + this.constantNum = traitConstantNum; + + this.prepareDeclarationTypes(); + } + + /** + * Randomly generates init expressions for constants. + */ + private makeInit(ty: Type): fc.Arbitrary<Ast.Expression> | undefined { + return ty.kind === "map" || randomBool() + ? undefined + : new Expression(this.scope, ty, { + useIdentifiersInExpressions: false, + }).generate(); + } + + /** + * Generates arbitrary types for fields, methods and constants that will be + * defined in the trait. + */ + private prepareDeclarationTypes() { + this.fieldDeclarations = Array.from({ length: this.fieldNum }).map( + (_) => { + const ty = TypeGen.fromScope(this.scope).pick(); + const field = new Field(this.scope, ty); + this.scope.addNamed("field", field); + return field; + }, + ); + this.methodDeclarations = Array.from({ + length: this.methodDeclarationsNum, + }).map((_) => { + const returnTy = TypeGen.fromScope(this.scope).pick(); + const funTy = makeFunctionTy("method", returnTy); + return new FunctionDecl(this.scope, "method", funTy); + }); + this.constantDeclarations = []; + this.constantDefinitions = []; + Array.from({ length: this.constantNum }).forEach((_) => { + const ty = TypeGen.fromScope(this.scope).pick(); + const init = this.makeInit(ty); + if (init) + this.constantDefinitions.push( + ConstantDef.fromScope(this.scope, ty, init), + ); + else + this.constantDeclarations.push( + new ConstantDecl(this.scope, ty), + ); + }); + } + + public generate(): fc.Arbitrary<Ast.Trait> { + // NOTE: It doesn't implement any receive functions, to don't clutter the top-level with them. + const constants = ( + this.constantDeclarations as (ConstantDecl | ConstantDef)[] + ) + .concat(this.constantDefinitions) + .map((c) => c.generate()); + const fields = this.fieldDeclarations.map((f) => f.generate()); + const methods = this.methodDeclarations.map((m) => m.generate()); + return fc + .tuple(...constants, ...fields, ...methods) + .map((decl) => + GlobalContext.makeF.makeDummyTrait(this.name, [], [], decl), + ); + } +} diff --git a/src/test/fuzzer/src/generators/uniform-expr-gen.ts b/src/test/fuzzer/src/generators/uniform-expr-gen.ts new file mode 100644 index 0000000000..cefd586577 --- /dev/null +++ b/src/test/fuzzer/src/generators/uniform-expr-gen.ts @@ -0,0 +1,1825 @@ +import type * as Ast from "@/ast/ast"; +import type { FactoryAst } from "@/ast/ast-helpers"; +import { getMakeAst } from "@/ast/generated/make-factory"; +import { getAstUtil } from "@/ast/util"; +import { Interpreter } from "@/optimizer/interpreter"; +import { GlobalContext } from "@/test/fuzzer/src/context"; +import type { Scope } from "@/test/fuzzer/src/scope"; +import { StdlibType } from "@/test/fuzzer/src/types"; +import type { Type } from "@/test/fuzzer/src/types"; +import { stringify } from "@/test/fuzzer/src/util"; +import { beginCell } from "@ton/core"; +import type { Address, Cell } from "@ton/core"; +import { sha256_sync } from "@ton/crypto"; +import { TreasuryContract } from "@ton/sandbox"; +import * as fc from "fast-check"; + +/*export const AllowedType = { + Int: "Int", + OptInt: "Int?", + Bool: "Bool", + OptBool: "Bool?", + Cell: "Cell", + OptCell: "Cell?", + Slice: "Slice", + OptSlice: "Slice?", + Address: "Address", + OptAddress: "Address?", + String: "String", + OptString: "String?", +} as const; + +export type AllowedTypeEnum = (typeof AllowedType)[keyof typeof AllowedType]; +*/ + +export type GenInitConfig = { + // The minimum expression size + minSize: number; + + // The maximum expression size + maxSize: number; + + // The non-terminals to choose from. Non-terminals not listed here will + // be disallowed during generation + allowedNonTerminals: NonTerminalEnum[]; + + // The terminals to choose from. Terminals not listed here will + // be disallowed during generation + allowedTerminals: TerminalEnum[]; + + useIdentifiers: boolean; +}; + +export const NonTerminal = { + Initial: { terminal: false, literal: false, id: 0 }, + Int: { terminal: false, literal: false, id: 1 }, + OptInt: { terminal: false, literal: false, id: 2 }, + LiteralInt: { terminal: false, literal: true, id: 3 }, + // LiteralOptInt: { terminal: false, literal: true, id: 4 }, + Bool: { terminal: false, literal: false, id: 4 }, + OptBool: { terminal: false, literal: false, id: 5 }, + LiteralBool: { terminal: false, literal: true, id: 6 }, + // LiteralOptBool: { terminal: false, literal: true, id: 8 }, + Cell: { terminal: false, literal: false, id: 7 }, + OptCell: { terminal: false, literal: false, id: 8 }, + LiteralCell: { terminal: false, literal: true, id: 9 }, + // LiteralOptCell: { terminal: false, literal: true, id: 12 }, + Slice: { terminal: false, literal: false, id: 10 }, + OptSlice: { terminal: false, literal: false, id: 11 }, + LiteralSlice: { terminal: false, literal: true, id: 12 }, + // LiteralOptSlice: { terminal: false, literal: true, id: 16 }, + Address: { terminal: false, literal: false, id: 13 }, + OptAddress: { terminal: false, literal: false, id: 14 }, + LiteralAddress: { terminal: false, literal: true, id: 15 }, + // LiteralOptAddress: { terminal: false, literal: true, id: 20 }, + String: { terminal: false, literal: false, id: 16 }, + OptString: { terminal: false, literal: false, id: 17 }, + LiteralString: { terminal: false, literal: true, id: 18 }, + // LiteralOptString: { terminal: false, literal: true, id: 24 }, +} as const; + +export type NonTerminalEnum = (typeof NonTerminal)[keyof typeof NonTerminal]; + +type GenericNonTerminal = { + id: number; + literal: boolean; + terminal: false; +}; + +export const Terminal = { + integer: { terminal: true, id: 1 }, + add: { terminal: true, id: 2 }, + minus: { terminal: true, id: 3 }, + mult: { terminal: true, id: 4 }, + div: { terminal: true, id: 5 }, + mod: { terminal: true, id: 6 }, + shift_r: { terminal: true, id: 7 }, + shift_l: { terminal: true, id: 8 }, + bit_and: { terminal: true, id: 9 }, + bit_or: { terminal: true, id: 10 }, + bit_xor: { terminal: true, id: 11 }, + // unary_plus: { terminal: true, id: 12 }, + unary_minus: { terminal: true, id: 12 }, + bit_not: { terminal: true, id: 13 }, + + bool: { terminal: true, id: 14 }, + eq: { terminal: true, id: 15 }, + neq: { terminal: true, id: 16 }, + lt: { terminal: true, id: 17 }, + le: { terminal: true, id: 18 }, + gt: { terminal: true, id: 19 }, + ge: { terminal: true, id: 20 }, + and: { terminal: true, id: 21 }, + or: { terminal: true, id: 22 }, + not: { terminal: true, id: 23 }, + + cell: { terminal: true, id: 24 }, + //code_of: { terminal: true, id: 25 }, + + slice: { terminal: true, id: 25 }, + + address: { terminal: true, id: 26 }, + + string: { terminal: true, id: 27 }, + + // opt_inj: { terminal: true, id: 30 }, + // null: { terminal: true, id: 30 }, + non_null_assert: { terminal: true, id: 28 }, + + cond: { terminal: true, id: 29 }, + + id_int: { terminal: true, id: 30 }, + id_opt_int: { terminal: true, id: 31 }, + id_bool: { terminal: true, id: 32 }, + id_opt_bool: { terminal: true, id: 33 }, + id_cell: { terminal: true, id: 34 }, + id_opt_cell: { terminal: true, id: 35 }, + id_slice: { terminal: true, id: 36 }, + id_opt_slice: { terminal: true, id: 37 }, + id_address: { terminal: true, id: 38 }, + id_opt_address: { terminal: true, id: 39 }, + id_string: { terminal: true, id: 40 }, + id_opt_string: { terminal: true, id: 41 }, +} as const; + +export type TerminalEnum = (typeof Terminal)[keyof typeof Terminal]; + +type Token = TerminalEnum | GenericNonTerminal; + +type ExprProduction = { + tokens: Token[]; + id: number; +}; + +const allProductions: ExprProduction[][] = [ + [ + // Productions for Initial + { id: 0, tokens: [NonTerminal.Int] }, + { id: 1, tokens: [NonTerminal.OptInt] }, + { id: 2, tokens: [NonTerminal.LiteralInt] }, + // { id: 3, tokens: [NonTerminal.LiteralOptInt] }, + { id: 3, tokens: [NonTerminal.Bool] }, + { id: 4, tokens: [NonTerminal.OptBool] }, + { id: 5, tokens: [NonTerminal.LiteralBool] }, + // { id: 7, tokens: [NonTerminal.LiteralOptBool] }, + { id: 6, tokens: [NonTerminal.Cell] }, + { id: 7, tokens: [NonTerminal.OptCell] }, + { id: 8, tokens: [NonTerminal.LiteralCell] }, + // { id: 11, tokens: [NonTerminal.LiteralOptCell] }, + { id: 9, tokens: [NonTerminal.Slice] }, + { id: 10, tokens: [NonTerminal.OptSlice] }, + { id: 11, tokens: [NonTerminal.LiteralSlice] }, + // { id: 15, tokens: [NonTerminal.LiteralOptSlice] }, + { id: 12, tokens: [NonTerminal.Address] }, + { id: 13, tokens: [NonTerminal.OptAddress] }, + { id: 14, tokens: [NonTerminal.LiteralAddress] }, + // { id: 19, tokens: [NonTerminal.LiteralOptAddress] }, + { id: 15, tokens: [NonTerminal.String] }, + { id: 16, tokens: [NonTerminal.LiteralString] }, + { id: 17, tokens: [NonTerminal.OptString] }, + // { id: 23, tokens: [NonTerminal.LiteralOptString] }, + ], + [ + // Productions for Int + { id: 0, tokens: [Terminal.add, NonTerminal.Int, NonTerminal.Int] }, + { + id: 1, + tokens: [Terminal.minus, NonTerminal.Int, NonTerminal.Int], + }, + { id: 2, tokens: [Terminal.mult, NonTerminal.Int, NonTerminal.Int] }, + { id: 3, tokens: [Terminal.div, NonTerminal.Int, NonTerminal.Int] }, + { id: 4, tokens: [Terminal.mod, NonTerminal.Int, NonTerminal.Int] }, + { + id: 5, + tokens: [Terminal.shift_r, NonTerminal.Int, NonTerminal.Int], + }, + { + id: 6, + tokens: [Terminal.shift_l, NonTerminal.Int, NonTerminal.Int], + }, + { + id: 7, + tokens: [Terminal.bit_and, NonTerminal.Int, NonTerminal.Int], + }, + { + id: 8, + tokens: [Terminal.bit_or, NonTerminal.Int, NonTerminal.Int], + }, + { + id: 9, + tokens: [Terminal.bit_xor, NonTerminal.Int, NonTerminal.Int], + }, + // { id: 10, tokens: [Terminal.unary_plus, NonTerminal.Int] }, + { id: 10, tokens: [Terminal.unary_minus, NonTerminal.Int] }, + { id: 11, tokens: [Terminal.bit_not, NonTerminal.Int] }, + + { id: 12, tokens: [Terminal.non_null_assert, NonTerminal.OptInt] }, + { + id: 13, + tokens: [ + Terminal.cond, + NonTerminal.Bool, + NonTerminal.Int, + NonTerminal.Int, + ], + }, + { id: 14, tokens: [Terminal.id_int] }, + { id: 15, tokens: [NonTerminal.LiteralInt] }, + ], + [ + // Productions for OptInt + // { id: 0, tokens: [Terminal.opt_inj, NonTerminal.Int] }, + { + id: 0, + tokens: [ + Terminal.cond, + NonTerminal.Bool, + NonTerminal.OptInt, + NonTerminal.OptInt, + ], + }, + { id: 1, tokens: [Terminal.id_opt_int] }, + // { id: 2, tokens: [NonTerminal.LiteralOptInt] }, + ], + [ + // Productions for LiteralInt + { id: 0, tokens: [Terminal.integer] }, + ], + // [ + // // Productions for LiteralOptInt + // // { id: 0, tokens: [Terminal.null] }, + // // { id: 1, tokens: [Terminal.opt_inj, NonTerminal.LiteralInt] }, + // ], + [ + // Productions for Bool + { id: 0, tokens: [Terminal.eq, NonTerminal.Int, NonTerminal.Int] }, + { + id: 1, + tokens: [Terminal.eq, NonTerminal.OptInt, NonTerminal.OptInt], + }, + { id: 2, tokens: [Terminal.eq, NonTerminal.Bool, NonTerminal.Bool] }, + { + id: 3, + tokens: [Terminal.eq, NonTerminal.OptBool, NonTerminal.OptBool], + }, + { + id: 4, + tokens: [Terminal.eq, NonTerminal.Address, NonTerminal.Address], + }, + { + id: 5, + tokens: [ + Terminal.eq, + NonTerminal.OptAddress, + NonTerminal.OptAddress, + ], + }, + { id: 6, tokens: [Terminal.eq, NonTerminal.Cell, NonTerminal.Cell] }, + { + id: 7, + tokens: [Terminal.eq, NonTerminal.OptCell, NonTerminal.OptCell], + }, + { + id: 8, + tokens: [Terminal.eq, NonTerminal.Slice, NonTerminal.Slice], + }, + { + id: 9, + tokens: [Terminal.eq, NonTerminal.OptSlice, NonTerminal.OptSlice], + }, + { + id: 10, + tokens: [Terminal.eq, NonTerminal.String, NonTerminal.String], + }, + { + id: 11, + tokens: [Terminal.eq, NonTerminal.OptString, NonTerminal.OptString], + }, + + { id: 12, tokens: [Terminal.neq, NonTerminal.Int, NonTerminal.Int] }, + { + id: 13, + tokens: [Terminal.neq, NonTerminal.OptInt, NonTerminal.OptInt], + }, + { + id: 14, + tokens: [Terminal.neq, NonTerminal.Bool, NonTerminal.Bool], + }, + { + id: 15, + tokens: [Terminal.neq, NonTerminal.OptBool, NonTerminal.OptBool], + }, + { + id: 16, + tokens: [Terminal.neq, NonTerminal.Address, NonTerminal.Address], + }, + { + id: 17, + tokens: [ + Terminal.neq, + NonTerminal.OptAddress, + NonTerminal.OptAddress, + ], + }, + { + id: 18, + tokens: [Terminal.neq, NonTerminal.Cell, NonTerminal.Cell], + }, + { + id: 19, + tokens: [Terminal.neq, NonTerminal.OptCell, NonTerminal.OptCell], + }, + { + id: 20, + tokens: [Terminal.neq, NonTerminal.Slice, NonTerminal.Slice], + }, + { + id: 21, + tokens: [Terminal.neq, NonTerminal.OptSlice, NonTerminal.OptSlice], + }, + { + id: 22, + tokens: [Terminal.neq, NonTerminal.String, NonTerminal.String], + }, + { + id: 23, + tokens: [ + Terminal.neq, + NonTerminal.OptString, + NonTerminal.OptString, + ], + }, + + { id: 24, tokens: [Terminal.lt, NonTerminal.Int, NonTerminal.Int] }, + { id: 25, tokens: [Terminal.le, NonTerminal.Int, NonTerminal.Int] }, + { id: 26, tokens: [Terminal.gt, NonTerminal.Int, NonTerminal.Int] }, + { id: 27, tokens: [Terminal.ge, NonTerminal.Int, NonTerminal.Int] }, + { + id: 28, + tokens: [Terminal.and, NonTerminal.Bool, NonTerminal.Bool], + }, + { + id: 29, + tokens: [Terminal.or, NonTerminal.Bool, NonTerminal.Bool], + }, + { id: 30, tokens: [Terminal.not, NonTerminal.Bool] }, + + { id: 31, tokens: [Terminal.non_null_assert, NonTerminal.OptBool] }, + { + id: 32, + tokens: [ + Terminal.cond, + NonTerminal.Bool, + NonTerminal.Bool, + NonTerminal.Bool, + ], + }, + { id: 33, tokens: [Terminal.id_bool] }, + { id: 34, tokens: [NonTerminal.LiteralBool] }, + ], + [ + // Productions for OptBool + // { id: 0, tokens: [Terminal.opt_inj, NonTerminal.Bool] }, + { + id: 0, + tokens: [ + Terminal.cond, + NonTerminal.Bool, + NonTerminal.OptBool, + NonTerminal.OptBool, + ], + }, + { id: 1, tokens: [Terminal.id_opt_bool] }, + // { id: 3, tokens: [NonTerminal.LiteralOptBool] }, + ], + [ + // Productions for LiteralBool + { id: 0, tokens: [Terminal.bool] }, + ], + // [ + // // Productions for LiteralOptBool + // // { id: 0, tokens: [Terminal.null] }, + // // { id: 1, tokens: [Terminal.opt_inj, NonTerminal.LiteralBool] }, + // ], + [ + // Productions for Cell + //{ id: 0, tokens: [Terminal.code_of] }, + { id: 0, tokens: [Terminal.non_null_assert, NonTerminal.OptCell] }, + { + id: 1, + tokens: [ + Terminal.cond, + NonTerminal.Bool, + NonTerminal.Cell, + NonTerminal.Cell, + ], + }, + { id: 2, tokens: [Terminal.id_cell] }, + { id: 3, tokens: [NonTerminal.LiteralCell] }, + ], + [ + // Productions for OptCell + // { id: 0, tokens: [Terminal.opt_inj, NonTerminal.Cell] }, + { + id: 0, + tokens: [ + Terminal.cond, + NonTerminal.Bool, + NonTerminal.OptCell, + NonTerminal.OptCell, + ], + }, + { id: 1, tokens: [Terminal.id_opt_cell] }, + // { id: 2, tokens: [NonTerminal.LiteralOptCell] }, + ], + [ + // Productions for LiteralCell + { id: 0, tokens: [Terminal.cell] }, + ], + // [ + // // Productions for LiteralOptCell + // // { id: 0, tokens: [Terminal.null] }, + // // { id: 1, tokens: [Terminal.opt_inj, NonTerminal.LiteralCell] }, + // ], + [ + // Productions for Slice + { id: 0, tokens: [Terminal.non_null_assert, NonTerminal.OptSlice] }, + { + id: 1, + tokens: [ + Terminal.cond, + NonTerminal.Bool, + NonTerminal.Slice, + NonTerminal.Slice, + ], + }, + { id: 2, tokens: [Terminal.id_slice] }, + { id: 3, tokens: [NonTerminal.LiteralSlice] }, + ], + [ + // Productions for OptSlice + // { id: 0, tokens: [Terminal.opt_inj, NonTerminal.Slice] }, + { + id: 0, + tokens: [ + Terminal.cond, + NonTerminal.Bool, + NonTerminal.OptSlice, + NonTerminal.OptSlice, + ], + }, + { id: 1, tokens: [Terminal.id_opt_slice] }, + // { id: 2, tokens: [NonTerminal.LiteralOptSlice] }, + ], + [ + // Productions for LiteralSlice + { id: 0, tokens: [Terminal.slice] }, + ], + // [ + // // Productions for LiteralOptSlice + // // { id: 0, tokens: [Terminal.null] }, + // // { id: 1, tokens: [Terminal.opt_inj, NonTerminal.LiteralSlice] }, + // ], + [ + // Productions for Address + { + id: 0, + tokens: [Terminal.non_null_assert, NonTerminal.OptAddress], + }, + { + id: 1, + tokens: [ + Terminal.cond, + NonTerminal.Bool, + NonTerminal.Address, + NonTerminal.Address, + ], + }, + { id: 2, tokens: [Terminal.id_address] }, + { id: 3, tokens: [NonTerminal.LiteralAddress] }, + ], + [ + // Productions for OptAddress + // { id: 0, tokens: [Terminal.opt_inj, NonTerminal.Address] }, + { + id: 0, + tokens: [ + Terminal.cond, + NonTerminal.Bool, + NonTerminal.OptAddress, + NonTerminal.OptAddress, + ], + }, + { id: 1, tokens: [Terminal.id_opt_address] }, + // { id: 3, tokens: [NonTerminal.LiteralOptAddress] }, + ], + [ + // Productions for LiteralAddress + { id: 0, tokens: [Terminal.address] }, + ], + // [ + // // Productions for LiteralOptAddress + // // { id: 0, tokens: [Terminal.null] }, + // // { id: 1, tokens: [Terminal.opt_inj, NonTerminal.LiteralAddress] }, + // ], + [ + // Productions for String + { id: 0, tokens: [Terminal.non_null_assert, NonTerminal.OptString] }, + { + id: 1, + tokens: [ + Terminal.cond, + NonTerminal.Bool, + NonTerminal.String, + NonTerminal.String, + ], + }, + { id: 2, tokens: [Terminal.id_string] }, + { id: 3, tokens: [NonTerminal.LiteralString] }, + ], + [ + // Productions for OptString + // { id: 0, tokens: [Terminal.opt_inj, NonTerminal.String] }, + { + id: 0, + tokens: [ + Terminal.cond, + NonTerminal.Bool, + NonTerminal.OptString, + NonTerminal.OptString, + ], + }, + { id: 1, tokens: [Terminal.id_opt_string] }, + // { id: 2, tokens: [NonTerminal.LiteralOptString] }, + ], + [ + // Productions for LiteralString + { id: 0, tokens: [Terminal.string] }, + ], + // [ + // // Productions for LiteralOptString + // // { id: 0, tokens: [Terminal.null] }, + // // { id: 1, tokens: [Terminal.opt_inj, NonTerminal.LiteralString] }, + // ], +]; + +function sum(counts: number[]): number { + // If at least one array element (which are counts represented as logarithms) is -Inf, it means that they represent the count 0. + // therefore, they can be filtered out from the array, because they will not affect the final sum + const filteredCounts = counts.filter((n) => n !== Number.NEGATIVE_INFINITY); + + if (filteredCounts.length === 0) { + // The sum would be 0. So, return -Inf + return Number.NEGATIVE_INFINITY; + } + if (filteredCounts.length === 1) { + return filteredCounts[0]!; + } + const first = filteredCounts[0]!; + // For the general case, we reduce the array thanks to the following formula: + // log(x + y) = log x + log(1 + 2^(log y - log x)) + // which tells us how we should add counts when they are represented as logarithms + return filteredCounts + .slice(1) + .reduce( + (prev, curr) => prev + Math.log2(1 + 2 ** (curr - prev)), + first, + ); +} + +function normalizeArray(counts: number[]): number[] { + // Any -Inf represents a count of 0, which means that such index should never get selected. + // So, it is enough to transform -Inf back to 0. + // Any 0 represents a count of 1. Since such index has a non-zero probability to be selected, + // we change the 0s to 1s. + // Also, 1 represents a count of 2. So, we transform 1s to 2s, to avoid + // squashing counts 1s and 2s together. + // The rest of numbers we take their ceil to transform them into integers. + return counts.map((n) => { + if (n === Number.NEGATIVE_INFINITY) { + return 0; + } + if (n === 0) { + return 1; + } + if (n === 1) { + return 2; + } + return Math.ceil(n); + }); +} + +function multiply(first: number, second: number): number { + // If at least one input (which are counts represented as logarithms) is -Inf, it means that they represent the count 0. + // therefore, their multiplication is also 0 (or -Inf in the logarithm representation) + if ( + first === Number.NEGATIVE_INFINITY || + second === Number.NEGATIVE_INFINITY + ) { + return Number.NEGATIVE_INFINITY; + } else { + // To multiply two counts represented as logarithms, it is enough to add their representations + // thanks to this formula: + // log(x * y) = log(x) + log(y) + return first + second; + } +} + +function transform(n: number): number { + // We transform counts into their base-2 logarithmic representation + return Math.log2(n); +} + +function filterProductions(initConfig: GenInitConfig): { + productions: ExprProduction[][]; + nonTerminals: GenericNonTerminal[]; + reindexMap: Map<number, number>; +} { + const nonTerminalIdsToInclude: Set<number> = new Set( + initConfig.allowedNonTerminals.map((e) => e.id), + ); + const terminalIdsToInclude: Set<number> = new Set( + initConfig.allowedTerminals.map((e) => e.id), + ); + + // Make a copy of all the productions + let productions: ExprProduction[][] = []; + for (let i = 0; i < allProductions.length; i++) { + productions[i] = allProductions[i]!.map((prod) => { + return { id: prod.id, tokens: prod.tokens }; + }); + } + + // If flag useIdentifiers is off, remove generation of identifiers + if (!initConfig.useIdentifiers) { + [ + Terminal.id_address, + Terminal.id_bool, + Terminal.id_cell, + Terminal.id_int, + Terminal.id_opt_address, + Terminal.id_opt_bool, + Terminal.id_opt_cell, + Terminal.id_opt_int, + Terminal.id_opt_slice, + Terminal.id_opt_string, + Terminal.id_slice, + Terminal.id_string, + ].forEach((terminal) => { + terminalIdsToInclude.delete(terminal.id); + }); + } + + // Remove productions that use terminals and non-terminals not listed in the allowed lists. + let initialNonTerminalsCount; + do { + initialNonTerminalsCount = nonTerminalIdsToInclude.size; + + for (let i = 0; i < productions.length; i++) { + productions[i] = productions[i]!.filter((prod) => + prod.tokens.every((t) => { + if (t.terminal) { + return terminalIdsToInclude.has(t.id); + } else { + return nonTerminalIdsToInclude.has(t.id); + } + }), + ); + // If non-terminal i has no productions at the end, we need + // to remove i from the final non-terminals + // and go again through the process of removing productions + if (productions[i]!.length === 0) { + nonTerminalIdsToInclude.delete(i); + } + } + } while (initialNonTerminalsCount !== nonTerminalIdsToInclude.size); + + // Remove unused non-terminals, and reindex them + const reindexMap: Map<number, number> = new Map(); + const nonTerminalsFiltered = Object.values(NonTerminal).filter((n) => + nonTerminalIdsToInclude.has(n.id), + ); + nonTerminalsFiltered.forEach((n, newIndex) => { + reindexMap.set(n.id, newIndex); + }); + const nonTerminals = nonTerminalsFiltered.map((n, newIndex) => { + return { id: newIndex, literal: n.literal, terminal: n.terminal }; + }); + + // Remove productions belonging to removed non-terminals. + productions = productions.filter((_, index) => + nonTerminalIdsToInclude.has(index), + ); + + // Reindex all the productions, including non-terminal tokens occurring inside the production + for (let i = 0; i < productions.length; i++) { + productions[i] = productions[i]!.map((prod, newIndex) => { + return { + id: newIndex, + tokens: prod.tokens.map((t) => { + if (t.terminal) { + return t; + } else { + const newIndex = reindexMap.get(t.id); + if (typeof newIndex === "undefined") { + throw new Error( + `Invalid old index ${t.id}: it does not have a re-indexing`, + ); + } + return { + id: newIndex, + literal: t.literal, + terminal: t.terminal, + }; + } + }), + }; + }); + } + + return { + productions, + nonTerminals, + reindexMap, + }; +} + +function computeCountTables( + minSize: number, + maxSize: number, + finalProductions: ExprProduction[][], + nonTerminals: GenericNonTerminal[], +): { + nonTerminalCounts: number[][][]; + sizeSplitCounts: number[][][][][]; + totalCounts: number[][]; +} { + const nonTerminalCounts: number[][][] = []; + const sizeSplitCounts: number[][][][][] = []; + const totalCounts: number[][] = []; + + function updateTotalCounts(idx: number, size: number, count: number) { + if (typeof totalCounts[idx] === "undefined") { + totalCounts[idx] = Array(maxSize + 1); + totalCounts[idx][size] = count; + } else { + totalCounts[idx][size] = count; + } + } + + function updateNonTerminalCounts( + idx: number, + size: number, + counts: number[], + ) { + if (typeof nonTerminalCounts[idx] === "undefined") { + nonTerminalCounts[idx] = Array(maxSize + 1); + nonTerminalCounts[idx][size] = counts; + } else { + nonTerminalCounts[idx][size] = counts; + } + } + + function peekNonTerminalCounts( + idx: number, + size: number, + ): number[] | undefined { + if (typeof nonTerminalCounts[idx] !== "undefined") { + return nonTerminalCounts[idx][size]; + } else { + return undefined; + } + } + + function updateSizeSplitCounts( + nonTerminalIndex: number, + prodIndex: number, + tokenIndex: number, + size: number, + counts: number[], + ) { + const prods = getProductions(finalProductions, nonTerminalIndex); + const prod = getProductionAt(prods, prodIndex); + + if (typeof sizeSplitCounts[nonTerminalIndex] === "undefined") { + sizeSplitCounts[nonTerminalIndex] = Array(prods.length); + sizeSplitCounts[nonTerminalIndex][prodIndex] = Array( + prod.tokens.length, + ); + sizeSplitCounts[nonTerminalIndex][prodIndex][tokenIndex] = Array( + maxSize + 1, + ); + sizeSplitCounts[nonTerminalIndex][prodIndex][tokenIndex][size] = + counts; + return; + } else { + if ( + typeof sizeSplitCounts[nonTerminalIndex][prodIndex] === + "undefined" + ) { + sizeSplitCounts[nonTerminalIndex][prodIndex] = Array( + prod.tokens.length, + ); + sizeSplitCounts[nonTerminalIndex][prodIndex][tokenIndex] = + Array(maxSize + 1); + sizeSplitCounts[nonTerminalIndex][prodIndex][tokenIndex][size] = + counts; + return; + } + if ( + typeof sizeSplitCounts[nonTerminalIndex][prodIndex][ + tokenIndex + ] === "undefined" + ) { + sizeSplitCounts[nonTerminalIndex][prodIndex][tokenIndex] = + Array(maxSize + 1); + sizeSplitCounts[nonTerminalIndex][prodIndex][tokenIndex][size] = + counts; + return; + } + sizeSplitCounts[nonTerminalIndex][prodIndex][tokenIndex][size] = + counts; + } + } + + function peekSizeSplitCounts( + nonTerminalIndex: number, + prodIndex: number, + tokenIndex: number, + size: number, + ): number[] | undefined { + return sizeSplitCounts[nonTerminalIndex]?.[prodIndex]?.[tokenIndex]?.[ + size + ]; + } + + function countFromNonTerminal(id: number, size: number): number[] { + const peekedCounts = peekNonTerminalCounts(id, size); + if (typeof peekedCounts !== "undefined") { + return peekedCounts; + } + const prods = getProductions(finalProductions, id); + return prods.map((prod) => sum(countFromProduction(id, prod, 0, size))); + } + + function countFromProduction( + nonTerminalIndex: number, + production: ExprProduction, + tokenIndex: number, + size: number, + ): number[] { + if (size === 0) { + return []; + } + const peekedCounts = peekSizeSplitCounts( + nonTerminalIndex, + production.id, + tokenIndex, + size, + ); + if (typeof peekedCounts !== "undefined") { + return peekedCounts; + } + const head = getTokenAt(production.tokens, tokenIndex); + if (head.terminal) { + if (tokenIndex === production.tokens.length - 1) { + // i.e., the head is the last symbol in the production + // Transform the values 1 and 0 to whatever representation of counts we are currently using + return size === 1 ? [transform(1)] : [transform(0)]; + } else { + return [ + sum( + countFromProduction( + nonTerminalIndex, + production, + tokenIndex + 1, + size - 1, + ), + ), + ]; + } + } + // head is not a terminal + if (tokenIndex === production.tokens.length - 1) { + // i.e., the head is the last symbol in the production + return [sum(countFromNonTerminal(head.id, size))]; + } else { + const result: number[] = []; + + for ( + let l = 1; + l <= size - production.tokens.length + tokenIndex + 1; + l++ + ) { + const partition1 = sum(countFromNonTerminal(head.id, l)); + const partition2 = sum( + countFromProduction( + nonTerminalIndex, + production, + tokenIndex + 1, + size - l, + ), + ); + result.push(multiply(partition1, partition2)); + } + return result; + } + } + + function doCountsForNonTerminals(nonTerminals: GenericNonTerminal[]) { + // First, compute the counts of all the non-terminals that produce literals + + // The first step is to initialize the tables for size 0 + for (const nonTerminal of nonTerminals) { + const nonTerminalIdx = nonTerminal.id; + + const productions = getProductions( + finalProductions, + nonTerminalIdx, + ); + + // Transform count 0 to whatever representation of counts we are currently using + updateNonTerminalCounts( + nonTerminalIdx, + 0, + productions.map((_) => transform(0)), + ); + + for (const prod of productions) { + for ( + let tokenIndx = 0; + tokenIndx < prod.tokens.length; + tokenIndx++ + ) { + updateSizeSplitCounts( + nonTerminalIdx, + prod.id, + tokenIndx, + 0, + [], + ); + } + } + } + + // Now, for the rest of sizes + for (let size = 1; size <= maxSize; size++) { + for (const nonTerminal of nonTerminals) { + const nonTerminalIdx = nonTerminal.id; + + const productions = getProductions( + finalProductions, + nonTerminalIdx, + ); + + for (const prod of productions) { + for ( + let tokenIndx = 0; + tokenIndx < prod.tokens.length; + tokenIndx++ + ) { + updateSizeSplitCounts( + nonTerminalIdx, + prod.id, + tokenIndx, + size, + countFromProduction( + nonTerminalIdx, + prod, + tokenIndx, + size, + ), + ); + } + } + + updateNonTerminalCounts( + nonTerminalIdx, + size, + countFromNonTerminal(nonTerminalIdx, size), + ); + } + } + } + + function doTotalCounts() { + // From 0 to minSize-1, set counts to 0, since we are not going to choose those sizes + // Transform the count 0 to whatever representation we are currently using + for (let size = 0; size < minSize; size++) { + for (const nonTerminal of nonTerminals) { + updateTotalCounts(nonTerminal.id, size, transform(0)); + } + } + + for (let size = minSize; size <= maxSize; size++) { + for (const nonTerminal of nonTerminals) { + updateTotalCounts( + nonTerminal.id, + size, + sum( + lookupNonTerminalCounts( + nonTerminalCounts, + nonTerminal.id, + size, + ), + ), + ); + } + } + } + + function normalizeCounts() { + // The total counts + for (const nonTerminal of nonTerminals) { + const counts = totalCounts[nonTerminal.id]; + if (typeof counts === "undefined") { + throw new Error(`Index ${nonTerminal.id} out of bounds`); + } + const newCounts = normalizeArray(counts); + totalCounts[nonTerminal.id] = newCounts; + } + + // The non-terminal counts + for (const nonTerminal of nonTerminals) { + for (let size = 0; size <= maxSize; size++) { + const counts = lookupNonTerminalCounts( + nonTerminalCounts, + nonTerminal.id, + size, + ); + const newCounts = normalizeArray(counts); + updateNonTerminalCounts(nonTerminal.id, size, newCounts); + } + } + + // Split size counts + for (const nonTerminal of nonTerminals) { + const nonTerminalIdx = nonTerminal.id; + + const productions = getProductions( + finalProductions, + nonTerminalIdx, + ); + + for (const prod of productions) { + for ( + let tokenIndx = 0; + tokenIndx < prod.tokens.length; + tokenIndx++ + ) { + for (let size = 0; size <= maxSize; size++) { + const counts = lookupSizeSplitCounts( + sizeSplitCounts, + nonTerminal.id, + prod.id, + tokenIndx, + size, + ); + const newCounts = normalizeArray(counts); + updateSizeSplitCounts( + nonTerminal.id, + prod.id, + tokenIndx, + size, + newCounts, + ); + } + } + } + } + } + + // First, fill the non-terminals that compute literals + doCountsForNonTerminals( + nonTerminals.filter((nonTerminal) => nonTerminal.literal), + ); + + // Now the rest of non-terminals + doCountsForNonTerminals( + nonTerminals.filter((nonTerminal) => !nonTerminal.literal), + ); + + doTotalCounts(); + + // Now that the tables are filled, we need to normalize the counts + // into non-negative integers (because they may be currently encoded + // in a different way. For example, when using logarithms to represent + // counts, the numbers have fractional part, and may also include -Inf). + normalizeCounts(); + + return { + nonTerminalCounts, + sizeSplitCounts, + totalCounts, + }; +} + +function lookupSizeSplitCounts( + sizeSplitCounts: number[][][][][], + nonTerminalIndex: number, + prodIndex: number, + tokenIndex: number, + size: number, +): number[] { + const nTCounts = sizeSplitCounts[nonTerminalIndex]; + if (typeof nTCounts === "undefined") { + throw new Error(`Index ${nonTerminalIndex} out of bounds`); + } + const prodCounts = nTCounts[prodIndex]; + if (typeof prodCounts === "undefined") { + throw new Error(`Index ${prodIndex} out of bounds`); + } + const tokenCounts = prodCounts[tokenIndex]; + if (typeof tokenCounts === "undefined") { + throw new Error(`Index ${tokenIndex} out of bounds`); + } + const result = tokenCounts[size]; + if (typeof result === "undefined") { + throw new Error(`Index ${size} out of bounds`); + } + return result; +} + +function lookupNonTerminalCounts( + nonTerminalCounts: number[][][], + nonTerminalIndex: number, + size: number, +): number[] { + const nTCounts = nonTerminalCounts[nonTerminalIndex]; + if (typeof nTCounts === "undefined") { + throw new Error(`Index ${nonTerminalIndex} out of bounds`); + } + const result = nTCounts[size]; + if (typeof result === "undefined") { + throw new Error(`Index ${size} out of bounds`); + } + return result; +} + +function lookupTotalCounts( + totalCounts: number[][], + nonTerminalIndex: number, +): number[] { + const nTCounts = totalCounts[nonTerminalIndex]; + if (typeof nTCounts === "undefined") { + throw new Error(`Index ${nonTerminalIndex} out of bounds`); + } + return nTCounts; +} + +function getProductions( + productions: ExprProduction[][], + idx: number, +): ExprProduction[] { + const prods = productions[idx]; + if (typeof prods === "undefined") { + throw new Error(`${idx} is not a valid id for a non-terminal`); + } + return prods; +} + +function getTokenAt(tokens: Token[], id: number): Token { + const token = tokens[id]; + if (typeof token === "undefined") { + throw new Error(`Index ${id} is out of bounds`); + } + return token; +} + +function getNonTerminalAt(tokens: Token[], id: number): GenericNonTerminal { + const token = getTokenAt(tokens, id); + if (token.terminal) { + throw new Error(`Was expecting a non-terminal`); + } + return token; +} + +function getProductionAt(prods: ExprProduction[], id: number): ExprProduction { + const prod = prods[id]; + if (typeof prod === "undefined") { + throw new Error(`Index ${id} out of bounds in productions array`); + } + return prod; +} + +function makeExpression( + astF: FactoryAst, + nonTerminalId: number, + scope: Scope, + nonTerminalCounts: number[][][], + sizeSplitCounts: number[][][][][], + finalProductions: ExprProduction[][], + size: number, +): fc.Arbitrary<Ast.Expression> { + const makeF = getMakeAst(astF); + const interpreter = new Interpreter(getAstUtil(astF)); + + function genFromNonTerminal( + id: number, + size: number, + ): fc.Arbitrary<Ast.Expression> { + const prods = getProductions(finalProductions, id); + const nonTerminalOptions = lookupNonTerminalCounts( + nonTerminalCounts, + id, + size, + ); + + const weightedNonTerminalOptions: fc.WeightedArbitrary<number>[] = + nonTerminalOptions.map((w, i) => { + return { arbitrary: fc.constant(i), weight: w }; + }); + return fc + .oneof(...weightedNonTerminalOptions) + .chain((chosenProdIndex) => { + const production = getProductionAt(prods, chosenProdIndex); + return genFromProduction(id, production, size); + }); + } + + function genFromProduction( + nonTerminalIndex: number, + production: ExprProduction, + size: number, + ): fc.Arbitrary<Ast.Expression> { + const head = getTokenAt(production.tokens, 0); + if (head.terminal) { + // The production must have the form: N -> head list_of_non_terminals + // where head indicates the kind of tree we need to produce + return makeTree( + nonTerminalIndex, + production.id, + head, + production.tokens.slice(1), + size, + ); + } + // head is not a terminal + // The production must have the form N -> head + return genFromNonTerminal(head.id, size); + } + + function chooseSizeSplit( + nonTerminalIndex: number, + prodIndex: number, + tokenIndex: number, + size: number, + ): fc.Arbitrary<number> { + const sizeSplits = lookupSizeSplitCounts( + sizeSplitCounts, + nonTerminalIndex, + prodIndex, + tokenIndex, + size, + ); + // We need to add 1 to the result because sizes + // in splits are always positive. So, index 0 + // represents size 1, and so on. + const weightedSizeSplits: fc.WeightedArbitrary<number>[] = + sizeSplits.map((w, i) => { + return { arbitrary: fc.constant(i + 1), weight: w }; + }); + return fc.oneof(...weightedSizeSplits); + } + + function handleShiftOperators( + op: Ast.BinaryOperation, + expr: Ast.Expression, + ): fc.Arbitrary<Ast.Expression> { + if (op !== "<<" && op !== ">>") { + return fc.constant(expr); + } + try { + const literal = interpreter.interpretExpression(expr); + if (literal.kind !== "number") { + // Generate an integer in range [0..256] + return fc + .bigInt(0n, 256n) + .map((n) => makeF.makeDummyNumber(10, n)); + } + if (literal.value >= 0n && literal.value <= 256n) { + return fc.constant(expr); + } else { + // Generate an integer in range [0..256] + return fc + .bigInt(0n, 256n) + .map((n) => makeF.makeDummyNumber(10, n)); + } + } catch (_) { + // Any kind of error, leave the expr as is + return fc.constant(expr); + } + } + + function makeBinaryOperatorTree( + op: Ast.BinaryOperation, + nonTerminalIndex: number, + prodIndex: number, + rest: Token[], + size: number, + ): fc.Arbitrary<Ast.Expression> { + const currSize = size - 1; + // Choose a single split for the size + return chooseSizeSplit(nonTerminalIndex, prodIndex, 1, currSize).chain( + (sizeSplit) => { + const leftNonTerminal = getNonTerminalAt(rest, 0); + const rightNonTerminal = getNonTerminalAt(rest, 1); + + return genFromNonTerminal(leftNonTerminal.id, sizeSplit).chain( + (leftOperand) => { + return genFromNonTerminal( + rightNonTerminal.id, + currSize - sizeSplit, + ).chain((rightOperand) => { + // We need special logic to handle the shift operators, because they check + // at compile time if their right-hand side is within the range [0..256]. + return handleShiftOperators(op, rightOperand).map( + (squashedRightOperand) => + makeF.makeDummyOpBinary( + op, + leftOperand, + squashedRightOperand, + ), + ); + }); + }, + ); + }, + ); + } + + function makeUnaryOperatorTree( + op: Ast.UnaryOperation, + rest: Token[], + size: number, + ): fc.Arbitrary<Ast.Expression> { + const currSize = size - 1; + + const operandNonTerminal = getNonTerminalAt(rest, 0); + return genFromNonTerminal(operandNonTerminal.id, currSize).map( + (operand) => makeF.makeDummyOpUnary(op, operand), + ); + } + + function makeIdentifier(ty: Type): fc.Arbitrary<Ast.Expression> { + const names = scope.getNamesRecursive("let", ty); + if (names.length === 0) { + throw new Error( + `There must exist at least one identifier for type ${stringify(ty, 0)}`, + ); + } + return fc.constantFrom(...names).map((id) => makeF.makeDummyId(id)); + } + + function makeTree( + nonTerminalIndex: number, + prodIndex: number, + head: TerminalEnum, + rest: Token[], + size: number, + ): fc.Arbitrary<Ast.Expression> { + switch (head.id) { + case Terminal.integer.id: { + return _generateIntBitLength(257, true).map((i) => + makeF.makeDummyNumber(10, i), + ); + } + case Terminal.add.id: { + return makeBinaryOperatorTree( + "+", + nonTerminalIndex, + prodIndex, + rest, + size, + ); + } + case Terminal.minus.id: { + return makeBinaryOperatorTree( + "-", + nonTerminalIndex, + prodIndex, + rest, + size, + ); + } + case Terminal.mult.id: { + return makeBinaryOperatorTree( + "*", + nonTerminalIndex, + prodIndex, + rest, + size, + ); + } + case Terminal.div.id: { + return makeBinaryOperatorTree( + "/", + nonTerminalIndex, + prodIndex, + rest, + size, + ); + } + case Terminal.mod.id: { + return makeBinaryOperatorTree( + "%", + nonTerminalIndex, + prodIndex, + rest, + size, + ); + } + case Terminal.shift_r.id: { + return makeBinaryOperatorTree( + ">>", + nonTerminalIndex, + prodIndex, + rest, + size, + ); + } + case Terminal.shift_l.id: { + return makeBinaryOperatorTree( + "<<", + nonTerminalIndex, + prodIndex, + rest, + size, + ); + } + case Terminal.bit_and.id: { + return makeBinaryOperatorTree( + "&", + nonTerminalIndex, + prodIndex, + rest, + size, + ); + } + case Terminal.bit_or.id: { + return makeBinaryOperatorTree( + "|", + nonTerminalIndex, + prodIndex, + rest, + size, + ); + } + case Terminal.bit_xor.id: { + return makeBinaryOperatorTree( + "^", + nonTerminalIndex, + prodIndex, + rest, + size, + ); + } + // case Terminal.unary_plus.id: { + // return makeUnaryOperatorTree("+", rest, size); + // } + case Terminal.unary_minus.id: { + return makeUnaryOperatorTree("-", rest, size); + } + case Terminal.bit_not.id: { + return makeUnaryOperatorTree("~", rest, size); + } + case Terminal.bool.id: { + return fc.boolean().map((b) => makeF.makeDummyBoolean(b)); + } + case Terminal.eq.id: { + return makeBinaryOperatorTree( + "==", + nonTerminalIndex, + prodIndex, + rest, + size, + ); + } + case Terminal.neq.id: { + return makeBinaryOperatorTree( + "!=", + nonTerminalIndex, + prodIndex, + rest, + size, + ); + } + case Terminal.lt.id: { + return makeBinaryOperatorTree( + "<", + nonTerminalIndex, + prodIndex, + rest, + size, + ); + } + case Terminal.le.id: { + return makeBinaryOperatorTree( + "<=", + nonTerminalIndex, + prodIndex, + rest, + size, + ); + } + case Terminal.gt.id: { + return makeBinaryOperatorTree( + ">", + nonTerminalIndex, + prodIndex, + rest, + size, + ); + } + case Terminal.ge.id: { + return makeBinaryOperatorTree( + ">=", + nonTerminalIndex, + prodIndex, + rest, + size, + ); + } + case Terminal.and.id: { + return makeBinaryOperatorTree( + "&&", + nonTerminalIndex, + prodIndex, + rest, + size, + ); + } + case Terminal.or.id: { + return makeBinaryOperatorTree( + "||", + nonTerminalIndex, + prodIndex, + rest, + size, + ); + } + case Terminal.not.id: { + return makeUnaryOperatorTree("!", rest, size); + } + case Terminal.cell.id: { + return _generateCell().map((c) => makeF.makeDummyCell(c)); + } + //case Terminal.code_of.id: { + // if (ctx.contractNames.length === 0) { + // throw new Error( + // "There must exist at least one contract name in generator context", + // ); + // } + // return fc + // .constantFrom(...ctx.contractNames) + // .map((name) => + // makeF.makeDummyCodeOf(makeF.makeDummyId(name)), + // ); + //} + case Terminal.slice.id: { + return _generateCell().map((c) => + makeF.makeDummySlice(c.asSlice()), + ); + } + case Terminal.address.id: { + return _generateAddress().map((a) => makeF.makeDummyAddress(a)); + } + case Terminal.string.id: { + return fc.string().map((s) => makeF.makeDummyString(s)); + } + // case Terminal.opt_inj.id: { + // const currSize = size - 1; + // const operandNonTerminal = getNonTerminalAt(rest, 0); + // return genFromNonTerminal(operandNonTerminal.id, currSize); + // } + // case Terminal.null.id: { + // return fc.constant(makeF.makeDummyNull()); + // } + case Terminal.non_null_assert.id: { + return makeUnaryOperatorTree("!!", rest, size); + } + case Terminal.cond.id: { + const currSize = size - 1; + // Choose two splits for the size + return chooseSizeSplit( + nonTerminalIndex, + prodIndex, + 1, + currSize, + ).chain((sizeSplit1) => { + return chooseSizeSplit( + nonTerminalIndex, + prodIndex, + 2, + currSize - sizeSplit1, + ).chain((sizeSplit2) => { + const condNonTerminal = getNonTerminalAt(rest, 0); + const thenNonTerminal = getNonTerminalAt(rest, 1); + const elseNonTerminal = getNonTerminalAt(rest, 2); + + return genFromNonTerminal( + condNonTerminal.id, + sizeSplit1, + ).chain((condOperand) => { + return genFromNonTerminal( + thenNonTerminal.id, + sizeSplit2, + ).chain((thenOperand) => { + return genFromNonTerminal( + elseNonTerminal.id, + currSize - sizeSplit1 - sizeSplit2, + ).map((elseOperand) => + makeF.makeDummyConditional( + condOperand, + thenOperand, + elseOperand, + ), + ); + }); + }); + }); + }); + } + case Terminal.id_int.id: { + return makeIdentifier({ kind: "stdlib", type: StdlibType.Int }); + } + case Terminal.id_opt_int.id: { + return makeIdentifier({ + kind: "optional", + type: { kind: "stdlib", type: StdlibType.Int }, + }); + } + case Terminal.id_bool.id: { + return makeIdentifier({ + kind: "stdlib", + type: StdlibType.Bool, + }); + } + case Terminal.id_opt_bool.id: { + return makeIdentifier({ + kind: "optional", + type: { kind: "stdlib", type: StdlibType.Bool }, + }); + } + case Terminal.id_cell.id: { + return makeIdentifier({ + kind: "stdlib", + type: StdlibType.Cell, + }); + } + case Terminal.id_opt_cell.id: { + return makeIdentifier({ + kind: "optional", + type: { kind: "stdlib", type: StdlibType.Cell }, + }); + } + case Terminal.id_slice.id: { + return makeIdentifier({ + kind: "stdlib", + type: StdlibType.Slice, + }); + } + case Terminal.id_opt_slice.id: { + return makeIdentifier({ + kind: "optional", + type: { kind: "stdlib", type: StdlibType.Slice }, + }); + } + case Terminal.id_address.id: { + return makeIdentifier({ + kind: "stdlib", + type: StdlibType.Address, + }); + } + case Terminal.id_opt_address.id: { + return makeIdentifier({ + kind: "optional", + type: { kind: "stdlib", type: StdlibType.Address }, + }); + } + case Terminal.id_string.id: { + return makeIdentifier({ + kind: "stdlib", + type: StdlibType.String, + }); + } + case Terminal.id_opt_string.id: { + return makeIdentifier({ + kind: "optional", + type: { kind: "stdlib", type: StdlibType.String }, + }); + } + } + } + + return genFromNonTerminal(nonTerminalId, size); +} + +export function initializeGenerator( + initConfig: GenInitConfig, +): ( + scope: Scope, + nonTerminal: NonTerminalEnum, +) => fc.Arbitrary<Ast.Expression> { + const { productions, nonTerminals, reindexMap } = + filterProductions(initConfig); + + const { nonTerminalCounts, sizeSplitCounts, totalCounts } = + computeCountTables( + initConfig.minSize, + initConfig.maxSize, + productions, + nonTerminals, + ); + + return (scope: Scope, nonTerminal: NonTerminalEnum) => { + const nonTerminalId = reindexMap.get(nonTerminal.id); + if (typeof nonTerminalId === "undefined") { + throw new Error( + `Non-terminal ${nonTerminal.id} does not have a re-indexing`, + ); + } + + if (nonTerminals.every((n) => n.id !== nonTerminalId)) { + throw new Error( + `Non-terminal ${nonTerminalId} is not among the allowed non-terminals`, + ); + } + const sizes = lookupTotalCounts(totalCounts, nonTerminalId); + if (sizes.every((s) => s === 0)) { + throw new Error( + `There are no trees for non-terminal ${nonTerminalId}`, + ); + } + const weightedSizes: fc.WeightedArbitrary<number>[] = sizes.map( + (w, i) => { + return { arbitrary: fc.constant(i), weight: w }; + }, + ); + + return fc.oneof(...weightedSizes).chain((size) => { + return makeExpression( + GlobalContext.astF, + nonTerminalId, + scope, + nonTerminalCounts, + sizeSplitCounts, + productions, + size, + ); + }); + }; +} + +function testSubwalletId(seed: string): bigint { + return BigInt("0x" + sha256_sync("TEST_SEED" + seed).toString("hex")); +} + +function _generateAddress(): fc.Arbitrary<Address> { + return fc.string().map((str) => { + const subwalletId = testSubwalletId(str); + const wallet = TreasuryContract.create(0, subwalletId); + return wallet.address; + }); +} + +function _generateCell(): fc.Arbitrary<Cell> { + return fc.int8Array().map((buf) => { + return beginCell().storeBuffer(Buffer.from(buf.buffer)).endCell(); + }); +} + +function _generateIntBitLength( + bitLength: number, + signed: boolean, +): fc.Arbitrary<bigint> { + const maxUnsigned = (1n << BigInt(bitLength)) - 1n; + + if (signed) { + const minSigned = -maxUnsigned / 2n - 1n; + const maxSigned = maxUnsigned / 2n; + return fc.bigInt(minSigned, maxSigned); + } else { + return fc.bigInt(0n, maxUnsigned); + } +} + +function generateStringValue( + nonEmpty: boolean, + constValue?: string, +): fc.Arbitrary<string> { + return constValue === undefined + ? nonEmpty + ? fc.string({ minLength: 1 }) + : fc.string() + : fc.constantFrom(constValue); +} + +export function generateString( + nonEmpty: boolean, + constValue?: string, +): fc.Arbitrary<Ast.String> { + return generateStringValue(nonEmpty, constValue).map((s) => + GlobalContext.makeF.makeDummyString(s), + ); +} diff --git a/src/test/fuzzer/src/minimal-fc-stdlib/fift-lib/Asm.fif b/src/test/fuzzer/src/minimal-fc-stdlib/fift-lib/Asm.fif new file mode 100644 index 0000000000..976093f809 --- /dev/null +++ b/src/test/fuzzer/src/minimal-fc-stdlib/fift-lib/Asm.fif @@ -0,0 +1,1661 @@ +library TVM_Asm +// simple TVM Assembler +namespace Asm +Asm definitions +"0.4.5" constant asm-fif-version + +variable @atend +variable @was-split +false @was-split ! +{ "not in asm context" abort } @atend ! +{ `normal eq? not abort"must be terminated by }>" } : @normal? +{ context@ @atend @ 2 { @atend ! context! @normal? } does @atend ! } : @pushatend +{ @pushatend Asm <b } : <{ +{ @atend @ execute } : @endblk +{ false @was-split ! `normal @endblk } : }> +{ }> b> } : }>c +{ }>c <s } : }>s +{ @atend @ 2 { true @was-split ! @atend ! rot b> ref, swap @endblk } does @atend ! <b } : @| +{ @atend @ 3 { @atend ! 2swap rot execute } does @atend ! <b } : @doafter<{ +{ over brembits <= } : @havebits +{ rot + -rot + swap } : pair+ +{ rot >= -rot <= and } : 2x<= +{ 2 pick brembitrefs 1- 2x<= } : @havebitrefs +{ @havebits ' @| ifnot } : @ensurebits +{ @havebitrefs ' @| ifnot } : @ensurebitrefs +{ rot over @ensurebits -rot u, } : @simpleuop +{ tuck sbitrefs @ensurebitrefs swap s, } : @addop +{ tuck bbitrefs @ensurebitrefs swap b+ } : @addopb +' @addopb : @inline +{ 1 ' @addop does create } : @Defop +{ 1 { <b swap s, swap 8 u, @addopb } does create } : @Defop(8u) +{ 1 { <b swap s, swap 8 i, @addopb } does create } : @Defop(8i) +{ 1 { <b swap s, swap 1- 8 u, @addopb } does create } : @Defop(8u+1) +{ 1 { <b swap s, swap 4 u, @addopb } does create } : @Defop(4u) +{ 1 { <b swap s, swap 12 u, @addopb } does create } : @Defop(12u) +{ 1 { <b swap s, rot 4 u, swap 4 u, @addopb } does create } : @Defop(4u,4u) +{ 1 { <b swap s, swap ref, @addopb } does create } : @Defop(ref) +{ 1 { <b swap s, rot ref, swap ref, @addopb } does create } : @Defop(ref*2) +{ <b 0xef 8 u, swap 12 i, b> } : si() +// x mi ma -- ? +{ rot tuck >= -rot <= and } : @range +{ rot tuck < -rot > or } : @-range +{ @-range abort"Out of range" } : @rangechk +{ dup 0 < over 255 > or abort"Invalid stack register number" si() } : s() +{ si() constant } : @Sreg +-2 @Sreg s(-2) +-1 @Sreg s(-1) +0 @Sreg s0 +1 @Sreg s1 +2 @Sreg s2 +3 @Sreg s3 +4 @Sreg s4 +5 @Sreg s5 +6 @Sreg s6 +7 @Sreg s7 +8 @Sreg s8 +9 @Sreg s9 +10 @Sreg s10 +11 @Sreg s11 +12 @Sreg s12 +13 @Sreg s13 +14 @Sreg s14 +15 @Sreg s15 +{ dup 0 < over 7 > or abort"Invalid control register number" <b 0xcc 8 u, swap 4 u, b> } : c() +{ c() constant } : @Creg +0 @Creg c0 +1 @Creg c1 +2 @Creg c2 +3 @Creg c3 +4 @Creg c4 +5 @Creg c5 +7 @Creg c7 +{ <s 8 u@+ swap 0xef <> abort"not a stack register" 12 i@+ s> } : @bigsridx +{ @bigsridx dup 16 >= over 0< or abort"stack register s0..s15 expected" } : @sridx +{ rot @bigsridx tuck < -rot tuck > rot or abort"stack register out of range" } : @sridxrange +{ swap @bigsridx + dup 16 >= over 0< or abort"stack register out of range" } : @sridx+ +{ <s 8 u@+ 4 u@+ s> swap 0xcc <> over 7 > or over 6 = or abort"not a control register c0..c5 or c7" } : @cridx +{ <s 8 u@ 0xcc = } : @iscr? +{ <b swap s, 1 { swap @sridx 4 u, @addopb } does create } : @Defop(s) +{ <b swap s, 1 { rot @sridx 4 u, swap @sridx 4 u, @addopb } does create } : @Defop(s,s) +{ <b swap s, 1 { swap @cridx 4 u, @addopb } does create } : @Defop(c) +// +// stack manipulation primitives +// (simple stack primitives) +// +x{00} @Defop NOP +x{01} @Defop SWAP +x{0} @Defop(s) XCHG0 +{ @bigsridx swap @bigsridx 2dup = + { 2drop <b } + { 2dup < { swap } if dup 0= + { drop dup 16 < + { <b x{0} s, swap 4 u, } + { <b x{11} s, swap 8 u, } + cond + } + { over 16 >= + { tuck 16 >= + { <b x{11} s, 2 pick 8 u, x{11} s, swap 8 u, x{11} s, swap 8 u, } + { <b x{0} s, 2 pick 4 u, x{11} s, swap 8 u, x{0} s, swap 4 u, } cond + } + { dup 1 = + { drop <b x{1} s, swap 4 u, } + { <b x{10} s, swap 4 u, swap 4 u, } + cond + } cond + } cond + } cond + @addopb } : XCHG +x{ED4} @Defop(c) PUSHCTR +x{ED5} @Defop(c) POPCTR +{ dup @iscr? + ' PUSHCTR + { @bigsridx dup 16 < + { <b x{2} s, swap 4 u, } + { <b x{56} s, swap 8 u, + } cond + @addopb + } cond +} : PUSH +x{20} @Defop DUP +x{21} @Defop OVER +{ dup @iscr? + ' POPCTR + { @bigsridx dup 16 < + { <b x{3} s, swap 4 u, } + { <b x{57} s, swap 8 u, + } cond + @addopb + } cond +} : POP +x{30} @Defop DROP +x{31} @Defop NIP + +// compound stack primitives +{ @sridx rot @sridx rot @sridx swap <b x{4} s, swap 4 u, swap 4 u, swap 4 u, @addopb } : XCHG3 +x{50} @Defop(s,s) XCHG2 +x{51} @Defop(s,s) XCPU +{ <b x{52} s, rot @sridx 4 u, swap 1 @sridx+ 4 u, @addopb } : PUXC +x{53} @Defop(s,s) PUSH2 +{ @sridx rot @sridx rot @sridx swap <b x{540} s, swap 4 u, swap 4 u, swap 4 u, @addopb } : XCHG3_l +{ @sridx rot @sridx rot @sridx swap <b x{541} s, swap 4 u, swap 4 u, swap 4 u, @addopb } : XC2PU +{ 1 @sridx+ rot @sridx rot @sridx swap <b x{542} s, swap 4 u, swap 4 u, swap 4 u, @addopb } : XCPUXC +{ @sridx rot @sridx rot @sridx swap <b x{543} s, swap 4 u, swap 4 u, swap 4 u, @addopb } : XCPU2 +{ 1 @sridx+ rot @sridx rot 1 @sridx+ swap <b x{544} s, swap 4 u, swap 4 u, swap 4 u, @addopb } : PUXC2 +{ 1 @sridx+ rot @sridx rot 1 @sridx+ swap <b x{545} s, swap 4 u, swap 4 u, swap 4 u, @addopb } : PUXCPU +{ 2 @sridx+ rot @sridx rot 1 @sridx+ swap <b x{546} s, swap 4 u, swap 4 u, swap 4 u, @addopb } : PU2XC +{ @sridx rot @sridx rot @sridx swap <b x{547} s, swap 4 u, swap 4 u, swap 4 u, @addopb } : PUSH3 +{ <b x{55} s, rot 1- 4 u, swap 1- 4 u, @addopb } : BLKSWAP +{ dup { 1 swap BLKSWAP } { drop } cond } : ROLL +{ dup { 1 BLKSWAP } { drop } cond } dup : -ROLL : ROLLREV +x{5513} dup @Defop 2ROT @Defop ROT2 + +// exotic stack primitives +x{58} @Defop ROT +x{59} dup @Defop -ROT @Defop ROTREV +x{5A} dup @Defop 2SWAP @Defop SWAP2 +x{5B} dup @Defop 2DROP @Defop DROP2 +x{5C} dup @Defop 2DUP @Defop DUP2 +x{5D} dup @Defop 2OVER @Defop OVER2 +{ <b x{5E} s, rot 2 - 4 u, swap 4 u, @addopb } : REVERSE +{ <b x{5F0} s, swap 4 u, @addopb } : BLKDROP +{ over 0= abort"first argument must be non-zero" + <b x{5F} s, rot 4 u, swap 4 u, @addopb } : BLKPUSH +x{60} dup @Defop PICK @Defop PUSHX +x{61} @Defop ROLLX +x{62} dup @Defop -ROLLX @Defop ROLLREVX +x{63} @Defop BLKSWX +x{64} @Defop REVX +x{65} @Defop DROPX +x{66} @Defop TUCK +x{67} @Defop XCHGX +x{68} @Defop DEPTH +x{69} @Defop CHKDEPTH +x{6A} @Defop ONLYTOPX +x{6B} @Defop ONLYX +{ over 0= abort"first argument must be non-zero" + <b x{6C} s, rot 4 u, swap 4 u, @addopb } : BLKDROP2 + +// null primitives +x{6D} dup @Defop NULL @Defop PUSHNULL +x{6E} @Defop ISNULL +// tuple primitives +x{6F0} @Defop(4u) TUPLE +x{6F00} @Defop NIL +x{6F01} @Defop SINGLE +x{6F02} dup @Defop PAIR @Defop CONS +x{6F03} @Defop TRIPLE +x{6F1} @Defop(4u) INDEX +x{6F10} dup @Defop FIRST @Defop CAR +x{6F11} dup @Defop SECOND @Defop CDR +x{6F12} @Defop THIRD +x{6F2} @Defop(4u) UNTUPLE +x{6F21} @Defop UNSINGLE +x{6F22} dup @Defop UNPAIR @Defop UNCONS +x{6F23} @Defop UNTRIPLE +x{6F3} @Defop(4u) UNPACKFIRST +x{6F30} @Defop CHKTUPLE +x{6F4} @Defop(4u) EXPLODE +x{6F5} @Defop(4u) SETINDEX +x{6F50} @Defop SETFIRST +x{6F51} @Defop SETSECOND +x{6F52} @Defop SETTHIRD +x{6F6} @Defop(4u) INDEXQ +x{6F60} dup @Defop FIRSTQ @Defop CARQ +x{6F61} dup @Defop SECONDQ @Defop CDRQ +x{6F62} @Defop THIRDQ +x{6F7} @Defop(4u) SETINDEXQ +x{6F70} @Defop SETFIRSTQ +x{6F71} @Defop SETSECONDQ +x{6F72} @Defop SETTHIRDQ +x{6F80} @Defop TUPLEVAR +x{6F81} @Defop INDEXVAR +x{6F82} @Defop UNTUPLEVAR +x{6F83} @Defop UNPACKFIRSTVAR +x{6F84} @Defop EXPLODEVAR +x{6F85} @Defop SETINDEXVAR +x{6F86} @Defop INDEXVARQ +x{6F87} @Defop SETINDEXVARQ +x{6F88} @Defop TLEN +x{6F89} @Defop QTLEN +x{6F8A} @Defop ISTUPLE +x{6F8B} @Defop LAST +x{6F8C} dup @Defop TPUSH @Defop COMMA +x{6F8D} @Defop TPOP +x{6FA0} @Defop NULLSWAPIF +x{6FA1} @Defop NULLSWAPIFNOT +x{6FA2} @Defop NULLROTRIF +x{6FA3} @Defop NULLROTRIFNOT +x{6FA4} @Defop NULLSWAPIF2 +x{6FA5} @Defop NULLSWAPIFNOT2 +x{6FA6} @Defop NULLROTRIF2 +x{6FA7} @Defop NULLROTRIFNOT2 +{ <b x{6FB} s, rot 2 u, swap 2 u, @addopb } : INDEX2 +x{6FB4} @Defop CADR +x{6FB5} @Defop CDDR +{ <b x{6FE_} s, 3 roll 2 u, rot 2 u, swap 2 u, @addopb } : INDEX3 +x{6FD4} @Defop CADDR +x{6FD5} @Defop CDDDR + +// integer constants +x{70} dup @Defop ZERO @Defop FALSE +x{71} @Defop ONE +x{72} @Defop TWO +x{7A} @Defop TEN +x{7F} @Defop TRUE +{ dup 10 <= over -5 >= and + { 15 and <b x{7} s, swap 4 u, } + { dup 8 fits + { <b x{80} s, swap 8 i, } + { dup 16 fits + { <b x{81} s, swap 16 i, } + { 11 { dup 259 > abort"integer too large" 8 + 2dup fits } until + <b x{82} s, over 3 >> 2- 5 u, -rot i, + } cond + } cond + } cond + @addopb } dup : PUSHINT : INT +{ dup 256 = abort"use PUSHNAN instead of 256 PUSHPOW2" <b x{83} s, swap 1- 8 u, @addopb } : PUSHPOW2 +x{83FF} @Defop PUSHNAN +{ <b x{84} s, swap 1- 8 u, @addopb } : PUSHPOW2DEC +{ <b x{85} s, swap 1- 8 u, @addopb } : PUSHNEGPOW2 +// +// other constants +x{88} @Defop(ref) PUSHREF +x{89} @Defop(ref) PUSHREFSLICE +x{8A} @Defop(ref) PUSHREFCONT +{ 1- dup 0< over 8 >= or abort"invalid slice padding" + swap 1 1 u, 0 rot u, } : @scomplete +{ tuck sbitrefs swap 26 + swap @havebitrefs not + { <b rot s, b> PUSHREFSLICE } + { over sbitrefs 2dup 123 0 2x<= + { drop tuck 4 + 3 >> swap x{8B} s, over 4 u, 3 roll s, + -rot 3 << 4 + swap - @scomplete } + { 2dup 1 >= swap 248 <= and + { rot x{8C} s, swap 1- 2 u, over 7 + 3 >> tuck 5 u, 3 roll s, + -rot 3 << 1 + swap - @scomplete } + { rot x{8D} s, swap 3 u, over 2 + 3 >> tuck 7 u, 3 roll s, + -rot 3 << 6 + swap - @scomplete + } cond + } cond + } cond +} dup : PUSHSLICE : SLICE +// ( b' -- ? ) +{ bbitrefs or 0= } : @cont-empty? +{ bbits 7 and 0= } : @cont-aligned? +// ( b b' -- ? ) +{ bbitrefs over 7 and { 2drop drop false } { + swap 16 + swap @havebitrefs nip + } cond +} : @cont-fits? +// ( b b' -- ? ) +{ bbitrefs over 7 and { 2drop drop false } { + 32 1 pair+ @havebitrefs nip + } cond +} : @cont-ref-fit? +// ( b b' b'' -- ? ) +{ over @cont-aligned? over @cont-aligned? and not { 2drop drop false } { + bbitrefs rot bbitrefs pair+ swap 32 + swap @havebitrefs nip + } cond +} : @two-cont-fit? +{ 2dup @cont-fits? not + { b> PUSHREFCONT } + { swap over bbitrefs 2dup 120 0 2x<= + { drop swap x{9} s, swap 3 >> 4 u, swap b+ } + { rot x{8F_} s, swap 2 u, swap 3 >> 7 u, swap b+ } cond + } cond +} dup : PUSHCONT : CONT +{ }> PUSHCONT } : }>CONT +{ { @normal? PUSHCONT } @doafter<{ } : CONT:<{ + +// arithmetic operations +{ 2 { rot dup 8 fits + { nip <b rot s, swap 8 i, } + { rot drop <b swap PUSHINT swap s, } + cond @addopb + } does create +} : @Defop(8i,alt) +x{A0} @Defop ADD +x{A1} @Defop SUB +x{A2} @Defop SUBR +x{A3} @Defop NEGATE +x{A4} @Defop INC +x{A5} @Defop DEC +x{A6} x{A0} @Defop(8i,alt) ADDCONST +{ negate ADDCONST } : SUBCONST +x{A7} x{A8} @Defop(8i,alt) MULCONST +' ADDCONST : ADDINT +' SUBCONST : SUBINT +' MULCONST : MULINT +x{A8} @Defop MUL + +x{A904} @Defop DIV +x{A905} @Defop DIVR +x{A906} @Defop DIVC +x{A908} @Defop MOD +x{A909} @Defop MODR +x{A90A} @Defop MODC +x{A90C} @Defop DIVMOD +x{A90D} @Defop DIVMODR +x{A90E} @Defop DIVMODC +x{A900} @Defop ADDDIVMOD +x{A901} @Defop ADDDIVMODR +x{A902} @Defop ADDDIVMODC + +x{A925} @Defop RSHIFTR +x{A926} @Defop RSHIFTC +x{A928} @Defop MODPOW2 +x{A929} @Defop MODPOW2R +x{A92A} @Defop MODPOW2C +x{A92C} @Defop RSHIFTMOD +x{A92D} @Defop RSHIFTMODR +x{A92E} @Defop RSHIFTMODC +x{A920} @Defop ADDRSHIFTMOD +x{A921} @Defop ADDRSHIFTMODR +x{A922} @Defop ADDRSHIFTMODC + +x{A935} @Defop(8u+1) RSHIFTR# +x{A936} @Defop(8u+1) RSHIFTC# +x{A938} @Defop(8u+1) MODPOW2# +x{A939} @Defop(8u+1) MODPOW2R# +x{A93A} @Defop(8u+1) MODPOW2C# +x{A93C} @Defop(8u+1) RSHIFT#MOD +x{A93D} @Defop(8u+1) RSHIFTR#MOD +x{A93E} @Defop(8u+1) RSHIFTC#MOD +x{A930} @Defop(8u+1) ADDRSHIFT#MOD +x{A931} @Defop(8u+1) ADDRSHIFTR#MOD +x{A932} @Defop(8u+1) ADDRSHIFTC#MOD + +x{A984} @Defop MULDIV +x{A985} @Defop MULDIVR +x{A986} @Defop MULDIVC +x{A988} @Defop MULMOD +x{A989} @Defop MULMODR +x{A98A} @Defop MULMODC +x{A98C} @Defop MULDIVMOD +x{A98D} @Defop MULDIVMODR +x{A98E} @Defop MULDIVMODC +x{A980} @Defop MULADDDIVMOD +x{A981} @Defop MULADDDIVMODR +x{A982} @Defop MULADDDIVMODC + +x{A9A4} @Defop MULRSHIFT +x{A9A5} @Defop MULRSHIFTR +x{A9A6} @Defop MULRSHIFTC +x{A9A8} @Defop MULMODPOW2 +x{A9A9} @Defop MULMODPOW2R +x{A9AA} @Defop MULMODPOW2C +x{A9AC} @Defop MULRSHIFTMOD +x{A9AD} @Defop MULRSHIFTRMOD +x{A9AE} @Defop MULRSHIFTCMOD +x{A9A0} @Defop MULADDRSHIFTMOD +x{A9A1} @Defop MULADDRSHIFTRMOD +x{A9A2} @Defop MULADDRSHIFTCMOD + +x{A9B4} @Defop(8u+1) MULRSHIFT# +x{A9B5} @Defop(8u+1) MULRSHIFTR# +x{A9B6} @Defop(8u+1) MULRSHIFTC# +x{A9B8} @Defop(8u+1) MULMODPOW2# +x{A9B9} @Defop(8u+1) MULMODPOW2R# +x{A9BA} @Defop(8u+1) MULMODPOW2C# +x{A9BC} @Defop(8u+1) MULRSHIFT#MOD +x{A9BD} @Defop(8u+1) MULRSHIFTR#MOD +x{A9BE} @Defop(8u+1) MULRSHIFTC#MOD +x{A9B0} @Defop(8u+1) MULADDRSHIFT#MOD +x{A9B1} @Defop(8u+1) MULADDRSHIFTR#MOD +x{A9B2} @Defop(8u+1) MULADDRSHIFTC#MOD + +x{A9C4} @Defop LSHIFTDIV +x{A9C5} @Defop LSHIFTDIVR +x{A9C6} @Defop LSHIFTDIVC +x{A9C8} @Defop LSHIFTMOD +x{A9C9} @Defop LSHIFTMODR +x{A9CA} @Defop LSHIFTMODC +x{A9CC} @Defop LSHIFTDIVMOD +x{A9CD} @Defop LSHIFTDIVMODR +x{A9CE} @Defop LSHIFTDIVMODC +x{A9C0} @Defop LSHIFTADDDIVMOD +x{A9C1} @Defop LSHIFTADDDIVMODR +x{A9C2} @Defop LSHIFTADDDIVMODC + +x{A9D4} @Defop(8u+1) LSHIFT#DIV +x{A9D5} @Defop(8u+1) LSHIFT#DIVR +x{A9D6} @Defop(8u+1) LSHIFT#DIVC +x{A9D8} @Defop(8u+1) LSHIFT#MOD +x{A9D9} @Defop(8u+1) LSHIFT#MODR +x{A9DA} @Defop(8u+1) LSHIFT#MODC +x{A9DC} @Defop(8u+1) LSHIFT#DIVMOD +x{A9DD} @Defop(8u+1) LSHIFT#DIVMODR +x{A9DE} @Defop(8u+1) LSHIFT#DIVMODC +x{A9D0} @Defop(8u+1) LSHIFT#ADDDIVMOD +x{A9D1} @Defop(8u+1) LSHIFT#ADDDIVMODR +x{A9D2} @Defop(8u+1) LSHIFT#ADDDIVMODC + +x{AA} @Defop(8u+1) LSHIFT# +x{AB} @Defop(8u+1) RSHIFT# +x{AC} @Defop LSHIFT +x{AD} @Defop RSHIFT +x{AE} @Defop POW2 +x{B0} @Defop AND +x{B1} @Defop OR +x{B2} @Defop XOR +x{B3} @Defop NOT +x{B4} @Defop(8u+1) FITS +x{B400} @Defop CHKBOOL +x{B5} @Defop(8u+1) UFITS +x{B500} @Defop CHKBIT +x{B600} @Defop FITSX +x{B601} @Defop UFITSX +x{B602} @Defop BITSIZE +x{B603} @Defop UBITSIZE +x{B608} @Defop MIN +x{B609} @Defop MAX +x{B60A} dup @Defop MINMAX @Defop INTSORT2 +x{B60B} @Defop ABS +x{B7} @Defop QUIET +x{B7A0} @Defop QADD +x{B7A1} @Defop QSUB +x{B7A2} @Defop QSUBR +x{B7A3} @Defop QNEGATE +x{B7A4} @Defop QINC +x{B7A5} @Defop QDEC +x{B7A8} @Defop QMUL + +x{B7A904} @Defop QDIV +x{B7A905} @Defop QDIVR +x{B7A906} @Defop QDIVC +x{B7A908} @Defop QMOD +x{B7A909} @Defop QMODR +x{B7A90A} @Defop QMODC +x{B7A90C} @Defop QDIVMOD +x{B7A90D} @Defop QDIVMODR +x{B7A90E} @Defop QDIVMODC +x{B7A900} @Defop QADDDIVMOD +x{B7A901} @Defop QADDDIVMODR +x{B7A902} @Defop QADDDIVMODC + +x{B7A925} @Defop QRSHIFTR +x{B7A926} @Defop QRSHIFTC +x{B7A928} @Defop QMODPOW2 +x{B7A929} @Defop QMODPOW2R +x{B7A92A} @Defop QMODPOW2C +x{B7A92C} @Defop QRSHIFTMOD +x{B7A92D} @Defop QRSHIFTMODR +x{B7A92E} @Defop QRSHIFTMODC +x{B7A920} @Defop QADDRSHIFTMOD +x{B7A921} @Defop QADDRSHIFTMODR +x{B7A922} @Defop QADDRSHIFTMODC + +x{B7A935} @Defop(8u+1) QRSHIFTR# +x{B7A936} @Defop(8u+1) QRSHIFTC# +x{B7A938} @Defop(8u+1) QMODPOW2# +x{B7A939} @Defop(8u+1) QMODPOW2R# +x{B7A93A} @Defop(8u+1) QMODPOW2C# +x{B7A93C} @Defop(8u+1) QRSHIFT#MOD +x{B7A93D} @Defop(8u+1) QRSHIFTR#MOD +x{B7A93E} @Defop(8u+1) QRSHIFTC#MOD +x{B7A930} @Defop(8u+1) QADDRSHIFT#MOD +x{B7A931} @Defop(8u+1) QADDRSHIFTR#MOD +x{B7A932} @Defop(8u+1) QADDRSHIFTC#MOD + +x{B7A984} @Defop QMULDIV +x{B7A985} @Defop QMULDIVR +x{B7A986} @Defop QMULDIVC +x{B7A988} @Defop QMULMOD +x{B7A989} @Defop QMULMODR +x{B7A98A} @Defop QMULMODC +x{B7A98C} @Defop QMULDIVMOD +x{B7A98D} @Defop QMULDIVMODR +x{B7A98E} @Defop QMULDIVMODC +x{B7A980} @Defop QMULADDDIVMOD +x{B7A981} @Defop QMULADDDIVMODR +x{B7A982} @Defop QMULADDDIVMODC + +x{B7A9A4} @Defop QMULRSHIFT +x{B7A9A5} @Defop QMULRSHIFTR +x{B7A9A6} @Defop QMULRSHIFTC +x{B7A9A8} @Defop QMULMODPOW2 +x{B7A9A9} @Defop QMULMODPOW2R +x{B7A9AA} @Defop QMULMODPOW2C +x{B7A9AC} @Defop QMULRSHIFTMOD +x{B7A9AD} @Defop QMULRSHIFTRMOD +x{B7A9AE} @Defop QMULRSHIFTCMOD +x{B7A9A0} @Defop QMULADDRSHIFTMOD +x{B7A9A1} @Defop QMULADDRSHIFTRMOD +x{B7A9A2} @Defop QMULADDRSHIFTCMOD + +x{B7A9B4} @Defop(8u+1) QMULRSHIFT# +x{B7A9B5} @Defop(8u+1) QMULRSHIFTR# +x{B7A9B6} @Defop(8u+1) QMULRSHIFTC# +x{B7A9B8} @Defop(8u+1) QMULMODPOW2# +x{B7A9B9} @Defop(8u+1) QMULMODPOW2R# +x{B7A9BA} @Defop(8u+1) QMULMODPOW2C# +x{B7A9BC} @Defop(8u+1) QMULRSHIFT#MOD +x{B7A9BD} @Defop(8u+1) QMULRSHIFTR#MOD +x{B7A9BE} @Defop(8u+1) QMULRSHIFTC#MOD +x{B7A9B0} @Defop(8u+1) QMULADDRSHIFT#MOD +x{B7A9B1} @Defop(8u+1) QMULADDRSHIFTR#MOD +x{B7A9B2} @Defop(8u+1) QMULADDRSHIFTC#MOD + +x{B7A9C4} @Defop QLSHIFTDIV +x{B7A9C5} @Defop QLSHIFTDIVR +x{B7A9C6} @Defop QLSHIFTDIVC +x{B7A9C8} @Defop QLSHIFTMOD +x{B7A9C9} @Defop QLSHIFTMODR +x{B7A9CA} @Defop QLSHIFTMODC +x{B7A9CC} @Defop QLSHIFTDIVMOD +x{B7A9CD} @Defop QLSHIFTDIVMODR +x{B7A9CE} @Defop QLSHIFTDIVMODC +x{B7A9C0} @Defop QLSHIFTADDDIVMOD +x{B7A9C1} @Defop QLSHIFTADDDIVMODR +x{B7A9C2} @Defop QLSHIFTADDDIVMODC + +x{B7A9D4} @Defop(8u+1) QLSHIFT#DIV +x{B7A9D5} @Defop(8u+1) QLSHIFT#DIVR +x{B7A9D6} @Defop(8u+1) QLSHIFT#DIVC +x{B7A9D8} @Defop(8u+1) QLSHIFT#MOD +x{B7A9D9} @Defop(8u+1) QLSHIFT#MODR +x{B7A9DA} @Defop(8u+1) QLSHIFT#MODC +x{B7A9DC} @Defop(8u+1) QLSHIFT#DIVMOD +x{B7A9DD} @Defop(8u+1) QLSHIFT#DIVMODR +x{B7A9DE} @Defop(8u+1) QLSHIFT#DIVMODC +x{B7A9D0} @Defop(8u+1) QLSHIFT#ADDDIVMOD +x{B7A9D1} @Defop(8u+1) QLSHIFT#ADDDIVMODR +x{B7A9D2} @Defop(8u+1) QLSHIFT#ADDDIVMODC + +x{B7AC} @Defop QLSHIFT +x{B7AD} @Defop QRSHIFT +x{B7AE} @Defop QPOW2 +x{B7B0} @Defop QAND +x{B7B1} @Defop QOR +x{B7B2} @Defop QXOR +x{B7B3} @Defop QNOT +x{B7B4} @Defop(8u+1) QFITS +x{B7B5} @Defop(8u+1) QUFITS +x{B7B600} @Defop QFITSX +x{B7B601} @Defop QUFITSX + +// advanced integer constants +{ 0 { over 1 and 0= } { 1+ swap 2/ swap } while } : pow2decomp +{ dup 8 fits { PUSHINT } { + dup pow2decomp over 1 = { nip nip PUSHPOW2 } { + over -1 = { nip nip PUSHNEGPOW2 } { + dup 20 >= { rot drop -rot PUSHINT swap LSHIFT# } { + { drop PUSHINT } { + not pow2decomp swap -1 = { nip PUSHPOW2DEC } { + drop PUSHINT + } cond } cond } cond } cond } cond } cond +} dup : PUSHINTX : INTX + +// integer comparison +x{B8} @Defop SGN +x{B9} @Defop LESS +x{BA} @Defop EQUAL +x{BB} @Defop LEQ +x{BC} @Defop GREATER +x{BD} @Defop NEQ +x{BE} @Defop GEQ +x{BF} @Defop CMP +x{C0} x{BA} @Defop(8i,alt) EQINT +x{C000} @Defop ISZERO +x{C1} x{B9} @Defop(8i,alt) LESSINT +{ 1+ LESSINT } : LEQINT +x{C100} @Defop ISNEG +x{C101} @Defop ISNPOS +x{C2} x{BC} @Defop(8i,alt) GTINT +{ 1- GTINT } : GEQINT +x{C200} @Defop ISPOS +x{C2FF} @Defop ISNNEG +x{C3} x{BD} @Defop(8i,alt) NEQINT +x{C300} @Defop ISNZERO +x{C4} @Defop ISNAN +x{C5} @Defop CHKNAN + +// other comparison +x{C700} @Defop SEMPTY +x{C701} @Defop SDEMPTY +x{C702} @Defop SREMPTY +x{C703} @Defop SDFIRST +x{C704} @Defop SDLEXCMP +x{C705} @Defop SDEQ +x{C708} @Defop SDPFX +x{C709} @Defop SDPFXREV +x{C70A} @Defop SDPPFX +x{C70B} @Defop SDPPFXREV +x{C70C} @Defop SDSFX +x{C70D} @Defop SDSFXREV +x{C70E} @Defop SDPSFX +x{C70F} @Defop SDPSFXREV +x{C710} @Defop SDCNTLEAD0 +x{C711} @Defop SDCNTLEAD1 +x{C712} @Defop SDCNTTRAIL0 +x{C713} @Defop SDCNTTRAIL1 + +// cell serialization (Builder manipulation primitives) +x{C8} @Defop NEWC +x{C9} @Defop ENDC +x{CA} @Defop(8u+1) STI +x{CB} @Defop(8u+1) STU +x{CC} @Defop STREF +x{CD} dup @Defop STBREFR @Defop ENDCST +x{CE} @Defop STSLICE +x{CF00} @Defop STIX +x{CF01} @Defop STUX +x{CF02} @Defop STIXR +x{CF03} @Defop STUXR +x{CF04} @Defop STIXQ +x{CF05} @Defop STUXQ +x{CF06} @Defop STIXRQ +x{CF07} @Defop STUXRQ +x{CF08} @Defop(8u+1) STI_l +x{CF09} @Defop(8u+1) STU_l +x{CF0A} @Defop(8u+1) STIR +x{CF0B} @Defop(8u+1) STUR +x{CF0C} @Defop(8u+1) STIQ +x{CF0D} @Defop(8u+1) STUQ +x{CF0E} @Defop(8u+1) STIRQ +x{CF0F} @Defop(8u+1) STURQ +x{CF10} @Defop STREF_l +x{CF11} @Defop STBREF +x{CF12} @Defop STSLICE_l +x{CF13} @Defop STB +x{CF14} @Defop STREFR +x{CF15} @Defop STBREFR_l +x{CF16} @Defop STSLICER +x{CF17} dup @Defop STBR @Defop BCONCAT +x{CF18} @Defop STREFQ +x{CF19} @Defop STBREFQ +x{CF1A} @Defop STSLICEQ +x{CF1B} @Defop STBQ +x{CF1C} @Defop STREFRQ +x{CF1D} @Defop STBREFRQ +x{CF1E} @Defop STSLICERQ +x{CF1F} dup @Defop STBRQ @Defop BCONCATQ +x{CF20} @Defop(ref) STREFCONST +{ <b x{CF21} s, rot ref, swap ref, @addopb } : STREF2CONST +x{CF23} @Defop ENDXC +x{CF28} @Defop STILE4 +x{CF29} @Defop STULE4 +x{CF2A} @Defop STILE8 +x{CF2B} @Defop STULE8 +x{CF30} @Defop BDEPTH +x{CF31} @Defop BBITS +x{CF32} @Defop BREFS +x{CF33} @Defop BBITREFS +x{CF35} @Defop BREMBITS +x{CF36} @Defop BREMREFS +x{CF37} @Defop BREMBITREFS +x{CF38} @Defop(8u+1) BCHKBITS# +x{CF39} @Defop BCHKBITS +x{CF3A} @Defop BCHKREFS +x{CF3B} @Defop BCHKBITREFS +x{CF3C} @Defop(8u+1) BCHKBITSQ# +x{CF3D} @Defop BCHKBITSQ +x{CF3E} @Defop BCHKREFSQ +x{CF3F} @Defop BCHKBITREFSQ +x{CF40} @Defop STZEROES +x{CF41} @Defop STONES +x{CF42} @Defop STSAME +{ tuck sbitrefs swap 22 + swap @havebitrefs not + { swap PUSHSLICE STSLICER } + { over sbitrefs 2dup 57 3 2x<= + { rot x{CFC_} s, swap 2 u, over 6 + 3 >> tuck 3 u, 3 roll s, + -rot 3 << 2 + swap - @scomplete } + { 2drop swap PUSHSLICE STSLICER } cond + } cond +} : STSLICECONST +x{CF81} @Defop STZERO +x{CF83} @Defop STONE + +// cell deserialization (CellSlice primitives) +x{D0} @Defop CTOS +x{D1} @Defop ENDS +x{D2} @Defop(8u+1) LDI +x{D3} @Defop(8u+1) LDU +x{D4} @Defop LDREF +x{D5} @Defop LDREFRTOS +x{D6} @Defop(8u+1) LDSLICE +x{D700} @Defop LDIX +x{D701} @Defop LDUX +x{D702} @Defop PLDIX +x{D703} @Defop PLDUX +x{D704} @Defop LDIXQ +x{D705} @Defop LDUXQ +x{D706} @Defop PLDIXQ +x{D707} @Defop PLDUXQ +x{D708} @Defop(8u+1) LDI_l +x{D709} @Defop(8u+1) LDU_l +x{D70A} @Defop(8u+1) PLDI +x{D70B} @Defop(8u+1) PLDU +x{D70C} @Defop(8u+1) LDIQ +x{D70D} @Defop(8u+1) LDUQ +x{D70E} @Defop(8u+1) PLDIQ +x{D70F} @Defop(8u+1) PLDUQ +{ dup 31 and abort"argument must be a multiple of 32" 5 >> 1- + <b x{D714_} s, swap 3 u, @addopb +} : PLDUZ +x{D718} @Defop LDSLICEX +x{D719} @Defop PLDSLICEX +x{D71A} @Defop LDSLICEXQ +x{D71B} @Defop PLDSLICEXQ +x{D71C} @Defop(8u+1) LDSLICE_l +x{D71D} @Defop(8u+1) PLDSLICE +x{D71E} @Defop(8u+1) LDSLICEQ +x{D71F} @Defop(8u+1) PLDSLICEQ +x{D720} @Defop SDCUTFIRST +x{D721} @Defop SDSKIPFIRST +x{D722} @Defop SDCUTLAST +x{D723} @Defop SDSKIPLAST +x{D724} @Defop SDSUBSTR +x{D726} @Defop SDBEGINSX +x{D727} @Defop SDBEGINSXQ +{ tuck sbits tuck 5 + 3 >> swap x{D72A_} s, over 7 u, 3 roll s, + -rot 3 << 3 + swap - @scomplete } : SDBEGINS:imm +{ tuck sbitrefs abort"no references allowed in slice" dup 26 <= + { drop <b rot SDBEGINS:imm @addopb } + { @havebits + { swap SDBEGINS:imm } + { swap PUSHSLICE SDBEGINSX + } cond + } cond +} : SDBEGINS +{ tuck sbits tuck 5 + 3 >> swap x{D72E_} s, over 7 u, 3 roll s, + -rot 3 << 3 + swap - @scomplete } : SDBEGINSQ:imm +{ tuck sbitrefs abort"no references allowed in slice" dup 26 <= + { drop <b rot SDBEGINSQ:imm @addopb } + { @havebits + { swap SDBEGINSQ:imm } + { swap PUSHSLICE SDBEGINSXQ + } cond + } cond +} : SDBEGINSQ +x{D730} @Defop SCUTFIRST +x{D731} @Defop SSKIPFIRST +x{D732} @Defop SCUTLAST +x{D733} @Defop SSKIPLAST +x{D734} @Defop SUBSLICE +x{D736} @Defop SPLIT +x{D737} @Defop SPLITQ +x{D739} @Defop XCTOS +x{D73A} @Defop XLOAD +x{D73B} @Defop XLOADQ +x{D741} @Defop SCHKBITS +x{D742} @Defop SCHKREFS +x{D743} @Defop SCHKBITREFS +x{D745} @Defop SCHKBITSQ +x{D746} @Defop SCHKREFSQ +x{D747} @Defop SCHKBITREFSQ +x{D748} @Defop PLDREFVAR +x{D749} @Defop SBITS +x{D74A} @Defop SREFS +x{D74B} @Defop SBITREFS +{ <b x{D74E_} s, swap 2 u, @addopb } : PLDREFIDX +x{D74C} @Defop PLDREF +x{D750} @Defop LDILE4 +x{D751} @Defop LDULE4 +x{D752} @Defop LDILE8 +x{D753} @Defop LDULE8 +x{D754} @Defop PLDILE4 +x{D755} @Defop PLDULE4 +x{D756} @Defop PLDILE8 +x{D757} @Defop PLDULE8 +x{D758} @Defop LDILE4Q +x{D759} @Defop LDULE4Q +x{D75A} @Defop LDILE8Q +x{D75B} @Defop LDULE8Q +x{D75C} @Defop PLDILE4Q +x{D75D} @Defop PLDULE4Q +x{D75E} @Defop PLDILE8Q +x{D75F} @Defop PLDULE8Q +x{D760} @Defop LDZEROES +x{D761} @Defop LDONES +x{D762} @Defop LDSAME +x{D764} @Defop SDEPTH +x{D765} @Defop CDEPTH +x{D766} @Defop CLEVEL +x{D767} @Defop CLEVELMASK +{ <b x{D76A_} s, swap 2 u, @addopb } : CHASHI +{ <b x{D76E_} s, swap 2 u, @addopb } : CDEPTHI +x{D770} @Defop CHASHIX +x{D771} @Defop CDEPTHIX +// +// continuation / flow control primitives +x{D8} dup @Defop EXECUTE @Defop CALLX +x{D9} @Defop JMPX +{ dup 1+ + { <b x{DA} s, rot 4 u, swap 4 u, } + { drop <b x{DB0} s, swap 4 u, + } cond @addopb +} : CALLXARGS +x{DB1} @Defop(4u) JMPXARGS +x{DB2} @Defop(4u) RETARGS +x{DB30} dup @Defop RET @Defop RETTRUE +x{DB31} dup @Defop RETALT @Defop RETFALSE +x{DB32} dup @Defop BRANCH @Defop RETBOOL +x{DB34} @Defop CALLCC +x{DB35} @Defop JMPXDATA +{ dup 1+ 0= { 16 + } if + <b x{DB36} s, rot 4 u, swap 4 u, @addopb +} : CALLCCARGS +x{DB38} @Defop CALLXVARARGS +x{DB39} @Defop RETVARARGS +x{DB3A} @Defop JMPXVARARGS +x{DB3B} @Defop CALLCCVARARGS +x{DB3C} @Defop(ref) CALLREF +x{DB3D} @Defop(ref) JMPREF +x{DB3E} @Defop(ref) JMPREFDATA +x{DB3F} @Defop RETDATA +x{DB4} @Defop(12u) RUNVM +x{DB50} @Defop RUNVMX +// conditional and iterated execution primitives +x{DC} @Defop IFRET +x{DD} @Defop IFNOTRET +x{DE} @Defop IF +x{DF} @Defop IFNOT +' IFNOTRET : IF: +' IFRET : IFNOT: +x{E0} @Defop IFJMP +x{E1} @Defop IFNOTJMP +x{E2} @Defop IFELSE + +x{E300} @Defop(ref) IFREF +x{E301} @Defop(ref) IFNOTREF +x{E302} @Defop(ref) IFJMPREF +x{E303} @Defop(ref) IFNOTJMPREF +x{E30D} @Defop(ref) IFREFELSE +x{E30E} @Defop(ref) IFELSEREF +x{E30F} @Defop(ref*2) IFREFELSEREF + +{ 16 1 @havebitrefs nip } : @refop-fits? +// b b1 [e0 e1 e2] -- b' +{ -rot dup @cont-empty? { drop swap 0 } { + 2dup @cont-fits? { rot 1 } { + over @refop-fits? { b> rot 2 } { + swap @| swap 2dup @cont-fits? { rot 1 } { + b> rot 2 + } cond } cond } cond } cond + [] execute +} : @run-cont-op +{ triple 1 ' @run-cont-op does create } : @def-cont-op +{ DROP } { PUSHCONT IF } { IFREF } @def-cont-op IF-cont +{ IFRET } { PUSHCONT IFJMP } { IFJMPREF } @def-cont-op IFJMP-cont +{ DROP } { PUSHCONT IFNOT } { IFNOTREF } @def-cont-op IFNOT-cont +{ IFNOTRET } { PUSHCONT IFNOTJMP } { IFNOTJMPREF } @def-cont-op IFNOTJMP-cont +{ dup 2over rot } : 3dup + +recursive IFELSE-cont2 { + dup @cont-empty? { drop IF-cont } { + over @cont-empty? { nip IFNOT-cont } { + 3dup @two-cont-fit? { -rot PUSHCONT swap PUSHCONT IFELSE } { + 3dup nip @cont-ref-fit? { rot swap PUSHCONT swap b> IFREFELSE } { + 3dup drop @cont-ref-fit? { -rot PUSHCONT swap b> IFELSEREF } { + rot 32 2 @havebitrefs { rot b> rot b> IFREFELSEREF } { + @| -rot IFELSE-cont2 + } cond } cond } cond } cond } cond } cond +} swap ! + +{ }> IF-cont } : }>IF +{ }> IFNOT-cont } : }>IFNOT +{ }> IFJMP-cont } : }>IFJMP +{ }> IFNOTJMP-cont } : }>IFNOTJMP +{ { @normal? IFJMP-cont } @doafter<{ } : IFJMP:<{ +{ { @normal? IFNOTJMP-cont } @doafter<{ } : IFNOTJMP:<{ +{ `else @endblk } : }>ELSE<{ +{ `else: @endblk } : }>ELSE: +{ 1 { swap @normal? swap IFELSE-cont2 } does @doafter<{ } : @doifelse +{ 1 { swap @normal? IFELSE-cont2 } does @doafter<{ } : @doifnotelse +{ + { dup `else eq? + { drop @doifelse } + { dup `else: eq? + { drop IFJMP-cont } + { @normal? IF-cont + } cond + } cond + } @doafter<{ +} : IF:<{ +{ + { dup `else eq? + { drop @doifnotelse } + { dup `else: eq? + { drop IFNOTJMP-cont } + { @normal? IFNOT-cont + } cond + } cond + } @doafter<{ +} : IFNOT:<{ + +x{E304} @Defop CONDSEL +x{E305} @Defop CONDSELCHK +x{E308} @Defop IFRETALT +x{E309} @Defop IFNOTRETALT +{ <b x{E39_} s, swap 5 u, @addopb } : IFBITJMP +{ <b x{E3B_} s, swap 5 u, @addopb } : IFNBITJMP +{ <b x{E3D_} s, swap 5 u, swap ref, @addopb } : IFBITJMPREF +{ <b x{E3F_} s, swap 5 u, swap ref, @addopb } : IFNBITJMPREF + +x{E4} @Defop REPEAT +x{E5} dup @Defop REPEATEND @Defop REPEAT: +x{E6} @Defop UNTIL +x{E7} dup @Defop UNTILEND @Defop UNTIL: +x{E8} @Defop WHILE +x{E9} @Defop WHILEEND +x{EA} @Defop AGAIN +x{EB} dup @Defop AGAINEND @Defop AGAIN: + +{ `do @endblk } : }>DO<{ +{ `do: @endblk } : }>DO: +{ }> PUSHCONT REPEAT } : }>REPEAT +{ { @normal? PUSHCONT REPEAT } @doafter<{ } : REPEAT:<{ +{ }> PUSHCONT UNTIL } : }>UNTIL +{ { @normal? PUSHCONT UNTIL } @doafter<{ } : UNTIL:<{ +{ PUSHCONT { @normal? PUSHCONT WHILE } @doafter<{ } : @dowhile +{ + { dup `do eq? + { drop @dowhile } + { `do: eq? not abort"`}>DO<{` expected" PUSHCONT WHILEEND + } cond + } @doafter<{ +} : WHILE:<{ +{ }> PUSHCONT AGAIN } : }>AGAIN +{ { @normal? PUSHCONT AGAIN } @doafter<{ } : AGAIN:<{ + +x{E314} @Defop REPEATBRK +x{E315} @Defop REPEATENDBRK +x{E316} @Defop UNTILBRK +x{E317} dup @Defop UNTILENDBRK @Defop UNTILBRK: +x{E318} @Defop WHILEBRK +x{E319} @Defop WHILEENDBRK +x{E31A} @Defop AGAINBRK +x{E31B} dup @Defop AGAINENDBRK @Defop AGAINBRK: + +{ }> PUSHCONT REPEATBRK } : }>REPEATBRK +{ { @normal? PUSHCONT REPEATBRK } @doafter<{ } : REPEATBRK:<{ +{ }> PUSHCONT UNTILBRK } : }>UNTILBRK +{ { @normal? PUSHCONT UNTILBRK } @doafter<{ } : UNTILBRK:<{ +{ PUSHCONT { @normal? PUSHCONT WHILEBRK } @doafter<{ } : @dowhile +{ + { dup `do eq? + { drop @dowhile } + { `do: eq? not abort"`}>DO<{` expected" PUSHCONT WHILEENDBRK + } cond + } @doafter<{ +} : WHILEBRK:<{ +{ }> PUSHCONT AGAINBRK } : }>AGAINBRK +{ { @normal? PUSHCONT AGAINBRK } @doafter<{ } : AGAINBRK:<{ + + +// +// continuation stack manipulation and continuation creation +// +{ <b x{EC} s, rot 4 u, swap dup 1+ { 16 + } ifnot 4 u, @addopb } : SETCONTARGS +{ 0 swap SETCONTARGS } : SETNUMARGS +x{ED0} @Defop(4u) RETURNARGS +x{ED10} @Defop RETURNVARARGS +x{ED11} @Defop SETCONTVARARGS +x{ED12} @Defop SETNUMVARARGS +x{ED1E} @Defop BLESS +x{ED1F} @Defop BLESSVARARGS +{ <b x{EE} s, rot 4 u, swap dup 1+ { 16 + } ifnot 4 u, @addopb } : BLESSARGS +{ 0 swap BLESSARGS } : BLESSNUMARGS +// +// control register and continuation savelist manipulation +// x{ED4} Defop(c) PUSHCTR +// x{ED5} Defop(c) POPCTR +{ c4 PUSHCTR } : PUSHROOT +{ c4 POPCTR } : POPROOT +x{ED6} dup @Defop(c) SETCONTCTR @Defop(c) SETCONT +x{ED7} @Defop(c) SETRETCTR +x{ED8} @Defop(c) SETALTCTR +x{ED9} dup @Defop(c) POPSAVE @Defop(c) POPCTRSAVE +x{EDA} dup @Defop(c) SAVE @Defop(c) SAVECTR +x{EDB} dup @Defop(c) SAVEALT @Defop(c) SAVEALTCTR +x{EDC} dup @Defop(c) SAVEBOTH @Defop(c) SAVEBOTHCTR +x{EDE0} @Defop PUSHCTRX +x{EDE1} @Defop POPCTRX +x{EDE2} @Defop SETCONTCTRX +x{EDE3} @Defop(8u) SETCONTCTRMANY +x{EDE3} @Defop(8u) SETCONTMANY +x{EDE4} @Defop SETCONTCTRMANYX +x{EDE4} @Defop SETCONTMANYX +x{EDF0} dup @Defop BOOLAND @Defop COMPOS +x{EDF1} dup @Defop BOOLOR @Defop COMPOSALT +x{EDF2} @Defop COMPOSBOTH +x{EDF3} @Defop ATEXIT +{ }> PUSHCONT ATEXIT } : }>ATEXIT +{ { @normal? PUSHCONT ATEXIT } @doafter<{ } : ATEXIT:<{ +x{EDF4} @Defop ATEXITALT +{ }> PUSHCONT ATEXITALT } : }>ATEXITALT +{ { @normal? PUSHCONT ATEXITALT } @doafter<{ } : ATEXITALT:<{ +x{EDF5} @Defop SETEXITALT +{ }> PUSHCONT SETEXITALT } : }>SETEXITALT +{ { @normal? PUSHCONT SETEXITALT } @doafter<{ } : SETEXITALT:<{ +x{EDF6} @Defop THENRET +x{EDF7} @Defop THENRETALT +x{EDF8} @Defop INVERT +x{EDF9} @Defop BOOLEVAL +x{EDFA} @Defop SAMEALT +x{EDFB} @Defop SAMEALTSAVE +// x{EE} is BLESSARGS +// +// dictionary subroutine call/jump primitives +{ c3 PUSH EXECUTE } : CALLVAR +{ c3 PUSH JMPX } : JMPVAR +{ c3 PUSH } : PREPAREVAR +{ dup 14 ufits { + dup 8 ufits { + <b x{F0} s, swap 8 u, } { + <b x{F12_} s, swap 14 u, + } cond @addopb } { + PUSHINT CALLVAR + } cond +} dup : CALL : CALLDICT +{ dup 14 ufits + { <b x{F16_} s, swap 14 u, @addopb } + { PUSHINT JMPVAR } cond +} dup : JMP : JMPDICT +{ dup 14 ufits + { <b x{F1A_} s, swap 14 u, @addopb } + { PUSHINT c3 PREPAREVAR } cond +} dup : PREPARE : PREPAREDICT +// +// inline support +{ dup sbits + { @addop } + { + dup srefs // + { ref@ CALLREF } + { drop } + cond + } + cond +} : INLINE +// +// throwing and handling exceptions +{ dup 6 ufits + { <b x{F22_} s, swap 6 u, } + { <b x{F2C4_} s, swap 11 u, + } cond +@addopb } : THROW +{ dup 6 ufits + { <b x{F26_} s, swap 6 u, } + { <b x{F2D4_} s, swap 11 u, + } cond +@addopb } : THROWIF +{ dup 6 ufits + { <b x{F2A_} s, swap 6 u, } + { <b x{F2E4_} s, swap 11 u, + } cond +@addopb } : THROWIFNOT +{ <b x{F2CC_} s, swap 11 u, @addopb } : THROWARG +{ <b x{F2DC_} s, swap 11 u, @addopb } : THROWARGIF +{ <b x{F2EC_} s, swap 11 u, @addopb } : THROWARGIFNOT +x{F2F0} @Defop THROWANY +x{F2F1} @Defop THROWARGANY +x{F2F2} @Defop THROWANYIF +x{F2F3} @Defop THROWARGANYIF +x{F2F4} @Defop THROWANYIFNOT +x{F2F5} @Defop THROWARGANYIFNOT +x{F2FF} @Defop TRY +x{F3} @Defop(4u,4u) TRYARGS +{ `catch @endblk } : }>CATCH<{ +{ PUSHCONT { @normal? PUSHCONT TRY } @doafter<{ } : @trycatch +{ + { `catch eq? not abort"`}>CATCH<{` expected" @trycatch + } @doafter<{ +} : TRY:<{ +// +// dictionary manipulation +' NULL : NEWDICT +' ISNULL : DICTEMPTY +' STSLICE : STDICTS +x{F400} dup @Defop STDICT @Defop STOPTREF +x{F401} dup @Defop SKIPDICT @Defop SKIPOPTREF +x{F402} @Defop LDDICTS +x{F403} @Defop PLDDICTS +x{F404} dup @Defop LDDICT @Defop LDOPTREF +x{F405} dup @Defop PLDDICT @Defop PLDOPTREF +x{F406} @Defop LDDICTQ +x{F407} @Defop PLDDICTQ + +x{F40A} @Defop DICTGET +x{F40B} @Defop DICTGETREF +x{F40C} @Defop DICTIGET +x{F40D} @Defop DICTIGETREF +x{F40E} @Defop DICTUGET +x{F40F} @Defop DICTUGETREF + +x{F412} @Defop DICTSET +x{F413} @Defop DICTSETREF +x{F414} @Defop DICTISET +x{F415} @Defop DICTISETREF +x{F416} @Defop DICTUSET +x{F417} @Defop DICTUSETREF +x{F41A} @Defop DICTSETGET +x{F41B} @Defop DICTSETGETREF +x{F41C} @Defop DICTISETGET +x{F41D} @Defop DICTISETGETREF +x{F41E} @Defop DICTUSETGET +x{F41F} @Defop DICTUSETGETREF + +x{F422} @Defop DICTREPLACE +x{F423} @Defop DICTREPLACEREF +x{F424} @Defop DICTIREPLACE +x{F425} @Defop DICTIREPLACEREF +x{F426} @Defop DICTUREPLACE +x{F427} @Defop DICTUREPLACEREF +x{F42A} @Defop DICTREPLACEGET +x{F42B} @Defop DICTREPLACEGETREF +x{F42C} @Defop DICTIREPLACEGET +x{F42D} @Defop DICTIREPLACEGETREF +x{F42E} @Defop DICTUREPLACEGET +x{F42F} @Defop DICTUREPLACEGETREF + +x{F432} @Defop DICTADD +x{F433} @Defop DICTADDREF +x{F434} @Defop DICTIADD +x{F435} @Defop DICTIADDREF +x{F436} @Defop DICTUADD +x{F437} @Defop DICTUADDREF +x{F43A} @Defop DICTADDGET +x{F43B} @Defop DICTADDGETREF +x{F43C} @Defop DICTIADDGET +x{F43D} @Defop DICTIADDGETREF +x{F43E} @Defop DICTUADDGET +x{F43F} @Defop DICTUADDGETREF + +x{F441} @Defop DICTSETB +x{F442} @Defop DICTISETB +x{F443} @Defop DICTUSETB +x{F445} @Defop DICTSETGETB +x{F446} @Defop DICTISETGETB +x{F447} @Defop DICTUSETGETB + +x{F449} @Defop DICTREPLACEB +x{F44A} @Defop DICTIREPLACEB +x{F44B} @Defop DICTUREPLACEB +x{F44D} @Defop DICTREPLACEGETB +x{F44E} @Defop DICTIREPLACEGETB +x{F44F} @Defop DICTUREPLACEGETB + +x{F451} @Defop DICTADDB +x{F452} @Defop DICTIADDB +x{F453} @Defop DICTUADDB +x{F455} @Defop DICTADDGETB +x{F456} @Defop DICTIADDGETB +x{F457} @Defop DICTUADDGETB + +x{F459} @Defop DICTDEL +x{F45A} @Defop DICTIDEL +x{F45B} @Defop DICTUDEL + +x{F462} @Defop DICTDELGET +x{F463} @Defop DICTDELGETREF +x{F464} @Defop DICTIDELGET +x{F465} @Defop DICTIDELGETREF +x{F466} @Defop DICTUDELGET +x{F467} @Defop DICTUDELGETREF + +x{F469} @Defop DICTGETOPTREF +x{F46A} @Defop DICTIGETOPTREF +x{F46B} @Defop DICTUGETOPTREF +x{F46D} @Defop DICTSETGETOPTREF +x{F46E} @Defop DICTISETGETOPTREF +x{F46F} @Defop DICTUSETGETOPTREF + +x{F470} @Defop PFXDICTSET +x{F471} @Defop PFXDICTREPLACE +x{F472} @Defop PFXDICTADD +x{F473} @Defop PFXDICTDEL + +x{F474} @Defop DICTGETNEXT +x{F475} @Defop DICTGETNEXTEQ +x{F476} @Defop DICTGETPREV +x{F477} @Defop DICTGETPREVEQ +x{F478} @Defop DICTIGETNEXT +x{F479} @Defop DICTIGETNEXTEQ +x{F47A} @Defop DICTIGETPREV +x{F47B} @Defop DICTIGETPREVEQ +x{F47C} @Defop DICTUGETNEXT +x{F47D} @Defop DICTUGETNEXTEQ +x{F47E} @Defop DICTUGETPREV +x{F47F} @Defop DICTUGETPREVEQ + +x{F482} @Defop DICTMIN +x{F483} @Defop DICTMINREF +x{F484} @Defop DICTIMIN +x{F485} @Defop DICTIMINREF +x{F486} @Defop DICTUMIN +x{F487} @Defop DICTUMINREF +x{F48A} @Defop DICTMAX +x{F48B} @Defop DICTMAXREF +x{F48C} @Defop DICTIMAX +x{F48D} @Defop DICTIMAXREF +x{F48E} @Defop DICTUMAX +x{F48F} @Defop DICTUMAXREF + +x{F492} @Defop DICTREMMIN +x{F493} @Defop DICTREMMINREF +x{F494} @Defop DICTIREMMIN +x{F495} @Defop DICTIREMMINREF +x{F496} @Defop DICTUREMMIN +x{F497} @Defop DICTUREMMINREF +x{F49A} @Defop DICTREMMAX +x{F49B} @Defop DICTREMMAXREF +x{F49C} @Defop DICTIREMMAX +x{F49D} @Defop DICTIREMMAXREF +x{F49E} @Defop DICTUREMMAX +x{F49F} @Defop DICTUREMMAXREF + +x{F4A0} @Defop DICTIGETJMP +x{F4A1} @Defop DICTUGETJMP +x{F4A2} @Defop DICTIGETEXEC +x{F4A3} @Defop DICTUGETEXEC +{ dup sbitrefs tuck 1 > swap 1 <> or abort"not a dictionary" swap 1 u@ over <> abort"not a dictionary" } : @chkdicts +{ dup null? tuck { <s } ifnot drop not } : @chkdict +{ over @chkdict + { swap <b x{F4A6_} s, swap ref, swap 10 u, @addopb } + { nip swap NEWDICT swap PUSHINT } + cond +} : DICTPUSHCONST +x{F4A8} @Defop PFXDICTGETQ +x{F4A9} @Defop PFXDICTGET +x{F4AA} @Defop PFXDICTGETJMP +x{F4AB} @Defop PFXDICTGETEXEC +{ over @chkdict + { swap <b x{F4AE_} s, swap ref, swap 10 u, @addopb + } if +} dup : PFXDICTCONSTGETJMP : PFXDICTSWITCH + +x{F4B1} @Defop SUBDICTGET +x{F4B2} @Defop SUBDICTIGET +x{F4B3} @Defop SUBDICTUGET +x{F4B5} @Defop SUBDICTRPGET +x{F4B6} @Defop SUBDICTIRPGET +x{F4B7} @Defop SUBDICTURPGET + +x{F4BC} @Defop DICTIGETJMPZ +x{F4BD} @Defop DICTUGETJMPZ +x{F4BE} @Defop DICTIGETEXECZ +x{F4BF} @Defop DICTUGETEXECZ + +// +// blockchain-specific primitives + +x{F800} @Defop ACCEPT +x{F801} @Defop SETGASLIMIT +x{F807} @Defop GASCONSUMED +x{F80F} @Defop COMMIT + +x{F810} @Defop RANDU256 +x{F811} @Defop RAND +x{F814} @Defop SETRAND +x{F815} dup @Defop ADDRAND @Defop RANDOMIZE + +x{F82} @Defop(4u) GETPARAM +x{F823} @Defop NOW +x{F824} @Defop BLOCKLT +x{F825} @Defop LTIME +x{F826} @Defop RANDSEED +x{F827} @Defop BALANCE +x{F828} @Defop MYADDR +x{F829} @Defop CONFIGROOT +x{F82A} @Defop MYCODE +x{F82B} @Defop INCOMINGVALUE +x{F82C} @Defop STORAGEFEES +x{F82D} @Defop PREVBLOCKSINFOTUPLE +x{F82E} @Defop UNPACKEDCONFIGTUPLE +x{F82F} @Defop DUEPAYMENT +x{F830} @Defop CONFIGDICT +x{F832} @Defop CONFIGPARAM +x{F833} @Defop CONFIGOPTPARAM +x{F83400} @Defop PREVMCBLOCKS +x{F83401} @Defop PREVKEYBLOCK +x{F83402} @Defop PREVMCBLOCKS_100 +x{F835} @Defop GLOBALID +x{F836} @Defop GETGASFEE +x{F837} @Defop GETSTORAGEFEE +x{F838} @Defop GETFORWARDFEE +x{F839} @Defop GETPRECOMPILEDGAS +x{F83A} @Defop GETORIGINALFWDFEE +x{F83B} @Defop GETGASFEESIMPLE +x{F83C} @Defop GETFORWARDFEESIMPLE + +x{F840} @Defop GETGLOBVAR +{ dup 1 31 @rangechk <b x{F85_} s, swap 5 u, @addopb } : GETGLOB +x{F860} @Defop SETGLOBVAR +{ dup 1 31 @rangechk <b x{F87_} s, swap 5 u, @addopb } : SETGLOB + +x{F900} @Defop HASHCU +x{F901} @Defop HASHSU +x{F902} @Defop SHA256U + +x{F904} @Defop(8u) HASHEXT +x{F90400} @Defop HASHEXT_SHA256 +x{F90401} @Defop HASHEXT_SHA512 +x{F90402} @Defop HASHEXT_BLAKE2B +x{F90403} @Defop HASHEXT_KECCAK256 +x{F90404} @Defop HASHEXT_KECCAK512 +x{F905} @Defop(8u) HASHEXTR +x{F90500} @Defop HASHEXTR_SHA256 +x{F90501} @Defop HASHEXTR_SHA512 +x{F90502} @Defop HASHEXTR_BLAKE2B +x{F90503} @Defop HASHEXTR_KECCAK256 +x{F90504} @Defop HASHEXTR_KECCAK512 +x{F906} @Defop(8u) HASHEXTA +x{F90600} @Defop HASHEXTA_SHA256 +x{F90601} @Defop HASHEXTA_SHA512 +x{F90602} @Defop HASHEXTA_BLAKE2B +x{F90603} @Defop HASHEXTA_KECCAK256 +x{F90604} @Defop HASHEXTA_KECCAK512 +x{F907} @Defop(8u) HASHEXTAR +x{F90700} @Defop HASHEXTAR_SHA256 +x{F90701} @Defop HASHEXTAR_SHA512 +x{F90702} @Defop HASHEXTAR_BLAKE2B +x{F90703} @Defop HASHEXTAR_KECCAK256 +x{F90704} @Defop HASHEXTAR_KECCAK512 + +x{F910} @Defop CHKSIGNU +x{F911} @Defop CHKSIGNS +x{F912} @Defop ECRECOVER +x{F913} @Defop SECP256K1_XONLY_PUBKEY_TWEAK_ADD +x{F914} @Defop P256_CHKSIGNU +x{F915} @Defop P256_CHKSIGNS + +x{F920} @Defop RIST255_FROMHASH +x{F921} @Defop RIST255_VALIDATE +x{F922} @Defop RIST255_ADD +x{F923} @Defop RIST255_SUB +x{F924} @Defop RIST255_MUL +x{F925} @Defop RIST255_MULBASE +x{F926} @Defop RIST255_PUSHL + +x{B7F921} @Defop RIST255_QVALIDATE +x{B7F922} @Defop RIST255_QADD +x{B7F923} @Defop RIST255_QSUB +x{B7F924} @Defop RIST255_QMUL +x{B7F925} @Defop RIST255_QMULBASE + +x{F93000} @Defop BLS_VERIFY +x{F93001} @Defop BLS_AGGREGATE +x{F93002} @Defop BLS_FASTAGGREGATEVERIFY +x{F93003} @Defop BLS_AGGREGATEVERIFY + +x{F93010} @Defop BLS_G1_ADD +x{F93011} @Defop BLS_G1_SUB +x{F93012} @Defop BLS_G1_NEG +x{F93013} @Defop BLS_G1_MUL +x{F93014} @Defop BLS_G1_MULTIEXP +x{F93015} @Defop BLS_G1_ZERO +x{F93016} @Defop BLS_MAP_TO_G1 +x{F93017} @Defop BLS_G1_INGROUP +x{F93018} @Defop BLS_G1_ISZERO + +x{F93020} @Defop BLS_G2_ADD +x{F93021} @Defop BLS_G2_SUB +x{F93022} @Defop BLS_G2_NEG +x{F93023} @Defop BLS_G2_MUL +x{F93024} @Defop BLS_G2_MULTIEXP +x{F93025} @Defop BLS_G2_ZERO +x{F93026} @Defop BLS_MAP_TO_G2 +x{F93027} @Defop BLS_G2_INGROUP +x{F93028} @Defop BLS_G2_ISZERO + +x{F93030} @Defop BLS_PAIRING +x{F93031} @Defop BLS_PUSHR + +x{F940} @Defop CDATASIZEQ +x{F941} @Defop CDATASIZE +x{F942} @Defop SDATASIZEQ +x{F943} @Defop SDATASIZE + +x{FA00} dup @Defop LDGRAMS @Defop LDVARUINT16 +x{FA01} @Defop LDVARINT16 +x{FA02} dup @Defop STGRAMS @Defop STVARUINT16 +x{FA03} @Defop STVARINT16 + +x{FA04} @Defop LDVARUINT32 // (s -- x s') +x{FA05} @Defop LDVARINT32 // (s -- x s') +x{FA06} @Defop STVARUINT32 // (b x -- b') +x{FA07} @Defop STVARINT32 // (b x -- b') + +x{FA40} @Defop LDMSGADDR +x{FA41} @Defop LDMSGADDRQ +x{FA42} @Defop PARSEMSGADDR +x{FA43} @Defop PARSEMSGADDRQ +x{FA44} @Defop REWRITESTDADDR +x{FA45} @Defop REWRITESTDADDRQ +x{FA46} @Defop REWRITEVARADDR +x{FA47} @Defop REWRITEVARADDRQ + +x{FB00} @Defop SENDRAWMSG +x{FB02} @Defop RAWRESERVE +x{FB03} @Defop RAWRESERVEX +x{FB04} @Defop SETCODE +x{FB06} @Defop SETLIBCODE +x{FB07} @Defop CHANGELIB +x{FB08} @Defop SENDMSG + +// +// debug primitives + +{ dup 0 239 @-range abort"debug selector out of range" + <b x{FE} s, swap 8 u, @addopb +} : DEBUG +{ dup $len 1- <b x{FEF} s, swap 4 u, swap $, @addopb +} : DEBUGSTR +{ over $len <b x{FEF} s, swap 4 u, swap 8 u, swap $, @addopb +} : DEBUGSTRI + +x{FE00} @Defop DUMPSTK +{ 1 15 @rangechk <b x{FE0} s, swap 4 u, @addopb +} : DUMPSTKTOP +x{FE10} @Defop HEXDUMP +x{FE11} @Defop HEXPRINT +x{FE12} @Defop BINDUMP +x{FE13} @Defop BINPRINT +x{FE14} @Defop STRDUMP +x{FE15} @Defop STRPRINT +x{FE1E} @Defop DEBUGOFF +x{FE1F} @Defop DEBUGON +x{FE2} @Defop(s) DUMP +x{FE3} @Defop(s) PRINT +' DEBUGSTR : DUMPTOSFMT +{ 0 DEBUGSTRI } : LOGSTR +{ 1 DEBUGSTRI } : PRINTSTR +x{FEF000} @Defop LOGFLUSH + +// +// codepage primitives +x{FF00} @Defop SETCP0 +x{FFF0} @Defop SETCPX +{ dup -14 239 @-range abort"codepage out of range" + 255 and <b x{FF} s, swap 8 u, @addopb +} : SETCP + +' @addop : CUSTOMOP + +// +// provisions for defining programs consisting of several mutually-recursive procedures +// +variable @proccnt +variable @proclist +variable @procdict +variable @procinfo +variable @gvarcnt +variable @parent-state +variable asm-mode 1 asm-mode ! +19 constant @procdictkeylen +32 constant @zcount +{ pair @proclist @ cons @proclist ! } : @proclistadd +{ @procinfo @ @procdictkeylen idict@ { 16 i@ } { 0 } cond } : @procinfo@ +{ <b rot 16 i, swap @procinfo @ @procdictkeylen b>idict! + not abort"cannot add key to procedure info dictionary" + @procinfo ! +} : @procinfo! +// ( x v1 v2 -- ) +{ not 2 pick @procinfo@ and xor swap @procinfo! } : @procinfo~! +// ( s i f -- ) +{ over @procdictkeylen fits not abort"procedure index out of range" + over swap dup @procinfo~! 2dup @proclistadd + 1 'nop does swap 0 (create) +} : @declproc +{ 1 'nop does swap 0 (create) } : @declglobvar +{ @proccnt @ 1+ dup @proccnt ! 1 @declproc } : @newproc +{ @gvarcnt @ 1+ dup @gvarcnt ! @declglobvar } : @newglobvar +variable @oldcurrent variable @oldctx +Fift-wordlist dup @oldcurrent ! @oldctx ! +{ current@ @oldcurrent ! context@ @oldctx ! Asm definitions + @proccnt @ @proclist @ @procdict @ @procinfo @ @gvarcnt @ @parent-state @ current@ @oldcurrent @ @oldctx @ + 9 tuple @parent-state ! + hole current! + 0 =: main @proclist null! @proccnt 0! @gvarcnt 0! + { bl word @newproc } : NEWPROC + { bl word dup (def?) ' drop ' @newproc cond } : DECLPROC + { bl word dup find + { nip execute <> abort"method redefined with different id" } + { swap 17 @declproc } + cond } : DECLMETHOD + { bl word @newglobvar } : DECLGLOBVAR + "main" 0 @proclistadd + dictnew dup @procdict ! + @procinfo ! 16 0 @procinfo! +} : PROGRAM{ +{ over sbits < { s>c <b swap ref, b> <s } if } : @adj-long-proc +{ // i s l + dup 0< { + negate + @was-split @ { drop 0 } if + } if + @adj-long-proc over @procdict @ @procdictkeylen + idict!+ not abort"cannot define procedure, redefined?" + @procdict ! 2 2 @procinfo~! +} : @def-proc +{ @procinfo @ null? not } : @have-procinfo? +{ @have-procinfo? { 4 4 @procinfo~! } { drop } cond } : @proc-inlined +{ @have-procinfo? { 8 8 @procinfo~! } { drop } cond } : @proc-called +{ 1000 @def-proc } : PROC +{ 0 @def-proc } : PROCREF +{ -1000 @def-proc } : PROCINLINE +{ @procdict @ @procdictkeylen idict@ abort"procedure already defined" +} : @fail-ifdef +{ u@?+ { swap abort"first bits are not zeroes" } if } : @cut-zeroes +{ over @fail-ifdef + 2 { rot @normal? rot b> <s @zcount @cut-zeroes swap @def-proc drop } does + null swap @doafter<{ 0 @zcount u, +} : @PROC:<{ +{ 1000 @PROC:<{ } : PROC:<{ +{ 0 @PROC:<{ } : PROCREF:<{ +{ -1000 @PROC:<{ } : PROCINLINE:<{ +{ dup @proc-called CALLDICT } dup : CALL : CALLDICT +{ dup @proc-called JMPDICT } dup : JMP : JMPDICT +{ dup @proc-called PREPAREDICT } dup : PREPARE : PREPAREDICT +{ dup @procdict @ @procdictkeylen idict@ + { swap @proc-inlined INLINE } { CALLDICT } cond +} dup : INLINECALL : INLINECALLDICT +{ 0 @procdict @ @procdictkeylen idict@ not abort"`main` procedure not defined" drop +} : @chkmaindef +{ @procdict @ @procdictkeylen idict- drop @procdict ! } : @remove-proc +{ ."Procedure `" over type ."` index=" 2 pick . ." flags=0x" dup x. cr } : @showprocinfo +// ( proc_name proc_idx f -- ) f:+1=declared, +2=defined, +4=inlined, +8=called, +16=method +{ // @showprocinfo + dup 0x1a and 2 = asm-mode @ 3 and and ?dup { + 2 and { + over ."Warning: removing (inlined) procedure `" type ."` from call dictionary" cr + } if + 2 pick @remove-proc + } if // remove unused procs + dup 0xc and 0xc = asm-mode @ 4 and and { + over ."Warning: inline procedure `" type ."` is not always inlined" cr + } if + dup 0x1e and 2 = asm-mode @ 8 and and { + over ."Warning: procedure `" type ."` defined but not used" cr + } if + drop 2drop +} : @chkprocdef +{ @chkmaindef + @proclist @ { dup null? not } { + uncons swap unpair over find not + { drop +": undefined procedure name in list" abort } if + drop tuck @procdict @ @procdictkeylen idict@ not + { +": procedure declared but left undefined" abort } if + drop swap 2dup @procinfo@ @chkprocdef (forget) + } while + drop @proclist null! @procinfo null! @proccnt 0! + @procdict dup @ swap null! + @parent-state @ dup null? { drop } { + 9 untuple + @oldctx ! @oldcurrent ! current! @parent-state ! @gvarcnt ! @procinfo ! @procdict ! @proclist ! @proccnt ! + } cond + @oldctx @ context! @oldcurrent @ current! +} : }END +forget @proclist forget @proccnt +{ }END <{ SETCP0 swap @procdictkeylen DICTPUSHCONST DICTIGETJMPZ 11 THROWARG }> } : }END> +{ }END> b> } : }END>c +{ }END>c <s } : }END>s + +// This is the way how FunC assigns method_id for reserved functions. +// Note, that Tolk entrypoints have other names (`onInternalMessage`, etc.), +// but method_id is assigned not by Fift, but by Tolk code generation. +0 constant recv_internal +-1 constant recv_external +-2 constant run_ticktock +-3 constant split_prepare +-4 constant split_install + +{ asm-mode 0 3 ~! } : asm-no-remove-unused +{ asm-mode 1 1 ~! } : asm-remove-unused // enabled by default +{ asm-mode 3 3 ~! } : asm-warn-remove-unused +{ asm-mode 4 4 ~! } : asm-warn-inline-mix +{ asm-mode 0 4 ~! } : asm-no-warn-inline-mix // disabled by default +{ asm-mode 8 8 ~! } : asm-warn-unused +{ asm-mode 0 8 ~! } : asm-no-warn-unused // disabled by default + +// ( c -- ) add vm library for later use with runvmcode +{ <b over ref, b> <s swap hash vmlibs @ 256 udict! not abort"cannot add library" vmlibs ! } : add-lib +// ( x -- c ) make library reference cell +{ <b 2 8 u, swap 256 u, b>spec } : hash>libref +// ( c -- c' ) +{ hash hash>libref } : >libref + +{ dup "." $pos dup -1 = + { drop 0 } + { $| 1 $| nip swap (number) 1- abort"invalid version" + dup dup 0 < swap 999 > or abort"invalid version" + } + cond +} : parse-version-level + +{ + 0 swap + "." $+ + { swap 1000 * swap parse-version-level rot + swap } 3 times + "" $= not abort"invalid version" +} : parse-asm-fif-version + +{ + dup =: required-version parse-asm-fif-version + asm-fif-version parse-asm-fif-version + = 1+ { + "Required Asm.fif version: " @' required-version "; actual Asm.fif version: " asm-fif-version $+ $+ $+ abort + } if +} : require-asm-fif-version + +{ + dup =: required-version parse-asm-fif-version + asm-fif-version parse-asm-fif-version + swap + >= 1+ { + "Required Asm.fif version: " @' required-version "; actual Asm.fif version: " asm-fif-version $+ $+ $+ abort + } if +} : require-asm-fif-version>= + + +Fift definitions Asm +' <{ : <{ +' PROGRAM{ : PROGRAM{ +' asm-fif-version : asm-fif-version +' require-asm-fif-version : require-asm-fif-version +' require-asm-fif-version>= : require-asm-fif-version>= +Fift diff --git a/src/test/fuzzer/src/minimal-fc-stdlib/fift-lib/Color.fif b/src/test/fuzzer/src/minimal-fc-stdlib/fift-lib/Color.fif new file mode 100644 index 0000000000..bd8ed7af6d --- /dev/null +++ b/src/test/fuzzer/src/minimal-fc-stdlib/fift-lib/Color.fif @@ -0,0 +1,21 @@ +library Color +{ 27 emit } : esc + { char " word 27 chr swap $+ 1 ' type does create } :_ make-esc" + make-esc"[0m" ^reset + make-esc"[30m" ^black + make-esc"[31m" ^red + make-esc"[32m" ^green +make-esc"[33m" ^yellow +make-esc"[34m" ^blue +make-esc"[35m" ^magenta +make-esc"[36m" ^cyan +make-esc"[37m" ^white + // bold +make-esc"[30;1m" ^Black +make-esc"[31;1m" ^Red +make-esc"[32;1m" ^Green +make-esc"[33;1m" ^Yellow +make-esc"[34;1m" ^Blue +make-esc"[35;1m" ^Magenta +make-esc"[36;1m" ^Cyan +make-esc"[37;1m" ^White diff --git a/src/test/fuzzer/src/minimal-fc-stdlib/fift-lib/Disasm.fif b/src/test/fuzzer/src/minimal-fc-stdlib/fift-lib/Disasm.fif new file mode 100644 index 0000000000..a46eb5b277 --- /dev/null +++ b/src/test/fuzzer/src/minimal-fc-stdlib/fift-lib/Disasm.fif @@ -0,0 +1,141 @@ +library TVM_Disasm +// simple TVM Disassembler +"Lists.fif" include + +variable 'disasm +{ 'disasm @ execute } : disasm // disassemble a slice +// usage: x{74B0} disasm + +variable @dismode @dismode 0! +{ rot over @ and rot xor swap ! } : andxor! +{ -2 0 @dismode andxor! } : stack-disasm // output 's1 s4 XCHG' +{ -2 1 @dismode andxor! } : std-disasm // output 'XCHG s1, s4' +{ -3 2 @dismode andxor! } : show-vm-code +{ -3 0 @dismode andxor! } : hide-vm-code +{ @dismode @ 1 and 0= } : stack-disasm? + +variable @indent @indent 0! +{ ' space @indent @ 2* times } : .indent +{ @indent 1+! } : +indent +{ @indent 1-! } : -indent + +{ " " $pos } : spc-pos +{ dup " " $pos swap "," $pos dup 0< { drop } { + over 0< { nip } { min } cond } cond +} : spc-comma-pos +{ { dup spc-pos 0= } { 1 $| nip } while } : -leading +{ -leading -trailing dup spc-pos dup 0< { + drop dup $len { atom single } { drop nil } cond } { + $| swap atom swap -leading 2 { over spc-comma-pos dup 0>= } { + swap 1+ -rot $| 1 $| nip -leading rot + } while drop tuple + } cond +} : parse-op +{ dup "s-1" $= { drop "s(-1)" true } { + dup "s-2" $= { drop "s(-2)" true } { + dup 1 $| swap "x" $= { nip "x{" swap $+ +"}" true } { + 2drop false } cond } cond } cond +} : adj-op-arg +{ over count over <= { drop } { 2dup [] adj-op-arg { swap []= } { drop } cond } cond } : adj-arg[] +{ 1 adj-arg[] 2 adj-arg[] 3 adj-arg[] + dup first + dup `XCHG eq? { + drop dup count 2 = { tpop swap "s0" , swap , } if } { + dup `LSHIFT eq? { + drop dup count 2 = stack-disasm? and { second `LSHIFT# swap pair } if } { + dup `RSHIFT eq? { + drop dup count 2 = stack-disasm? and { second `RSHIFT# swap pair } if } { + drop + } cond } cond } cond +} : adjust-op + +variable @cp @cp 0! +variable @curop +variable @contX variable @contY variable @cdict + +{ atom>$ type } : .atom +{ dup first .atom dup count 1 > { space 0 over count 2- { 1+ 2dup [] type .", " } swap times 1+ [] type } { drop } cond } : std-show-op +{ 0 over count 1- { 1+ 2dup [] type space } swap times drop first .atom } : stk-show-op +{ @dismode @ 2 and { .indent ."// " @curop @ csr. } if } : .curop? +{ .curop? .indent @dismode @ 1 and ' std-show-op ' stk-show-op cond cr +} : show-simple-op +{ dup 4 u@ 9 = { 8 u@+ swap 15 and 3 << s@ } { + dup 7 u@ 0x47 = { 7 u@+ nip 2 u@+ 7 u@+ -rot 3 << swap sr@ } { + dup 8 u@ 0x8A = { ref@ <s } { + abort"invalid PUSHCONT" + } cond } cond } cond +} : get-cont-body +{ 14 u@+ nip 10 u@+ ref@ dup rot pair swap <s empty? { drop null } if } : get-const-dict +{ @contX @ @contY @ @contX ! @contY ! } : scont-swap +{ .indent swap type type cr @contY @ @contY null! @contX @ @contX null! + +indent disasm -indent @contY ! +} : show-cont-bodyx +{ ":<{" show-cont-bodyx .indent ."}>" cr } : show-cont-op +{ swap scont-swap ":<{" show-cont-bodyx scont-swap + "" show-cont-bodyx .indent ."}>" cr } : show-cont2-op + +{ @contX @ null? { "CONT" show-cont-op } ifnot +} : flush-contX +{ @contY @ null? { scont-swap "CONT" show-cont-op scont-swap } ifnot +} : flush-contY +{ flush-contY flush-contX } : flush-cont +{ @contX @ null? not } : have-cont? +{ @contY @ null? not } : have-cont2? +{ flush-contY @contY ! scont-swap } : save-cont-body + +{ @cdict ! } : save-const-dict +{ @cdict null! } : flush-dict +{ @cdict @ null? not } : have-dict? + +{ flush-cont .indent type .":<{" cr + @curop @ ref@ <s +indent disasm -indent .indent ."}>" cr +} : show-ref-op +{ flush-contY .indent rot type .":<{" cr + @curop @ ref@ <s @contX @ @contX null! rot ' swap if + +indent disasm -indent .indent swap type cr + +indent disasm -indent .indent ."}>" cr +} : show-cont-ref-op +{ flush-cont .indent swap type .":<{" cr + @curop @ ref@+ <s +indent disasm -indent .indent swap type cr + ref@ <s +indent disasm -indent .indent ."}>" cr +} : show-ref2-op + +{ flush-cont first atom>$ dup 5 $| drop "DICTI" $= swap + .indent type ." {" cr +indent @cdict @ @cdict null! unpair + rot { + swap .indent . ."=> <{" cr +indent disasm -indent .indent ."}>" cr true + } swap ' idictforeach ' dictforeach cond drop + -indent .indent ."}" cr +} : show-const-dict-op + +( `PUSHCONT `PUSHREFCONT ) constant @PushContL +( `REPEAT `UNTIL `IF `IFNOT `IFJMP `IFNOTJMP ) constant @CmdC1 +( `IFREF `IFNOTREF `IFJMPREF `IFNOTJMPREF `CALLREF `JMPREF ) constant @CmdR1 +( `DICTIGETJMP `DICTIGETJMPZ `DICTUGETJMP `DICTUGETJMPZ `DICTIGETEXEC `DICTUGETEXEC ) constant @JmpDictL +{ dup first `DICTPUSHCONST eq? { + flush-cont @curop @ get-const-dict save-const-dict show-simple-op } { + dup first @JmpDictL list-member? have-dict? and { + flush-cont show-const-dict-op } { + flush-dict + dup first @PushContL list-member? { + drop @curop @ get-cont-body save-cont-body } { + dup first @CmdC1 list-member? have-cont? and { + flush-contY first atom>$ .curop? show-cont-op } { + dup first @CmdR1 list-member? { + flush-cont first atom>$ dup $len 3 - $| drop .curop? show-ref-op } { + dup first `WHILE eq? have-cont2? and { + drop "WHILE" "}>DO<{" .curop? show-cont2-op } { + dup first `IFELSE eq? have-cont2? and { + drop "IF" "}>ELSE<{" .curop? show-cont2-op } { + dup first dup `IFREFELSE eq? swap `IFELSEREF eq? or have-cont? and { + first `IFREFELSE eq? "IF" "}>ELSE<{" rot .curop? show-cont-ref-op } { + dup first `IFREFELSEREF eq? { + drop "IF" "}>ELSE<{" .curop? show-ref2-op } { + flush-cont show-simple-op + } cond } cond } cond } cond } cond } cond } cond } cond } cond +} : show-op +{ dup @cp @ (vmoplen) dup 0> { 65536 /mod swap sr@+ swap dup @cp @ (vmopdump) parse-op swap s> true } { drop false } cond } : fetch-one-op +{ { fetch-one-op } { swap @curop ! adjust-op show-op } while } : disasm-slice +{ { disasm-slice dup sbitrefs 1- or 0= } { ref@ <s } while flush-dict flush-cont } : disasm-chain +{ @curop @ swap disasm-chain dup sbitrefs or { .indent ."Cannot disassemble: " csr. } { drop } cond @curop ! } +'disasm ! diff --git a/src/test/fuzzer/src/minimal-fc-stdlib/fift-lib/Fift.fif b/src/test/fuzzer/src/minimal-fc-stdlib/fift-lib/Fift.fif new file mode 100644 index 0000000000..0fde5cb945 --- /dev/null +++ b/src/test/fuzzer/src/minimal-fc-stdlib/fift-lib/Fift.fif @@ -0,0 +1,142 @@ +{ 0 word drop 0 'nop } :: // +{ char " word 1 { swap { abort } if drop } } ::_ abort" +{ { bl word dup "" $= abort"comment extends after end of file" "*/" $= } until 0 'nop } :: /* +// { bl word 1 2 ' (create) } "::" 1 (create) +// { bl word 0 2 ' (create) } :: : +// { bl word 2 2 ' (create) } :: :_ +// { bl word 3 2 ' (create) } :: ::_ +// { bl word 0 (create) } : create +// { bl word (forget) } : forget +{ bl word 1 ' (forget) } :: [forget] +{ char " word 1 ' type } ::_ ." +{ char } word x>B 1 'nop } ::_ B{ +{ swap ({) over 2+ -roll swap (compile) (}) } : does +{ 1 'nop does create } : constant +{ 2 'nop does create } : 2constant +{ hole constant } : variable +10 constant ten +{ bl word 1 { find 0= abort"word not found" } } :: (') +{ bl word find not abort"-?" 0 swap } :: [compile] +{ bl word 1 { + dup find { " -?" $+ abort } ifnot nip execute +} } :: @' +{ bl word 1 { swap 1 'nop does swap 0 (create) } +} :: =: +{ bl word 1 { -rot 2 'nop does swap 0 (create) } +} :: 2=: +{ <b swap s, b> } : s>c +{ s>c hashB } : shash +// to be more efficiently re-implemented in C++ in the future +{ dup 0< ' negate if } : abs +{ 2dup > ' swap if } : minmax +{ minmax drop } : min +{ minmax nip } : max +"" constant <# +' $reverse : #> +{ swap 10 /mod char 0 + rot swap hold } : # +{ { # over 0<= } until } : #s +{ 0< { char - hold } if } : sign +// { dup abs <# #s rot sign #> nip } : (.) +// { (.) type } : ._ +// { ._ space } : . +{ dup 10 < { 48 } { 55 } cond + } : Digit +{ dup 10 < { 48 } { 87 } cond + } : digit +// x s b -- x' s' +{ rot swap /mod Digit rot swap hold } : B# +{ rot swap /mod digit rot swap hold } : b# +{ 16 B# } : X# +{ 16 b# } : x# +// x s b -- 0 s' +{ -rot { 2 pick B# over 0<= } until rot drop } : B#s +{ -rot { 2 pick b# over 0<= } until rot drop } : b#s +{ 16 B#s } : X#s +{ 16 b#s } : x#s +variable base +{ 10 base ! } : decimal +{ 16 base ! } : hex +{ 8 base ! } : octal +{ 2 base ! } : binary +{ base @ B# } : Base# +{ base @ b# } : base# +{ base @ B#s } : Base#s +{ base @ b#s } : base#s +// x w -- s +{ over abs <# rot 1- ' X# swap times X#s rot sign #> nip } : (0X.) +{ over abs <# rot 1- ' x# swap times x#s rot sign #> nip } : (0x.) +{ (0X.) type } : 0X._ +{ 0X._ space } : 0X. +{ (0x.) type } : 0x._ +{ 0x._ space } : 0x. +{ bl (-trailing) } : -trailing +{ char 0 (-trailing) } : -trailing0 +{ char " word 1 ' $+ } ::_ +" +{ find 0<> dup ' nip if } : (def?) +{ bl word 1 ' (def?) } :: def? +{ bl word 1 { (def?) not } } :: undef? +{ def? ' skip-to-eof if } : skip-ifdef +{ bl word dup (def?) { drop skip-to-eof } { 'nop swap 0 (create) } cond } : library +{ bl word dup (def?) { 2drop skip-to-eof } { swap 1 'nop does swap 0 (create) } cond } : library-version +{ hole dup 1 'nop does swap 1 { context! } does bl word tuck 0 (create) +"-wordlist" 0 (create) } : namespace +{ context@ current! } : definitions +{ char ) word "$" swap $+ 1 { find 0= abort"undefined parameter" execute } } ::_ $( +// b s -- ? +{ sbitrefs rot brembitrefs rot >= -rot <= and } : s-fits? +// b s x -- ? +{ swap sbitrefs -rot + rot brembitrefs -rot <= -rot <= and } : s-fits-with? +{ 0 swap ! } : 0! +{ tuck @ + swap ! } : +! +{ tuck @ swap - swap ! } : -! +{ 1 swap +! } : 1+! +{ -1 swap +! } : 1-! +{ null swap ! } : null! +{ not 2 pick @ and xor swap ! } : ~! +0 tuple constant nil +{ 1 tuple } : single +{ 2 tuple } : pair +{ 3 tuple } : triple +{ 1 untuple } : unsingle +{ 2 untuple } : unpair +{ 3 untuple } : untriple +{ over tuple? { swap count = } { 2drop false } cond } : tuple-len? +{ 0 tuple-len? } : nil? +{ 1 tuple-len? } : single? +{ 2 tuple-len? } : pair? +{ 3 tuple-len? } : triple? +{ 0 [] } : first +{ 1 [] } : second +{ 2 [] } : third +' pair : cons +' unpair : uncons +{ 0 [] } : car +{ 1 [] } : cdr +{ cdr car } : cadr +{ cdr cdr } : cddr +{ cdr cdr car } : caddr +{ null ' cons rot times } : list +{ -rot pair swap ! } : 2! +{ @ unpair } : 2@ +{ true (atom) drop } : atom +{ bl word atom 1 'nop } ::_ ` +{ hole dup 1 { @ execute } does create } : recursive +{ 0 { 1+ dup 1 ' $() does over (.) "$" swap $+ 0 (create) } rot times drop } : :$1..n +{ 10 hold } : +cr +{ 9 hold } : +tab +{ "" swap { 0 word 2dup $cmp } { rot swap $+ +cr swap } while 2drop } : scan-until-word +{ 0 word -trailing scan-until-word 1 'nop } ::_ $<< +{ 0x40 runvmx } : runvmcode +{ 0x48 runvmx } : gasrunvmcode +{ 0xc8 runvmx } : gas2runvmcode +{ 0x43 runvmx } : runvmdict +{ 0x4b runvmx } : gasrunvmdict +{ 0xcb runvmx } : gas2runvmdict +{ 0x45 runvmx } : runvm +{ 0x4d runvmx } : gasrunvm +{ 0xcd runvmx } : gas2runvm +{ 0x55 runvmx } : runvmctx +{ 0x5d runvmx } : gasrunvmctx +{ 0xdd runvmx } : gas2runvmctx +{ 0x75 runvmx } : runvmctxact +{ 0x7d runvmx } : gasrunvmctxact +{ 0xfd runvmx } : gas2runvmctxact +{ 0x35 runvmx } : runvmctxactq +{ 0x3d runvmx } : gasrunvmctxactq diff --git a/src/test/fuzzer/src/minimal-fc-stdlib/fift-lib/FiftExt.fif b/src/test/fuzzer/src/minimal-fc-stdlib/fift-lib/FiftExt.fif new file mode 100644 index 0000000000..6ed677d7f2 --- /dev/null +++ b/src/test/fuzzer/src/minimal-fc-stdlib/fift-lib/FiftExt.fif @@ -0,0 +1,118 @@ +{ ?dup { 1+ { execute } { 0 swap } cond } + { (number) ?dup 0= abort"-?" 'nop } cond +} : (interpret-prepare) +{ { include-depth 0= (seekeof?) not } { + (word-prefix-find) (interpret-prepare) (execute) + } while +} : interpret +{ ({) + { 0 (seekeof?) abort"no }" (word-prefix-find) (interpret-prepare) (compile) over atom? not } until + (}) swap execute +} : begin-block +{ swap 0 'nop } : end-block +{ { 1 'nop } `{ begin-block } +{ { swap `{ eq? not abort"} without {" swap execute } end-block } +:: } :: { + +// if{ ... }then{ ... }elseif{ ... }then{ ... }else{ ... } +{ eq? not abort"unexpected" } : ?pairs +{ dup `if eq? swap `ifnot eq? over or not abort"without if{" } : if-ifnot? +// cond then ? -- exec +{ { ' if } { ' ifnot } cond rot ({) 0 rot (compile) -rot 1 swap (compile) (}) +} : (make-if) +// cond then else -- exec +{ rot ({) 0 rot (compile) -rot 2 ' cond (compile) (}) +} : (make-cond) +{ `noelse `if begin-block } :: if{ +{ `noelse `ifnot begin-block } :: ifnot{ +{ 1 ' end-block does } : end-block-does +{ { over `else eq? } { + nip rot if-ifnot? ' swap ifnot (make-cond) + } while + swap `noelse ?pairs 0 swap +} : finish-else-chain +{ swap dup if-ifnot? drop `then { + swap `then ?pairs + swap if-ifnot? (make-if) finish-else-chain + } `{ begin-block +} end-block-does :: }then{ +{ swap `{ ?pairs nip + swap `then eq? not abort"without }then{" `else +} : ?else-ok +{ ?else-ok { finish-else-chain } `{ begin-block } end-block-does :: }else{ +{ ?else-ok `if begin-block } end-block-does :: }elseif{ +{ ?else-ok `ifnot begin-block } end-block-does :: }elseifnot{ + +// while{ ... }do{ ... } +{ 2 ' while does } : (make-while) +{ `while begin-block } :: while{ +{ swap `while eq? not abort"without while{" `while-do { + swap `while-do ?pairs (make-while) 0 swap + } `{ begin-block +} end-block-does :: }do{ + +// repeat{ ... }until{ ... } +{ swap ({) 0 rot (compile) 0 rot (compile) (}) 1 ' until does } : (make-until) +{ `repeat begin-block } :: repeat{ +{ swap `repeat eq? not abort"without repeat{" `until { + swap `until ?pairs (make-until) 0 swap + } `{ begin-block +} end-block-does :: }until{ + +// def <name> { ... } instead of { ... } : <name> +{ bl word swap bl word "{" $cmp abort"{ expected" `def { + swap `def ?pairs -rot 3 ' (create) + } `{ begin-block +} : (def) +{ 0 (def) } :: def +{ 1 (def) } :: def:: + +// defrec <name> { ... } instead of recursive <name> { ... } swap ! +{ recursive bl word "{" $cmp abort"{ expected" `defrec { + swap `defrec ?pairs swap ! 0 'nop + } `{ begin-block +} :: defrec + +def .sgn { + if{ ?dup 0= }then{ + ."zero" + }elseif{ 0> }then{ + ."positive" + }else{ + ."negative" + } + cr +} +// equivalent to: { ?dup 0= { ."zero" } { 0> { ."positive" } { ."negative" } cond } cond cr } : .sgn + +defrec fact { + if{ dup }then{ + dup 1- fact * + }else{ + drop 1 + } +} +// equivalent to: recursive fact { dup { dup 1- fact * } { drop 1 } cond } swap ! + +// [[ ... ]] computes arbitrary constants inside definitions +// { [[ 5 dup * ]] + } : add25 +// is equivalent to +// { 25 + } : add25 +{ "without [[" abort } box constant ']] +{ ']] @ execute } : ]] +{ { ']] @ 2 { ']] ! call/cc } does ']] ! + interpret 'nop ']] ! "]] not found" abort + } call/cc + drop 1 'nop +} :: [[ + +{ { over @ swap 2 { call/cc } does swap ! + interpret "literal to eof" abort + } call/cc + drop execute 1 'nop +} : interpret-literal-to +// use next line only if Lists.fif is loaded (or move it to Lists.fif if FiftExt.fif becomes part of Fift.fif) +// { ( ') interpret-literal-to } :: '( +// then you can use list literals '( a b c ... ) inside definitions: +// { '( 1 2 3 ) } : test +// { '( ( `a { ."A" } ) ( `b { ."B" } ) ) assoc { cadr execute } { ."???" } cond } : test2 diff --git a/src/test/fuzzer/src/minimal-fc-stdlib/fift-lib/GetOpt.fif b/src/test/fuzzer/src/minimal-fc-stdlib/fift-lib/GetOpt.fif new file mode 100644 index 0000000000..442552b636 --- /dev/null +++ b/src/test/fuzzer/src/minimal-fc-stdlib/fift-lib/GetOpt.fif @@ -0,0 +1,131 @@ +library GetOpt // Simple command-line options parser +"Lists.fif" include + +// May be used as follows: +// begin-options +// "h" { ."Help Message" 0 halt } short-option +// "v" { parse-int =: verbosity } short-option-arg +// "i" "--interactive" { true =: interactive } short-long-option +// parse-options + +// ( l -- l') computes tail of list l if non-empty; else () +{ dup null? ' cdr ifnot } : safe-cdr +// ( l c -- l') deletes first c elements from list l +{ ' safe-cdr swap times } : list-delete-first +// ( l n c -- l' ) deletes c elements starting from n-th in list l +recursive list-delete-range { + dup 0<= { 2drop } { + over 0<= { nip list-delete-first } { + swap 1- swap rot uncons 2swap list-delete-range cons + } cond } cond +} swap ! +// ( n c -- ) deletes $n .. $(n+c-1) from the argument list $* +{ swap 1- $* @ swap rot list-delete-range $* ! } : $*del.. +// ( s s' -- ? ) checks whether s' is a prefix of s +{ tuck $len over $len over >= { $| drop $= } { 2drop drop false } cond +} : $pfx? +// ( s -- ? ) checks whether s is an option (a string beginning with '-') +{ dup $len 1 > { "-" $pfx? } { drop false } cond } : is-opt? +// ( s -- ? ) checks whether s is a digit option +{ 2 $| drop 1 $| nip $>B 8 B>u@ dup 57 <= swap 48 >= and } : is-digit-opt? +0 box constant disable-digit-opts +// ( l -- s i or 0 ) finds first string in l beginning with '-' +{ 0 { 1+ over null? { 2drop 0 true } { + swap uncons over is-opt? + { disable-digit-opts @ { over is-digit-opt? not } { true } cond } { false } cond + { drop swap true } { nip swap false } cond + } cond } until +} : list-find-opt +// ( -- s i or 0 ) finds first option in cmdline args +{ $* @ list-find-opt } : first-opt +' second : get-opt-flags +' first : get-opt-exec +// ( s t -- ? ) checks whether short/long option s matches description t +{ third $= } : short-option-matches +{ dup get-opt-flags 4 and 0= 3 + [] $= +} : long-option-matches +// ( t -- s -1 or 0 ) extracts help message from description +{ dup get-opt-flags 4 and 0= 4 + over count over > + { [] true } { 2drop false } cond +} : get-opt-help +// ( s l -- t -1 or 0 ) finds short/long option s in list l +{ swap 1 { swap short-option-matches } does assoc-gen +} : lookup-short-option +{ swap 1 { swap long-option-matches } does assoc-gen +} : lookup-long-option +// ( s -- s' null or s' s'' ) Splits long option --opt=arg at '=' +{ dup "=" $pos 1+ ?dup { tuck $| swap rot 1- $| drop swap } { null } cond +} : split-longopt +// ( l -- f or 0 ) Extracts global option flags from first entry of l +{ dup null? { drop 0 } { car get-opt-flags -256 and } cond +} : get-global-option-flags +variable options-list +// ( l -- i or 0 ) +// parses command line arguments according to option description list l +// and returns index i of first incorrect option +{ dup options-list ! get-global-option-flags + 256 and disable-digit-opts ! + { first-opt dup 0= { true } { + swap dup "--" $pfx? { // i s + dup $len 2 = { drop dup 1 $*del.. 0 true } { + split-longopt swap options-list @ + lookup-long-option not { drop true } { // i s' t f + dup get-opt-exec swap get-opt-flags 3 and // i s' e f' + 2 pick null? { dup 1 = } { dup 0= negate } cond // i s' e f' f'' + dup 1 = { 2drop 2drop true } { + { drop nip over 1+ $() swap execute 2 $*del.. false } { + ' nip ifnot execute 1 $*del.. false + } cond } cond } cond } cond } { // i s + 1 $| nip { + dup $len 0= { drop 1 $*del.. false true } { + 1 $| swap options-list @ // i s' s l + lookup-short-option not { drop true true } { // i s' t + dup get-opt-exec swap get-opt-flags 3 and // i s' e f' + ?dup 0= { execute false } { + 2 pick $len { drop execute "" false } { + 2 = { nip null swap execute "" false } { // i e + nip over 1+ $() swap execute 2 $*del.. false true + } cond } cond } cond } cond } cond } until + } cond + } cond } until +} : getopt +// ( t -- ) Displays help message for one option +{ dup get-opt-flags dup 4 and 2 pick third swap { + ."-" type ."/" over 3 [] type } { + dup $len { dup "--" $pfx? { ."-" } ifnot type } { + drop ."usage: " $0 type + } cond } cond + dup 3 and ?dup { + 2 = { ."[=<optarg>]" } { ."=<optarg>" } cond + } if + 8 and { 9 emit } ifnot + get-opt-help { type } { ."No help available" } cond cr +} : show-opt-help +// ( -- ) Displays options help message according to options-list +{ options-list @ { dup null? not } { + uncons swap show-opt-help + } while drop +} : show-options-help +// ( l -- ) Parses options and throws an error on failure +{ getopt ?dup { + $() "cannot parse command line options near `" swap $+ +"`" + show-options-help abort } if +} : run-getopt +anon constant opt-list-marker +' opt-list-marker : begin-options +{ opt-list-marker list-until-marker } : end-options +{ end-options run-getopt } : parse-options +// ( s e -- o ) Creates short/long option s with execution token e +{ 0 rot triple } dup : short-option : long-option +// ( s s' e -- o ) Creates a combined short option s and long option s' with execution token e +{ 4 2swap 4 tuple } : short-long-option +{ 1 rot triple } dup : short-option-arg : long-option-arg +{ 2 rot triple } dup : short-option-?arg : long-option-?arg +{ 5 2swap 4 tuple } : short-long-option-arg +{ 6 2swap 4 tuple } : short-long-option-?arg +// ( o s -- s' ) Adds help message to option +' , : option-help +// ( s f -- o ) Creates a generic help message +{ swap 'nop rot "" 3 roll 4 tuple } : generic-help-setopt +{ 0 generic-help-setopt } : generic-help +256 constant disable-digit-options diff --git a/src/test/fuzzer/src/minimal-fc-stdlib/fift-lib/Lisp.fif b/src/test/fuzzer/src/minimal-fc-stdlib/fift-lib/Lisp.fif new file mode 100644 index 0000000000..fb91408d33 --- /dev/null +++ b/src/test/fuzzer/src/minimal-fc-stdlib/fift-lib/Lisp.fif @@ -0,0 +1,436 @@ +library Lisp // tiny Lisp (or rather Scheme) interpreter +"Lists.fif" include +variable lisp-dict +{ hole dup 1 { @ execute } does create } : recursive +{ atom>$ +" undefined" abort } : report-not-found +// a l -- d -1 or a 0 Look up definition d of atom a in dictionary l +{ { dup null? { drop false true } + { uncons -rot unpair -rot over eq? + { drop nip true true } { nip swap false } cond + } cond + } until +} : lookup-in +// a dict -- def +{ lookup-in ' report-not-found ifnot } : lookup-or-fail +{ lisp-dict @ lookup-or-fail } : lisp-dict-lookup +// a d -- Defines a with definition d in dictionary lisp-dict +{ pair lisp-dict @ cons lisp-dict ! } : lisp-dict-int-define +{ box lisp-dict-int-define } : lisp-dict-define +// a d -- Defines new a with defininition d +{ over lisp-dict @ lookup-in { 2drop atom>$ +" already defined" abort } + { drop lisp-dict-int-define } cond +} : lisp-dict-int-define-new +{ box lisp-dict-int-define-new } : lisp-dict-define-new +// a e -- Defines a with executable definition given by e +{ single lisp-dict-define-new } : lisp-dict-define-exec +// expr ctx def -- val +{ dup first execute } : run-definition +// expr ctx -- val +recursive lisp-ctx-eval { + over tuple? + { over first over lisp-ctx-eval run-definition } + { over atom? { lookup-or-fail @ } { drop } cond } + cond +} swap ! +// exp -- value +{ lisp-dict @ lisp-ctx-eval } : lisp-eval +// (exprs) ctx -- (vals) +recursive lisp-ctx-eval-list +{ over null? { drop } { + swap uncons -rot over lisp-ctx-eval -rot lisp-ctx-eval-list cons + } cond +} swap ! +// (exprs) ctx -- val +{ null rot { + dup null? { drop nip true } { + nip uncons swap 2 pick lisp-ctx-eval swap false + } cond } until +} : lisp-ctx-eval-list-last +// l c -- (args) +{ swap uncons nip swap lisp-ctx-eval-list } : extract-eval-arg-list +{ drop uncons nip } : extract-arg-list +// (x1 .. xn) e n -- x1 .. xn e +{ { swap uncons rot } swap times + swap null? not abort"invalid number of arguments" +} : unpack-list +// l c n e -- v +{ swap 2swap extract-eval-arg-list // e n (args) + -rot unpack-list execute +} : eval-exec-fixed +// l c n e -- v +{ 2 pick pair + swap 2swap extract-arg-list // [e c] n (args) + -rot unpack-list unpair swap execute +} : exec-fixed +// l c e -- v +{ -rot extract-eval-arg-list // e (args) + swap execute +} : eval-exec-list +{ -rot tuck extract-arg-list // e c (args) + swap rot execute +} : exec-list +// e a n -- +{ rot 2 { // expr ctx def n e + rot drop eval-exec-fixed } does + lisp-dict-define-exec +} : lisp-fixed-primitive +{ rot 2 { rot drop exec-fixed } does lisp-dict-define-exec +} : lisp-fixed-lazy-primitive +// e a -- +{ swap 1 { nip eval-exec-list } does lisp-dict-define-exec +} : lisp-primitive +{ swap 1 { nip exec-list } does lisp-dict-define-exec +} : lisp-lazy-primitive + +// Uncomment next line for Fift booleans +// false constant #f true constant #t null constant no-answer +// Uncomment next line for Scheme booleans +`#f constant #f `#t constant #t #f constant no-answer +{ #f eq? } : lisp-false? +{ lisp-false? 0= } : lisp-true? +{ ' #t ' #f cond } : lisp-bool + +// temp for defining a lot of primitives +{ bl word atom lisp-primitive } : L: +{ bl word atom swap lisp-dict-define } : L=: +{ bl word atom swap lisp-fixed-primitive } : #L: +{ 0 #L: } : 0L: +{ 1 #L: } : 1L: +{ 2 #L: } : 2L: + +// basic primitives +{ sum-list } L: + +{ - } 2L: - +{ dup null? { drop 1 } { ' * foldl-ne } cond } L: * +{ / } 2L: / +{ mod } 2L: modulo +{ abs } 1L: abs +{ ' min foldl-ne } L: min +{ ' max foldl-ne } L: max +{ true ' and foldl } L: integer-and +{ false ' or foldl } L: integer-or +{ 0 ' xor foldl } L: integer-xor +{ not } 1L: integer-not +{ = lisp-bool } 2L: = +{ <> lisp-bool } 2L: <> +{ < lisp-bool } 2L: < +{ <= lisp-bool } 2L: <= +{ > lisp-bool } 2L: > +{ >= lisp-bool } 2L: >= +{ eq? lisp-bool } 2L: eq? +{ eqv? lisp-bool } 2L: eqv? +{ equal? lisp-bool } 2L: equal? +{ cons } 2L: cons +{ car } 1L: car +{ cdr } 1L: cdr +{ cadr } 1L: cadr +{ cddr } 1L: cddr +{ caddr } 1L: caddr +{ cdr cddr } 1L: cdddr +{ concat-list-lists } L: append +{ list-reverse } 1L: reverse +{ list-tail } 2L: list-tail +{ list-ref } 2L: list-ref +{ list-member-eq } 2L: memq +{ list-member-eqv } 2L: memv +{ list-member-equal } 2L: member +{ assq ' #f ifnot } 2L: assq +{ assv ' #f ifnot } 2L: assv +{ assoc ' #f ifnot } 2L: assoc +{ list? lisp-bool } 1L: list? +{ pair? lisp-bool } 1L: pair? +{ tuple? lisp-bool } 1L: tuple? +{ string? lisp-bool } 1L: string? +{ integer? lisp-bool } 1L: integer? +{ integer? lisp-bool } 1L: number? +{ count } 1L: width +{ list-length } 1L: length +{ [] } 2L: tuple-ref +{ first } 1L: first +{ second } 1L: second +{ third } 1L: third +{ 3 [] } 1L: fourth +{ list>tuple } 1L: list->tuple +{ explode list } 1L: tuple->list +null L=: null +{ atom? lisp-bool } 1L: symbol? +{ atom } 1L: string->symbol +{ atom>$ } 1L: symbol->string +{ dup #f eq? swap #t eq? or lisp-bool } 1L: boolean? +#t L=: else +#f L=: #f +#t L=: #t +{ null? lisp-bool } 1L: null? +{ 0= lisp-bool } 1L: zero? +{ 0> lisp-bool } 1L: positive? +{ 0< lisp-bool } 1L: negative? +{ 1 and 0= lisp-bool } 1L: even? +{ 1 and 0<> lisp-bool } 1L: odd? +{ bye } 0L: exit +{ .l null } 1L: write +{ lisp-eval } 1L: eval +{ drop } `quote 1 lisp-fixed-lazy-primitive +'nop L: list +{ list>tuple } L: tuple +{ list-last } L: begin +{ $len } 1L: string-length +{ concat-string-list } L: string-append +{ $= lisp-bool } 2L: string=? +{ $cmp 0< lisp-bool } 2L: string<? +{ $cmp 0<= lisp-bool } 2L: string<=? +{ $cmp 0> lisp-bool } 2L: string>? +{ $cmp 0>= lisp-bool } 2L: string>=? +{ (number) dup 1 = { drop } { ' 2drop if no-answer } cond +} 1L: string->number +{ (.) } 1L: number->string +{ box? lisp-bool } 1L: box? +{ box } 1L: box +{ hole } 0L: new-box +{ @ } 1L: unbox +{ tuck swap ! } 2L: set-box! +{ abort } 1L: error +{ dup find { nip execute } { +" -?" abort } cond } : find-execute +{ explode-list 1- roll find-execute } L: fift-exec +{ explode-list dup 1- swap roll find-execute } L: fift-exec-cnt +{ uncons swap find-execute } L: fift-exec-list +// end of basic primitives +forget L: forget #L: forget L=: +forget 0L: forget 1L: forget 2L: + +{ { dup tuple? ' do-quote if } list-map } : map-quote +{ uncons ' cons foldr-ne map-quote + null swap cons lisp-dict @ rot run-definition +} `apply lisp-primitive // bad: should have preserved original context +// e1 e2 e3 ctx +{ 3 exch 3 pick lisp-ctx-eval lisp-true? ' swap if nip swap lisp-ctx-eval } +`if 3 lisp-fixed-lazy-primitive +// (e) ctx +{ #t -rot + { over null? { 2drop true } { + swap uncons swap 2 pick lisp-ctx-eval dup lisp-true? // v' c t v ? + { swap 2swap nip false } { -rot 2drop nip true } cond + } cond } until +} `and lisp-lazy-primitive +{ #f -rot + { over null? { 2drop true } { + swap uncons swap 2 pick lisp-ctx-eval dup lisp-false? // v' c t v ? + { swap 2swap nip false } { -rot 2drop nip true } cond + } cond } until +} `or lisp-lazy-primitive +{ lisp-false? lisp-bool } `not 1 lisp-fixed-primitive +// cond-clause ctx -- v -1 or 0 +{ swap uncons -rot dup `else eq? { + drop lisp-ctx-eval-list-last true } { + over lisp-ctx-eval lisp-true? { + lisp-ctx-eval-list-last true } { + 2drop false + } cond } cond +} : eval-cond-clause +// (clauses) ctx -- v +{ { over null? { no-answer true } { + swap uncons -rot over eval-cond-clause } cond + } until -rot 2drop +} `cond lisp-lazy-primitive +{ lisp-dict @ lookup-in { hole tuck lisp-dict-int-define } ifnot +} : lisp-create-global-var +// a e ctx -- old (simple) define +{ drop over atom? not abort"only a variable can be define'd" + over lisp-create-global-var swap lisp-eval swap ! +} drop // `define 2 lisp-fixed-lazy-primitive +{ tuck lisp-ctx-eval rot dup atom? not abort"only a variable can be set" + rot lookup-or-fail dup @ -rot ! +} `set! 2 lisp-fixed-lazy-primitive +// define lambda +{ { dup null? { drop true true } + { uncons swap atom? { false } { drop false true } cond } cond + } until +} : var-list? +{ { dup null? over atom? or { drop true true } + { uncons swap atom? { false } { drop false true } cond } cond + } until +} : lambda-var-list? +// (quote x) -- x -1 ; else 0 +{ dup pair? { uncons swap `quote eq? { car true } { drop false } cond } + { drop false } cond +} : is-quote? +recursive match-arg-list-acc +// l (vars) (args) -- ((var . arg) ...)+l -1 or ? 0 +{ over atom? { over `_ eq? { 2drop } { pair swap cons } cond true } { + over null? { nip null? } { // (vars) (args) + over tuple? not { 2drop false } { + over is-quote? { eq? nip } { // (v) (a) + dup tuple? not { 2drop false } { + over count over count over <> { drop 2drop false } { // l [v] [a] n + 3 roll 0 rot { // [v] [a] l i + dup 0< { + 3 pick over [] swap // [v] [a] l vi i + 3 pick over [] 2swap rot // [v] [a] i l vi ai + match-arg-list-acc { // [v] [a] i l' + swap 1+ } { nip -1 } cond + } ifnot + } swap times + 2swap 2drop 0>= + } cond } cond } cond } cond } cond } cond +} swap ! +{ null -rot match-arg-list-acc } : match-arg-list +// ((var . arg)...) ctx -- ctx' +{ { over null? not } + { swap uncons swap unpair box pair rot cons } while + nip +} : extend-ctx-by-list +// ((vars) body) ctx +{ swap uncons -rot + dup lambda-var-list? not abort"invalid formal parameter list" + { // l-expr ctx' [_ body ctx (vars)] + -rot 2 pick 3 [] swap rot // [_ body ...] (vars) ctx' l-expr + uncons nip swap lisp-ctx-eval-list // [_ body ...] (vars) (arg-vals) + match-arg-list not abort"invalid arguments to lambda" // [_ body ...] ((var arg)...) + over third extend-ctx-by-list // [_ body ctx (vars)] ctx'' + swap second swap lisp-ctx-eval-list-last + } 3 -roll 4 tuple +} : make-lambda +{ make-lambda } `lambda lisp-lazy-primitive +// (a e) ctx -- more sophisticated (define a e) +{ drop uncons swap dup atom? { // (e) a + tuck lisp-create-global-var + swap lisp-dict @ lisp-ctx-eval-list-last swap ! + } { // (e) (a v..) + uncons over atom? not abort"only variables can be define'd" // (e) a (v..) + rot cons over lisp-create-global-var // a ((v..) (e)) h + swap lisp-dict @ make-lambda swap ! + } cond +} `define lisp-lazy-primitive +// ((x e) ..) ctx -- ((x.v) ..) +recursive eval-assign-list +{ over null? { drop } { + swap uncons swap uncons // ctx t x (e) + over atom? not abort"invalid variable name in assignment list" + 3 pick lisp-ctx-eval-list-last // ctx t x v + pair swap rot eval-assign-list cons + } cond +} swap ! +// (((x v) ..) body) ctx -- let construct +{ swap uncons swap 2 pick eval-assign-list // ctx body ((x v)...) + rot extend-ctx-by-list lisp-ctx-eval-list-last +} `let lisp-lazy-primitive +// ((x e) ..) ctx -- ctx' +{ swap { + dup null? { drop true } { + uncons swap uncons // ctx t x (e) + over atom? not abort"invalid variable name in assignment list" + 3 pick lisp-ctx-eval-list-last // ctx t x v + box pair rot cons swap false + } cond } until +} : compute-let*-ctx +// (((x v) ..) body) ctx -- let* construct +{ swap uncons swap rot compute-let*-ctx lisp-ctx-eval-list-last +} `let* lisp-lazy-primitive +// ((x e) ..) ctx -- ((h e) ..) ctx' , with x bound to h in ctx' +recursive prepare-letrec-ctx { + over null? { + swap uncons swap uncons swap // ctx t (e) x + hole tuck pair swap rot cons // ctx t (x.h) (h e) + 3 -roll rot cons prepare-letrec-ctx // (h e) t ctx' + -rot cons swap + } ifnot +} swap ! +// (((x v) ..) body) ctx -- letrec construct +{ swap uncons swap rot prepare-letrec-ctx swap { // body ctx' ((h e)..) + dup null? { drop true } { + uncons -rot uncons 2 pick lisp-ctx-eval-list-last // body t ctx' h v + swap ! swap false + } cond } until + lisp-ctx-eval-list-last +} `letrec lisp-lazy-primitive +// (e (p e)...) ctx -- match construct +{ swap uncons swap 2 pick lisp-ctx-eval swap { // ctx v ((p e)..) + dup null? { drop 2drop no-answer true } { + uncons swap uncons swap 3 pick // ctx v t e p v + match-arg-list { // ctx v t e ((x' . v')...) + 2swap 2drop rot extend-ctx-by-list lisp-ctx-eval-list-last true } { + 2drop false + } cond } cond } until +} `match lisp-lazy-primitive +// +lisp-dict @ constant original-lisp-dict +{ original-lisp-dict lisp-dict ! } : reset-lisp +{ ' drop { lisp-eval .l cr } List-generic( } :_ LISP-EVAL-PRINT( +// LISP-EVAL-PRINT((+ 3 4) (* 5 6)) computes and prints 12 and 30 +{ hole dup 1 { @ nip } does swap + 1 { swap lisp-eval swap ! } does + List-generic( +} :_ LISP-EVAL( +// LISP-EVAL((+ 3 4) (* 5 6)) computes 12 and 30, returns only 30 +// /* +LISP-EVAL-PRINT( + (define succ (lambda (x) (+ x 1))) + (define (twice f) (lambda (x) (f (f x)))) + (define (fact n) (if (= n 0) 1 (* n (fact (- n 1))))) + (fact ((twice succ) 5)) + (define compare (lambda (x y) (cond ((< x y) 'less) ((= x y) 'equal) (else 'greater)))) + (compare 2 3) + (compare 7 (+ 2 3)) + (define next (let ((cnt 0)) (lambda () (set! cnt (+ cnt 1)) cnt))) + (list (next) (next)) + (define new-counter (lambda () (let ((x 0)) (lambda () (set! x (+ x 1)) x)))) + (define c1 (new-counter)) + (define c2 (new-counter)) + (list (c1) (c1) (c2) (c1) (c2) (c1) (c1) (c2) (c2)) + (let* ((x (+ 2 3)) (y (* x x)) (z (+ x y))) (list x y z)) + (letrec ((even? (lambda (n) (if (= n 0) #t (odd? (- n 1))))) + (odd? (lambda (n) (if (= n 0) #f (even? (- n 1)))))) + (even? 88)) + (define (len l) (if (null? l) 0 (+ 1 (len (cdr l))))) + (len '(2 3 9)) + (define (len2 l) (match l (() 0) ((x . t) (+ 1 (len2 t))))) + (len2 '(2 3 9)) + (define (foo x) (match x + (('zero) 0) + (('succ x) (+ (foo x) 1)) + (('plus x y) (+ (foo x) (foo y))) + (('minus x y) (- (foo x) (foo y))) + (x x))) + (foo '(plus (succ (zero)) (minus (succ (succ 5)) 3))) + (define (bar x) (match x + (['zero] 0) + (['succ x] (+ (bar x) 1)) + (['plus x y] (+ (bar x) (bar y))) + (['minus x y] (- (bar x) (bar y))) + (['const x] x))) + (bar '[plus [succ [zero]] [minus [succ [succ [const 5]]] [const 3]]]) + (define (map f l) (letrec + ((map-f (lambda (l) (match l + (() ()) + ((h . t) (cons (f h) (map-f t))))))) + (map-f l))) + (map (lambda (x) (* x (+ 2 x))) '(2 3 9)) + (define (make-promise proc) (let ((result-ready? #f) (result #f)) + (lambda () + (if result-ready? result + (let ((x (proc))) + (if result-ready? result + (begin (set! result x) (set! result-ready? #t) result))))))) + (define (force promise) (promise)) +) +// */ +// words for invoking Lisp definitions from Fift +// (args) def -- val +{ null rot map-quote cons lisp-dict @ rot run-definition +} : invoke-lisp-definition +{ atom lisp-dict-lookup 1 { @ invoke-lisp-definition } +} : (invoke-lisp) +{ bl word (invoke-lisp) } :: invoke-lisp +// ( 2 3 ) invoke-lisp compare .l +{ atom lisp-dict-lookup 2 { @ mklist-1 invoke-lisp-definition } +} : (invoke-lisp-fixed) +{ bl word (invoke-lisp-fixed) } :: invoke-lisp-fixed +// 9 8 2 invoke-lisp-fixed compare .l +{ bl word (invoke-lisp) does } : make-lisp-invoker +{ bl word (invoke-lisp-fixed) does } : make-lisp-fixed-invoker +// 2 make-lisp-fixed-invoker compare : compare +// 3 9 compare +// import Lisp definitions as Fift words +{ bl word dup (invoke-lisp) does swap 0 (create) } : import-lisp +{ bl word tuck (invoke-lisp-fixed) does swap 0 (create) } : import-lisp-fixed +// 1 import-lisp-fixed fact +// 7 fact . diff --git a/src/test/fuzzer/src/minimal-fc-stdlib/fift-lib/Lists.fif b/src/test/fuzzer/src/minimal-fc-stdlib/fift-lib/Lists.fif new file mode 100644 index 0000000000..b59e40a0d9 --- /dev/null +++ b/src/test/fuzzer/src/minimal-fc-stdlib/fift-lib/Lists.fif @@ -0,0 +1,220 @@ +library Lists // List utilities +// +{ hole dup 1 { @ execute } does create } : recursive +// x x' -- ? recursively compares two S-expressions +recursive equal? { + dup tuple? { + over tuple? { + over count over count over = { // t t' l ? + 0 { dup 0>= { 2dup [] 3 pick 2 pick [] equal? { 1+ } { drop -1 } cond + } if } rot times + nip nip 0>= + } { drop 2drop false } cond + } { 2drop false } cond + } { eqv? } cond +} swap ! +// (a1 .. an) -- (an .. a1) +{ null swap { dup null? not } { uncons swap rot cons swap } while drop } : list-reverse +// (a1 .. an) -- an Computes last element of non-empty list l +{ { uncons dup null? { drop true } { nip false } cond } until } : list-last +// l l' -- l++l' Concatenates two lists +recursive list+ { + over null? { nip } { swap uncons rot list+ cons } cond +} swap ! +// l l' -- l'' -1 or 0, where l = l' ++ l'' +// Removes prefix from list +{ { dup null? { drop true true } { + swap dup null? { 2drop false true } { // l' l + uncons swap rot uncons -rot equal? { false } { + 2drop false true + } cond } cond } cond } until +} : list- +// (a1 .. an) -- a1 .. an n Explodes a list +{ 0 { over null? not } { swap uncons rot 1+ } while nip } : explode-list +// (a1 .. an) x -- a1 .. an n x Explodes a list under the topmost element +{ swap explode-list dup 1+ roll } : explode-list-1 +// l -- t Transforms a list into a tuple with the same elements +{ explode-list tuple } : list>tuple +// a1 ... an n x -- (a1 .. an) x +{ null swap rot { -rot cons swap } swap times } : mklist-1 +// (s1 ... sn) -- s1+...+sn Concatenates a list of strings +{ "" { over null? not } { swap uncons -rot $+ } while nip +} : concat-string-list +// (x1 ... xn) -- x1+...+xn Sums a list of integers +{ 0 { over null? not } { swap uncons -rot + } while nip +} : sum-list +// (a1 ... an) a e -- e(...e(e(a,a1),a2),...),an) +{ -rot { over null? not } { swap uncons -rot 3 pick execute } while nip nip +} : foldl +// (a1 ... an) e -- e(...e(e(a1,a2),a3),...),an) +{ swap uncons swap rot foldl } : foldl-ne +// (a1 ... an) a e -- e(a1,e(a2,...,e(an,a)...)) +recursive foldr { + rot dup null? { 2drop } { + uncons -rot 2swap swap 3 pick foldr rot execute + } cond +} swap ! +// (a1 ... an) e -- e(a1,e(a2,...,e(a[n-1],an)...)) +recursive foldr-ne { + over cdr null? { drop car } { + swap uncons 2 pick foldr-ne rot execute + } cond +} swap ! +// (l1 ... ln) -- l1++...++ln Concatenates a list of lists +{ dup null? { ' list+ foldr-ne } ifnot } : concat-list-lists +// (a1 .. an . t) n -- t Computes the n-th tail of a list +{ ' cdr swap times } : list-tail +// (a0 .. an ..) n -- an Computes the n-th element of a list +{ list-tail car } : list-ref +// l -- ? +{ { dup null? { drop true true } { + dup pair? { cdr false } { + drop false true + } cond } cond } until +} : list? +// l -- n +{ 0 { over null? not } { 1+ swap uncons nip swap } while nip +} : list-length +// l e -- t // returns tail of l after first member that satisfies e +{ swap { + dup null? { nip true } { + tuck car over execute { drop true } { + swap cdr false + } cond } cond } until +} : list-tail-from +// a l -- t // tail of l after first occurence of a using eq? +{ swap 1 ' eq? does list-tail-from } : list-member-eq +{ swap 1 ' eqv? does list-tail-from } : list-member-eqv +{ swap 1 ' equal? does list-tail-from } : list-member-equal +// a l -- ? +{ list-member-eq null? not } : list-member? +{ list-member-eqv null? not } : list-member-eqv? +// l -- a -1 or 0 // returns car l if l is non-empty +{ dup null? { drop false } { car true } cond +} : safe-car +{ dup null? { drop false } { car second true } cond +} : get-first-value +// l e -- v -1 or 0 +{ list-tail-from safe-car } : assoc-gen +{ list-tail-from get-first-value } : assoc-gen-x +// a l -- (a.v) -1 or 0 -- returns first entry (a . v) in l +{ swap 1 { swap first eq? } does assoc-gen } : assq +{ swap 1 { swap first eqv? } does assoc-gen } : assv +{ swap 1 { swap first equal? } does assoc-gen } : assoc +// a l -- v -1 or 0 -- returns v from first entry (a . v) in l +{ swap 1 { swap first eq? } does assoc-gen-x } : assq-val +{ swap 1 { swap first eqv? } does assoc-gen-x } : assv-val +{ swap 1 { swap first equal? } does assoc-gen-x } : assoc-val +// (a1 .. an) e -- (e(a1) .. e(an)) +recursive list-map { + over null? { drop } { + swap uncons -rot over execute -rot list-map cons + } cond +} swap ! + +variable ctxdump variable curctx +// (a1 .. an) e -- executes e for a1, ..., an +{ ctxdump @ curctx @ ctxdump 2! curctx 2! + { curctx 2@ over null? not } { swap uncons rot tuck curctx 2! execute } + while 2drop ctxdump 2@ curctx ! ctxdump ! +} : list-foreach +forget ctxdump forget curctx + +// +// Experimental implementation of `for` loops with index +// +variable loopdump variable curloop +{ curloop @ loopdump @ loopdump 2! } : push-loop-ctx +{ loopdump 2@ loopdump ! curloop ! } : pop-loop-ctx +// ilast i0 e -- executes e for i=i0,i0+1,...,ilast-1 +{ -rot 2dup > { + push-loop-ctx { + triple dup curloop ! first execute curloop @ untriple 1+ 2dup <= + } until pop-loop-ctx + } if 2drop drop +} : for +// ilast i0 e -- same as 'for', but pushes current index i before executing e +{ -rot 2dup > { + push-loop-ctx { + triple dup curloop ! untriple nip swap execute curloop @ untriple 1+ 2dup <= + } until pop-loop-ctx + } if 2drop drop +} : for-i +// ( -- i ) Returns innermost loop index +{ curloop @ third } : i +// ( -- j ) Returns outer loop index +{ loopdump @ car third } : j +{ loopdump @ cadr third } : k +forget curloop forget loopdump + +// +// create Lisp-style lists using words "(" and ")" +// +variable ') +'nop box constant ', +{ ") without (" abort } ') ! +{ ') @ execute } : ) +anon constant dot-marker +// m x1 ... xn t m -- (x1 ... xn . t) +{ swap + { -rot 2dup eq? not } + { over dot-marker eq? abort"invalid dotted list" + swap rot cons } while 2drop +} : list-tail-until-marker +// m x1 ... xn m -- (x1 ... xn) +{ null swap list-tail-until-marker } : list-until-marker +{ over dot-marker eq? { nip 2dup eq? abort"invalid dotted list" } + { null swap } cond + list-tail-until-marker +} : list-until-marker-ext +{ ') @ ', @ } : ops-get +{ ', ! ') ! } : ops-set +{ anon dup ops-get 3 { ops-set list-until-marker-ext } does ') ! 'nop ', ! +} : ( +// test of Lisp-style lists +// ( 42 ( `+ 9 ( `* 3 4 ) ) "test" ) .l cr +// ( `eq? ( `* 3 4 ) 3 4 * ) .l cr +// `alpha ( `beta `gamma `delta ) cons .l cr +// { ( `eq? ( `* 3 5 pick ) 3 4 roll * ) } : 3*sample +// 17 3*sample .l cr + +// similar syntax _( x1 .. xn ) for tuples +{ 2 { 1+ 2dup pick eq? } until 3 - nip } : count-to-marker +{ count-to-marker tuple nip } : tuple-until-marker +{ anon dup ops-get 3 { ops-set tuple-until-marker } does ') ! 'nop ', ! } : _( +// test of tuples +// _( _( 2 "two" ) _( 3 "three" ) _( 4 "four" ) ) .dump cr + +// pseudo-Lisp tokenizer +"()[]'" 34 hold constant lisp-delims +{ lisp-delims 11 (word) } : lisp-token +{ null cons `quote swap cons } : do-quote +{ 1 { ', @ 2 { 2 { ', ! execute ', @ execute } does ', ! } + does ', ! } does +} : postpone-prefix +{ ', @ 1 { ', ! } does ', ! } : postpone-', +( `( ' ( pair + `) ' ) pair + `[ ' _( pair + `] ' ) pair + `' ' do-quote postpone-prefix pair + `. ' dot-marker postpone-prefix pair + `" { char " word } pair + `;; { 0 word drop postpone-', } pair +) constant lisp-token-dict +variable eol +{ eol @ eol 0! anon dup ') @ 'nop 3 + { ops-set list-until-marker-ext true eol ! } does ') ! rot ', ! + { lisp-token dup (number) dup { roll drop } { + drop atom dup lisp-token-dict assq { nip second execute } if + } cond + ', @ execute + eol @ + } until + -rot eol ! execute +} :_ List-generic( +{ 'nop 'nop List-generic( } :_ LIST( +// LIST((lambda (x) (+ x 1)) (* 3 4)) +// LIST('(+ 3 4)) +// LIST(2 3 "test" . 9) +// LIST((process '[plus 3 4])) diff --git a/src/test/fuzzer/src/minimal-fc-stdlib/fift-lib/Stack.fif b/src/test/fuzzer/src/minimal-fc-stdlib/fift-lib/Stack.fif new file mode 100644 index 0000000000..c7ef6f8e58 --- /dev/null +++ b/src/test/fuzzer/src/minimal-fc-stdlib/fift-lib/Stack.fif @@ -0,0 +1,266 @@ +library Stack // advanced stack manupulation library +"Lists.fif" include +// S(a b c - a c 2 a b) would compile to code performing the requested stack manipulation + +// interface to low-level stack manipulation primitives +{ (number) 1- abort"index expected" dup 0 < over 255 > or + abort"index 0..255 expected" +} : (idx) +// push(n) : a0 .. an - a0 .. an a0 equivalent to "n pick" +// push(0) = dup, push(1) = over +{ 0 char ) word (idx) <push> } ::_ push( +// pop(n) : a0 a1 .. a(n-1) an - an a1 .. a(n-1) +// pop(0) = drop, pop(1) = nip +{ 0 char ) word (idx) <pop> } ::_ pop( +// xchg(i,j) : equivalent to "i j exch2" +{ 0 char , word (idx) char ) word (idx) <xchg> } ::_ xchg( +// xchg0(i) : equivalent to "i exch" or "xchg(0,i)" +// xchg0(1) = swap +{ 0 char ) word (idx) 0 <xchg> } ::_ xchg0( +forget (idx) + +// parser for stack notation expressions +")" 34 hold +" -" constant stk-delims +anon constant stk-start +anon constant stk-to +variable stk-mode +{ stk-delims 11 (word) } : stk-token +'nop : mk-lit +// stk-start vn ... v0 -- stk-start ... v0 i where v[i]=v0 +{ 0 { + 1+ 2dup 2+ pick dup stk-start eq? { 2drop drop 0 true } { eqv? } cond + } until +} : stk-lookup +// stk-start a1 .. an stk-to b1 .. bm -- [a1 .. an] [b1 .. bm] +{ stk-mode @ 0= abort"identifier expected" } : chk-lit +{ stk-to list-until-marker stk-mode ! + stk-start list-until-marker stk-mode @ + stk-mode 0! +} : build-stk-effect +{ stk-start stk-mode 0! { + stk-token dup ")" $= { drop true } { + dup "-" $= { + drop stk-mode @ abort"duplicate -" true stk-mode ! stk-to false } { + dup 34 chr $= { chk-lit drop char " word mk-lit false } { + dup (number) ?dup { chk-lit 1- { swap mk-lit -rot } if mk-lit nip false } { + atom dup `_ eq? { stk-mode @ abort"identifier expected" false } { + stk-lookup 0= stk-mode @ = { + stk-mode @ { atom>$ +" -?" } { atom>$ +" redefined" } cond abort } { + false + } cond } cond } cond } cond } cond } cond } until + stk-mode @ 0= abort"'-' expected" + build-stk-effect +} :_ parse-stk-list( + +// stack operation list construction +variable op-rlist +{ op-rlist null! } : clear-op-list +{ op-rlist @ list-reverse } : get-op-list +{ op-rlist @ cons op-rlist ! } : issue-op +{ minmax `xchg -rot triple } : op-xchg +{ `push swap pair } : op-push +{ `lit swap pair } : op-lit +{ `pop swap pair } : op-pop +0 op-pop constant op-drop +{ 2dup <> { op-xchg issue-op } if } : issue-xchg +{ op-push issue-op } : issue-push +{ op-lit issue-op } : issue-lit +{ op-pop issue-op } : issue-pop +{ op-drop issue-op } : issue-drop +{ ' issue-drop swap times } : issue-drop-# + +// emulated stack contents +variable emul-stk +{ emul-stk @ count } : emul-depth +{ emul-depth 1- swap - } : adj-i +{ emul-depth 1- tuck swap - swap rot - swap } : adj-ij +// i j -- +{ adj-ij 2dup emul-stk @ tuck swap [] swap rot [] rot // i sj si j + emul-stk @ -rot []= swap rot []= emul-stk ! +} : emul-xchg +{ emul-stk @ tpop drop emul-stk ! } : emul-drop +// i -- +{ 0 emul-xchg emul-drop } : emul-pop +// i -- s[i] +{ emul-stk @ swap [] } : emul-stk[] +// i -- si +{ adj-i emul-stk[] } : emul-get +{ 0 emul-get } : emul-tos +// v i -- ? Check whether s[i]=v +{ dup emul-depth < { emul-stk[] eqv? } { 2drop false } cond } : emul[]-eq? +// v -- i or -1 Returns maximum i with s[i]=v +{ emul-stk @ dup count { // v s i + ?dup 0= { -1 true } { 1- 2dup [] 3 pick eqv? } cond // v s i' ? + } until nip nip +} : emul-stk-lookup-rev +// i -- +{ emul-get emul-stk @ swap , emul-stk ! } : emul-push +{ emul-stk @ swap , emul-stk ! } : emul-lit +// show emulated stack contents similarly to .s +{ emul-stk @ explode dup 1 reverse ' .l swap times cr } : .e + +// both issue an operation and emulate it +{ 2dup issue-xchg emul-xchg } : issue-emul-xchg +{ dup issue-push emul-push } : issue-emul-push +{ dup issue-lit emul-lit } : issue-emul-lit +{ dup issue-pop emul-pop } : issue-emul-pop +{ issue-drop emul-drop } : issue-emul-drop +{ ' issue-emul-drop swap times } : issue-emul-drop-# + +// b.. s -- b.. s moves tos value to stk[s] +{ dup emul-stk[] 2 pick cdr list-member-eqv? { + dup adj-i 0 issue-emul-xchg } { dup adj-i issue-emul-pop } cond +} : move-tos-to + +// new s -- ops registered +{ { over null? not } { + // .sl .e get-op-list .l cr + // get-op-list list-length 100 > abort"too long" + emul-depth over > + { over emul-tos swap list-member-eqv? not } { false } cond { + // b.. s tos unneeded + issue-emul-drop } { + over car // b.. s b1 + 2dup swap emul[]-eq? { drop swap cdr swap 1+ } { + dup emul-stk-lookup-rev // b.. s b1 i + dup 0< { // b.. s b1 i not found, must be a literal + drop dup atom? abort"unavailable value" + issue-emul-lit } { + dup 3 pick < { // b.. s b1 i found in bottom s stack values + nip adj-i issue-emul-push // b.. s + dup emul-depth 1- < { move-tos-to } if + } { + emul-depth 1- over = { // b.. s b1 i found in tos + 2drop move-tos-to + } { // b.. s b1 i + nip over adj-ij issue-emul-xchg + } cond } cond } cond } cond } cond } while + nip emul-depth swap - issue-emul-drop-# +} : generate-reorder-ops + +// old new -- op-list +{ emul-stk @ op-rlist @ 2swap + swap list>tuple emul-stk ! clear-op-list + 0 generate-reorder-ops get-op-list + -rot op-rlist ! emul-stk ! +} : generate-reorder +{ parse-stk-list( generate-reorder } :_ SG( + +// op-list rewriting according to a ruleset +// l f l1 l2 -- l' -1 or l f with l' = l2 + (l - l1) +{ push(3) rot list- { list+ nip nip true } { drop } cond +} : try-rule +// l f ll -- l' -1 or l f +{ { dup null? not } { uncons 3 -roll unpair try-rule rot } while drop +} : try-ruleset +// l ll -- l' +{ swap { over false swap try-ruleset 0= } until nip +} : try-ruleset* +// l ruleset -- l' +recursive try-ruleset*-everywhere { + tuck try-ruleset* dup null? { nip } { + uncons rot try-ruleset*-everywhere cons } cond +} swap ! +LIST( + [([xchg 0 1] [xchg 0 2]) ([rot])] + [([xchg 0 1] [xchg 1 2]) ([-rot])] + [([xchg 0 2] [xchg 1 2]) ([rot])] + [([xchg 0 2] [xchg 0 1]) ([-rot])] + [([xchg 1 2] [xchg 0 1]) ([rot])] + [([xchg 1 2] [xchg 0 2]) ([-rot])] + [([xchg 0 1] [rot]) ([xchg 0 2])] + [([-rot] [xchg 0 1]) ([xchg 0 2])] + [([xchg 0 2] [xchg 1 3]) ([2swap])] + [([xchg 1 3] [xchg 0 2]) ([2swap])] + [([push 1] [push 1]) ([2dup])] + [([push 3] [push 3]) ([2over])] + [([pop 0] [pop 0]) ([2drop])] + [([pop 1] [pop 0]) ([2drop])] + [([xchg 0 1] [push 1]) ([tuck])] + [([rot] [-rot]) ()] + [([-rot] [rot]) ()] +) constant fift-stack-ruleset +{ fift-stack-ruleset try-ruleset*-everywhere } : fift-ops-rewrite +{ SG( fift-ops-rewrite } :_ SGF( + +// helpers for creating Fift source strings for one fift-op +// i j -- s +{ minmax over { "xchg(" rot (.) $+ +"," swap (.) $+ +")" } + { nip dup 1 = { drop "swap" } { + ?dup { "xchg0(" swap (.) $+ +")" } { "" } cond + } cond } cond +} : source-<xchg> +// i -- s +{ dup 1 = { drop "over" } { + ?dup { "push(" swap (.) $+ +")" } { "dup" } cond + } cond +} : source-<push> +// i -- s +{ dup 1 = { drop "nip" } { + ?dup { "pop(" swap (.) $+ +")" } { "drop" } cond + } cond +} : source-<pop> +// lit -- s +{ dup string? { char " chr swap $+ char " hold } { (.) } cond +} : source-<lit> + +// dictionary with all fift op compilation/source creation +{ 0 swap (compile) } : fop-compile +( _( `xchg 2 { <xchg> fop-compile } { source-<xchg> swap cons } ) + _( `push 1 { <push> fop-compile } { source-<push> swap cons } ) + _( `pop 1 { <pop> fop-compile } { source-<pop> swap cons } ) + _( `lit 1 { 1 'nop (compile) } { source-<lit> swap cons } ) + _( `rot 0 { ' rot fop-compile } { "rot" swap cons } ) + _( `-rot 0 { ' -rot fop-compile } { "-rot" swap cons } ) + _( `tuck 0 { ' tuck fop-compile } { "tuck" swap cons } ) + _( `2swap 0 { ' 2swap fop-compile } { "2swap" swap cons } ) + _( `2drop 0 { ' 2drop fop-compile } { "2drop" swap cons } ) + _( `2dup 0 { ' 2dup fop-compile } { "2dup" swap cons } ) + _( `2over 0 { ' 2over fop-compile } { "2over" swap cons } ) +) box constant fift-op-dict + +{ dup atom? { atom>$ } { drop "" } cond + "unknown operation " swap $+ abort +} : report-unknown-op +variable 'fop-entry-exec +// process fift-op according to 'fop-entry-exec +// ... op - ... +{ dup first dup fift-op-dict @ assq { report-unknown-op } ifnot + dup second 1+ push(3) count <> abort"incorrect param count" + nip swap explode dup roll drop 1- roll // o2 .. on entry + 'fop-entry-exec @ execute +} : process-fift-op + +// compile op-list into Fift wordlist +// wl op-list -- wl' +{ { third execute } 'fop-entry-exec ! + swap ' process-fift-op foldl } : compile-fift-op* +// op-list -- e +{ fift-ops-rewrite ({) swap compile-fift-op* (}) } : ops>wdef + +// S(<orig-stack> - <new-stack>) compiles a "word" performing required action +{ SG( ops>wdef 0 swap } ::_ S( +// 1 2 3 S(a b c - c a b a) .s would print 3 1 2 1 + +// transform op-list into Fift source +// ls op -- ls' +{ fift-ops-rewrite + { 3 [] execute } 'fop-entry-exec ! + null ' process-fift-op foldl + dup null? { drop "" } { { +" " swap $+ } foldr-ne } cond +} : ops>$ +{ SG( ops>$ 1 'nop } ::_ $S( +{ SG( ops>$ type } :_ .$S( +// $S(a b c - b c a c a c) => string "rot 2dup over" +// S(a b c - b c a c a c) => compile/execute block { rot 2dup over } +// $S(_ x y _ - y x) => string "drop pop(2)" +// .$S(x1 x2 - 17 x1) => print string "drop 17 swap" + +// simplify/transform sequences of stack manipulation operations +LIST(. [a b c d e f g h i j]) constant std-stack +{ stk-start std-stack explode drop stk-to std-stack explode drop +} : simplify<{ +{ build-stk-effect generate-reorder ops>$ } : }>stack +// simplify<{ drop drop over over -13 }>stack => string "2drop 2dup -13" +// simplify<{ 17 rot }>stack => string "swap 17 swap" +// simplify<{ 5 1 reverse }>stack => string "xchg(1,5) xchg(2,4)" diff --git a/src/test/fuzzer/src/minimal-fc-stdlib/fift-lib/TonUtil.fif b/src/test/fuzzer/src/minimal-fc-stdlib/fift-lib/TonUtil.fif new file mode 100644 index 0000000000..f31b591eb3 --- /dev/null +++ b/src/test/fuzzer/src/minimal-fc-stdlib/fift-lib/TonUtil.fif @@ -0,0 +1,381 @@ +library TonUtil // TON Blockchain Fift Library +"Lists.fif" include + +-1 constant Masterchain +0 constant Basechain + +// parse workchain id +// ( S -- workchain ) +{ (number) 1- abort"workchain id must be an integer" + dup 32 fits not abort"workchain id must fit in 32 bits" +} : parse-workchain-id + +{ (number) 1- abort"integer expected" } : parse-int + +{ over null? ' swap if drop } : replace-if-null + +// Private key load/generate +// ( fname -- pubkey privkey ) +{ dup ."Loading private key from file " type cr + file>B dup Blen 32 <> abort"Private key must be exactly 32 bytes long" + dup priv>pub swap +} : load-keypair +// ( fname -- pubkey privkey ) +{ dup file-exists? + { load-keypair } + { dup newkeypair swap rot over swap B>file + rot ."Saved new private key to file " type cr + } cond +} : load-generate-keypair + +// Parse smart-contract address +// ( S -- workchain addr bounce? ) +{ $>smca not abort"invalid smart-contract address" + 1 and 0= +} : parse-smc-addr + +// ( x -- ) Displays a 64-digit hex number +{ 64 0x. } : 64x. +{ 64 0X. } : 64X. +// ( wc addr -- ) Show address in <workchain>:<account> form +{ swap ._ .":" 64x. } : .addr +// ( wc addr flags -- ) Show address in base64url form +{ smca>$ type } : .Addr +// ( wc addr fname -- ) Save address to file in 36-byte format +{ -rot 256 u>B swap 32 i>B B+ swap B>file } : save-address +// ( wc addr fname -- ) Save address and print message +{ dup ."(Saving address to file " type .")" cr save-address +} : save-address-verbose + +// ( fname -- wc addr ) Load address from file +{ file>B 32 B| + dup Blen { 32 B>i@ } { drop Basechain } cond + swap 256 B>u@ +} : load-address +// ( fname -- wc addr ) Load address from file and print message +{ dup ."(Loading address from file " type .")" cr load-address +} : load-address-verbose +// Parse string as address or load address from file (if string is prefixed by @) +// ( S default-bounce -- workchain addr bounce? ) +{ over $len 0= abort"empty smart-contract address" + swap dup 1 $| swap "@" $= + { nip load-address rot } { drop nip parse-smc-addr } cond +} : parse-load-address + +// ( hex-str -- addr ) Parses ADNL address +{ dup $len 64 <> abort"ADNL address must consist of exactly 64 hexadecimal characters" + (hex-number) 1 <> abort"ADNL address must consist of 64 hexadecimal characters" + dup 256 ufits not abort"invalid ADNL address" +} : parse-adnl-address + +// ( b wc addr -- b' ) Serializes address into Builder b +{ -rot 8 i, swap 256 u, } : addr, +{ over 8 fits { rot b{100} s, -rot addr, } { + rot b{110} s, 256 9 u, rot 32 i, swap 256 u, } cond +} : Addr, + +// Gram utilities +1000000000 constant Gram +{ Gram swap */r } : Gram*/ +{ Gram * } : Gram* +{ (number) dup { 1- ' Gram*/ ' Gram* cond true } if +} : $>GR? +// ( S -- nanograms ) +{ $>GR? not abort"not a valid Gram amount" +} : $>GR +{ bl word $>GR 1 'nop } ::_ GR$ +// ( nanograms -- S ) +{ dup abs <# ' # 9 times char . hold #s rot sign #> +nip -trailing0 } : (.GR) +{ (.GR) ."GR$" type } : .GR_ +{ .GR_ space } : .GR + +// b x -- b' ( serializes a Gram amount ) +{ -1 { 1+ 2dup 8 * ufits } until + rot over 4 u, -rot 8 * u, } : Gram, +// s -- x s' ( deserializes a Gram amount ) +{ 4 u@+ swap 8 * u@+ } : Gram@+ +// s -- x +{ 4 u@+ swap 8 * u@ } : Gram@ + +// currency collections +// b x --> b' ( serializes a VarUInteger32 ) +{ -1 { 1+ 2dup 8 * ufits } until + rot over 5 u, -rot 8 * u, } : VarUInt32, +// s --> x ( deserializes a VarUInteger32 ) +{ 5 u@+ swap 8 * u@ } : VarUInt32@ +32 constant cc-key-bits +' VarUInt32, : val, +' VarUInt32@ : val@ +// d k v -- d' +{ <b swap val, b> <s swap rot cc-key-bits idict!+ not abort"cannot add key-value to CurrencyCollection" +} : +newccpair +{ dup { -rot tuck swap cc-key-bits idict@- { val@ 2swap -rot + } { swap rot } cond +newccpair + } { 2drop } cond +} : +ccpair +dictnew constant cc0 // zero currency collection +// ( v k -- d ) Creates currency collection representing v units of currency k +{ cc0 swap rot +ccpair } : of-cc +{ dictnew { over null? not } { swap uncons -rot unpair +ccpair } while nip } : list>cc +{ dup null? { ."(null)" drop } { val@ ._ } cond } dup : .maybeVarUInt32 : .val +{ swap cc-key-bits { rot { ."+" } if .val ."*$" ._ true true } idictforeach drop } : (.cc) +{ false (.cc) { ."0" } ifnot } : .cc_ +{ .cc_ space } : .cc +{ true (.cc) drop } : .+cc_ +{ .+cc_ space } : .+cc +{ cc-key-bits { rot . ."-> " swap .val .val ."; " true } dictdiff drop cr } : show-cc-diff +{ cc-key-bits { val@ swap val@ + val, true } dictmerge } : cc+ +{ null swap cc-key-bits { val@ pair swap cons true } idictforeach drop } : cc>list-rev +{ cc>list-rev list-reverse } : cc>list +forget val, forget val@ forget .val + +// ( S -- x -1 or 0 ) +{ (number) dup 2 = { -rot 2drop } if 1 = } : int? +{ int? dup { drop dup 0< { drop false } { true } cond } if } : pos-int? +// ( S -- k v -1 or 0 ) Parses expression <value>*<currency> or <value>*$<currency> +{ dup "*" $pos dup 0< { 2drop false } { + $| dup $len 2 < { 2drop false } { + 1 $| nip dup 1 $| swap "$" $= { swap } if drop + int? dup { over 32 fits { 2drop false } ifnot } if + not { drop false } { + swap pos-int? not { drop false } { + true + } cond } cond } cond } cond +} : cc-key-value? +// ( S -- D -1 or 0 ) Parses an extra currency collection +// e.g. "10000*$3+7777*$-11" means "10000 units of currency #3 and 7777 units of currency #-11" +{ dictnew { // S D + swap dup "+" $pos dup 0< { drop null -rot } { $| 1 $| nip -rot } cond + cc-key-value? { +ccpair over null? dup { rot drop true } if } { 2drop false true } cond + } until +} : $>xcc? +{ $>xcc? not abort"invalid extra currency collection" } : $>xcc +{ char } word dup $len { $>xcc } { drop dictnew } cond 1 'nop } ::_ CX{ + +// complete currency collections +{ $>xcc? { true } { drop false } cond } : end-parse-cc +// ( S -- x D -1 or 0 ) Parses a currency collection +// e.g. "1.2+300*$2" means "1200000000ng plus 300 units of currency #2" +{ 0 swap dup "+" $pos dup 0< { drop dup + $>GR? { nip nip dictnew true } { end-parse-cc } cond + } { over swap $| swap $>GR? { 2swap 2drop swap 1 $| nip } { drop + } cond end-parse-cc } cond +} : $>cc? +{ $>cc? not abort"invalid currency collection" } : $>cc +{ char } word dup $len { $>cc } { drop 0 dictnew } cond 2 'nop } ::_ CC{ +// ( x D -- ) +{ swap ?dup { .GR_ .+cc_ } { .cc_ } cond } : .GR+cc_ +{ .GR+cc_ space } : .GR+cc +{ -rot Gram, swap dict, } : Gram+cc, + +// Libraries +// ( -- D ) New empty library collection +' dictnew : Libs{ +// ( D -- D ) Return library collection as dictionary +'nop : }Libs +// ( D c x -- D' ) Add a public/private library c to collection D +{ <b swap 1 u, over ref, b> <s swap hash rot 256 udict!+ + 0= abort"duplicate library in collection" } : lib+ +// ( D c -- D' ) Add private library c to collection D +{ 0 lib+ } : private_lib +// ( D c -- D' ) Add public library c to collection D +{ 1 lib+ } : public_lib + +// serialize simple transfers with long comments +// b B n -- b' +recursive append-long-bytes { + over Blen over <= { drop B, } { + B| <b swap 127 append-long-bytes b> -rot B, swap ref, + } cond +} swap ! +// b S n -- b' +{ swap $>B swap append-long-bytes } : append-long-string +// S -- c +{ <b over $len { 0 32 u, swap 36 append-long-string } { nip } cond b> +} : simple-transfer-body + +// ( S -- x ) parse public key +{ dup $len 48 <> abort"public key must be 48 characters long" + base64url>B dup Blen 36 <> abort"public key must be 48 characters long" + 34 B| 16 B>u@ over crc16 <> abort"crc16 mismatch in public key" + 16 B>u@+ 0x3ee6 <> abort"invalid tag in public key" + 256 B>u@ +} : parse-pubkey +{ bl word parse-pubkey 1 'nop } ::_ PK' +// ( x -- S ) serialize public key +{ 256 u>B B{3ee6} swap B+ dup crc16 16 u>B B+ B>base64 } : pubkey>$ +{ pubkey>$ type } : .pubkey + +// ( S -- x ) parse validator-encoded public key +{ base64>B dup Blen 36 <> abort"public key with magic must be 36 bytes long" + 4 B| swap 32 B>u@ 0xC6B41348 <> abort"unknown magic for public key (not Ed25519)" +} : parse-val-pubkey +{ bl word parse-val-pubkey 1 'nop } ::_ VPK' +{ char } word base64>B 1 'nop } ::_ B64{ + +// adnl address parser +{ 256 u>B B{2D} swap B+ dup crc16 16 u>B B+ } : adnl-preconv +{ swap 32 /mod dup 26 < { 65 } { 24 } cond + rot swap hold } : Base32# +{ <# ' Base32# 8 times #> } : Base32#*8 +{ "" over Blen 5 / { swap 40 B>u@+ Base32#*8 nip rot swap $+ } swap times nip } : B>Base32 + +// ( x -- S ) Converts an adnl-address from a 256-bit integer to a string +{ adnl-preconv B>Base32 1 $| nip } : adnl>$ + +{ 65 - dup 0>= { -33 and dup 26 < } { 41 + dup 25 > over 32 < and } cond ?dup nip } : Base32-digit? +{ Base32-digit? not abort"not a Base32 digit" } : Base32-digit +{ 0 { over $len } { swap 1 $| -rot (char) Base32-digit swap 5 << + } while nip } : Base32-number +{ B{} { over $len } { swap 8 $| -rot Base32-number 40 u>B B+ } while nip } : Base32>B + +// ( S -- x ) Converts an adnl address from a string to 256-bit integer +{ dup $len 55 <> abort"not 55 alphanumeric characters" "F" swap $+ Base32>B + 33 B| 16 B>u@ over crc16 <> abort"crc16 checksum mismatch" + 8 B>u@+ 0x2D <> abort"not a valid adnl address" 256 B>u@ } : $>adnl + +{ 65 - dup 0>= { -33 and 10 + dup 16 < } { 17 + dup 0>= over 10 < and } cond ?dup nip } : hex-digit? +// ( S -- x -1 or 0 ) Parses a hexadecimal integer +{ dup $len { + 0 { + 4 << swap 1 $| -rot (char) hex-digit? // S a d -1 or S a 0 + { + over $len 0= } { drop -1 true } cond + } until + dup 0< { 2drop false } { nip true } cond + } { drop false } cond +} : hex$>u? +// ( S -- x ) +{ hex$>u? not abort"not a hexadecimal number" } : hex$>u + +{ dup $len 64 = { hex$>u } { + dup $len 55 = { $>adnl } { + true abort"invalid adnl address" + } cond } cond +} : parse-adnl-addr +{ adnl>$ type } : .adnl +{ bl word parse-adnl-addr 1 'nop } ::_ adnl: + +// ( x a b -- a<=x<=b ) +{ 2 pick >= -rot >= and } : in-range? + +// ( c i -- ? ) Checks whether c is a valid value for config param #i +def? config-valid? { + { nip 0>= { ."warning: cannot check validity of configuration parameter value, use create-state instead of fift to check validity" cr } if + true } : config-valid? +} ifnot + +{ dup -1000 = { drop <s ref@ <s 12 u@ 0xFF0 = } { + dup -1001 = { drop <s ref@ <s 12 u@ 0xFF0 = } { + over null? { 2drop true } { + config-valid? + } cond } cond } cond +} : is-valid-config? + + +// Get anycast depth / rewrite_pfx or return 0 +// ( S -- x y S ) +{ + // maybe + 1 u@+ swap 0 > + { + // anycast_info$_ depth:(#<= 30) { depth >= 1 } + // rewrite_pfx:(bits depth) = Anycast; + 30 u@+ swap // get depth + + dup 1 > { + dup 2 roll swap u@+ // get rewrite_pfx + // return depth, rewrite_pfx, slice + } + { + drop // drop depth (<=1) + 0 0 2 roll // set anycast to none + } cond + } + { + 0 0 2 roll // set anycast to none + } cond +} : maybe-anycast + +// Rewrite first bits of addr with anycast info +{ // input: anycast depth, rewrite_pfx, workchain, slice, address length + 4 -roll + 3 roll dup dup 0 = { 2drop 2 roll drop } + { + rot swap u@+ swap drop + 3 roll + <b swap 3 roll u, b> <s swap |+ + } cond // rewrite first bits of address with rewrite_pfx + 2 roll + u@+ // get address +} : parse-address-with-anycast + +// Parse Slice S and return: +// 0 `addr_none S - if addr_none$00 is parsed +// addr `addr_extern S - if addr_extern$01 is parsed +// wc addr `addr_std S - if addr_std$10 is parsed +// wc addr `addr_var S - if addr_var$11 is parsed +// ( S -- 0 A S or addr A S or wc addr A S ) +{ 2 u@+ swap dup 0> // Get addr: addr_none$00 / addr_extern$01 / addr_std$10 / addr_var$11 + { // if greater that zero + dup 1 > + { + 2 = + { + // if addr_std$10 + // anycast:(Maybe Anycast) + // workchain_id:int8 + // address:bits256 = MsgAddressInt; + maybe-anycast // get anycast depth, bits, slice + 8 i@+ // get workchain + 256 parse-address-with-anycast + `addr-std swap + } + + { + // if addr_var$11 + // anycast:(Maybe Anycast) + // addr_len:(## 9) + // workchain_id:int32 + // address:(bits addr_len) = MsgAddressInt; + maybe-anycast // get anycast depth, bits, slice + 9 u@+ // get addr_len + 32 i@+ // get workchain + swap 2 -roll // move workchain to neede position + swap parse-address-with-anycast + `addr-var swap + } cond + + } + { + drop // drop header (dup for statment upper) + // if addr_extern$01 + // addr_extern$01 len:(## 9) + // external_address:(bits len) + 9 u@+ swap // bit len + u@+ // external_address + `addr-extern swap + } cond + } + { + swap + // if addr_none$00 + `addr-none swap + } cond +} : addr@+ + +{ addr@+ drop } : addr@ + +// User-friendly prints output of addr@ +// (0 A or addr A or wc addr A -- ) +{ + dup `addr-none eq? + { 2drop ."addr_none" } + { + `addr-extern eq? + { (dump) type } + { (x.) swap (dump) ":" $+ swap $+ type } + cond + } + cond +} : print-addr // print addr with workchain + +forget maybe-anycast +forget parse-address-with-anycast diff --git a/src/test/fuzzer/src/minimal-fc-stdlib/funcplusfift b/src/test/fuzzer/src/minimal-fc-stdlib/funcplusfift new file mode 100755 index 0000000000..77e0449436 Binary files /dev/null and b/src/test/fuzzer/src/minimal-fc-stdlib/funcplusfift differ diff --git a/src/test/fuzzer/src/minimal-fc-stdlib/stdlib.fc b/src/test/fuzzer/src/minimal-fc-stdlib/stdlib.fc new file mode 100644 index 0000000000..29dfb07a09 --- /dev/null +++ b/src/test/fuzzer/src/minimal-fc-stdlib/stdlib.fc @@ -0,0 +1,71 @@ +cell get_data() asm "c4 PUSH"; + +slice begin_parse(cell c) asm "CTOS"; + +slice skip_bits(slice s, int len) asm "SDSKIPFIRST"; +(slice, ()) ~skip_bits(slice s, int len) asm "SDSKIPFIRST"; + +int slice_bits(slice s) asm "SBITS"; + +(slice, slice) load_msg_addr(slice s) asm(-> 1 0) "LDMSGADDR"; + +forall X -> X null() asm "PUSHNULL"; + +(slice, cell) load_ref(slice s) asm(-> 1 0) "LDREF"; + +builder begin_cell() asm "NEWC"; + +() set_data(cell c) impure asm "c4 POP"; + +cell end_cell(builder b) asm "ENDC"; + +builder store_dict(builder b, cell c) asm(c b) "STDICT"; + +(slice, cell) load_dict(slice s) asm(-> 1 0) "LDDICT"; + +(cell, int) idict_delete?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIDEL"; +(slice, int) idict_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIGET" "NULLSWAPIFNOT"; + +cell idict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTISET"; +(cell, ()) ~idict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTISET"; + +cell idict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTISETREF"; +(cell, ()) ~idict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTISETREF"; + +(cell, int) idict_get_ref?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIGETREF" "NULLSWAPIFNOT"; + +cell idict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTISETB"; +(cell, ()) ~idict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTISETB"; +cell dict_set_builder(cell dict, int key_len, slice index, builder value) asm(value index dict key_len) "DICTSETB"; +(cell, ()) ~dict_set_builder(cell dict, int key_len, slice index, builder value) asm(value index dict key_len) "DICTSETB"; + +(slice, int) load_coins(slice s) asm(-> 1 0) "LDVARUINT16"; +builder store_coins(builder b, int x) asm "STVARUINT16"; + +(slice, int) load_varuint16(slice s) asm(-> 1 0) "LDVARUINT16"; + +builder store_slice(builder b, slice s) asm "STSLICER"; + +(slice, int) udict_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUGET" "NULLSWAPIFNOT"; +(cell, int) udict_delete?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUDEL"; + +cell udict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUSETB"; +(cell, ()) ~udict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUSETB"; + +cell udict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUSET"; +(cell, ()) ~udict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUSET"; + +(cell, int) udict_get_ref?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUGETREF" "NULLSWAPIFNOT"; + +cell udict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTUSETREF"; +(cell, ()) ~udict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTUSETREF"; + +int cell_hash(cell c) asm "HASHCU"; + +int slice_hash(slice s) asm "HASHSU"; + +int string_hash(slice s) asm "SHA256U"; + +int equal_slices_bits(slice a, slice b) asm "SDEQ"; + +cell my_code() asm "MYCODE"; \ No newline at end of file diff --git a/src/test/fuzzer/src/minimal-fc-stdlib/stdlib_ex.fc b/src/test/fuzzer/src/minimal-fc-stdlib/stdlib_ex.fc new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/src/test/fuzzer/src/minimal-fc-stdlib/stdlib_ex.fc @@ -0,0 +1 @@ + diff --git a/src/test/fuzzer/src/scope.ts b/src/test/fuzzer/src/scope.ts new file mode 100644 index 0000000000..ab71e2a3fc --- /dev/null +++ b/src/test/fuzzer/src/scope.ts @@ -0,0 +1,453 @@ +import type { Type } from "@/test/fuzzer/src/types"; +import { getReturnType, tyEq } from "@/test/fuzzer/src/types"; +import type { GenerativeEntity } from "@/test/fuzzer/src/generators"; +import type * as Ast from "@/ast/ast"; +import type { NamedGenerativeEntity } from "@/test/fuzzer/src/generators/generator"; + +export type ScopeKind = + | "program" + | "trait" + | "contract" + | "function" + | "method" + | "receive" + | "block"; + +const namedScopeItemKinds = [ + "field", + "contract", + "trait", + "struct", + "message", + "constantDecl", + "constantDef", + "functionDecl", + "functionDef", + "methodDecl", + "methodDef", + "let", + "parameter", +] as const; +export type NamedScopeItemKind = (typeof namedScopeItemKinds)[number]; + +/* +function isNamedScopeItemKind(val: string): val is NamedScopeItemKind { + return namedScopeItemKinds.find((tpe) => tpe === val) ? true : false; +} +*/ + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +//const unnamedScopeItemKinds = ["statement", "receive"] as const; +//export type UnnamedScopeItemKind = (typeof unnamedScopeItemKinds)[number]; + +export type ScopeItemKind = NamedScopeItemKind; //| UnnamedScopeItemKind; + +/** Maps each ScopeItemKind to its respective GenerativeEntity specialization. */ +type NamedGenerativeEntityMap = { + let: NamedGenerativeEntity<Ast.Statement>; + parameter: NamedGenerativeEntity<Ast.TypedParameter>; + struct: NamedGenerativeEntity<Ast.StructDecl>; + message: NamedGenerativeEntity<Ast.MessageDecl>; + constantDecl: NamedGenerativeEntity<Ast.ConstantDecl>; + constantDef: NamedGenerativeEntity<Ast.ConstantDef>; + functionDecl: NamedGenerativeEntity<Ast.FunctionDecl>; + functionDef: NamedGenerativeEntity<Ast.FunctionDef>; + methodDecl: NamedGenerativeEntity<Ast.FunctionDecl>; + methodDef: NamedGenerativeEntity<Ast.FunctionDef>; + field: NamedGenerativeEntity<Ast.FieldDecl>; + contract: NamedGenerativeEntity<Ast.Contract>; + trait: NamedGenerativeEntity<Ast.Trait>; +}; +/*type GenerativeEntityMap = { + statement: GenerativeEntity<Ast.Statement>; + receive: GenerativeEntity<Ast.Receiver>; +};*/ + +/** + * Scope contains AST entries generated during the bottom-up AST generation and + * provides an information to access data in parent scopes. + */ +export class Scope { + kind: ScopeKind; + + /** Reference to the parent scope. `undefined` for the top-level scope. */ + readonly parentScope?: Scope; + + /** + * Contains AST entries generated during the bottom-up AST generation. + * + private mapUnnamed: Map< + UnnamedScopeItemKind, + Map<number, GenerativeEntity<any>> // eslint-disable-line @typescript-eslint/no-explicit-any + > = new Map(); + */ + + private mapNamed: Map< + NamedScopeItemKind, + Map<string, NamedGenerativeEntity<any>> // eslint-disable-line @typescript-eslint/no-explicit-any + > = new Map(); + + constructor(kind: ScopeKind, parentScope: Scope | undefined) { + this.kind = kind; + this.parentScope = parentScope; + } + + public isProgramScope(): boolean { + return this.parentScope === undefined; + } + + /** + * Returns the top-level scope. + */ + public getProgramScope(): Scope { + return this.isProgramScope() + ? this + : this.parentScope!.getProgramScope(); + } + + /** + * Returns the contract-level scope or `undefined` if it is not possible to reach it from the current scope. + */ + public getContractScope(): Scope | undefined { + if (this.isContractScope()) { + return this; + } + if (this.parentScope === undefined) { + return undefined; + } + return this.parentScope!.getContractScope(); + } + + public isContractScope(): boolean { + return ( + this.parentScope !== undefined && + this.parentScope.isProgramScope() && + this.kind === "contract" + ); + } + + /** + * Determine the appropriate parent scope based on the kind of entity + */ + private getTargetScopeToAdd(kind: ScopeItemKind) { + let targetScope: Scope | undefined; + switch (kind) { + case "let": + case "parameter": + //case "statement": + // eslint-disable-next-line @typescript-eslint/no-this-alias + targetScope = this; + break; + case "constantDecl": + case "constantDef": + targetScope = this.findParent("trait", "contract", "program"); + break; + case "functionDecl": + case "functionDef": + case "trait": + targetScope = this.findParent("program"); + break; + case "methodDecl": + case "methodDef": + case "field": + //case "receive": + targetScope = this.findParent("trait", "contract"); + break; + case "contract": + case "struct": + case "message": + targetScope = this.findParent("program"); + break; + default: + throw new Error("Unsupported kind for adding to scope."); + } + if (targetScope === undefined) { + throw new Error(`Cannot add "${kind}" to the "${this.kind}" scope`); + } + return targetScope; + } + + /** + * Put a new entity in the scope according to the Tact semantics. + * + public addUnnamed<T extends UnnamedScopeItemKind>( + _kind: T, + _entity: GenerativeEntityMap[T], + ): void { + throw new Error("Currently not supported"); + // const targetScope = this.getTargetScopeToAdd(kind); + // if (targetScope.mapUnnamed.has(kind)) { + // targetScope.mapUnnamed.get(kind)!.set(entity.idx, entity); + // } else { + // targetScope.mapUnnamed + // .set(kind, new Map()) + // .get(kind)! + // .set(entity.idx, entity); + // } + }*/ + + /** + * Put a new entity in the scope according to the Tact semantics. + */ + public addNamed<T extends NamedScopeItemKind>( + kind: T, + entity: NamedGenerativeEntityMap[T], + ): void { + const targetScope = this.getTargetScopeToAdd(kind); + + //if (isNamedScopeItemKind(kind)) { + if (targetScope.mapNamed.has(kind)) { + targetScope.mapNamed.get(kind)!.set(entity.name.text, entity); + } else { + targetScope.mapNamed + .set(kind, new Map()) + .get(kind)! + .set(entity.name.text, entity); + } + //} + } + + /* + public getAllUnnamed<T extends UnnamedScopeItemKind>( + _kind: T, + ): GenerativeEntityMap[T][] { + throw new Error("Currently not supported"); + // const kindMap = this.mapUnnamed.get(kind); + // if (kindMap) { + // return Array.from(kindMap.values()); + // } + // return []; + }*/ + + public getAllNamed<T extends NamedScopeItemKind>( + kind: T, + ): NamedGenerativeEntityMap[T][] { + const kindMap = this.mapNamed.get(kind); + if (kindMap) { + return Array.from(kindMap.values()); + } + return []; + } + + /** + * Collects name-type tuples of all the entries with the given type defined within this scope. + */ + public getNamedEntries(kind: NamedScopeItemKind): [string, Type][] { + const names = this.mapNamed.get(kind); + if (names === undefined) { + return []; + } + return Array.from(names) + .map( + ([_id, entry]) => + [entry.name.text, entry.type] as [string | undefined, Type], + ) + .filter( + (nameType): nameType is [string, Type] => + nameType[0] !== undefined, + ); + } + + /** + * Collects name-type tuples of all the entries with the given type defined within scope + * and its parent scopes. + */ + public getNamedEntriesRecursive( + ...kinds: NamedScopeItemKind[] + ): [string, Type][] { + const recursiveHelper = ( + kinds: NamedScopeItemKind[], + acc: [string, Type][], + scope?: Scope, + ): [string, Type][] => { + if (scope === undefined) { + return acc; + } + const entries = kinds.flatMap((kind) => + scope.getNamedEntries(kind), + ); + if (scope.isProgramScope()) { + return acc.concat(entries); + } else { + return recursiveHelper( + kinds, + acc.concat(entries), + scope.parentScope, + ); + } + }; + return recursiveHelper(kinds, [], this); + } + + /** + * Collects names of all the entries with the given type defined within this scope. + */ + public getNames(kind: NamedScopeItemKind, ty: Type): string[] { + return this.getNamedEntries(kind) + .filter(([_name, type]) => tyEq(type, ty)) + .map(([name, _type]) => name); + } + + /** + * Collects names of all the entries with the given type defined within scope + * and its parent scopes. + */ + public getNamesRecursive( + kind: NamedScopeItemKind, + ty: Type, + acc: string[] = [], + ): string[] { + const names = this.getNames(kind, ty); + if (this.isProgramScope()) { + return acc.concat(names); + } else { + return acc.concat( + this.parentScope!.getNamesRecursive(kind, ty, names), + ); + } + } + + /** + * Collects all names of all entities in the scope. + */ + public getAllNames(): string[] { + return Array.from(this.mapNamed.values()).flatMap((m) => + Array.from(m.values()).map((entity) => entity.name.text), + ); + } + + /** + * Collects all names of all entities in the scope and it all parent scopes. + */ + public getAllNamesRecursive(): string[] { + return this.getAllNames().concat( + this.parentScope?.getAllNamesRecursive() ?? [], + ); + } + + /** + * Returns all items of the given type defined within this scope. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public getItems(kind: ScopeItemKind): GenerativeEntity<any>[] { + //const result = isNamedScopeItemKind(kind) + // ? this.mapNamed.get(kind) + // : this.mapUnnamed.get(kind); + const result = this.mapNamed.get(kind); + return result === undefined ? [] : Array.from(result.values()); + } + + /** + * Returns all items of the given type defined within this scope and its parents. + */ + public getItemsRecursive( + kind: NamedScopeItemKind, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + acc?: GenerativeEntity<any>[], // eslint-disable-next-line @typescript-eslint/no-explicit-any + ): NamedGenerativeEntity<any>[]; + //public getItemsRecursive( + // kind: UnnamedScopeItemKind, + // // eslint-disable-next-line @typescript-eslint/no-explicit-any + // acc?: GenerativeEntity<any>[], // eslint-disable-next-line @typescript-eslint/no-explicit-any + //): GenerativeEntity<any>[]; + public getItemsRecursive( + kind: ScopeItemKind, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + acc: GenerativeEntity<any>[] = [], + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ): GenerativeEntity<any>[] { + const currentItems = this.getItems(kind); + const accN = acc.concat(currentItems); + if (!this.isProgramScope() && this.parentScope) + //return isNamedScopeItemKind(kind) + // ? this.parentScope.getItemsRecursive(kind, accN) + // : this.parentScope.getItemsRecursive(kind, accN); + return this.parentScope.getItemsRecursive(kind, accN); + else { + return accN; + } + } + + /** + * Recursively searches for functions or methods that return the specified type. + * @param kind The kind of callable to search for. + * @param returnTy The return type to match. + * @return An array of tuples containing the function/method names and their full signatures. + */ + public findFunction( + kind: "methodDecl" | "methodDef" | "functionDecl" | "functionDef", + returnTy: Type, + ): [string, Type][] { + const functions = this.getItemsRecursive(kind); + return Array.from(functions.values()).reduce<[string, Type][]>( + (acc, entry) => { + if ( + entry.type.kind === "function" && + getReturnType(entry.type) === returnTy + ) { + acc.push([entry.name.text, entry.type]); + } + return acc; + }, + [], + ); + } + + /** + * Checks if the given scope defines an identifier. + */ + public has(kind: NamedScopeItemKind, name: string): boolean { + return ( + this.mapNamed.has(kind) && + Array.from(this.mapNamed.get(kind)!).find( + ([_id, entry]) => entry.name.text === name, + ) !== undefined + ); + } + + /** + * Checks if the given scope or its parents define an identifier. + */ + public hasRecursive(kind: NamedScopeItemKind, name: string): boolean { + if (this.has(kind, name)) { + return true; + } else if (this.isProgramScope()) { + return false; + } else { + return this.parentScope!.hasRecursive(kind, name); + } + } + + /** + * Looks for a parent scope with one of the given kinds. + */ + public findParent(...kinds: ScopeKind[]): Scope | undefined { + if (kinds.find((kind) => this.kind === kind)) { + return this; + } else if (this.parentScope === undefined) { + return undefined; + } else if ( + kinds.find( + (kind) => this.parentScope && this.parentScope.kind === kind, + ) + ) { + return this.parentScope; + } else { + return this.parentScope.findParent(...kinds); + } + } + + /** + * Returns true if the given scope has one or more ancestors with the given kind. + */ + public hasParent(...kinds: ScopeKind[]): boolean { + return this.findParent(...kinds) !== undefined; + } + + /** + * Returns true if the given scope is defined inside one of the given kinds. + */ + public definedIn(...kinds: ScopeKind[]): boolean { + return kinds.find((k) => this.kind == k) !== undefined; + } +} diff --git a/src/test/fuzzer/src/stdlib.ts b/src/test/fuzzer/src/stdlib.ts new file mode 100644 index 0000000000..684c1317c6 --- /dev/null +++ b/src/test/fuzzer/src/stdlib.ts @@ -0,0 +1,35 @@ +import type * as Ast from "@/ast/ast"; +import * as path from "path"; +import { files } from "@/stdlib/stdlib"; +import { createVirtualFileSystem } from "@/vfs/createVirtualFileSystem"; +import { generateAstIdFromName } from "@/test/fuzzer/src/util"; +import { GlobalContext } from "@/test/fuzzer/src/context"; + +const StdlibFilePath = path.join( + __dirname, + "..", + "..", + "src", + "stdlib", + "stdlib", +); +const StdlibVFS = createVirtualFileSystem(StdlibFilePath, files); +export const StdlibPath = StdlibVFS.resolve("std/stdlib.fc"); +export const StdlibCode = StdlibVFS.readFile(StdlibPath).toString(); +// export const StdlibExPath = StdlibVFS.resolve("std/stdlib_ex.fc"); +// export const StdlibExCode = StdlibVFS.readFile(StdlibExPath).toString(); + +/** + * Returns traits defined in stdlib. + * TODO: We should parse its sources instead + */ +export function getStdlibTraits(): Ast.TypeDecl[] { + return [ + GlobalContext.makeF.makeDummyTrait( + generateAstIdFromName("BaseTrait"), + [], + [], + [], + ), + ]; +} diff --git a/src/test/fuzzer/src/types.ts b/src/test/fuzzer/src/types.ts new file mode 100644 index 0000000000..37ca2f5742 --- /dev/null +++ b/src/test/fuzzer/src/types.ts @@ -0,0 +1,447 @@ +import type * as Ast from "@/ast/ast"; +import { + createSample, + generateName, + randomInt, + randomBool, + generateAstIdFromName, + stringify, +} from "@/test/fuzzer/src/util"; +import type { Scope } from "@/test/fuzzer/src/scope"; +import type { TypeRef } from "@/types/types"; +import fc from "fast-check"; +import { GlobalContext } from "@/test/fuzzer/src/context"; + +/** + * Types from Tact stdlib. + */ +export enum StdlibType { + Int = "Int", + Bool = "Bool", + Builder = "Builder", + Slice = "Slice", + Cell = "Cell", + Address = "Address", + String = "String", + StringBuilder = "StringBuilder", +} + +/** User-defined maps. */ +export type MapType = { + key: Type; + value: Type; +}; + +/** A single struct or message field. */ +export type StructField = { + name: string; + type: Type; + default?: fc.Arbitrary<Ast.Expression>; +}; + +/** Utility types used internally in the generator. */ +export enum UtilType { + Contract = "Contract", + Trait = "Trait", + Program = "Program", + This = "This", + /** + * Functional Unit type that refers to the `void` type in languages like C++. + * Typically used when returning nothing from functions/methods or in imperative + * constructions which only mutate the state. + */ + Unit = "Unit", +} + +export type OptionalType = { + kind: "optional"; + type: Type; +}; + +/** + * Represents the signature of a function in a format typical for functional languages, such as Int -> Int -> Int. + * The last element of the list means the return type, previous elements are types of the arguments. + */ +export type FunctionType = { + kind: "function"; + signature: Type[]; +}; + +export type Type = + | { + kind: "stdlib"; + type: StdlibType; + } + | { + kind: "map"; + type: MapType; + } + | { + kind: "struct"; + name: string; + fields: StructField[]; + } + | { + kind: "message"; + name: string; + fields: StructField[]; + } + | { + kind: "util"; + type: UtilType; + } + | FunctionType + | OptionalType; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function throwTyError(ty: any): never { + throw new Error(`Unsupported type: ${stringify(ty, 0)}`); +} + +export function tyToString(ty: Type): string { + switch (ty.kind) { + case "stdlib": + return ty.type; + case "struct": + case "message": + return ty.name; + case "map": + return `map<${tyToString(ty.type.key)}, ${tyToString(ty.type.value)}>`; + case "optional": + return `${tyToString(ty.type)}?`; + default: + throwTyError(ty); + } +} + +export function tyEq(lhs: Type, rhs: Type): boolean { + return tyToString(lhs) === tyToString(rhs); +} + +/** + * A subset of supported Stdlib types that might be used in AST generation. + */ +export const SUPPORTED_STDLIB_TYPES: StdlibType[] = [ + StdlibType.String, + StdlibType.Bool, + StdlibType.Int, +]; + +function makePrimitiveType(name: string): Ast.PrimitiveTypeDecl { + return GlobalContext.makeF.makeDummyPrimitiveTypeDecl( + generateAstIdFromName(name), + ); +} +function makeASTTypeRef(name: string): Ast.TypeId { + return GlobalContext.makeF.makeDummyTypeId(name); +} + +function makeTypeRef(name: string): TypeRef { + return { + kind: "ref", + name, + optional: false, + }; +} + +/** + * Cache for Stdlib types. + */ +const StdlibTypeCache: Map<StdlibType, [Ast.TypeDecl, Ast.Type, TypeRef]> = + new Map(); +Object.values(StdlibType).forEach((ty) => { + StdlibTypeCache.set(ty, [ + transformTy<Ast.TypeDecl>(ty, makePrimitiveType), + transformTy<Ast.Type>(ty, makeASTTypeRef), + transformTy<TypeRef>(ty, makeTypeRef), + ]); +}); + +/** + * Creates a Tact type entry from the given tact-check type definition. + */ +function transformTy<T>(ty: StdlibType, transform: (type: StdlibType) => T): T { + if (!Object.values(StdlibType).includes(ty)) { + throwTyError(ty); + } + return transform(ty); +} +export function tyToAstTypeDecl(ty: Type): Ast.TypeDecl { + switch (ty.kind) { + case "stdlib": { + const result = StdlibTypeCache.get(ty.type); + if (!result) { + throwTyError(ty); + } + return result[0]; + } + default: + throwTyError(ty); + } +} +export function tyToAstType(ty: Type, isBounced = false): Ast.Type { + const generateAstTypeId = (text: string) => + GlobalContext.makeF.makeDummyTypeId(text); + + switch (ty.kind) { + case "stdlib": { + const result = StdlibTypeCache.get(ty.type); + if (!result) { + throwTyError(ty); + } + return result[1]; + } + case "struct": + case "message": { + const simpleType = GlobalContext.makeF.makeDummyTypeId(ty.name); + return isBounced + ? GlobalContext.makeF.makeDummyBouncedMessageType(simpleType) + : simpleType; + } + case "map": + return GlobalContext.makeF.makeDummyMapType( + generateAstTypeId(tyToString(ty.type.key)), + undefined, + generateAstTypeId(tyToString(ty.type.value)), + undefined, + ); + case "optional": + return GlobalContext.makeF.makeDummyOptionalType( + tyToAstType(ty.type, isBounced), + ); + default: + throwTyError(ty); + } +} + +export function tyToTypeRef(ty: Type): TypeRef { + switch (ty.kind) { + case "stdlib": { + const result = StdlibTypeCache.get(ty.type); + if (!result) { + throwTyError(ty); + } + return result[2]; + } + default: + throwTyError(ty); + } +} + +/** + * Retrieves a return type from the function type. + */ +export function getReturnType(ty: FunctionType): Type { + if (ty.signature.length === 0) { + throw new Error("Empty function signature"); + } + const result = ty.signature[ty.signature.length - 1]; + if (typeof result === "undefined") { + throw new Error("Unexpected 'undefined'"); + } + return result; +} + +/** + * Returns mock AST entries for types defined in standard library. + */ +export function getStdlibTypes(): Ast.TypeDecl[] { + return [...Object.values(StdlibType)].map((type) => + makePrimitiveType(type), + ); +} + +/** + * An utility class used to generate internal tact-check types. + */ +export class TypeGen { + private constructor(private scope: Scope) {} + + public static fromScope(scope: Scope): TypeGen { + return new TypeGen(scope); + } + + /** Arbitrary that generates stdlib types. */ + public stdlibArbitrary: fc.Arbitrary<Type> = fc.record({ + kind: fc.constant("stdlib"), + type: fc.constantFrom(...SUPPORTED_STDLIB_TYPES), + }); + + /** Arbitrary that generates map types. */ + public mapArbitrary: fc.Arbitrary<Type> = fc.record({ + kind: fc.constant("map"), + type: fc.record({ + key: fc.record({ + kind: fc.constant("stdlib"), + type: fc.constantFrom( + // TODO: Support Address + StdlibType.Int, + ), + }) as fc.Arbitrary<Type>, + value: fc.record({ + kind: fc.constant("stdlib"), + type: fc.constantFrom( + // TODO: Support Address, Cell, Struct, Message + StdlibType.Int, + StdlibType.Bool, + ), + }) as fc.Arbitrary<Type>, + }), + }); + + /** + * Picks an arbitrary type available within the scope. + * This doesn't generate new type definitions. + */ + public pick(): Type { + const arb = fc.oneof( + this.stdlibArbitrary, + // this.mapArbitrary, + // ...this.getStructs(), + ); + return createSample(arb); + } + + /** + * Generates any of the supported types. + */ + public generate(): fc.Arbitrary<Type> { + return fc.oneof( + this.stdlibArbitrary, + this.generateFun(), + this.generateStruct(randomBool()), + ); + } + + /** + * Generates an arbitrary function signature. + */ + public generateFun( + minLength = 1, + maxLength = 3, + ): fc.Arbitrary<FunctionType> { + const structs = this.getStructs(); + return fc.record<FunctionType>({ + kind: fc.constant("function"), + signature: fc.array( + fc.oneof(this.stdlibArbitrary, this.mapArbitrary, ...structs), + { + minLength, + maxLength, + }, + ), + }); + } + + /** + * Generates an arbitrary method signature that always starts with `this`. + */ + public generateMethod(): fc.Arbitrary<FunctionType> { + return this.generateFun().map((funType) => ({ + kind: "function", + signature: [ + { kind: "util", type: UtilType.This }, + ...funType.signature, + ], + })); + } + + /** + * Generates an arbitrary struct or message signature. + */ + public generateStruct(isMessage: boolean): fc.Arbitrary<Type> { + const structName = createSample( + generateName(this.scope, /*shadowing=*/ true, /*isType=*/ true), + ); + + // NOTE: It doesn't support nested structs/messages as they are not + const fields = fc + .array( + fc.record<StructField>({ + name: generateName(this.scope), + type: this.stdlibArbitrary, + default: fc.constantFrom(undefined), + }), + { minLength: 1, maxLength: 4 }, + ) + .filter((generatedFields) => + generatedFields.every( + (item, index) => + generatedFields.findIndex( + (other) => other.name === item.name, + ) === index, + ), + ); + if (isMessage) { + return fc.record<Type>({ + kind: fc.constant("message"), + name: fc.constant(structName), + fields: fields, + }) as fc.Arbitrary<Type>; + } else { + return fc.record<Type>({ + kind: fc.constant("struct"), + name: fc.constant(structName), + fields: fields, + }) as fc.Arbitrary<Type>; + } + } + + /** + * Returns arbitraries to generate structs available in the program scope. + */ + private getStructs(): fc.Arbitrary<Type>[] { + const structs = this.scope.getItemsRecursive("struct"); + if (structs.length === 0) { + return []; + } + return structs.map((s) => fc.constantFrom(s.type)); + } +} + +/** + * Creates an arbitrary function or method signature that returns the given type. + */ +export function makeFunctionTy( + kind: "function" | "method", + returnTy: Type, + minArgs = 1, + maxArgs = 3, +): FunctionType { + const argsLength = randomInt(minArgs, maxArgs); + const thisArg: Type[] = + kind === "method" ? [{ kind: "util", type: UtilType.This }] : []; + const args: Type[] = Array.from({ length: argsLength }, () => { + const idx = randomInt(0, SUPPORTED_STDLIB_TYPES.length - 1); + const selectedType = SUPPORTED_STDLIB_TYPES[idx]; + if (typeof selectedType === "undefined") { + throw new Error("Unexpected 'undefined'"); + } + return { kind: "stdlib", type: selectedType }; + }); + return { kind: "function", signature: [...thisArg, ...args, returnTy] }; +} + +export function isUnit(ty: Type): boolean { + return ty.kind === "util" && ty.type === "Unit"; +} + +export function isThis(ty: Type): boolean { + return ty.kind === "util" && ty.type === "This"; +} + +/** + * An heuristic that replicates the `resolvePartialFields` logic in the compiler in order to + * detect if the message ought to be wrapped in `bounced<>`. + */ +export function isBouncedMessage(ty: Type): boolean { + if (ty.kind !== "message") { + throwTyError(ty); + } + for (const f of ty.fields) { + if (!(f.type.kind === "stdlib" && f.type.type === StdlibType.Bool)) { + return true; // too big; must be wrapped + } + } + return false; +} diff --git a/src/test/fuzzer/src/util.ts b/src/test/fuzzer/src/util.ts new file mode 100644 index 0000000000..3b0f658813 --- /dev/null +++ b/src/test/fuzzer/src/util.ts @@ -0,0 +1,587 @@ +import os from "os"; +import { createNodeFileSystem } from "@/vfs/createNodeFileSystem"; +import type { VirtualFileSystem } from "@/vfs/VirtualFileSystem"; +import { mkdtemp } from "fs/promises"; +import * as fs from "fs"; +import * as path from "path"; +import fc from "fast-check"; + +import type { NamedScopeItemKind, Scope } from "@/test/fuzzer/src/scope"; +import { GlobalContext } from "@/test/fuzzer/src/context"; +import type { Type } from "@/test/fuzzer/src/types"; +import type * as Ast from "@/ast/ast"; +import { getSrcInfo } from "@/grammar/src-info"; +import type { FactoryAst } from "@/ast/ast-helpers"; +import { idText } from "@/ast/ast-helpers"; +import { CompilerContext } from "@/context/context"; +import { getParser } from "@/grammar"; +import { createVirtualFileSystem } from "@/vfs/createVirtualFileSystem"; +import { files } from "@/stdlib/stdlib"; +import { resolveImports } from "@/imports/resolveImports"; +import { getRawAST, openContext, parseModules } from "@/context/store"; +import type { MakeAstFactory } from "@/ast/generated/make-factory"; +import { precompile } from "@/pipeline/precompile"; +import { getAllTypes } from "@/types/resolveDescriptors"; +import { topSortContracts } from "@/pipeline/utils"; +import { featureEnable } from "@/config/features"; +import { posixNormalize } from "@/utils/filePath"; +import { funcCompile } from "@/func/funcCompile"; +import { Logger } from "@/context/logger"; +import { beginCell, Cell, contractAddress, TupleBuilder } from "@ton/core"; +import type { + StateInit, + Address, + Contract, + ContractProvider, + Sender, + TupleItem, +} from "@ton/core"; +import type { Blockchain, SandboxContract } from "@ton/sandbox"; +import { compileTact } from "@/pipeline/compile"; +import { enableFeatures, type BuildContext } from "@/pipeline/build"; + +export const VALID_ID = /^[a-zA-Z_]+[a-zA-Z_0-9]$/; +export const VALID_TYPE_ID = /^[A-Z]+[a-zA-Z_0-9]$/; + +/** + * Creates a temp node file system to use inside a property. + */ +export async function withNodeFS(f: (vfs: VirtualFileSystem) => Promise<void>) { + const tempDir = await mkdtemp( + path.join(GlobalContext.config.compileDir, "tact-check-"), + ); + const vfs = createNodeFileSystem(tempDir, false); + try { + await f(vfs); + } finally { + if (GlobalContext.config.compileDir == os.tmpdir()) { + await fs.promises.rm(tempDir, { recursive: true }); + } + } +} + +/** + * Creates a new property that executes additional logic implemented in tact-check. + */ +export function createProperty<Ts extends [unknown, ...unknown[]]>( + ...args: [ + ...arbitraries: { [K in keyof Ts]: fc.Arbitrary<Ts[K]> }, + predicate: (...args: Ts) => boolean | void, // eslint-disable-line @typescript-eslint/no-invalid-void-type + ] +): fc.IPropertyWithHooks<Ts> { + const arbitraries = args.slice(0, -1) as unknown as { + [K in keyof Ts]: fc.Arbitrary<Ts[K]>; + }; + const originalPredicate = args[args.length - 1] as ( + ...args: Ts + ) => boolean | void; // eslint-disable-line @typescript-eslint/no-invalid-void-type + // eslint-disable-next-line @typescript-eslint/no-invalid-void-type + const enhancedPredicate = (...args: Ts): boolean | void => { + args.forEach((arg) => { + GlobalContext.printSample(arg as Ast.AstNode); + }); + return originalPredicate(...args); + }; + return fc.property(...arbitraries, enhancedPredicate); +} + +/** + * Create parameters for custom property checking. + */ +function makeParams<T>( + counterexamplePrinter: (generated: T) => string, + numRuns: number | undefined, +): fc.Parameters<T> { + return { + numRuns: numRuns ?? GlobalContext.config.numRuns, + seed: GlobalContext.config.seed, + reporter(out) { + if (out.failed) { + let errorSufffix = ""; + if (out.counterexample !== null) { + errorSufffix = counterexamplePrinter(out.counterexample); + } + throw new Error(fc.defaultReportMessage(out) + errorSufffix); + } + }, + }; +} +/** + * Create parameters for custom property checking. + */ +function makeAsyncParams<T>( + counterexamplePrinter: (generated: T) => string, + numRuns: number | undefined, +): fc.Parameters<T> { + return { + numRuns: numRuns ?? GlobalContext.config.numRuns, + seed: GlobalContext.config.seed, + reporter: undefined, + async asyncReporter(out) { + if (out.failed) { + let errorSuffix = ""; + if (out.counterexample !== null) { + errorSuffix = counterexamplePrinter(out.counterexample); + } + throw new Error( + (await fc.asyncDefaultReportMessage(out)) + errorSuffix, + ); + } + }, + }; +} + +/** + * Checks the given property enhancing `fc.assert` with additional functionality. + */ +export function checkProperty<T>( + property: fc.IPropertyWithHooks<T>, + counterexamplePrinter: (generated: T) => string, + numRuns: number | undefined = undefined, +) { + fc.assert(property, makeParams(counterexamplePrinter, numRuns)); +} + +/** + * Checks the given async property enhancing `fc.assert` with additional functionality. + */ +export async function checkAsyncProperty<T>( + property: fc.IAsyncPropertyWithHooks<T>, + counterexamplePrinter: (generated: T) => string, + numRuns: number | undefined = undefined, +) { + await fc.assert(property, makeAsyncParams(counterexamplePrinter, numRuns)); +} + +export function astNodeCounterexamplePrinter( + generated: Ast.AstNode | Ast.AstNode[], +) { + const node = "kind" in generated ? generated : generated[0]!; + return `\n-----\nGenerated ${node.kind}:\n${GlobalContext.format(node)}\n-----\n`; +} + +/** + * Creates a single fast-check sample with respect to the current global configuration. + * @param gen The arbitrary generator used to create the sample. + * @throws If the arbitrary cannot generate any elements. + */ +export function createSample<T>(gen: fc.Arbitrary<T>): T { + const result = fc.sample(gen, { + seed: GlobalContext.config.seed, + numRuns: 1, + })[0]; + if (typeof result === "undefined") { + throw new Error("Unexpected 'undefined'"); + } + return result; +} + +/** + * Generates an array of items using the provided generator function, with a length determined by a sampled range. + * @param fn The generator function to create items. + * @param minLength The minimum length of the array. + * @param maxLength The maximum length of the array. + * @returns An array of generated items. + */ +export function createSamplesArray<T>( + fn: () => T, + minLength: number, + maxLength: number, +): T[] { + const length = createSample(fc.integer({ min: minLength, max: maxLength })); + return Array.from({ length }, () => fn()); +} + +/** + * Generates a new valid identifier with a name unique within the current scope and with unique id. + * @param shadowing Allow shadowing (using names available in parent scopes) + */ +export function generateName( + scope: Scope, + shadowing: boolean = true, + isType: boolean = false, +): fc.Arbitrary<string> { + const availableNames = shadowing + ? scope.getAllNames() + : scope.getAllNamesRecursive(); + + return fc + .stringMatching(isType ? VALID_TYPE_ID : VALID_ID) + .filter((generatedName) => { + if (availableNames.find(([name, _]) => name == generatedName)) { + return false; + } + return true; + }); +} + +/** + * Generates Ast.Id from string name and with new id. + */ +export function generateAstIdFromName(name: string): Ast.Id { + return GlobalContext.makeF.makeDummyId(name); +} + +/** + * Generates Ast.Id. + * @param scope Current scope, from which Ast.Id.text will be generated. + * @param shadowing Allow shadowing (using names available in parent scopes) + */ +export function generateAstId( + scope: Scope, + shadowing: boolean = true, + isType: boolean = false, +): fc.Arbitrary<Ast.Id> { + return generateName(scope, shadowing, isType).map((s) => + GlobalContext.makeF.makeDummyId(s), + ); +} + +/** + * Chooses an arbitrary identifier available in the current scope. + * @returns Chosen identifier or `undefined` if there are no identifiers available with the given kind/type. + */ +export function choose( + scope: Scope, + kind: NamedScopeItemKind, + ty: Type, +): string | undefined { + const availableNames = scope.getNamesRecursive(kind, ty); + if (availableNames.length === 0) { + return undefined; + } + return createSample(fc.constantFrom(...availableNames)); +} + +/** + * Randomly chooses a boolean value using wrt to SEED. + */ +export function randomBool(): boolean { + return createSample(fc.boolean()); +} + +/** + * Randomly chooses an integer value using wrt to SEED. + */ +export function randomInt(min: number, max: number): number { + return createSample(fc.integer({ min, max })); +} + +/** + * Chooses a random list element wrt to SEED. + */ +export function randomElement<T>(list: T[]): T { + if (list.length === 0) { + throw new Error("Empty list"); + } + if (list.length === 1) { + const result = list[0]; + if (typeof result === "undefined") { + throw new Error("Unexpected 'undefined'"); + } + return result; + } + const result = list[randomInt(1, list.length - 1)]; + if (typeof result === "undefined") { + throw new Error("Unexpected 'undefined'"); + } + return result; +} + +export function packArbitraries<T>( + arbs?: fc.Arbitrary<T>[], +): fc.Arbitrary<T[]> { + return arbs ? fc.tuple(...(arbs as [fc.Arbitrary<T>])) : fc.constant([]); +} + +export const dummySrcInfoPrintable = getSrcInfo(" ", 0, 0, null, "user"); + +export function stringify(obj: unknown, space: number): string { + return JSON.stringify( + obj, + (_, value) => (typeof value === "bigint" ? value.toString() : value), + space, + ); +} + +/*** + * Utility functions for compiling contracts and sandbox them + */ + +export function parseStandardLibrary(astF: FactoryAst): CompilerContext { + let ctx = new CompilerContext(); + const parser = getParser(astF); + const fileSystem = { + [`contracts/empty.tact`]: "", + }; + const project = createVirtualFileSystem("/", fileSystem, false); + const stdlib = createVirtualFileSystem("@stdlib", files); + + const imported = resolveImports({ + entrypoint: "contracts/empty.tact", + project, + stdlib, + parser, + }); + + // Add information about all the source code entries to the context + ctx = openContext( + ctx, + imported.tact, + imported.func, + parseModules(imported.tact, getParser(astF)), + ); + + return ctx; +} + +export function filterStdlib( + ctx: CompilerContext, + mF: MakeAstFactory, + names: Set<string>, +): CustomStdlib { + const result: Ast.ModuleItem[] = []; + + const rawAst = getRawAST(ctx); + + for (const c of rawAst.constants) { + if (names.has(idText(c.name))) { + result.push(c); + } + } + + for (const f of rawAst.functions) { + if (names.has(idText(f.name))) { + result.push(f); + } + } + + for (const t of rawAst.types) { + if (names.has(idText(t.name))) { + result.push(t); + } + } + + const customTactStdlib = mF.makeModule([], result); + //const stdlib_fc = fs + // .readFileSync(path.join(__dirname, "minimal-fc-stdlib", "stdlib.fc")) + // .toString("base64"); + + return { + modules: [customTactStdlib], + stdlib_fc: "", + stdlib_ex_fc: "", + }; +} + +export type CustomStdlib = { + // Parsed modules of Tact stdlib + modules: Ast.Module[]; + // Contents of the stdlib.fc file + stdlib_fc: string; + // Contents of the stdlib_ex.fc file + stdlib_ex_fc: string; +}; + +// If flag useCustomStdlib is false, it will parse the entire stdlib. Otherwise, +// it will use the provided data in CustomStdlib. +export async function buildModule( + astF: FactoryAst, + module: Ast.Module, + customStdlib: CustomStdlib, + blockchain: Blockchain, +): Promise<Map<string, SandboxContract<ProxyContract>>> { + // We need an entrypoint for precompile, even if it is empty + const fileSystem = { + [`contracts/empty.tact`]: "", + }; + const minimalStdlib = { + // Needed by precompile, but we set its contents to be empty + ["std/stdlib.tact"]: "", + // These two func files are needed during tvm compilation + ["stdlib_ex.fc"]: customStdlib.stdlib_ex_fc, + ["stdlib.fc"]: customStdlib.stdlib_fc, + }; + + const project = createVirtualFileSystem("/", fileSystem, false); + const stdlib = createVirtualFileSystem("@stdlib", minimalStdlib); + + const config = { + name: "test", + path: "contracts/empty.tact", + output: ".", + options: { + debug: true, + external: true, + ipfsAbiGetter: false, + interfacesGetter: false, + safety: { + nullChecks: true, + }, + }, + }; + + const contractsToTest: Map< + string, + SandboxContract<ProxyContract> + > = new Map(); + + const logger = new Logger(); + + let ctx = new CompilerContext(); + ctx = enableFeatures(ctx, logger, config); + + ctx = precompile(ctx, project, stdlib, config.path, [ + module, + ...customStdlib.modules, + ]); + + const built: Record< + string, + | { + codeBoc: Buffer; + abi: string; + } + | undefined + > = {}; + + const compilerInfo: string = JSON.stringify({ + entrypoint: posixNormalize(config.path), + options: config.options, + }); + + const bCtx: BuildContext = { + config, + logger, + project, + stdlib, + compilerInfo, + ctx, + built: {}, + errorMessages: [], + }; + + const allContracts = getAllTypes(ctx).filter((v) => v.kind === "contract"); + + // Sort contracts in topological order + // If a cycle is found, return undefined + const sortedContracts = topSortContracts(allContracts); + if (sortedContracts !== undefined) { + ctx = featureEnable(ctx, "optimizedChildCode"); + } + for (const contract of sortedContracts ?? allContracts) { + const contractName = contract.name; + + // Compiling contract to func + const res = await compileTact(bCtx, contractName); + if (!res) { + throw new Error( + "Tact compilation failed. " + + bCtx.errorMessages.map((error) => error.message).join("\n"), + ); + } + const codeEntrypoint = res.entrypointPath; + + // Compiling contract to TVM + const stdlibPath = stdlib.resolve("stdlib.fc"); + //const stdlibCode = stdlib.readFile(stdlibPath).toString(); + const stdlibExPath = stdlib.resolve("stdlib_ex.fc"); + //const stdlibExCode = stdlib.readFile(stdlibExPath).toString(); + + process.env.USE_NATIVE = "true"; + process.env.FC_STDLIB_PATH = path.join( + __dirname, + "/minimal-fc-stdlib/", + ); + process.env.FUNC_FIFT_COMPILER_PATH = path.join( + __dirname, + "/minimal-fc-stdlib/funcplusfift", + ); + process.env.FIFT_LIBS_PATH = path.join( + __dirname, + "/minimal-fc-stdlib/fift-lib/", + ); + + const contractFilePath = path.join( + __dirname, + "/minimal-fc-stdlib/", + codeEntrypoint, + ); + + fs.writeFileSync(contractFilePath, res.funcSource.content); + + const c = await funcCompile({ + entries: [stdlibPath, stdlibExPath, contractFilePath], + sources: [], + logger, + }); + + if (!c.ok) { + throw new Error(c.log); + } + + // Add to built map + built[contractName] = { + codeBoc: c.output, + abi: "", + }; + + contractsToTest.set( + contractName, + blockchain.openContract( + new ProxyContract(getContractStateInit(c.output)), + ), + ); + } + + return contractsToTest; +} + +function getContractStateInit(contractCode: Buffer): StateInit { + const data = beginCell().storeUint(0, 1).endCell(); + const code = Cell.fromBoc(contractCode)[0]; + if (typeof code === "undefined") { + throw new Error("Code cell expected"); + } + return { code, data }; +} + +export class ProxyContract implements Contract { + address: Address; + init: StateInit; + + constructor(stateInit: StateInit) { + this.address = contractAddress(0, stateInit); + this.init = stateInit; + } + + async send( + provider: ContractProvider, + via: Sender, + args: { value: bigint; bounce?: boolean | null | undefined }, + body?: Cell, + ) { + await provider.internal(via, { ...args, body: body }); + } + + async getInt(provider: ContractProvider) { + const builder = new TupleBuilder(); + const result = (await provider.get("getInt", builder.build())).stack; + return result.readBigNumber(); + } + + async getIndexed(provider: ContractProvider, index: number) { + const builder = new TupleBuilder(); + const result = (await provider.get(`getInt${index}`, builder.build())) + .stack; + return result.readBigNumber(); + } + + async getGeneric( + provider: ContractProvider, + getterName: string, + params: TupleItem[], + ) { + return (await provider.get(getterName, params)).stack; + } +} diff --git a/src/test/fuzzer/test/compilation.spec.ts b/src/test/fuzzer/test/compilation.spec.ts new file mode 100644 index 0000000000..974f26a7ba --- /dev/null +++ b/src/test/fuzzer/test/compilation.spec.ts @@ -0,0 +1,137 @@ +import { funcCompile } from "@/func/funcCompile"; +import { posixNormalize } from "@/utils/filePath"; +import type * as Ast from "@/ast/ast"; +import { writeFileSync } from "fs"; +import * as path from "path"; +import fc from "fast-check"; + +import { Program } from "@/test/fuzzer/src/generators"; +import { StdlibCode, StdlibPath } from "@/test/fuzzer/src/stdlib"; +import { + withNodeFS, + checkAsyncProperty, + astNodeCounterexamplePrinter, +} from "@/test/fuzzer/src/util"; +import { + compile, + precompile, + createContext, + enableFeatures, +} from "@/test/fuzzer/test/testUtils"; +import { GlobalContext } from "@/test/fuzzer/src/context"; +import { getAstFactory } from "@/ast/ast-helpers"; + +function getContract(program: Ast.Module): Ast.Contract | undefined { + for (const entry of program.items) { + if (entry.kind === "contract") { + return entry; + } + } + return undefined; +} + +async function compileProgram(program: Ast.Module) { + throw new Error("Deprecated function"); + // await withNodeFS(async (vfs) => { + // const factoryAst = getAstFactory(); + // let ctx = createContext(program); + // ctx = enableFeatures(ctx, "external"); + // ctx = precompile(ctx, factoryAst); + // const compilationOutput = vfs.root; + + // const contract = getContract(program)!; + + // // Save the generated contract to a file + // const contractCode = GlobalContext.format(contract, "ast"); + // writeFileSync( + // path.join(compilationOutput, "contract.tact"), + // contractCode, + // ); + + // // Compile contracts to FunC + // const res = await compile(ctx, contract.name.text); + // for (const files of res.output.files) { + // const ffc = vfs.resolve(compilationOutput, files.name); + // vfs.writeFile(ffc, files.code); + // } + + // // Process compilation output + // const codeFc = res.output.files.map((v) => ({ + // path: posixNormalize(vfs.resolve(compilationOutput, v.name)), + // content: v.code, + // })); + // const codeEntrypoint = res.output.entrypoint; + + // // Compile the resulted FunC code + // // NOTE: We intentionally disabled stdlibEx, since the generated + // // contracts currently don't use it. + // const c = await funcCompile({ + // entries: [ + // StdlibPath, + // // stdlibExPath, + // posixNormalize(vfs.resolve(compilationOutput, codeEntrypoint)), + // ], + // sources: [ + // { path: StdlibPath, content: StdlibCode }, + // // { + // // path: stdlibExPath, + // // content: stdlibExCode, + // // }, + // ...codeFc, + // ], + // logger: { + // info: (_) => {}, + // debug: (_) => {}, + // warn: (_) => {}, + // error: (_) => {}, + // }, + // }); + // try { + // expect(c.ok).toBeTruthy(); + // } catch (_error) { + // throw new Error(`FunC compilation failed:\n${c.log}`); + // } + + // GlobalContext.resetDepth(); + // }); +} + +describe("properties", () => { + it( + "compiles contracts", + async () => { + // The generated AST is compiled once on compilation tests. + // This approach is used to speed-up testing, since non-structural changes + // are not significant for this case. + // + // Instead, the original NUM_RUNS option is used to generate the requested number of + // programs with a different structure. + const numRuns = GlobalContext.config.numRuns; + + const compileAndCheckProperty = async () => { + const property = fc.asyncProperty( + new Program({ addStdlib: true }).generate(), + compileProgram, + ); + await checkAsyncProperty( + property, + astNodeCounterexamplePrinter, + /*numRuns=*/ 1, + ); + }; + + if (numRuns === Infinity) { + for (;;) { + await compileAndCheckProperty(); + } + } else { + await Promise.all( + Array.from({ length: numRuns }).map(async () => { + await compileAndCheckProperty(); + }), + ); + } + }, + /*timeout_ms=*/ 60 * 60 * 1000 /*1hr*/, + ); +}); diff --git a/src/test/fuzzer/test/expressions/expression-stats.ts b/src/test/fuzzer/test/expressions/expression-stats.ts new file mode 100644 index 0000000000..58c62b9b93 --- /dev/null +++ b/src/test/fuzzer/test/expressions/expression-stats.ts @@ -0,0 +1,272 @@ +import type * as Ast from "@/ast/ast"; +import * as fc from "fast-check"; +import * as fs from "fs"; +import { Scope } from "@/test/fuzzer/src/scope"; +import { SUPPORTED_STDLIB_TYPES } from "@/test/fuzzer/src/types"; +import type { Type } from "@/test/fuzzer/src/types"; +import { Expression } from "@/test/fuzzer/src/generators"; +import path from "path"; + +/** + * AST utility functions + */ + +function getHeight(tree: Ast.Expression): number { + switch (tree.kind) { + case "address": + case "boolean": + case "cell": + case "id": + case "null": + case "number": + case "string": + case "slice": + case "code_of": + return 0; + case "init_of": { + const children = tree.args.map((arg) => getHeight(arg)); + return children.length === 0 ? 0 : Math.max(...children) + 1; + } + case "field_access": + return getHeight(tree.aggregate) + 1; + case "conditional": + return ( + Math.max( + getHeight(tree.condition), + getHeight(tree.thenBranch), + getHeight(tree.elseBranch), + ) + 1 + ); + case "method_call": + return ( + Math.max( + getHeight(tree.self), + ...tree.args.map((arg) => getHeight(arg)), + ) + 1 + ); + case "static_call": { + const children = tree.args.map((arg) => getHeight(arg)); + return children.length === 0 ? 0 : Math.max(...children) + 1; + } + case "struct_instance": { + const children = tree.args.map((init) => + getHeight(init.initializer), + ); + return children.length === 0 ? 0 : Math.max(...children) + 1; + } + case "struct_value": { + const children = tree.args.map((init) => + getHeight(init.initializer), + ); + return children.length === 0 ? 0 : Math.max(...children) + 1; + } + case "op_unary": + return getHeight(tree.operand) + 1; + case "op_binary": + return Math.max(getHeight(tree.left), getHeight(tree.right)) + 1; + } +} + +function countNodes(tree: Ast.Expression): number { + switch (tree.kind) { + case "address": + case "boolean": + case "cell": + case "id": + case "null": + case "number": + case "string": + case "slice": + case "code_of": + return 1; + case "init_of": + return sum(tree.args.map((arg) => countNodes(arg))) + 1; + case "field_access": + return countNodes(tree.aggregate) + 1; + case "conditional": + return ( + countNodes(tree.condition) + + countNodes(tree.thenBranch) + + countNodes(tree.elseBranch) + + 1 + ); + case "method_call": + return ( + countNodes(tree.self) + + sum(tree.args.map((arg) => countNodes(arg))) + + 1 + ); + case "static_call": + return sum(tree.args.map((arg) => countNodes(arg))) + 1; + case "struct_instance": + return ( + sum(tree.args.map((init) => countNodes(init.initializer))) + 1 + ); + case "struct_value": + return ( + sum(tree.args.map((init) => countNodes(init.initializer))) + 1 + ); + case "op_unary": + return countNodes(tree.operand) + 1; + case "op_binary": + return countNodes(tree.left) + countNodes(tree.right) + 1; + } +} + +function sum(items: number[]): number { + return items.reduce((prev, curr) => prev + curr, 0); +} + +function preorderTraversal(tree: Ast.Expression, accumulator: string[]) { + switch (tree.kind) { + case "address": + case "boolean": + case "cell": + case "id": + case "null": + case "number": + case "string": + case "slice": + case "code_of": { + accumulator.push(tree.kind); + break; + } + case "init_of": { + accumulator.push(tree.kind + "_" + tree.contract.text); + tree.args.forEach((arg) => { + preorderTraversal(arg, accumulator); + }); + break; + } + case "field_access": { + accumulator.push(tree.kind); + preorderTraversal(tree.aggregate, accumulator); + break; + } + case "conditional": { + accumulator.push(tree.kind); + preorderTraversal(tree.condition, accumulator); + preorderTraversal(tree.thenBranch, accumulator); + preorderTraversal(tree.elseBranch, accumulator); + break; + } + case "method_call": { + accumulator.push(tree.kind + "_" + tree.method.text); + preorderTraversal(tree.self, accumulator); + tree.args.forEach((arg) => { + preorderTraversal(arg, accumulator); + }); + break; + } + case "static_call": { + accumulator.push(tree.kind + "_" + tree.function.text); + tree.args.forEach((arg) => { + preorderTraversal(arg, accumulator); + }); + break; + } + case "struct_instance": { + accumulator.push( + tree.kind + "_" + tree.args.length + "_" + tree.type.text, + ); + tree.args.forEach((arg) => { + preorderTraversal(arg.initializer, accumulator); + }); + break; + } + case "struct_value": { + accumulator.push( + tree.kind + "_" + tree.args.length + "_" + tree.type.text, + ); + tree.args.forEach((arg) => { + preorderTraversal(arg.initializer, accumulator); + }); + break; + } + case "op_unary": { + accumulator.push(tree.kind + "_" + tree.op); + preorderTraversal(tree.operand, accumulator); + break; + } + case "op_binary": { + accumulator.push(tree.kind + "_" + tree.op); + preorderTraversal(tree.left, accumulator); + preorderTraversal(tree.right, accumulator); + break; + } + } +} + +function incrementKey<K>( + key: K, + map: Map<K, TreeStats>, + height: number, + size: number, +) { + const value = map.get(key); + if (value) { + const newVal = { + count: value.count + 1, + height: value.height, + size: value.size, + }; + map.set(key, newVal); + } else { + map.set(key, { + count: 1, + height, + size, + }); + } +} + +type TreeStats = { + count: number; + height: number; + size: number; +}; + +function getRows(dist: Map<string, TreeStats>): string[] { + const rows: string[] = []; + for (const [tree, stats] of dist) { + rows.push(`${tree} ${stats.count} ${stats.height} ${stats.size}`); + } + return rows; +} + +export function statistics( + gen: fc.Arbitrary<Ast.Expression>, + numberOfSamples: number, + fileName: string, +) { + const trees = fc.sample(gen, numberOfSamples); + + const totalPreTraversals: Map<string, TreeStats> = new Map(); + + for (const tree of trees) { + const preTraversal: string[] = []; + preorderTraversal(tree, preTraversal); + const treeName = preTraversal.join("@"); + const height = getHeight(tree); + const size = countNodes(tree); + incrementKey(treeName, totalPreTraversals, height, size); + } + + fs.writeFileSync( + fileName, + `tree count height size\n${getRows(totalPreTraversals).join("\n")}`, + ); +} + +function main() { + const globalScope = new Scope("program", undefined); + const generator = fc + .constantFrom(...SUPPORTED_STDLIB_TYPES) + .chain((type) => { + const ty: Type = { kind: "stdlib", type }; + return new Expression(globalScope, ty).generate(); + }); + statistics(generator, 50000, path.join(__dirname, "counts.txt")); +} + +main(); diff --git a/src/test/fuzzer/test/expressions/expression.spec.ts b/src/test/fuzzer/test/expressions/expression.spec.ts new file mode 100644 index 0000000000..8a5aabc444 --- /dev/null +++ b/src/test/fuzzer/test/expressions/expression.spec.ts @@ -0,0 +1,268 @@ +import type * as Ast from "@/ast/ast"; +import { CompilerContext } from "@/context/context"; +import { resolveExpression, getExpType } from "@/types/resolveExpression"; +import type { StatementContext } from "@/types/resolveStatements"; +import type { TypeRef } from "@/types/types"; +import assert from "assert"; + +import { Expression } from "@/test/fuzzer/src/generators"; +import { Scope } from "@/test/fuzzer/src/scope"; +import { StdlibType, SUPPORTED_STDLIB_TYPES } from "@/test/fuzzer/src/types"; +import type { Type } from "@/test/fuzzer/src/types"; +import { + createProperty, + checkProperty, + dummySrcInfoPrintable, + checkAsyncProperty, + astNodeCounterexamplePrinter, + packArbitraries, +} from "@/test/fuzzer/src/util"; +import fc from "fast-check"; +import { + initializeGenerator, + NonTerminal, + Terminal, +} from "../../src/generators/uniform-expr-gen"; +import type { NonTerminalEnum } from "../../src/generators/uniform-expr-gen"; +import { + bindingsAndExpressionPrtinter, + compileExpression, + interpretExpression, + saveExpressionTest, + setupEnvironment, +} from "./utils"; +import type { ExpressionTestingEnvironment } from "./utils"; +import { Let } from "@/test/fuzzer/src/generators/statement"; +import { GlobalContext } from "@/test/fuzzer/src/context"; + +function emptyStatementContext(): StatementContext { + return { + root: dummySrcInfoPrintable, + returns: { kind: "void" }, + vars: new Map<string, TypeRef>(), + requiredFields: [], + funName: null, + }; +} + +function setupContexts(): [CompilerContext, StatementContext] { + const ctx: CompilerContext = new CompilerContext(); + const sctx = emptyStatementContext(); + return [ctx, sctx]; +} + +// describe("generation properties", () => { +// it("generates well-typed expressions", () => { +// const results = setupContexts(); +// let compilerCtx = results[0]; +// const stmtCtx = results[1]; +// const globalScope = new Scope("program", undefined); +// for (const type of SUPPORTED_STDLIB_TYPES) { +// const ty: Type = { kind: "stdlib", type }; +// // NOTE: This test checks only pure expressions, without introducing new +// // entries to any scopes. +// const exprGen = new Expression(globalScope, ty).generate(); +// const property = createProperty(exprGen, (expr) => { +// compilerCtx = resolveExpression(expr, stmtCtx, compilerCtx); +// const resolvedTy = getExpType(compilerCtx, expr); +// if (resolvedTy.kind == "ref") { +// assert.strictEqual( +// resolvedTy.name, +// ty.type, +// `The resolved type ${resolvedTy.name} does not match the expected type ${ty.type}`, +// ); +// } else { +// assert.fail(`Unexpected type: ${resolvedTy.kind}`); +// } +// }); +// checkProperty(property, astNodeCounterexamplePrinter); +// } +// }); +// }); + +describe("evaluation properties", () => { + let expressionTestingEnvironment: ExpressionTestingEnvironment; + + beforeAll(async () => { + expressionTestingEnvironment = await setupEnvironment(); + }); + + afterAll(() => { + expressionTestingEnvironment.outputStream.close(); + }); + + test( + "compiler and interpreter evaluate generated expressions equally", + async () => { + /*const expressionGenerationIds: Map<AllowedTypeEnum, string[]> = + new Map(); + expressionGenerationIds.set(AllowedType.Int, ["int1"]); + expressionGenerationIds.set(AllowedType.OptInt, ["int_null"]); + expressionGenerationIds.set(AllowedType.Bool, ["bool1"]); + expressionGenerationIds.set(AllowedType.OptBool, ["bool_null"]); + expressionGenerationIds.set(AllowedType.Cell, ["cell1"]); + expressionGenerationIds.set(AllowedType.OptCell, ["cell_null"]); + expressionGenerationIds.set(AllowedType.Slice, ["slice1"]); + expressionGenerationIds.set(AllowedType.OptSlice, ["slice_null"]); + expressionGenerationIds.set(AllowedType.Address, ["address1"]); + expressionGenerationIds.set(AllowedType.OptAddress, [ + "address_null", + ]); + expressionGenerationIds.set(AllowedType.String, ["string1"]); + expressionGenerationIds.set(AllowedType.OptString, ["string_null"]); + */ + + const globalScope = new Scope("block", undefined); + const initializerCtx = { + minSize: 1, + maxSize: 1, + useIdentifiers: false, + allowedNonTerminals: Object.values(NonTerminal), + allowedTerminals: Object.values(Terminal), + }; + const bindingsGenerator = initializeGenerator(initializerCtx); + + const expressionGenerationCtx = { + minSize: 1, + maxSize: 10, + useIdentifiers: true, + allowedNonTerminals: Object.values(NonTerminal), + allowedTerminals: Object.values(Terminal), + }; + const exprGenerator = new Expression( + globalScope, + { kind: "stdlib", type: StdlibType.Int }, + expressionGenerationCtx, + ); + + const property = fc.asyncProperty( + generateBindings(globalScope, bindingsGenerator), + exprGenerator.generate(), + async (bindings, expr) => { + const compilationResult = await compileExpression( + expressionTestingEnvironment, + bindings, + expr, + ); + + const interpretationResult = interpretExpression( + expressionTestingEnvironment, + bindings, + expr, + ); + if ( + (compilationResult instanceof Error && + interpretationResult instanceof BigInt) || + (interpretationResult instanceof Error && + compilationResult instanceof BigInt) + ) { + expect(compilationResult).toEqual(interpretationResult); + } else if ( + compilationResult instanceof Error && + interpretationResult instanceof Error + ) { + saveExpressionTest( + bindings, + expr, + compilationResult, + interpretationResult, + expressionTestingEnvironment.outputStream, + ); + } else { + expect(compilationResult).toBe(interpretationResult); + } + }, + ); + await checkAsyncProperty(property, bindingsAndExpressionPrtinter); + }, + 60 * 1000, // 1 minute + ); +}); + +function generateBindings( + scope: Scope, + bindingsGenerator: ( + scope: Scope, + nonTerminal: NonTerminalEnum, + ) => fc.Arbitrary<Ast.Expression>, +): fc.Arbitrary<Ast.StatementLet[]> { + // For each of the types, we create a let generator + const types: Type[] = [ + { kind: "stdlib", type: StdlibType.Int }, + { kind: "stdlib", type: StdlibType.Bool }, + { kind: "stdlib", type: StdlibType.Address }, + { kind: "stdlib", type: StdlibType.Cell }, + { kind: "stdlib", type: StdlibType.Slice }, + { kind: "stdlib", type: StdlibType.String }, + { kind: "optional", type: { kind: "stdlib", type: StdlibType.Int } }, + { kind: "optional", type: { kind: "stdlib", type: StdlibType.Bool } }, + { + kind: "optional", + type: { kind: "stdlib", type: StdlibType.Address }, + }, + { kind: "optional", type: { kind: "stdlib", type: StdlibType.Cell } }, + { kind: "optional", type: { kind: "stdlib", type: StdlibType.Slice } }, + { kind: "optional", type: { kind: "stdlib", type: StdlibType.String } }, + ]; + const result: fc.Arbitrary<Ast.StatementLet>[] = []; + + for (const ty of types) { + const genBuilder = new Let( + scope, + ty, + bindingsGenerator(scope, typeToNonTerminal(ty)), + ); + scope.addNamed("let", genBuilder); + result.push(genBuilder.generate() as fc.Arbitrary<Ast.StatementLet>); + + if (ty.kind === "optional") { + const genBuilder = new Let( + scope, + ty, + fc.constant(GlobalContext.makeF.makeDummyNull()), + ); + scope.addNamed("let", genBuilder); + result.push( + genBuilder.generate() as fc.Arbitrary<Ast.StatementLet>, + ); + } + } + + return packArbitraries(result); +} + +function typeToNonTerminal(ty: Type): NonTerminalEnum { + switch (ty.kind) { + case "optional": { + // Treat them as if they were non-optionals + return typeToNonTerminal(ty.type); + } + case "stdlib": { + switch (ty.type) { + case StdlibType.Int: + return NonTerminal.Int; + case StdlibType.Address: + return NonTerminal.Address; + case StdlibType.Bool: + return NonTerminal.Bool; + case StdlibType.Cell: + return NonTerminal.Cell; + case StdlibType.Slice: + return NonTerminal.Slice; + case StdlibType.String: + return NonTerminal.String; + case StdlibType.Builder: + throw new Error("Not supported"); + case StdlibType.StringBuilder: + throw new Error("Not supported"); + } + break; + } + case "function": + case "map": + case "message": + case "struct": + case "util": + throw new Error("Not supported."); + } +} diff --git a/src/test/fuzzer/test/expressions/utils.ts b/src/test/fuzzer/test/expressions/utils.ts new file mode 100644 index 0000000000..b867af71a4 --- /dev/null +++ b/src/test/fuzzer/test/expressions/utils.ts @@ -0,0 +1,268 @@ +import type * as Ast from "@/ast/ast"; +import type { MakeAstFactory } from "@/ast/generated/make-factory"; +import { NonTerminal } from "../../src/generators/uniform-expr-gen"; +import { GlobalContext } from "../../src/context"; +import { Interpreter } from "@/optimizer/interpreter"; +import { CompilerContext } from "@/context/context"; +import type { AstUtil } from "@/ast/util"; +import { getAstUtil } from "@/ast/util"; +import type { FactoryAst } from "@/ast/ast-helpers"; +import type { CustomStdlib } from "../../src/util"; +import { + buildModule, + filterStdlib, + parseStandardLibrary, +} from "../../src/util"; +import { Blockchain } from "@ton/sandbox"; +import type { Sender } from "@ton/core"; +import { toNano } from "@ton/core"; +import * as fs from "node:fs"; + +export function bindingsAndExpressionPrtinter([bindings, expr]: [ + Ast.StatementLet[], + Ast.Expression, +]) { + return ( + `\n-----\nGenerated bindings:\n` + + bindings.map((bind) => GlobalContext.format(bind)).join("\n") + + `\nGenerated expression:\n` + + GlobalContext.format(expr) + + `\n-----\n` + ); +} + +export function makeExpressionGetter( + makeF: MakeAstFactory, + getterName: string, + bindings: Ast.Statement[], + returnExpr: Ast.Expression, +): Ast.FunctionDef { + return makeF.makeDummyFunctionDef( + [makeF.makeDummyFunctionAttributeGet(undefined)], + makeF.makeDummyId(getterName), + makeF.makeDummyTypeId("Int"), + [], + bindings.concat([makeF.makeDummyStatementReturn(returnExpr)]), + ); +} + +export function createModuleWithExpressionGetter( + makeF: MakeAstFactory, + contractName: string, + getterName: string, + bindings: Ast.Statement[], + returnExpr: Ast.Expression, +): Ast.Module { + // throw new Error("createContract is not implemented yet"); // TODO: implement, probably should place this function in a different file + return makeF.makeModule( + [], + [ + makeF.makeDummyContract( + makeF.makeDummyId(contractName), + [], + [], + [], + [makeExpressionGetter(makeF, getterName, bindings, returnExpr)], + ), + ], + ); +} + +export const initializersMapping = { + Int: NonTerminal.LiteralInt, + "Int?": NonTerminal.LiteralInt, + Bool: NonTerminal.LiteralBool, + "Bool?": NonTerminal.LiteralBool, + Cell: NonTerminal.LiteralCell, + "Cell?": NonTerminal.LiteralCell, + Address: NonTerminal.LiteralAddress, + "Address?": NonTerminal.LiteralAddress, + Slice: NonTerminal.LiteralSlice, + "Slice?": NonTerminal.LiteralSlice, + String: NonTerminal.LiteralString, + "String?": NonTerminal.LiteralString, +} as const; + +export type ExpressionTestingEnvironment = { + astF: FactoryAst; + makeF: MakeAstFactory; + customStdlib: CustomStdlib; + blockchain: Blockchain; + emptyCompilerContext: CompilerContext; + astUtil: AstUtil; + sender: Sender; + contractNameToCompile: string; + outputStream: fs.WriteStream; +}; + +export async function setupEnvironment(): Promise<ExpressionTestingEnvironment> { + const astF = GlobalContext.astF; + const makeF = GlobalContext.makeF; + const customStdlib = filterStdlib( + parseStandardLibrary(astF), + makeF, + new Set([ + "Int", + "Bool", + "Address", + "Cell", + "Context", + "Slice", + "Slice?", + "String", + "String?", + "StateInit", + "SendParameters", + "BaseTrait", + "SendDefaultMode", + "SendRemainingValue", + "SendIgnoreErrors", + "SendRemainingBalance", + "ReserveExact", + "sender", + "context", + "myBalance", + "nativeReserve", + "toString", + "StringBuilder", + //"contractAddress", + //"contractAddressExt", + //"storeUint", + //"storeInt", + //"contractHash", + //"newAddress", + //"beginCell", + //"endCell", + "send", + //"asSlice", + //"asAddressUnsafe", + //"beginParse", + ]), + ); + const blockchain = await Blockchain.create(); + const astUtil = getAstUtil(astF); + const sender = (await blockchain.treasury("treasury")).getSender(); + return { + astF, + makeF, + customStdlib, + blockchain, + astUtil, + sender, + emptyCompilerContext: new CompilerContext(), + contractNameToCompile: "ExpressionContract", + outputStream: fs.createWriteStream("interesting-failing-tests.txt", { + flags: "a", + }), + }; +} + +export function interpretExpression( + { astUtil, emptyCompilerContext }: ExpressionTestingEnvironment, + bindings: Ast.StatementLet[], + expr: Ast.Expression, +): bigint | Error { + try { + const interpreter = new Interpreter(astUtil, emptyCompilerContext); + bindings.forEach((bind) => { + interpreter.interpretStatement(bind); + }); + const result = interpreter.interpretExpression(expr); + expect(result.kind).toBe("number"); + return (result as Ast.Number).value; + } catch (e: any) { + return e as Error; + } +} + +export async function compileExpression( + { + makeF, + contractNameToCompile, + astF, + customStdlib, + blockchain, + sender, + }: ExpressionTestingEnvironment, + bindings: Ast.StatementLet[], + expr: Ast.Expression, +): Promise<bigint | Error> { + const contractModule = createModuleWithExpressionGetter( + makeF, + contractNameToCompile, + "getInt", + bindings, + expr, + ); + + try { + const contractMapPromise = buildModule( + astF, + contractModule, + customStdlib, + blockchain, + ); + + const contractMap = await contractMapPromise; + const contract = contractMap.get(contractNameToCompile)!; + await contract.send(sender, { value: toNano(1) }); + return await contract.getInt(); + } catch (e: any) { + return e; + } +} + +/* +export function generateBindings( + expressionTestingEnvironment: ExpressionTestingEnvironment, + expressionGenerationIds: Map<AllowedTypeEnum, string[]>, + generator: (nonTerminalId: NonTerminalEnum) => fc.Arbitrary<Ast.Expression>, +): fc.Arbitrary<Ast.StatementLet[]> { + return fc.tuple( + ...expressionGenerationIds + .entries() + .flatMap(([type, names]) => + names.map((name) => + type.slice(-1) === "?" + ? fc.constant( + expressionTestingEnvironment.makeF.makeDummyStatementLet( + expressionTestingEnvironment.makeF.makeDummyId( + name, + ), + expressionTestingEnvironment.makeF.makeDummyOptionalType( + expressionTestingEnvironment.makeF.makeDummyTypeId( + type.slice(0, -1), + ), + ), + expressionTestingEnvironment.makeF.makeDummyNull(), + ), + ) + : generator(initializersMapping[type]).map((expr) => + expressionTestingEnvironment.makeF.makeDummyStatementLet( + expressionTestingEnvironment.makeF.makeDummyId( + name, + ), + expressionTestingEnvironment.makeF.makeDummyTypeId( + type, + ), + expr, + ), + ), + ), + ), + ); +} +*/ + +export function saveExpressionTest( + bindings: Ast.StatementLet[], + expr: Ast.Expression, + compilationResult: Error, + interpretationResult: Error, + outputStream: fs.WriteStream, +): void { + outputStream.write( + bindingsAndExpressionPrtinter([bindings, expr]) + + `\nCompilation error: ${compilationResult}\nInterpretation error: ${interpretationResult}\n`, + ); +} diff --git a/src/test/fuzzer/test/program.spec.ts b/src/test/fuzzer/test/program.spec.ts new file mode 100644 index 0000000000..c2b996ab16 --- /dev/null +++ b/src/test/fuzzer/test/program.spec.ts @@ -0,0 +1,69 @@ +import { Program } from "@/test/fuzzer/src/generators"; +import assert from "assert"; + +import { + precompile, + createContext, + enableFeatures, +} from "@/test/fuzzer/test/testUtils"; +import { + createProperty, + checkProperty, + astNodeCounterexamplePrinter, +} from "@/test/fuzzer/src/util"; +import { GlobalContext } from "@/test/fuzzer/src/context"; +import { getAstFactory } from "@/ast/ast-helpers"; + +describe("properties", () => { + it("generates well-typed programs", () => { + const property = createProperty( + new Program({ addStdlib: true }).generate(), + (program) => { + const factoryAst = getAstFactory(); + let ctx = createContext(program); + ctx = enableFeatures(ctx, "external"); + precompile(ctx, factoryAst); + GlobalContext.resetDepth(); + }, + ); + checkProperty(property, astNodeCounterexamplePrinter); + }); + + it("generates reproducible AST", () => { + // Setting a fixed seed for reproducibility + const originalSeed = GlobalContext.config.seed; + GlobalContext.config.seed = 42; + + let program1: string | undefined; + let program2: string | undefined; + + // Create a single property that generates two programs + const property = createProperty(new Program().generate(), (program) => { + if (program1 === undefined) { + program1 = GlobalContext.format(program, "ast"); + } else { + program2 = GlobalContext.format(program, "ast"); + } + GlobalContext.resetDepth(); + }); + + // Execute property twice + checkProperty(property, astNodeCounterexamplePrinter, /*numRuns=*/ 1); + checkProperty(property, astNodeCounterexamplePrinter, /*numRuns=*/ 1); + + assert.notEqual( + program1, + undefined, + "First program should not be undefined", + ); + assert.notEqual( + program2, + undefined, + "Second program should not be undefined", + ); + assert.equal(program1, program2, "Both programs should be identical"); + + // Restore the original seed + GlobalContext.config.seed = originalSeed; + }); +}); diff --git a/src/test/fuzzer/test/testUtils.ts b/src/test/fuzzer/test/testUtils.ts new file mode 100644 index 0000000000..86e0d94fa1 --- /dev/null +++ b/src/test/fuzzer/test/testUtils.ts @@ -0,0 +1,68 @@ +import { CompilerContext } from "@/context/context"; +import { createABI } from "@/generator/createABI"; +import { writeProgram } from "@/generator/writeProgram"; +import type * as Ast from "@/ast/ast"; +import { openContext } from "@/context/store"; +import { resolveAllocations } from "@/storage/resolveAllocation"; +import { featureEnable } from "@/config/features"; +import { resolveDescriptors } from "@/types/resolveDescriptors"; +import { resolveSignatures } from "@/types/resolveSignatures"; +import { resolveStatements } from "@/types/resolveStatements"; +import { evalComptimeExpressions } from "@/types/evalComptimeExpressions"; +import { resolveErrors } from "@/types/resolveErrors"; +import type { FactoryAst } from "@/ast/ast-helpers"; + +export function createContext(program: Ast.Module): CompilerContext { + let ctx = new CompilerContext(); + ctx = openContext( + ctx, + /*sources=*/ [], + /*funcSources=*/ [], + //getParser(factoryAst), + [program], + ); + return ctx; +} + +/** + * Replicates the `precompile` pipeline. + */ +export function precompile( + ctx: CompilerContext, + factoryAst: FactoryAst, +): CompilerContext { + ctx = resolveDescriptors(ctx, factoryAst); + ctx = resolveStatements(ctx); + evalComptimeExpressions(ctx, factoryAst); + ctx = resolveSignatures(ctx, factoryAst); + ctx = resolveErrors(ctx, factoryAst); + ctx = resolveAllocations(ctx); + return ctx; +} + +/** + * Enables compiler's features. + */ +export function enableFeatures( + ctx: CompilerContext, + ...features: ["inline" | "debug" | "masterchain" | "external"] +): CompilerContext { + return features.reduce((accCtx, feature) => { + return featureEnable(accCtx, feature); + }, ctx); +} + +/** + * Replicates the `compile` pipeline. + */ +export async function compile(ctx: CompilerContext, contractName: string) { + const abi = createABI(ctx, contractName); + const output = await writeProgram( + ctx, + abi, + `tact_check_${contractName}`, + {}, //ContractCodes + false, + ); + return { output, ctx }; +}